Files
yaze/src/cli/handlers/agent/general_commands.cc

695 lines
23 KiB
C++
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "cli/handlers/agent/commands.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "absl/flags/declare.h"
#include "absl/flags/flag.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/string_view.h"
#include "app/core/project.h"
#include "app/zelda3/dungeon/room.h"
#include "cli/handlers/agent/common.h"
#include "cli/modern_cli.h"
#include "cli/service/ai/ai_service.h"
#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"
#include "cli/service/resources/resource_catalog.h"
#include "cli/service/resources/resource_context_builder.h"
#include "cli/service/rom/rom_sandbox_manager.h"
#include "cli/tui/chat_tui.h"
#include "cli/z3ed.h"
#include "util/macro.h"
ABSL_DECLARE_FLAG(std::string, rom);
ABSL_DECLARE_FLAG(std::string, ai_provider);
namespace yaze {
namespace cli {
namespace agent {
namespace {
struct DescribeOptions {
std::optional<std::string> resource;
std::string format = "json";
std::optional<std::string> output_path;
std::string version = "0.1.0";
std::optional<std::string> last_updated;
};
// Helper to load project and labels if available
absl::Status TryLoadProjectAndLabels(Rom& rom) {
// Try to find and load a project file in current directory
core::YazeProject project;
auto project_status = project.Open(".");
if (project_status.ok()) {
std::cout << "📂 Loaded project: " << project.name << "\n";
// Initialize embedded labels (all default Zelda3 resource names)
auto labels_status = project.InitializeEmbeddedLabels();
if (labels_status.ok()) {
std::cout << "✅ Embedded labels initialized (all Zelda3 resources available)\n";
}
// Load labels from project (either embedded or external)
if (!project.labels_filename.empty()) {
auto* label_mgr = rom.resource_label();
if (label_mgr && label_mgr->LoadLabels(project.labels_filename)) {
std::cout << "🏷️ Loaded custom labels from: " << project.labels_filename << "\n";
}
} else if (!project.resource_labels.empty() || project.use_embedded_labels) {
// Use labels embedded in project or default Zelda3 labels
auto* label_mgr = rom.resource_label();
if (label_mgr) {
label_mgr->labels_ = project.resource_labels;
label_mgr->labels_loaded_ = true;
std::cout << "🏷️ Using embedded Zelda3 labels (rooms, sprites, entrances, items, etc.)\n";
}
}
} else {
// No project found - use embedded defaults anyway
std::cout << " No project file found. Using embedded default Zelda3 labels.\n";
project.InitializeEmbeddedLabels();
}
return absl::OkStatus();
}
absl::Status EnsureRomLoaded(Rom& rom, const std::string& command) {
if (rom.is_loaded()) {
return absl::OkStatus();
}
std::string rom_path = absl::GetFlag(FLAGS_rom);
if (rom_path.empty()) {
return absl::FailedPreconditionError(
absl::StrFormat(
"No ROM loaded. Pass --rom=<path> when running %s.\n"
"Example: z3ed %s --rom=zelda3.sfc",
command, command));
}
// Load the ROM
auto status = rom.LoadFromFile(rom_path);
if (!status.ok()) {
return absl::FailedPreconditionError(absl::StrFormat(
"Failed to load ROM from '%s': %s", rom_path, status.message()));
}
return absl::OkStatus();
}
absl::StatusOr<DescribeOptions> ParseDescribeArgs(
const std::vector<std::string>& args) {
DescribeOptions options;
for (size_t i = 0; i < args.size(); ++i) {
const std::string& token = args[i];
std::string flag = token;
std::optional<std::string> inline_value;
if (absl::StartsWith(token, "--")) {
auto eq_pos = token.find('=');
if (eq_pos != std::string::npos) {
flag = token.substr(0, eq_pos);
inline_value = token.substr(eq_pos + 1);
}
}
auto require_value =
[&](absl::string_view flag_name) -> absl::StatusOr<std::string> {
if (inline_value.has_value()) {
return *inline_value;
}
if (i + 1 >= args.size()) {
return absl::InvalidArgumentError(
absl::StrFormat("Flag %s requires a value", flag_name));
}
return args[++i];
};
if (flag == "--resource") {
ASSIGN_OR_RETURN(auto value, require_value("--resource"));
options.resource = std::move(value);
} else if (flag == "--format") {
ASSIGN_OR_RETURN(auto value, require_value("--format"));
options.format = std::move(value);
} else if (flag == "--output") {
ASSIGN_OR_RETURN(auto value, require_value("--output"));
options.output_path = std::move(value);
} else if (flag == "--version") {
ASSIGN_OR_RETURN(auto value, require_value("--version"));
options.version = std::move(value);
} else if (flag == "--last-updated") {
ASSIGN_OR_RETURN(auto value, require_value("--last-updated"));
options.last_updated = std::move(value);
} else {
return absl::InvalidArgumentError(
absl::StrFormat("Unknown flag for agent describe: %s", token));
}
}
options.format = absl::AsciiStrToLower(options.format);
if (options.format != "json" && options.format != "yaml") {
return absl::InvalidArgumentError("--format must be either json or yaml");
}
return options;
}
} // namespace
absl::Status HandleRunCommand(const std::vector<std::string>& arg_vec,
Rom& rom) {
if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") {
return absl::InvalidArgumentError("Usage: agent run --prompt <prompt>");
}
std::string prompt = arg_vec[1];
RETURN_IF_ERROR(EnsureRomLoaded(rom, "agent run --prompt \"<prompt>\""));
// 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());
if (response.commands.empty()) {
return absl::FailedPreconditionError(
"Agent response did not include any executable commands.");
}
std::string provider = absl::GetFlag(FLAGS_ai_provider);
ProposalCreationRequest request;
request.prompt = prompt;
request.response = &response;
request.rom = &rom;
request.sandbox_label = "agent-run";
request.ai_provider = std::move(provider);
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::cout
<< "✅ Agent successfully planned and executed changes in a sandbox."
<< std::endl;
std::cout << " Proposal ID: " << metadata.id << 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";
std::cout << " z3ed agent accept --proposal-id " << metadata.id << std::endl;
return absl::OkStatus();
}
absl::Status HandlePlanCommand(const std::vector<std::string>& arg_vec) {
if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") {
return absl::InvalidArgumentError("Usage: agent plan --prompt <prompt>");
}
std::string prompt = arg_vec[1];
auto ai_service = CreateAIService(); // Use service factory
auto response_or = ai_service->GenerateResponse(prompt);
if (!response_or.ok()) {
return response_or.status();
}
std::vector<std::string> commands = response_or.value().commands;
// Create a proposal from the commands
Tile16ProposalGenerator generator;
auto proposal_or =
generator.GenerateFromCommands(prompt, commands, "ollama", nullptr);
if (!proposal_or.ok()) {
return proposal_or.status();
}
auto proposal = proposal_or.value();
// TODO: Save the proposal to disk using ProposalRegistry
// For now, just print it.
std::cout << "AI Agent Plan (Proposal ID: " << proposal.id << "):\n";
std::cout << proposal.ToJson() << std::endl;
return absl::OkStatus();
}
absl::Status HandleDiffCommand(Rom& rom, const std::vector<std::string>& args) {
std::optional<std::string> proposal_id;
for (size_t i = 0; i < args.size(); ++i) {
const std::string& token = args[i];
if (absl::StartsWith(token, "--proposal-id=")) {
proposal_id = token.substr(14);
} else if (token == "--proposal-id" && i + 1 < args.size()) {
proposal_id = args[i + 1];
++i;
}
}
auto& registry = ProposalRegistry::Instance();
absl::StatusOr<ProposalRegistry::ProposalMetadata> proposal_or;
if (proposal_id.has_value()) {
proposal_or = registry.GetProposal(proposal_id.value());
} else {
proposal_or = registry.GetLatestPendingProposal();
}
if (proposal_or.ok()) {
const auto& proposal = proposal_or.value();
std::cout << "\n=== Proposal Diff ===\n";
std::cout << "Proposal ID: " << proposal.id << "\n";
std::cout << "Sandbox ID: " << proposal.sandbox_id << "\n";
std::cout << "Prompt: " << proposal.prompt << "\n";
std::cout << "Description: " << proposal.description << "\n";
std::cout << "Status: ";
switch (proposal.status) {
case ProposalRegistry::ProposalStatus::kPending:
std::cout << "Pending";
break;
case ProposalRegistry::ProposalStatus::kAccepted:
std::cout << "Accepted";
break;
case ProposalRegistry::ProposalStatus::kRejected:
std::cout << "Rejected";
break;
}
std::cout << "\n";
std::cout << "Created: " << absl::FormatTime(proposal.created_at) << "\n";
std::cout << "Commands Executed: " << proposal.commands_executed << "\n";
std::cout << "Bytes Changed: " << proposal.bytes_changed << "\n\n";
if (!proposal.sandbox_rom_path.empty()) {
std::cout << "Sandbox ROM: " << proposal.sandbox_rom_path << "\n";
}
std::cout << "Proposal directory: "
<< proposal.log_path.parent_path() << "\n";
std::cout << "Diff file: " << proposal.diff_path << "\n";
std::cout << "Log file: " << proposal.log_path << "\n\n";
if (std::filesystem::exists(proposal.diff_path)) {
std::cout << "--- Diff Content ---\n";
std::ifstream diff_file(proposal.diff_path);
if (diff_file.is_open()) {
std::string line;
while (std::getline(diff_file, line)) {
std::cout << line << "\n";
}
} else {
std::cout << "(Unable to read diff file)\n";
}
} else {
std::cout << "(No diff file found)\n";
}
std::cout << "\n--- Execution Log ---\n";
if (std::filesystem::exists(proposal.log_path)) {
std::ifstream log_file(proposal.log_path);
if (log_file.is_open()) {
std::string line;
int line_count = 0;
while (std::getline(log_file, line)) {
std::cout << line << "\n";
line_count++;
if (line_count > 50) {
std::cout << "... (log truncated, see " << proposal.log_path
<< " for full output)\n";
break;
}
}
} else {
std::cout << "(Unable to read log file)\n";
}
} else {
std::cout << "(No log file found)\n";
}
std::cout << "\n=== Next Steps ===\n";
std::cout << "To accept changes: z3ed agent commit\n";
std::cout << "To reject changes: z3ed agent revert\n";
std::cout << "To review in GUI: yaze --proposal=" << proposal.id << "\n";
return absl::OkStatus();
}
if (rom.is_loaded()) {
auto sandbox_or = RomSandboxManager::Instance().ActiveSandbox();
if (!sandbox_or.ok()) {
return absl::NotFoundError(
"No pending proposals found and no active sandbox. Run 'z3ed agent "
"run' first.");
}
RomDiff diff_handler;
auto status =
diff_handler.Run({rom.filename(), sandbox_or->rom_path.string()});
if (!status.ok()) {
return status;
}
} else {
return absl::AbortedError("No ROM loaded.");
}
return absl::OkStatus();
}
absl::Status HandleLearnCommand() {
std::cout << "Agent learn not yet implemented." << std::endl;
return absl::OkStatus();
}
absl::Status HandleListCommand() {
auto& registry = ProposalRegistry::Instance();
auto proposals = registry.ListProposals();
if (proposals.empty()) {
std::cout << "No proposals found.\n";
std::cout
<< "Run 'z3ed agent run --prompt \"...\"' to create a proposal.\n";
return absl::OkStatus();
}
std::cout << "\n=== Agent Proposals ===\n\n";
for (const auto& proposal : proposals) {
std::cout << "ID: " << proposal.id << "\n";
std::cout << " Status: ";
switch (proposal.status) {
case ProposalRegistry::ProposalStatus::kPending:
std::cout << "Pending";
break;
case ProposalRegistry::ProposalStatus::kAccepted:
std::cout << "Accepted";
break;
case ProposalRegistry::ProposalStatus::kRejected:
std::cout << "Rejected";
break;
}
std::cout << "\n";
std::cout << " Created: " << absl::FormatTime(proposal.created_at) << "\n";
std::cout << " Prompt: " << proposal.prompt << "\n";
std::cout << " Commands: " << proposal.commands_executed << "\n";
std::cout << " Bytes Changed: " << proposal.bytes_changed << "\n";
std::cout << "\n";
}
std::cout << "Total: " << proposals.size() << " proposal(s)\n";
std::cout << "\nUse 'z3ed agent diff --proposal-id=<id>' to view details.\n";
return absl::OkStatus();
}
absl::Status HandleCommitCommand(Rom& rom) {
if (rom.is_loaded()) {
auto status = rom.SaveToFile({.save_new = false});
if (!status.ok()) {
return status;
}
std::cout << "✅ Changes committed successfully." << std::endl;
} else {
return absl::AbortedError("No ROM loaded.");
}
return absl::OkStatus();
}
absl::Status HandleRevertCommand(Rom& rom) {
if (rom.is_loaded()) {
auto status = rom.LoadFromFile(rom.filename());
if (!status.ok()) {
return status;
}
std::cout << "✅ Changes reverted successfully." << std::endl;
} else {
return absl::AbortedError("No ROM loaded.");
}
return absl::OkStatus();
}
absl::Status HandleDescribeCommand(const std::vector<std::string>& arg_vec) {
ASSIGN_OR_RETURN(auto options, ParseDescribeArgs(arg_vec));
const auto& catalog = ResourceCatalog::Instance();
std::optional<ResourceSchema> resource_schema;
if (options.resource.has_value()) {
auto resource_or = catalog.GetResource(*options.resource);
if (!resource_or.ok()) {
return resource_or.status();
}
resource_schema = resource_or.value();
}
std::string payload;
if (options.format == "json") {
if (resource_schema.has_value()) {
payload = catalog.SerializeResource(*resource_schema);
} else {
payload = catalog.SerializeResources(catalog.AllResources());
}
} else {
std::string last_updated =
options.last_updated.has_value()
? *options.last_updated
: absl::FormatTime("%Y-%m-%d", absl::Now(), absl::LocalTimeZone());
if (resource_schema.has_value()) {
std::vector<ResourceSchema> schemas{*resource_schema};
payload = catalog.SerializeResourcesAsYaml(schemas, options.version,
last_updated);
} else {
payload = catalog.SerializeResourcesAsYaml(catalog.AllResources(),
options.version, last_updated);
}
}
if (options.output_path.has_value()) {
std::ofstream out(*options.output_path, std::ios::binary | std::ios::trunc);
if (!out.is_open()) {
return absl::InternalError(absl::StrFormat(
"Failed to open %s for writing", *options.output_path));
}
out << payload;
out.close();
if (!out) {
return absl::InternalError(absl::StrFormat("Failed to write schema to %s",
*options.output_path));
}
std::cout << absl::StrFormat("Wrote %s schema to %s", options.format,
*options.output_path)
<< std::endl;
return absl::OkStatus();
}
std::cout << payload << std::endl;
return absl::OkStatus();
}
absl::Status HandleChatCommand(Rom& rom) {
RETURN_IF_ERROR(EnsureRomLoaded(rom, "agent chat"));
// Try to load project and labels automatically
auto _ = TryLoadProjectAndLabels(rom); // Ignore errors - we'll use defaults
tui::ChatTUI chat_tui(&rom);
chat_tui.Run();
return absl::OkStatus();
}
absl::Status HandleSimpleChatCommand(const std::vector<std::string>& arg_vec,
Rom* rom, bool quiet) {
RETURN_IF_ERROR(EnsureRomLoaded(*rom, "agent simple-chat"));
auto _ = TryLoadProjectAndLabels(*rom);
std::optional<std::string> batch_file;
std::optional<std::string> single_message;
bool verbose = false;
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& arg = arg_vec[i];
if (absl::StartsWith(arg, "--file=")) {
batch_file = arg.substr(7);
} else if (arg == "--file" && i + 1 < arg_vec.size()) {
batch_file = arg_vec[++i];
} else if (arg == "--verbose" || arg == "-v") {
verbose = true;
} else if (!absl::StartsWith(arg, "--") && !single_message.has_value()) {
single_message = arg;
}
}
agent::AgentConfig config;
config.verbose = verbose;
SimpleChatSession session;
session.SetConfig(config);
session.SetRomContext(rom);
if (batch_file.has_value()) {
std::ifstream file(*batch_file);
if (!file.is_open()) {
return absl::NotFoundError(absl::StrCat("Failed to open file: ", *batch_file));
}
if (!quiet) {
std::cout << "Running batch session from: " << *batch_file << std::endl;
std::cout << "----------------------------------------\n\n";
}
std::string line;
int line_num = 0;
while (std::getline(file, line)) {
line_num++;
std::string trimmed_line = std::string(absl::StripAsciiWhitespace(line));
if (trimmed_line.empty() || absl::StartsWith(trimmed_line, "#")) {
continue;
}
if (!quiet) {
std::cout << "Input [" << line_num << "]: " << trimmed_line << std::endl;
}
std::string response;
auto status = session.SendAndWaitForResponse(trimmed_line, &response);
if (!status.ok()) {
std::cerr << "Error processing line " << line_num << ": " << status.message() << std::endl;
continue;
}
std::cout << response << "\n";
if (!quiet) {
std::cout << "\n";
}
}
return absl::OkStatus();
} else if (single_message.has_value()) {
std::string response;
auto status = session.SendAndWaitForResponse(*single_message, &response);
if (!status.ok()) {
return status;
}
std::cout << response << "\n";
return absl::OkStatus();
} else {
return session.RunInteractive();
}
}
absl::Status HandleAcceptCommand(const std::vector<std::string>& arg_vec,
Rom& rom) {
std::optional<std::string> proposal_id;
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (absl::StartsWith(token, "--proposal-id=")) {
proposal_id = token.substr(14);
break;
}
if (token == "--proposal-id" && i + 1 < arg_vec.size()) {
proposal_id = arg_vec[i + 1];
break;
}
}
if (!proposal_id.has_value() || proposal_id->empty()) {
return absl::InvalidArgumentError(
"Usage: agent accept --proposal-id <proposal_id>");
}
auto& registry = ProposalRegistry::Instance();
ASSIGN_OR_RETURN(auto metadata, registry.GetProposal(*proposal_id));
if (metadata.status == ProposalRegistry::ProposalStatus::kAccepted) {
std::cout << "Proposal '" << *proposal_id << "' is already accepted."
<< std::endl;
return absl::OkStatus();
}
if (metadata.sandbox_rom_path.empty()) {
return absl::FailedPreconditionError(absl::StrCat(
"Proposal '", *proposal_id,
"' is missing sandbox ROM metadata. Cannot accept."));
}
if (!std::filesystem::exists(metadata.sandbox_rom_path)) {
return absl::NotFoundError(absl::StrCat(
"Sandbox ROM not found at ", metadata.sandbox_rom_path.string()));
}
RETURN_IF_ERROR(
EnsureRomLoaded(rom, "agent accept --proposal-id <proposal_id>"));
Rom sandbox_rom;
auto sandbox_load_status = sandbox_rom.LoadFromFile(
metadata.sandbox_rom_path.string(), RomLoadOptions::CliDefaults());
if (!sandbox_load_status.ok()) {
return absl::InternalError(absl::StrCat(
"Failed to load sandbox ROM: ", sandbox_load_status.message()));
}
if (rom.size() != sandbox_rom.size()) {
rom.Expand(static_cast<int>(sandbox_rom.size()));
}
auto copy_status = rom.WriteVector(0, sandbox_rom.vector());
if (!copy_status.ok()) {
return absl::InternalError(absl::StrCat(
"Failed to copy sandbox ROM data: ", copy_status.message()));
}
auto save_status = rom.SaveToFile({.save_new = false});
if (!save_status.ok()) {
return absl::InternalError(absl::StrCat(
"Failed to save changes to main ROM: ", save_status.message()));
}
RETURN_IF_ERROR(registry.UpdateStatus(
*proposal_id, ProposalRegistry::ProposalStatus::kAccepted));
RETURN_IF_ERROR(registry.AppendLog(
*proposal_id,
absl::StrCat("Proposal accepted and applied to ", rom.filename())));
if (!metadata.sandbox_id.empty()) {
auto remove_status =
RomSandboxManager::Instance().RemoveSandbox(metadata.sandbox_id);
if (!remove_status.ok()) {
std::cerr << "Warning: Failed to remove sandbox '" << metadata.sandbox_id
<< "': " << remove_status.message() << "\n";
}
}
std::cout << "✅ Proposal '" << *proposal_id << "' accepted and applied to '"
<< rom.filename() << "'." << std::endl;
std::cout << " Source sandbox ROM: " << metadata.sandbox_rom_path
<< std::endl;
return absl::OkStatus();
}
} // namespace agent
} // namespace cli
} // namespace yaze