clawseed-agent — Agent Core Runtime¶
Overview¶
clawseed-agent is the core agent crate, responsible for tool dispatch, hook execution, security policy, cron scheduling, and more. It is the hub connecting Provider, Tool, Memory, and Hook.
Note: Beyond orchestration, this crate also owns runtime assembly.
Agent::from_config_with_registry()directly instantiates provider (viaProviderFactoryRegistry), memory (viaclawseed_memory::create_memory()), and tools (viaclawseed_tools::registry::all_tools()), then selects a dispatcher based onprovider.supports_native_tools(). Tools depend on memory being constructed first; dispatcher depends on provider capabilities.
Core Structures¶
Agent — Agent Registry¶
pub struct Agent {
provider: Arc<dyn Provider>,
tool_registry: Arc<dyn ToolRegistry>,
memory: Arc<dyn Memory>,
observer: Arc<dyn Observer>,
tool_dispatcher: Box<dyn ToolDispatcher>,
capabilities: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
workspace_dir: PathBuf,
// ...
}
Agent is a registry that manages all tool sources (built-in, MCP, remote) through the ToolRegistry trait, and manages the Hook pipeline through HookRunner. Core code has no knowledge of specific tool implementations — extensions simply add entries to the registry.
Note on MCP: All MCP types (
McpRegistry,DeferredMcpToolSet,McpToolWrapper,ToolSearchTool) incrates/clawseed-agent/src/tools.rsare stubs — they return empty collections or errors. TheToolSource::Mcpenum variant andMcpConfigschema exist, but there is no actual MCP protocol client. Do not treat MCP as a usable capability.Note on provider field: The
providerfield isArc<dyn Provider>, notBox. This enables gateway connections to share a single provider instance (with its HTTP connection pools) across all WebSocket/webhook sessions.AgentBuilder.provider()acceptsBox<dyn Provider>and wraps it asArc;shared_provider()acceptsArc<dyn Provider>directly for gateway use.
AgentBuilder — Builder Pattern¶
let agent = Agent::builder()
.provider(provider) // Box<dyn Provider> → wrapped as Arc
.shared_provider(arc_provider) // Arc<dyn Provider> directly (gateway use)
.tools(tools) // Option 1: pass tool list, auto-builds DefaultToolRegistry
.tool_registry(registry) // Option 2: pass pre-built ToolRegistry (takes priority)
.memory(memory)
.observer(observer)
.tool_dispatcher(dispatcher)
.workspace_dir(path)
.allowed_tools(Some(vec!["file_*".into()])) // Glob pattern tool allowlist
.denied_tools(Some(vec!["shell".into()])) // Glob pattern tool denylist
.mcp_tool_filters(Some(filters)) // Per-MCP-server filtering
.hook_runner(Some(Arc::new(hook_runner))) // Hook pipeline
.build()?;
// Build from config (optionally with a custom ProviderFactoryRegistry) — CLI/embedded use
let agent = Agent::from_config(&config).await?;
let agent = Agent::from_config_with_registry(&config, Some(provider_factory_registry)).await?;
// Build with shared components — gateway use (reuses AppState provider/memory/observer/builtin-tools)
let agent = Agent::from_config_with_shared_components(
&config, state.provider, state.mem, state.observer, state.model, state.temperature,
Some(state.shared_builtin_tools)
).await?;
Module Architecture¶
agent.rs — Types and Runtime Assembly¶
Defines Agent, AgentBuilder, TurnEvent, and the configuration-based constructors. Runtime behavior is split into focused child modules while the public clawseed_agent::agent::Agent path remains unchanged.
agent/state.rs — Conversation State and Prompt Context¶
Owns conversation history, memory and user-profile refresh, skill activation, prompt rebuilding, remote tool registration, and history trimming. When AgentConfig.context_compaction_enabled is set, it also performs token-thresholded chunk summarization while retaining a stable summary prefix and recent raw tail.
agent/tool_execution.rs — Hook and Tool Execution¶
- Resolves before-hooks before dispatch
- Looks up tools via
tool_registry.get_tool(name) - Executes independent tool calls in parallel
- Records observer events and fires after-hooks
agent/turn.rs — Agent Turn Orchestration¶
Implements standard and streaming turns:
- Prepare the user message and system prompt
- Send conversation history and tool specs to the provider
- Parse text and tool calls through the selected dispatcher
- Execute tools and append their results to history
- Repeat until the provider returns a final response
agent_loop.rs and tool_loop.rs are compatibility shims for older gateway integrations; they do not contain the active turn implementation.
dispatcher.rs — Tool Dispatcher¶
Two implementations:
| Dispatcher | Use Case | How It Works |
|---|---|---|
NativeToolDispatcher |
Providers with native tool calling | Extracts tool_calls directly from response |
XmlToolDispatcher |
Providers without native tool calling | Tries ◁▷ format first, falls back to multi-format parser |
parser.rs — Tool Call Parser¶
Multi-format tool call parsing supporting 12+ LLM output formats:
- OpenAI native JSON
tool_callsarray - XML tags:
◁▷,<toolcall>,<tool-call>,<invoke> - MiniMax
<invoke>format - Markdown code blocks (
```tool_call) - Anthropic
<FunctionCall>tags - GLM shortened format
- Perl/hash-ref style
- xAI grok
```tool <name>format
XmlToolDispatcher::parse_response() tries ◁▷ format first (deterministic prompt-guided parsing), then falls back to parser::parse_tool_calls() with the original response text for multi-format parsing.
Security design: Raw JSON without explicit wrappers is never extracted, preventing prompt injection attacks.
context.rs — Tool Context¶
pub struct AgentToolContext {
workspace_dir: PathBuf,
capabilities: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
}
impl ToolContext for AgentToolContext {
fn workspace_dir(&self) -> &Path { &self.workspace_dir }
fn get<T: 'static>(&self) -> Option<&T> {
self.capabilities.get(&TypeId::of::<T>())
.and_then(|arc| arc.downcast_ref::<T>())
}
}
hooks.rs — Hook Runner¶
Execution flow:
1. run_before_tool_call() — Iterates hooks sequentially
- First hook to return Cancel stops the pipeline
- Modify passes the modified call to the next hook
2. fire_after_tool_call() — Notifies all hooks, observation only, no modification
Declarative hook chain: Hooks can be declared in the [hooks] config section. HookFactoryRegistry creates Hook instances by hook_type. In from_config(), SecurityPolicy is always auto-registered as the first hook in the pipeline.
pub trait HookFactory: Send + Sync {
fn hook_type(&self) -> &str;
fn create(&self, config: &serde_json::Value) -> Option<Box<dyn Hook>>;
}
tool_registry.rs — Tool Registry Implementation¶
DefaultToolRegistry is the default implementation of the ToolRegistry trait:
- Uses
DashMapfor lock-free concurrent access, safe in async contexts ToolSpeccaching with write-time invalidation to avoid recomputation- Three-layer glob pattern filtering: denied takes precedence → allowed allowlist → per-MCP-server filtering
register_all()for bulk registration,register_all_arc()for bulk registration with sharedArc<dyn Tool>instances (avoids re-construction in gateway),unregister_by_source()for bulk removal by source
Dual Registry Note: At runtime there are two independent
ToolRegistryinstances. The gateway-levelAppState.tool_registryserves/api/toolsendpoint visibility; each Agent'stool_registryserves actual tool dispatch. Remote tools must be registered in both. See the "Dual Tool Registry" section in Architecture Overview for details.
security/ — Security Policy¶
mod.rs—SecurityPolicystruct- Autonomy levels (ReadOnly / Supervised / Full)
- Command allowlists
- Medium-risk command list (touch, rm, cp, mv, mkdir, chmod, chown, kill)
- Path restrictions (
/etc/passwd,/etc/shadow,/etc/ssh,/root/.ssh) - Action rate limiting (
max_actions_per_hour) - Implements
Hooktrait:before_tool_call()checks autonomy level, rate limits, command allowlists, and path guards;after_tool_call()records action count. SecurityPolicy is always registered as the first hook in the pipeline, and is no longer injected as a Capability pairing.rs—PairingGuardfor device pairing verification with constant-time comparisonsecrets.rs—SecretStorecredential management,WebAuthnManagersupport
cron/ — Cron Scheduling¶
scheduler.rs— Job execution engine, tracks jobs and run historystore.rs— Persistent storageadd_agent_job()/add_shell_job()— Create cron jobsupdate_job()/remove_job()— Modify/deletelist_jobs()/due_jobs()— Queryrecord_run()/list_runs()— Run historysync_declarative_jobs()— Sync from config filestypes.rs—CronJob,Schedule,SessionTarget,DeliveryConfigmod.rs— Security integrationvalidate_shell_command()— Security policy checkadd_shell_job_with_approval()— Approval before persistence
prompt.rs — Modular System Prompt Builder¶
The system prompt is assembled from pluggable PromptSection implementations via SystemPromptBuilder:
SystemPromptBuilder::with_defaults()
├── IdentitySection — AIEOS identity + personality markdown files
├── PlatformSection — Platform and environment info
├── WorkspaceSection — Working directory path
├── StableMemorySection — Core memories injected into system prompt
├── ToolsSection — Available tool descriptions
├── MemorySection — Memory system instructions
├── SafetySection — Safety rules (autonomy-level-aware)
├── ToolHonestySection — Tool honesty constraints
├── SkillsIndexSection — Available skill definitions
└── ActiveSkillsSection — Currently active skills
Note: DateTimeSection was removed from the system prompt. Current time is now provided via the [YYYY-MM-DD HH:MM:SS TZ] prefix on each user message, keeping the system prompt 100% stable across turns for prefix caching optimization (see Prompt Cache Optimization).
Custom sections can be added via SystemPromptBuilder::add_section().
personality.rs — Personality File Loader¶
Loads well-known markdown files from the workspace directory:
| File | Purpose |
|---|---|
SOUL.md |
Core personality and behavioral guidelines |
IDENTITY.md |
Name, role, background |
USER.md |
User preferences and context |
AGENTS.md |
Multi-agent coordination rules |
TOOLS.md |
Tool usage guidelines |
HEARTBEAT.md |
Periodic self-check instructions |
BOOTSTRAP.md |
First-run initialization instructions |
MEMORY.md |
Memory management guidelines |
Files are truncated at 20K characters. A default SOUL.md is auto-generated on first run.
identity.rs — AIEOS Identity System¶
Supports AIEOS v1.1 (AI Entity Object Specification) — a structured JSON format for portable AI identity. Covers identity, psychology, linguistics, motivations, capabilities, physicality, history, and interests.
load_aieos_identity()— loads from file or inline JSONaieos_to_system_prompt()— renders AIEOS identity to markdown- Handles both official generator shape and simplified JSON formats via normalization
See Personality & Identity Tutorial for full documentation.
Other Modules¶
| Module | Responsibility |
|---|---|
cost.rs |
Token-based cost tracking |
observer.rs |
Event emission (NoopObserver by default, defined locally in clawseed-agent) |
observability.rs |
Re-exports Observer types for external consumers |
approval.rs |
Approval workflow for risky operations |
history.rs |
Conversation history management |
parser.rs |
Multi-format tool call parsing (12+ LLM output formats) |
health.rs |
Health check stubs |
Post-Turn Learning and Context Assembly¶
Both turn and turn_streamed create one CompletedTurn only after a final successful response. Failed, cancelled, and intermediate truncated responses do not learn. KnowledgeCoordinator accepts completed turns through a bounded non-blocking queue and can drain accepted work during shutdown. Gateway transports do not perform a second memory write.
KnowledgeRouter deterministically assigns user text to profile, memory, or drop. Profile inference receives only user text, remains disabled by default, filters sensitive categories, and cannot replace explicit, imported, or rejected values. The profile key registry canonicalizes built-in keys and migrates aliases while retaining recovery snapshots.
ContextAssembler treats active, unexpired profile values as authoritative for user attributes. It selects them by category priority, provenance, confidence, recency, and per-category quota. Stable and dynamic Core memory exclude matching profile keys or exact values; dynamic recall also excludes entries already present in the stable Core section before applying the final limit.