Step 157: add multi-agent roles and provenance
This commit is contained in:
@@ -883,6 +883,9 @@ 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)
|
||||
|
||||
add_executable(step157_test tests/step157_test.cpp)
|
||||
target_include_directories(step157_test PRIVATE src)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
55
editor/src/AgentPermissionPolicy.h
Normal file
55
editor/src/AgentPermissionPolicy.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
// Step 157: Agent role permissions
|
||||
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
enum class AgentRole {
|
||||
Linter = 0,
|
||||
Refactor = 1,
|
||||
Generator = 2
|
||||
};
|
||||
|
||||
struct AgentPermissionPolicy {
|
||||
static const char* roleLabel(AgentRole role) {
|
||||
switch (role) {
|
||||
case AgentRole::Linter: return "Linter";
|
||||
case AgentRole::Refactor: return "Refactor";
|
||||
case AgentRole::Generator: return "Generator";
|
||||
}
|
||||
return "Linter";
|
||||
}
|
||||
|
||||
static AgentRole roleFromString(const std::string& role) {
|
||||
std::string lower = role;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
if (lower == "refactor") return AgentRole::Refactor;
|
||||
if (lower == "generator") return AgentRole::Generator;
|
||||
return AgentRole::Linter;
|
||||
}
|
||||
|
||||
static bool canInvoke(AgentRole role, const std::string& method) {
|
||||
if (method == "getAST" ||
|
||||
method == "getAnnotationSuggestions" ||
|
||||
method == "recordAnnotationFeedback" ||
|
||||
method == "setAgentRole") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (method == "generateCode") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
if (method == "applyMutation" ||
|
||||
method == "applyAnnotationSuggestion") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool canMutate(AgentRole role) {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
};
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "AgentLibraryPolicy.h"
|
||||
#include "AgentCodeGen.h"
|
||||
#include "AgentAnnotationAssistant.h"
|
||||
#include "AgentPermissionPolicy.h"
|
||||
#include "CompositionPanel.h"
|
||||
#include "IncrementalOptimizer.h"
|
||||
#include "EmacsIntegration.h"
|
||||
@@ -174,7 +175,7 @@ struct EditorState {
|
||||
MockWebSocketTransport* agentTransport = nullptr;
|
||||
int agentPort = 8765;
|
||||
std::vector<std::string> agentLog;
|
||||
std::map<std::string, bool> agentMutationPermissions;
|
||||
std::map<std::string, AgentRole> agentRoles;
|
||||
BuildSystem::Type buildType = BuildSystem::Type::None;
|
||||
std::vector<BuildError> buildErrors;
|
||||
std::string lastBuildOutput;
|
||||
@@ -910,9 +911,9 @@ struct EditorState {
|
||||
const std::string& event) {
|
||||
logAgentEvent("Agent " + s.sessionId + " " + event);
|
||||
if (event == "connected") {
|
||||
agentMutationPermissions[s.sessionId] = false;
|
||||
agentRoles[s.sessionId] = AgentRole::Linter;
|
||||
} else if (event == "disconnected") {
|
||||
agentMutationPermissions.erase(s.sessionId);
|
||||
agentRoles.erase(s.sessionId);
|
||||
}
|
||||
});
|
||||
agentServer->setRequestLogCallback([this](const std::string& sid,
|
||||
@@ -937,6 +938,27 @@ struct EditorState {
|
||||
agentLog.push_back(msg);
|
||||
}
|
||||
|
||||
AgentRole getAgentRole(const std::string& sessionId) const {
|
||||
auto it = agentRoles.find(sessionId);
|
||||
return it != agentRoles.end() ? it->second : AgentRole::Linter;
|
||||
}
|
||||
|
||||
void setAgentRole(const std::string& sessionId, AgentRole role) {
|
||||
agentRoles[sessionId] = role;
|
||||
}
|
||||
|
||||
std::string agentActorLabel(const std::string& sessionId) const {
|
||||
std::string label = "agent:" + sessionId;
|
||||
if (agentServer) {
|
||||
if (const auto* sess = agentServer->getSession(sessionId)) {
|
||||
if (!sess->agentName.empty()) {
|
||||
label = "agent:" + sess->agentName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
void refreshBuildSystem() {
|
||||
buildType = BuildSystem::detect(workspaceRoot);
|
||||
}
|
||||
@@ -1108,6 +1130,7 @@ struct EditorState {
|
||||
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
|
||||
|
||||
std::string method = request.value("method", "");
|
||||
AgentRole role = getAgentRole(sessionId);
|
||||
if (method == "getAST") {
|
||||
if (!active() || !isStructured()) {
|
||||
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
|
||||
@@ -1127,6 +1150,10 @@ struct EditorState {
|
||||
}
|
||||
|
||||
if (method == "generateCode") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method)) {
|
||||
response["error"] = {{"code", -32031}, {"message", "Role not permitted"}};
|
||||
return response;
|
||||
}
|
||||
if (!active() || !isStructured()) {
|
||||
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
|
||||
return response;
|
||||
@@ -1162,7 +1189,22 @@ struct EditorState {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (method == "setAgentRole") {
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string roleName = params.value("role", "linter");
|
||||
AgentRole newRole = AgentPermissionPolicy::roleFromString(roleName);
|
||||
setAgentRole(sessionId, newRole);
|
||||
response["result"] = {
|
||||
{"role", AgentPermissionPolicy::roleLabel(newRole)}
|
||||
};
|
||||
return response;
|
||||
}
|
||||
|
||||
if (method == "getAnnotationSuggestions") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method)) {
|
||||
response["error"] = {{"code", -32031}, {"message", "Role not permitted"}};
|
||||
return response;
|
||||
}
|
||||
if (!active() || !isStructured()) {
|
||||
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
|
||||
return response;
|
||||
@@ -1220,9 +1262,8 @@ struct EditorState {
|
||||
}
|
||||
|
||||
if (method == "applyAnnotationSuggestion") {
|
||||
auto permIt = agentMutationPermissions.find(sessionId);
|
||||
if (permIt == agentMutationPermissions.end() || !permIt->second) {
|
||||
response["error"] = {{"code", -32030}, {"message", "Mutation not permitted"}};
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method)) {
|
||||
response["error"] = {{"code", -32031}, {"message", "Role not permitted"}};
|
||||
return response;
|
||||
}
|
||||
if (!active() || !isStructured()) {
|
||||
@@ -1256,6 +1297,18 @@ struct EditorState {
|
||||
}
|
||||
|
||||
applyOrchestratorToActive();
|
||||
if (active()) {
|
||||
std::vector<std::string> affected;
|
||||
if (!suggestion.nodeId.empty()) affected.push_back(suggestion.nodeId);
|
||||
if (!suggestion.annotationType.empty() && !suggestion.nodeId.empty()) {
|
||||
affected.push_back("anno_" + suggestion.annotationType + "_" + suggestion.nodeId);
|
||||
}
|
||||
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
|
||||
active()->incrementalOptimizer.recordExternalTransform(
|
||||
"agent-annotation",
|
||||
affected,
|
||||
agentActorLabel(sessionId));
|
||||
}
|
||||
response["result"] = {
|
||||
{"success", true},
|
||||
{"warning", res.warning}
|
||||
@@ -1264,6 +1317,10 @@ struct EditorState {
|
||||
}
|
||||
|
||||
if (method == "recordAnnotationFeedback") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method)) {
|
||||
response["error"] = {{"code", -32031}, {"message", "Role not permitted"}};
|
||||
return response;
|
||||
}
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
MemoryStrategyInference::Suggestion suggestion;
|
||||
suggestion.nodeId = params.value("nodeId", "");
|
||||
@@ -1280,9 +1337,8 @@ struct EditorState {
|
||||
}
|
||||
|
||||
if (method == "applyMutation") {
|
||||
auto permIt = agentMutationPermissions.find(sessionId);
|
||||
if (permIt == agentMutationPermissions.end() || !permIt->second) {
|
||||
response["error"] = {{"code", -32030}, {"message", "Mutation not permitted"}};
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method)) {
|
||||
response["error"] = {{"code", -32031}, {"message", "Role not permitted"}};
|
||||
return response;
|
||||
}
|
||||
if (!active() || !isStructured()) {
|
||||
@@ -1302,8 +1358,11 @@ struct EditorState {
|
||||
mut.setRoot(ast);
|
||||
ASTMutationAPI::MutationResult res;
|
||||
LibraryPolicyResult policy;
|
||||
std::vector<std::string> affectedIds;
|
||||
|
||||
if (type == "setProperty") {
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.setProperty(params.value("nodeId", ""),
|
||||
params.value("property", ""),
|
||||
params.value("value", ""));
|
||||
@@ -1314,13 +1373,19 @@ struct EditorState {
|
||||
props[k] = v.get<std::string>();
|
||||
}
|
||||
}
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.updateNode(params.value("nodeId", ""), props);
|
||||
} else if (type == "deleteNode") {
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.deleteNode(params.value("nodeId", ""));
|
||||
} else if (type == "insertNode") {
|
||||
ASTNode* node = nullptr;
|
||||
std::string insertedId;
|
||||
if (params.contains("node")) {
|
||||
node = fromJson(params["node"]);
|
||||
if (node) insertedId = node->id;
|
||||
}
|
||||
if (node) {
|
||||
policy = checkMutationLibraryPolicy(node, ast, preferImports, strictMode);
|
||||
@@ -1334,6 +1399,8 @@ struct EditorState {
|
||||
params.value("role", ""), node);
|
||||
if (!res.success && node) {
|
||||
deleteTree(node);
|
||||
} else if (res.success && !insertedId.empty()) {
|
||||
affectedIds.push_back(insertedId);
|
||||
}
|
||||
} else {
|
||||
response["error"] = {{"code", -32602}, {"message", "Unknown mutation type"}};
|
||||
@@ -1346,6 +1413,16 @@ struct EditorState {
|
||||
}
|
||||
|
||||
applyOrchestratorToActive();
|
||||
if (active()) {
|
||||
if (affectedIds.empty() && !params.value("nodeId", "").empty()) {
|
||||
affectedIds.push_back(params.value("nodeId", ""));
|
||||
}
|
||||
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
|
||||
active()->incrementalOptimizer.recordExternalTransform(
|
||||
"agent-mutation:" + type,
|
||||
affectedIds,
|
||||
agentActorLabel(sessionId));
|
||||
}
|
||||
response["result"] = {
|
||||
{"success", true},
|
||||
{"warning", res.warning},
|
||||
|
||||
@@ -18,6 +18,8 @@ public:
|
||||
std::string transformName;
|
||||
std::vector<std::string> affectedNodeIds;
|
||||
std::string timestamp;
|
||||
std::string actor;
|
||||
bool reversible = true;
|
||||
};
|
||||
|
||||
void setRoot(ASTNode* root) { root_ = root; }
|
||||
@@ -29,6 +31,7 @@ public:
|
||||
record.info.transformId = tid;
|
||||
record.info.transformName = transformName;
|
||||
record.info.timestamp = currentTimestamp();
|
||||
record.info.actor = "optimizer";
|
||||
|
||||
if (transformName == "constant-fold") {
|
||||
applyConstantFolding(root_, tid, record);
|
||||
@@ -48,9 +51,30 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string recordExternalTransform(const std::string& transformName,
|
||||
const std::vector<std::string>& affectedNodeIds,
|
||||
const std::string& actor) {
|
||||
std::string tid = "t" + std::to_string(nextId_++);
|
||||
InternalRecord record;
|
||||
record.info.transformId = tid;
|
||||
record.info.transformName = transformName;
|
||||
record.info.timestamp = currentTimestamp();
|
||||
record.info.actor = actor;
|
||||
record.info.reversible = false;
|
||||
record.info.affectedNodeIds = affectedNodeIds;
|
||||
|
||||
for (const auto& nodeId : affectedNodeIds) {
|
||||
provenance_[nodeId] = tid;
|
||||
}
|
||||
|
||||
history_.push_back(std::move(record));
|
||||
return tid;
|
||||
}
|
||||
|
||||
bool undoTransform(const std::string& transformId) {
|
||||
for (auto it = history_.begin(); it != history_.end(); ++it) {
|
||||
if (it->info.transformId == transformId) {
|
||||
if (!it->info.reversible) return false;
|
||||
// Apply undo actions in reverse order
|
||||
for (auto ait = it->undoActions.rbegin(); ait != it->undoActions.rend(); ++ait) {
|
||||
ait->undo();
|
||||
|
||||
@@ -1901,10 +1901,12 @@ int main(int, char**) {
|
||||
if (!s.agentName.empty()) label += " (" + s.agentName + ")";
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::SameLine(260.0f);
|
||||
bool canMutate = state.agentMutationPermissions[s.sessionId];
|
||||
std::string checkId = "Can mutate##" + s.sessionId;
|
||||
if (ImGui::Checkbox(checkId.c_str(), &canMutate)) {
|
||||
state.agentMutationPermissions[s.sessionId] = canMutate;
|
||||
AgentRole role = state.getAgentRole(s.sessionId);
|
||||
int roleIndex = static_cast<int>(role);
|
||||
const char* roleLabels[] = {"Linter", "Refactor", "Generator"};
|
||||
std::string comboId = "Role##" + s.sessionId;
|
||||
if (ImGui::Combo(comboId.c_str(), &roleIndex, roleLabels, 3)) {
|
||||
state.setAgentRole(s.sessionId, static_cast<AgentRole>(roleIndex));
|
||||
}
|
||||
ImGui::SameLine(420.0f);
|
||||
ImGui::Text("Msgs: %d", s.messageCount);
|
||||
@@ -2248,12 +2250,18 @@ int main(int, char**) {
|
||||
ImGui::TextColored(color, "%s", h.transformName.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("@ %s", h.timestamp.c_str());
|
||||
if (!h.actor.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("by %s", h.actor.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (!h.reversible) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Undo")) {
|
||||
if (inc.undoTransform(h.transformId)) {
|
||||
state.applyOrchestratorToActive();
|
||||
}
|
||||
}
|
||||
if (!h.reversible) ImGui::EndDisabled();
|
||||
ImGui::Text("Affected: %d", (int)h.affectedNodeIds.size());
|
||||
std::string nodeList;
|
||||
for (size_t i = 0; i < h.affectedNodeIds.size() && i < 5; ++i) {
|
||||
@@ -2385,10 +2393,12 @@ int main(int, char**) {
|
||||
ImGui::TextDisabled("(disabled in Text mode)");
|
||||
} else if (ast) {
|
||||
std::map<std::string, std::string> transformNames;
|
||||
std::map<std::string, std::string> transformActors;
|
||||
if (state.active()) {
|
||||
auto history = state.active()->incrementalOptimizer.getTransformHistory();
|
||||
for (const auto& h : history) {
|
||||
transformNames[h.transformId] = h.transformName;
|
||||
transformActors[h.transformId] = h.actor;
|
||||
}
|
||||
}
|
||||
// Show basic AST info
|
||||
@@ -2412,7 +2422,13 @@ int main(int, char**) {
|
||||
ImGui::TextColored(fnColor, " Function: %s", fn->name.c_str());
|
||||
if (!tname.empty() && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Transform: %s (%s)", tname.c_str(), tid.c_str());
|
||||
std::string actor = transformActors[tid];
|
||||
if (actor.empty()) {
|
||||
ImGui::Text("Transform: %s (%s)", tname.c_str(), tid.c_str());
|
||||
} else {
|
||||
ImGui::Text("Transform: %s (%s) by %s",
|
||||
tname.c_str(), tid.c_str(), actor.c_str());
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
auto params = fn->getChildren("parameters");
|
||||
@@ -2431,7 +2447,13 @@ int main(int, char**) {
|
||||
ImGui::TextColored(pColor, " param: %s", param->name.c_str());
|
||||
if (!pname.empty() && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Transform: %s (%s)", pname.c_str(), pid.c_str());
|
||||
std::string actor = transformActors[pid];
|
||||
if (actor.empty()) {
|
||||
ImGui::Text("Transform: %s (%s)", pname.c_str(), pid.c_str());
|
||||
} else {
|
||||
ImGui::Text("Transform: %s (%s) by %s",
|
||||
pname.c_str(), pid.c_str(), actor.c_str());
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
61
editor/tests/step157_test.cpp
Normal file
61
editor/tests/step157_test.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
// Step 157 TDD Test: Agent roles + transform provenance
|
||||
#include "AgentPermissionPolicy.h"
|
||||
#include "IncrementalOptimizer.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
|
||||
expect(!AgentPermissionPolicy::canInvoke(AgentRole::Linter, "applyMutation"),
|
||||
"linter cannot mutate", passed, failed);
|
||||
expect(AgentPermissionPolicy::canInvoke(AgentRole::Refactor, "applyMutation"),
|
||||
"refactor can mutate", passed, failed);
|
||||
expect(AgentPermissionPolicy::canInvoke(AgentRole::Generator, "generateCode"),
|
||||
"generator can generate code", passed, failed);
|
||||
|
||||
Module mod;
|
||||
mod.id = "mod1";
|
||||
mod.name = "Test";
|
||||
mod.targetLanguage = "cpp";
|
||||
auto* fn = new Function();
|
||||
fn->id = "fn1";
|
||||
fn->name = "work";
|
||||
mod.addChild("functions", fn);
|
||||
|
||||
IncrementalOptimizer inc;
|
||||
inc.setRoot(&mod);
|
||||
std::string tid = inc.recordExternalTransform(
|
||||
"agent-mutation:setProperty",
|
||||
{fn->id},
|
||||
"agent:test");
|
||||
|
||||
auto history = inc.getTransformHistory();
|
||||
bool found = false;
|
||||
for (const auto& h : history) {
|
||||
if (h.transformId == tid) {
|
||||
found = true;
|
||||
expect(h.actor == "agent:test", "actor recorded", passed, failed);
|
||||
expect(!h.reversible, "external transform not reversible", passed, failed);
|
||||
}
|
||||
}
|
||||
expect(found, "external transform in history", passed, failed);
|
||||
expect(inc.getProvenance(fn->id) == tid, "provenance recorded", passed, failed);
|
||||
expect(!inc.undoTransform(tid), "external transform cannot undo", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 157 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user