-
Notifications
You must be signed in to change notification settings - Fork 4.4k
07 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.
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
The agent turn cycle is implemented by run_tool_call_loop(), which coordinates between the provider, tools, memory, and observer subsystems.
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
Sources: src/agent/loop_.rs:851-1100
| 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
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
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
The agent maintains conversation context using a Vec<ChatMessage> that grows with each turn. Automatic compaction prevents unbounded memory usage.
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
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
The system prompt is built by build_system_prompt() in the channels module, following the OpenClaw framework structure.
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
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
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
Sources: src/agent/loop_.rs:995-1097
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
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
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()"]
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
Channels that support draft updates (e.g., Telegram, Discord) receive progressive response streaming.
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)
Constants:
-
STREAM_CHUNK_MIN_CHARS = 80- Minimum characters before sending chunk
Sources: src/channels/mod.rs:636-720
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
The agent core is invoked from channels via process_channel_message(), which handles:
- Runtime model switching (
/models,/modelcommands) - Memory context enrichment
- Typing indicators
- Draft message updates
- Response delivery
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
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
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
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
See Security Model for detailed enforcement rules.
Sources: src/agent/loop_.rs:995-1045
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
| 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 |
Iteration Limits:
- Default: 10 tool calls per turn
- Fallback:
DEFAULT_MAX_TOOL_ITERATIONS = 10when 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