Files
drharness/src/main.cpp
T

700 lines
24 KiB
C++

#include "harness.hpp"
#include "json_types.hpp"
#include "ollama_client.hpp"
#include "debug_client.hpp"
#include "editor.hpp"
#include "tool_registry.hpp"
#include "agent_profile.hpp"
#include "rebase_engine.hpp"
#include "utility.hpp"
#include "base64_util.hpp"
#if !defined(_WIN32)
#include "shell_tool.hpp"
#endif
#include "agent_tool.hpp"
#include "edit_tool.hpp"
#include "read_multi_modal.hpp"
#include "file_list_tool.hpp"
#include "file_read_tool.hpp"
#include "grep_tool.hpp"
#include "skill.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <filesystem>
#include <random>
#include <iomanip>
static const std::vector<AgentProfile> profiles = {
#if !defined(_WIN32)
{
.name = "shell_expert",
.description = "An agent that can run shell commands to inspect the system, "
"files, and processes. Delegate multi-step shell tasks here.",
.model = "gemma4:26b",
.tool_names = {"run_shell"}
},
#endif
// Add more agent profiles here
};
static const std::vector<std::string> default_tools =
{
};
// UUID v4 generation using <random> and <filesystem> only
static std::string generate_uuid() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 15);
std::uniform_int_distribution<> dis2(8, 11); // for variant bits
std::ostringstream ss;
for (int i = 0; i < 8; i++) {
ss << std::hex << dis(gen);
}
ss << "-";
for (int i = 0; i < 4; i++) {
ss << std::hex << dis(gen);
}
ss << "-4"; // version 4
for (int i = 0; i < 3; i++) {
ss << std::hex << dis(gen);
}
ss << "-";
ss << std::hex << dis2(gen);
for (int i = 0; i < 3; i++) {
ss << std::hex << dis(gen);
}
ss << "-";
for (int i = 0; i < 12; i++) {
ss << std::hex << dis(gen);
}
return ss.str();
}
// Helper function to read and deserialize HarnessState from a file
// Returns the parsed HarnessState, or exits on error
static HarnessState read_harness_state_or_exit(const char* file_path) {
std::ifstream file(file_path);
if (!file.is_open()) {
std::cerr << "Error: Cannot open file " << file_path << std::endl;
exit(1);
}
std::string file_content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
auto data = expect_or_exit(parse_json_safely(file_content));
auto state_result = json_to<HarnessState>(data);
if (!state_result) {
std::cerr << "Error: Failed to parse chat file: " << state_result.error() << std::endl;
exit(1);
}
return state_result.value();
}
int main(int argc, char** argv) {
OllamaConfig cfg;
if (const char* m = std::getenv("OLLAMA_MODEL")) cfg.model = m;
if (const char* u = std::getenv("OLLAMA_URL")) cfg.base_url = u;
if (const char* a = std::getenv("OLLAMA_AUTHORIZATION_TOKEN")) cfg.auth_token = a;
bool use_debug = false;
bool debug_paths = false;
char const* debug_frontmatter = nullptr;
bool list_skills = false;
char const* test_json_parse = nullptr;
char const* test_expect_exit_success = nullptr;
char const* test_expect_exit_error = nullptr;
char const* test_type_required_type = nullptr;
char const* test_type_required_json = nullptr;
char const* test_rebase_history_size = nullptr;
char const* test_rebase_content = nullptr;
char const* test_build_rebase_json = nullptr;
char const* test_apply_rebase_lines = nullptr;
char const* test_apply_rebase_history = nullptr;
char const* test_edit_prompt_json = nullptr;
char const* test_edit_prompt_index = nullptr;
char const* test_edit_prompt_role = nullptr;
char const* test_base64_encode = nullptr;
char const* test_base64_decode = nullptr;
char const* test_mime_type = nullptr;
char const* test_data_uri_ext = nullptr;
char const* test_data_uri_b64 = nullptr;
char const* test_image_file = nullptr;
ToolRegistry master_registery;
ToolRegistry agent_tools;
expect_or_exit(master_registery.register_tool(make_edit_tool()));
expect_or_exit(master_registery.register_tool(make_read_multi_modal()));
expect_or_exit(master_registery.register_tool(make_file_list_tool()));
expect_or_exit(master_registery.register_tool(make_file_read_tool()));
expect_or_exit(master_registery.register_tool(make_grep_tool()));
#if !defined(_WIN32)
expect_or_exit(master_registery.register_tool(make_shell_tool(true))); // run_shell (safe)
expect_or_exit(master_registery.register_tool(make_shell_tool(false))); // run_dangerous_shell
#endif
// Add more base tools here:
for (const auto& profile : profiles)
expect_or_exit(master_registery.register_tool(make_agent_tool(profile, master_registery, cfg.base_url)));
auto consume_arg = [&](){
argc--;
argv++;
if (argc < 0)
{
std::cerr << "Error expected another argument" << std::endl;
exit(1);
}
return argv[-1];
};
char const* file_to_open = nullptr;
std::string auto_temp_file; // Path to auto-generated temp file (if any)
char const* prompt = nullptr;
char const* prompt_file = nullptr;
bool start_new = false;
bool list_models = false;
char const* print_file = nullptr;
std::vector<std::string> tools;
bool full_history = false;
bool edit_history = false;
std::vector<std::string> skill_filters;
std::vector<std::string> image_paths;
consume_arg(); // progname
while (argc > 0)
{
if (argv[0] == std::string("-F"))
{
consume_arg();
file_to_open = consume_arg();
}
else if (argv[0] == std::string("-p"))
{
consume_arg();
print_file = consume_arg();
}
else if (argv[0] == std::string("-t"))
{
consume_arg();
tools.push_back(consume_arg());
}
else if (argv[0] == std::string("-n"))
{
consume_arg();
start_new = true;
}
else if (argv[0] == std::string("-m"))
{
consume_arg();
prompt = consume_arg();
}
else if (argv[0] == std::string("-P"))
{
consume_arg();
prompt_file = consume_arg();
}
else if (argv[0] == std::string("-M"))
{
consume_arg();
cfg.model = consume_arg();
}
else if (argv[0] == std::string("-d"))
{
consume_arg();
use_debug = true;
}
else if (argv[0] == std::string("-D"))
{
consume_arg();
debug_paths = true;
}
else if (argv[0] == std::string("-y"))
{
consume_arg();
debug_frontmatter = consume_arg();
}
else if (argv[0] == std::string("-S"))
{
consume_arg();
list_skills = true;
}
else if (argv[0] == std::string("-J"))
{
consume_arg();
test_json_parse = consume_arg();
}
else if (argv[0] == std::string("-E"))
{
consume_arg();
test_expect_exit_success = consume_arg();
}
else if (argv[0] == std::string("-e"))
{
consume_arg();
test_expect_exit_error = consume_arg();
}
else if (argv[0] == std::string("-R"))
{
consume_arg();
test_type_required_type = consume_arg();
test_type_required_json = consume_arg();
}
else if (argv[0] == std::string("-P"))
{
consume_arg();
test_rebase_history_size = consume_arg();
test_rebase_content = consume_arg();
}
else if (argv[0] == std::string("-Z"))
{
consume_arg();
test_build_rebase_json = consume_arg();
}
else if (argv[0] == std::string("-A"))
{
consume_arg();
test_apply_rebase_lines = consume_arg();
test_apply_rebase_history = consume_arg();
}
else if (argv[0] == std::string("-B"))
{
consume_arg();
test_base64_encode = consume_arg();
}
else if (argv[0] == std::string("-K"))
{
consume_arg();
test_base64_decode = consume_arg();
}
else if (argv[0] == std::string("-W"))
{
consume_arg();
test_edit_prompt_json = consume_arg();
test_edit_prompt_index = consume_arg();
test_edit_prompt_role = consume_arg();
}
else if (argv[0] == std::string("-T"))
{
consume_arg();
test_mime_type = consume_arg();
}
else if (argv[0] == std::string("-U"))
{
consume_arg();
test_data_uri_ext = consume_arg();
test_data_uri_b64 = consume_arg();
}
else if (argv[0] == std::string("-L"))
{
consume_arg();
list_models = true;
}
else if (argv[0] == std::string("-l"))
{
for (auto const& x : master_registery.tool_names())
{
std::cout << x << std::endl;
}
exit(0);
}
else if (argv[0] == std::string("-f"))
{
consume_arg();
full_history = true;
}
else if (argv[0] == std::string("-s"))
{
consume_arg();
skill_filters.push_back(consume_arg());
}
else if (argv[0] == std::string("-b") || argv[0] == std::string("--edit-history"))
{
consume_arg();
edit_history = true;
}
else if (argv[0] == std::string("-i"))
{
consume_arg();
image_paths.push_back(consume_arg());
}
else if (argv[0] == std::string("-I"))
{
consume_arg();
test_image_file = consume_arg();
}
else if (argv[0] == std::string("-h"))
{
std::cout << "Usage: drharness [options]\n"
<< "Options:\n"
<< " -F <file> Chat file\n"
<< " -p <file> Pretty print a saved conversation JSON file\n"
<< " -t <tool> Tool name (can be specified multiple times)\n"
<< " -s <name> Skill name to enable (can be specified multiple times)\n"
<< " -i <path> Image file to attach (can be specified multiple times)\n"
<< " -n Start a new conversation (auto-generates temp file if -F not specified)\n"
<< " -m <prompt> Prompt message (required)\n"
<< " -P <file> Read prompt from file\n"
<< " -M <model> Ollama model name\n"
<< " -f Open editor with full history (requires -F)\n"
<< " -b Edit history (requires -F)\n"
<< " --edit-history\n"
<< " -l List available tools\n"
<< " -L List available models on server side\n"
<< " -D Debug: print all discovered skill paths and exit\n"
<< " -y <file> Debug: print YAML frontmatter from a SKILL.md file and exit\n"
<< " -S List discovered skills and exit\n"
<< " -J <json> Debug: test parse_json_safely and exit\n"
<< " -E <string> Debug: test expect_or_exit success case and exit\n"
<< " -e <string> Debug: test expect_or_exit error case and exit\n"
<< " -R <type> <json> Debug: test json_to<Type>() for required fields and exit\n"
<< " -P <size> <content> Debug: test rebase parser and exit\n"
<< " -Z <json> Debug: test build_rebase_content with JSON history and exit\n"
<< " -A <lines> <history> Debug: test apply_rebase with JSON and exit\n"
<< " -W <json> <index> <role> Debug: test build_edit_prompt and exit\n"
<< " -B <string> Debug: test base64 encode and exit\n"
<< " -K <string> Debug: test base64 decode and exit\n"
<< " -T <ext> Debug: test MIME type detection and exit\n"
<< " -U <ext> <b64> Debug: test data URI generation and exit\n"
<< " -I <file> Debug: test image file reading and exit\n"
<< " -d Use debug client (mock API via stdin)\n"
<< " -h Display this help menu\n";
exit(0);
}
else
{
std::cerr << "Unexpected argument: " << argv[0] << std::endl;
return 1;
}
}
// Validate -b / --edit-history flag requirements
if (edit_history && !file_to_open)
{
std::cerr << "Error: -b/--edit-history requires -F <chat_file> to be specified" << std::endl;
return 1;
}
// Validate -b / --edit-history flag with empty history
if (edit_history && file_to_open)
{
HarnessState state_data = read_harness_state_or_exit(file_to_open);
if (state_data.history.empty()) {
std::cerr << "Error: Chat file has no history, cannot edit" << std::endl;
return 1;
}
// Run the interactive edit flow
HistoryEditor editor;
auto edit_result = editor.edit_history_interactive(state_data.history);
if (!edit_result) {
std::cerr << "Error: " << edit_result.error() << std::endl;
return 1;
}
// Save modified history to chat file
state_data.history = edit_result.value();
std::ofstream outfile(file_to_open);
if (!outfile.is_open()) {
std::cerr << "Error: Cannot write to file " << file_to_open << std::endl;
return 1;
}
outfile << nlohmann::json(state_data);
// Exit after successful edit
return 0;
}
// Helper function to create the client based on the debug flag
auto create_client = [&](bool debug) -> std::unique_ptr<Client> {
if (debug)
return std::make_unique<DebugClient>();
return std::make_unique<OllamaClient>(cfg);
};
// Scan for skills
auto all_skills_result = scan_skills();
if (!all_skills_result) {
std::cerr << "Fatal: " << all_skills_result.error() << std::endl;
return 1;
}
std::vector<SkillMetadata> all_skills = std::move(all_skills_result.value());
// Build list of discovered paths for debug output
std::vector<std::string> discovered_paths;
for (const auto& skill : all_skills) {
discovered_paths.push_back((std::filesystem::path(skill.directory) / "SKILL.md").string());
}
// Build enabled skills list based on filters
std::vector<SkillMetadata> enabled_skills;
if (skill_filters.empty()) {
enabled_skills = all_skills;
} else {
for (const auto& skill : all_skills) {
if (std::find(skill_filters.begin(), skill_filters.end(), skill.name) != skill_filters.end()) {
enabled_skills.push_back(skill);
}
}
}
expect_or_exit(master_registery.register_tool(make_skill_tool(enabled_skills)));
if (debug_paths) {
print_discovered_paths(discovered_paths);
return 0;
}
if (list_skills) {
print_skills(all_skills);
return 0;
}
if (debug_frontmatter) {
auto metadata = parse_skill_frontmatter(debug_frontmatter);
if (!metadata) {
std::cerr << "Error parsing frontmatter: " << metadata.error() << "\n";
return 1;
}
std::cout << "name: " << metadata->name << "\n";
if (metadata->description.has_value()) {
std::cout << "description: " << metadata->description.value() << "\n";
}
return 0;
}
if (test_json_parse) {
auto result = parse_json_safely(test_json_parse);
std::cout << expect_or_exit(result).dump(2) << std::endl;
return 0;
}
if (test_expect_exit_success) {
auto result = std::expected<std::string, std::string>(test_expect_exit_success);
std::string value = expect_or_exit(result);
std::cout << value << std::endl;
return 0;
}
if (test_expect_exit_error) {
auto result = std::expected<std::string, std::string>(std::unexpected(test_expect_exit_error));
expect_or_exit(result);
return 0; // Should not reach here
}
if (test_type_required_type && test_type_required_json) {
auto j = expect_or_exit(parse_json_safely(test_type_required_json));
std::string type_str(test_type_required_type);
auto it = json_dispatch_map.find(type_str);
if (it != json_dispatch_map.end()) {
it->second(j);
}
return 0;
}
if (test_rebase_history_size && test_rebase_content) {
size_t history_size = expect_or_exit(string_toul(test_rebase_history_size));
RebaseEngine engine;
auto result = engine.parse_rebase_file(test_rebase_content, history_size);
nlohmann::json output = expect_or_exit(result);
std::cout << output.dump(2) << std::endl;
return 0;
}
if (test_build_rebase_json) {
auto j = expect_or_exit(parse_json_safely(test_build_rebase_json));
auto history_result = expect_or_exit(json_to<std::vector<Message>>(j));
HistoryEditor editor;
std::string content = editor.build_rebase_content(history_result);
std::cout << content;
return 0;
}
if (test_apply_rebase_lines && test_apply_rebase_history) {
auto lines_j = expect_or_exit(parse_json_safely(test_apply_rebase_lines));
auto lines_result = expect_or_exit(json_to<std::vector<RebaseLine>>(lines_j));
auto history_j = expect_or_exit(parse_json_safely(test_apply_rebase_history));
auto history_result = expect_or_exit(json_to<std::vector<Message>>(history_j));
RebaseEngine engine;
auto result = engine.apply_rebase(lines_result, history_result);
nlohmann::json output = expect_or_exit(result);
std::cout << output.dump(2) << std::endl;
return 0;
}
if (test_edit_prompt_json && test_edit_prompt_index && test_edit_prompt_role) {
auto j = expect_or_exit(parse_json_safely(test_edit_prompt_json));
auto history_result = expect_or_exit(json_to<std::vector<Message>>(j));
size_t edit_index = expect_or_exit(string_toul(test_edit_prompt_index));
const std::string role = test_edit_prompt_role;
HistoryEditor editor;
std::string prompt = editor.build_edit_prompt(history_result, edit_index, role);
std::cout << prompt;
return 0;
}
if (test_base64_encode) {
std::cout << base64_encode(test_base64_encode) << std::endl;
return 0;
}
if (test_base64_decode) {
auto result = base64_decode(test_base64_decode);
if (!result) {
std::cout << "error: " << result.error() << std::endl;
return 1;
}
std::cout << result.value() << std::endl;
return 0;
}
if (test_mime_type) {
std::cout << get_mime_type_from_extension(test_mime_type) << std::endl;
return 0;
}
if (test_data_uri_ext && test_data_uri_b64) {
std::cout << generate_data_uri(test_data_uri_ext, test_data_uri_b64) << std::endl;
return 0;
}
if (test_image_file) {
auto result = read_image_as_data_uri(test_image_file);
if (!result) {
std::cerr << "Error: " << result.error() << std::endl;
return 1;
}
std::cout << result.value() << std::endl;
return 0;
}
if (list_models)
{
auto models_result = create_client(use_debug)->list_models();
if (!models_result) {
std::cerr << "Error listing models: " << models_result.error() << std::endl;
return 1;
}
auto models = models_result.value();
for (auto const& x : models)
{
std::cout << x << std::endl;
}
exit(0);
}
if (print_file)
{
std::ifstream file(print_file);
if (!file.is_open()) {
std::cerr << "Error: Cannot open file " << print_file << "\n";
return 1;
}
std::string file_content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
auto data = expect_or_exit(parse_json_safely(file_content));
print_history(std::cout, data);
return 0;
}
if (tools.empty())
agent_tools = expect_or_exit(master_registery.create_subregistry(default_tools));
else
agent_tools = expect_or_exit(master_registery.create_subregistry(tools));
// Auto-generate temp file if -n is specified without -F
if (start_new && !file_to_open) {
std::filesystem::path temp_dir = std::filesystem::temp_directory_path();
std::string uuid = generate_uuid();
auto temp_file_path = temp_dir / ("harness-" + uuid + ".json");
// Create empty temp file
std::ofstream temp_file(temp_file_path);
temp_file.close();
if (!std::filesystem::exists(temp_file_path)) {
std::cerr << "Error: Failed to create temp file" << std::endl;
exit(1);
}
auto_temp_file = temp_file_path.string();
file_to_open = auto_temp_file.c_str();
}
if (!file_to_open)
{
std::cerr << "Chat file is required (-F)" << std::endl;
exit(1);
}
if (start_new) {
std::ifstream file(file_to_open, std::ios::ate);
if (file.is_open()) {
std::streampos size = file.tellg();
if (size > 0) {
std::cerr << "Error: Cannot start a new conversation with an existing, non-empty file: " << file_to_open << "\n";
exit(1);
}
}
}
Harness harness(create_client(use_debug), agent_tools, {.max_tool_iterations = 1000, .verbose = true});
if (!start_new)
{
HarnessState state_data = read_harness_state_or_exit(file_to_open);
harness = Harness(create_client(use_debug), master_registery, state_data);
}
std::cerr << "drharness v0.3 | model: " << cfg.model
<< " | " << cfg.base_url << "\n\n";
std::string final_prompt;
if (prompt_file) {
std::ifstream pf(prompt_file);
if (!pf.is_open()) {
std::cerr << "Error: Cannot open prompt file " << prompt_file << "\n";
return 1;
}
final_prompt = std::string((std::istreambuf_iterator<char>(pf)), std::istreambuf_iterator<char>());
} else if (prompt) {
final_prompt = prompt;
} else {
final_prompt = expect_or_exit(run_editor(harness.serialize(), full_history));
}
std::cerr << "Prompt input:" << std::endl;
std::cerr << final_prompt << std::endl;
std::expected<std::string, std::string> reply;
if (!image_paths.empty()) {
reply = harness.chat(final_prompt, image_paths);
} else {
reply = harness.chat(final_prompt);
}
if (!reply) {
std::cerr << "Error: " << reply.error() << std::endl;
return 1;
}
std::ofstream harness_state(file_to_open);
std::cout << reply.value() << std::endl;
harness_state << nlohmann::json(harness.serialize());
// Print the auto-generated temp file path to stderr if it was auto-created
if (!auto_temp_file.empty()) {
std::cerr << auto_temp_file << std::endl;
}
}