Step 382: add orchestrator RPC and MCP tools
This commit is contained in:
@@ -2317,4 +2317,13 @@ target_link_libraries(step381_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step382_test tests/step382_test.cpp)
|
||||
target_include_directories(step382_test PRIVATE src)
|
||||
target_link_libraries(step382_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)
|
||||
|
||||
@@ -70,7 +70,9 @@ struct AgentPermissionPolicy {
|
||||
method == "getReadyTasks" ||
|
||||
method == "getWorkItem" ||
|
||||
method == "getRoutingExplanation" ||
|
||||
method == "getReviewPolicy") {
|
||||
method == "getReviewPolicy" ||
|
||||
method == "getBlockers" ||
|
||||
method == "getProgress") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -103,7 +105,11 @@ struct AgentPermissionPolicy {
|
||||
method == "routeTask" ||
|
||||
method == "routeAllReady" ||
|
||||
method == "executeTask" ||
|
||||
method == "setReviewPolicy") {
|
||||
method == "setReviewPolicy" ||
|
||||
method == "orchestrateStep" ||
|
||||
method == "orchestrateAdvance" ||
|
||||
method == "orchestrateRunDeterministic" ||
|
||||
method == "submitExternalResult") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// Deliberately omits GUI-only methods. Stays under 600 lines.
|
||||
|
||||
struct HeadlessEditorState;
|
||||
#include "HeadlessOrchestratorRPC.h"
|
||||
|
||||
// Re-use the response helpers (same signature as AgentRPCHandler.h)
|
||||
static inline json headlessRpcError(const json& id, int code,
|
||||
@@ -2260,6 +2261,7 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
state.workflow = WorkflowState(projectName);
|
||||
std::string bufferId = state.activeBuffer->path;
|
||||
int count = state.workflow->populateFromSkeleton(state.activeAST(), bufferId);
|
||||
state.workflowProgress = WorkflowProgress(count);
|
||||
|
||||
auto stats = state.workflow->getStats();
|
||||
return headlessRpcResult(id, {
|
||||
@@ -2612,5 +2614,10 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
return headlessRpcResult(id, state.reviewPolicy.toJson());
|
||||
}
|
||||
|
||||
if (auto routed = tryHandleHeadlessOrchestratorRPC(
|
||||
state, request, id, method, role); routed.has_value()) {
|
||||
return routed.value();
|
||||
}
|
||||
|
||||
return headlessRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "WorkerRegistry.h"
|
||||
#include "ContextAssembler.h"
|
||||
#include "ReviewGate.h"
|
||||
#include "WorkflowProgress.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
@@ -141,6 +142,7 @@ struct HeadlessEditorState {
|
||||
std::string defaultLanguage = "python";
|
||||
bool verbose = false;
|
||||
std::optional<WorkflowState> workflow;
|
||||
std::optional<WorkflowProgress> workflowProgress;
|
||||
RoutingEngine routingEngine;
|
||||
WorkerRegistry workerRegistry = WorkerRegistry::getDefaultRegistry();
|
||||
ContextAssembler contextAssembler;
|
||||
|
||||
287
editor/src/HeadlessOrchestratorRPC.h
Normal file
287
editor/src/HeadlessOrchestratorRPC.h
Normal file
@@ -0,0 +1,287 @@
|
||||
#pragma once
|
||||
// Step 382: Orchestrator RPC surface for headless agent requests.
|
||||
|
||||
#include "HeadlessEditorState.h"
|
||||
#include <optional>
|
||||
|
||||
static inline json orchestratorRpcError(const json& id, int code,
|
||||
const std::string& msg) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"error", {{"code", code}, {"message", msg}}}};
|
||||
}
|
||||
|
||||
static inline json orchestratorRpcResult(const json& id, const json& result) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id}, {"result", result}};
|
||||
}
|
||||
|
||||
static inline json orchestratorEventToJson(const OrchestratorEvent& event) {
|
||||
return {
|
||||
{"type", event.type},
|
||||
{"itemId", event.itemId},
|
||||
{"detail", event.detail},
|
||||
{"timestamp", event.timestamp}
|
||||
};
|
||||
}
|
||||
|
||||
static inline json orchestratorEventsToJson(
|
||||
const std::vector<OrchestratorEvent>& events) {
|
||||
json arr = json::array();
|
||||
for (const auto& event : events) arr.push_back(orchestratorEventToJson(event));
|
||||
return arr;
|
||||
}
|
||||
|
||||
static inline json orchestratorBlockersToJson(const std::vector<BlockerInfo>& blockers) {
|
||||
json arr = json::array();
|
||||
for (const auto& b : blockers) {
|
||||
arr.push_back({
|
||||
{"type", b.type},
|
||||
{"itemIds", b.itemIds},
|
||||
{"description", b.description}
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
static inline std::map<std::string, BufferInfo> collectOrchestratorBufferInfos(
|
||||
HeadlessEditorState& state) {
|
||||
std::map<std::string, BufferInfo> infos;
|
||||
for (const auto& [path, buf] : state.bufferStates) {
|
||||
BufferInfo bi;
|
||||
bi.path = path;
|
||||
bi.content = buf->editBuf;
|
||||
Module* ast = buf->sync.getAST();
|
||||
bi.compactAst = ast ? toJsonCompactSummary(ast) : json::array();
|
||||
bi.importGraph = json::object();
|
||||
infos[path] = std::move(bi);
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
inline std::optional<json> tryHandleHeadlessOrchestratorRPC(
|
||||
HeadlessEditorState& state,
|
||||
const json& request,
|
||||
const json& id,
|
||||
const std::string& method,
|
||||
AgentRole role) {
|
||||
if (method == "orchestrateStep") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return orchestratorRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return orchestratorRpcError(id, -32000, "No active workflow");
|
||||
|
||||
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
|
||||
state.workerRegistry, state.contextAssembler,
|
||||
state.reviewGate);
|
||||
orchestrator.setReviewPolicy(state.reviewPolicy);
|
||||
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
|
||||
|
||||
OrchestratorEvent event = orchestrator.step();
|
||||
if (!state.workflowProgress) {
|
||||
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
|
||||
}
|
||||
state.workflowProgress->recordEvent(event);
|
||||
return orchestratorRpcResult(id, {{"event", orchestratorEventToJson(event)}});
|
||||
}
|
||||
|
||||
if (method == "orchestrateAdvance") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return orchestratorRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return orchestratorRpcError(id, -32000, "No active workflow");
|
||||
|
||||
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
|
||||
state.workerRegistry, state.contextAssembler,
|
||||
state.reviewGate);
|
||||
orchestrator.setReviewPolicy(state.reviewPolicy);
|
||||
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
|
||||
|
||||
BatchResult batch = orchestrator.advanceBatch();
|
||||
if (!state.workflowProgress) {
|
||||
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
|
||||
}
|
||||
for (const auto& event : batch.events) {
|
||||
state.workflowProgress->recordEvent(event);
|
||||
}
|
||||
|
||||
return orchestratorRpcResult(id, {
|
||||
{"events", orchestratorEventsToJson(batch.events)},
|
||||
{"itemsAdvanced", batch.itemsAdvanced},
|
||||
{"itemsBlocked", batch.itemsBlocked},
|
||||
{"contextTokensSaved", batch.contextTokensSaved}
|
||||
});
|
||||
}
|
||||
|
||||
if (method == "orchestrateRunDeterministic") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return orchestratorRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return orchestratorRpcError(id, -32000, "No active workflow");
|
||||
|
||||
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
|
||||
state.workerRegistry, state.contextAssembler,
|
||||
state.reviewGate);
|
||||
orchestrator.setReviewPolicy(state.reviewPolicy);
|
||||
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
|
||||
|
||||
std::vector<OrchestratorEvent> allEvents;
|
||||
int totalAdvanced = 0;
|
||||
int totalBlocked = 0;
|
||||
int totalSaved = 0;
|
||||
while (true) {
|
||||
BatchResult batch = orchestrator.advanceBatch();
|
||||
if (batch.events.empty()) break;
|
||||
bool anyProgress = false;
|
||||
for (const auto& event : batch.events) {
|
||||
if (event.type != "blocked") anyProgress = true;
|
||||
allEvents.push_back(event);
|
||||
}
|
||||
totalAdvanced += batch.itemsAdvanced;
|
||||
totalBlocked += batch.itemsBlocked;
|
||||
totalSaved += batch.contextTokensSaved;
|
||||
if (!anyProgress) break;
|
||||
}
|
||||
|
||||
if (!state.workflowProgress) {
|
||||
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
|
||||
}
|
||||
for (const auto& event : allEvents) {
|
||||
state.workflowProgress->recordEvent(event);
|
||||
}
|
||||
|
||||
return orchestratorRpcResult(id, {
|
||||
{"events", orchestratorEventsToJson(allEvents)},
|
||||
{"itemsAdvanced", totalAdvanced},
|
||||
{"itemsBlocked", totalBlocked},
|
||||
{"contextTokensSaved", totalSaved},
|
||||
{"stats", state.workflow->getStats().toJson()},
|
||||
{"blockers", orchestratorBlockersToJson(orchestrator.getBlockers())}
|
||||
});
|
||||
}
|
||||
|
||||
if (method == "getBlockers") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return orchestratorRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return orchestratorRpcError(id, -32000, "No active workflow");
|
||||
|
||||
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
|
||||
state.workerRegistry, state.contextAssembler,
|
||||
state.reviewGate);
|
||||
orchestrator.setReviewPolicy(state.reviewPolicy);
|
||||
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
|
||||
auto blockers = orchestrator.getBlockers();
|
||||
return orchestratorRpcResult(id, {
|
||||
{"blockers", orchestratorBlockersToJson(blockers)},
|
||||
{"count", (int)blockers.size()}
|
||||
});
|
||||
}
|
||||
|
||||
if (method == "getProgress") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return orchestratorRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return orchestratorRpcError(id, -32000, "No active workflow");
|
||||
|
||||
if (!state.workflowProgress) {
|
||||
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
|
||||
} else {
|
||||
state.workflowProgress->setTotalItems(state.workflow->getStats().total);
|
||||
}
|
||||
ProgressSnapshot snapshot = state.workflowProgress->getSnapshot();
|
||||
if (snapshot.blockers.empty()) {
|
||||
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
|
||||
state.workerRegistry, state.contextAssembler,
|
||||
state.reviewGate);
|
||||
orchestrator.setReviewPolicy(state.reviewPolicy);
|
||||
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
|
||||
snapshot.blockers = orchestrator.getBlockers();
|
||||
}
|
||||
return orchestratorRpcResult(id, snapshot.toJson());
|
||||
}
|
||||
|
||||
if (method == "submitExternalResult") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return orchestratorRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return orchestratorRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return orchestratorRpcError(id, -32602, "Missing itemId");
|
||||
if (!params.contains("result") || !params["result"].is_object())
|
||||
return orchestratorRpcError(id, -32602, "Missing result payload");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return orchestratorRpcError(id, -32602, "Work item not found");
|
||||
if (!(item->workerType == "slm" || item->workerType == "llm"))
|
||||
return orchestratorRpcError(id, -32000, "submitExternalResult only supports slm/llm items");
|
||||
if (item->status != WI_IN_PROGRESS && item->status != WI_ASSIGNED)
|
||||
return orchestratorRpcError(id, -32000,
|
||||
"Item must be assigned or in-progress for external submission");
|
||||
|
||||
WorkItem updated = *item;
|
||||
if (updated.status == WI_ASSIGNED) transitionWorkItem(updated, WI_IN_PROGRESS);
|
||||
|
||||
const auto& result = params["result"];
|
||||
updated.result.generatedCode = result.value("generatedCode", "");
|
||||
updated.result.confidence = result.value("confidence", 0.0f);
|
||||
updated.result.reasoning = result.value("reasoning", "");
|
||||
updated.result.tokensGenerated = result.value("tokensGenerated", 0);
|
||||
updated.result.tokensBudget = result.value("tokensBudget",
|
||||
estimateContextBudget(updated.contextWidth));
|
||||
if (result.contains("astJson")) updated.result.astJson = result["astJson"];
|
||||
if (result.contains("diagnostics") && result["diagnostics"].is_array()) {
|
||||
updated.result.diagnostics = result["diagnostics"].get<std::vector<json>>();
|
||||
}
|
||||
|
||||
state.workflow->queue.updateItem(itemId, updated);
|
||||
|
||||
std::vector<OrchestratorEvent> events;
|
||||
events.push_back({
|
||||
"executed",
|
||||
itemId,
|
||||
{{"workerType", updated.workerType},
|
||||
{"confidence", updated.result.confidence},
|
||||
{"tokensGenerated", updated.result.tokensGenerated}},
|
||||
workItemTimestamp()
|
||||
});
|
||||
|
||||
ReviewDecision reviewDecision =
|
||||
state.reviewGate.shouldAutoApprove(updated, updated.result, state.reviewPolicy);
|
||||
if (reviewDecision.approved) {
|
||||
state.workflow->queue.complete(itemId);
|
||||
events.push_back({"auto-approved", itemId,
|
||||
{{"rule", reviewDecision.ruleMatched}},
|
||||
workItemTimestamp()});
|
||||
events.push_back({"completed", itemId, json::object(), workItemTimestamp()});
|
||||
} else {
|
||||
auto maybeUpdated = state.workflow->queue.getItem(itemId);
|
||||
if (!maybeUpdated) {
|
||||
return orchestratorRpcError(id, -32000, "Failed to reload item");
|
||||
}
|
||||
WorkItem reviewItem = *maybeUpdated;
|
||||
transitionWorkItem(reviewItem, WI_REVIEW);
|
||||
state.workflow->queue.updateItem(itemId, reviewItem);
|
||||
events.push_back({"sent-to-review", itemId,
|
||||
{{"reason", reviewDecision.reasoning}},
|
||||
workItemTimestamp()});
|
||||
}
|
||||
|
||||
if (!state.workflowProgress) {
|
||||
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
|
||||
}
|
||||
for (const auto& event : events) {
|
||||
state.workflowProgress->recordEvent(event);
|
||||
}
|
||||
|
||||
auto latest = state.workflow->queue.getItem(itemId);
|
||||
return orchestratorRpcResult(id, {
|
||||
{"success", true},
|
||||
{"events", orchestratorEventsToJson(events)},
|
||||
{"item", latest ? workItemToJson(*latest) : json::object()}
|
||||
});
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -1522,6 +1522,86 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
void registerOrchestratorTools() {
|
||||
// whetstone_orchestrate_step
|
||||
tools_.push_back({"whetstone_orchestrate_step",
|
||||
"Advance one ready workflow item through the orchestration loop. "
|
||||
"Returns one orchestrator event (routed/executed/completed/blocked).",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_orchestrate_step"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("orchestrateStep", args);
|
||||
};
|
||||
|
||||
// whetstone_orchestrate_advance
|
||||
tools_.push_back({"whetstone_orchestrate_advance",
|
||||
"Advance all currently ready workflow items in one batch. "
|
||||
"Returns event stream and batch counters.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_orchestrate_advance"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("orchestrateAdvance", args);
|
||||
};
|
||||
|
||||
// whetstone_orchestrate_run_deterministic
|
||||
tools_.push_back({"whetstone_orchestrate_run_deterministic",
|
||||
"Run deterministic/template workflow items to completion. "
|
||||
"Stops automatically at human or external-model blockers.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_orchestrate_run_deterministic"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("orchestrateRunDeterministic", args);
|
||||
};
|
||||
|
||||
// whetstone_get_blockers
|
||||
tools_.push_back({"whetstone_get_blockers",
|
||||
"Get current workflow blockers grouped by type: needs-human, "
|
||||
"needs-review, and needs-external-model.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_blockers"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getBlockers", args);
|
||||
};
|
||||
|
||||
// whetstone_get_progress
|
||||
tools_.push_back({"whetstone_get_progress",
|
||||
"Get workflow progress snapshot: completion %, throughput, ETA, "
|
||||
"worker stats, and active blockers.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_progress"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getProgress", args);
|
||||
};
|
||||
|
||||
// whetstone_submit_result
|
||||
tools_.push_back({"whetstone_submit_result",
|
||||
"Submit external SLM/LLM output for a prepared workflow item. "
|
||||
"Orchestrator applies review gate and advances lifecycle.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Workflow item ID receiving external output"}}},
|
||||
{"result", {{"type", "object"}, {"properties", {
|
||||
{"generatedCode", {{"type", "string"}}},
|
||||
{"confidence", {{"type", "number"}}},
|
||||
{"reasoning", {{"type", "string"}}},
|
||||
{"tokensGenerated", {{"type", "integer"}}},
|
||||
{"tokensBudget", {{"type", "integer"}}},
|
||||
{"astJson", {{"type", "object"}}},
|
||||
{"diagnostics", {{"type", "array"}, {"items", {{"type", "object"}}}}}
|
||||
}}}}
|
||||
}}, {"required", json::array({"itemId", "result"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_submit_result"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("submitExternalResult", args);
|
||||
};
|
||||
}
|
||||
|
||||
void registerReviewTools() {
|
||||
// whetstone_set_review_policy
|
||||
tools_.push_back({"whetstone_set_review_policy",
|
||||
@@ -1566,6 +1646,7 @@ private:
|
||||
registerWorkflowTools();
|
||||
registerWorkflowExecutionTools();
|
||||
registerRoutingTools();
|
||||
registerOrchestratorTools();
|
||||
registerReviewTools();
|
||||
}
|
||||
};
|
||||
|
||||
250
editor/tests/step382_test.cpp
Normal file
250
editor/tests/step382_test.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
// Step 382: Orchestrator RPC + MCP tools (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "HeadlessEditorState.h"
|
||||
#include "HeadlessAgentRPCHandler.h"
|
||||
#include "MCPServer.h"
|
||||
|
||||
static WorkItem makeItem(const std::string& id,
|
||||
const std::string& name,
|
||||
const std::string& workerType = "",
|
||||
const std::string& contextWidth = "local") {
|
||||
WorkItem item;
|
||||
item.id = id;
|
||||
item.nodeId = id + "_node";
|
||||
item.nodeName = name;
|
||||
item.nodeType = "Function";
|
||||
item.bufferId = "main.py";
|
||||
item.workerType = workerType;
|
||||
item.contextWidth = contextWidth;
|
||||
item.priority = "medium";
|
||||
item.status = WI_PENDING;
|
||||
item.createdAt = workItemTimestamp();
|
||||
return item;
|
||||
}
|
||||
|
||||
static HeadlessEditorState makeState(AgentRole role = AgentRole::Generator) {
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("main.py", "def a():\n return 1\n", "python");
|
||||
state.setAgentRole("test-session", role);
|
||||
state.workflow = WorkflowState("wf");
|
||||
return state;
|
||||
}
|
||||
|
||||
static json rpc(HeadlessEditorState& state, const std::string& method,
|
||||
const json& params = json::object()) {
|
||||
json request = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method},
|
||||
{"params", params}};
|
||||
return handleHeadlessAgentRequest(state, request, "test-session");
|
||||
}
|
||||
|
||||
static json mcpCall(MCPServer& server, const std::string& method,
|
||||
const json& params = json::object()) {
|
||||
json request = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method},
|
||||
{"params", params}};
|
||||
return server.handleRequest(request);
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
// Test 1: orchestrateStep advances one template task
|
||||
{
|
||||
auto state = makeState();
|
||||
state.workflow->queue.enqueue(makeItem("t1", "getValue", "template"));
|
||||
auto resp = rpc(state, "orchestrateStep");
|
||||
assert(resp.contains("result"));
|
||||
assert(resp["result"]["event"]["type"] == "completed");
|
||||
std::cout << "Test 1 PASSED: orchestrateStep advances one task\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: orchestrateAdvance processes a batch of ready items
|
||||
{
|
||||
auto state = makeState();
|
||||
state.workflow->queue.enqueue(makeItem("a", "getA", "template"));
|
||||
state.workflow->queue.enqueue(makeItem("b", "getB", "template"));
|
||||
auto resp = rpc(state, "orchestrateAdvance");
|
||||
assert(resp.contains("result"));
|
||||
assert(resp["result"]["itemsAdvanced"].get<int>() == 2);
|
||||
std::cout << "Test 2 PASSED: orchestrateAdvance handles batches\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: orchestrateRunDeterministic stops at blockers
|
||||
{
|
||||
auto state = makeState();
|
||||
state.workflow->queue.enqueue(makeItem("d1", "getFast", "template"));
|
||||
state.workflow->queue.enqueue(makeItem("h1", "manual", "human"));
|
||||
auto resp = rpc(state, "orchestrateRunDeterministic");
|
||||
assert(resp.contains("result"));
|
||||
assert(resp["result"]["stats"]["complete"].get<int>() == 1);
|
||||
assert(!resp["result"]["blockers"].empty());
|
||||
std::cout << "Test 3 PASSED: runDeterministic stops at blockers\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: getBlockers reports human items
|
||||
{
|
||||
auto state = makeState();
|
||||
state.workflow->queue.enqueue(makeItem("h1", "manual", "human"));
|
||||
rpc(state, "orchestrateStep"); // move to blocked human state
|
||||
auto blockers = rpc(state, "getBlockers");
|
||||
assert(blockers.contains("result"));
|
||||
assert(blockers["result"]["count"].get<int>() >= 1);
|
||||
std::cout << "Test 4 PASSED: getBlockers reports workflow blockers\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: getProgress returns valid snapshot fields
|
||||
{
|
||||
auto state = makeState();
|
||||
state.workflow->queue.enqueue(makeItem("t1", "getV", "template"));
|
||||
rpc(state, "orchestrateStep");
|
||||
auto resp = rpc(state, "getProgress");
|
||||
assert(resp.contains("result"));
|
||||
assert(resp["result"].contains("completionPercent"));
|
||||
assert(resp["result"].contains("itemsPerMinute"));
|
||||
assert(resp["result"].contains("byWorkerType"));
|
||||
std::cout << "Test 5 PASSED: getProgress returns snapshot\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: submitExternalResult accepts LLM output and advances task
|
||||
{
|
||||
auto state = makeState();
|
||||
state.reviewPolicy.defaultAction = "auto-approve";
|
||||
WorkItem llm = makeItem("x1", "buildGraph", "", "project");
|
||||
state.workflow->queue.enqueue(llm);
|
||||
rpc(state, "orchestrateStep"); // routes to llm, blocks in-progress
|
||||
auto submit = rpc(state, "submitExternalResult", {
|
||||
{"itemId", "x1"},
|
||||
{"result", {
|
||||
{"generatedCode", "def buildGraph(data):\n return {}"},
|
||||
{"confidence", 0.92},
|
||||
{"reasoning", "external model result"},
|
||||
{"tokensGenerated", 40}
|
||||
}}
|
||||
});
|
||||
assert(submit.contains("result"));
|
||||
assert(submit["result"]["success"].get<bool>());
|
||||
auto item = state.workflow->queue.getItem("x1");
|
||||
assert(item.has_value());
|
||||
assert(item->status == WI_COMPLETE);
|
||||
std::cout << "Test 6 PASSED: submitExternalResult advances agent task\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: linter can read progress but cannot orchestrate
|
||||
{
|
||||
auto state = makeState(AgentRole::Linter);
|
||||
state.workflow->queue.enqueue(makeItem("l1", "getName", "template"));
|
||||
auto progress = rpc(state, "getProgress");
|
||||
auto step = rpc(state, "orchestrateStep");
|
||||
assert(progress.contains("result"));
|
||||
assert(step.contains("error"));
|
||||
std::cout << "Test 7 PASSED: linter read-only orchestration access\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 8: MCP tool registration includes orchestrator tools
|
||||
{
|
||||
MCPServer server;
|
||||
const auto& tools = server.getTools();
|
||||
std::vector<std::string> needed = {
|
||||
"whetstone_orchestrate_step",
|
||||
"whetstone_orchestrate_advance",
|
||||
"whetstone_orchestrate_run_deterministic",
|
||||
"whetstone_get_blockers",
|
||||
"whetstone_get_progress",
|
||||
"whetstone_submit_result"
|
||||
};
|
||||
int found = 0;
|
||||
for (const auto& t : tools) {
|
||||
for (const auto& n : needed) {
|
||||
if (t.name == n) found++;
|
||||
}
|
||||
}
|
||||
assert(found == 6);
|
||||
std::cout << "Test 8 PASSED: MCP orchestrator tools registered\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: MCP orchestrate_run_deterministic executes through callback
|
||||
{
|
||||
auto state = makeState();
|
||||
state.workflow->queue.enqueue(makeItem("m1", "getOne", "template"));
|
||||
MCPServer server;
|
||||
server.setRpcCallback([&state](const json& request) {
|
||||
return handleHeadlessAgentRequest(state, request, "test-session");
|
||||
});
|
||||
auto resp = mcpCall(server, "tools/call", {
|
||||
{"name", "whetstone_orchestrate_run_deterministic"},
|
||||
{"arguments", json::object()}
|
||||
});
|
||||
assert(resp.contains("result"));
|
||||
assert(resp["result"]["isError"].get<bool>() == false);
|
||||
std::cout << "Test 9 PASSED: MCP run_deterministic tool call works\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: combined flow updates progress after external submit
|
||||
{
|
||||
auto state = makeState();
|
||||
state.reviewPolicy.defaultAction = "auto-approve";
|
||||
state.workflow->queue.enqueue(makeItem("d1", "getFast", "template"));
|
||||
state.workflow->queue.enqueue(makeItem("x1", "buildGraph", "", "project"));
|
||||
|
||||
rpc(state, "orchestrateRunDeterministic");
|
||||
auto blockers = rpc(state, "getBlockers");
|
||||
assert(blockers["result"]["count"].get<int>() >= 1);
|
||||
rpc(state, "submitExternalResult", {
|
||||
{"itemId", "x1"},
|
||||
{"result", {
|
||||
{"generatedCode", "def buildGraph(data):\n return data"},
|
||||
{"confidence", 0.88},
|
||||
{"reasoning", "llm output"},
|
||||
{"tokensGenerated", 24}
|
||||
}}
|
||||
});
|
||||
auto progress = rpc(state, "getProgress");
|
||||
assert(progress["result"]["completedItems"].get<int>() == 2);
|
||||
std::cout << "Test 10 PASSED: combined orchestrator flow tracked\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: submitExternalResult rejects non-agent worker types
|
||||
{
|
||||
auto state = makeState();
|
||||
WorkItem t = makeItem("t1", "getName", "template");
|
||||
transitionWorkItem(t, WI_READY);
|
||||
transitionWorkItem(t, WI_ASSIGNED);
|
||||
transitionWorkItem(t, WI_IN_PROGRESS);
|
||||
state.workflow->queue.enqueue(t);
|
||||
auto resp = rpc(state, "submitExternalResult", {
|
||||
{"itemId", "t1"},
|
||||
{"result", {{"generatedCode", "return name"}}}
|
||||
});
|
||||
assert(resp.contains("error"));
|
||||
std::cout << "Test 11 PASSED: submitExternalResult enforces worker type\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 12: orchestrator RPC methods require active workflow
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
auto resp = rpc(state, "orchestrateAdvance");
|
||||
assert(resp.contains("error"));
|
||||
std::cout << "Test 12 PASSED: workflow guards enforced\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
std::cout << "\nResults: " << passed << "/12\n";
|
||||
assert(passed == 12);
|
||||
return 0;
|
||||
}
|
||||
59
progress.md
59
progress.md
@@ -2919,6 +2919,65 @@ worker metrics, blockers, and timeline entries.
|
||||
- `editor/src/WorkflowProgress.h` within header-size limit (`303` <= `600`)
|
||||
- `editor/tests/step381_test.cpp` within test-file size guidance (`196` lines)
|
||||
|
||||
### Step 382: Orchestrator RPC + MCP
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Added orchestrator control RPC methods and MCP tool bindings so a client can
|
||||
step/advance workflows, run deterministic items, inspect blockers/progress, and
|
||||
submit external model output back into the workflow loop.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/HeadlessOrchestratorRPC.h` — extracted orchestrator RPC handling:
|
||||
- `orchestrateStep`
|
||||
- `orchestrateAdvance`
|
||||
- `orchestrateRunDeterministic`
|
||||
- `getBlockers`
|
||||
- `getProgress`
|
||||
- `submitExternalResult`
|
||||
- event/blocker JSON serialization helpers and buffer-context collection
|
||||
- `editor/tests/step382_test.cpp` — 12 tests covering:
|
||||
1. `orchestrateStep` advances one task
|
||||
2. `orchestrateAdvance` processes batch
|
||||
3. `orchestrateRunDeterministic` stops at blockers
|
||||
4. `getBlockers` reports human blockers
|
||||
5. `getProgress` snapshot shape
|
||||
6. `submitExternalResult` accepts LLM output and advances lifecycle
|
||||
7. Linter role can read progress but cannot orchestrate
|
||||
8. MCP tool registration for all 6 orchestrator tools
|
||||
9. MCP `whetstone_orchestrate_run_deterministic` callback wiring
|
||||
10. combined deterministic + external-submit + progress flow
|
||||
11. submit-result worker-type guard
|
||||
12. no-workflow guard behavior
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` — delegated orchestrator-method
|
||||
handling to `tryHandleHeadlessOrchestratorRPC(...)`
|
||||
- `editor/src/HeadlessEditorState.h` — add `workflowProgress` state
|
||||
- `editor/src/AgentPermissionPolicy.h` — permissions for new orchestrator methods
|
||||
(`getProgress/getBlockers` read-only, orchestration/submit methods write)
|
||||
- `editor/src/MCPServer.h` — register orchestrator MCP tools:
|
||||
- `whetstone_orchestrate_step`
|
||||
- `whetstone_orchestrate_advance`
|
||||
- `whetstone_orchestrate_run_deterministic`
|
||||
- `whetstone_get_blockers`
|
||||
- `whetstone_get_progress`
|
||||
- `whetstone_submit_result`
|
||||
- `editor/CMakeLists.txt` — `step382_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step382_test` — PASS (12/12) new step coverage
|
||||
- `step381_test` — PASS (12/12) regression coverage
|
||||
- `step380_test` — PASS (12/12) regression coverage
|
||||
- `step379_test` — PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/HeadlessOrchestratorRPC.h` within header-size limit (`287` <= `600`)
|
||||
- `editor/src/WorkflowProgress.h` within header-size limit (`303` <= `600`)
|
||||
- `editor/tests/step382_test.cpp` within test-file size guidance (`250` lines)
|
||||
- Legacy oversized headers remain and should be split in follow-up:
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2623` > `600`)
|
||||
- `editor/src/MCPServer.h` (`1652` > `600`)
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user