Skip to content

07 Agent Core

Nikolay Vyahhi edited this page Feb 19, 2026 · 3 revisions

Agent Core

Relevant source files

The following files were used as context for generating this wiki page:

The Agent Core is ZeroClaw's central reasoning engine that orchestrates the iterative tool-calling loop between LLM providers and tool execution. It manages conversation history, memory integration, security enforcement, and response generation across all entry points (CLI, channels, gateway).

For provider-specific configurations, see Providers. For tool execution details, see Tools. For security enforcement, see Security Model.


Overview

The Agent Core implements an agentic execution model where the LLM can recursively invoke tools until it produces a final text response. This pattern enables autonomous task completion while maintaining security boundaries and conversation context.

Key Responsibilities:

  • Execute the tool-calling loop with configurable iteration limits
  • Parse tool calls from LLM responses (native API format and prompt-guided XML tags)
  • Enforce security policies before tool execution
  • Maintain conversation history with automatic compaction
  • Integrate memory context into prompts
  • Support streaming responses to channels
  • Scrub credentials from tool outputs

Sources: src/agent/loop_.rs:1-1500


Core Loop Architecture

The agent turn cycle is implemented by run_tool_call_loop(), which coordinates between the provider, tools, memory, and observer subsystems.

Tool Call Loop Flow

flowchart TB
    Start["run_tool_call_loop()"]
    CheckIter{Iteration < max_tool_iterations?}
    CallLLM["Provider::chat()<br/>(with native tool specs if supported)"]
    ParseResp["Parse response:<br/>- Native tool_calls array<br/>- XML tags <tool_call><br/>- Markdown ```tool_call<br/>- GLM-style calls"]
    HasTools{Tool calls found?}
    
    LoopTools["For each tool call"]
    FindTool["find_tool() in registry"]
    ToolExists{Tool exists?}
    SecurityCheck["SecurityPolicy::can_act()?"]
    SecurityPass{Approved?}
    ExecTool["Tool::execute()"]
    ScrubCreds["scrub_credentials()"]
    AppendResult["Append ChatMessage::tool()<br/>with result to history"]
    
    NoTools["Break loop<br/>(final text response)"]
    MaxIter["Max iterations reached<br/>(return last response)"]
    
    Start --> CheckIter
    CheckIter -->|Yes| CallLLM
    CheckIter -->|No| MaxIter
    
    CallLLM --> ParseResp
    ParseResp --> HasTools
    
    HasTools -->|Yes| LoopTools
    HasTools -->|No| NoTools
    
    LoopTools --> FindTool
    FindTool --> ToolExists
    ToolExists -->|Yes| SecurityCheck
    ToolExists -->|No| AppendResult
    
    SecurityCheck --> SecurityPass
    SecurityPass -->|Yes| ExecTool
    SecurityPass -->|No| AppendResult
    
    ExecTool --> ScrubCreds
    ScrubCreds --> AppendResult
    AppendResult --> CheckIter
Loading

Sources: src/agent/loop_.rs:851-1100


Function Reference

Primary Entry Points

Function Purpose Key Parameters
run_tool_call_loop() Main agent loop implementation provider, history, tools_registry, max_tool_iterations, on_delta
agent_turn() Simplified wrapper for single turn Calls run_tool_call_loop() internally
build_context() Fetch relevant memory entries mem, user_msg, min_relevance_score
build_hardware_context() Fetch hardware datasheet context rag, user_msg, boards, chunk_limit

Sources: src/agent/loop_.rs:820-863

Tool Call Parsing

The agent supports multiple tool call formats to maximize compatibility across providers:

flowchart LR
    Response["LLM Response Text"]
    
    TryNative["Try native tool_calls array<br/>(OpenAI/Anthropic format)"]
    TryXML["Try XML tags:<br/><tool_call>, <toolcall>,<br/><tool-call>, <invoke>"]
    TryMarkdown["Try markdown:<br/>```tool_call ... ```"]
    TryGLM["Try GLM-style:<br/>shell/command>ls"]
    
    ParsedCalls["Vec<ParsedToolCall>"]
    
    Response --> TryNative
    TryNative -->|Found| ParsedCalls
    TryNative -->|Not found| TryXML
    TryXML -->|Found| ParsedCalls
    TryXML -->|Not found| TryMarkdown
    TryMarkdown -->|Found| ParsedCalls
    TryMarkdown -->|Not found| TryGLM
    TryGLM --> ParsedCalls
Loading

Parsing Functions:

Function Format Handled
parse_structured_tool_calls() Native ToolCall[] from provider API
parse_tool_calls() XML tags, markdown blocks, JSON objects
parse_glm_style_tool_calls() tool_name/param>value format
parse_tool_calls_from_json_value() OpenAI JSON with tool_calls array

Sources: src/agent/loop_.rs:595-748, src/agent/loop_.rs:750-759, src/agent/loop_.rs:512-580


History Management

The agent maintains conversation context using a Vec<ChatMessage> that grows with each turn. Automatic compaction prevents unbounded memory usage.

History Compaction

sequenceDiagram
    participant Loop as run_tool_call_loop
    participant History as Vec<ChatMessage>
    participant Check as auto_compact_history
    participant LLM as Provider
    participant Summarizer as Summarization LLM
    
    Loop->>History: Check message count
    History-->>Loop: non_system_count > max_history
    Loop->>Check: Trigger compaction
    
    Check->>History: Extract oldest messages<br/>(keep recent 20)
    Check->>Check: build_compaction_transcript()
    Check->>Summarizer: chat_with_system()<br/>(model, temp=0.2)
    Summarizer-->>Check: Bullet-point summary
    Check->>History: Replace old messages<br/>with summary ChatMessage
    
    Note over History: System prompt preserved<br/>Recent 20 messages kept<br/>Older messages → 1 summary
Loading

Configuration:

  • DEFAULT_MAX_HISTORY_MESSAGES = 50 - Trigger compaction threshold
  • COMPACTION_KEEP_RECENT_MESSAGES = 20 - Recent messages to preserve
  • COMPACTION_MAX_SOURCE_CHARS = 12000 - Max chars sent to summarizer
  • COMPACTION_MAX_SUMMARY_CHARS = 2000 - Max chars in stored summary

Sources: src/agent/loop_.rs:80-205


System Prompt Construction

The system prompt is built by build_system_prompt() in the channels module, following the OpenClaw framework structure.

Prompt Sections

flowchart TB
    Start["build_system_prompt()"]
    
    Tooling["## Tools<br/>List + descriptions<br/>Tool Use Protocol"]
    Hardware["## Hardware Access<br/>(if GPIO tools present)"]
    Task["## Your Task<br/>ACT, don't summarize"]
    Safety["## Safety<br/>Guardrails"]
    Skills["## Available Skills<br/>On-demand skill list"]
    Workspace["## Workspace<br/>Working directory"]
    
    CheckIdentity{identity_config?}
    AIEOS["Load AIEOS identity"]
    OpenClaw["Load OpenClaw files:<br/>AGENTS.md, SOUL.md,<br/>TOOLS.md, IDENTITY.md,<br/>USER.md, MEMORY.md"]
    
    DateTime["## Current Date & Time<br/>Timezone"]
    Runtime["## Runtime<br/>Host, OS, model"]
    
    Result["Final prompt string"]
    
    Start --> Tooling
    Tooling --> Hardware
    Hardware --> Task
    Task --> Safety
    Safety --> Skills
    Skills --> Workspace
    Workspace --> CheckIdentity
    
    CheckIdentity -->|AIEOS| AIEOS
    CheckIdentity -->|OpenClaw| OpenClaw
    
    AIEOS --> DateTime
    OpenClaw --> DateTime
    DateTime --> Runtime
    Runtime --> Result
Loading

Bootstrap File Injection:

  • AGENTS.md - Sub-agent definitions
  • SOUL.md - Personality and communication style
  • TOOLS.md - Tool-specific instructions
  • IDENTITY.md - Agent identity and context
  • USER.md - User preferences and details
  • BOOTSTRAP.md - First-run ritual (optional)
  • MEMORY.md - Curated long-term memory

Files are injected with a per-file size limit (BOOTSTRAP_MAX_CHARS = 20000).

Sources: src/channels/mod.rs:888-1051


Tool Execution Flow

sequenceDiagram
    participant Loop as run_tool_call_loop
    participant Parse as parse_tool_calls
    participant Find as find_tool
    participant Security as SecurityPolicy
    participant Tool as Tool trait
    participant Runtime as RuntimeAdapter
    participant Scrub as scrub_credentials
    
    Loop->>Parse: Parse LLM response
    Parse-->>Loop: Vec<ParsedToolCall>
    
    loop For each tool call
        Loop->>Find: find_tool(name)
        Find-->>Loop: Option<&dyn Tool>
        
        alt Tool not found
            Loop->>Loop: Append error result
        else Tool found
            Loop->>Security: can_act(tool_name)?
            Security-->>Loop: Allowed/Denied
            
            alt Denied
                Loop->>Loop: Append denial message
            else Allowed
                Loop->>Tool: execute(args, runtime)
                Tool->>Runtime: Execute in sandbox
                Runtime-->>Tool: Output
                Tool-->>Loop: ToolResult
                Loop->>Scrub: scrub_credentials(output)
                Scrub-->>Loop: Sanitized output
                Loop->>Loop: Append ChatMessage::tool
            end
        end
    end
    
    Loop->>Loop: Continue to next iteration
Loading

Sources: src/agent/loop_.rs:995-1097


Credential Scrubbing

To prevent accidental credential exfiltration, the agent scrubs sensitive patterns from tool outputs before sending them to the LLM.

Patterns Detected:

  • token, api_key, password, secret, user_key, bearer, credential
  • Key-value formats: key: value, key=value, "key": "value"

Example:

Input:  API_KEY=sk-proj-abc123xyz789def
Output: API_KEY=sk-p*[REDACTED]

Sources: src/agent/loop_.rs:25-77


Configuration

Agent behavior is controlled via [agent] section in config.toml:

[agent]
max_tool_iterations = 10      # Max recursive tool calls per turn
max_history_messages = 50     # Trigger compaction threshold
compact_context = false       # Use 6000-char bootstrap for small models
parallel_tools = false        # Execute tools in parallel (experimental)
tool_dispatcher = "auto"      # "auto" | "sequential" | "parallel"

Sources: src/config/schema.rs:243-280


Memory Integration

The agent enriches prompts with relevant memory entries before each LLM call.

flowchart LR
    UserMsg["User Message"]
    Recall["Memory::recall()<br/>(hybrid search)"]
    Filter["Filter by min_relevance_score"]
    Context["[Memory context]<br/>- key: content<br/>- key: content"]
    Enriched["Enriched message<br/>(context + user msg)"]
    
    UserMsg --> Recall
    Recall --> Filter
    Filter --> Context
    Context --> Enriched
    Enriched --> LLM["Provider::chat()"]
Loading

Configuration:

  • min_relevance_score (default: 0.4) - Discard entries below this threshold
  • Recall limit: 5 entries per query
  • Auto-save: Optionally store user messages to memory

Sources: src/agent/loop_.rs:210-233, src/channels/mod.rs:443-469


Streaming Support

Channels that support draft updates (e.g., Telegram, Discord) receive progressive response streaming.

Streaming Flow

sequenceDiagram
    participant Channel as process_channel_message
    participant TxRx as tokio::mpsc channel
    participant Loop as run_tool_call_loop
    participant Provider as Provider::chat
    participant Updater as Draft updater task
    participant API as Channel API
    
    Channel->>Channel: Create mpsc channel(64)
    Channel->>API: send_draft("...")
    API-->>Channel: draft_message_id
    
    Channel->>Updater: Spawn draft updater task
    
    Channel->>Loop: run_tool_call_loop(on_delta=Some(tx))
    Loop->>Provider: chat()
    
    loop Streaming
        Provider-->>Loop: Text delta chunk
        Loop->>TxRx: Send chunk (if >= 80 chars)
        TxRx-->>Updater: Receive chunk
        Updater->>API: update_draft(id, accumulated)
    end
    
    Provider-->>Loop: Final response
    Loop-->>Channel: Complete
    
    Channel->>Updater: Close channel
    Updater->>API: finalize_draft(id, final_text)
Loading

Constants:

  • STREAM_CHUNK_MIN_CHARS = 80 - Minimum characters before sending chunk

Sources: src/channels/mod.rs:636-720


Observability

The agent emits events to the configured observer backend (Prometheus, logs) for monitoring and debugging.

Events Emitted:

Event When
AgentStart Agent turn begins
LlmRequest Before calling provider
LlmResponse After provider response (success/error)
ToolExecution Tool called
AgentEnd Agent turn completes
Error Any error occurs

Sources: src/agent/loop_.rs:876-881, src/agent/loop_.rs:905-911


Channel Integration

The agent core is invoked from channels via process_channel_message(), which handles:

  • Runtime model switching (/models, /model commands)
  • Memory context enrichment
  • Typing indicators
  • Draft message updates
  • Response delivery

Channel Message Flow

flowchart TB
    Receive["Channel::listen()<br/>receives ChannelMessage"]
    Dispatch["Message dispatcher<br/>(semaphore-controlled)"]
    Process["process_channel_message()"]
    
    RuntimeCmd{Runtime command?}
    HandleCmd["handle_runtime_command_if_needed()<br/>(/models, /model)"]
    
    GetRoute["get_route_selection()<br/>(per-sender routing)"]
    GetProvider["get_or_create_provider()"]
    BuildContext["build_memory_context()"]
    Autosave["Auto-save to memory"]
    
    CallAgent["run_tool_call_loop()"]
    SaveHistory["Save to conversation_histories"]
    Reply["Channel::send() or<br/>finalize_draft()"]
    
    Receive --> Dispatch
    Dispatch --> Process
    Process --> RuntimeCmd
    
    RuntimeCmd -->|Yes| HandleCmd
    RuntimeCmd -->|No| GetRoute
    
    HandleCmd --> Reply
    GetRoute --> GetProvider
    GetProvider --> BuildContext
    BuildContext --> Autosave
    Autosave --> CallAgent
    CallAgent --> SaveHistory
    SaveHistory --> Reply
Loading

Per-Sender State:

  • conversation_histories - Last 50 messages per {channel}_{sender}
  • route_overrides - Active provider/model per sender
  • provider_cache - Cached provider instances

Sources: src/channels/mod.rs:556-814


Runtime Model Switching

Telegram and Discord channels support dynamic provider/model switching per sender without restarting the daemon.

Commands:

  • /models - Show available providers
  • /models <provider> - Switch to provider
  • /model - Show current model and cached models
  • /model <model-id> - Switch to model

Implementation:

  • parse_runtime_command() - Parse command syntax
  • resolve_provider_alias() - Normalize provider names
  • get_route_selection() - Retrieve sender's active route
  • set_route_selection() - Update sender's route
  • clear_sender_history() - Reset history on provider switch

Sources: src/channels/mod.rs:146-234, src/channels/mod.rs:365-441


Security Integration

The agent enforces security policies before executing any tool:

flowchart LR
    ToolCall["Tool call parsed"]
    Security["SecurityPolicy::can_act()"]
    
    CheckAutonomy["Check autonomy level:<br/>ReadOnly, Supervised, Full"]
    CheckRateLimit["Check rate limits"]
    CheckCommands["Check allowed_commands"]
    CheckPaths["Check workspace_only,<br/>forbidden dirs, symlinks"]
    
    Approved{Approved?}
    Execute["Tool::execute()"]
    Deny["Append denial message"]
    
    ToolCall --> Security
    Security --> CheckAutonomy
    CheckAutonomy --> CheckRateLimit
    CheckRateLimit --> CheckCommands
    CheckCommands --> CheckPaths
    CheckPaths --> Approved
    
    Approved -->|Yes| Execute
    Approved -->|No| Deny
Loading

See Security Model for detailed enforcement rules.

Sources: src/agent/loop_.rs:995-1045


Error Handling

The agent implements defensive error handling to prevent cascading failures:

Error Type Handling Strategy
Provider timeout Return error message to user, don't crash loop
Tool not found Append error result, continue iteration
Tool execution failure Scrub error, append to history, continue
Memory recall failure Log warning, continue without context
Compaction failure Fall back to deterministic truncation

Example Error Message:

⚠️ Failed to initialize provider `openai-codex`. Please run `/models` to choose another provider.
Details: Authentication failed (invalid API key)

Sources: src/channels/mod.rs:769-813


Code Entity Map

Concept Code Entity Location
Main loop run_tool_call_loop() src/agent/loop_.rs:851-1100
Single turn agent_turn() src/agent/loop_.rs:820-846
Tool parsing parse_tool_calls() src/agent/loop_.rs:595-748
History compaction auto_compact_history() src/agent/loop_.rs:158-205
Memory context build_context() src/agent/loop_.rs:210-233
Hardware RAG build_hardware_context() src/agent/loop_.rs:237-273
System prompt build_system_prompt() src/channels/mod.rs:888-1051
Message processing process_channel_message() src/channels/mod.rs:556-814
Tool execution Tool::execute() trait src/tools/traits.rs
Credential scrubbing scrub_credentials() src/agent/loop_.rs:45-77
Config struct AgentConfig src/config/schema.rs:243-280

Performance Characteristics

Iteration Limits:

  • Default: 10 tool calls per turn
  • Fallback: DEFAULT_MAX_TOOL_ITERATIONS = 10 when config is 0
  • Rationale: Prevent runaway loops from misbehaving models

History Compaction:

  • Trigger: 50+ messages (configurable)
  • Keep: 20 most recent messages
  • Cost: 1 LLM call to summarizer (temp=0.2)

Streaming:

  • Chunk size: 80 characters minimum
  • Channel buffer: 64 chunks
  • Refresh interval: Every chunk received

Concurrency:

  • Message workers: 4 per channel (semaphore-controlled)
  • Max in-flight: 8-64 messages (scales with channel count)
  • Timeout: 300s per message (supports slow local LLMs)

Sources: src/channels/mod.rs:61-69, src/agent/loop_.rs:19-23


Clone this wiki locally