# CodeAct Agent Architecture

**Date:** 2026-01-09
**Version:** 1.0
**Codebase:** `src/universal_mcp/agents/codeact0/`

---

## Table of Contents

1. [Overview](#overview)
2. [Core Architecture](#core-architecture)
3. [LangGraph Structure](#langgraph-structure)
4. [Sandbox Execution System](#sandbox-execution-system)
5. [Tool Discovery & Loading](#tool-discovery--loading)
6. [State Management](#state-management)
7. [LLM Integration](#llm-integration)
8. [Execution Flows](#execution-flows)
9. [Key Design Patterns](#key-design-patterns)
10. [Configuration](#configuration)

---

## Overview

CodeAct is a **general-purpose AI agent** built on LangGraph that enables code-based task execution through a sophisticated sandbox environment. The agent can discover and load external tools dynamically, execute Python code with persistent context, and handle complex multi-step tasks.

### Key Capabilities

- **Stateful Sandbox Execution**: Python code execution with context persistence across turns
- **Dynamic Tool Loading**: Vector-based semantic search and runtime tool injection
- **Multi-Provider LLM Support**: Anthropic, OpenAI, Google, AWS Bedrock with automatic fallback
- **Error Recovery**: Enhanced error messages with specific recovery guidance
- **Agent Building**: Self-modifying capability to create new specialized agents

### Design Philosophy

- **General-Purpose**: No task-specific prompts; adapts to any user request
- **Code-First**: Tasks are solved through Python code execution, not hardcoded logic
- **Tool-Agnostic**: Works with any tool that follows the naming convention
- **Resilient**: Automatic fallback, retry logic, and comprehensive error handling

---

## Core Architecture

### Directory Structure

```
src/universal_mcp/agents/codeact0/
├── agent.py                    # Main agent factory and graph builder
├── graph.py                    # LangGraph node definitions
├── sandbox.py                  # Python execution sandbox
├── state.py                    # State schema definitions
├── config.py                   # Configuration classes
├── nodes/
│   ├── route_entry.py          # Entry point routing
│   ├── call_model.py           # LLM invocation with fallback
│   ├── execute_tools.py        # Tool execution handlers
│   └── planner.py              # Agent builder planning nodes
├── prompts/
│   ├── core.py                 # Core directives
│   ├── instructions.py         # Task instructions
│   ├── personality.py          # Agent personality
│   ├── tools.py                # Tool usage guidelines
│   ├── output.py               # Output formatting rules
│   └── agent_builder.py        # Agent creation prompts
└── tools/
    ├── meta_tools.py           # Search, load, web_search tools
    └── sandbox_tools.py        # Sandbox tool wrappers
```

### Component Responsibilities

| Component | Responsibility |
|-----------|----------------|
| **agent.py** | Agent factory, graph assembly, entry point |
| **graph.py** | LangGraph node definitions and logic |
| **sandbox.py** | Isolated Python execution with context management |
| **state.py** | State schema and type definitions |
| **nodes/** | Individual graph node implementations |
| **prompts/** | Modular prompt composition |
| **tools/** | Meta-tools for tool discovery and execution |

---

## LangGraph Structure

### Graph Overview

CodeAct uses a **3-node cyclic graph** with command-based routing:

```mermaid
graph LR
    START([START]) --> route_entry[route_entry]
    route_entry --> call_model[call_model]
    call_model --> execute_tools[execute_tools]
    execute_tools --> call_model
    call_model --> END([END])
```

### Node Descriptions

#### 1. `route_entry` (Entry Point)
**File:** `nodes/route_entry.py`

**Purpose:** Initialize system prompts and route to model invocation

**Responsibilities:**
- Add system message with complete instructions (core + personality + tools + output)
- Set up prompt caching breakpoints
- Route to `call_model` node

**Key Features:**
- Modular prompt composition from multiple sources
- Anthropic prompt caching with 5-minute TTL
- First-turn cache creation, subsequent turns cache hit

#### 2. `call_model` (LLM Invocation)
**File:** `nodes/call_model.py`

**Purpose:** Invoke LLM with bound tools and handle responses

**Bound Tools (6 meta-tools):**
1. `execute_python_code` - Run Python in sandbox
2. `search_functions` - Search for external tools
3. `load_functions` - Load tools into sandbox
4. `plan_agent` - Create agent creation plan
5. `code_and_save_agent` - Generate and save agent code
6. `write_agent_creation_checklist` - Document agent creation

**Responsibilities:**
- Bind tools to LLM
- Invoke model with retry logic (max 3 attempts)
- Handle fallback to alternative model on failure
- Route to `execute_tools` if tool calls present, else END

**Fallback Mechanism:**
```python
Primary Model (Anthropic) Fails
    ↓
Remove thinking blocks (thinking not supported by all models)
    ↓
Invoke Fallback Model (Gemini)
    ↓
Return response or raise error
```

#### 3. `execute_tools` (Tool Execution)
**File:** `nodes/execute_tools.py`

**Purpose:** Execute tool calls and update state

**Tool Handlers:**
- `handle_execute_python_code` - Sandbox execution with context injection
- `handle_search_functions` - Vector search in tool registry
- `handle_load_functions` - Validate and load tools
- `handle_plan_agent` - Create structured agent plan
- `handle_code_and_save_agent` - Generate agent code
- `handle_write_agent_creation_checklist` - Document workflow

**Responsibilities:**
- Parse tool calls from LLM response
- Execute each tool sequentially
- Update state with results
- Handle errors gracefully
- Route back to `call_model` with tool results

### Command-Based Routing

**No conditional edges needed** - routing is determined by:
- `route_entry` → always routes to `call_model`
- `call_model` → routes to `execute_tools` if `tool_calls` present, else END
- `execute_tools` → always routes back to `call_model`

---

## Sandbox Execution System

### Overview
**File:** `sandbox.py`

The sandbox provides **isolated Python code execution** with persistent context across conversation turns.

### Key Features

#### 1. Stateful Execution

```python
# Turn 1
code = "x = 10"
# Context: {x: 10}

# Turn 2
code = "y = x * 2"
# Context: {x: 10, y: 20}

# Turn 3
code = "print(x + y)"
# Output: "30"
```

**Implementation:** `Sandbox.context` dictionary persists variables, functions, and classes

#### 2. AST-Based Context Derivation

**Purpose:** Automatically extract reusable code entities

**Method:** `_derive_context(code, context)`

**Extracted Entities:**
- **Imports:** `import pandas as pd` → stored in `context["imports"]`
- **Functions:** `async def helper():` → stored in `context["functions"]`
- **Classes:** `class MyClass:` → stored in `context["classes"]`

**Why AST?**
- Robust parsing even with complex code
- Fallback to regex if syntax errors
- Enables context injection in future executions

#### 3. Context Injection

**Purpose:** Restore previously defined code entities

**Method:** `_inject_context(context_dict, existing_namespace)`

**Process:**
1. Start with existing namespace (loaded tools + previous variables)
2. Execute import statements: `exec("import pandas as pd", namespace)`
3. Execute class definitions: `exec(class_def, namespace)`
4. Execute function definitions: `exec(func_def, namespace)`
5. Return updated namespace

**Handles Failures:**
- Import failures → create placeholders
- Class failures → create placeholder class
- Function failures → create placeholder function

#### 4. Smart Output Truncation

**Purpose:** Prevent context overflow while preserving useful information

**Method:** `smart_truncate(output)`

**Truncation Rules:**
- Output ≤ 5000 chars → return full output
- Output > 5000 chars → take first 50 lines + last 50 lines
- Output > 10,000 chars → hard truncate to 10,000 chars

**Why Truncate?**
- LLM context windows are limited
- Large outputs (e.g., 100,000 line CSV) would fill entire context
- Head + tail preserves structure and end results

#### 5. Async Support

**Feature:** Top-level `await` support

**Implementation:**
```python
compile(code, "<string>", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
```

**Allows:**
```python
# User can write this directly
result = await async_function()
print(result)
```

**Without needing:**
```python
# Traditional approach (NOT required)
async def main():
    result = await async_function()
    print(result)

asyncio.run(main())
```

#### 6. Enhanced Error Handling

**8 Error Categories with Recovery Hints:**

1. **TimeoutError** - Timeout exceeded (600s default)
   - Suggests breaking into smaller steps
   - Warns about infinite loops

2. **SyntaxError/IndentationError** - Code syntax issues
   - Provides syntax checklist
   - Reminds about `async def` requirement

3. **ImportError/ModuleNotFoundError** - Module not available
   - Lists available modules (stdlib + pandas)
   - Guides to use `search_functions` for tools

4. **NameError** - Undefined variable/function
   - Smart detection: identifies tool functions (contains `__`)
   - Specific guidance to load tools first

5. **ZeroDivisionError** - Division by zero
   - Specific recovery steps
   - Code examples

6. **TypeError** - Type mismatch or wrong arguments
   - Check function signatures
   - Ensure async functions use `await`

7. **ValueError/KeyError/IndexError/AttributeError** - Runtime errors
   - Customized hints per error type
   - Suggests using `smart_print()` to examine data

8. **Generic Exception** - Unexpected errors
   - General debugging advice
   - Suggests breaking down code

#### 7. Variable Filtering

**Purpose:** Keep only serializable variables in context

**Method:** Pickle test in `eval_unsafe()`

**Excluded Types:**
- Modules: `types.ModuleType`
- Regex patterns: `re.Pattern`
- Threading objects: `Lock`, `Event`, `Semaphore`
- I/O objects: `socket.socket`, `io.IOBase`
- Coroutines and async generators

**Why?**
- State must be serializable for checkpointing
- Prevents "pickle errors" on state save
- Keeps context clean and manageable

---

## Tool Discovery & Loading

### Overview
**File:** `tools/meta_tools.py`

CodeAct uses a **3-phase tool workflow**: search → load → execute

### Phase 1: Search (`search_functions`)

**Purpose:** Find relevant tools using semantic search

**Input:**
```python
queries = ["send email", "calendar events"]
```

**Process:**
1. Query tool registry with vector similarity search
2. Threshold: 0.75 distance (high relevance required)
3. Prioritize connected apps over unconnected
4. Return metadata: tool ID, name, description, connection status

**Output:**
```json
[
  {
    "id": "tool_123",
    "name": "google_gmail__send_email",
    "description": "Send an email via Gmail",
    "app": "Google Gmail",
    "connected": true,
    "distance": 0.82
  }
]
```

### Phase 2: Load (`load_functions`)

**Purpose:** Validate and inject tools into sandbox

**Input:**
```python
tool_ids = ["tool_123", "tool_456"]
```

**Process:**
1. Fetch tool definitions from registry
2. Validate tool availability
3. Add to `state.selected_tool_ids` queue
4. Mark tools for injection in next execution

**State Update:**
```python
state.selected_tool_ids = ["tool_123", "tool_456"]
# Tools will be available in next execute_python_code call
```

### Phase 3: Execute (`execute_python_code`)

**Purpose:** Use loaded tools in Python code

**How Tools Become Available:**

1. **Tool Loading** (`_load_tools()` in `execute_tools.py`):
```python
tools_context = {}
for tool_id in state.selected_tool_ids:
    tool_func = registry.get_tool(tool_id)
    tools_context[tool_func.__name__] = tool_func
# Result: {"google_gmail__send_email": <function>, ...}
```

2. **Context Merging** (in `sandbox.handle_execute_python_code()`):
```python
context = {**tools_context, **effective_existing_context}
# tools_context: loaded tool functions
# effective_existing_context: previous variables/functions
```

3. **Execution**:
```python
# User's code
result = await google_gmail__send_email(
    to="user@example.com",
    subject="Hello",
    body="Test email"
)
print(result)
```

### Tool Naming Convention

**Format:** `{app}__{function_name}`

**Examples:**
- `google_gmail__send_email`
- `google_calendar__create_event`
- `zenquotes__get_random_quote`

**Why `__`?**
- Parseble separator
- Clear namespace distinction
- Easy to detect in NameError (tool vs regular variable)

### Connection Prioritization

**Connected Apps:**
- User has authenticated
- Tools work immediately
- Shown first in search results

**Unconnected Apps:**
- User hasn't authenticated
- May require OAuth flow
- Shown after connected apps

**User Choice:**
- If multiple apps available, agent asks user to choose
- If user specifies app name, use that regardless of connection status

---

## State Management

### State Schema
**File:** `state.py`

### `CodeActState` (Main State)

```python
@dataclass
class CodeActState:
    messages: Annotated[list[BaseMessage], add_messages]
    # Conversation history with LangGraph's add_messages reducer

    selected_tool_ids: Annotated[list[str], _tool_ids_reducer]
    # FIFO queue of loaded tool IDs (max 20-30 tools)

    execution_context: dict[str, Any]
    # Persistent variables/functions from code execution

    add_context: dict[str, list[str]]
    # User-defined imports, functions, classes
    # Structure: {"imports": [...], "functions": [...], "classes": [...]}

    plan: dict | None
    # Agent creation plan (for agent builder workflow)

    agent: dict | None
    # Agent metadata (for agent builder workflow)
```

### Key State Updates

#### Messages
- Updated by: LangGraph's `add_messages` reducer
- Append-only list
- Contains: user messages, assistant messages, tool calls, tool results

#### Selected Tool IDs
- Updated by: `load_functions` tool
- Custom reducer: `_tool_ids_reducer`
- Behavior: FIFO queue, keeps recent tools loaded
- Why: Prevents unbounded growth, maintains relevant tools

#### Execution Context
- Updated by: `execute_python_code` after successful execution
- Contains: picklable variables, user-defined functions
- Persists across turns within same thread

#### Add Context
- Updated by: `sandbox._derive_context()` after execution
- Contains: import statements, class definitions, function definitions
- Used for: context injection in future executions

### State Persistence

**LangGraph Checkpointing:**
- State saved after each node execution
- Enables: conversation history, multi-turn interactions, resumability
- Backend: PostgreSQL (configured in `langgraph.json`)

---

## LLM Integration

### Multi-Provider Support
**File:** `llm.py`

### Supported Providers

| Provider | Models | Features |
|----------|--------|----------|
| **Anthropic** | Claude Sonnet, Opus, Haiku | Prompt caching, thinking blocks |
| **OpenAI** | GPT-4, GPT-3.5 | Function calling |
| **Google** | Gemini 2.0, 1.5 | Fast inference |
| **AWS Bedrock** | Claude via Bedrock | AWS integration |

### Model String Format

**Pattern:** `provider:model-name`

**Examples:**
- `anthropic:claude-sonnet-4-5-20250929`
- `openai:gpt-4-turbo`
- `google:gemini-2.0-flash`
- `bedrock:anthropic.claude-v2`

### Fallback Mechanism

**Primary Model:** Configured in agent initialization (default: Claude Sonnet)
**Fallback Model:** Configured as backup (default: Gemini 2.0 Flash)

**Trigger Conditions:**
- Primary model API error
- Rate limit exceeded
- Service unavailable

**Process:**
1. Try primary model
2. On failure, remove thinking blocks (not all models support)
3. Try fallback model
4. On failure, raise error to user

**Code:**
```python
try:
    response = await self.model.ainvoke(state["messages"])
except Exception as e:
    logger.warning(f"Primary model failed: {e}, trying fallback")
    messages_no_thinking = remove_thinking(state["messages"])
    response = await self.fallback_model.ainvoke(messages_no_thinking)
```

### Prompt Caching (Anthropic Only)

**Purpose:** Reduce latency and cost for repeated context

**Implementation:**
```python
cache_control = {"type": "ephemeral", "ttl": "5m"}
system_message.cache_control = cache_control  # Cache system prompt
last_user_message.cache_control = cache_control  # Cache conversation
```

**Performance:**
- **First turn:** `cache_creation: 8148, cache_read: 0`
- **Second turn:** `cache_creation: 350, cache_read: 7804`
- **Hit rate:** ~96% after warm-up

**TTL:** 5 minutes (reasonable for conversation sessions)

---

## Execution Flows

### 1. Simple Question Flow

```mermaid
sequenceDiagram
    participant User
    participant RouteEntry
    participant CallModel
    participant LLM

    User->>RouteEntry: "What is 2+2?"
    RouteEntry->>CallModel: Add system prompt
    CallModel->>LLM: Invoke with messages
    LLM->>CallModel: Text response (no tools)
    CallModel->>User: "The answer is 4"
```

### 2. Code Execution Flow

```mermaid
sequenceDiagram
    participant User
    participant CallModel
    participant ExecuteTools
    participant Sandbox

    User->>CallModel: "Calculate 123 * 456"
    CallModel->>ExecuteTools: execute_python_code(snippet="result = 123 * 456\nprint(result)")
    ExecuteTools->>Sandbox: eval_unsafe(code)
    Sandbox->>ExecuteTools: output="56088"
    ExecuteTools->>CallModel: Tool result message
    CallModel->>User: "The result is 56,088"
```

### 3. Tool Discovery & Usage Flow

```mermaid
sequenceDiagram
    participant User
    participant CallModel
    participant ExecuteTools
    participant Registry
    participant Sandbox

    User->>CallModel: "Get a random quote"

    Note over CallModel: Phase 1: Search
    CallModel->>ExecuteTools: search_functions(queries=["random quote"])
    ExecuteTools->>Registry: Vector search
    Registry->>ExecuteTools: [zenquotes__get_random_quote]
    ExecuteTools->>CallModel: Search results

    Note over CallModel: Phase 2: Load
    CallModel->>ExecuteTools: load_functions(tool_ids=["tool_123"])
    ExecuteTools->>ExecuteTools: Update state.selected_tool_ids
    ExecuteTools->>CallModel: Tools loaded

    Note over CallModel: Phase 3: Execute
    CallModel->>ExecuteTools: execute_python_code(snippet="quote = await zenquotes__get_random_quote()\nprint(quote)")
    ExecuteTools->>Sandbox: eval_unsafe with tools_context
    Sandbox->>ExecuteTools: output={"quote": "...", "author": "..."}
    ExecuteTools->>CallModel: Tool result
    CallModel->>User: "Here's a random quote: ..."
```

### 4. Multi-Turn Context Flow

```mermaid
sequenceDiagram
    participant User
    participant CallModel
    participant ExecuteTools
    participant Sandbox

    Note over Sandbox: execution_context = {}

    User->>CallModel: "Set x = 10"
    CallModel->>ExecuteTools: execute_python_code("x = 10")
    ExecuteTools->>Sandbox: eval_unsafe
    Sandbox->>ExecuteTools: output, context={x: 10}
    Note over Sandbox: execution_context = {x: 10}
    ExecuteTools->>CallModel: Success

    User->>CallModel: "What is x * 2?"
    CallModel->>ExecuteTools: execute_python_code("result = x * 2\nprint(result)")
    ExecuteTools->>Sandbox: eval_unsafe(context={x: 10})
    Sandbox->>ExecuteTools: output="20", context={x: 10, result: 20}
    Note over Sandbox: execution_context = {x: 10, result: 20}
    ExecuteTools->>CallModel: "20"
    CallModel->>User: "The result is 20"
```

### 5. Error Recovery Flow

```mermaid
sequenceDiagram
    participant User
    participant CallModel
    participant ExecuteTools
    participant Sandbox

    User->>CallModel: "Divide 10 by 0"
    CallModel->>ExecuteTools: execute_python_code("result = 10 / 0")
    ExecuteTools->>Sandbox: eval_unsafe
    Sandbox->>Sandbox: Catch ZeroDivisionError
    Sandbox->>ExecuteTools: Error with recovery hints
    ExecuteTools->>CallModel: Tool result with error
    CallModel->>User: "Got ZeroDivisionError. Let me fix this..."

    Note over CallModel: LLM reads recovery hints

    CallModel->>ExecuteTools: execute_python_code("result = 10 / 2 if 2 != 0 else 0")
    ExecuteTools->>Sandbox: eval_unsafe
    Sandbox->>ExecuteTools: output="5.0"
    ExecuteTools->>CallModel: Success
    CallModel->>User: "Fixed! Result is 5.0"
```

---

## Key Design Patterns

### 1. Factory Pattern with Closures

**Problem:** Avoid global state in LangGraph nodes

**Solution:** Factory functions that close over agent instance

```python
def get_codeact_agent(config):
    # Create agent instance
    agent = CodeActAgent(config)

    # Factory function for route_entry node
    def create_route_entry():
        def route_entry_node(state):
            # Closure over agent instance
            return agent.route_entry(state)
        return route_entry_node

    # Build graph with factory-created nodes
    graph.add_node("route_entry", create_route_entry())
```

**Benefits:**
- No global state
- Each agent instance independent
- Thread-safe

### 2. Command-Based Routing

**Problem:** Complex conditional edges in LangGraph

**Solution:** Nodes return routing commands in state

```python
def call_model(state):
    response = llm.invoke(state["messages"])

    if response.tool_calls:
        return {"messages": [response], "next": "execute_tools"}
    else:
        return {"messages": [response], "next": END}
```

**Benefits:**
- No conditional edges needed
- Clear routing logic
- Easier debugging

### 3. Context Injection Pattern

**Problem:** Make tools available in sandbox without manual imports

**Solution:** Merge tool functions into execution namespace

```python
# Load tools
tools_context = {"tool1": <func>, "tool2": <func>}

# Merge with existing context
context = {**tools_context, **execution_context, **user_context}

# Execute in merged namespace
exec(code, context, context)
```

**Benefits:**
- Tools "just work" in user code
- No import statements needed
- Clean user experience

### 4. Stateful Sandbox Pattern

**Problem:** Context persistence across code executions

**Solution:** Maintain execution context in state

```python
# After execution
filtered_vars = {k: v for k, v in locals.items() if picklable(v)}
state["execution_context"] = filtered_vars

# Next execution
namespace = state["execution_context"].copy()
exec(code, namespace, namespace)
```

**Benefits:**
- Variables persist across turns
- Functions remain available
- Natural Python experience

### 5. Graceful Degradation Pattern

**Problem:** Primary model failures

**Solution:** Automatic fallback with feature removal

```python
try:
    return await primary_model.invoke(messages)
except Exception:
    messages_simplified = remove_unsupported_features(messages)
    return await fallback_model.invoke(messages_simplified)
```

**Benefits:**
- High availability
- Transparent to user
- Resilient system

---

## Configuration

### Configuration System
**File:** `config.py`

### `CodeActConfig` Structure

```python
@dataclass
class SandboxConfig:
    timeout: int = 600  # Execution timeout in seconds

@dataclass
class ToolConfig:
    tool_search_threshold: float = 0.75  # Vector similarity
    search_limit: int = 20  # Max search results
    max_tools_per_app: int = 5  # Tools per app in results

@dataclass
class RetryConfig:
    max_retries: int = 3  # LLM invocation retries

@dataclass
class LLMConfig:
    default_model: str = "anthropic:claude-haiku-4-5"
    fallback_model: str = "google:gemini-2.0-flash"

@dataclass
class GraphConfig:
    recursion_limit: int = 75  # Max graph depth

@dataclass
class CodeActConfig:
    sandbox: SandboxConfig = SandboxConfig()
    tools: ToolConfig = ToolConfig()
    retry: RetryConfig = RetryConfig()
    llm: LLMConfig = LLMConfig()
    graph: GraphConfig = GraphConfig()
```

### Hardcoded Constants

**Sandbox Output Truncation:**
```python
MAX_CHARS_FULL = 5000  # Full output threshold
MAX_LINES_HEADTAIL = 50  # Lines to keep from head/tail
SUMMARY_THRESHOLD = 10000  # Hard truncation limit
```

**Why Hardcoded?**
- Values are well-tuned and stable
- Rarely need adjustment
- Simpler than config file
- No configuration management needed (per team decision)

---

## Best Practices

### For Developers

1. **Node Development:**
   - Use factory functions with closures
   - Keep nodes stateless (state passed explicitly)
   - Return state updates, don't mutate directly

2. **Tool Development:**
   - Follow `app__function` naming convention
   - Use async def for all tool functions
   - Include clear docstrings
   - Return structured data (dict/list)

3. **Error Handling:**
   - Catch specific exceptions
   - Provide recovery guidance
   - Log unexpected errors
   - Don't expose internal details to user

4. **Testing:**
   - Test with multiple LLM providers
   - Test error paths
   - Test multi-turn conversations
   - Test tool loading/unloading

### For Agent Users

1. **Code Execution:**
   - Use `async def` for all functions
   - Keep functions small (< 20 lines)
   - Use `smart_print()` to examine data
   - Let context persist naturally

2. **Tool Usage:**
   - Search first, load second, execute third
   - Use specific search queries
   - Check connection status
   - Handle tool errors gracefully

3. **Error Recovery:**
   - Read error messages carefully
   - Follow recovery suggestions
   - Break complex tasks into steps
   - Ask for help when stuck

---

## Performance Characteristics

### Latency

- **Simple question:** ~1-2 seconds (cache hit)
- **Code execution:** ~2-4 seconds (+ execution time)
- **Tool search:** ~0.5-1 second (vector search)
- **Multi-turn:** ~1-2 seconds (cache hit)

### Token Usage (Anthropic Claude)

- **First turn:** 8,000-10,000 tokens (cache creation)
- **Subsequent turns:** 300-500 tokens (cache hit)
- **Cache hit rate:** ~96% after warm-up

### Sandbox Limits

- **Timeout:** 600 seconds (10 minutes)
- **Output truncation:** 5,000 chars full, 10,000 chars hard limit
- **Context size:** Limited by LLM context window
- **Tool limit:** ~20-30 tools loaded at once

---

## Security Considerations

### Sandbox Isolation

**Current State:** Sandbox is NOT fully isolated

**Capabilities:**
- File system access (limited to process permissions)
- Network access (can make HTTP requests)
- Standard library imports
- Pandas with specific engines

**Risks:**
- User code can access files readable by process
- Network requests to arbitrary URLs
- Resource exhaustion (mitigated by timeout)

**Mitigations:**
- Execution timeout (600s)
- Output truncation
- Variable filtering (no sockets, threads preserved)
- Read-only approach recommended

**Future Improvements:**
- Consider docker/gvisor isolation
- Implement resource limits (CPU, memory)
- Network policy enforcement
- File system restrictions

### Tool Security

**Authentication:** Tools handle their own OAuth flows
**Authorization:** Checked by tool registry
**Secrets:** Stored in registry, not exposed to user code

---

## Troubleshooting

### Common Issues

**Issue:** Agent doesn't find my tool
**Solution:** Use broader search terms, check tool description, verify tool is registered

**Issue:** Code execution timeout
**Solution:** Break into smaller steps, check for infinite loops, use async patterns

**Issue:** Variable not found from previous turn
**Solution:** Verify same thread_id, check variable was assigned (not just printed)

**Issue:** Tool not available in execute_python_code
**Solution:** Call search_functions first, then load_functions, then execute

### Debug Techniques

1. **Check State:** Examine `execution_context` in state
2. **Check Tool IDs:** Verify `selected_tool_ids` contains expected tools
3. **Check Messages:** Review conversation history for errors
4. **Check Logs:** Look for exception traces in server logs

---

## Future Enhancements

### Planned

1. **Enhanced Sandbox Security:**
   - Docker container isolation
   - Resource limits (CPU, memory)
   - Network policy controls

2. **Improved Tool Management:**
   - Tool versioning
   - Tool dependencies
   - Tool categories/tags

3. **Performance Optimizations:**
   - Parallel tool execution
   - Incremental context updates
   - Smarter caching strategies

### Under Consideration

1. **Multi-Agent Collaboration:** CodeAct agents working together
2. **Streaming Execution:** Stream code output as it runs
3. **Visual Debugging:** Step-through debugger for code execution
4. **Custom Sandboxes:** Per-user sandbox configurations

---

## References

- **LangGraph Documentation:** https://langchain-ai.github.io/langgraph/
- **Anthropic API:** https://docs.anthropic.com/
- **AgentR Platform:** Internal documentation
- **Universal MCP:** https://github.com/universal-mcp

---

## Changelog

- **2026-01-09:** Initial architecture documentation (v1.0)
- Enhanced error handling with recovery hints
- Improved prompt engineering (output formats, search strategy, personality)

---

*This document is maintained by the AgentR team. For questions or suggestions, please open an issue in the repository.*
