This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
The system is built on pure event sourcing principles:
- 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 - State is projected from events - The
ContextWindowProjectionclass insrc/projection/openai/context_window_projection.hpprebuilds conversation state by replaying events - No direct state mutation - The
AgentCorenever mutates state directly; it only appends events to the event store
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) │
└──────────────────────┘
- Separation of Concerns:
AgentCorecontains pure business logic with no I/O, making it testable and reusable across different interfaces (CLI, GUI, web API) - Dependency Inversion: Core logic depends on interfaces (
ILLMClient,IEventStore,ITool), not concrete implementations - Event-Driven: All state changes emit events, enabling auditability, time-travel debugging, and event replay
- Streaming Support: Full support for OpenAI streaming with granular events (StreamingStarted, ContentDeltaReceived, ToolCallStarted, etc.)
- Tool Integration: MCP (Model Context Protocol) integration for external tool execution with process isolation
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
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
The agent integrates with MCP (Model Context Protocol) servers for external tool execution:
- 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
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.
- 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
sudo apt-get install -y libcurl4-openssl-dev nlohmann-json3-dev cmake build-essential# 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 --streamThe MCP C++ SDK is automatically fetched from GitHub during the CMake build process.
# 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 fileWhen 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)/quitor/exit- Exit the application
# 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/*.hppConfiguration in CPPLINT.cfg:
- Line length: 100 characters
- Filter: -legal/copyright,-build/c++11,-readability/todo
- Header-only implementation - All code is in .hpp files for simplicity (no separate .cpp files for libraries)
- Interface naming - Prefix interfaces with
I(ILLMClient, IEventStore, ITool) - Const correctness - Use
constfor all methods that don't modify state,std::string_viewfor read-only string parameters - Smart pointers - Use
std::shared_ptr<const Event>for events (immutability) - RAII - All resource cleanup via destructors (CURL handles, file streams, mutex locks)
- Thread safety - FileEventStore and tool registry use
std::mutexfor thread-safe operations
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.
The streaming implementation in OpenAILLMClient::complete_streaming (src/llm_client/openai_llm_client.hpp) handles OpenAI's SSE (Server-Sent Events) format:
- SSE Parsing - Custom CURL write callback parses
data: {...}lines - Buffering - Incomplete lines are buffered until
\narrives - Chunk Processing - Each JSON chunk is parsed and converted to
StreamChunk - Delta Accumulation - Content deltas are accumulated into final response
- 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.
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
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
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
- Create event class in
src/agent/events.hppinheriting fromEvent - Implement
to_json()method for serialization - Update
ContextWindowProjection::apply_event()if the event affects conversation state - Update
ToolsProjection::apply_event()if the event affects tool state - Emit the event in
AgentCoreat the appropriate point
- Create new class implementing
ILLMClient(src/agent/llm_client_interface.hpp) - Implement
complete()andcomplete_streaming()methods - Return
LLMResponsewith standardized fields - Handle provider-specific authentication and error formats
- Create new class implementing
IEventStore(src/agent/event_store_interface.hpp) - Implement
append()andget_events_by_conversation()methods - Ensure thread-safety if needed for your use case
- Update constructor in CLI to accept the new store type
- Create class implementing
IToolinterface (src/tools/tool_interface.hpp) - Implement required methods:
get_name(),get_description(),execute(),to_json_schema() - Register with agent:
agent.add_tool(std::move(tool))
For MCP-based tools, use MCPToolPlugin to load tools from external MCP servers.
While the project currently has no formal test suite, the event sourcing architecture makes testing straightforward:
- Unit testing AgentCore: Use
InMemoryEventStoreand a mockILLMClientto verify event emission - Integration testing: Compare event logs from
.sessions/files against expected sequences - Projection testing: Feed known events to
ContextWindowProjectionand verify state - 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.
# 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# 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# Environment variable (lowest priority)
export OPENAI_MODEL="gpt-4o"
# CLI argument (highest priority)
./samples/cli/build/chat --model gpt-4o --stream# 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 eventsFor 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)
The agent uses the MCP C++ SDK for tool integration, which is automatically fetched from GitHub during build:
- Repository: https://github.com/MKAbdElrahman/cpp-mcp
- Fetch Method: CMake FetchContent
- Build: Automatic during
make build
No manual installation required - CMake handles it.
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