From c21c52f55b69ac525375b31062e5f3f7696dba09 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 19:36:45 -0700 Subject: [PATCH] Step 158: add agent workflow recorder --- PROGRESS.md | 1 + editor/CMakeLists.txt | 4 + editor/src/EditorState.h | 49 ++++++++++++ editor/src/WorkflowRecorder.h | 140 ++++++++++++++++++++++++++++++++++ editor/tests/step158_test.cpp | 41 ++++++++++ sprint5_plan.md | 2 +- 6 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 editor/src/WorkflowRecorder.h create mode 100644 editor/tests/step158_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index a78b387..fa4f5eb 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -505,3 +505,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step | 2026-02-10 | Codex | Step 155: Agent library-aware code generation RPC + generator helper. 3/3 tests pass. | | 2026-02-10 | Codex | Step 156: Agent annotation assistant RPC with suggestions/diagnostics, feedback learning, and mutation apply helpers. 5/5 tests pass. | | 2026-02-10 | Codex | Step 157: Multi-agent roles with permission policy, agent provenance in transform history, and UI role controls. 8/8 tests pass. | +| 2026-02-10 | Codex | Step 158: Agent workflow recorder with JSON export, replay support, and RPC wiring. 5/5 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 273e358..13c0974 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -886,6 +886,10 @@ target_include_directories(step156_test PRIVATE src) add_executable(step157_test tests/step157_test.cpp) target_include_directories(step157_test PRIVATE src) +add_executable(step158_test tests/step158_test.cpp) +target_include_directories(step158_test PRIVATE src) +target_link_libraries(step158_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 3f0f307..ca0f013 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -42,6 +42,7 @@ #include "ZoomUtils.h" #include "TerminalPanel.h" #include "WebSocketServer.h" +#include "WorkflowRecorder.h" #include "BuildSystem.h" #include "DependencyPanel.h" #include "LibraryIndexer.h" @@ -176,6 +177,7 @@ struct EditorState { int agentPort = 8765; std::vector agentLog; std::map agentRoles; + WorkflowRecorder workflowRecorder; BuildSystem::Type buildType = BuildSystem::Type::None; std::vector buildErrors; std::string lastBuildOutput; @@ -922,6 +924,7 @@ struct EditorState { std::string method = req.value("method", ""); bool ok = !res.contains("error"); logAgentEvent("RPC " + sid + " " + method + (ok ? " ok" : " error")); + workflowRecorder.record(sid, req, res); }); if (agentServer->start(agentPort)) { logAgentEvent("Agent server started on port " + std::to_string(agentPort)); @@ -1200,6 +1203,52 @@ struct EditorState { return response; } + if (method == "startWorkflowRecording") { + auto params = request.contains("params") ? request["params"] : json::object(); + std::string name = params.value("name", "workflow"); + workflowRecorder.startRecording(name, sessionId); + response["result"] = { + {"recording", true}, + {"name", name} + }; + return response; + } + + if (method == "stopWorkflowRecording") { + response["result"] = workflowRecorder.stopRecording(); + return response; + } + + if (method == "getWorkflowRecording") { + response["result"] = workflowRecorder.exportWorkflow(); + return response; + } + + if (method == "replayWorkflow") { + auto params = request.contains("params") ? request["params"] : json::object(); + if (!params.contains("workflow")) { + response["error"] = {{"code", -32602}, {"message", "Missing workflow payload"}}; + return response; + } + WorkflowRecorder temp; + if (!temp.loadWorkflow(params["workflow"])) { + response["error"] = {{"code", -32602}, {"message", "Invalid workflow payload"}}; + return response; + } + auto requests = temp.buildReplayRequests(); + json results = json::array(); + workflowRecorder.setReplaying(true); + for (auto& req : requests) { + results.push_back(processAgentRequest(req, sessionId)); + } + workflowRecorder.setReplaying(false); + response["result"] = { + {"count", results.size()}, + {"responses", results} + }; + return response; + } + if (method == "getAnnotationSuggestions") { if (!AgentPermissionPolicy::canInvoke(role, method)) { response["error"] = {{"code", -32031}, {"message", "Role not permitted"}}; diff --git a/editor/src/WorkflowRecorder.h b/editor/src/WorkflowRecorder.h new file mode 100644 index 0000000..60b19a1 --- /dev/null +++ b/editor/src/WorkflowRecorder.h @@ -0,0 +1,140 @@ +#pragma once +// Step 158: Agent workflow recording + +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +class WorkflowRecorder { +public: + struct WorkflowCall { + json request; + json response; + std::string timestamp; + }; + + void startRecording(const std::string& name, const std::string& sessionId) { + name_ = name.empty() ? "workflow" : name; + sessionId_ = sessionId; + calls_.clear(); + recording_ = true; + createdAt_ = currentTimestamp(); + stoppedAt_.clear(); + } + + json stopRecording() { + recording_ = false; + stoppedAt_ = currentTimestamp(); + return exportWorkflow(); + } + + bool isRecording() const { return recording_; } + bool isReplaying() const { return replaying_; } + + void setReplaying(bool replaying) { replaying_ = replaying; } + + void record(const std::string& sessionId, + const json& request, + const json& response) { + if (!recording_ || replaying_) return; + if (!sessionId_.empty() && sessionId != sessionId_) return; + std::string method = request.value("method", ""); + if (isControlMethod(method)) return; + WorkflowCall call; + call.request = request; + call.response = response; + call.timestamp = currentTimestamp(); + calls_.push_back(std::move(call)); + } + + json exportWorkflow() const { + json out; + out["name"] = name_; + out["sessionId"] = sessionId_; + out["createdAt"] = createdAt_; + out["stoppedAt"] = stoppedAt_; + json arr = json::array(); + for (const auto& c : calls_) { + arr.push_back({ + {"timestamp", c.timestamp}, + {"request", c.request}, + {"response", c.response} + }); + } + out["calls"] = arr; + return out; + } + + bool loadWorkflow(const json& workflow) { + if (!workflow.contains("calls")) return false; + name_ = workflow.value("name", "workflow"); + sessionId_ = workflow.value("sessionId", ""); + createdAt_ = workflow.value("createdAt", ""); + stoppedAt_ = workflow.value("stoppedAt", ""); + calls_.clear(); + for (const auto& c : workflow["calls"]) { + WorkflowCall call; + call.timestamp = c.value("timestamp", ""); + call.request = c.value("request", json::object()); + call.response = c.value("response", json::object()); + calls_.push_back(std::move(call)); + } + return true; + } + + std::vector buildReplayRequests() const { + std::vector out; + int id = 1; + for (const auto& c : calls_) { + json req = c.request; + req["jsonrpc"] = "2.0"; + req["id"] = id++; + out.push_back(std::move(req)); + } + return out; + } + + void clear() { + name_.clear(); + sessionId_.clear(); + createdAt_.clear(); + stoppedAt_.clear(); + calls_.clear(); + recording_ = false; + } + +private: + static std::string currentTimestamp() { + auto now = std::chrono::system_clock::now(); + std::time_t tt = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &tt); +#else + localtime_r(&tt, &tm); +#endif + std::ostringstream oss; + oss << std::put_time(&tm, "%H:%M:%S"); + return oss.str(); + } + + static bool isControlMethod(const std::string& method) { + return method == "startWorkflowRecording" || + method == "stopWorkflowRecording" || + method == "replayWorkflow" || + method == "getWorkflowRecording"; + } + + std::string name_; + std::string sessionId_; + std::string createdAt_; + std::string stoppedAt_; + std::vector calls_; + bool recording_ = false; + bool replaying_ = false; +}; diff --git a/editor/tests/step158_test.cpp b/editor/tests/step158_test.cpp new file mode 100644 index 0000000..9ea6f66 --- /dev/null +++ b/editor/tests/step158_test.cpp @@ -0,0 +1,41 @@ +// Step 158 TDD Test: Workflow recorder +#include "WorkflowRecorder.h" +#include + +static void expect(bool cond, const std::string& name, int& passed, int& failed) { + if (cond) { + std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n"; + ++passed; + } else { + std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n"; + ++failed; + } +} + +int main() { + int passed = 0; + int failed = 0; + + WorkflowRecorder rec; + rec.startRecording("wf1", "s1"); + + json req1 = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "getAST"}}; + json res1 = {{"jsonrpc", "2.0"}, {"id", 1}, {"result", true}}; + rec.record("s1", req1, res1); + rec.record("s2", req1, res1); + + json wf = rec.stopRecording(); + expect(wf.value("name", "") == "wf1", "workflow name recorded", passed, failed); + expect(wf["calls"].size() == 1, "recorded calls filtered by session", passed, failed); + + WorkflowRecorder rec2; + expect(rec2.loadWorkflow(wf), "load workflow succeeds", passed, failed); + auto replay = rec2.buildReplayRequests(); + expect(replay.size() == 1, "replay request count", passed, failed); + expect(replay[0].value("method", "") == "getAST", + "replay request method", passed, failed); + + std::cout << "\n=== Step 158 Results: " << passed << " passed, " + << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint5_plan.md b/sprint5_plan.md index 138cedb..57bb7d9 100644 --- a/sprint5_plan.md +++ b/sprint5_plan.md @@ -322,7 +322,7 @@ primitives, and assist with constructive coding. Agent actions appear in the transform history with agent name as provenance. *Modifies:* `WebSocketServer.h`, `IncrementalOptimizer.h` -- [ ] **Step 158: Agent workflow recording** +- [x] **Step 158: Agent workflow recording** Record agent actions as reproducible workflows. A sequence of agent RPC calls → saved as a "workflow script" (JSON). User can replay workflows on different codebases. Workflows are shareable.