BareGit
# Technical Design Document: Agent Smith Framework

## 1. Introduction
This document outlines the technical design for **Agent Smith**, a modular C++23 framework for building LLM-powered agents. It is designed to be highly efficient and leverages `libmw` for everyday system tasks, networking, and data handling. 

This guide is structured to help a new graduate software engineer understand the architecture and implement the system systematically.

---

## 2. High-Level Architecture
The framework relies on a decoupled architecture centered around the `Agent` class.

*   **`Agent`**: The orchestrator. It manages the conversational loop, maintains state via `Memory`, interacts with the `LlmClient`, and dynamically executes `Tools` and `Skills`.
*   **`LlmClient`**: Handles network communication with LLM APIs (OpenAI and Gemini) using `mw::url`.
*   **`Memory`**: Stores the conversation history. Can be in-memory or backed by `mw::sqlite` for persistence.
*   **`Tool`**: An interface representing a callable action. Includes native tools and integrations via the Model Context Protocol (MCP).
*   **`Skill`**: A configuration that bundles a specific system prompt and a subset of tools to focus the agent on a specific workflow.
*   **CLI**: A command-line interface implemented using `cxxopts` to allow users to configure the agent's connection parameters (API key and endpoint) at runtime.

---

## 3. Data Models

We use `nlohmann/json` (provided via `libmw`) for all dynamic data structures and serialization.

```cpp
#include <string>
#include <optional>
#include <vector>
#include <variant>
#include <nlohmann/json.hpp>

// 1. System Message: Defines the agent's persona and overarching rules.
struct SystemMessage
{
    std::string content;
};

// 2. User Message: Input provided by the human user.
struct UserMessage
{
    std::string content;
};

// Represents a single tool call requested by the Assistant
struct ToolCall
{
    std::string id;
    std::string name;
    nlohmann::json arguments;
};

// 3. Assistant Message: The LLM's response. Can contain text or tool calls.
struct AssistantMessage
{
    std::optional<std::string> content;
    std::vector<ToolCall> tool_calls; 
};

// 4. Tool Result Message: The result of executing a tool, sent back to LLM.
struct ToolResultMessage
{
    std::string tool_call_id;
    std::string content;
};

// The unified Message type using std::variant for compile-time type safety.
using Message = std::variant<
    SystemMessage, 
    UserMessage, 
    AssistantMessage, 
    ToolResultMessage>;

// Standalone serialization functions for each message type
nlohmann::json toJson(const SystemMessage& msg);
nlohmann::json toJson(const UserMessage& msg);
nlohmann::json toJson(const ToolCall& msg);
nlohmann::json toJson(const AssistantMessage& msg);
nlohmann::json toJson(const ToolResultMessage& msg);
nlohmann::json toJson(const Message& msg);
```

---

## 4. Component Design

### 4.1 LlmClient Interface
The `LlmClient` abstracts the underlying LLM provider. This component uses `mw::E<>` for explicit error handling and C++23 coroutines for asynchronous task management.

```cpp
#include <mw/error.hpp>

// A placeholder for a C++23 coroutine Task type. 
template<typename T> class Task; 

class LlmClient
{
public:
    virtual ~LlmClient() = default;
    
    // Submits the history and tools, returning the Assistant's response or an error
    virtual Task<mw::E<Message>> generateResponse(
        const std::vector<Message>& history, 
        const nlohmann::json& available_tools_schema
    ) = 0;
};
```
*   **Implementation Steps:**
    *   Implement `OpenAiClient`. Allow dependency injection of `mw::HTTPSessionInterface` (falling back to `mw::HTTPSession`) to enable rigorous mocked network testing.
    *   Use `mw::URL` to safely parse custom endpoints and append standard paths (like `chat/completions`).
    *   Deserialize the response body into the `Message` struct, utilizing `std::unexpected(mw::runtimeError(...))` on JSON parsing failures.

### 4.2 Tool and MCP System
A `Tool` must expose its JSON Schema so the LLM understands its parameters, and an execution method.

```cpp
class Tool
{
public:
    virtual ~Tool() = default;
    virtual std::string name() const = 0;
    virtual std::string description() const = 0;
    
    // Returns the JSON Schema describing the arguments the tool accepts
    virtual nlohmann::json parametersSchema() const = 0;
    
    // Executes the tool with the JSON arguments provided by the LLM
    virtual Task<mw::E<std::string>> execute(const nlohmann::json& arguments) = 0;
};
```

**Tool Registry:**
To support multiple agents and centralized tool management, tools are owned by a global or application-level `ToolRegistry`. Methods returning `mw::E<void>` signal success or report duplication errors.

```cpp
#include <unordered_map>
#include <memory>
#include <stdexcept>
#include <format>

class ToolRegistry
{
public:
    // Registers a tool. Throws an exception if the tool name already exists
    // to prevent silent collisions.
    void registerTool(std::unique_ptr<Tool> tool)
    {
        if(tools_.contains(tool->name()))
        {
            throw std::runtime_error(
                std::format("Tool '{}' is already registered.", tool->name()));
        }
        tools_[tool->name()] = std::move(tool);
    }

    // Registers a tool with a namespace prefix to avoid collisions
    // (e.g., from different MCP servers).
    void registerToolWithNamespace(
        const std::string& ns, 
        std::unique_ptr<Tool> tool)
    {
        // Implementation detail: The Tool interface or a wrapper class
        // would need to return the new name when name() is called.
        std::string prefixed_name = std::format("{}_{}", ns, tool->name());
        if(tools_.contains(prefixed_name))
        {
            throw std::runtime_error(
                std::format("Tool '{}' is already registered.", prefixed_name));
        }
        // ... wrap tool and store with prefixed_name
    }

    Tool* getTool(const std::string& name) const
    {
        auto it = tools_.find(name);
        return it != tools_.end() ? it->second.get() : nullptr;
    }
    
    std::vector<Tool*> getAllTools() const
    {
        std::vector<Tool*> result;
        for(const auto& [name, tool] : tools_)
        {
            result.push_back(tool.get());
        }
        return result;
    }

private:
    std::unordered_map<std::string, std::unique_ptr<Tool>> tools_;
};
```

*   **Handling Collisions & Namespacing:** LLMs require unique tool names. If an MCP server provides a tool named `search` and another server also provides `search`, the `ToolRegistry` will reject the second registration. To resolve this, external tools (like those from MCP) should be registered using `registerToolWithNamespace` (e.g., `github_search` vs `local_search`).
*   **MCP Integration:** 
    *   Implement an `McpClient` that can parse MCP server manifests.
    *   The `McpClient` will act as a factory, dynamically generating `Tool` objects that map to remote MCP functions and registering them with the `ToolRegistry` using the server's name as a namespace.

### 4.3 Agent Skills
A `Skill` overrides the agent's default behavior, giving it a new persona and restricted capabilities.

```cpp
struct Skill
{
    std::string name;
    std::string system_prompt;
    // Names of the tools allowed for this skill
    std::vector<std::string> allowed_tools; 
};
```

### 4.4 Memory Management
Handles the context window.

```cpp
class Memory
{
public:
    virtual ~Memory() = default;
    virtual void addMessage(const Message& msg) = 0;
    virtual std::vector<Message> getHistory() const = 0;
    virtual void clear() = 0;
};
```
*   **Implementation Steps:**
    *   Create `InMemoryMemory` (a simple `std::vector` wrapper).
    *   Create `SqliteMemory` using `mw::sqlite` to persist `Message` structs to a database file.

### 4.5 The Agent Core
The `Agent` ties the components together in its main conversational loop.

```cpp
#include <memory>
#include <vector>

class Agent
{
public:
    Agent(
        std::unique_ptr<LlmClient> client, 
        std::unique_ptr<Memory> memory, 
        ToolRegistry& tool_registry);
    
    // Grants the agent permission to use a specific tool from the registry
    mw::E<void> allowTool(const std::string& tool_name);
    void activateSkill(const Skill& skill);
    
    // The main entry point for user interaction
    Task<mw::E<std::string>> run(const std::string& user_input);
    
private:
    std::unique_ptr<LlmClient> client_;
    std::unique_ptr<Memory> memory_;
    ToolRegistry& tool_registry_;
    
    // List of tool names this agent is currently allowed to use
    std::vector<std::string> allowed_tools_;
    std::optional<Skill> current_skill_;
};
```

**The `run` Loop Logic (Pseudo-code):**
1. Append `user_input` as a `UserMessage` to `memory_`.
2. `loop`:
    1. Fetch `history` from `memory_`.
    2. Resolve `allowed_tools_` against the `tool_registry_` and convert them into a single JSON Schema array.
    3. `response_msg = co_await client_->generateResponse(history, tools_schema)`.
    4. Append `response_msg` (which is an `AssistantMessage`) to `memory_`.
    5. `if response_msg contains tool_calls`:
        1. For each `call` in `tool_calls`:
            1. Find matching `Tool* tool` in `tool_registry_`.
            2. `result = co_await tool->execute(call.arguments)`.
            3. Append a `ToolResultMessage` (containing `result` and `call.id`) to `memory_`.
    6. `else`:
        1. Break loop.
3. Return `response_msg.content` (or extract content from the variant).

---

## 5. Build System & Dependencies
*   **CMake:** The project will be built using CMake (`CMakeLists.txt`).
*   **C++23:** Ensure compiler flags support standard C++23 (`-std=c++23`).
*   **libmw:** Include `libmw` via `FetchContent` or as a Git submodule. 
    *   Link against `mw::core`, `mw::url`, `mw::sqlite`.
*   **Logging:** Use `spdlog` (bundled with `libmw`) for detailed debug and execution logging.

---

## 6. Implementation Plan (For the Developer)

To build this systematically, follow these phases:

1.  **Phase 1: Foundations & Build Setup** 
    *   Create the `CMakeLists.txt`. Integrate `libmw`. 
    *   Define the core structs (`Message`, `Role`, `Skill`).
2.  **Phase 2: Networking & LLM Client** 
    *   Implement the `LlmClient` interface. 
    *   Build `OpenAiClient` using `mw::url`. Test it with a hardcoded prompt to ensure you can parse the JSON responses.
3.  **Phase 3: The Core Loop** 
    *   Implement the `Agent` class and a simple `InMemoryMemory` class. 
    *   Get a basic back-and-forth chat working in the terminal *without* tools.
4.  **Phase 4: Tool Calling** 
    *   Implement the `Tool` interface. 
    *   Create a simple mock tool (e.g., `CalculatorTool`). 
    *   Implement the `Agent::run` loop logic to parse tool calls, execute them, and feed the results back to the LLM.
5.  **Phase 5: Persistence & Skills** 
    *   Implement `SqliteMemory` utilizing `mw::sqlite`.
    *   Add the ability to load and switch between `Skill` profiles.
6.  **Phase 6: Async Polish & MCP** 
    *   Finalize C++23 coroutine integration for all network and execution paths.
    *   Research and implement an `McpClient` to communicate with external MCP servers over stdio.