diff --git a/PROGRESS.md b/PROGRESS.md index d03f4db..f183e65 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -503,3 +503,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step | 2026-02-10 | Codex | Step 153: Language coverage integration tests across all supported languages + full projection matrix. 208/208 tests pass. | | 2026-02-10 | Codex | Step 154: Agent library context payload sent on connect with primitives snapshot. 4/4 tests pass. | | 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. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 9bc6ecc..684321f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -880,6 +880,9 @@ add_executable(step155_test tests/step155_test.cpp) target_include_directories(step155_test PRIVATE src) target_link_libraries(step155_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step156_test tests/step156_test.cpp) +target_include_directories(step156_test PRIVATE src) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/AgentAnnotationAssistant.h b/editor/src/AgentAnnotationAssistant.h new file mode 100644 index 0000000..651b558 --- /dev/null +++ b/editor/src/AgentAnnotationAssistant.h @@ -0,0 +1,139 @@ +#pragma once +// Step 156: Agent annotation assistant +// +// Provides annotation suggestions + validation diagnostics for a scoped region, +// plus helpers to apply suggestions via ASTMutationAPI and record feedback. + +#include +#include +#include + +#include "MemoryStrategyInference.h" +#include "AnnotationValidator.h" +#include "ASTMutationAPI.h" +#include "ast/Annotation.h" + +class AgentAnnotationAssistant { +public: + struct Result { + std::vector suggestions; + std::vector diagnostics; + std::string scopeNodeId; + }; + + Result suggest(ASTNode* root, const std::string& nodeId = "") const { + Result out; + if (!root) return out; + + ASTNode* scope = root; + if (!nodeId.empty()) { + ASTNode* found = findById(root, nodeId); + if (found) scope = found; + } + out.scopeNodeId = scope->id; + + MemoryStrategyInference inf; + out.suggestions = inf.inferAnnotations(scope); + + AnnotationValidator validator; + auto allDiags = validator.validate(root); + if (scope == root) { + out.diagnostics = std::move(allDiags); + return out; + } + + std::unordered_set ids; + collectNodeIds(scope, ids); + for (const auto& diag : allDiags) { + if (ids.find(diag.nodeId) != ids.end()) { + out.diagnostics.push_back(diag); + } + } + + return out; + } + + ASTMutationAPI::MutationResult applySuggestion( + ASTNode* root, + const MemoryStrategyInference::Suggestion& suggestion) const { + ASTMutationAPI::MutationResult res; + if (!root) { + res.error = "AST unavailable"; + return res; + } + + ASTNode* target = findById(root, suggestion.nodeId); + if (!target) { + res.error = "Node not found: " + suggestion.nodeId; + return res; + } + + Annotation* anno = createAnnotation(suggestion); + if (!anno) { + res.error = "Unknown annotation type: " + suggestion.annotationType; + return res; + } + + ASTMutationAPI mut; + mut.setRoot(root); + res = mut.insertNode(target->id, "annotations", anno); + if (!res.success) { + delete anno; + } + return res; + } + + void recordFeedback(const MemoryStrategyInference::Suggestion& suggestion, + bool accepted) const { + MemoryStrategyInference inf; + inf.recordFeedback(suggestion, accepted); + } + +private: + static ASTNode* findById(ASTNode* node, const std::string& id) { + if (!node) return nullptr; + if (node->id == id) return node; + for (auto* child : node->allChildren()) { + if (auto* found = findById(child, id)) return found; + } + return nullptr; + } + + static void collectNodeIds(ASTNode* node, + std::unordered_set& ids) { + if (!node) return; + ids.insert(node->id); + for (auto* child : node->allChildren()) { + collectNodeIds(child, ids); + } + } + + static std::string makeAnnotationId(const MemoryStrategyInference::Suggestion& s) { + return "anno_" + s.annotationType + "_" + s.nodeId; + } + + static Annotation* createAnnotation( + const MemoryStrategyInference::Suggestion& s) { + if (s.annotationType == "ReclaimAnnotation") { + auto* a = new ReclaimAnnotation(makeAnnotationId(s), s.strategy); + return a; + } + if (s.annotationType == "LifetimeAnnotation") { + auto* a = new LifetimeAnnotation(makeAnnotationId(s), s.strategy); + return a; + } + if (s.annotationType == "DeallocateAnnotation") { + auto* a = new DeallocateAnnotation(makeAnnotationId(s), s.strategy); + return a; + } + if (s.annotationType == "OwnerAnnotation") { + auto* a = new OwnerAnnotation(makeAnnotationId(s), s.strategy); + return a; + } + if (s.annotationType == "AllocateAnnotation") { + auto* a = new AllocateAnnotation(makeAnnotationId(s), s.strategy); + return a; + } + return nullptr; + } +}; diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 455f164..2b46d47 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -50,6 +50,7 @@ #include "PrimitivesRegistry.h" #include "AgentLibraryPolicy.h" #include "AgentCodeGen.h" +#include "AgentAnnotationAssistant.h" #include "CompositionPanel.h" #include "IncrementalOptimizer.h" #include "EmacsIntegration.h" @@ -1161,6 +1162,123 @@ struct EditorState { return response; } + if (method == "getAnnotationSuggestions") { + if (!active() || !isStructured()) { + response["error"] = {{"code", -32000}, {"message", "No structured buffer"}}; + return response; + } + Module* ast = activeAST(); + if (!ast) { + response["error"] = {{"code", -32001}, {"message", "AST unavailable"}}; + return response; + } + + auto params = request.contains("params") ? request["params"] : json::object(); + std::string nodeId = params.value("nodeId", ""); + int line = params.value("line", -1); + int col = params.value("col", -1); + + if (!nodeId.empty()) { + if (!findNodeById(ast, nodeId)) { + response["error"] = {{"code", -32002}, {"message", "Node not found: " + nodeId}}; + return response; + } + } else if (line >= 0 && col >= 0) { + ASTNode* posNode = findNodeAtPosition(ast, line, col); + if (posNode) nodeId = posNode->id; + } + + AgentAnnotationAssistant assistant; + auto result = assistant.suggest(ast, nodeId); + + json suggArr = json::array(); + for (const auto& s : result.suggestions) { + suggArr.push_back({ + {"nodeId", s.nodeId}, + {"annotationType", s.annotationType}, + {"strategy", s.strategy}, + {"reason", s.reason}, + {"confidence", s.confidence} + }); + } + + json diagArr = json::array(); + for (const auto& d : result.diagnostics) { + diagArr.push_back({ + {"severity", d.severity}, + {"message", d.message}, + {"nodeId", d.nodeId} + }); + } + + response["result"] = { + {"scopeId", result.scopeNodeId}, + {"suggestions", suggArr}, + {"diagnostics", diagArr} + }; + return response; + } + + if (method == "applyAnnotationSuggestion") { + auto permIt = agentMutationPermissions.find(sessionId); + if (permIt == agentMutationPermissions.end() || !permIt->second) { + response["error"] = {{"code", -32030}, {"message", "Mutation not permitted"}}; + return response; + } + if (!active() || !isStructured()) { + response["error"] = {{"code", -32000}, {"message", "No structured buffer"}}; + return response; + } + Module* ast = mutationAST(); + if (!ast) { + response["error"] = {{"code", -32001}, {"message", "AST unavailable"}}; + return response; + } + + auto params = request.contains("params") ? request["params"] : json::object(); + MemoryStrategyInference::Suggestion suggestion; + suggestion.nodeId = params.value("nodeId", ""); + suggestion.annotationType = params.value("annotationType", ""); + suggestion.strategy = params.value("strategy", ""); + suggestion.reason = params.value("reason", ""); + suggestion.confidence = params.value("confidence", 0.0); + + AgentAnnotationAssistant assistant; + auto res = assistant.applySuggestion(ast, suggestion); + if (!res.success) { + response["error"] = {{"code", -32010}, {"message", res.error}}; + return response; + } + + if (params.contains("accepted")) { + bool accepted = params.value("accepted", true); + assistant.recordFeedback(suggestion, accepted); + } + + applyOrchestratorToActive(); + response["result"] = { + {"success", true}, + {"warning", res.warning} + }; + return response; + } + + if (method == "recordAnnotationFeedback") { + auto params = request.contains("params") ? request["params"] : json::object(); + MemoryStrategyInference::Suggestion suggestion; + suggestion.nodeId = params.value("nodeId", ""); + suggestion.annotationType = params.value("annotationType", ""); + suggestion.strategy = params.value("strategy", ""); + suggestion.reason = params.value("reason", ""); + suggestion.confidence = params.value("confidence", 0.0); + + bool accepted = params.value("accepted", false); + AgentAnnotationAssistant assistant; + assistant.recordFeedback(suggestion, accepted); + response["result"] = true; + return response; + } + if (method == "applyMutation") { auto permIt = agentMutationPermissions.find(sessionId); if (permIt == agentMutationPermissions.end() || !permIt->second) { diff --git a/editor/src/MemoryStrategyInference.h b/editor/src/MemoryStrategyInference.h index bd643cd..67d8195 100644 --- a/editor/src/MemoryStrategyInference.h +++ b/editor/src/MemoryStrategyInference.h @@ -8,6 +8,8 @@ #include #include +#include +#include #include "ast/ASTNode.h" #include "ast/Module.h" #include "ast/Function.h" @@ -25,15 +27,32 @@ public: double confidence; // 0.0 – 1.0 }; + struct FeedbackStats { + int accepted = 0; + int rejected = 0; + }; + // Analyse the AST and return suggestions. Does NOT modify the AST. std::vector inferAnnotations(const ASTNode* root) const { std::vector out; if (!root) return out; inferNode(root, out); + applyFeedback(out); return out; } + void recordFeedback(const Suggestion& suggestion, bool accepted) const { + auto& stats = feedback_[feedbackKey(suggestion)]; + if (accepted) { + stats.accepted += 1; + } else { + stats.rejected += 1; + } + } + private: + inline static std::unordered_map feedback_; + void inferNode(const ASTNode* node, std::vector& out) const { if (!node) return; @@ -169,4 +188,22 @@ private: scanForAllocDealloc(child, hasAlloc, hasDealloc); } } + + static std::string feedbackKey(const Suggestion& suggestion) { + return suggestion.annotationType + ":" + suggestion.strategy; + } + + void applyFeedback(std::vector& out) const { + for (auto& s : out) { + auto it = feedback_.find(feedbackKey(s)); + if (it == feedback_.end()) continue; + const auto& stats = it->second; + int total = stats.accepted + stats.rejected; + if (total <= 0) continue; + double bias = (double)(stats.accepted - stats.rejected) / + (double)total; + double factor = 1.0 + 0.25 * bias; + s.confidence = std::clamp(s.confidence * factor, 0.0, 1.0); + } + } }; diff --git a/editor/tests/step156_test.cpp b/editor/tests/step156_test.cpp new file mode 100644 index 0000000..00994e2 --- /dev/null +++ b/editor/tests/step156_test.cpp @@ -0,0 +1,93 @@ +// Step 156 TDD Test: Agent annotation assistant +#include "AgentAnnotationAssistant.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Annotation.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; + } +} + +static bool hasAnnotationType(const ASTNode* node, const std::string& type) { + if (!node) return false; + for (auto* anno : node->getChildren("annotations")) { + if (anno->conceptType == type) return true; + } + return false; +} + +int main() { + int passed = 0; + int failed = 0; + + Module mod; + mod.id = "mod1"; + mod.name = "TestModule"; + mod.targetLanguage = "cpp"; + + auto* fn = new Function(); + fn->id = "fn1"; + fn->name = "doWork"; + auto* da = new DeallocateAnnotation("da1", "Explicit"); + fn->addChild("annotations", da); + mod.addChild("functions", fn); + + MemoryStrategyInference inf; + auto base = inf.inferAnnotations(&mod); + double baseConf = 0.0; + MemoryStrategyInference::Suggestion baseSug; + for (const auto& s : base) { + if (s.nodeId == mod.id && s.annotationType == "LifetimeAnnotation") { + baseConf = s.confidence; + baseSug = s; + break; + } + } + expect(baseConf > 0.0, "module suggestion present", passed, failed); + + inf.recordFeedback(baseSug, true); + auto after = inf.inferAnnotations(&mod); + double afterConf = 0.0; + for (const auto& s : after) { + if (s.nodeId == mod.id && s.annotationType == "LifetimeAnnotation") { + afterConf = s.confidence; + break; + } + } + expect(afterConf >= baseConf, "confidence increases with positive feedback", passed, failed); + + AgentAnnotationAssistant assistant; + auto result = assistant.suggest(&mod, fn->id); + bool hasMissingIntent = false; + for (const auto& d : result.diagnostics) { + if (d.nodeId == fn->id && + d.message.find("Missing Intent") != std::string::npos) { + hasMissingIntent = true; + break; + } + } + expect(hasMissingIntent, "diagnostics scoped to function", passed, failed); + + MemoryStrategyInference::Suggestion applySug; + applySug.nodeId = fn->id; + applySug.annotationType = "LifetimeAnnotation"; + applySug.strategy = "RAII"; + applySug.reason = "test"; + applySug.confidence = 0.6; + + auto applyRes = assistant.applySuggestion(&mod, applySug); + expect(applyRes.success, "apply suggestion mutation succeeds", passed, failed); + expect(hasAnnotationType(fn, "LifetimeAnnotation"), + "annotation inserted on target", passed, failed); + + std::cout << "\n=== Step 156 Results: " << passed << " passed, " + << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint5_plan.md b/sprint5_plan.md index 0edfc68..ea4cadb 100644 --- a/sprint5_plan.md +++ b/sprint5_plan.md @@ -305,7 +305,7 @@ primitives, and assist with constructive coding. Returns AST nodes that the agent can review before insertion. *New:* `AgentCodeGen.h` -- [ ] **Step 156: Agent annotation assistant** +- [x] **Step 156: Agent annotation assistant** Agent can request annotation suggestions for a code region. Whetstone runs `MemoryStrategyInference` and `AnnotationValidator`, returns suggestions with confidence scores. Agent can apply suggestions via