Step 245: HeadlessEditorState — agent API surface without ImGui/SDL
Adds the headless agent architecture for Sprint 9 Phase 9a: - ASTUtils.h: pure AST utilities extracted from EditorUtils.h - HeadlessEditorState.h: GUI-free state with buffer management - HeadlessAgentRPCHandler.h: full RPC dispatch (20+ methods) - step245_test.cpp: 20 tests all passing Also fixes test compilation errors (NotificationSystem API changes, AgentRole permissions, DependencyPanel missing include) and adds SDL2 system library detection fix in CMakeLists.txt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
77
editor/src/ASTUtils.h
Normal file
77
editor/src/ASTUtils.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
// ASTUtils.h — Pure AST utility functions (no ImGui/SDL dependency).
|
||||
// Extracted for use by both EditorUtils.h and HeadlessEditorState.h.
|
||||
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// Clone a Module tree (deep copy via JSON roundtrip)
|
||||
static inline std::unique_ptr<Module> cloneModule(const Module* ast) {
|
||||
if (!ast) return nullptr;
|
||||
json j = toJson(ast);
|
||||
ASTNode* node = fromJson(j);
|
||||
if (!node || node->conceptType != "Module") return nullptr;
|
||||
return std::unique_ptr<Module>(static_cast<Module*>(node));
|
||||
}
|
||||
|
||||
// Check if a node is an annotation type
|
||||
static inline bool isAnnotationNode(const ASTNode* node) {
|
||||
if (!node) return false;
|
||||
if (node->conceptType.find("Annotation") != std::string::npos) return true;
|
||||
if (node->conceptType == "DerefStrategy") return true;
|
||||
if (node->conceptType == "OptimizationLock") return true;
|
||||
if (node->conceptType == "LangSpecific") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count annotation nodes in an AST subtree
|
||||
static inline int countAnnotationNodes(const ASTNode* node) {
|
||||
if (!node) return 0;
|
||||
int count = isAnnotationNode(node) ? 1 : 0;
|
||||
for (const auto* child : node->allChildren())
|
||||
count += countAnnotationNodes(child);
|
||||
return count;
|
||||
}
|
||||
|
||||
// Find a node by ID in an AST tree
|
||||
static inline ASTNode* findNodeById(ASTNode* node,
|
||||
const std::string& id) {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == id) return node;
|
||||
for (auto* child : node->allChildren())
|
||||
if (auto* found = findNodeById(child, id)) return found;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if a span contains a position
|
||||
static inline bool spanContains(const ASTNode* node, int line, int col) {
|
||||
if (!node || !node->hasSpan()) return false;
|
||||
if (line < node->spanStartLine || line > node->spanEndLine) return false;
|
||||
if (line == node->spanStartLine && col < node->spanStartCol) return false;
|
||||
if (line == node->spanEndLine && col > node->spanEndCol) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Span score (smaller span = better match)
|
||||
static inline int spanScore(const ASTNode* node) {
|
||||
if (!node || !node->hasSpan()) return 999999;
|
||||
return (node->spanEndLine - node->spanStartLine) * 1000 +
|
||||
(node->spanEndCol - node->spanStartCol);
|
||||
}
|
||||
|
||||
// Find a node at a line/col position
|
||||
static inline ASTNode* findNodeAtPosition(ASTNode* node,
|
||||
int line, int col) {
|
||||
if (!node) return nullptr;
|
||||
ASTNode* best = nullptr;
|
||||
if (spanContains(node, line, col)) best = node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
ASTNode* cand = findNodeAtPosition(child, line, col);
|
||||
if (cand) {
|
||||
if (!best || spanScore(cand) < spanScore(best)) best = cand;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
558
editor/src/HeadlessAgentRPCHandler.h
Normal file
558
editor/src/HeadlessAgentRPCHandler.h
Normal file
@@ -0,0 +1,558 @@
|
||||
#pragma once
|
||||
// Step 245: Headless Agent RPC Handler
|
||||
//
|
||||
// Mirrors AgentRPCHandler.h but operates on HeadlessEditorState.
|
||||
// Uses the same agentRpcError/agentRpcResult helpers and supports
|
||||
// the same JSON-RPC methods for the agent-critical path:
|
||||
// getAST, parseSource, generateFromAST, runPipeline, projectLanguage,
|
||||
// applyMutation, applyBatch, getInScopeSymbols, getCallHierarchy,
|
||||
// getDependencyGraph, generateCode, setAgentRole,
|
||||
// getAnnotationSuggestions, applyAnnotationSuggestion,
|
||||
// recordAnnotationFeedback, startWorkflowRecording,
|
||||
// stopWorkflowRecording, getWorkflowRecording, replayWorkflow, ping
|
||||
//
|
||||
// Deliberately omits GUI-only methods. Stays under 600 lines.
|
||||
|
||||
struct HeadlessEditorState;
|
||||
|
||||
// Re-use the response helpers (same signature as AgentRPCHandler.h)
|
||||
static inline json headlessRpcError(const json& id, int code,
|
||||
const std::string& msg) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"error", {{"code", code}, {"message", msg}}}};
|
||||
}
|
||||
|
||||
static inline json headlessRpcResult(const json& id, const json& result) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id}, {"result", result}};
|
||||
}
|
||||
|
||||
static inline json headlessRequireAST(HeadlessEditorState& state,
|
||||
const json& id) {
|
||||
if (!state.active() || !state.isStructured())
|
||||
return headlessRpcError(id, -32000, "No structured buffer");
|
||||
if (!state.activeAST())
|
||||
return headlessRpcError(id, -32001, "AST unavailable");
|
||||
return json();
|
||||
}
|
||||
|
||||
static inline json headlessRequireMutable(HeadlessEditorState& state,
|
||||
const json& id) {
|
||||
if (!state.active() || !state.isStructured())
|
||||
return headlessRpcError(id, -32000, "No structured buffer");
|
||||
if (!state.mutationAST())
|
||||
return headlessRpcError(id, -32001, "AST unavailable");
|
||||
return json();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main dispatch
|
||||
// -----------------------------------------------------------------------
|
||||
inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
const json& request,
|
||||
const std::string& sessionId) {
|
||||
json id = request.contains("id") ? request["id"] : json(nullptr);
|
||||
std::string method = request.value("method", "");
|
||||
AgentRole role = state.getAgentRole(sessionId);
|
||||
|
||||
// --- ping ---
|
||||
if (method == "ping")
|
||||
return headlessRpcResult(id, "pong");
|
||||
|
||||
// --- getAST ---
|
||||
if (method == "getAST") {
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
return headlessRpcResult(id, {
|
||||
{"ast", toJson(state.activeAST())},
|
||||
{"annotationCount", countAnnotationNodes(state.activeAST())},
|
||||
{"diagnostics", state.buildDiagnosticsJson()}
|
||||
});
|
||||
}
|
||||
|
||||
// --- parseSource ---
|
||||
if (method == "parseSource") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string source = params.value("source", "");
|
||||
std::string language = params.value("language", "");
|
||||
if (source.empty() || language.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing source or language");
|
||||
Pipeline pipeline;
|
||||
std::vector<ParseDiagnostic> diags;
|
||||
auto mod = pipeline.parse(source, language, diags);
|
||||
json diagArr = json::array();
|
||||
for (const auto& d : diags)
|
||||
diagArr.push_back({{"line", d.line}, {"column", d.column},
|
||||
{"message", d.message},
|
||||
{"severity", d.severity}});
|
||||
json result = {{"diagnostics", diagArr}};
|
||||
if (mod) result["ast"] = toJson(mod.get());
|
||||
return headlessRpcResult(id, result);
|
||||
}
|
||||
|
||||
// --- generateFromAST ---
|
||||
if (method == "generateFromAST") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string language = params.value("language",
|
||||
state.active()->language);
|
||||
Pipeline pipeline;
|
||||
std::string code = pipeline.generate(state.activeAST(), language);
|
||||
return headlessRpcResult(id, {{"code", code},
|
||||
{"language", language}});
|
||||
}
|
||||
|
||||
// --- runPipeline ---
|
||||
if (method == "runPipeline") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string source = params.value("source", "");
|
||||
std::string srcLang = params.value("sourceLanguage", "");
|
||||
std::string tgtLang = params.value("targetLanguage", "");
|
||||
if (source.empty() || srcLang.empty() || tgtLang.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing source, sourceLanguage, or targetLanguage");
|
||||
Pipeline pipeline;
|
||||
auto pr = pipeline.run(source, srcLang, tgtLang);
|
||||
json diagArr = json::array();
|
||||
for (const auto& d : pr.parseDiags)
|
||||
diagArr.push_back({{"line", d.line}, {"column", d.column},
|
||||
{"message", d.message},
|
||||
{"severity", d.severity}});
|
||||
json valArr = json::array();
|
||||
for (const auto& d : pr.validationDiags)
|
||||
valArr.push_back({{"severity", d.severity},
|
||||
{"message", d.message},
|
||||
{"nodeId", d.nodeId}});
|
||||
json violArr = json::array();
|
||||
for (const auto& v : pr.violations)
|
||||
violArr.push_back({{"severity", v.severity},
|
||||
{"category", v.category},
|
||||
{"message", v.message},
|
||||
{"nodeId", v.nodeId}});
|
||||
json suggArr = json::array();
|
||||
for (const auto& s : pr.suggestions)
|
||||
suggArr.push_back({{"nodeId", s.nodeId},
|
||||
{"annotationType", s.annotationType},
|
||||
{"strategy", s.strategy},
|
||||
{"reason", s.reason},
|
||||
{"confidence", s.confidence}});
|
||||
json result = {
|
||||
{"success", pr.success}, {"generatedCode", pr.generatedCode},
|
||||
{"parseDiagnostics", diagArr},
|
||||
{"validationDiagnostics", valArr},
|
||||
{"violations", violArr}, {"suggestions", suggArr},
|
||||
{"foldCount", pr.foldResult.nodesModified},
|
||||
{"dceCount", pr.dceResult.nodesModified}
|
||||
};
|
||||
if (pr.ast) result["ast"] = toJson(pr.ast.get());
|
||||
return headlessRpcResult(id, result);
|
||||
}
|
||||
|
||||
// --- projectLanguage ---
|
||||
if (method == "projectLanguage") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string targetLanguage = params.value("targetLanguage", "");
|
||||
if (targetLanguage.empty())
|
||||
return headlessRpcError(id, -32602, "Missing targetLanguage");
|
||||
CrossLanguageProjector projector;
|
||||
auto projected = projector.project(state.activeAST(),
|
||||
targetLanguage);
|
||||
if (!projected)
|
||||
return headlessRpcError(id, -32020, "Projection failed");
|
||||
Pipeline pipeline;
|
||||
std::string code = pipeline.generate(projected.get(),
|
||||
targetLanguage);
|
||||
return headlessRpcResult(id, {
|
||||
{"ast", toJson(projected.get())}, {"generatedCode", code},
|
||||
{"sourceLanguage", state.active()->language},
|
||||
{"targetLanguage", targetLanguage}
|
||||
});
|
||||
}
|
||||
|
||||
// --- setAgentRole ---
|
||||
if (method == "setAgentRole") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string roleName = params.value("role", "linter");
|
||||
AgentRole newRole =
|
||||
AgentPermissionPolicy::roleFromString(roleName);
|
||||
state.setAgentRole(sessionId, newRole);
|
||||
return headlessRpcResult(id,
|
||||
{{"role", AgentPermissionPolicy::roleLabel(newRole)}});
|
||||
}
|
||||
|
||||
// --- generateCode ---
|
||||
if (method == "generateCode") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string spec = params.value("spec", "");
|
||||
bool preferImports = params.value("preferImports", true);
|
||||
state.library.primitives.setRoot(state.activeAST());
|
||||
state.library.primitives.setLanguage(state.active()->language);
|
||||
AgentCodeGen gen;
|
||||
state.library.primitives.setContextTags(
|
||||
state.library.semanticTags.inferTagsFromText(spec));
|
||||
AgentCodeGenResult genRes = gen.generate(
|
||||
spec, state.library.primitives,
|
||||
state.active()->language, preferImports);
|
||||
if (!genRes.node)
|
||||
return headlessRpcError(id, -32020, "Code generation failed");
|
||||
json nodeJson = toJson(genRes.node);
|
||||
deleteTree(genRes.node);
|
||||
return headlessRpcResult(id, {
|
||||
{"node", nodeJson}, {"note", genRes.note},
|
||||
{"usedSymbols", genRes.usedSymbols},
|
||||
{"language", state.active()->language}
|
||||
});
|
||||
}
|
||||
|
||||
// --- applyMutation ---
|
||||
if (method == "applyMutation") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.active() || !state.isStructured())
|
||||
return headlessRpcError(id, -32000, "No structured buffer");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string type = params.value("type", "");
|
||||
bool preferImports = params.value("preferImports", false);
|
||||
bool strictMode = params.value("strictMode", false);
|
||||
Module* ast = state.mutationAST();
|
||||
if (!ast) return headlessRpcError(id, -32001, "AST unavailable");
|
||||
ASTMutationAPI mut;
|
||||
mut.setRoot(ast);
|
||||
ASTMutationAPI::MutationResult res;
|
||||
LibraryPolicyResult policy;
|
||||
std::vector<std::string> affectedIds;
|
||||
if (type == "setProperty") {
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.setProperty(params.value("nodeId", ""),
|
||||
params.value("property", ""),
|
||||
params.value("value", ""));
|
||||
} else if (type == "updateNode") {
|
||||
std::map<std::string, std::string> props;
|
||||
if (params.contains("properties")) {
|
||||
for (auto& [k, v] : params["properties"].items())
|
||||
props[k] = v.get<std::string>();
|
||||
}
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.updateNode(params.value("nodeId", ""), props);
|
||||
} else if (type == "deleteNode") {
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.deleteNode(params.value("nodeId", ""));
|
||||
} else if (type == "insertNode") {
|
||||
ASTNode* node = nullptr;
|
||||
std::string insertedId;
|
||||
if (params.contains("node")) {
|
||||
node = fromJson(params["node"]);
|
||||
if (node) insertedId = node->id;
|
||||
}
|
||||
if (node) {
|
||||
policy = checkMutationLibraryPolicy(
|
||||
node, ast, preferImports, strictMode);
|
||||
if (!policy.ok) {
|
||||
deleteTree(node);
|
||||
return headlessRpcError(id, -32011, policy.error);
|
||||
}
|
||||
}
|
||||
res = mut.insertNode(params.value("parentId", ""),
|
||||
params.value("role", ""), node);
|
||||
if (!res.success && node) {
|
||||
deleteTree(node);
|
||||
} else if (res.success && !insertedId.empty()) {
|
||||
affectedIds.push_back(insertedId);
|
||||
}
|
||||
} else {
|
||||
return headlessRpcError(id, -32602, "Unknown mutation type");
|
||||
}
|
||||
if (!res.success)
|
||||
return headlessRpcError(id, -32010, res.error);
|
||||
state.applyOrchestratorToActive();
|
||||
if (state.active()) {
|
||||
state.active()->incrementalOptimizer.setRoot(
|
||||
state.active()->sync.getAST());
|
||||
state.active()->incrementalOptimizer.recordExternalTransform(
|
||||
"agent-mutation:" + type, affectedIds,
|
||||
state.agentActorLabel(sessionId));
|
||||
}
|
||||
return headlessRpcResult(id, {
|
||||
{"success", true}, {"warning", res.warning},
|
||||
{"libraryWarning", policy.warning},
|
||||
{"unknownFunctions", policy.unknownFunctions}
|
||||
});
|
||||
}
|
||||
|
||||
// --- applyBatch ---
|
||||
if (method == "applyBatch") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto errChk = headlessRequireMutable(state, id);
|
||||
if (!errChk.is_null()) return errChk;
|
||||
Module* ast = state.mutationAST();
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
if (!params.contains("mutations") ||
|
||||
!params["mutations"].is_array())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing mutations array");
|
||||
BatchMutationAPI batch;
|
||||
batch.setRoot(ast);
|
||||
std::vector<BatchMutationAPI::Mutation> mutations;
|
||||
std::vector<ASTNode*> ownedNodes;
|
||||
for (const auto& m : params["mutations"]) {
|
||||
BatchMutationAPI::Mutation mut;
|
||||
mut.type = m.value("type", "");
|
||||
mut.nodeId = m.value("nodeId", "");
|
||||
mut.property = m.value("property", "");
|
||||
mut.value = m.value("value", "");
|
||||
mut.parentId = m.value("parentId", "");
|
||||
mut.role = m.value("role", "");
|
||||
if (m.contains("node")) {
|
||||
mut.newNode = fromJson(m["node"]);
|
||||
if (mut.newNode) ownedNodes.push_back(mut.newNode);
|
||||
}
|
||||
mutations.push_back(mut);
|
||||
}
|
||||
auto batchRes = batch.applySequence(mutations);
|
||||
if (!batchRes.success) {
|
||||
for (auto* n : ownedNodes) {
|
||||
if (n->parent == nullptr) deleteTree(n);
|
||||
}
|
||||
return headlessRpcError(id, -32010, batchRes.error);
|
||||
}
|
||||
state.applyOrchestratorToActive();
|
||||
if (state.active()) {
|
||||
state.active()->incrementalOptimizer.setRoot(
|
||||
state.active()->sync.getAST());
|
||||
state.active()->incrementalOptimizer.recordExternalTransform(
|
||||
"agent-batch", {},
|
||||
state.agentActorLabel(sessionId));
|
||||
}
|
||||
return headlessRpcResult(id,
|
||||
{{"success", true}, {"appliedCount", batchRes.appliedCount}});
|
||||
}
|
||||
|
||||
// --- getInScopeSymbols ---
|
||||
if (method == "getInScopeSymbols") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (nodeId.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing nodeId parameter");
|
||||
ContextAPI ctx;
|
||||
ctx.setRoot(state.activeAST());
|
||||
auto symbols = ctx.getInScopeSymbols(nodeId);
|
||||
json arr = json::array();
|
||||
for (const auto& s : symbols)
|
||||
arr.push_back({{"name", s.name}, {"kind", s.kind},
|
||||
{"nodeId", s.nodeId}});
|
||||
return headlessRpcResult(id, {{"symbols", arr}});
|
||||
}
|
||||
|
||||
// --- getCallHierarchy ---
|
||||
if (method == "getCallHierarchy") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string functionId = params.value("functionId", "");
|
||||
if (functionId.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing functionId parameter");
|
||||
ContextAPI ctx;
|
||||
ctx.setRoot(state.activeAST());
|
||||
auto info = ctx.getCallHierarchy(functionId);
|
||||
return headlessRpcResult(id, {
|
||||
{"functionId", info.functionId},
|
||||
{"functionName", info.functionName},
|
||||
{"callerIds", info.callerIds},
|
||||
{"calleeIds", info.calleeIds}
|
||||
});
|
||||
}
|
||||
|
||||
// --- getDependencyGraph ---
|
||||
if (method == "getDependencyGraph") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (nodeId.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing nodeId parameter");
|
||||
ContextAPI ctx;
|
||||
ctx.setRoot(state.activeAST());
|
||||
auto deps = ctx.getDependencyGraph(nodeId);
|
||||
return headlessRpcResult(id, {{"dependencies", deps}});
|
||||
}
|
||||
|
||||
// --- getAnnotationSuggestions ---
|
||||
if (method == "getAnnotationSuggestions") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
Module* ast = state.activeAST();
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) {
|
||||
if (!findNodeById(ast, nodeId))
|
||||
return headlessRpcError(id, -32002,
|
||||
"Node not found: " + nodeId);
|
||||
}
|
||||
AgentAnnotationAssistant assistant;
|
||||
auto result = assistant.suggest(ast, nodeId);
|
||||
json suggArr = json::array();
|
||||
for (const auto& s : result.suggestions)
|
||||
suggArr.push_back({
|
||||
{"nodeId", s.nodeId},
|
||||
{"annotationType", s.annotationType},
|
||||
{"strategy", s.strategy}, {"reason", s.reason},
|
||||
{"confidence", s.confidence}
|
||||
});
|
||||
json diagArr = json::array();
|
||||
for (const auto& d : result.diagnostics)
|
||||
diagArr.push_back({{"severity", d.severity},
|
||||
{"message", d.message},
|
||||
{"nodeId", d.nodeId}});
|
||||
return headlessRpcResult(id, {{"scopeId", result.scopeNodeId},
|
||||
{"suggestions", suggArr},
|
||||
{"diagnostics", diagArr}});
|
||||
}
|
||||
|
||||
// --- applyAnnotationSuggestion ---
|
||||
if (method == "applyAnnotationSuggestion") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireMutable(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
Module* ast = state.mutationAST();
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
MemoryStrategyInference::Suggestion suggestion;
|
||||
suggestion.nodeId = params.value("nodeId", "");
|
||||
suggestion.annotationType = params.value("annotationType", "");
|
||||
suggestion.strategy = params.value("strategy", "");
|
||||
suggestion.reason = params.value("reason", "");
|
||||
suggestion.confidence = params.value("confidence", 0.0);
|
||||
AgentAnnotationAssistant assistant;
|
||||
auto res = assistant.applySuggestion(ast, suggestion);
|
||||
if (!res.success)
|
||||
return headlessRpcError(id, -32010, res.error);
|
||||
if (params.contains("accepted")) {
|
||||
bool accepted = params.value("accepted", true);
|
||||
assistant.recordFeedback(suggestion, accepted);
|
||||
}
|
||||
state.applyOrchestratorToActive();
|
||||
return headlessRpcResult(id,
|
||||
{{"success", true}, {"warning", res.warning}});
|
||||
}
|
||||
|
||||
// --- recordAnnotationFeedback ---
|
||||
if (method == "recordAnnotationFeedback") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
MemoryStrategyInference::Suggestion suggestion;
|
||||
suggestion.nodeId = params.value("nodeId", "");
|
||||
suggestion.annotationType = params.value("annotationType", "");
|
||||
suggestion.strategy = params.value("strategy", "");
|
||||
suggestion.reason = params.value("reason", "");
|
||||
suggestion.confidence = params.value("confidence", 0.0);
|
||||
bool accepted = params.value("accepted", false);
|
||||
AgentAnnotationAssistant assistant;
|
||||
assistant.recordFeedback(suggestion, accepted);
|
||||
return headlessRpcResult(id, true);
|
||||
}
|
||||
|
||||
// --- startWorkflowRecording ---
|
||||
if (method == "startWorkflowRecording") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string name = params.value("name", "workflow");
|
||||
state.agent.workflowRecorder.startRecording(
|
||||
name, sessionId, WorkflowRecorder::RecordingConfig{},
|
||||
state.buildSessionMetadata(false));
|
||||
return headlessRpcResult(id,
|
||||
{{"recording", true}, {"name", name}});
|
||||
}
|
||||
|
||||
// --- stopWorkflowRecording ---
|
||||
if (method == "stopWorkflowRecording")
|
||||
return headlessRpcResult(id,
|
||||
state.agent.workflowRecorder.stopRecording());
|
||||
|
||||
// --- getWorkflowRecording ---
|
||||
if (method == "getWorkflowRecording")
|
||||
return headlessRpcResult(id,
|
||||
state.agent.workflowRecorder.exportWorkflow());
|
||||
|
||||
// --- replayWorkflow ---
|
||||
if (method == "replayWorkflow") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
if (!params.contains("workflow"))
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing workflow payload");
|
||||
WorkflowRecorder temp;
|
||||
if (!temp.loadWorkflow(params["workflow"]))
|
||||
return headlessRpcError(id, -32602,
|
||||
"Invalid workflow payload");
|
||||
auto requests = temp.buildReplayRequests();
|
||||
json results = json::array();
|
||||
state.agent.workflowRecorder.setReplaying(true);
|
||||
for (auto& req : requests)
|
||||
results.push_back(
|
||||
handleHeadlessAgentRequest(state, req, sessionId));
|
||||
state.agent.workflowRecorder.setReplaying(false);
|
||||
return headlessRpcResult(id,
|
||||
{{"count", results.size()}, {"responses", results}});
|
||||
}
|
||||
|
||||
// --- getSessionInfo ---
|
||||
if (method == "getSessionInfo") {
|
||||
return headlessRpcResult(id, {
|
||||
{"mode", "headless"},
|
||||
{"workspace", state.workspaceRoot},
|
||||
{"language", state.defaultLanguage},
|
||||
{"bufferCount", (int)state.bufferStates.size()},
|
||||
{"activeBuffer", state.active() ? state.active()->path : ""},
|
||||
{"role", AgentPermissionPolicy::roleLabel(role)}
|
||||
});
|
||||
}
|
||||
|
||||
return headlessRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
239
editor/src/HeadlessEditorState.h
Normal file
239
editor/src/HeadlessEditorState.h
Normal file
@@ -0,0 +1,239 @@
|
||||
#pragma once
|
||||
// Step 245: Headless EditorState — agent API surface without ImGui/SDL.
|
||||
//
|
||||
// Provides the same method interface that AgentRPCHandler.h expects,
|
||||
// but with zero GUI dependencies. Uses std::chrono for timestamps,
|
||||
// stderr for logging, and holds its own lightweight buffer map.
|
||||
|
||||
// --- Core AST and pipeline (no ImGui) ---
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Generator.h"
|
||||
#include "ast/Annotation.h"
|
||||
#include "ASTUtils.h"
|
||||
#include "Pipeline.h"
|
||||
#include "ASTMutationAPI.h"
|
||||
#include "BatchMutationAPI.h"
|
||||
#include "ContextAPI.h"
|
||||
#include "CrossLanguageProjector.h"
|
||||
#include "MemoryStrategyInference.h"
|
||||
#include "AgentAnnotationAssistant.h"
|
||||
#include "AgentPermissionPolicy.h"
|
||||
#include "AgentCodeGen.h"
|
||||
#include "AgentLibraryPolicy.h"
|
||||
#include "WorkflowRecorder.h"
|
||||
#include "Orchestrator.h"
|
||||
#include "IncrementalOptimizer.h"
|
||||
#include "TextASTSync.h"
|
||||
#include "BufferManager.h"
|
||||
#include "EditorModePolicy.h"
|
||||
#include "PrimitivesRegistry.h"
|
||||
#include "SemanticTags.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Headless agent state — no WebSocket, no AgentMarketplace (ImGui dep)
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessAgentState {
|
||||
std::map<std::string, AgentRole> roles;
|
||||
AgentRole defaultRole = AgentRole::Linter;
|
||||
WorkflowRecorder workflowRecorder;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Headless library state — no ImGui panels
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessLibraryState {
|
||||
PrimitivesRegistry primitives;
|
||||
SemanticTags semanticTags;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HeadlessBufferState — lightweight buffer without ImGui widgets
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessBufferState {
|
||||
TextASTSync sync;
|
||||
Orchestrator orchestrator;
|
||||
IncrementalOptimizer incrementalOptimizer;
|
||||
std::string language = "python";
|
||||
std::string path = "(untitled)";
|
||||
std::string editBuf;
|
||||
BufferManager::BufferMode bufferMode =
|
||||
BufferManager::BufferMode::Structured;
|
||||
bool orchestratorDirty = true;
|
||||
bool modified = false;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HeadlessEditorState — agent-facing state, no GUI
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessEditorState {
|
||||
std::map<std::string, std::unique_ptr<HeadlessBufferState>>
|
||||
bufferStates;
|
||||
HeadlessBufferState* activeBuffer = nullptr;
|
||||
HeadlessAgentState agent;
|
||||
HeadlessLibraryState library;
|
||||
std::string workspaceRoot;
|
||||
std::string defaultLanguage = "python";
|
||||
bool verbose = false;
|
||||
|
||||
// --- Buffer access ---
|
||||
|
||||
HeadlessBufferState* active() { return activeBuffer; }
|
||||
|
||||
bool isStructured() const {
|
||||
return activeBuffer &&
|
||||
allowStructuredFeatures(activeBuffer->bufferMode);
|
||||
}
|
||||
|
||||
Module* activeAST() {
|
||||
if (!activeBuffer) return nullptr;
|
||||
if (!allowStructuredFeatures(activeBuffer->bufferMode))
|
||||
return nullptr;
|
||||
return activeBuffer->sync.getAST();
|
||||
}
|
||||
|
||||
Module* mutationAST() {
|
||||
if (!activeBuffer) return nullptr;
|
||||
if (!allowStructuredFeatures(activeBuffer->bufferMode))
|
||||
return nullptr;
|
||||
if (activeBuffer->orchestratorDirty ||
|
||||
!activeBuffer->orchestrator.getAST()) {
|
||||
syncOrchestratorFromActive();
|
||||
}
|
||||
return activeBuffer->orchestrator.getAST();
|
||||
}
|
||||
|
||||
void syncOrchestratorFromActive() {
|
||||
if (!activeBuffer) return;
|
||||
Module* ast = activeBuffer->sync.getAST();
|
||||
if (ast) {
|
||||
activeBuffer->orchestrator.setAST(cloneModule(ast));
|
||||
activeBuffer->orchestratorDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void applyOrchestratorToActive() {
|
||||
if (!activeBuffer) return;
|
||||
if (!isStructured()) return;
|
||||
Module* ast = activeBuffer->orchestrator.getAST();
|
||||
if (!ast) return;
|
||||
activeBuffer->sync.setAST(cloneModule(ast));
|
||||
activeBuffer->incrementalOptimizer.setRoot(
|
||||
activeBuffer->sync.getAST());
|
||||
Pipeline pipeline;
|
||||
std::string code = pipeline.generate(
|
||||
activeBuffer->sync.getAST(), activeBuffer->language);
|
||||
activeBuffer->editBuf = code;
|
||||
activeBuffer->orchestratorDirty = false;
|
||||
}
|
||||
|
||||
// --- Agent role management ---
|
||||
|
||||
AgentRole getAgentRole(const std::string& sessionId) const {
|
||||
auto it = agent.roles.find(sessionId);
|
||||
if (it != agent.roles.end()) return it->second;
|
||||
return agent.defaultRole;
|
||||
}
|
||||
|
||||
void setAgentRole(const std::string& sessionId, AgentRole role) {
|
||||
agent.roles[sessionId] = role;
|
||||
}
|
||||
|
||||
std::string agentActorLabel(const std::string& sessionId) const {
|
||||
AgentRole role = getAgentRole(sessionId);
|
||||
return std::string(AgentPermissionPolicy::roleLabel(role)) +
|
||||
":" + sessionId;
|
||||
}
|
||||
|
||||
// --- Diagnostics ---
|
||||
|
||||
json buildDiagnosticsJson() const {
|
||||
return json::array();
|
||||
}
|
||||
|
||||
json buildSessionMetadata(bool /*autoRecording*/) const {
|
||||
return {
|
||||
{"mode", "headless"},
|
||||
{"workspace", workspaceRoot},
|
||||
{"language", defaultLanguage},
|
||||
{"timestamp",
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count()}
|
||||
};
|
||||
}
|
||||
|
||||
// --- Buffer operations ---
|
||||
|
||||
HeadlessBufferState* openBuffer(const std::string& filePath,
|
||||
const std::string& content,
|
||||
const std::string& lang) {
|
||||
auto buf = std::make_unique<HeadlessBufferState>();
|
||||
buf->path = filePath;
|
||||
buf->language = lang.empty() ? defaultLanguage : lang;
|
||||
buf->editBuf = content;
|
||||
buf->bufferMode = BufferManager::BufferMode::Structured;
|
||||
if (!content.empty()) {
|
||||
Pipeline pipeline;
|
||||
std::vector<ParseDiagnostic> diags;
|
||||
auto mod = pipeline.parse(content, buf->language, diags);
|
||||
if (mod) {
|
||||
buf->sync.setAST(std::move(mod));
|
||||
buf->orchestratorDirty = true;
|
||||
}
|
||||
} else {
|
||||
auto mod = std::make_unique<Module>(
|
||||
"root", filePath, buf->language);
|
||||
buf->sync.setAST(std::move(mod));
|
||||
buf->orchestratorDirty = true;
|
||||
}
|
||||
HeadlessBufferState* raw = buf.get();
|
||||
bufferStates[filePath] = std::move(buf);
|
||||
if (!activeBuffer) activeBuffer = raw;
|
||||
return raw;
|
||||
}
|
||||
|
||||
bool setActiveBuffer(const std::string& path) {
|
||||
auto it = bufferStates.find(path);
|
||||
if (it == bufferStates.end()) return false;
|
||||
activeBuffer = it->second.get();
|
||||
return true;
|
||||
}
|
||||
|
||||
void closeBuffer(const std::string& path) {
|
||||
auto it = bufferStates.find(path);
|
||||
if (it == bufferStates.end()) return;
|
||||
if (activeBuffer == it->second.get())
|
||||
activeBuffer = nullptr;
|
||||
bufferStates.erase(it);
|
||||
if (!activeBuffer && !bufferStates.empty())
|
||||
activeBuffer = bufferStates.begin()->second.get();
|
||||
}
|
||||
|
||||
// --- RPC entry point ---
|
||||
json processAgentRequest(const json& request,
|
||||
const std::string& sessionId);
|
||||
|
||||
// --- Logging ---
|
||||
void log(const std::string& msg) const {
|
||||
if (verbose)
|
||||
std::cerr << "[whetstone-headless] " << msg << "\n";
|
||||
}
|
||||
};
|
||||
|
||||
// Include the headless RPC handler and wire up processAgentRequest
|
||||
#include "HeadlessAgentRPCHandler.h"
|
||||
|
||||
inline json HeadlessEditorState::processAgentRequest(
|
||||
const json& request, const std::string& sessionId) {
|
||||
return handleHeadlessAgentRequest(*this, request, sessionId);
|
||||
}
|
||||
Reference in New Issue
Block a user