From 52d1e71e39873620fd88f737b9a188bfc8dee100 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 16:14:17 -0700 Subject: [PATCH] Step 419: add MCP agent workflow loop --- editor/CMakeLists.txt | 9 + editor/src/MCPAgentWorkflowLoop.h | 137 ++++++++++++++ editor/tests/step419_test.cpp | 299 ++++++++++++++++++++++++++++++ progress.md | 51 +++++ 4 files changed, 496 insertions(+) create mode 100644 editor/src/MCPAgentWorkflowLoop.h create mode 100644 editor/tests/step419_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 876c48c..3a03cc4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/MCPAgentWorkflowLoop.h b/editor/src/MCPAgentWorkflowLoop.h new file mode 100644 index 0000000..cd1b2c0 --- /dev/null +++ b/editor/src/MCPAgentWorkflowLoop.h @@ -0,0 +1,137 @@ +#pragma once + +#include "MCPServer.h" + +#include +#include +#include +#include + +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 toolCalls; + json finalProgress = json::object(); + }; + + using AgentHandler = std::function; + + 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 parseBlockers(const json& response) { + std::vector 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 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}}; + } + } +}; diff --git a/editor/tests/step419_test.cpp b/editor/tests/step419_test.cpp new file mode 100644 index 0000000..f1af5ac --- /dev/null +++ b/editor/tests/step419_test.cpp @@ -0,0 +1,299 @@ +// Step 419: Agent Workflow Loop Tests (12 tests) + +#include "MCPAgentWorkflowLoop.h" + +#include +#include +#include +#include + +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 methods; + std::vector 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 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 skeletonParams; + std::vector 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 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; +} diff --git a/progress.md b/progress.md index b470b15..40f6181 100644 --- a/progress.md +++ b/progress.md @@ -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)