BareGit
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>

#include <agent_smith/agent_smith.hpp>

namespace
{

std::string envOr(std::string_view name, std::string_view fallback)
{
    const char* value = std::getenv(std::string(name).c_str());
    return value == nullptr ? std::string(fallback) : std::string(value);
}

void printUsage(std::ostream& output)
{
    output << "Usage: agent_smith_demo [--no-mcp | --mcp PROGRAM [ARG...]]\n"
           << "\n"
           << "Environment variables:\n"
           << "  LLAMA_SERVER_URL  Provider origin "
              "(default: http://127.0.0.1:8080)\n"
           << "  LLAMA_MODEL       Model name (default: local-model)\n"
           << "  LLAMA_API_KEY     Optional bearer token\n"
           << "\n"
           << "Tools: weather by city and the bundled Python MCP server.\n"
           << "MCP provides addition, example://help, and a greeting prompt.\n"
           << "\n"
           << "Commands: /help, /clear, /stream on, /stream off, /quit\n";
}

agent_smith::ToolSchema weatherSchema()
{
    return agent_smith::ToolSchema{
        .name = "get_weather",
        .description = "Get the current weather for a city.",
        .parameters =
            {
                {"type", "object"},
                {"properties", {{"city", {{"type", "string"}}}}},
                {"required", {"city"}},
                {"additionalProperties", false},
            },
    };
}

mw::Task<mw::E<agent_smith::ToolResult>>
queryWeather(nlohmann::json arguments, agent_smith::ToolContext context)
{
    if(context.runtime == nullptr)
    {
        co_return std::unexpected(
            mw::runtimeError("weather tool needs a runtime"));
    }

    const auto city = arguments.find("city");
    if(city == arguments.end() || !city->is_string() || city->empty())
    {
        co_return std::unexpected(
            mw::runtimeError("weather tool requires a non-empty city"));
    }

    const std::string city_name = *city;
    const std::string url =
        "https://wttr.in/" + mw::URL::encode(city_name) + "?format=3";
    auto response =
        co_await context.runtime->http().get(mw::HTTPRequest(url));
    if(!response)
    {
        co_return std::unexpected(response.error());
    }
    if(response->status != 200)
    {
        co_return std::unexpected(
            mw::runtimeError("weather service returned HTTP " +
                             std::to_string(response->status)));
    }

    co_return agent_smith::ToolResult{
        .content = std::string(response->payloadAsStr()),
    };
}

void printToolResult(const agent_smith::ToolMessage& result)
{
    std::cout << (result.is_error ? "[tool error: " : "[tool result: ")
              << result.content << "]\n";
}

void printUsageReport(const agent_smith::Message& message)
{
    const auto* assistant =
        std::get_if<agent_smith::AssistantMessage>(&message);
    if(assistant != nullptr && assistant->usage)
    {
        std::cout << "[usage: prompt=" << assistant->usage->prompt
                  << ", completion=" << assistant->usage->completion
                  << ", total=" << assistant->usage->total << "]\n";
    }
}

void printStreamEvent(const agent_smith::StreamEvent& event)
{
    std::visit(
        [](const auto& value)
        {
            using Event = std::decay_t<decltype(value)>;
            if constexpr(std::is_same_v<Event, agent_smith::StreamTokenDelta>)
            {
                std::cout << value.text << std::flush;
            }
            else if constexpr(std::is_same_v<
                                  Event, agent_smith::StreamToolCallStarted>)
            {
                std::cout << "\n[tool call " << value.index << ": "
                          << value.name << "]\n";
            }
            else if constexpr(std::is_same_v<
                                  Event, agent_smith::StreamToolCallCompleted>)
            {
                std::cout << "[tool arguments " << value.index << ": "
                          << value.arguments.dump() << "]\n";
            }
            else
            {
                std::cout << '\n';
            }
        },
        event);
}

} // namespace

int main(int argc, char* argv[])
{
    if(argc > 1 && std::string_view(argv[1]) == "--help")
    {
        printUsage(std::cout);
        return 0;
    }

    const std::string endpoint_text =
        envOr("LLAMA_SERVER_URL", "http://127.0.0.1:8080");
    auto endpoint = mw::URL::fromStr(endpoint_text);
    if(!endpoint)
    {
        std::cerr << "Invalid LLAMA_SERVER_URL: " << endpoint.error().msg()
                  << '\n';
        return 2;
    }

    agent_smith::LlmClientConfig config{
        .endpoint = std::move(*endpoint),
        .model = envOr("LLAMA_MODEL", "local-model"),
    };
    if(const char* api_key = std::getenv("LLAMA_API_KEY"); api_key != nullptr)
    {
        config.api_key = api_key;
    }

    agent_smith::Runtime runtime;
    agent_smith::LlmClient client(runtime, std::move(config));
    // Retain at most 64 messages and approximately 8K tokens of history.
    auto memory = std::make_shared<agent_smith::RollingWindowMemory>(
        agent_smith::RollingWindowConfig{
            .max_messages = 64,
            .max_tokens = 8192,
        });
    auto tree = std::make_shared<agent_smith::AgentTreeState>();
    agent_smith::Agent agent(runtime, client, memory,
        {.max_iterations = 16, .tree_state = tree});
    auto registered = agent.addTool(
        agent_smith::makeTool(weatherSchema(), queryWeather));
    if(!registered)
    {
        std::cerr << "Tool registration failed: "
                  << registered.error().msg() << '\n';
        return 2;
    }
    agent.hooks().on_stream_event = printStreamEvent;
    agent.hooks().on_tool_result = printToolResult;
    agent.hooks().on_message = printUsageReport;

    if(argc <= 1 || std::string_view(argv[1]) != "--no-mcp")
    {
        std::vector<std::string> command{
            "python3", AGENT_SMITH_DEMO_MCP_SERVER};
        if(argc > 1)
        {
            if(std::string_view(argv[1]) != "--mcp" || argc < 3)
            {
                printUsage(std::cerr);
                return 2;
            }
            command.assign(argv + 2, argv + argc);
        }
        agent_smith::mcp::ServerConfig mcp_config{
            .alias = "demo",
            .transport = agent_smith::mcp::StdioServerConfig{
                std::move(command)},
        };
        auto connected = agent_smith::syncWait(
            agent.connectMcp(std::move(mcp_config)));
        if(!connected)
        {
            std::cerr << "MCP connection failed: " << connected.error().msg()
                      << "\nUse --no-mcp to run without an MCP server.\n";
            return 2;
        }
        std::cout << "[MCP demo connected]\n";
    }

    std::cout << "Agent Smith " << agent_smith::VERSION
              << " agent CLI. Enter /help for commands.\n";
    bool stream = true;
    std::string prompt;
    while(std::cout << "> " << std::flush, std::getline(std::cin, prompt))
    {
        if(prompt == "/quit")
        {
            break;
        }
        if(prompt == "/help")
        {
            printUsage(std::cout);
            continue;
        }
        if(prompt == "/clear")
        {
            memory->clear();
            tree->iterations.store(0);
            std::cout << "Conversation cleared.\n";
            continue;
        }
        if(prompt == "/stream on" || prompt == "/stream off")
        {
            stream = prompt == "/stream on";
            std::cout << "Streaming " << (stream ? "on" : "off") << ".\n";
            continue;
        }
        if(prompt.empty())
        {
            continue;
        }

        // Each interactive turn starts a fresh tree iteration budget.
        tree->iterations.store(0);
        auto result = agent_smith::syncWait(agent.send(
            agent_smith::AgentRequest{
                .message = {prompt},
                .stream = stream,
            }));
        if(!result)
        {
            std::cerr << "\nRequest failed: " << result.error().msg()
                      << "\nUse /clear to discard this conversation.\n";
            continue;
        }
        if(!stream)
        {
            std::cout << result->content << '\n';
        }
    }

    return 0;
}