feat: Add proposal executor for agent response handling and command execution

This commit is contained in:
scawful
2025-10-04 05:18:09 -04:00
parent 8deb2656d5
commit acada1bec5
5 changed files with 320 additions and 117 deletions

View File

@@ -65,6 +65,7 @@ endfunction()
_yaze_ensure_yaml_cpp(YAZE_YAML_CPP_TARGET)
set(YAZE_AGENT_SOURCES
cli/service/agent/proposal_executor.cc
cli/handlers/agent/tool_commands.cc
cli/service/agent/conversational_agent_service.cc
cli/service/agent/simple_chat_session.cc

View File

@@ -28,6 +28,7 @@
#include "cli/service/ai/gemini_ai_service.h"
#include "cli/service/ai/ollama_ai_service.h"
#include "cli/service/ai/service_factory.h"
#include "cli/service/agent/proposal_executor.h"
#include "cli/service/agent/simple_chat_session.h"
#include "cli/service/planning/proposal_registry.h"
#include "cli/service/planning/tile16_proposal_generator.h"
@@ -39,6 +40,7 @@
#include "util/macro.h"
ABSL_DECLARE_FLAG(std::string, rom);
ABSL_DECLARE_FLAG(std::string, ai_provider);
namespace yaze {
namespace cli {
@@ -186,143 +188,47 @@ absl::Status HandleRunCommand(const std::vector<std::string>& arg_vec,
RETURN_IF_ERROR(EnsureRomLoaded(rom, "agent run --prompt \"<prompt>\""));
// 1. Create a sandbox ROM to apply changes to
auto sandbox_or =
RomSandboxManager::Instance().CreateSandbox(rom, "agent-run");
if (!sandbox_or.ok()) {
return sandbox_or.status();
}
auto sandbox = sandbox_or.value();
// 2. Get commands from the AI service
// Get commands from the AI service
auto ai_service = CreateAIService(); // Use service factory
auto response_or = ai_service->GenerateResponse(prompt);
if (!response_or.ok()) {
return response_or.status();
}
AgentResponse response = std::move(response_or.value());
const std::vector<std::string>& commands = response.commands;
// 3. Generate a structured proposal from the commands
Tile16ProposalGenerator generator;
auto proposal_or = generator.GenerateFromCommands(
prompt, commands, "ollama", &rom); // Pass original ROM to get old tiles
if (!proposal_or.ok()) {
return proposal_or.status();
}
auto proposal = proposal_or.value();
// 4. Apply the proposal to the sandbox ROM for preview
Rom sandbox_rom;
auto load_status = sandbox_rom.LoadFromFile(sandbox.rom_path.string());
if (!load_status.ok()) {
return absl::InternalError(
absl::StrCat("Failed to load sandbox ROM: ", load_status.message()));
if (response.commands.empty()) {
return absl::FailedPreconditionError(
"Agent response did not include any executable commands.");
}
auto apply_status = generator.ApplyProposal(proposal, &sandbox_rom);
if (!apply_status.ok()) {
return absl::InternalError(absl::StrCat(
"Failed to apply proposal to sandbox ROM: ", apply_status.message()));
}
std::string provider = absl::GetFlag(FLAGS_ai_provider);
// 5. Save the sandbox ROM to persist the changes for diffing
auto save_status = sandbox_rom.SaveToFile({.save_new = false});
if (!save_status.ok()) {
return absl::InternalError(
absl::StrCat("Failed to save sandbox ROM: ", save_status.message()));
}
ProposalCreationRequest request;
request.prompt = prompt;
request.response = &response;
request.rom = &rom;
request.sandbox_label = "agent-run";
request.ai_provider = std::move(provider);
// 6. Persist the proposal metadata and artifacts.
auto& registry = ProposalRegistry::Instance();
int executed_commands = 0;
for (const auto& command : commands) {
if (command.empty() || command[0] == '#') {
continue;
}
++executed_commands;
}
std::string description = absl::StrFormat(
"Tile16 overworld edits (%d change%s)", proposal.changes.size(),
proposal.changes.size() == 1 ? "" : "s");
ASSIGN_OR_RETURN(
auto metadata,
registry.CreateProposal(sandbox.id, sandbox.rom_path, prompt, description));
proposal.id = metadata.id;
std::ostringstream diff_stream;
diff_stream << "Tile16 Proposal ID: " << metadata.id << "\n";
diff_stream << "Sandbox ID: " << sandbox.id << "\n";
diff_stream << "Sandbox ROM: " << sandbox.rom_path << "\n\n";
diff_stream << "Changes (" << proposal.changes.size() << "):\n";
for (const auto& change : proposal.changes) {
diff_stream << " - " << change.ToString() << "\n";
}
RETURN_IF_ERROR(registry.RecordDiff(metadata.id, diff_stream.str()));
RETURN_IF_ERROR(registry.AppendLog(
metadata.id, absl::StrCat("Prompt: ", prompt)));
if (!response.text_response.empty()) {
RETURN_IF_ERROR(registry.AppendLog(
metadata.id, absl::StrCat("AI Response: ", response.text_response)));
}
if (!response.reasoning.empty()) {
RETURN_IF_ERROR(registry.AppendLog(
metadata.id, absl::StrCat("Reasoning: ", response.reasoning)));
}
if (!response.tool_calls.empty()) {
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Tool Calls: ", response.tool_calls.size())));
}
for (const auto& command : commands) {
if (command.empty() || command[0] == '#') {
continue;
}
RETURN_IF_ERROR(registry.AppendLog(
metadata.id, absl::StrCat("Command: ", command)));
}
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Sandbox ROM saved to ", sandbox.rom_path.string())));
RETURN_IF_ERROR(
registry.UpdateCommandStats(metadata.id, executed_commands));
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Commands executed: ", executed_commands)));
ASSIGN_OR_RETURN(auto proposal_result,
CreateProposalFromAgentResponse(request));
const auto& metadata = proposal_result.metadata;
std::filesystem::path proposal_dir = metadata.log_path.parent_path();
std::filesystem::path proposal_path = proposal_dir / "proposal.json";
auto save_proposal_status =
generator.SaveProposal(proposal, proposal_path.string());
if (!save_proposal_status.ok()) {
return absl::InternalError(absl::StrCat("Failed to save proposal file: ",
save_proposal_status.message()));
}
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Saved proposal JSON to ", proposal_path.string())));
std::cout
<< "✅ Agent successfully planned and executed changes in a sandbox."
<< std::endl;
std::cout << " Proposal ID: " << metadata.id << std::endl;
std::cout << " Sandbox ROM: " << sandbox.rom_path << std::endl;
std::cout << " Sandbox ROM: " << metadata.sandbox_rom_path << std::endl;
std::cout << " Proposal dir: " << proposal_dir << std::endl;
std::cout << " Diff file: " << metadata.diff_path << std::endl;
std::cout << " Log file: " << metadata.log_path << std::endl;
std::cout << " Proposal JSON: " << proposal_result.proposal_json_path
<< std::endl;
std::cout << " Commands executed: "
<< proposal_result.executed_commands << std::endl;
std::cout << " Tile16 changes: " << proposal_result.change_count
<< std::endl;
std::cout << "\nTo review the changes, run:\n";
std::cout << " z3ed agent diff --proposal-id " << metadata.id << std::endl;
std::cout << "\nTo accept the changes, run:\n";

View File

@@ -3,17 +3,26 @@
#include <algorithm>
#include <cctype>
#include <iostream>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "absl/flags/declare.h"
#include "absl/flags/flag.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/time/clock.h"
#include "app/rom.h"
#include "cli/service/agent/proposal_executor.h"
#include "cli/service/ai/service_factory.h"
#include "cli/util/terminal_colors.h"
#include "nlohmann/json.hpp"
ABSL_DECLARE_FLAG(std::string, ai_provider);
namespace yaze {
namespace cli {
namespace agent {
@@ -300,6 +309,47 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
continue;
}
std::optional<ProposalCreationResult> proposal_result;
absl::Status proposal_status = absl::OkStatus();
bool attempted_proposal = false;
if (!agent_response.commands.empty()) {
attempted_proposal = true;
if (rom_context_ == nullptr) {
proposal_status = absl::FailedPreconditionError(
"No ROM context available for proposal creation");
util::PrintWarning(
"Cannot create proposal because no ROM context is active.");
} else if (!rom_context_->is_loaded()) {
proposal_status = absl::FailedPreconditionError(
"ROM context is not loaded");
util::PrintWarning(
"Cannot create proposal because the ROM context is not loaded.");
} else {
ProposalCreationRequest request;
request.prompt = message;
request.response = &agent_response;
request.rom = rom_context_;
request.sandbox_label = "agent-chat";
request.ai_provider = absl::GetFlag(FLAGS_ai_provider);
auto creation_or = CreateProposalFromAgentResponse(request);
if (!creation_or.ok()) {
proposal_status = creation_or.status();
util::PrintError(absl::StrCat(
"Failed to create proposal: ", proposal_status.message()));
} else {
proposal_result = std::move(creation_or.value());
if (config_.verbose) {
util::PrintSuccess(absl::StrCat(
"Created proposal ", proposal_result->metadata.id,
" with ", proposal_result->change_count, " change(s)."));
}
}
}
}
std::string response_text = agent_response.text_response;
if (!agent_response.reasoning.empty()) {
if (!response_text.empty()) {
@@ -316,6 +366,30 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
response_text.append(absl::StrJoin(agent_response.commands, "\n"));
}
if (proposal_result.has_value()) {
const auto& metadata = proposal_result->metadata;
if (!response_text.empty()) {
response_text.append("\n\n");
}
response_text.append(absl::StrFormat(
"✅ Proposal %s ready with %d change%s (%d command%s).\n"
"Review it in the Proposal drawer or run `z3ed agent diff --proposal-id %s`.\n"
"Sandbox ROM: %s\nProposal JSON: %s",
metadata.id, proposal_result->change_count,
proposal_result->change_count == 1 ? "" : "s",
proposal_result->executed_commands,
proposal_result->executed_commands == 1 ? "" : "s",
metadata.id, metadata.sandbox_rom_path.string(),
proposal_result->proposal_json_path.string()));
} else if (attempted_proposal && !proposal_status.ok()) {
if (!response_text.empty()) {
response_text.append("\n\n");
}
response_text.append(absl::StrCat(
"⚠️ Failed to prepare a proposal automatically: ",
proposal_status.message()));
}
ChatMessage chat_response =
CreateMessage(ChatMessage::Sender::kAgent, response_text);
history_.push_back(chat_response);

View File

@@ -0,0 +1,182 @@
#include "cli/service/agent/proposal_executor.h"
#include <filesystem>
#include <sstream>
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "app/rom.h"
#include "cli/service/planning/tile16_proposal_generator.h"
#include "cli/service/rom/rom_sandbox_manager.h"
#include "util/macro.h"
namespace yaze {
namespace cli {
namespace agent {
namespace {
std::string InferProvider(const std::string& provider) {
if (!provider.empty()) {
return provider;
}
return "unknown";
}
bool IsExecutableCommand(absl::string_view command) {
return !command.empty() && command.front() != '#';
}
} // namespace
absl::StatusOr<ProposalCreationResult> CreateProposalFromAgentResponse(
const ProposalCreationRequest& request) {
if (request.response == nullptr) {
return absl::InvalidArgumentError("Agent response is required");
}
if (request.rom == nullptr) {
return absl::InvalidArgumentError("ROM context is required");
}
if (!request.rom->is_loaded()) {
return absl::FailedPreconditionError(
"ROM must be loaded before creating proposals");
}
if (request.response->commands.empty()) {
return absl::InvalidArgumentError(
"Agent response did not contain any commands to execute");
}
auto sandbox_or =
RomSandboxManager::Instance().CreateSandbox(*request.rom,
request.sandbox_label);
if (!sandbox_or.ok()) {
return sandbox_or.status();
}
auto sandbox = sandbox_or.value();
Tile16ProposalGenerator generator;
ASSIGN_OR_RETURN(auto proposal,
generator.GenerateFromCommands(
request.prompt, request.response->commands,
InferProvider(request.ai_provider), request.rom));
Rom sandbox_rom;
RETURN_IF_ERROR(
sandbox_rom.LoadFromFile(sandbox.rom_path.string()));
RETURN_IF_ERROR(generator.ApplyProposal(proposal, &sandbox_rom));
RETURN_IF_ERROR(sandbox_rom.SaveToFile({.save_new = false}));
auto& registry = ProposalRegistry::Instance();
int executed_commands = 0;
for (const auto& command : request.response->commands) {
if (IsExecutableCommand(command)) {
++executed_commands;
}
}
std::string description = absl::StrFormat(
"Tile16 overworld edits (%d change%s)", proposal.changes.size(),
proposal.changes.size() == 1 ? "" : "s");
ASSIGN_OR_RETURN(auto metadata,
registry.CreateProposal(sandbox.id, sandbox.rom_path,
request.prompt, description));
proposal.id = metadata.id;
std::ostringstream diff_stream;
diff_stream << "Tile16 Proposal ID: " << metadata.id << "\n";
diff_stream << "Sandbox ID: " << sandbox.id << "\n";
diff_stream << "Sandbox ROM: " << sandbox.rom_path << "\n\n";
diff_stream << "Changes (" << proposal.changes.size() << "):\n";
for (const auto& change : proposal.changes) {
diff_stream << " - " << change.ToString() << "\n";
}
RETURN_IF_ERROR(registry.RecordDiff(metadata.id, diff_stream.str()));
RETURN_IF_ERROR(registry.AppendLog(
metadata.id, absl::StrCat("Prompt: ", request.prompt)));
if (!request.response->text_response.empty()) {
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("AI Response: ", request.response->text_response)));
}
if (!request.response->reasoning.empty()) {
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Reasoning: ", request.response->reasoning)));
}
if (!request.response->tool_calls.empty()) {
std::vector<std::string> call_summaries;
call_summaries.reserve(request.response->tool_calls.size());
for (const auto& tool_call : request.response->tool_calls) {
std::vector<std::string> args;
for (const auto& [key, value] : tool_call.args) {
args.push_back(absl::StrCat(key, "=", value));
}
call_summaries.push_back(absl::StrCat(
tool_call.tool_name, "(", absl::StrJoin(args, ", "), ")"));
}
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Tool Calls (", call_summaries.size(), "): ",
absl::StrJoin(call_summaries, "; "))));
}
for (const auto& command : request.response->commands) {
if (!IsExecutableCommand(command)) {
continue;
}
RETURN_IF_ERROR(registry.AppendLog(
metadata.id, absl::StrCat("Command: ", command)));
}
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Sandbox ROM saved to ", sandbox.rom_path.string())));
RETURN_IF_ERROR(
registry.UpdateCommandStats(metadata.id, executed_commands));
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Commands executed: ", executed_commands)));
std::filesystem::path proposal_dir = metadata.log_path.parent_path();
std::filesystem::path proposal_path = proposal_dir / "proposal.json";
RETURN_IF_ERROR(generator.SaveProposal(proposal, proposal_path.string()));
RETURN_IF_ERROR(registry.AppendLog(
metadata.id,
absl::StrCat("Saved proposal JSON to ", proposal_path.string())));
if (!request.ai_provider.empty()) {
RETURN_IF_ERROR(registry.AppendLog(
metadata.id, absl::StrCat("AI Provider: ", request.ai_provider)));
}
ASSIGN_OR_RETURN(auto refreshed_metadata,
registry.GetProposal(metadata.id));
ProposalCreationResult result;
result.metadata = std::move(refreshed_metadata);
result.proposal_json_path = std::move(proposal_path);
result.executed_commands = executed_commands;
result.change_count = static_cast<int>(proposal.changes.size());
return result;
}
} // namespace agent
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,40 @@
#ifndef YAZE_SRC_CLI_SERVICE_AGENT_PROPOSAL_EXECUTOR_H_
#define YAZE_SRC_CLI_SERVICE_AGENT_PROPOSAL_EXECUTOR_H_
#include <filesystem>
#include <string>
#include "absl/status/statusor.h"
#include "cli/service/ai/common.h"
#include "cli/service/planning/proposal_registry.h"
namespace yaze {
class Rom;
namespace cli {
namespace agent {
struct ProposalCreationRequest {
std::string prompt;
const AgentResponse* response = nullptr;
Rom* rom = nullptr;
std::string sandbox_label;
std::string ai_provider;
};
struct ProposalCreationResult {
ProposalRegistry::ProposalMetadata metadata;
std::filesystem::path proposal_json_path;
int executed_commands = 0;
int change_count = 0;
};
absl::StatusOr<ProposalCreationResult> CreateProposalFromAgentResponse(
const ProposalCreationRequest& request);
} // namespace agent
} // namespace cli
} // namespace yaze
#endif // YAZE_SRC_CLI_SERVICE_AGENT_PROPOSAL_EXECUTOR_H_