Skip to content

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 (via ProviderFactoryRegistry), memory (via clawseed_memory::create_memory()), and tools (via clawseed_tools::registry::all_tools()), then selects a dispatcher based on provider.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) in crates/clawseed-agent/src/tools.rs are stubs — they return empty collections or errors. The ToolSource::Mcp enum variant and McpConfig schema exist, but there is no actual MCP protocol client. Do not treat MCP as a usable capability.

Note on provider field: The provider field is Arc<dyn Provider>, not Box. This enables gateway connections to share a single provider instance (with its HTTP connection pools) across all WebSocket/webhook sessions. AgentBuilder.provider() accepts Box<dyn Provider> and wraps it as Arc; shared_provider() accepts Arc<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:

  1. Prepare the user message and system prompt
  2. Send conversation history and tool specs to the provider
  3. Parse text and tool calls through the selected dispatcher
  4. Execute tools and append their results to history
  5. 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_calls array
  • 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

pub struct HookRunner {
    hooks: Vec<Box<dyn Hook>>,
}

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 DashMap for lock-free concurrent access, safe in async contexts
  • ToolSpec caching 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 shared Arc<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 ToolRegistry instances. The gateway-level AppState.tool_registry serves /api/tools endpoint visibility; each Agent's tool_registry serves 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 — SecurityPolicy struct
  • 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 Hook trait: 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 — PairingGuard for device pairing verification with constant-time comparison
  • secrets.rs — SecretStore credential management, WebAuthnManager support

cron/ — Cron Scheduling

  • scheduler.rs — Job execution engine, tracks jobs and run history
  • store.rs — Persistent storage
  • add_agent_job() / add_shell_job() — Create cron jobs
  • update_job() / remove_job() — Modify/delete
  • list_jobs() / due_jobs() — Query
  • record_run() / list_runs() — Run history
  • sync_declarative_jobs() — Sync from config files
  • types.rs — CronJob, Schedule, SessionTarget, DeliveryConfig
  • mod.rs — Security integration
  • validate_shell_command() — Security policy check
  • add_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 JSON
  • aieos_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.