From acada1bec514fa8fd12aa88f9a4f38d512eb2bd1 Mon Sep 17 00:00:00 2001 From: scawful Date: Sat, 4 Oct 2025 05:18:09 -0400 Subject: [PATCH] feat: Add proposal executor for agent response handling and command execution --- src/cli/agent.cmake | 1 + src/cli/handlers/agent/general_commands.cc | 140 +++----------- .../agent/conversational_agent_service.cc | 74 +++++++ src/cli/service/agent/proposal_executor.cc | 182 ++++++++++++++++++ src/cli/service/agent/proposal_executor.h | 40 ++++ 5 files changed, 320 insertions(+), 117 deletions(-) create mode 100644 src/cli/service/agent/proposal_executor.cc create mode 100644 src/cli/service/agent/proposal_executor.h diff --git a/src/cli/agent.cmake b/src/cli/agent.cmake index 36605237..569d2aa9 100644 --- a/src/cli/agent.cmake +++ b/src/cli/agent.cmake @@ -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 diff --git a/src/cli/handlers/agent/general_commands.cc b/src/cli/handlers/agent/general_commands.cc index 645684fd..8c0a0bc4 100644 --- a/src/cli/handlers/agent/general_commands.cc +++ b/src/cli/handlers/agent/general_commands.cc @@ -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& arg_vec, RETURN_IF_ERROR(EnsureRomLoaded(rom, "agent run --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& 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"; diff --git a/src/cli/service/agent/conversational_agent_service.cc b/src/cli/service/agent/conversational_agent_service.cc index 8909a493..d119850a 100644 --- a/src/cli/service/agent/conversational_agent_service.cc +++ b/src/cli/service/agent/conversational_agent_service.cc @@ -3,17 +3,26 @@ #include #include #include +#include #include #include #include +#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 ConversationalAgentService::SendMessage( continue; } + std::optional 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 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); diff --git a/src/cli/service/agent/proposal_executor.cc b/src/cli/service/agent/proposal_executor.cc new file mode 100644 index 00000000..55b4c7c0 --- /dev/null +++ b/src/cli/service/agent/proposal_executor.cc @@ -0,0 +1,182 @@ +#include "cli/service/agent/proposal_executor.h" + +#include +#include +#include +#include + +#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 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 call_summaries; + call_summaries.reserve(request.response->tool_calls.size()); + for (const auto& tool_call : request.response->tool_calls) { + std::vector 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(proposal.changes.size()); + return result; +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/service/agent/proposal_executor.h b/src/cli/service/agent/proposal_executor.h new file mode 100644 index 00000000..c0f552ea --- /dev/null +++ b/src/cli/service/agent/proposal_executor.h @@ -0,0 +1,40 @@ +#ifndef YAZE_SRC_CLI_SERVICE_AGENT_PROPOSAL_EXECUTOR_H_ +#define YAZE_SRC_CLI_SERVICE_AGENT_PROPOSAL_EXECUTOR_H_ + +#include +#include + +#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 CreateProposalFromAgentResponse( + const ProposalCreationRequest& request); + +} // namespace agent +} // namespace cli +} // namespace yaze + +#endif // YAZE_SRC_CLI_SERVICE_AGENT_PROPOSAL_EXECUTOR_H_