feat: Implement proposal loading from disk in ProposalRegistry and enhance agent command handling
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
#include "cli/service/rom_sandbox_manager.h"
|
||||
#include "util/macro.h"
|
||||
|
||||
#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"
|
||||
@@ -18,6 +20,9 @@
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
|
||||
// Declare the rom flag so we can access it
|
||||
ABSL_DECLARE_FLAG(std::string, rom);
|
||||
|
||||
// Platform-specific includes for process management and executable path detection
|
||||
#if !defined(_WIN32)
|
||||
#include <sys/types.h>
|
||||
@@ -106,9 +111,21 @@ absl::Status HandleRunCommand(const std::vector<std::string>& arg_vec, Rom& rom)
|
||||
}
|
||||
std::string prompt = arg_vec[1];
|
||||
|
||||
// Save a sandbox copy of the ROM for proposal tracking.
|
||||
// Load ROM if not already loaded
|
||||
if (!rom.is_loaded()) {
|
||||
return absl::FailedPreconditionError("No ROM loaded");
|
||||
std::string rom_path = absl::GetFlag(FLAGS_rom);
|
||||
if (rom_path.empty()) {
|
||||
return absl::FailedPreconditionError(
|
||||
"No ROM loaded. Use --rom=<path> to specify ROM file.\n"
|
||||
"Example: z3ed agent run --rom=zelda3.sfc --prompt \"Your prompt here\"");
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
auto sandbox_or = RomSandboxManager::Instance().CreateSandbox(
|
||||
|
||||
@@ -5,17 +5,24 @@ namespace cli {
|
||||
|
||||
absl::StatusOr<std::vector<std::string>> MockAIService::GetCommands(
|
||||
const std::string& prompt) {
|
||||
// NOTE: These commands use positional arguments (not --flags) because
|
||||
// the command handlers haven't been updated to parse flags yet.
|
||||
// TODO: Update handlers to use absl::flags parsing
|
||||
|
||||
if (prompt == "Make all the soldiers in Hyrule Castle wear red armor.") {
|
||||
// Simplified command sequence - just export then import
|
||||
// (In reality, you'd modify the palette file between export and import)
|
||||
return std::vector<std::string>{
|
||||
"palette export --group sprites_aux1 --id 4 --to soldier_palette.col",
|
||||
"palette set-color --file soldier_palette.col --index 5 --color \"#FF0000\"",
|
||||
"palette import --group sprites_aux1 --id 4 --from soldier_palette.col"};
|
||||
} else if (prompt.find("Place a tree at coordinates") != std::string::npos) {
|
||||
// Example prompt: "Place a tree at coordinates (10, 20) on the light world map"
|
||||
// For simplicity, we'll hardcode the tile id for a tree
|
||||
return std::vector<std::string>{"overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E"};
|
||||
"palette export sprites_aux1 4 soldier_palette.col"
|
||||
// Would normally modify soldier_palette.col here to change colors
|
||||
// Then import it back
|
||||
};
|
||||
} else if (prompt == "Place a tree") {
|
||||
// Example: Place a tree on the light world map
|
||||
// Command format: map_id x y tile_id (hex)
|
||||
return std::vector<std::string>{"overworld set-tile 0 10 20 0x02E"};
|
||||
}
|
||||
return absl::UnimplementedError("Prompt not supported by mock AI service.");
|
||||
return absl::UnimplementedError("Prompt not supported by mock AI service. Try: 'Make all the soldiers in Hyrule Castle wear red armor.' or 'Place a tree'");
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "cli/service/proposal_registry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
@@ -61,6 +63,99 @@ absl::Status ProposalRegistry::EnsureRootExistsLocked() {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ProposalRegistry::LoadProposalsFromDiskLocked() {
|
||||
std::error_code ec;
|
||||
|
||||
// Check if root directory exists
|
||||
if (!std::filesystem::exists(root_directory_, ec)) {
|
||||
return absl::OkStatus(); // No proposals to load
|
||||
}
|
||||
|
||||
// Iterate over all directories in the root
|
||||
for (const auto& entry : std::filesystem::directory_iterator(root_directory_, ec)) {
|
||||
if (ec) {
|
||||
continue; // Skip entries that cause errors
|
||||
}
|
||||
|
||||
if (!entry.is_directory()) {
|
||||
continue; // Skip non-directories
|
||||
}
|
||||
|
||||
std::string proposal_id = entry.path().filename().string();
|
||||
|
||||
// Skip if already loaded (shouldn't happen, but be defensive)
|
||||
if (proposals_.find(proposal_id) != proposals_.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reconstruct metadata from directory contents
|
||||
// Since we don't have a metadata.json file, we need to infer what we can
|
||||
std::filesystem::path log_path = entry.path() / "execution.log";
|
||||
std::filesystem::path diff_path = entry.path() / "diff.txt";
|
||||
|
||||
// Check if log file exists to determine if this is a valid proposal
|
||||
if (!std::filesystem::exists(log_path, ec)) {
|
||||
continue; // Not a valid proposal directory
|
||||
}
|
||||
|
||||
// Extract timestamp from proposal ID (format: proposal-20251001T200215-1)
|
||||
absl::Time created_at = absl::Now(); // Default to now if parsing fails
|
||||
if (proposal_id.starts_with("proposal-")) {
|
||||
std::string time_str = proposal_id.substr(9, 15); // Extract YYYYMMDDTHHmmSS
|
||||
std::string error;
|
||||
if (absl::ParseTime("%Y%m%dT%H%M%S", time_str, &created_at, &error)) {
|
||||
// Successfully parsed time
|
||||
}
|
||||
}
|
||||
|
||||
// Get file modification time as a fallback
|
||||
auto ftime = std::filesystem::last_write_time(log_path, ec);
|
||||
if (!ec) {
|
||||
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
||||
ftime - std::filesystem::file_time_type::clock::now() +
|
||||
std::chrono::system_clock::now());
|
||||
auto time_t_value = std::chrono::system_clock::to_time_t(sctp);
|
||||
created_at = absl::FromTimeT(time_t_value);
|
||||
}
|
||||
|
||||
// Create minimal metadata for this proposal
|
||||
ProposalMetadata metadata{
|
||||
.id = proposal_id,
|
||||
.sandbox_id = "", // Unknown - not stored in logs
|
||||
.description = "Loaded from disk",
|
||||
.prompt = "", // Unknown - not stored in logs
|
||||
.status = ProposalStatus::kPending,
|
||||
.created_at = created_at,
|
||||
.reviewed_at = std::nullopt,
|
||||
.diff_path = diff_path,
|
||||
.log_path = log_path,
|
||||
.screenshots = {},
|
||||
.bytes_changed = 0,
|
||||
.commands_executed = 0,
|
||||
};
|
||||
|
||||
// Count diff size if it exists
|
||||
if (std::filesystem::exists(diff_path, ec) && !ec) {
|
||||
metadata.bytes_changed = static_cast<int>(
|
||||
std::filesystem::file_size(diff_path, ec));
|
||||
}
|
||||
|
||||
// Scan for screenshots
|
||||
for (const auto& file : std::filesystem::directory_iterator(entry.path(), ec)) {
|
||||
if (ec) continue;
|
||||
if (file.path().extension() == ".png" ||
|
||||
file.path().extension() == ".jpg" ||
|
||||
file.path().extension() == ".jpeg") {
|
||||
metadata.screenshots.push_back(file.path());
|
||||
}
|
||||
}
|
||||
|
||||
proposals_[proposal_id] = metadata;
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string ProposalRegistry::GenerateProposalIdLocked() {
|
||||
absl::Time now = absl::Now();
|
||||
std::string time_component = absl::FormatTime("%Y%m%dT%H%M%S", now,
|
||||
@@ -208,7 +303,20 @@ ProposalRegistry::GetProposal(const std::string& proposal_id) const {
|
||||
|
||||
std::vector<ProposalRegistry::ProposalMetadata>
|
||||
ProposalRegistry::ListProposals(std::optional<ProposalStatus> filter_status) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
|
||||
// Load proposals from disk if we haven't already
|
||||
if (proposals_.empty()) {
|
||||
// Cast away const for loading - this is a lazy initialization pattern
|
||||
auto* self = const_cast<ProposalRegistry*>(this);
|
||||
auto status = self->LoadProposalsFromDiskLocked();
|
||||
if (!status.ok()) {
|
||||
// Log error but continue - return empty list if loading fails
|
||||
std::cerr << "Warning: Failed to load proposals from disk: "
|
||||
<< status.message() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ProposalMetadata> result;
|
||||
|
||||
for (const auto& [id, metadata] : proposals_) {
|
||||
|
||||
@@ -108,6 +108,7 @@ class ProposalRegistry {
|
||||
ProposalRegistry();
|
||||
|
||||
absl::Status EnsureRootExistsLocked();
|
||||
absl::Status LoadProposalsFromDiskLocked();
|
||||
std::string GenerateProposalIdLocked();
|
||||
std::filesystem::path ProposalDirectory(absl::string_view proposal_id) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user