Step 157: add multi-agent roles and provenance

This commit is contained in:
Bill
2026-02-09 19:33:42 -07:00
parent d12757b38a
commit 9624e8e16f
8 changed files with 259 additions and 16 deletions

View File

@@ -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)

View 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;
}
};

View File

@@ -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},

View File

@@ -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();

View File

@@ -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();
}
}

View 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;
}