Step 329: Routing RPC + MCP Tools (12/12 tests)
4 new RPC methods and MCP tools for routing and worker dispatch: routeTask, routeAllReady, executeTask, getRoutingExplanation. Template workers auto-approve getter/setter patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,8 @@ struct AgentPermissionPolicy {
|
||||
method == "inferAnnotations" ||
|
||||
method == "getWorkflowState" ||
|
||||
method == "getReadyTasks" ||
|
||||
method == "getWorkItem") {
|
||||
method == "getWorkItem" ||
|
||||
method == "getRoutingExplanation") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -97,7 +98,10 @@ struct AgentPermissionPolicy {
|
||||
method == "assignTask" ||
|
||||
method == "completeTask" ||
|
||||
method == "rejectTask" ||
|
||||
method == "saveWorkflow") {
|
||||
method == "saveWorkflow" ||
|
||||
method == "routeTask" ||
|
||||
method == "routeAllReady" ||
|
||||
method == "executeTask") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
|
||||
@@ -2442,5 +2442,157 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
});
|
||||
}
|
||||
|
||||
// --- 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()}
|
||||
});
|
||||
}
|
||||
|
||||
return headlessRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
#include "SkeletonAST.h"
|
||||
#include "WorkflowState.h"
|
||||
#include "WorkflowPersistence.h"
|
||||
#include "RoutingEngine.h"
|
||||
#include "WorkerRegistry.h"
|
||||
#include "ContextAssembler.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
@@ -137,6 +140,9 @@ struct HeadlessEditorState {
|
||||
std::string defaultLanguage = "python";
|
||||
bool verbose = false;
|
||||
std::optional<WorkflowState> workflow;
|
||||
RoutingEngine routingEngine;
|
||||
WorkerRegistry workerRegistry = WorkerRegistry::getDefaultRegistry();
|
||||
ContextAssembler contextAssembler;
|
||||
|
||||
// --- Buffer access ---
|
||||
|
||||
|
||||
@@ -1466,6 +1466,62 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
void registerRoutingTools() {
|
||||
// whetstone_route_task
|
||||
tools_.push_back({"whetstone_route_task",
|
||||
"Route a single work item — determine worker type, context width, "
|
||||
"budget, and review requirements based on annotations.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to route"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_route_task"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("routeTask", args);
|
||||
};
|
||||
|
||||
// whetstone_route_all_ready
|
||||
tools_.push_back({"whetstone_route_all_ready",
|
||||
"Route all ready work items in the workflow, applying routing "
|
||||
"decisions based on their annotations.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_route_all_ready"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("routeAllReady", args);
|
||||
};
|
||||
|
||||
// whetstone_execute_task
|
||||
tools_.push_back({"whetstone_execute_task",
|
||||
"Execute a work item using its assigned worker. Deterministic and "
|
||||
"template workers produce code directly; agent/human workers prepare "
|
||||
"context bundles for external invocation.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to execute"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_execute_task"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("executeTask", args);
|
||||
};
|
||||
|
||||
// whetstone_get_routing_explanation
|
||||
tools_.push_back({"whetstone_get_routing_explanation",
|
||||
"Explain why a work item was routed to a specific worker type. "
|
||||
"Returns reasoning, annotations used, and rules applied.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to explain"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_routing_explanation"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getRoutingExplanation", args);
|
||||
};
|
||||
}
|
||||
|
||||
void registerWhetstoneTools() {
|
||||
registerASTTools();
|
||||
registerAnnotationTools();
|
||||
@@ -1480,5 +1536,6 @@ private:
|
||||
registerTrainingDataTools();
|
||||
registerWorkflowTools();
|
||||
registerWorkflowExecutionTools();
|
||||
registerRoutingTools();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,10 +134,10 @@ public:
|
||||
// Rule 6: Pattern defaults (no complexity annotations available
|
||||
// through WorkItem, so use heuristics)
|
||||
|
||||
// Getter/setter/accessor → deterministic
|
||||
// Getter/setter/accessor → template
|
||||
if (isGetterSetterPattern(item.nodeName)) {
|
||||
decision.workerType = "deterministic";
|
||||
decision.reasoning = "Getter/setter pattern → deterministic";
|
||||
decision.workerType = "template";
|
||||
decision.reasoning = "Getter/setter pattern → template";
|
||||
decision.confidence = 0.9f;
|
||||
decision.agentRole = "refactor";
|
||||
return decision;
|
||||
|
||||
Reference in New Issue
Block a user