Step 382: add orchestrator RPC and MCP tools

This commit is contained in:
Bill
2026-02-16 12:47:58 -07:00
parent 5300be080a
commit 448d051077
8 changed files with 703 additions and 2 deletions

View File

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

View File

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

View File

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

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

View File

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