diff --git a/PROGRESS.md b/PROGRESS.md index eeeb39d..8303623 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -751,3 +751,4 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi | 2026-02-10 | Codex | Step 229: Tool definition tests (schema alignment, prompt coverage, examples validation, prompt variable checks). 1/1 tests pass. | | 2026-02-10 | Codex | Step 230: Enhanced session recorder (metadata, event capture, ms timestamps, UI toggle, auto-record setting). 1/1 tests pass. | | 2026-02-10 | Codex | Step 231: Session anonymizer (paths, secrets, identifier anonymization, comment/string stripping, levels, deterministic seed). 1/1 tests pass. | +| 2026-02-10 | Codex | Step 232: Session-to-trace converter (events->user msgs, RPC->tool calls/results, gap splitting, synthetic thinking). 1/1 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 1a50d20..6d7c4b8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1178,6 +1178,9 @@ target_link_libraries(step230_test PRIVATE nlohmann_json::nlohmann_json) add_executable(step231_test tests/step231_test.cpp) target_include_directories(step231_test PRIVATE src) target_link_libraries(step231_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step232_test tests/step232_test.cpp) +target_include_directories(step232_test PRIVATE src) +target_link_libraries(step232_test PRIVATE nlohmann_json::nlohmann_json) add_executable(step213_test tests/step213_test.cpp) target_include_directories(step213_test PRIVATE src) target_link_libraries(step213_test PRIVATE nlohmann_json::nlohmann_json) diff --git a/editor/src/SessionTraceConverter.h b/editor/src/SessionTraceConverter.h new file mode 100644 index 0000000..dcfdd54 --- /dev/null +++ b/editor/src/SessionTraceConverter.h @@ -0,0 +1,188 @@ +#pragma once +// Step 232: Session-to-trace converter + +#include +#include +#include +#include +#include "TraceGenerator.h" + +using json = nlohmann::json; + +class SessionTraceConverter { +public: + void setGapSeconds(int seconds) { gapSeconds_ = seconds; } + + std::vector convert(const json& session) const { + std::vector traces; + if (!session.is_object()) return traces; + + std::string sessionId = session.value("sessionId", "session"); + std::string language = session.value("metadata", json::object()).value("language", "unknown"); + + std::vector items = collectItems(session); + if (items.empty()) return traces; + std::sort(items.begin(), items.end(), [](const Item& a, const Item& b) { + return a.timestamp < b.timestamp; + }); + + Trace current; + int traceIndex = 0; + current.id = sessionId + "_" + std::to_string(traceIndex); + current.scenario = "session_replay"; + current.difficulty = "intermediate"; + current.language = language; + + std::string prevTs; + for (const auto& item : items) { + if (!prevTs.empty() && gapSeconds_ > 0) { + int gap = secondsBetween(prevTs, item.timestamp); + if (gap > gapSeconds_ && !current.steps.empty()) { + finalizeTrace(current, traces, sessionId, ++traceIndex, language); + prevTs.clear(); + } + } + prevTs = item.timestamp; + + if (item.kind == ItemKind::Event) { + std::string message = eventToUserMessage(item.eventType, item.payload); + if (!message.empty()) { + current.steps.push_back({"user", message, "", json(), json()}); + } + } else if (item.kind == ItemKind::Call) { + std::string tool = mapMethodToTool(item.method); + if (tool.empty()) continue; + current.steps.push_back({"assistant", "Thinking about the next action.", "", json(), json()}); + TraceStep call; + call.role = "tool_call"; + call.toolName = tool; + call.toolInput = item.params; + current.steps.push_back(call); + + TraceStep result; + result.role = "tool_result"; + result.toolName = tool; + result.toolOutput = item.response; + current.steps.push_back(result); + + current.toolsUsed.push_back(tool); + current.toolCallCount++; + } + } + + if (!current.steps.empty()) { + traces.push_back(current); + } + return traces; + } + +private: + enum class ItemKind { Event, Call }; + struct Item { + ItemKind kind; + std::string timestamp; + std::string eventType; + json payload; + std::string method; + json params; + json response; + }; + + int gapSeconds_ = 300; + + static int secondsBetween(const std::string& a, const std::string& b) { + // ISO-like timestamps sort lexicographically, parse only to seconds precision + auto toSeconds = [](const std::string& ts) { + if (ts.size() < 19) return 0; + int year = std::stoi(ts.substr(0, 4)); + int mon = std::stoi(ts.substr(5, 2)); + int day = std::stoi(ts.substr(8, 2)); + int hour = std::stoi(ts.substr(11, 2)); + int min = std::stoi(ts.substr(14, 2)); + int sec = std::stoi(ts.substr(17, 2)); + std::tm tm{}; + tm.tm_year = year - 1900; + tm.tm_mon = mon - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + return (int)std::mktime(&tm); + }; + int ta = toSeconds(a); + int tb = toSeconds(b); + return tb - ta; + } + + static std::vector collectItems(const json& session) { + std::vector items; + if (session.contains("events") && session["events"].is_array()) { + for (const auto& ev : session["events"]) { + Item item; + item.kind = ItemKind::Event; + item.timestamp = ev.value("timestamp", ""); + item.eventType = ev.value("type", ""); + item.payload = ev.value("payload", json::object()); + items.push_back(std::move(item)); + } + } + if (session.contains("calls") && session["calls"].is_array()) { + for (const auto& call : session["calls"]) { + Item item; + item.kind = ItemKind::Call; + item.timestamp = call.value("timestamp", ""); + item.method = call.value("method", call.value("request", json::object()).value("method", "")); + item.params = call.value("request", json::object()).value("params", json::object()); + item.response = call.value("response", json::object()); + items.push_back(std::move(item)); + } + } + return items; + } + + static void finalizeTrace(Trace& current, + std::vector& out, + const std::string& sessionId, + int traceIndex, + const std::string& language) { + if (!current.steps.empty()) out.push_back(current); + current = {}; + current.id = sessionId + "_" + std::to_string(traceIndex); + current.scenario = "session_replay"; + current.difficulty = "intermediate"; + current.language = language; + } + + static std::string eventToUserMessage(const std::string& type, const json& payload) { + if (type == "file_opened") { + return "Opened file " + payload.value("path", "(unknown)") + "."; + } + if (type == "buffer_switched") { + return "Switched buffer to " + payload.value("path", "(unknown)") + "."; + } + if (type == "theme_changed") { + return "Changed theme."; + } + if (type == "file_saved") { + return "Saved file " + payload.value("path", "(unknown)") + "."; + } + if (type == "notification_posted") { + return "Noted: " + payload.value("detail", ""); + } + return ""; + } + + static std::string mapMethodToTool(const std::string& method) { + if (method == "getAST") return "whetstone_get_ast"; + if (method == "applyMutation") return "whetstone_mutate"; + if (method == "applyBatch") return "whetstone_batch_mutate"; + if (method == "getInScopeSymbols") return "whetstone_get_scope"; + if (method == "getCallHierarchy") return "whetstone_get_call_hierarchy"; + if (method == "getAnnotationSuggestions") return "whetstone_suggest_annotations"; + if (method == "applyAnnotationSuggestion") return "whetstone_apply_annotation"; + if (method == "generateCode") return "whetstone_generate_code"; + if (method == "runPipeline") return "whetstone_run_pipeline"; + if (method == "projectLanguage") return "whetstone_project_language"; + return ""; + } +}; diff --git a/editor/tests/step232_test.cpp b/editor/tests/step232_test.cpp new file mode 100644 index 0000000..4a699ab --- /dev/null +++ b/editor/tests/step232_test.cpp @@ -0,0 +1,61 @@ +// Step 232: Session-to-trace converter. +#include "SessionTraceConverter.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; + + try { + json session = { + {"sessionId", "s1"}, + {"metadata", {{"language", "python"}}}, + {"events", json::array({{ + {"timestamp", "2026-02-10T10:00:00.000"}, + {"type", "file_opened"}, + {"payload", {{"path", "main.py"}}} + }})}, + {"calls", json::array({{ + {"timestamp", "2026-02-10T10:00:01.000"}, + {"method", "getAST"}, + {"request", {{"params", json::object()}}}, + {"response", {{"result", json::object()}}} + }})} + }; + + SessionTraceConverter converter; + auto traces = converter.convert(session); + expect(traces.size() == 1, "one trace generated", passed, failed); + if (!traces.empty()) { + expect(traces[0].toolCallCount == 1, "tool call count", passed, failed); + bool hasUser = false; + bool hasToolCall = false; + bool hasToolResult = false; + for (const auto& step : traces[0].steps) { + if (step.role == "user") hasUser = true; + if (step.role == "tool_call") hasToolCall = true; + if (step.role == "tool_result") hasToolResult = true; + } + expect(hasUser, "user step generated", passed, failed); + expect(hasToolCall, "tool_call step generated", passed, failed); + expect(hasToolResult, "tool_result step generated", passed, failed); + } + } catch (const std::exception& ex) { + std::cout << "Exception: " << ex.what() << "\n"; + failed++; + } + + std::cout << "\n=== Step 232 Results: " << passed << " passed, " + << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint7_plan.md b/sprint7_plan.md index 1a43159..e7bd3aa 100644 --- a/sprint7_plan.md +++ b/sprint7_plan.md @@ -421,7 +421,7 @@ Capture real editing sessions, anonymize them, and export as training data. - Deterministic: same input produces same anonymized output (with seed) *New:* `editor/src/SessionAnonymizer.h` -- [ ] **Step 232: Session-to-trace converter** +- [x] **Step 232: Session-to-trace converter** Convert recorded sessions into training trace format: - Map editor events → user messages ("I opened file X and want to annotate it") - Map RPC requests → tool calls