Step 419: add MCP agent workflow loop

This commit is contained in:
Bill
2026-02-16 16:14:17 -07:00
parent 29ae2aae7a
commit 52d1e71e39
4 changed files with 496 additions and 0 deletions

View File

@@ -2650,4 +2650,13 @@ target_link_libraries(step418_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step419_test tests/step419_test.cpp)
target_include_directories(step419_test PRIVATE src)
target_link_libraries(step419_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

@@ -0,0 +1,137 @@
#pragma once
#include "MCPServer.h"
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>
class MCPAgentWorkflowLoop {
public:
struct Config {
std::string projectName = "workflow-project";
std::string skeletonName = "skeleton";
std::string language = "cpp";
};
struct Blocker {
std::string itemId;
std::string kind;
std::string prompt;
};
struct AgentSubmission {
std::string generatedCode;
double confidence = 0.8;
std::string reasoning;
};
struct Report {
bool success = false;
int blockersProcessed = 0;
std::vector<std::string> toolCalls;
json finalProgress = json::object();
};
using AgentHandler = std::function<AgentSubmission(const Blocker&)>;
explicit MCPAgentWorkflowLoop(MCPServer& server) : server_(server) {}
Report run(const Config& config, AgentHandler handler) {
Report r;
ensureRequiredToolsPresent();
callTool("whetstone_infer_annotations", json::object(), r);
callTool("whetstone_create_skeleton",
{{"name", config.skeletonName}, {"language", config.language}}, r);
callTool("whetstone_create_workflow", {{"projectName", config.projectName}}, r);
callTool("whetstone_orchestrate_run_deterministic", json::object(), r);
json blockersResp = callTool("whetstone_get_blockers", json::object(), r);
auto blockers = parseBlockers(blockersResp);
for (const auto& b : blockers) {
AgentSubmission sub = handler ? handler(b) : AgentSubmission{};
json result = {
{"generatedCode", sub.generatedCode},
{"confidence", sub.confidence},
{"reasoning", sub.reasoning}
};
callTool("whetstone_submit_result",
{{"itemId", b.itemId}, {"result", result}}, r);
r.blockersProcessed++;
}
r.finalProgress = callTool("whetstone_get_progress", json::object(), r);
r.success = !r.finalProgress.contains("error");
return r;
}
static std::vector<Blocker> parseBlockers(const json& response) {
std::vector<Blocker> out;
if (!response.is_object()) return out;
if (!response.contains("blockers") || !response["blockers"].is_array()) return out;
for (const auto& b : response["blockers"]) {
Blocker item;
item.itemId = b.value("itemId", b.value("id", ""));
item.kind = b.value("kind", b.value("type", ""));
item.prompt = b.value("prompt", b.value("reason", ""));
if (!item.itemId.empty()) out.push_back(item);
}
return out;
}
private:
MCPServer& server_;
void ensureRequiredToolsPresent() const {
const std::vector<std::string> required = {
"whetstone_infer_annotations",
"whetstone_create_skeleton",
"whetstone_create_workflow",
"whetstone_orchestrate_run_deterministic",
"whetstone_get_blockers",
"whetstone_submit_result",
"whetstone_get_progress"
};
for (const auto& name : required) {
if (!hasTool(name)) {
throw std::runtime_error("Missing required MCP tool: " + name);
}
}
}
bool hasTool(const std::string& name) const {
for (const auto& t : server_.getTools()) if (t.name == name) return true;
return false;
}
json callTool(const std::string& name, const json& args, Report& r) {
r.toolCalls.push_back(name);
json req = {
{"jsonrpc", "2.0"},
{"id", 1},
{"method", "tools/call"},
{"params", {
{"name", name},
{"arguments", args}
}}
};
json resp = server_.handleRequest(req);
if (!resp.contains("result")) return json{{"error", "missing result"}};
auto result = resp["result"];
if (result.value("isError", false)) return json{{"error", "tool call failed"}};
if (!result.contains("content") || !result["content"].is_array() || result["content"].empty()) {
return json{{"error", "missing content"}};
}
std::string text = result["content"][0].value("text", "");
try {
return json::parse(text);
} catch (...) {
return json{{"raw", text}};
}
}
};

View File

@@ -0,0 +1,299 @@
// Step 419: Agent Workflow Loop Tests (12 tests)
#include "MCPAgentWorkflowLoop.h"
#include <iostream>
#include <map>
#include <string>
#include <vector>
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 {}
struct RpcRecorder {
std::vector<std::string> methods;
std::vector<json> params;
json operator()(const json& request) {
std::string method = request.value("method", "");
json p = request.value("params", json::object());
methods.push_back(method);
params.push_back(p);
if (method == "getBlockers") {
return {{"jsonrpc", "2.0"}, {"id", 1}, {"result", {{"blockers", json::array()}}}};
}
if (method == "getProgress") {
return {{"jsonrpc", "2.0"}, {"id", 1},
{"result", {{"percentComplete", 100}, {"status", "complete"}}}};
}
return {{"jsonrpc", "2.0"}, {"id", 1}, {"result", json::object()}};
}
};
void test_loop_calls_expected_tools_without_blockers() {
TEST(loop_calls_expected_tools_without_blockers);
MCPServer server;
RpcRecorder rec;
server.setRpcCallback([&](const json& req){ return rec(req); });
MCPAgentWorkflowLoop loop(server);
auto report = loop.run({}, [&](const MCPAgentWorkflowLoop::Blocker&) {
return MCPAgentWorkflowLoop::AgentSubmission{};
});
CHECK(report.success, "expected success");
CHECK(report.blockersProcessed == 0, "expected zero blockers processed");
CHECK(rec.methods.size() == 6, "expected six RPC calls without submit");
PASS();
}
void test_loop_processes_single_blocker_and_submits_result() {
TEST(loop_processes_single_blocker_and_submits_result);
MCPServer server;
std::vector<std::string> methods;
server.setRpcCallback([&](const json& req) -> json {
std::string m = req.value("method", "");
methods.push_back(m);
if (m == "getBlockers") {
return {{"jsonrpc", "2.0"}, {"id", 1},
{"result", {{"blockers", json::array({
{{"itemId", "w1"}, {"kind", "needs-external-model"}, {"prompt", "Implement W1"}}
})}}}};
}
if (m == "getProgress") {
return {{"jsonrpc", "2.0"}, {"id", 1},
{"result", {{"percentComplete", 75}, {"status", "in-progress"}}}};
}
return {{"jsonrpc", "2.0"}, {"id", 1}, {"result", json::object()}};
});
MCPAgentWorkflowLoop loop(server);
auto report = loop.run({}, [&](const MCPAgentWorkflowLoop::Blocker& b) {
MCPAgentWorkflowLoop::AgentSubmission s;
s.generatedCode = "// completed " + b.itemId;
s.reasoning = "done";
s.confidence = 0.92;
return s;
});
CHECK(report.blockersProcessed == 1, "expected one processed blocker");
bool submitted = false;
for (const auto& m : methods) if (m == "submitExternalResult") submitted = true;
CHECK(submitted, "expected submitExternalResult call");
PASS();
}
void test_loop_processes_multiple_blockers() {
TEST(loop_processes_multiple_blockers);
MCPServer server;
int submits = 0;
server.setRpcCallback([&](const json& req) -> json {
std::string m = req.value("method", "");
if (m == "getBlockers") {
return {{"jsonrpc", "2.0"}, {"id", 1},
{"result", {{"blockers", json::array({
{{"itemId", "w1"}, {"prompt", "p1"}},
{{"itemId", "w2"}, {"prompt", "p2"}}
})}}}};
}
if (m == "submitExternalResult") submits++;
if (m == "getProgress") {
return {{"jsonrpc", "2.0"}, {"id", 1}, {"result", {{"percentComplete", 60}}}};
}
return {{"jsonrpc", "2.0"}, {"id", 1}, {"result", json::object()}};
});
MCPAgentWorkflowLoop loop(server);
auto report = loop.run({}, [&](const MCPAgentWorkflowLoop::Blocker&) {
return MCPAgentWorkflowLoop::AgentSubmission{"code", 0.8, "ok"};
});
CHECK(report.blockersProcessed == 2, "expected two blockers processed");
CHECK(submits == 2, "expected two submit calls");
PASS();
}
void test_loop_propagates_config_values_to_calls() {
TEST(loop_propagates_config_values_to_calls);
MCPServer server;
std::vector<json> skeletonParams;
std::vector<json> workflowParams;
server.setRpcCallback([&](const json& req) -> json {
std::string m = req.value("method", "");
if (m == "createSkeleton") skeletonParams.push_back(req.value("params", json::object()));
if (m == "createWorkflow") workflowParams.push_back(req.value("params", json::object()));
if (m == "getBlockers") return {{"jsonrpc","2.0"},{"id",1},{"result",{{"blockers",json::array()}}}};
if (m == "getProgress") return {{"jsonrpc","2.0"},{"id",1},{"result",{{"percentComplete",100}}}};
return {{"jsonrpc","2.0"},{"id",1},{"result",json::object()}};
});
MCPAgentWorkflowLoop loop(server);
MCPAgentWorkflowLoop::Config cfg;
cfg.projectName = "Alpha";
cfg.skeletonName = "AlphaSkeleton";
cfg.language = "python";
loop.run(cfg, nullptr);
CHECK(!skeletonParams.empty(), "missing createSkeleton params");
CHECK(!workflowParams.empty(), "missing createWorkflow params");
CHECK(skeletonParams[0].value("name", "") == "AlphaSkeleton", "wrong skeleton name");
CHECK(skeletonParams[0].value("language", "") == "python", "wrong language");
CHECK(workflowParams[0].value("projectName", "") == "Alpha", "wrong project name");
PASS();
}
void test_parse_blockers_accepts_itemId_format() {
TEST(parse_blockers_accepts_itemId_format);
json r = {{"blockers", json::array({{{"itemId", "w1"}, {"kind", "k"}, {"prompt", "p"}}})}};
auto v = MCPAgentWorkflowLoop::parseBlockers(r);
CHECK(v.size() == 1, "expected one blocker");
CHECK(v[0].itemId == "w1", "wrong blocker id");
PASS();
}
void test_parse_blockers_accepts_id_alias() {
TEST(parse_blockers_accepts_id_alias);
json r = {{"blockers", json::array({{{"id", "w9"}, {"type", "human"}, {"reason", "needs review"}}})}};
auto v = MCPAgentWorkflowLoop::parseBlockers(r);
CHECK(v.size() == 1, "expected one blocker");
CHECK(v[0].itemId == "w9", "wrong alias id");
CHECK(v[0].kind == "human", "wrong alias kind");
PASS();
}
void test_parse_blockers_ignores_invalid_entries() {
TEST(parse_blockers_ignores_invalid_entries);
json r = {{"blockers", json::array({{{"prompt", "missing id"}}, {{"itemId", "ok"}}})}};
auto v = MCPAgentWorkflowLoop::parseBlockers(r);
CHECK(v.size() == 1, "expected one valid blocker");
CHECK(v[0].itemId == "ok", "expected valid item");
PASS();
}
void test_report_tracks_tool_call_order() {
TEST(report_tracks_tool_call_order);
MCPServer server;
server.setRpcCallback([&](const json& req) -> json {
std::string m = req.value("method", "");
if (m == "getBlockers") return {{"jsonrpc","2.0"},{"id",1},{"result",{{"blockers",json::array()}}}};
if (m == "getProgress") return {{"jsonrpc","2.0"},{"id",1},{"result",{{"percentComplete",100}}}};
return {{"jsonrpc","2.0"},{"id",1},{"result",json::object()}};
});
MCPAgentWorkflowLoop loop(server);
auto report = loop.run({}, nullptr);
CHECK(report.toolCalls.size() == 6, "expected six tool calls");
CHECK(report.toolCalls.front() == "whetstone_infer_annotations", "first call mismatch");
CHECK(report.toolCalls.back() == "whetstone_get_progress", "last call mismatch");
PASS();
}
void test_agent_handler_receives_blocker_payload() {
TEST(agent_handler_receives_blocker_payload);
MCPServer server;
server.setRpcCallback([&](const json& req) -> json {
std::string m = req.value("method", "");
if (m == "getBlockers") {
return {{"jsonrpc","2.0"},{"id",1},
{"result",{{"blockers",json::array({
{{"itemId","w42"},{"kind","needs-external-model"},{"prompt","Write adapter"}}
})}}}};
}
if (m == "getProgress") return {{"jsonrpc","2.0"},{"id",1},{"result",{{"percentComplete",50}}}};
return {{"jsonrpc","2.0"},{"id",1},{"result",json::object()}};
});
MCPAgentWorkflowLoop loop(server);
std::string seenId, seenPrompt;
loop.run({}, [&](const MCPAgentWorkflowLoop::Blocker& b) {
seenId = b.itemId;
seenPrompt = b.prompt;
return MCPAgentWorkflowLoop::AgentSubmission{"code", 0.7, "reason"};
});
CHECK(seenId == "w42", "blocker id not delivered");
CHECK(seenPrompt == "Write adapter", "blocker prompt not delivered");
PASS();
}
void test_submit_result_contains_generated_code_and_confidence() {
TEST(submit_result_contains_generated_code_and_confidence);
MCPServer server;
json submitParams = json::object();
server.setRpcCallback([&](const json& req) -> json {
std::string m = req.value("method", "");
if (m == "getBlockers") {
return {{"jsonrpc","2.0"},{"id",1},
{"result",{{"blockers",json::array({{{"itemId","w1"},{"prompt","do"}}})}}}};
}
if (m == "submitExternalResult") submitParams = req.value("params", json::object());
if (m == "getProgress") return {{"jsonrpc","2.0"},{"id",1},{"result",{{"percentComplete",80}}}};
return {{"jsonrpc","2.0"},{"id",1},{"result",json::object()}};
});
MCPAgentWorkflowLoop loop(server);
loop.run({}, [&](const MCPAgentWorkflowLoop::Blocker&) {
return MCPAgentWorkflowLoop::AgentSubmission{"int x = 1;", 0.91, "ok"};
});
CHECK(submitParams.value("itemId", "") == "w1", "missing item id");
CHECK(submitParams["result"].value("generatedCode", "") == "int x = 1;", "missing code");
CHECK(submitParams["result"].value("confidence", 0.0) > 0.9, "missing confidence");
PASS();
}
void test_final_progress_is_returned_in_report() {
TEST(final_progress_is_returned_in_report);
MCPServer server;
server.setRpcCallback([&](const json& req) -> json {
std::string m = req.value("method", "");
if (m == "getBlockers") return {{"jsonrpc","2.0"},{"id",1},{"result",{{"blockers",json::array()}}}};
if (m == "getProgress") return {{"jsonrpc","2.0"},{"id",1},
{"result",{{"percentComplete",100},{"etaMinutes",0}}}};
return {{"jsonrpc","2.0"},{"id",1},{"result",json::object()}};
});
MCPAgentWorkflowLoop loop(server);
auto report = loop.run({}, nullptr);
CHECK(report.finalProgress.value("percentComplete", 0) == 100, "missing progress percent");
CHECK(report.success, "report should mark success");
PASS();
}
void test_tool_set_contains_required_workflow_tools() {
TEST(tool_set_contains_required_workflow_tools);
MCPServer server;
auto tools = server.getTools();
std::map<std::string, bool> needed = {
{"whetstone_infer_annotations", false},
{"whetstone_create_skeleton", false},
{"whetstone_create_workflow", false},
{"whetstone_orchestrate_run_deterministic", false},
{"whetstone_get_blockers", false},
{"whetstone_submit_result", false},
{"whetstone_get_progress", false}
};
for (const auto& t : tools) {
if (needed.count(t.name)) needed[t.name] = true;
}
for (const auto& [name, ok] : needed) {
CHECK(ok, "missing required tool: " + name);
}
PASS();
}
int main() {
std::cout << "Step 419: Agent Workflow Loop Tests\n";
test_loop_calls_expected_tools_without_blockers(); // 1
test_loop_processes_single_blocker_and_submits_result(); // 2
test_loop_processes_multiple_blockers(); // 3
test_loop_propagates_config_values_to_calls(); // 4
test_parse_blockers_accepts_itemId_format(); // 5
test_parse_blockers_accepts_id_alias(); // 6
test_parse_blockers_ignores_invalid_entries(); // 7
test_report_tracks_tool_call_order(); // 8
test_agent_handler_receives_blocker_payload(); // 9
test_submit_result_contains_generated_code_and_confidence(); // 10
test_final_progress_is_returned_in_report(); // 11
test_tool_set_contains_required_workflow_tools(); // 12
std::cout << "\nResults: " << passed << "/" << (passed + failed)
<< " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -4322,6 +4322,57 @@ flows (`architect_project`, `modernize_module`, `port_to_language`,
- `editor/src/MCPServer.h` (`1679` > `600`)
- `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`)
### Step 419: Agent Workflow Loop
**Status:** PASS (12/12 tests)
Added a concrete MCP-driven agent loop wrapper that executes the Phase 18a
sequence across real MCP tool calls and handles external-result submission for
blocker items.
**Files created:**
- `editor/src/MCPAgentWorkflowLoop.h` — workflow-loop support:
- required-tool gate for:
- `whetstone_infer_annotations`
- `whetstone_create_skeleton`
- `whetstone_create_workflow`
- `whetstone_orchestrate_run_deterministic`
- `whetstone_get_blockers`
- `whetstone_submit_result`
- `whetstone_get_progress`
- MCP `tools/call` execution wrapper with JSON result parsing
- blocker parsing support (`itemId` and `id` aliases)
- orchestrated run sequence with configurable project/skeleton metadata
- loop report with call trace, blocker count, and final progress snapshot
- `editor/tests/step419_test.cpp` — 12 tests covering:
1. baseline loop order without blockers
2. single-blocker submit flow
3. multi-blocker submit flow
4. config propagation to skeleton/workflow creation calls
5. blocker parsing (`itemId`)
6. blocker parsing (`id` alias)
7. invalid blocker filtering
8. report call-order tracking
9. agent handler payload delivery
10. submit payload shape (code + confidence)
11. final progress propagation into report
12. required MCP tool presence check
**Files modified:**
- `editor/CMakeLists.txt``step419_test` target
**Verification run:**
- `step419_test` — PASS (12/12) new step coverage
- `step418_test` — PASS (12/12) regression coverage
- `step417_test` — PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/MCPAgentWorkflowLoop.h` within header-size limit (`137` <= `600`)
- `editor/tests/step419_test.cpp` within test-file size guidance (`299` lines)
- Legacy oversized headers persist:
- `editor/src/ast/Serialization.h` (`1427` > `600`)
- `editor/src/MCPServer.h` (`1679` > `600`)
- `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`)
# Roadmap Planning — Sprints 12-25+
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)