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:
Bill
2026-02-15 19:20:00 -07:00
parent 1d931abdf2
commit 6352a04836
9 changed files with 572 additions and 8 deletions

View File

@@ -1971,4 +1971,14 @@ add_executable(step328_test tests/step328_test.cpp)
target_include_directories(step328_test PRIVATE src)
target_link_libraries(step328_test PRIVATE nlohmann_json::nlohmann_json)
# Step 329: Routing RPC + MCP Tools
add_executable(step329_test tests/step329_test.cpp)
target_include_directories(step329_test PRIVATE src)
target_link_libraries(step329_test PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

View File

@@ -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;
}

View File

@@ -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");
}

View File

@@ -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 ---

View File

@@ -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();
}
};

View File

@@ -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;

View File

@@ -58,7 +58,7 @@ void test_getter_deterministic() {
TEST(getter_deterministic);
auto wi = makeItem("getName", "", "local");
auto d = engine.route(wi);
CHECK(d.workerType == "deterministic", "getter → deterministic");
CHECK(d.workerType == "template", "getter → template");
CHECK(d.agentRole == "refactor", "agent role = refactor");
PASS();
}
@@ -68,7 +68,7 @@ void test_setter_deterministic() {
TEST(setter_deterministic);
auto wi = makeItem("setValue", "", "local");
auto d = engine.route(wi);
CHECK(d.workerType == "deterministic", "setter → deterministic");
CHECK(d.workerType == "template", "setter → template");
PASS();
}
@@ -113,7 +113,7 @@ void test_batch_routing() {
};
auto decisions = engine.routeBatch(items);
CHECK(decisions.size() == 3, "3 decisions");
CHECK(decisions[0].workerType == "deterministic", "first=deterministic");
CHECK(decisions[0].workerType == "template", "first=template");
CHECK(decisions[1].workerType == "llm", "second=llm");
CHECK(decisions[2].workerType == "human", "third=human");
PASS();

View File

@@ -0,0 +1,307 @@
// Step 329: Routing RPC + MCP Tools (12 tests)
// Tests routeTask, routeAllReady, executeTask, getRoutingExplanation,
// Linter restrictions, MCP registration.
#include "HeadlessEditorState.h"
#include "HeadlessAgentRPCHandler.h"
#include "MCPServer.h"
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
using json = nlohmann::json;
static int passed = 0, failed = 0;
#define TEST(name) { std::cout << " " << #name << "... "; }
#define PASS() { std::cout << "PASS\n"; ++passed; }
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
static json rpcCall(HeadlessEditorState& state, const std::string& method,
const json& params = json::object(),
const std::string& session = "test-session") {
json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method}};
if (!params.empty()) req["params"] = params;
return handleHeadlessAgentRequest(state, req, session);
}
// Setup: state with skeleton workflow
static HeadlessEditorState makeState() {
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("test-session", AgentRole::Generator);
auto* mod = createSkeletonModule("proj", "python");
addSkeletonFunction(mod, "getName", {}, "str"); // getter → deterministic
addSkeletonFunction(mod, "processData", {"x"}, "int"); // local → slm
addSkeletonFunction(mod, "orchestrate", {"ctx"}, "void"); // will set to project
state.openBuffer("test", "# skeleton", "python");
state.activeBuffer->sync.setAST(std::unique_ptr<Module>(mod));
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
return state;
}
// 1. routeTask returns valid decision
void test_route_task() {
TEST(route_task);
auto state = makeState();
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string itemId = ready[0]["id"].get<std::string>();
auto resp = rpcCall(state, "routeTask", {{"itemId", itemId}});
CHECK(resp.contains("result"), "has result");
auto decision = resp["result"]["decision"];
CHECK(decision.contains("workerType"), "has workerType");
CHECK(decision.contains("contextWidth"), "has contextWidth");
CHECK(decision.contains("contextBudgetTokens"), "has budget");
CHECK(decision.contains("reasoning"), "has reasoning");
PASS();
}
// 2. routeAllReady batch processes queue
void test_route_all_ready() {
TEST(route_all_ready);
auto state = makeState();
auto resp = rpcCall(state, "routeAllReady");
CHECK(resp.contains("result"), "has result");
CHECK(resp["result"]["routed"].get<int>() == 3, "3 items routed");
CHECK(resp["result"]["decisions"].size() == 3, "3 decisions");
PASS();
}
// 3. executeTask runs deterministic worker — produces code
void test_execute_deterministic() {
TEST(execute_deterministic);
auto state = makeState();
// Route all first to set worker types
rpcCall(state, "routeAllReady");
// Find the getter item (should be deterministic)
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string getterId;
for (const auto& item : ready) {
if (item["nodeName"] == "getName") {
getterId = item["id"].get<std::string>();
break;
}
}
CHECK(!getterId.empty(), "found getter item");
auto resp = rpcCall(state, "executeTask", {{"itemId", getterId}});
CHECK(resp.contains("result"), "has result: " + resp.dump().substr(0, 200));
CHECK(resp["result"]["workerType"] == "template", "template worker");
CHECK(!resp["result"]["result"]["generatedCode"].get<std::string>().empty(),
"produced code");
PASS();
}
// 4. Agent items marked as prepared, not executed
void test_agent_prepared() {
TEST(agent_prepared);
auto state = makeState();
rpcCall(state, "routeAllReady");
// Find an SLM-routed item
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string slmId;
for (const auto& item : ready) {
if (item["workerType"] == "slm") {
slmId = item["id"].get<std::string>();
break;
}
}
CHECK(!slmId.empty(), "found slm item");
auto resp = rpcCall(state, "executeTask", {{"itemId", slmId}});
CHECK(resp.contains("result"), "has result");
CHECK(resp["result"]["result"]["confidence"].get<float>() == 0.0f,
"deferred confidence");
CHECK(resp["result"]["result"]["reasoning"].get<std::string>().find("MCP") !=
std::string::npos, "mentions MCP");
PASS();
}
// 5. Human items marked for review
void test_human_marked() {
TEST(human_marked);
auto state = makeState();
// Manually set an item to human worker type
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string itemId = ready[0]["id"].get<std::string>();
auto item = *state.workflow->queue.getItem(itemId);
item.workerType = "human";
state.workflow->queue.updateItem(itemId, item);
auto resp = rpcCall(state, "executeTask", {{"itemId", itemId}});
CHECK(resp.contains("result"), "has result");
CHECK(resp["result"]["workerType"] == "human", "human worker");
CHECK(resp["result"]["result"]["reasoning"].get<std::string>().find("human") !=
std::string::npos, "mentions human");
PASS();
}
// 6. Routing explanation includes annotation references
void test_routing_explanation() {
TEST(routing_explanation);
auto state = makeState();
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string itemId = ready[0]["id"].get<std::string>();
auto resp = rpcCall(state, "getRoutingExplanation", {{"itemId", itemId}});
CHECK(resp.contains("result"), "has result");
CHECK(resp["result"].contains("reasoning"), "has reasoning");
CHECK(resp["result"].contains("annotationsUsed"), "has annotationsUsed");
CHECK(resp["result"].contains("rulesApplied"), "has rulesApplied");
CHECK(resp["result"].contains("decision"), "has decision");
PASS();
}
// 7. Linter can read explanation but not route
void test_linter_restrictions() {
TEST(linter_restrictions);
auto state = makeState();
state.setAgentRole("lint", AgentRole::Linter);
auto ready = rpcCall(state, "getReadyTasks", {}, "lint")["result"]["items"];
std::string itemId = ready[0]["id"].get<std::string>();
// Read: should work
auto resp1 = rpcCall(state, "getRoutingExplanation",
{{"itemId", itemId}}, "lint");
CHECK(resp1.contains("result"), "Linter can read explanation");
// Mutate: should fail
auto resp2 = rpcCall(state, "routeTask", {{"itemId", itemId}}, "lint");
CHECK(resp2.contains("error"), "Linter cannot route");
auto resp3 = rpcCall(state, "executeTask", {{"itemId", itemId}}, "lint");
CHECK(resp3.contains("error"), "Linter cannot execute");
PASS();
}
// 8. MCP tool registration (4 new routing tools)
void test_mcp_registration() {
TEST(mcp_registration);
MCPServer server;
const auto& tools = server.getTools();
int routingCount = 0;
for (const auto& t : tools) {
if (t.name == "whetstone_route_task" ||
t.name == "whetstone_route_all_ready" ||
t.name == "whetstone_execute_task" ||
t.name == "whetstone_get_routing_explanation") {
routingCount++;
}
}
CHECK(routingCount == 4, "4 routing tools, got " + std::to_string(routingCount));
CHECK((int)tools.size() >= 54, "54+ total tools, got " + std::to_string(tools.size()));
PASS();
}
// 9. routeTask updates item workerType
void test_route_updates_item() {
TEST(route_updates_item);
auto state = makeState();
// Find getter item
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string getterId;
for (const auto& item : ready) {
if (item["nodeName"] == "getName") {
getterId = item["id"].get<std::string>();
break;
}
}
CHECK(!getterId.empty(), "found getter");
rpcCall(state, "routeTask", {{"itemId", getterId}});
auto itemResp = rpcCall(state, "getWorkItem", {{"itemId", getterId}});
CHECK(itemResp["result"]["workerType"] == "template",
"workerType updated to template");
PASS();
}
// 10. executeTask with no worker returns error
void test_execute_no_worker() {
TEST(execute_no_worker);
auto state = makeState();
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string itemId = ready[0]["id"].get<std::string>();
// Set to unknown worker type
auto item = *state.workflow->queue.getItem(itemId);
item.workerType = "quantum";
state.workflow->queue.updateItem(itemId, item);
auto resp = rpcCall(state, "executeTask", {{"itemId", itemId}});
CHECK(resp.contains("error"), "error for unknown worker");
PASS();
}
// 11. routeAllReady returns decisions with itemIds
void test_route_all_has_item_ids() {
TEST(route_all_has_item_ids);
auto state = makeState();
auto resp = rpcCall(state, "routeAllReady");
for (const auto& d : resp["result"]["decisions"]) {
CHECK(d.contains("itemId"), "decision has itemId");
CHECK(d.contains("workerType"), "decision has workerType");
}
PASS();
}
// 12. executeTask autoApproved for deterministic with high confidence
void test_auto_approved() {
TEST(auto_approved);
auto state = makeState();
rpcCall(state, "routeAllReady");
// Find deterministic (getter) item
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
std::string getterId;
for (const auto& item : ready) {
if (item["workerType"] == "template") {
getterId = item["id"].get<std::string>();
break;
}
}
CHECK(!getterId.empty(), "found template item");
auto resp = rpcCall(state, "executeTask", {{"itemId", getterId}});
CHECK(resp.contains("result"), "has result");
// Template/deterministic with high confidence → autoApproved
CHECK(resp["result"]["autoApproved"].get<bool>(), "auto approved");
PASS();
}
int main() {
std::cout << "=== Step 329: Routing RPC + MCP Tools ===\n";
try {
test_route_task();
test_route_all_ready();
test_execute_deterministic();
test_agent_prepared();
test_human_marked();
test_routing_explanation();
test_linter_restrictions();
test_mcp_registration();
test_route_updates_item();
test_execute_no_worker();
test_route_all_has_item_ids();
test_auto_approved();
} catch (const std::exception& e) {
std::cout << "EXCEPTION: " << e.what() << "\n";
++failed;
}
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -1393,6 +1393,34 @@ siblings > buffer content > project summaries under truncation.
**Files modified:**
- `editor/CMakeLists.txt` — step328_test target
### Step 329: Routing RPC + MCP Tools
**Status:** PASS (12/12 tests)
Exposes routing and worker dispatch through 4 RPC methods and 4 MCP tools.
routeTask/routeAllReady apply routing decisions, executeTask runs workers
(deterministic/template produce code, agent/human prepare context bundles),
getRoutingExplanation shows reasoning.
**Files created:**
- `editor/tests/step329_test.cpp` — 12 tests: routeTask, routeAllReady batch,
execute deterministic, agent prepared, human marked, routing explanation,
Linter restrictions, MCP registration (4 new, 54+ total), route updates item,
no-worker error, itemIds in batch, auto-approved for template
**Files modified:**
- `editor/src/HeadlessEditorState.h` — added RoutingEngine, WorkerRegistry,
ContextAssembler members
- `editor/src/HeadlessAgentRPCHandler.h` — 4 new RPC methods: routeTask,
routeAllReady, executeTask, getRoutingExplanation
- `editor/src/AgentPermissionPolicy.h` — read-only: getRoutingExplanation;
mutation: routeTask, routeAllReady, executeTask
- `editor/src/MCPServer.h` — registerRoutingTools() with 4 tools
- `editor/src/RoutingEngine.h` — getter/setter routes to "template" (not
"deterministic") for correct auto-approve with TemplateWorker
- `editor/CMakeLists.txt` — step329_test target
**Tool count:** 54+ (50 existing + 4 routing tools)
---
# Roadmap Planning — Sprints 12-25+