Configurable auto-approve rules for review gate. Default policy auto-approves deterministic/template with >=0.9 confidence. @Review(required) always overrides. Policy serialization, wildcard rules, and 2 new MCP tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2617 lines
112 KiB
C++
2617 lines
112 KiB
C++
#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;
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
bool compact = params.value("compact", false);
|
|
json result;
|
|
if (compact) {
|
|
json nodes = toJsonCompactSummary(state.activeAST());
|
|
result = {{"nodes", nodes}, {"nodeCount", (int)nodes.size()},
|
|
{"totalNodes", (int)toJsonCompactTree(
|
|
state.activeAST()).size()}};
|
|
} else {
|
|
result = {
|
|
{"ast", toJson(state.activeAST())},
|
|
{"annotationCount",
|
|
countAnnotationNodes(state.activeAST())},
|
|
{"diagnostics", state.buildDiagnosticsJson()}
|
|
};
|
|
}
|
|
result["version"] = state.active()->versionTracker.version;
|
|
result["tokenEstimate"] = tokenEstimate(result);
|
|
int budget = params.value("budget", 0);
|
|
if (budget > 0) {
|
|
auto br = applyBudget(result, budget);
|
|
return headlessRpcResult(id, br.result);
|
|
}
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- 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));
|
|
state.active()->versionTracker.recordMutation(affectedIds);
|
|
// Record post-mutation state for undo
|
|
state.active()->undoStack.record(
|
|
state.active()->editBuf, state.activeAST());
|
|
}
|
|
return headlessRpcResult(id, {
|
|
{"success", true}, {"warning", res.warning},
|
|
{"libraryWarning", policy.warning},
|
|
{"unknownFunctions", policy.unknownFunctions},
|
|
{"version", state.active()
|
|
? state.active()->versionTracker.version : 0}
|
|
});
|
|
}
|
|
|
|
// --- 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));
|
|
std::vector<std::string> batchIds;
|
|
for (const auto& m : mutations)
|
|
if (!m.nodeId.empty()) batchIds.push_back(m.nodeId);
|
|
state.active()->versionTracker.recordMutation(batchIds);
|
|
// Record post-mutation state for undo
|
|
state.active()->undoStack.record(
|
|
state.active()->editBuf, state.activeAST());
|
|
}
|
|
return headlessRpcResult(id,
|
|
{{"success", true}, {"appliedCount", batchRes.appliedCount},
|
|
{"version", state.active()
|
|
? state.active()->versionTracker.version : 0}});
|
|
}
|
|
|
|
// --- 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");
|
|
bool detailed = params.value("detailed", false);
|
|
bool crossFile = params.value("crossFile", false);
|
|
ContextAPI ctx;
|
|
ctx.setRoot(state.activeAST());
|
|
auto symbols = ctx.getInScopeSymbols(nodeId);
|
|
json arr = json::array();
|
|
for (const auto& s : symbols) {
|
|
if (detailed) {
|
|
ASTNode* node = findNodeById(state.activeAST(), s.nodeId);
|
|
json entry = {{"name", s.name}, {"kind", s.kind},
|
|
{"nodeId", s.nodeId}};
|
|
if (node) entry["node"] = toJson(node);
|
|
arr.push_back(entry);
|
|
} else {
|
|
arr.push_back({{"name", s.name}, {"kind", s.kind},
|
|
{"nodeId", s.nodeId}});
|
|
}
|
|
}
|
|
// Cross-file: add exported symbols from other open buffers
|
|
if (crossFile) {
|
|
std::string activePath = state.activeBuffer
|
|
? state.activeBuffer->path : "";
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
if (path == activePath) continue;
|
|
Module* otherAST = buf->sync.getAST();
|
|
if (!otherAST) continue;
|
|
auto exports = collectExportedSymbols(otherAST, path);
|
|
for (const auto& ex : exports) {
|
|
json entry = {{"name", ex.name}, {"kind", ex.kind},
|
|
{"nodeId", ex.nodeId},
|
|
{"file", ex.filePath}};
|
|
if (detailed) {
|
|
ASTNode* node = findNodeById(otherAST, ex.nodeId);
|
|
if (node) entry["node"] = toJson(node);
|
|
}
|
|
arr.push_back(entry);
|
|
}
|
|
}
|
|
}
|
|
json result = {{"symbols", arr},
|
|
{"count", (int)arr.size()},
|
|
{"mode", detailed ? "detailed" : "symbols"}};
|
|
if (crossFile) result["crossFile"] = true;
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- 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");
|
|
bool detailed = params.value("detailed", false);
|
|
ContextAPI ctx;
|
|
ctx.setRoot(state.activeAST());
|
|
auto info = ctx.getCallHierarchy(functionId);
|
|
json result;
|
|
if (detailed) {
|
|
// Full mode: include node JSON for each caller/callee
|
|
json callers = json::array();
|
|
for (const auto& cid : info.callerIds) {
|
|
ASTNode* n = findNodeById(state.activeAST(), cid);
|
|
json entry = {{"nodeId", cid}};
|
|
if (n) { entry["name"] = getNodeName(n); entry["node"] = toJson(n); }
|
|
callers.push_back(entry);
|
|
}
|
|
json callees = json::array();
|
|
for (const auto& cid : info.calleeIds) {
|
|
ASTNode* n = findNodeById(state.activeAST(), cid);
|
|
json entry = {{"nodeId", cid}};
|
|
if (n) { entry["name"] = getNodeName(n); entry["node"] = toJson(n); }
|
|
callees.push_back(entry);
|
|
}
|
|
result = {{"functionId", info.functionId},
|
|
{"functionName", info.functionName},
|
|
{"callers", callers}, {"callees", callees},
|
|
{"mode", "detailed"}};
|
|
} else {
|
|
// Lean mode: names and IDs only
|
|
json callerNames = json::array();
|
|
for (const auto& cid : info.callerIds) {
|
|
ASTNode* n = findNodeById(state.activeAST(), cid);
|
|
callerNames.push_back(n ? getNodeName(n) : cid);
|
|
}
|
|
json calleeNames = json::array();
|
|
for (const auto& cid : info.calleeIds) {
|
|
ASTNode* n = findNodeById(state.activeAST(), cid);
|
|
calleeNames.push_back(n ? getNodeName(n) : cid);
|
|
}
|
|
result = {{"functionId", info.functionId},
|
|
{"functionName", info.functionName},
|
|
{"callerIds", info.callerIds},
|
|
{"calleeIds", info.calleeIds},
|
|
{"callerNames", callerNames},
|
|
{"calleeNames", calleeNames},
|
|
{"mode", "symbols"}};
|
|
}
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- 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");
|
|
bool detailed = params.value("detailed", false);
|
|
ContextAPI ctx;
|
|
ctx.setRoot(state.activeAST());
|
|
auto deps = ctx.getDependencyGraph(nodeId);
|
|
json result;
|
|
if (detailed) {
|
|
// Full mode: resolve each dependency ID to node JSON
|
|
json depArr = json::array();
|
|
for (const auto& did : deps) {
|
|
ASTNode* n = findNodeById(state.activeAST(), did);
|
|
json entry = {{"nodeId", did}};
|
|
if (n) { entry["name"] = getNodeName(n); entry["node"] = toJson(n); }
|
|
depArr.push_back(entry);
|
|
}
|
|
result = {{"dependencies", depArr}, {"mode", "detailed"}};
|
|
} else {
|
|
// Lean mode: just nodeId list
|
|
json idList = json::array();
|
|
for (const auto& did : deps)
|
|
idList.push_back(did);
|
|
result = {{"dependencyIds", idList},
|
|
{"count", (int)idList.size()},
|
|
{"mode", "symbols"}};
|
|
}
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- 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)}
|
|
});
|
|
}
|
|
|
|
// --- getDiagnostics ---
|
|
if (method == "getDiagnostics") {
|
|
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();
|
|
auto diags = collectAllDiagnostics(state.activeAST());
|
|
// Optional severity filter
|
|
if (params.contains("severity")) {
|
|
std::string sevStr = params.value("severity", "");
|
|
DiagnosticSeverity maxSev = severityFromStr(sevStr);
|
|
diags = filterBySeverity(diags, maxSev);
|
|
}
|
|
// Optional source filter
|
|
if (params.contains("source")) {
|
|
std::string src = params.value("source", "");
|
|
diags = filterBySource(diags, src);
|
|
}
|
|
// Record snapshot for delta tracking
|
|
auto allDiagsUnfiltered = collectAllDiagnostics(state.activeAST());
|
|
state.active()->diagTracker.recordSnapshot(allDiagsUnfiltered);
|
|
json diagJson = diagnosticsToJson(diags);
|
|
sortDiagnosticsByPriority(diagJson);
|
|
json result = {
|
|
{"diagnostics", diagJson},
|
|
{"count", (int)diags.size()},
|
|
{"version", state.active()->diagTracker.version}
|
|
};
|
|
int budget = params.value("budget", 0);
|
|
if (budget > 0) {
|
|
auto br = applyBudget(result, budget);
|
|
return headlessRpcResult(id, br.result);
|
|
}
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- getDiagnosticsDelta ---
|
|
if (method == "getDiagnosticsDelta") {
|
|
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();
|
|
int sinceVersion = params.value("sinceVersion", 0);
|
|
auto currentDiags = collectAllDiagnostics(state.activeAST());
|
|
state.active()->diagTracker.recordSnapshot(currentDiags);
|
|
auto delta = state.active()->diagTracker.getDelta(
|
|
currentDiags, sinceVersion);
|
|
return headlessRpcResult(id, deltaToJson(delta));
|
|
}
|
|
|
|
// --- getQuickFixes ---
|
|
if (method == "getQuickFixes") {
|
|
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", "");
|
|
std::vector<QuickFix> fixes;
|
|
if (nodeId.empty())
|
|
fixes = getQuickFixesAll(state.activeAST());
|
|
else
|
|
fixes = getQuickFixesForNode(state.activeAST(), nodeId);
|
|
json arr = json::array();
|
|
for (const auto& f : fixes)
|
|
arr.push_back(quickFixToJson(f));
|
|
return headlessRpcResult(id, {
|
|
{"fixes", arr}, {"count", (int)fixes.size()}
|
|
});
|
|
}
|
|
|
|
// --- applyQuickFix ---
|
|
if (method == "applyQuickFix") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto err = headlessRequireMutable(state, id);
|
|
if (!err.is_null()) return err;
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string diagCode = params.value("diagCode", "");
|
|
std::string nodeId = params.value("nodeId", "");
|
|
if (diagCode.empty() || nodeId.empty())
|
|
return headlessRpcError(id, -32602,
|
|
"Missing diagCode or nodeId");
|
|
auto fix = findQuickFix(state.activeAST(), diagCode, nodeId);
|
|
if (fix.mutation.is_null())
|
|
return headlessRpcError(id, -32002,
|
|
"No fix found for " + diagCode + " on " + nodeId);
|
|
// Apply the fix mutation via the existing mutation path
|
|
json mutRequest = {
|
|
{"jsonrpc", "2.0"}, {"id", id},
|
|
{"method", "applyMutation"}, {"params", fix.mutation}
|
|
};
|
|
json mutResp = handleHeadlessAgentRequest(
|
|
state, mutRequest, sessionId);
|
|
if (mutResp.contains("error"))
|
|
return mutResp;
|
|
// Re-check diagnostics to see if it cleared
|
|
auto remainingDiags = collectAllDiagnostics(state.activeAST());
|
|
bool cleared = true;
|
|
for (const auto& d : remainingDiags) {
|
|
if (d.code == diagCode && d.nodeId == nodeId) {
|
|
cleared = false;
|
|
break;
|
|
}
|
|
}
|
|
json result = mutResp["result"];
|
|
result["fixApplied"] = fix.id;
|
|
result["diagnosticCleared"] = cleared;
|
|
result["remainingDiagnostics"] = (int)remainingDiags.size();
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- getASTSubtree ---
|
|
if (method == "getASTSubtree") {
|
|
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");
|
|
json subtree = toJsonSubtree(state.activeAST(), nodeId);
|
|
if (subtree.is_null())
|
|
return headlessRpcError(id, -32002,
|
|
"Node not found: " + nodeId);
|
|
json result = {{"subtree", subtree}};
|
|
result["version"] = state.active()->versionTracker.version;
|
|
result["tokenEstimate"] = tokenEstimate(result);
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- getASTDiff ---
|
|
if (method == "getASTDiff") {
|
|
auto err = headlessRequireAST(state, id);
|
|
if (!err.is_null()) return err;
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
int sinceVersion = params.value("sinceVersion", 0);
|
|
json diff = state.active()->versionTracker.buildDiff(
|
|
state.activeAST(), sinceVersion);
|
|
diff["tokenEstimate"] = tokenEstimate(diff);
|
|
return headlessRpcResult(id, diff);
|
|
}
|
|
|
|
// --- fileRead ---
|
|
if (method == "fileRead") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty())
|
|
return headlessRpcError(id, -32602, "Missing path parameter");
|
|
auto [ok, resolved] =
|
|
fileOpsResolvePath(state.workspaceRoot, path);
|
|
if (!ok)
|
|
return headlessRpcError(id, -32040, resolved);
|
|
int startLine = params.value("startLine", 0);
|
|
int endLine = params.value("endLine", 0);
|
|
auto [success, content, lineCount] =
|
|
fileOpsRead(resolved, startLine, endLine);
|
|
if (!success)
|
|
return headlessRpcError(id, -32041, content);
|
|
return headlessRpcResult(id, {
|
|
{"content", content}, {"lineCount", lineCount},
|
|
{"path", resolved}
|
|
});
|
|
}
|
|
|
|
// --- fileWrite ---
|
|
if (method == "fileWrite") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty())
|
|
return headlessRpcError(id, -32602, "Missing path parameter");
|
|
std::string content = params.value("content", "");
|
|
auto [ok, resolved] =
|
|
fileOpsResolvePath(state.workspaceRoot, path);
|
|
if (!ok)
|
|
return headlessRpcError(id, -32040, resolved);
|
|
auto [success, msg, bytes] = fileOpsWrite(resolved, content);
|
|
if (!success)
|
|
return headlessRpcError(id, -32041, msg);
|
|
return headlessRpcResult(id, {
|
|
{"success", true}, {"path", resolved},
|
|
{"bytesWritten", bytes}
|
|
});
|
|
}
|
|
|
|
// --- fileCreate ---
|
|
if (method == "fileCreate") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty())
|
|
return headlessRpcError(id, -32602, "Missing path parameter");
|
|
auto [ok, resolved] =
|
|
fileOpsResolvePath(state.workspaceRoot, path);
|
|
if (!ok)
|
|
return headlessRpcError(id, -32040, resolved);
|
|
std::string language = params.value("language", "");
|
|
std::string tmpl = params.value("template", "");
|
|
auto [success, msg] = fileOpsCreate(resolved, language, tmpl);
|
|
if (!success)
|
|
return headlessRpcError(id, -32041, msg);
|
|
return headlessRpcResult(id, {
|
|
{"success", true}, {"path", resolved}
|
|
});
|
|
}
|
|
|
|
// --- fileDiff ---
|
|
if (method == "fileDiff") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
std::string bufContent;
|
|
std::string diskPath;
|
|
if (!path.empty()) {
|
|
auto [ok, resolved] =
|
|
fileOpsResolvePath(state.workspaceRoot, path);
|
|
if (!ok)
|
|
return headlessRpcError(id, -32040, resolved);
|
|
diskPath = resolved;
|
|
auto it = state.bufferStates.find(path);
|
|
if (it != state.bufferStates.end())
|
|
bufContent = it->second->editBuf;
|
|
else {
|
|
auto [rok, content, lc] = fileOpsRead(resolved);
|
|
if (rok) bufContent = content;
|
|
}
|
|
} else if (state.active()) {
|
|
diskPath = state.active()->path;
|
|
bufContent = state.active()->editBuf;
|
|
auto [ok2, resolved2] =
|
|
fileOpsResolvePath(state.workspaceRoot, diskPath);
|
|
if (ok2) diskPath = resolved2;
|
|
} else {
|
|
return headlessRpcError(id, -32000, "No active buffer");
|
|
}
|
|
auto [diffText, added, removed] =
|
|
fileOpsDiff(bufContent, diskPath);
|
|
return headlessRpcResult(id, {
|
|
{"diff", diffText}, {"linesAdded", added},
|
|
{"linesRemoved", removed}
|
|
});
|
|
}
|
|
|
|
// --- workspaceList ---
|
|
if (method == "workspaceList") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string glob = params.value("glob", "*");
|
|
auto entries = fileOpsListWorkspace(state.workspaceRoot, glob);
|
|
json files = json::array();
|
|
for (const auto& e : entries)
|
|
files.push_back({
|
|
{"path", e.path}, {"size", e.size},
|
|
{"isDir", e.isDir}
|
|
});
|
|
return headlessRpcResult(id, {{"files", files}});
|
|
}
|
|
|
|
// --- openFile ---
|
|
if (method == "openFile") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty())
|
|
return headlessRpcError(id, -32602, "Missing path parameter");
|
|
std::string content = params.value("content", "");
|
|
std::string language = params.value("language", "");
|
|
// Auto-detect language from extension if not specified
|
|
if (language.empty())
|
|
language = detectLanguage(path);
|
|
if (language.empty())
|
|
language = state.defaultLanguage;
|
|
// Read from disk if no content provided and workspace is set
|
|
if (content.empty() && !state.workspaceRoot.empty()) {
|
|
auto [ok, resolved] =
|
|
fileOpsResolvePath(state.workspaceRoot, path);
|
|
if (ok) {
|
|
auto [rok, fileContent, lc] = fileOpsRead(resolved);
|
|
if (rok) content = fileContent;
|
|
path = resolved;
|
|
}
|
|
}
|
|
auto* buf = state.openBuffer(path, content, language);
|
|
if (!buf)
|
|
return headlessRpcError(id, -32041, "Failed to open buffer");
|
|
// Update import graph from the new buffer's AST + source
|
|
Module* bufAST = buf->sync.getAST();
|
|
if (bufAST)
|
|
state.project.importGraph.updateFromAST(path, bufAST);
|
|
// Fallback: scan source text for imports not captured by parser
|
|
if (!content.empty())
|
|
state.project.importGraph.updateFromSource(path, content);
|
|
return headlessRpcResult(id, {
|
|
{"path", path}, {"language", language},
|
|
{"bufferCount", (int)state.bufferStates.size()}
|
|
});
|
|
}
|
|
|
|
// --- closeFile ---
|
|
if (method == "closeFile") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty())
|
|
return headlessRpcError(id, -32602, "Missing path parameter");
|
|
auto it = state.bufferStates.find(path);
|
|
if (it == state.bufferStates.end())
|
|
return headlessRpcError(id, -32002, "Buffer not found: " + path);
|
|
state.project.importGraph.clearFile(path);
|
|
state.closeBuffer(path);
|
|
return headlessRpcResult(id, {
|
|
{"closed", path},
|
|
{"bufferCount", (int)state.bufferStates.size()}
|
|
});
|
|
}
|
|
|
|
// --- listBuffers ---
|
|
if (method == "listBuffers") {
|
|
json buffers = json::array();
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
bool isActive = (buf.get() == state.activeBuffer);
|
|
buffers.push_back({
|
|
{"path", buf->path},
|
|
{"language", buf->language},
|
|
{"modified", buf->modified},
|
|
{"active", isActive}
|
|
});
|
|
}
|
|
return headlessRpcResult(id, {
|
|
{"buffers", buffers},
|
|
{"count", (int)buffers.size()},
|
|
{"activeBuffer", state.activeBuffer
|
|
? state.activeBuffer->path : ""}
|
|
});
|
|
}
|
|
|
|
// --- setActiveBuffer ---
|
|
if (method == "setActiveBuffer") {
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty())
|
|
return headlessRpcError(id, -32602, "Missing path parameter");
|
|
if (!state.setActiveBuffer(path))
|
|
return headlessRpcError(id, -32002, "Buffer not found: " + path);
|
|
return headlessRpcResult(id, {
|
|
{"activeBuffer", path},
|
|
{"language", state.activeBuffer->language}
|
|
});
|
|
}
|
|
|
|
// --- indexWorkspace ---
|
|
if (method == "indexWorkspace") {
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string root = params.value("root", state.workspaceRoot);
|
|
if (root.empty())
|
|
return headlessRpcError(id, -32602,
|
|
"No workspace root (set via --workspace or root param)");
|
|
state.project.scanWorkspace(root);
|
|
state.workspaceRoot = root;
|
|
const auto& idx = state.project.index;
|
|
return headlessRpcResult(id, {
|
|
{"root", root},
|
|
{"fileCount", idx.fileCount()},
|
|
{"dirCount", idx.dirCount()},
|
|
{"totalEntries", (int)idx.files().size()}
|
|
});
|
|
}
|
|
|
|
// --- getImportGraph ---
|
|
if (method == "getImportGraph") {
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string filePath = params.value("file", "");
|
|
const auto& graph = state.project.importGraph;
|
|
if (!filePath.empty()) {
|
|
// Imports for a specific file
|
|
auto imports = graph.importsOf(filePath);
|
|
json importArr = json::array();
|
|
for (const auto& m : imports) importArr.push_back(m);
|
|
auto importers = graph.importedBy(
|
|
fs::path(filePath).stem().string());
|
|
json importerArr = json::array();
|
|
for (const auto& f : importers) importerArr.push_back(f);
|
|
return headlessRpcResult(id, {
|
|
{"file", filePath},
|
|
{"imports", importArr},
|
|
{"importedBy", importerArr}
|
|
});
|
|
}
|
|
// Full import graph
|
|
json edges = json::object();
|
|
for (const auto& [file, imports] : graph.edges()) {
|
|
json importArr = json::array();
|
|
for (const auto& m : imports) importArr.push_back(m);
|
|
edges[file] = importArr;
|
|
}
|
|
return headlessRpcResult(id, {
|
|
{"edges", edges},
|
|
{"fileCount", (int)graph.edges().size()}
|
|
});
|
|
}
|
|
|
|
// --- searchProject ---
|
|
if (method == "searchProject") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string name = params.value("name", "");
|
|
std::string nodeId = params.value("nodeId", "");
|
|
|
|
// If nodeId given, resolve name from the active buffer
|
|
if (!name.empty()) {
|
|
// use name as-is
|
|
} else if (!nodeId.empty()) {
|
|
// Search all buffers for a node with this ID
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
Module* ast = buf->sync.getAST();
|
|
if (!ast) continue;
|
|
ASTNode* node = findNodeById(ast, nodeId);
|
|
if (node) {
|
|
name = getNodeName(node);
|
|
break;
|
|
}
|
|
}
|
|
if (name.empty())
|
|
return headlessRpcError(id, -32002,
|
|
"Node not found: " + nodeId);
|
|
} else {
|
|
return headlessRpcError(id, -32602,
|
|
"Missing name or nodeId parameter");
|
|
}
|
|
|
|
json results = json::array();
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
Module* ast = buf->sync.getAST();
|
|
if (!ast) continue;
|
|
auto refs = collectSymbolReferences(ast, name, path);
|
|
for (const auto& ref : refs) {
|
|
results.push_back({
|
|
{"file", ref.file}, {"line", ref.line},
|
|
{"col", ref.col}, {"nodeId", ref.nodeId},
|
|
{"kind", ref.kind}, {"context", ref.context}
|
|
});
|
|
}
|
|
}
|
|
|
|
std::set<std::string> searchFiles;
|
|
for (const auto& r : results)
|
|
searchFiles.insert(r.value("file", ""));
|
|
|
|
return headlessRpcResult(id, {
|
|
{"name", name},
|
|
{"references", results},
|
|
{"count", (int)results.size()},
|
|
{"fileCount", (int)searchFiles.size()}
|
|
});
|
|
}
|
|
|
|
// --- renameSymbol ---
|
|
if (method == "renameSymbol") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string oldName = params.value("oldName", "");
|
|
std::string newName = params.value("newName", "");
|
|
bool preview = params.value("preview", false);
|
|
|
|
if (oldName.empty() || newName.empty())
|
|
return headlessRpcError(id, -32602,
|
|
"Missing oldName or newName parameter");
|
|
|
|
|
|
// Collect changes across all open buffers
|
|
std::vector<RenameChange> allChanges;
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
Module* ast = buf->sync.getAST();
|
|
if (!ast) continue;
|
|
auto changes = buildRenameChanges(
|
|
ast, oldName, newName, path);
|
|
allChanges.insert(allChanges.end(),
|
|
changes.begin(), changes.end());
|
|
}
|
|
|
|
if (allChanges.empty())
|
|
return headlessRpcError(id, -32002,
|
|
"No references found for '" + oldName + "'");
|
|
|
|
// Build preview JSON
|
|
json changesJson = json::array();
|
|
std::set<std::string> affectedFiles;
|
|
for (const auto& c : allChanges) {
|
|
changesJson.push_back({
|
|
{"file", c.file}, {"nodeId", c.nodeId},
|
|
{"property", c.property},
|
|
{"oldValue", c.oldValue},
|
|
{"newValue", c.newValue},
|
|
{"kind", c.kind}
|
|
});
|
|
affectedFiles.insert(c.file);
|
|
}
|
|
|
|
if (preview) {
|
|
return headlessRpcResult(id, {
|
|
{"preview", true},
|
|
{"changes", changesJson},
|
|
{"changeCount", (int)allChanges.size()},
|
|
{"fileCount", (int)affectedFiles.size()}
|
|
});
|
|
}
|
|
|
|
// Apply changes: use setProperty on each node
|
|
int applied = 0;
|
|
std::vector<std::string> errors;
|
|
for (const auto& c : allChanges) {
|
|
auto it = state.bufferStates.find(c.file);
|
|
if (it == state.bufferStates.end()) continue;
|
|
Module* ast = it->second->sync.getAST();
|
|
if (!ast) continue;
|
|
ASTNode* node = findNodeById(ast, c.nodeId);
|
|
if (!node) {
|
|
errors.push_back("Node " + c.nodeId +
|
|
" not found in " + c.file);
|
|
continue;
|
|
}
|
|
// Apply the property change directly
|
|
if (node->conceptType == "Function" &&
|
|
c.property == "name") {
|
|
static_cast<Function*>(node)->name = c.newValue;
|
|
++applied;
|
|
} else if (node->conceptType == "FunctionCall" &&
|
|
c.property == "functionName") {
|
|
static_cast<FunctionCall*>(node)->functionName =
|
|
c.newValue;
|
|
++applied;
|
|
} else if (node->conceptType == "Variable" &&
|
|
c.property == "name") {
|
|
static_cast<Variable*>(node)->name = c.newValue;
|
|
++applied;
|
|
} else if (node->conceptType == "VariableReference" &&
|
|
c.property == "variableName") {
|
|
static_cast<VariableReference*>(node)->variableName =
|
|
c.newValue;
|
|
++applied;
|
|
} else if (node->conceptType == "Parameter" &&
|
|
c.property == "name") {
|
|
static_cast<Parameter*>(node)->name = c.newValue;
|
|
++applied;
|
|
}
|
|
}
|
|
|
|
// Mark affected buffers as modified, regenerate editBuf, record undo
|
|
for (const auto& f : affectedFiles) {
|
|
auto it = state.bufferStates.find(f);
|
|
if (it != state.bufferStates.end()) {
|
|
it->second->modified = true;
|
|
// Regenerate code from the modified AST
|
|
Module* bufAST = it->second->sync.getAST();
|
|
if (bufAST) {
|
|
Pipeline pipeline;
|
|
it->second->editBuf = pipeline.generate(
|
|
bufAST, it->second->language);
|
|
// Record post-rename state for undo
|
|
it->second->undoStack.record(
|
|
it->second->editBuf, bufAST);
|
|
}
|
|
}
|
|
}
|
|
|
|
json result = {
|
|
{"applied", applied},
|
|
{"changes", changesJson},
|
|
{"changeCount", (int)allChanges.size()},
|
|
{"fileCount", (int)affectedFiles.size()}
|
|
};
|
|
if (!errors.empty()) {
|
|
json errArr = json::array();
|
|
for (const auto& e : errors) errArr.push_back(e);
|
|
result["errors"] = errArr;
|
|
}
|
|
return headlessRpcResult(id, result);
|
|
}
|
|
|
|
// --- getProjectDiagnostics ---
|
|
if (method == "getProjectDiagnostics") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
// Optional severity filter
|
|
std::string sevFilter = params.value("severity", "");
|
|
// Optional file path glob filter (simple suffix match)
|
|
std::string fileGlob = params.value("fileGlob", "");
|
|
|
|
// Collect module names from open buffers for cross-file checks
|
|
std::set<std::string> openModuleNames;
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
std::string stem = fs::path(path).stem().string();
|
|
if (!stem.empty()) openModuleNames.insert(stem);
|
|
}
|
|
|
|
json filesDiags = json::object();
|
|
int totalCount = 0;
|
|
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
// Apply file glob filter (simple suffix/extension match)
|
|
if (!fileGlob.empty()) {
|
|
// Match *.ext or exact name
|
|
if (fileGlob[0] == '*') {
|
|
std::string suffix = fileGlob.substr(1);
|
|
if (path.size() < suffix.size() ||
|
|
path.substr(path.size() - suffix.size()) != suffix)
|
|
continue;
|
|
} else if (path.find(fileGlob) == std::string::npos) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
Module* bufAST = buf->sync.getAST();
|
|
std::vector<StructuredDiagnostic> diags;
|
|
|
|
// Per-file diagnostics (annotation + strategy)
|
|
if (bufAST) {
|
|
auto fileDiags = collectAllDiagnostics(bufAST);
|
|
diags.insert(diags.end(), fileDiags.begin(),
|
|
fileDiags.end());
|
|
// Cross-file: undefined imports from AST
|
|
auto crossDiags = collectCrossFileDiagnostics(
|
|
bufAST, path, openModuleNames);
|
|
diags.insert(diags.end(), crossDiags.begin(),
|
|
crossDiags.end());
|
|
}
|
|
|
|
// Cross-file: undefined imports from source text
|
|
if (!buf->editBuf.empty()) {
|
|
auto srcCross = collectCrossFileDiagnosticsFromSource(
|
|
buf->editBuf, path, openModuleNames);
|
|
// Deduplicate: only add source-based if no AST-based
|
|
// cross-file diags exist for the same line
|
|
std::set<int> astCrossLines;
|
|
for (const auto& d : diags) {
|
|
if (d.source == "cross-file")
|
|
astCrossLines.insert(d.line);
|
|
}
|
|
for (auto& d : srcCross) {
|
|
if (astCrossLines.find(d.line) == astCrossLines.end())
|
|
diags.push_back(std::move(d));
|
|
}
|
|
}
|
|
|
|
// Apply severity filter
|
|
if (!sevFilter.empty()) {
|
|
DiagnosticSeverity maxSev = severityFromStr(sevFilter);
|
|
diags = filterBySeverity(diags, maxSev);
|
|
}
|
|
|
|
if (!diags.empty()) {
|
|
json diagArr = diagnosticsToJson(diags);
|
|
sortDiagnosticsByPriority(diagArr);
|
|
filesDiags[path] = diagArr;
|
|
totalCount += (int)diags.size();
|
|
}
|
|
}
|
|
|
|
return headlessRpcResult(id, {
|
|
{"files", filesDiags},
|
|
{"fileCount", (int)filesDiags.size()},
|
|
{"totalDiagnostics", totalCount}
|
|
});
|
|
}
|
|
|
|
// --- undo ---
|
|
if (method == "undo") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32000, "No active buffer");
|
|
auto& stack = state.activeBuffer->undoStack;
|
|
if (!stack.canUndo())
|
|
return headlessRpcError(id, -32050, "Nothing to undo");
|
|
const auto& snap = stack.undo();
|
|
// Restore AST from JSON
|
|
if (!snap.astJson.is_null()) {
|
|
ASTNode* node = fromJson(snap.astJson);
|
|
if (node && node->conceptType == "Module") {
|
|
state.activeBuffer->sync.setAST(
|
|
std::unique_ptr<Module>(
|
|
static_cast<Module*>(node)));
|
|
state.activeBuffer->orchestratorDirty = true;
|
|
} else {
|
|
deleteTree(node);
|
|
}
|
|
}
|
|
state.activeBuffer->editBuf = snap.text;
|
|
state.activeBuffer->modified = true;
|
|
return headlessRpcResult(id, {
|
|
{"success", true},
|
|
{"undoDepth", stack.undoDepth()},
|
|
{"redoDepth", stack.redoDepth()}
|
|
});
|
|
}
|
|
|
|
// --- redo ---
|
|
if (method == "redo") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32000, "No active buffer");
|
|
auto& stack = state.activeBuffer->undoStack;
|
|
if (!stack.canRedo())
|
|
return headlessRpcError(id, -32050, "Nothing to redo");
|
|
const auto& snap = stack.redo();
|
|
if (!snap.astJson.is_null()) {
|
|
ASTNode* node = fromJson(snap.astJson);
|
|
if (node && node->conceptType == "Module") {
|
|
state.activeBuffer->sync.setAST(
|
|
std::unique_ptr<Module>(
|
|
static_cast<Module*>(node)));
|
|
state.activeBuffer->orchestratorDirty = true;
|
|
} else {
|
|
deleteTree(node);
|
|
}
|
|
}
|
|
state.activeBuffer->editBuf = snap.text;
|
|
state.activeBuffer->modified = true;
|
|
return headlessRpcResult(id, {
|
|
{"success", true},
|
|
{"undoDepth", stack.undoDepth()},
|
|
{"redoDepth", stack.redoDepth()}
|
|
});
|
|
}
|
|
|
|
// --- saveBuffer ---
|
|
if (method == "saveBuffer") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
|
|
// Default to active buffer if no path specified
|
|
if (path.empty()) {
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32000, "No active buffer");
|
|
path = state.activeBuffer->path;
|
|
}
|
|
|
|
auto it = state.bufferStates.find(path);
|
|
if (it == state.bufferStates.end())
|
|
return headlessRpcError(id, -32002,
|
|
"Buffer not found: " + path);
|
|
|
|
auto& buf = it->second;
|
|
|
|
// Resolve path against workspace root
|
|
std::string writePath = path;
|
|
if (!state.workspaceRoot.empty()) {
|
|
auto [ok, resolved] =
|
|
fileOpsResolvePath(state.workspaceRoot, path);
|
|
if (!ok)
|
|
return headlessRpcError(id, -32040, resolved);
|
|
writePath = resolved;
|
|
}
|
|
|
|
// Create parent directories if needed
|
|
fs::path parentDir = fs::path(writePath).parent_path();
|
|
if (!parentDir.empty()) {
|
|
std::error_code ec;
|
|
fs::create_directories(parentDir, ec);
|
|
}
|
|
|
|
// Write editBuf to disk
|
|
auto [success, msg, bytes] = fileOpsWrite(writePath, buf->editBuf);
|
|
if (!success)
|
|
return headlessRpcError(id, -32041, msg);
|
|
|
|
buf->modified = false;
|
|
return headlessRpcResult(id, {
|
|
{"success", true}, {"path", writePath},
|
|
{"bytesWritten", bytes}
|
|
});
|
|
}
|
|
|
|
// --- saveAllBuffers ---
|
|
if (method == "saveAllBuffers") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
|
|
json savedPaths = json::array();
|
|
int savedCount = 0;
|
|
int skippedCount = 0;
|
|
|
|
for (auto& [path, buf] : state.bufferStates) {
|
|
if (!buf->modified) {
|
|
++skippedCount;
|
|
continue;
|
|
}
|
|
|
|
std::string writePath = path;
|
|
if (!state.workspaceRoot.empty()) {
|
|
auto [ok, resolved] =
|
|
fileOpsResolvePath(state.workspaceRoot, path);
|
|
if (!ok) {
|
|
++skippedCount;
|
|
continue;
|
|
}
|
|
writePath = resolved;
|
|
}
|
|
|
|
fs::path parentDir = fs::path(writePath).parent_path();
|
|
if (!parentDir.empty()) {
|
|
std::error_code ec;
|
|
fs::create_directories(parentDir, ec);
|
|
}
|
|
|
|
auto [success, msg, bytes] =
|
|
fileOpsWrite(writePath, buf->editBuf);
|
|
if (success) {
|
|
buf->modified = false;
|
|
savedPaths.push_back(writePath);
|
|
++savedCount;
|
|
} else {
|
|
++skippedCount;
|
|
}
|
|
}
|
|
|
|
return headlessRpcResult(id, {
|
|
{"savedCount", savedCount},
|
|
{"skippedCount", skippedCount},
|
|
{"saved", savedPaths}
|
|
});
|
|
}
|
|
|
|
// --- batchQuery ---
|
|
if (method == "batchQuery") {
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
if (!params.contains("queries") || !params["queries"].is_array())
|
|
return headlessRpcError(id, -32602, "Missing queries array");
|
|
json results = json::array();
|
|
for (const auto& q : params["queries"]) {
|
|
std::string subMethod = q.value("method", "");
|
|
json subParams = q.contains("params") ? q["params"]
|
|
: json::object();
|
|
json subReq = {{"jsonrpc", "2.0"}, {"id", 1},
|
|
{"method", subMethod}, {"params", subParams}};
|
|
json subResp = handleHeadlessAgentRequest(
|
|
state, subReq, sessionId);
|
|
// Extract result or error from the sub-response
|
|
if (subResp.contains("result"))
|
|
results.push_back({{"method", subMethod},
|
|
{"result", subResp["result"]}});
|
|
else if (subResp.contains("error"))
|
|
results.push_back({{"method", subMethod},
|
|
{"error", subResp["error"]}});
|
|
else
|
|
results.push_back({{"method", subMethod},
|
|
{"error", "Unknown response"}});
|
|
}
|
|
return headlessRpcResult(id,
|
|
{{"results", results}, {"count", (int)results.size()}});
|
|
}
|
|
|
|
// --- saveAnnotatedAST ---
|
|
if (method == "saveAnnotatedAST") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty() && state.activeBuffer)
|
|
path = state.activeBuffer->path;
|
|
auto it = state.bufferStates.find(path);
|
|
if (it == state.bufferStates.end())
|
|
return headlessRpcError(id, -32602, "No buffer: " + path);
|
|
Module* ast = it->second->sync.getAST();
|
|
auto res = saveSidecarAST(state.workspaceRoot, path, ast);
|
|
if (!res.success)
|
|
return headlessRpcError(id, -32010, res.error);
|
|
return headlessRpcResult(id,
|
|
{{"success", true},
|
|
{"sidecarPath", res.sidecarPath},
|
|
{"annotationCount", res.annotationCount}});
|
|
}
|
|
|
|
// --- loadAnnotatedAST ---
|
|
if (method == "loadAnnotatedAST") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string path = params.value("path", "");
|
|
if (path.empty() && state.activeBuffer)
|
|
path = state.activeBuffer->path;
|
|
auto it = state.bufferStates.find(path);
|
|
if (it == state.bufferStates.end())
|
|
return headlessRpcError(id, -32602, "No buffer: " + path);
|
|
Module* ast = it->second->sync.getAST();
|
|
auto res = loadSidecarAST(state.workspaceRoot, path, ast);
|
|
if (!res.success)
|
|
return headlessRpcError(id, -32010, res.error);
|
|
return headlessRpcResult(id,
|
|
{{"success", true},
|
|
{"mergedCount", res.mergedCount},
|
|
{"staleCount", res.staleCount}});
|
|
}
|
|
|
|
// --- listAnnotatedFiles ---
|
|
if (method == "listAnnotatedFiles") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto files = listSidecarFiles(state.workspaceRoot);
|
|
return headlessRpcResult(id,
|
|
{{"files", files}, {"count", (int)files.size()}});
|
|
}
|
|
|
|
// --- setSemanticAnnotation ---
|
|
if (method == "setSemanticAnnotation") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string nodeId = params.value("nodeId", "");
|
|
std::string type = params.value("type", "");
|
|
json fields = params.contains("fields") ? params["fields"]
|
|
: json::object();
|
|
if (nodeId.empty() || type.empty())
|
|
return headlessRpcError(id, -32602, "nodeId and type required");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
ASTNode* target = findNodeById(ast, nodeId);
|
|
if (!target)
|
|
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
|
|
|
|
// Map type string to conceptType
|
|
std::string conceptType;
|
|
if (type == "intent") conceptType = "IntentAnnotation";
|
|
else if (type == "complexity") conceptType = "ComplexityAnnotation";
|
|
else if (type == "risk") conceptType = "RiskAnnotation";
|
|
else if (type == "contract") conceptType = "ContractAnnotation";
|
|
else if (type == "tags") conceptType = "SemanticTagAnnotation";
|
|
// Type System (Steps 272-273)
|
|
else if (type == "bitWidth") conceptType = "BitWidthAnnotation";
|
|
else if (type == "endian") conceptType = "EndianAnnotation";
|
|
else if (type == "layout") conceptType = "LayoutAnnotation";
|
|
else if (type == "nullability") conceptType = "NullabilityAnnotation";
|
|
else if (type == "variance") conceptType = "VarianceAnnotation";
|
|
else if (type == "identity") conceptType = "IdentityAnnotation";
|
|
else if (type == "mut") conceptType = "MutAnnotation";
|
|
else if (type == "typeState") conceptType = "TypeStateAnnotation";
|
|
// Concurrency (Step 274)
|
|
else if (type == "atomic") conceptType = "AtomicAnnotation";
|
|
else if (type == "sync") conceptType = "SyncAnnotation";
|
|
else if (type == "threadModel") conceptType = "ThreadModelAnnotation";
|
|
else if (type == "memoryBarrier") conceptType = "MemoryBarrierAnnotation";
|
|
// Async / Error Handling (Step 275)
|
|
else if (type == "exec") conceptType = "ExecAnnotation";
|
|
else if (type == "blocking") conceptType = "BlockingAnnotation";
|
|
else if (type == "parallel") conceptType = "ParallelAnnotation";
|
|
else if (type == "trap") conceptType = "TrapAnnotation";
|
|
else if (type == "exception") conceptType = "ExceptionAnnotation";
|
|
else if (type == "panic") conceptType = "PanicAnnotation";
|
|
// Scope & Namespace (Step 276)
|
|
else if (type == "binding") conceptType = "BindingAnnotation";
|
|
else if (type == "lookup") conceptType = "LookupAnnotation";
|
|
else if (type == "capture") conceptType = "CaptureAnnotation";
|
|
else if (type == "visibility") conceptType = "VisibilityAnnotation";
|
|
else if (type == "namespace") conceptType = "NamespaceAnnotation";
|
|
else if (type == "scope") conceptType = "ScopeAnnotation";
|
|
// Shim & Escape Hatch (Step 278)
|
|
else if (type == "intrinsic") conceptType = "IntrinsicAnnotation";
|
|
else if (type == "raw") conceptType = "RawAnnotation";
|
|
else if (type == "callingConv") conceptType = "CallingConvAnnotation";
|
|
else if (type == "link") conceptType = "LinkAnnotation";
|
|
else if (type == "shim") conceptType = "ShimAnnotation";
|
|
else if (type == "pointerArithmetic") conceptType = "PointerArithmeticAnnotation";
|
|
else if (type == "opaque") conceptType = "OpaqueAnnotation";
|
|
// Platform & Provenance (Step 279)
|
|
else if (type == "target") conceptType = "TargetAnnotation";
|
|
else if (type == "feature") conceptType = "FeatureAnnotation";
|
|
else if (type == "original") conceptType = "OriginalAnnotation";
|
|
else if (type == "mapping") conceptType = "MappingAnnotation";
|
|
// Optimization Completion (Step 280)
|
|
else if (type == "tailCall") conceptType = "TailCallAnnotation";
|
|
else if (type == "loop") conceptType = "LoopAnnotation";
|
|
else if (type == "dataHint") conceptType = "DataAnnotation";
|
|
else if (type == "align") conceptType = "AlignAnnotation";
|
|
else if (type == "pack") conceptType = "PackAnnotation";
|
|
else if (type == "boundsCheck") conceptType = "BoundsCheckAnnotation";
|
|
else if (type == "overflow") conceptType = "OverflowAnnotation";
|
|
// Meta-Programming (Step 281)
|
|
else if (type == "meta") conceptType = "MetaAnnotation";
|
|
else if (type == "symbolMode") conceptType = "SymbolAnnotation";
|
|
else if (type == "evaluate") conceptType = "EvaluateAnnotation";
|
|
else if (type == "template") conceptType = "TemplateAnnotation";
|
|
else if (type == "synthetic") conceptType = "SyntheticAnnotation";
|
|
// Strategy & Policy (Step 282)
|
|
else if (type == "policy") conceptType = "PolicyAnnotation";
|
|
else if (type == "ambiguity") conceptType = "AmbiguityAnnotation";
|
|
else if (type == "candidate") conceptType = "CandidateAnnotation";
|
|
else if (type == "tradeoff") conceptType = "TradeoffAnnotation";
|
|
else if (type == "choice") conceptType = "ChoiceAnnotation";
|
|
else if (type == "decision") conceptType = "DecisionAnnotation";
|
|
// Subject 9: Workflow Routing (Step 315)
|
|
else if (type == "contextWidth") conceptType = "ContextWidthAnnotation";
|
|
else if (type == "review") conceptType = "ReviewAnnotation";
|
|
else if (type == "automatability") conceptType = "AutomatabilityAnnotation";
|
|
else if (type == "priority") conceptType = "PriorityAnnotation";
|
|
else if (type == "implementationStatus") conceptType = "ImplementationStatusAnnotation";
|
|
// Environment Layer (Step 285)
|
|
else if (type == "capabilityRequirement") conceptType = "CapabilityRequirement";
|
|
else return headlessRpcError(id, -32602, "Unknown annotation type: " + type);
|
|
|
|
// Remove existing annotation of same type (update semantics)
|
|
auto annos = target->getChildren("annotations");
|
|
for (auto* a : annos) {
|
|
if (a->conceptType == conceptType) {
|
|
target->removeChild(a);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Create new annotation node
|
|
json nodeJson = {{"concept", conceptType}, {"properties", fields}};
|
|
ASTNode* newAnno = fromJson(nodeJson);
|
|
if (!newAnno)
|
|
return headlessRpcError(id, -32010, "Failed to create annotation");
|
|
target->addChild("annotations", newAnno);
|
|
|
|
return headlessRpcResult(id, {{"success", true}, {"type", type}});
|
|
}
|
|
|
|
// --- getSemanticAnnotations ---
|
|
if (method == "getSemanticAnnotations") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string nodeId = params.value("nodeId", "");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
if (!nodeId.empty()) {
|
|
// Return annotations for a specific node
|
|
ASTNode* target = findNodeById(ast, nodeId);
|
|
if (!target)
|
|
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
|
|
json annos = json::array();
|
|
for (auto* a : target->getChildren("annotations")) {
|
|
if (!isSemanticAnnotation(a->conceptType)) continue;
|
|
json entry;
|
|
if (a->conceptType == "IntentAnnotation") {
|
|
auto* ia = static_cast<IntentAnnotation*>(a);
|
|
entry = {{"type", "intent"}, {"summary", ia->summary},
|
|
{"category", ia->category}};
|
|
} else if (a->conceptType == "ComplexityAnnotation") {
|
|
auto* ca = static_cast<ComplexityAnnotation*>(a);
|
|
entry = {{"type", "complexity"},
|
|
{"timeComplexity", ca->timeComplexity},
|
|
{"cognitiveComplexity", ca->cognitiveComplexity},
|
|
{"linesOfLogic", ca->linesOfLogic}};
|
|
} else if (a->conceptType == "RiskAnnotation") {
|
|
auto* ra = static_cast<RiskAnnotation*>(a);
|
|
entry = {{"type", "risk"}, {"level", ra->level},
|
|
{"reason", ra->reason},
|
|
{"dependentCount", ra->dependentCount}};
|
|
} else if (a->conceptType == "ContractAnnotation") {
|
|
auto* ca = static_cast<ContractAnnotation*>(a);
|
|
entry = {{"type", "contract"},
|
|
{"preconditions", ca->preconditions},
|
|
{"postconditions", ca->postconditions},
|
|
{"returnShape", ca->returnShape},
|
|
{"sideEffects", ca->sideEffects}};
|
|
} else if (a->conceptType == "SemanticTagAnnotation") {
|
|
auto* ta = static_cast<SemanticTagAnnotation*>(a);
|
|
entry = {{"type", "tags"}, {"tags", ta->tags}};
|
|
} else {
|
|
// Generic fallback for all other semantic annotations
|
|
entry = {{"type", a->conceptType}};
|
|
json props = propertiesToJson(a);
|
|
for (auto& [k, v] : props.items())
|
|
entry[k] = v;
|
|
}
|
|
if (!entry.empty()) annos.push_back(entry);
|
|
}
|
|
return headlessRpcResult(id,
|
|
{{"nodeId", nodeId}, {"annotations", annos}});
|
|
} else {
|
|
// Return all annotated nodes
|
|
json nodes = json::array();
|
|
for (auto* child : ast->allChildren()) {
|
|
auto annos = child->getChildren("annotations");
|
|
json annoList = json::array();
|
|
for (auto* a : annos) {
|
|
if (!isSemanticAnnotation(a->conceptType)) continue;
|
|
json entry;
|
|
if (a->conceptType == "IntentAnnotation") {
|
|
auto* ia = static_cast<IntentAnnotation*>(a);
|
|
entry = {{"type", "intent"}, {"summary", ia->summary}};
|
|
} else if (a->conceptType == "ComplexityAnnotation") {
|
|
entry = {{"type", "complexity"}};
|
|
} else if (a->conceptType == "RiskAnnotation") {
|
|
auto* ra = static_cast<RiskAnnotation*>(a);
|
|
entry = {{"type", "risk"}, {"level", ra->level}};
|
|
} else if (a->conceptType == "ContractAnnotation") {
|
|
entry = {{"type", "contract"}};
|
|
} else if (a->conceptType == "SemanticTagAnnotation") {
|
|
auto* ta = static_cast<SemanticTagAnnotation*>(a);
|
|
entry = {{"type", "tags"}, {"tags", ta->tags}};
|
|
} else {
|
|
entry = {{"type", a->conceptType}};
|
|
json props = propertiesToJson(a);
|
|
for (auto& [k, v] : props.items())
|
|
entry[k] = v;
|
|
}
|
|
if (!entry.empty()) annoList.push_back(entry);
|
|
}
|
|
if (!annoList.empty()) {
|
|
nodes.push_back({
|
|
{"nodeId", child->id},
|
|
{"name", getNodeName(child)},
|
|
{"kind", child->conceptType},
|
|
{"annotations", annoList}
|
|
});
|
|
}
|
|
}
|
|
return headlessRpcResult(id, {{"nodes", nodes}});
|
|
}
|
|
}
|
|
|
|
// --- removeSemanticAnnotation ---
|
|
if (method == "removeSemanticAnnotation") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string nodeId = params.value("nodeId", "");
|
|
std::string type = params.value("type", "");
|
|
if (nodeId.empty() || type.empty())
|
|
return headlessRpcError(id, -32602, "nodeId and type required");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
ASTNode* target = findNodeById(ast, nodeId);
|
|
if (!target)
|
|
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
|
|
|
|
std::string conceptType;
|
|
if (type == "intent") conceptType = "IntentAnnotation";
|
|
else if (type == "complexity") conceptType = "ComplexityAnnotation";
|
|
else if (type == "risk") conceptType = "RiskAnnotation";
|
|
else if (type == "contract") conceptType = "ContractAnnotation";
|
|
else if (type == "tags") conceptType = "SemanticTagAnnotation";
|
|
else return headlessRpcError(id, -32602, "Unknown type: " + type);
|
|
|
|
bool removed = false;
|
|
for (auto* a : target->getChildren("annotations")) {
|
|
if (a->conceptType == conceptType) {
|
|
target->removeChild(a);
|
|
removed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return headlessRpcResult(id, {{"removed", removed}, {"type", type}});
|
|
}
|
|
|
|
// --- getUnannotatedNodes ---
|
|
if (method == "getUnannotatedNodes") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string filterType = params.value("type", "");
|
|
bool includeHints = params.value("includeHints", false);
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
// Map filter type to conceptType
|
|
std::string filterConcept;
|
|
if (filterType == "intent") filterConcept = "IntentAnnotation";
|
|
else if (filterType == "complexity") filterConcept = "ComplexityAnnotation";
|
|
else if (filterType == "risk") filterConcept = "RiskAnnotation";
|
|
else if (filterType == "contract") filterConcept = "ContractAnnotation";
|
|
else if (filterType == "tags") filterConcept = "SemanticTagAnnotation";
|
|
|
|
json nodes = json::array();
|
|
for (auto* child : ast->allChildren()) {
|
|
// Only report Function and Variable nodes
|
|
if (child->conceptType != "Function" &&
|
|
child->conceptType != "Variable")
|
|
continue;
|
|
|
|
auto annos = child->getChildren("annotations");
|
|
bool hasTarget = false;
|
|
if (filterConcept.empty()) {
|
|
// Check for any semantic annotation
|
|
for (auto* a : annos) {
|
|
if (isSemanticAnnotation(a->conceptType)) {
|
|
hasTarget = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for (auto* a : annos) {
|
|
if (a->conceptType == filterConcept) {
|
|
hasTarget = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasTarget) {
|
|
json entry = {
|
|
{"nodeId", child->id},
|
|
{"name", getNodeName(child)},
|
|
{"kind", child->conceptType}
|
|
};
|
|
// Add static analysis hints if requested
|
|
if (includeHints) {
|
|
json hints;
|
|
// Body statement count
|
|
auto body = child->getChildren("body");
|
|
hints["bodyStatements"] = (int)body.size();
|
|
// Side effect heuristic: check for io/network calls
|
|
bool hasSideEffects = false;
|
|
// Check body statements and their descendants
|
|
std::vector<const ASTNode*> toCheck;
|
|
for (auto* stmt : body) {
|
|
toCheck.push_back(stmt);
|
|
for (auto* desc : stmt->allChildren())
|
|
toCheck.push_back(desc);
|
|
}
|
|
for (auto* node : toCheck) {
|
|
if (node->conceptType == "FunctionCall" ||
|
|
node->conceptType == "MethodCall") {
|
|
std::string callee = getNodeName(node);
|
|
if (callee.find("print") != std::string::npos ||
|
|
callee.find("write") != std::string::npos ||
|
|
callee.find("send") != std::string::npos ||
|
|
callee.find("read") != std::string::npos ||
|
|
callee.find("open") != std::string::npos)
|
|
hasSideEffects = true;
|
|
}
|
|
}
|
|
hints["sideEffectHint"] = hasSideEffects
|
|
? "possible" : "none";
|
|
entry["hints"] = hints;
|
|
}
|
|
nodes.push_back(entry);
|
|
}
|
|
}
|
|
|
|
return headlessRpcResult(id,
|
|
{{"nodes", nodes}, {"count", (int)nodes.size()}});
|
|
}
|
|
|
|
// --- setEnvironment ---
|
|
if (method == "setEnvironment") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
// Remove existing EnvironmentSpec if any
|
|
for (auto* child : ast->getChildren("environment")) {
|
|
ast->removeChild(child);
|
|
break;
|
|
}
|
|
|
|
auto* env = new EnvironmentSpec();
|
|
envSpecFromJson(env, params);
|
|
ast->addChild("environment", env);
|
|
|
|
return headlessRpcResult(id, {{"success", true},
|
|
{"envId", env->envId}});
|
|
}
|
|
|
|
// --- getEnvironment ---
|
|
if (method == "getEnvironment") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
auto envChildren = ast->getChildren("environment");
|
|
if (envChildren.empty())
|
|
return headlessRpcResult(id, {{"hasEnvironment", false}});
|
|
|
|
auto* env = static_cast<EnvironmentSpec*>(envChildren[0]);
|
|
json envJson = envSpecToJson(env);
|
|
envJson["hasEnvironment"] = true;
|
|
return headlessRpcResult(id, envJson);
|
|
}
|
|
|
|
// --- validateEnvironment ---
|
|
if (method == "validateEnvironment") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
auto envChildren = ast->getChildren("environment");
|
|
if (envChildren.empty())
|
|
return headlessRpcError(id, -32602, "No EnvironmentSpec set on module");
|
|
|
|
auto* env = static_cast<EnvironmentSpec*>(envChildren[0]);
|
|
auto capDiags = validateCapabilities(ast, env);
|
|
auto annoDiags = validateEnvAnnotations(ast, env);
|
|
|
|
json diagsJson = json::array();
|
|
for (const auto& d : capDiags) {
|
|
diagsJson.push_back({{"severity", d.severity},
|
|
{"message", d.message}, {"nodeId", d.nodeId},
|
|
{"capability", d.capability}});
|
|
}
|
|
for (const auto& d : annoDiags) {
|
|
diagsJson.push_back({{"severity", d.severity},
|
|
{"message", d.message}, {"nodeId", d.nodeId}});
|
|
}
|
|
|
|
return headlessRpcResult(id, {
|
|
{"diagnostics", diagsJson},
|
|
{"count", (int)diagsJson.size()}
|
|
});
|
|
}
|
|
|
|
// --- getLoweringHints ---
|
|
if (method == "getLoweringHints") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"]
|
|
: json::object();
|
|
std::string nodeId = params.value("nodeId", "");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
auto envChildren = ast->getChildren("environment");
|
|
if (envChildren.empty())
|
|
return headlessRpcError(id, -32602, "No EnvironmentSpec set");
|
|
|
|
auto* env = static_cast<EnvironmentSpec*>(envChildren[0]);
|
|
ASTNode* target = nodeId.empty() ? ast : findNodeById(ast, nodeId);
|
|
if (!target)
|
|
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
|
|
|
|
auto hints = getLoweringHints(target, env);
|
|
json hintsJson = json::array();
|
|
for (const auto& h : hints) {
|
|
hintsJson.push_back({{"pattern", h.pattern},
|
|
{"description", h.description}});
|
|
}
|
|
return headlessRpcResult(id, {{"hints", hintsJson},
|
|
{"count", (int)hintsJson.size()}});
|
|
}
|
|
|
|
// --- Step 317: Workflow Annotation Foundation ---
|
|
|
|
// createSkeleton — create a new skeleton module
|
|
if (method == "createSkeleton") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string name = params.value("name", "skeleton");
|
|
std::string language = params.value("language", "python");
|
|
|
|
// Create skeleton module using openBuffer with empty content
|
|
std::string bufPath = "skel_" + name;
|
|
auto* buf = state.openBuffer(bufPath, "", language);
|
|
// Replace the default empty module with a properly named skeleton
|
|
auto* rawMod = createSkeletonModule(name, language);
|
|
buf->sync.setAST(std::unique_ptr<Module>(rawMod));
|
|
buf->modified = true;
|
|
return headlessRpcResult(id, {{"bufferId", bufPath},
|
|
{"name", name}, {"language", language}});
|
|
}
|
|
|
|
// addSkeletonNode — add function/class skeleton with annotations
|
|
if (method == "addSkeletonNode") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
std::string nodeType = params.value("nodeType", "function");
|
|
std::string name = params.value("name", "unnamed");
|
|
std::vector<std::string> paramNames;
|
|
if (params.contains("parameters") && params["parameters"].is_array()) {
|
|
for (const auto& p : params["parameters"])
|
|
if (p.is_string()) paramNames.push_back(p.get<std::string>());
|
|
}
|
|
|
|
// Build annotations from params
|
|
std::vector<ASTNode*> annos;
|
|
if (params.contains("contextWidth")) {
|
|
auto* cw = new ContextWidthAnnotation();
|
|
static int cwC = 0;
|
|
cw->id = "rpc_cw_" + std::to_string(++cwC);
|
|
cw->width = params["contextWidth"].get<std::string>();
|
|
annos.push_back(cw);
|
|
}
|
|
if (params.contains("automatability")) {
|
|
auto* aa = new AutomatabilityAnnotation();
|
|
static int aaC = 0;
|
|
aa->id = "rpc_aa_" + std::to_string(++aaC);
|
|
aa->strategy = params["automatability"].get<std::string>();
|
|
annos.push_back(aa);
|
|
}
|
|
if (params.contains("priority")) {
|
|
auto* pa = new PriorityAnnotation();
|
|
static int paC = 0;
|
|
pa->id = "rpc_pa_" + std::to_string(++paC);
|
|
pa->level = params["priority"].get<std::string>();
|
|
if (params.contains("blockedBy") && params["blockedBy"].is_array()) {
|
|
for (const auto& b : params["blockedBy"])
|
|
if (b.is_string()) pa->blockedBy.push_back(b.get<std::string>());
|
|
}
|
|
annos.push_back(pa);
|
|
}
|
|
|
|
std::string nodeId;
|
|
if (nodeType == "class") {
|
|
std::vector<std::string> methods;
|
|
if (params.contains("methods") && params["methods"].is_array()) {
|
|
for (const auto& m : params["methods"])
|
|
if (m.is_string()) methods.push_back(m.get<std::string>());
|
|
}
|
|
auto* cls = addSkeletonClass(ast, name, methods, {});
|
|
nodeId = cls->id;
|
|
} else {
|
|
auto* fn = addSkeletonFunction(ast, name, paramNames, "", annos);
|
|
nodeId = fn->id;
|
|
}
|
|
|
|
state.activeBuffer->modified = true;
|
|
return headlessRpcResult(id, {{"nodeId", nodeId}, {"name", name},
|
|
{"nodeType", nodeType}});
|
|
}
|
|
|
|
// getProjectModel — skeleton summary + task list
|
|
if (method == "getProjectModel") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
auto summary = getSkeletonSummary(ast);
|
|
auto tasks = skeletonToTaskList(ast);
|
|
|
|
json tasksJson = json::array();
|
|
for (const auto& t : tasks) {
|
|
json tj;
|
|
tj["nodeId"] = t.nodeId;
|
|
tj["nodeName"] = t.nodeName;
|
|
tj["nodeType"] = t.nodeType;
|
|
tj["contextWidth"] = t.contextWidth;
|
|
tj["automatability"] = t.automatability;
|
|
tj["priority"] = t.priority;
|
|
tj["reviewRequired"] = t.reviewRequired;
|
|
tj["status"] = t.status;
|
|
if (!t.dependencies.empty())
|
|
tj["dependencies"] = t.dependencies;
|
|
tasksJson.push_back(tj);
|
|
}
|
|
|
|
return headlessRpcResult(id, {
|
|
{"totalNodes", summary.totalNodes},
|
|
{"skeletonNodes", summary.skeletonNodes},
|
|
{"implementedNodes", summary.implementedNodes},
|
|
{"tasks", tasksJson}
|
|
});
|
|
}
|
|
|
|
// inferAnnotations — run AnnotationInference on active buffer
|
|
if (method == "inferAnnotations") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.activeBuffer)
|
|
return headlessRpcError(id, -32602, "No active buffer");
|
|
Module* ast = state.activeAST();
|
|
if (!ast)
|
|
return headlessRpcError(id, -32602, "No AST available");
|
|
|
|
AnnotationInference infEngine;
|
|
auto inferred = infEngine.inferAll(ast);
|
|
|
|
json suggestionsJson = json::array();
|
|
for (const auto& inf : inferred) {
|
|
json sj;
|
|
sj["nodeId"] = inf.nodeId;
|
|
sj["annotationType"] = inf.annotationType;
|
|
if (!inf.key.empty()) sj["key"] = inf.key;
|
|
if (!inf.value.empty()) sj["value"] = inf.value;
|
|
sj["reason"] = inf.reason;
|
|
sj["confidence"] = inf.confidence;
|
|
suggestionsJson.push_back(sj);
|
|
}
|
|
|
|
return headlessRpcResult(id, {
|
|
{"suggestions", suggestionsJson},
|
|
{"count", (int)suggestionsJson.size()}
|
|
});
|
|
}
|
|
|
|
// --- createWorkflow ---
|
|
if (method == "createWorkflow") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string projectName = params.value("projectName", "");
|
|
if (projectName.empty())
|
|
return headlessRpcError(id, -32602, "Missing projectName");
|
|
if (!state.activeBuffer || !state.activeAST())
|
|
return headlessRpcError(id, -32000, "No active buffer with AST");
|
|
|
|
state.workflow = WorkflowState(projectName);
|
|
std::string bufferId = state.activeBuffer->path;
|
|
int count = state.workflow->populateFromSkeleton(state.activeAST(), bufferId);
|
|
|
|
auto stats = state.workflow->getStats();
|
|
return headlessRpcResult(id, {
|
|
{"itemCount", count},
|
|
{"phase", workflowPhaseToString(state.workflow->getPhase())},
|
|
{"stats", stats.toJson()}
|
|
});
|
|
}
|
|
|
|
// --- getWorkflowState ---
|
|
if (method == "getWorkflowState") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
|
|
auto stats = state.workflow->getStats();
|
|
return headlessRpcResult(id, {
|
|
{"phase", workflowPhaseToString(state.workflow->getPhase())},
|
|
{"stats", stats.toJson()},
|
|
{"readyCount", state.workflow->queue.readyCount()},
|
|
{"blockedCount", state.workflow->queue.blockedCount()}
|
|
});
|
|
}
|
|
|
|
// --- getReadyTasks ---
|
|
if (method == "getReadyTasks") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
|
|
auto ready = state.workflow->queue.getReady();
|
|
json items = json::array();
|
|
for (const auto& wi : ready) {
|
|
items.push_back(workItemToJson(wi));
|
|
}
|
|
return headlessRpcResult(id, {{"items", items}, {"count", (int)items.size()}});
|
|
}
|
|
|
|
// --- getWorkItem ---
|
|
if (method == "getWorkItem") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string itemId = params.value("itemId", "");
|
|
if (itemId.empty())
|
|
return headlessRpcError(id, -32602, "Missing itemId");
|
|
|
|
auto item = state.workflow->queue.getItem(itemId);
|
|
if (!item)
|
|
return headlessRpcError(id, -32602, "Work item not found");
|
|
|
|
return headlessRpcResult(id, workItemToJson(*item));
|
|
}
|
|
|
|
// --- assignTask ---
|
|
if (method == "assignTask") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string itemId = params.value("itemId", "");
|
|
std::string assignee = params.value("assignee", "");
|
|
if (itemId.empty())
|
|
return headlessRpcError(id, -32602, "Missing itemId");
|
|
|
|
auto item = state.workflow->queue.getItem(itemId);
|
|
if (!item)
|
|
return headlessRpcError(id, -32602, "Work item not found");
|
|
|
|
WorkItem updated = *item;
|
|
bool ok = transitionWorkItem(updated, WI_ASSIGNED);
|
|
if (!ok)
|
|
return headlessRpcError(id, -32000, "Cannot assign item in status: " + updated.status);
|
|
updated.assignee = assignee;
|
|
state.workflow->queue.updateItem(itemId, updated);
|
|
state.workflow->recordChange(itemId, item->status, WI_ASSIGNED,
|
|
"agent:" + sessionId);
|
|
|
|
return headlessRpcResult(id, {{"success", true}, {"item", workItemToJson(updated)}});
|
|
}
|
|
|
|
// --- completeTask ---
|
|
if (method == "completeTask") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string itemId = params.value("itemId", "");
|
|
if (itemId.empty())
|
|
return headlessRpcError(id, -32602, "Missing itemId");
|
|
|
|
auto item = state.workflow->queue.getItem(itemId);
|
|
if (!item)
|
|
return headlessRpcError(id, -32602, "Work item not found");
|
|
|
|
// Attach result
|
|
WorkItem updated = *item;
|
|
if (params.contains("result")) {
|
|
auto r = params["result"];
|
|
updated.result.generatedCode = r.value("generatedCode", "");
|
|
updated.result.confidence = r.value("confidence", 0.0f);
|
|
updated.result.reasoning = r.value("reasoning", "");
|
|
}
|
|
|
|
// Transition: must be in-progress (or review) to complete
|
|
if (updated.status == WI_IN_PROGRESS || updated.status == WI_REVIEW) {
|
|
transitionWorkItem(updated, WI_COMPLETE);
|
|
} else {
|
|
return headlessRpcError(id, -32000,
|
|
"Cannot complete item in status: " + updated.status);
|
|
}
|
|
state.workflow->queue.updateItem(itemId, updated);
|
|
state.workflow->recordChange(itemId, item->status, WI_COMPLETE,
|
|
"agent:" + sessionId);
|
|
|
|
// Re-evaluate dependencies via complete()
|
|
// (already handled internally since we set status to complete)
|
|
// Check for newly ready items
|
|
auto ready = state.workflow->queue.getReady();
|
|
json newlyReady = json::array();
|
|
for (const auto& wi : ready) {
|
|
newlyReady.push_back(workItemToJson(wi));
|
|
}
|
|
|
|
return headlessRpcResult(id, {
|
|
{"success", true},
|
|
{"newlyReady", newlyReady}
|
|
});
|
|
}
|
|
|
|
// --- rejectTask ---
|
|
if (method == "rejectTask") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string itemId = params.value("itemId", "");
|
|
std::string reason = params.value("reason", "");
|
|
if (itemId.empty())
|
|
return headlessRpcError(id, -32602, "Missing itemId");
|
|
|
|
auto item = state.workflow->queue.getItem(itemId);
|
|
if (!item)
|
|
return headlessRpcError(id, -32602, "Work item not found");
|
|
|
|
if (item->status != WI_REVIEW)
|
|
return headlessRpcError(id, -32000,
|
|
"Cannot reject item in status: " + item->status);
|
|
|
|
bool ok = state.workflow->queue.reject(itemId, reason);
|
|
if (!ok)
|
|
return headlessRpcError(id, -32000, "Reject failed");
|
|
|
|
state.workflow->recordChange(itemId, "review", WI_READY,
|
|
"human:" + sessionId, reason);
|
|
|
|
return headlessRpcResult(id, {{"success", true}});
|
|
}
|
|
|
|
// --- saveWorkflow ---
|
|
if (method == "saveWorkflow") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
|
|
std::string root = state.workspaceRoot.empty() ? "." : state.workspaceRoot;
|
|
auto sr = ::saveWorkflow(root, *state.workflow);
|
|
return headlessRpcResult(id, {
|
|
{"success", sr.success},
|
|
{"path", sr.path},
|
|
{"bytesWritten", sr.bytesWritten}
|
|
});
|
|
}
|
|
|
|
// --- routeTask ---
|
|
if (method == "routeTask") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string itemId = params.value("itemId", "");
|
|
if (itemId.empty())
|
|
return headlessRpcError(id, -32602, "Missing itemId");
|
|
|
|
auto item = state.workflow->queue.getItem(itemId);
|
|
if (!item)
|
|
return headlessRpcError(id, -32602, "Work item not found");
|
|
|
|
auto decision = state.routingEngine.route(*item);
|
|
|
|
// Apply routing to the item
|
|
WorkItem updated = *item;
|
|
updated.workerType = decision.workerType;
|
|
updated.reviewRequired = decision.reviewRequired;
|
|
state.workflow->queue.updateItem(itemId, updated);
|
|
|
|
return headlessRpcResult(id, {{"decision", decision.toJson()}});
|
|
}
|
|
|
|
// --- routeAllReady ---
|
|
if (method == "routeAllReady") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
|
|
auto ready = state.workflow->queue.getReady();
|
|
json decisions = json::array();
|
|
int routed = 0;
|
|
|
|
for (const auto& wi : ready) {
|
|
auto decision = state.routingEngine.route(wi);
|
|
WorkItem updated = wi;
|
|
updated.workerType = decision.workerType;
|
|
updated.reviewRequired = decision.reviewRequired;
|
|
state.workflow->queue.updateItem(wi.id, updated);
|
|
|
|
json entry = decision.toJson();
|
|
entry["itemId"] = wi.id;
|
|
decisions.push_back(entry);
|
|
routed++;
|
|
}
|
|
|
|
return headlessRpcResult(id, {{"routed", routed}, {"decisions", decisions}});
|
|
}
|
|
|
|
// --- executeTask ---
|
|
if (method == "executeTask") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string itemId = params.value("itemId", "");
|
|
if (itemId.empty())
|
|
return headlessRpcError(id, -32602, "Missing itemId");
|
|
|
|
auto item = state.workflow->queue.getItem(itemId);
|
|
if (!item)
|
|
return headlessRpcError(id, -32602, "Work item not found");
|
|
|
|
// Get worker for this item's type
|
|
auto* worker = state.workerRegistry.getWorker(item->workerType);
|
|
if (!worker)
|
|
return headlessRpcError(id, -32000,
|
|
"No worker for type: " + item->workerType);
|
|
|
|
// Build context
|
|
std::map<std::string, BufferInfo> bufferInfos;
|
|
for (const auto& [path, buf] : state.bufferStates) {
|
|
BufferInfo bi;
|
|
bi.path = path;
|
|
bi.content = buf->editBuf;
|
|
bi.compactAst = json::array();
|
|
bufferInfos[path] = bi;
|
|
}
|
|
|
|
WorkerContext ctx = state.contextAssembler.assembleContext(
|
|
*item, bufferInfos, item->contextWidth);
|
|
|
|
// Execute
|
|
auto result = worker->execute(*item, ctx);
|
|
|
|
// Update item with result
|
|
WorkItem updated = *item;
|
|
updated.result = result;
|
|
|
|
bool autoApproved = false;
|
|
// Deterministic/template with high confidence → auto-approve
|
|
if ((item->workerType == "deterministic" || item->workerType == "template") &&
|
|
result.confidence >= 0.8f && !item->reviewRequired) {
|
|
autoApproved = true;
|
|
}
|
|
|
|
state.workflow->queue.updateItem(itemId, updated);
|
|
|
|
return headlessRpcResult(id, {
|
|
{"result", result.toJson()},
|
|
{"workerType", item->workerType},
|
|
{"autoApproved", autoApproved}
|
|
});
|
|
}
|
|
|
|
// --- getRoutingExplanation ---
|
|
if (method == "getRoutingExplanation") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
if (!state.workflow)
|
|
return headlessRpcError(id, -32000, "No active workflow");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
std::string itemId = params.value("itemId", "");
|
|
if (itemId.empty())
|
|
return headlessRpcError(id, -32602, "Missing itemId");
|
|
|
|
auto item = state.workflow->queue.getItem(itemId);
|
|
if (!item)
|
|
return headlessRpcError(id, -32602, "Work item not found");
|
|
|
|
auto decision = state.routingEngine.route(*item);
|
|
|
|
json annotationsUsed = json::array();
|
|
if (!item->workerType.empty()) annotationsUsed.push_back("@Automatability");
|
|
if (!item->contextWidth.empty()) annotationsUsed.push_back("@ContextWidth");
|
|
if (item->reviewRequired) annotationsUsed.push_back("@Review");
|
|
|
|
json rulesApplied = json::array();
|
|
if (decision.reasoning.find("Explicit") != std::string::npos)
|
|
rulesApplied.push_back("explicit-override");
|
|
if (decision.reasoning.find("etter") != std::string::npos)
|
|
rulesApplied.push_back("getter-setter-pattern");
|
|
if (decision.reasoning.find("Cross") != std::string::npos ||
|
|
decision.reasoning.find("cross") != std::string::npos)
|
|
rulesApplied.push_back("context-width-escalation");
|
|
if (decision.reasoning.find("default") != std::string::npos ||
|
|
decision.reasoning.find("Default") != std::string::npos)
|
|
rulesApplied.push_back("default-routing");
|
|
|
|
return headlessRpcResult(id, {
|
|
{"reasoning", decision.reasoning},
|
|
{"annotationsUsed", annotationsUsed},
|
|
{"rulesApplied", rulesApplied},
|
|
{"decision", decision.toJson()}
|
|
});
|
|
}
|
|
|
|
// --- setReviewPolicy ---
|
|
if (method == "setReviewPolicy") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
auto params = request.contains("params") ? request["params"] : json::object();
|
|
if (params.contains("policy")) {
|
|
state.reviewPolicy = ReviewPolicy::fromJson(params["policy"]);
|
|
}
|
|
return headlessRpcResult(id, {{"success", true}, {"policy", state.reviewPolicy.toJson()}});
|
|
}
|
|
|
|
// --- getReviewPolicy ---
|
|
if (method == "getReviewPolicy") {
|
|
if (!AgentPermissionPolicy::canInvoke(role, method))
|
|
return headlessRpcError(id, -32031, "Role not permitted");
|
|
return headlessRpcResult(id, state.reviewPolicy.toJson());
|
|
}
|
|
|
|
return headlessRpcError(id, -32601, "Method not found");
|
|
}
|