Step 382: add orchestrator RPC and MCP tools
This commit is contained in:
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user