Skip to content

Latest commit

 

History

History
453 lines (340 loc) · 17.8 KB

File metadata and controls

453 lines (340 loc) · 17.8 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Repository Overview

This repository contains an Event-Sourced C++ Agent Framework - a conversational AI agent using event sourcing with OpenAI's API and MCP tool integration.

The agent framework uses event sourcing as its core architectural pattern, where all state changes are captured as immutable events and conversation state is reconstructed through event projection.

Core Architecture

Event Sourcing Pattern

The system is built on pure event sourcing principles:

  1. Events are the source of truth - All state changes (user messages, LLM responses, streaming deltas, tool calls, errors) are captured as immutable events in src/agent/events.hpp
  2. State is projected from events - The ContextWindowProjection class in src/projection/openai/context_window_projection.hpp rebuilds conversation state by replaying events
  3. No direct state mutation - The AgentCore never mutates state directly; it only appends events to the event store

Component Architecture

The framework follows a clean, layered architecture with dependency inversion:

┌─────────────────────────────────────────────────┐
│  I/O Layer (samples/cli/main.cpp)               │
│  - CLI11 argument parsing                       │
│  - User input/output                            │
│  - Command handling (/help, /clear, etc.)      │
└─────────────────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│  Agent Core (src/agent/agent_core.hpp)          │
│  - Pure business logic, no I/O                  │
│  - Emits events for all state changes           │
│  - Coordinates LLM client & event store         │
│  - Manages tool registry and execution          │
└─────────────────────────────────────────────────┘
         │                              │
         ▼                              ▼
┌──────────────────────┐    ┌──────────────────────┐
│  ILLMClient          │    │  IEventStore         │
│  (interface)         │    │  (interface)         │
└──────────────────────┘    └──────────────────────┘
         │                              │
         ▼                              ▼
┌──────────────────────┐    ┌──────────────────────┐
│  OpenAILLMClient     │    │  FileEventStore      │
│  - CURL-based HTTP   │    │  - JSONL files in    │
│  - SSE streaming     │    │    .sessions/        │
│  - Chunk parsing     │    │  - Thread-safe       │
└──────────────────────┘    └──────────────────────┘
                                       │
                                       ▼
                            ┌──────────────────────┐
                            │  InMemoryEventStore  │
                            │  (testing/dev)       │
                            └──────────────────────┘

Key Design Principles

  1. Separation of Concerns: AgentCore contains pure business logic with no I/O, making it testable and reusable across different interfaces (CLI, GUI, web API)
  2. Dependency Inversion: Core logic depends on interfaces (ILLMClient, IEventStore, ITool), not concrete implementations
  3. Event-Driven: All state changes emit events, enabling auditability, time-travel debugging, and event replay
  4. Streaming Support: Full support for OpenAI streaming with granular events (StreamingStarted, ContentDeltaReceived, ToolCallStarted, etc.)
  5. Tool Integration: MCP (Model Context Protocol) integration for external tool execution with process isolation

Event Types

Events are defined in src/agent/events.hpp:

Lifecycle Events:

  • AgentLoaded - Agent initialized with system prompt
  • AgentSwitched - Agent changed during conversation

Message Events:

  • UserMessageSubmitted - User sent a message
  • LLMResponseReceived - Complete LLM response received (non-streaming or final streaming result)

Streaming Events (for UI/debugging, not projected into state):

  • StreamingStarted - Streaming response began (chunk_id, model)
  • ContentDeltaReceived - Incremental content received during streaming
  • ToolCallStarted - LLM initiated a tool call
  • ToolCallArgumentsDelta - Incremental tool arguments during streaming
  • StreamingCompleted - Streaming finished with token usage

Tool Events:

  • ToolAdded - Tool registered with agent
  • ToolRemoved - Tool unregistered from agent
  • ToolUpdated - Tool configuration changed
  • ToolCalled - Tool execution initiated
  • ToolResulted - Tool execution succeeded
  • ToolErrored - Tool execution failed

Error Events:

  • ErrorOccurred - Error during processing

Event Projection

ContextWindowProjection (src/projection/openai/context_window_projection.hpp) rebuilds conversation state from events:

  • Filters out streaming detail events (ContentDeltaReceived, etc.) - they're for debugging/UI, not state
  • Accumulates messages (system, user, assistant, tool) from AgentLoaded, UserMessageSubmitted, LLMResponseReceived, ToolCalled, ToolResulted
  • Tracks token usage across all responses
  • Handles AgentSwitched by updating system prompt

ToolsProjection (src/projection/openai/tools_projection.hpp) rebuilds tool registry state from events:

  • Tracks available tools from ToolAdded/ToolRemoved/ToolUpdated events
  • Converts to OpenAI function calling format
  • Normalizes tool names (dots → underscores for OpenAI compatibility)
  • Maps normalized names back to original names

Tool System

The agent integrates with MCP (Model Context Protocol) servers for external tool execution:

Architecture

  • ITool Interface - Abstract interface all tools must implement
  • MCPTool - Wraps MCP server functions as ITool implementations
  • MCPToolPlugin - Manages connection to MCP server and auto-loads tools
  • Tool Registry - Name-based lookup in AgentCore
  • Tool Loop - Automatic execution with max 10 iterations

Adding MCP Tools

Tools are loaded from external MCP servers:

// In main.cpp or agent initialization
auto plugin = create_mcp_stdio_plugin(
    "calculator",
    "Math operations",
    "./calculator_server"
);
agent.add_mcp_plugin(std::move(plugin));

Each MCP function becomes a separate tool in the registry (e.g., calculator.add, calculator.multiply).

See Architecture/TOOL_SYSTEM_ARCHITECTURE.md for detailed tool system documentation.

Build and Development

Dependencies

  • C++17 compiler
  • CMake 3.14+
  • libcurl - HTTP client for OpenAI API
  • nlohmann/json - JSON parsing
  • CLI11 - Command-line parsing (fetched by CMake)
  • MCP C++ SDK - Fetched from https://github.com/MKAbdElrahman/cpp-mcp

Installation (Ubuntu/Debian)

sudo apt-get install -y libcurl4-openssl-dev nlohmann-json3-dev cmake build-essential

Build Commands

# Build the chat CLI application
make build
# or manually:
mkdir -p samples/cli/build && cd samples/cli/build && cmake .. && make

# Clean all build artifacts
make clean

# Run the chat CLI with streaming
make chat
# or manually:
./samples/cli/build/chat --stream

The MCP C++ SDK is automatically fetched from GitHub during the CMake build process.

Running the Application

# Set API key
export OPENAI_API_KEY="your-key-here"

# Run with streaming (recommended)
./samples/cli/build/chat --stream

# Run with custom model
./samples/cli/build/chat --model gpt-4o --stream

# Run with config file
./samples/cli/build/chat --config samples/cli/configs/your-config.ini

# Run with verbose output (shows token counts)
./samples/cli/build/chat --stream -v

# Available CLI options:
#   -k, --api-key    OpenAI API key (or use OPENAI_API_KEY env var)
#   -m, --model      Model name (default: gpt-4)
#   -s, --system     System message (default: "You are a helpful assistant.")
#   -d, --dir        Event store directory (default: current directory)
#   -v, --verbose    Enable verbose output (token counts)
#   --stream         Enable streaming responses
#   --config         Read configuration from INI file

Interactive Commands

When running the chat CLI:

  • /help - Show available commands
  • /clear - Clear conversation and restart
  • /history - Show conversation state (messages and token usage)
  • /events - Show full event log (all events including streaming details)
  • /quit or /exit - Exit the application

Code Style

Linting

# Install cpplint (Google C++ Style Guide)
make lint-install

# Run linting on all source files
make lint

# Manually run cpplint
cpplint --counting=detailed --extensions=hpp,cpp src/agent/*.hpp

Configuration in CPPLINT.cfg:

  • Line length: 100 characters
  • Filter: -legal/copyright,-build/c++11,-readability/todo

Coding Conventions

  1. Header-only implementation - All code is in .hpp files for simplicity (no separate .cpp files for libraries)
  2. Interface naming - Prefix interfaces with I (ILLMClient, IEventStore, ITool)
  3. Const correctness - Use const for all methods that don't modify state, std::string_view for read-only string parameters
  4. Smart pointers - Use std::shared_ptr<const Event> for events (immutability)
  5. RAII - All resource cleanup via destructors (CURL handles, file streams, mutex locks)
  6. Thread safety - FileEventStore and tool registry use std::mutex for thread-safe operations

Helper Pattern

The codebase uses small, focused helper functions with descriptive names to improve readability:

// Example: Instead of complex conditionals, use named helpers
static bool is_empty_line(const std::string& line) {
    return line.empty() || line == "\r";
}

static bool has_content_delta(const StreamChunk& chunk) {
    return !chunk.content_delta.empty();
}

This pattern is used extensively in openai_llm_client.hpp for SSE parsing and agent_core.hpp for event emission logic.

Streaming Implementation

The streaming implementation in OpenAILLMClient::complete_streaming (src/llm_client/openai_llm_client.hpp) handles OpenAI's SSE (Server-Sent Events) format:

  1. SSE Parsing - Custom CURL write callback parses data: {...} lines
  2. Buffering - Incomplete lines are buffered until \n arrives
  3. Chunk Processing - Each JSON chunk is parsed and converted to StreamChunk
  4. Delta Accumulation - Content deltas are accumulated into final response
  5. Usage Reporting - Token usage comes in the final chunk with include_usage: true

The callback pattern allows the CLI to display content in real-time while AgentCore emits granular events.

Event Store

FileEventStore

Events are persisted to .sessions/{timestamp}_{conversation_id}.jsonl:

  • One JSON object per line (JSONL format)
  • Thread-safe writes with mutex
  • Files created automatically in .sessions/ directory
  • Filename format: YYYYMMDD_HHMMSS_{conversation_id}.jsonl

InMemoryEventStore

Available in src/event_store/in_memory_event_store.hpp for testing/development:

  • No persistence, events stored in vector
  • Thread-safe with mutex
  • Useful for unit tests

Project Structure

cpp-agent/
├── src/
│   ├── agent/
│   │   ├── agent_core.hpp           # Core agent logic (pure, no I/O)
│   │   ├── events.hpp               # Event class definitions
│   │   ├── event_store_interface.hpp # IEventStore interface
│   │   └── llm_client_interface.hpp # ILLMClient interface
│   ├── event_store/
│   │   ├── file_event_store.hpp     # JSONL file persistence
│   │   └── in_memory_event_store.hpp # In-memory storage
│   ├── llm_client/
│   │   └── openai_llm_client.hpp    # OpenAI API client with streaming
│   ├── projection/
│   │   └── openai/                  # OpenAI-specific projections
│   │       ├── context_window_projection.hpp # Event → OpenAI messages
│   │       └── tools_projection.hpp # Event → OpenAI tools
│   └── tools/
│       ├── tool_interface.hpp       # ITool interface
│       ├── tool_base.hpp            # BaseTool implementation
│       ├── tool_result.hpp          # ToolExecutionResult
│       └── mcp/                     # MCP integration
│           ├── mcp_tool.hpp         # MCPTool wrapper
│           ├── mcp_plugin.hpp       # MCPToolPlugin
│           └── mcp_transport.hpp    # Subprocess transport
├── samples/
│   └── cli/
│       ├── main.cpp                 # CLI application (I/O layer)
│       ├── CMakeLists.txt           # Build configuration
│       ├── configs/                 # Example config files
│       └── build/                   # Build artifacts (gitignored)
├── Architecture/
│   ├── EVENT_STORE_PROJECTION_ARCHITECTURE.md
│   └── TOOL_SYSTEM_ARCHITECTURE.md
├── .sessions/                       # Event log files (gitignored)
├── Makefile                         # Convenience targets
├── CPPLINT.cfg                      # Linting configuration
└── README.md                        # User-facing documentation

Adding New Features

Adding a New Event Type

  1. Create event class in src/agent/events.hpp inheriting from Event
  2. Implement to_json() method for serialization
  3. Update ContextWindowProjection::apply_event() if the event affects conversation state
  4. Update ToolsProjection::apply_event() if the event affects tool state
  5. Emit the event in AgentCore at the appropriate point

Adding a New LLM Provider

  1. Create new class implementing ILLMClient (src/agent/llm_client_interface.hpp)
  2. Implement complete() and complete_streaming() methods
  3. Return LLMResponse with standardized fields
  4. Handle provider-specific authentication and error formats

Adding a New Event Store Backend

  1. Create new class implementing IEventStore (src/agent/event_store_interface.hpp)
  2. Implement append() and get_events_by_conversation() methods
  3. Ensure thread-safety if needed for your use case
  4. Update constructor in CLI to accept the new store type

Adding Custom Tools

  1. Create class implementing ITool interface (src/tools/tool_interface.hpp)
  2. Implement required methods: get_name(), get_description(), execute(), to_json_schema()
  3. Register with agent: agent.add_tool(std::move(tool))

For MCP-based tools, use MCPToolPlugin to load tools from external MCP servers.

Testing Approach

While the project currently has no formal test suite, the event sourcing architecture makes testing straightforward:

  1. Unit testing AgentCore: Use InMemoryEventStore and a mock ILLMClient to verify event emission
  2. Integration testing: Compare event logs from .sessions/ files against expected sequences
  3. Projection testing: Feed known events to ContextWindowProjection and verify state
  4. Tool testing: Verify ToolCalled/ToolResulted events in event log

The separation of AgentCore (pure logic) from the CLI (I/O) makes the core highly testable without mocking I/O.

Common Development Tasks

Debugging Event Flow

# Run with verbose mode to see token counts
./samples/cli/build/chat --stream -v

# During conversation, use /events to see all emitted events
> /events

# Check the event log file directly
cat .sessions/[most-recent-file].jsonl | jq

Modifying System Prompt

# Via CLI
./samples/cli/build/chat --system "You are a pirate assistant." --stream

# Via config file (samples/cli/configs/)
./samples/cli/build/chat --config my-config.ini

Changing Models

# Environment variable (lowest priority)
export OPENAI_MODEL="gpt-4o"

# CLI argument (highest priority)
./samples/cli/build/chat --model gpt-4o --stream

Working with Tools

# Tools are loaded from MCP servers at startup
# See samples/cli/main.cpp for examples

# During conversation:
> /history    # Shows tool calls in conversation
> /events     # Shows ToolCalled/ToolResulted events

Architecture Documentation

For detailed architecture diagrams and explanations:

  • Event Store & Projections: Architecture/EVENT_STORE_PROJECTION_ARCHITECTURE.md
  • Tool System: Architecture/TOOL_SYSTEM_ARCHITECTURE.md
  • Complete Architecture: ARCHITECTURE.md (if exists)

External Dependencies

MCP C++ SDK

The agent uses the MCP C++ SDK for tool integration, which is automatically fetched from GitHub during build:

No manual installation required - CMake handles it.

Future Extensions

Potential improvements:

  • Retry logic with exponential backoff
  • Async/await support
  • Connection pooling for HTTP requests
  • Tool approval system (UI for approving tool executions)
  • Multiple LLM provider support (Anthropic, etc.)
  • Better error handling and recovery
  • Formal test suite
  • Event replay UI for debugging