anda_engine 0.16.2

Agents engine for Anda -- an AI agent framework built with Rust, powered by ICP and TEEs.
Documentation

anda_engine

License Crates.io Test Docs.rs Latest Version

Runtime engine for Anda, a Rust framework for building autonomous AI agents powered by ICP identities and Trusted Execution Environments (TEEs).

anda_engine implements the runtime behind the traits and data contracts in anda_core. It wires together agent execution, tool dispatch, model providers, persistent storage, hooks, remote engines, and built-in extensions.

Full API documentation is available on docs.rs.

What It Provides

anda_engine is designed as the embeddable runtime layer for applications that host Anda agents.

  • Agent and tool registration with scoped execution contexts.
  • Direct agent runs and direct tool calls with cancellation support.
  • Label-based model routing with a primary model.
  • Built-in model adapters for OpenAI-compatible APIs, Anthropic, and Gemini.
  • Object storage backed by the object_store ecosystem.
  • Persistent memory tools built on AndaDB, Cognitive Nexus, and KIP.
  • Remote engine discovery and cross-engine tool or agent calls.
  • Hook APIs for observing and transforming agent and tool execution.
  • Workspace tools for filesystem access, shell execution, web fetch, notes, skills, todos, and search.
  • MCP client support with versioned catalogs, stable routes, input/output budgets, configurable concurrency/deadlines, OAuth refresh coordination, and opt-in elicitation/resources.
  • Web3 and TEE challenge signing through the Anda Web3 stack.

Installation

cargo add anda_engine

The crate has no default optional features.

[dependencies]
anda_engine = "0.16"

Quick Start

The example below builds an explicitly public demo engine with the built-in EchoEngineInfo agent. Engines remain private by default. The same example is runnable with cargo run -p anda_engine --example quick_start. Real applications usually register their own anda_core::Agent and anda_core::Tool implementations.

use anda_core::AgentInput;
use anda_engine::{
    ANONYMOUS,
    engine::{AgentInfo, EchoEngineInfo, Engine},
    management::{BaseManagement, Visibility},
};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let echo_info = AgentInfo {
        handle: "echo".to_string(),
        name: "Echo Agent".to_string(),
        description: "Returns engine metadata as JSON.".to_string(),
        ..Default::default()
    };

    // This demo explicitly permits anonymous callers; engines default to private.
    let engine = Engine::builder()
        .with_management(Arc::new(BaseManagement {
            controller: ANONYMOUS,
            managers: Default::default(),
            visibility: Visibility::Public,
        }))
        .register_agent(Arc::new(EchoEngineInfo::new(echo_info)), None)?
        .build("echo".to_string())
        .await?;

    let output = engine
        .agent_run(
            ANONYMOUS,
            AgentInput::new("echo".to_string(), "hello".to_string()),
        )
        .await?;

    println!("{}", output.content);
    Ok(())
}

Core Concepts

Engine

Engine is the top-level runtime. It owns registered agents, tools, models, hooks, storage, management policy, Web3 or TEE identity, and remote engine metadata.

Use EngineBuilder to configure an engine, then call build(default_agent) to initialize tools and agents. The selected default agent is automatically exported.

Contexts

Agents receive AgentCtx; tools receive BaseCtx. Contexts carry caller identity, request metadata, cancellation tokens, scoped cache and storage, shared state, HTTP and canister features, Web3 signing, and remote engine access.

Context namespaces are derived from agent and tool names so cache and object storage remain isolated between components.

Subagent sessions are isolated by caller, host-created SubAgentScope, worker, and alias. Install the same scope clone to continue a root task across engine entry contexts. Use get_session_in_scope and session_details_in_scope for model-facing access; legacy caller-only lookups are host administration helpers. Turn-completion hooks and bounded event waits are independent of session closure. Typed messages distinguish queued notifications from tasks that wake idle workers. Shared limits govern residency, concurrent inference, queues and optional budgets. Idle checkpoints and explicit provider-neutral handoffs are opt-in host capabilities. See subagent lifecycle and migration and the runnable subagent_sessions example.

CompletionStream preserves follow-up and steering input submitted while a model step is pending. Failed context compaction leaves the original runner usable. Stop/cancel controls interrupt pending subagent work; native shell cancellation cleans up the process tree and output readers.

Agent dependencies supplied by dynamic providers are verified after provider initialization during engine build. Exact SA_, RA_, and RT_ callable names can be used when requesting definitions.

Models

Models is a thread-safe model registry. You can register concrete Model values under labels such as primary, pro, flash, or lite. Agents can route requests by label while applications remain free to change provider-specific model names.

Built-in provider adapters include:

  • openai: Responses API for model names starting with gpt, otherwise Chat Completions.
  • openai-response: Explicit Responses API selection for any model name.
  • anthropic: Anthropic Messages API completion.
  • gemini: Google Gemini completion.

Custom providers can implement CompletionFeaturesDyn and be wrapped with Model::with_completer.

Completion adapter behavior:

  • Chat Completions sends max_completion_tokens for an explicit output budget. A default request template containing only max_tokens opts into the legacy field for compatible endpoints; DeepSeek model names also use that field.
  • Responses always uses streaming transport and store: false, then returns the aggregated AgentOutput. Its with_stream setting is retained for source compatibility but has no effect. Non-empty stop sequences return an error.
  • Incomplete streams return retryable errors. Chat accepts an explicit finish reason or [DONE] (for compatible providers omitting the reason); Gemini requires a finish reason or prompt blocking; Anthropic requires both a stop reason and message_stop.
  • Anthropic maps minimal/low effort to low, and medium/high/max to their corresponding API values. Gemini 2.5 maps minimal/low/medium/high/max to budgets of 0/1024/4096/16384/24576 tokens; Pro uses 128 for minimal and 32768 for max. Gemini 3 uses thinking levels, mapping minimal to low on Pro and medium to high on the original Gemini 3 Pro. Use a default request template for other provider-specific settings. ThinkingConfig::thinking_budget is signed so -1 can request dynamic thinking.
  • Anthropic forwards FunctionDefinition::strict and closes objects in strict tool and output schemas while preserving optional properties. A strict tool whose schema uses unsupported constraints (including numeric bounds and string lengths, such as minimum: 0 on unsigned integers) is sent as a regular tool with its schema unchanged. In an output schema, such constraints return a local error rather than being discarded.
  • Chat audio inputs must contain inline WAV/MP3 data. Remote audio/file URLs return errors; use Responses for remote file inputs. Video is sent as a video_url part for compatible providers; OpenAI itself rejects it. Anthropic inline PDFs use Base64 regardless of whether their bytes are UTF-8.
  • OpenAI and Anthropic pair missing tool-call IDs when replaying neutral history, including repeated calls to the same function. Existing IDs, provider-native raw_history, and results whose calls exist only in raw_history remain intact.

Tools and Extensions

The extension module provides reusable tools for common agent capabilities:

  • fetch: signed HTTP fetching and resource loading.
  • fs: workspace-scoped file read, write, search, and edit tools.
  • shell: native or sandboxed shell command execution.
  • mcp: MCP servers as runtime-discovered tool providers.
  • note: per-agent persistent notes with bounded ID reads, listings, and substring search.
  • skill: file-backed skills with bounded discovery, immutable catalog generations, stable identities, package resource reads, and optional delegated execution.
  • todo: validated session tasks with incremental updates and bounded recovery after handoff.

Notes have an opt-in context index; task changes use existing typed hooks with an optional explanation. See notes and tasks for pagination, limits, context lifecycle, and Rust API migration.

Filesystem tools enforce configured workspace roots. A native shell's working directory alone does not confine the process: use an isolated host or opt into NativeRuntime::with_sandbox. Shell commands receive a restricted environment; only allowlisted host variables and explicitly configured keys are forwarded. Configured environment keys are normalized case-insensitively on Windows.

Use extension::workspace::coding_tools for a coding agent: it registers shell, shell_session, and apply_patch, with no redundant read/search tools. readonly_file_tools registers only read_file and search_file; file_tools preserves the four filesystem tools for hosts without a shell. Existing tools remain available individually.

The coding bundle uses ShellCommandTool in place of the legacy ShellTool (both have the name shell; do not register both). Its CommandArgs supports a scoped cwd, a wait duration separate from the process deadline, and a combined stdout/stderr preview budget. Optional stdin and Unix PTYs require host opt-in through SessionLimits. Shell selection is host-controlled through NativeShell; login profiles are not enabled. Legacy ExecArgs, ExecOutput, custom Executor implementations, and ShellTool remain source-compatible. The new session methods on Executor default to unsupported.

Before dispatching session tools, the host must install a ShellSessionScope::new() in parent context state. Reuse that capability only for the same conversation on subsequent turns. Sessions are also checked against the engine, verified caller, and agent; request metadata is not an authorization source. A session retains its launch runtime policy; changing runtime sandbox, shell, environment inheritance, log directory or session limits invalidates and cancels old sessions. No approval UI or persistent permission grants are implemented by the tool. Argument hooks are typed on the session tools' own types (DynToolHook<CommandArgs, CommandOutput> and DynToolHook<SessionArgs, SessionOutput>), so an approval gate installed as ShellToolHook does not intercept them; ShellToolHook and DynToolJsonHook only receive background events.

Session execution defaults to a 10-second foreground wait (ending early only when the process exits), a 10-minute total runtime limit, a 32KiB combined output preview (minimum request 128 bytes), 1MiB capture per stream, and a 32MiB combined log quota. Host limits cap caller requests. Output preserves the beginning and tail; omitted_bytes distinguishes a non-complete preview, while log_complete separately describes raw-log completeness. The log keeps raw chunks in arrival order and labels each switch between stdout and stderr. shell_session provides poll, write, close_stdin, stop, list, and bounded log reads by byte offset. PTYs combine both streams, and close_stdin is only supported for pipes. A write timeout or cancellation can leave a partially sent input; callers must not blindly retry it.

The default registry retains at most 64 sessions and refuses new launches when all slots are active. Completed sessions expire after five minutes or are evicted to admit a new command; expiration cleanup is lazy on the next session operation. Logs are deleted when their session is released. Dropping the runtime cancels retained sessions. Request cancellation also propagates to its processes. On Unix, when the launched shell exits, the session terminates descendants still in its process group. Use background: true to keep a supervised command running instead of appending &. Legacy shell raw-output files retain their previous caller-managed cleanup behavior, and legacy background commands retain their existing lifecycle; use the session bundle when total runtime limits and polling are required.

apply_patch accepts JSON containing a patch string, optional dry_run, and optional expected_versions (path and original SHA-256, or missing). It supports *** Add File, *** Delete File, *** Update File, *** Move to, @@ chunks, and *** End of File. Exact context must identify one location; ambiguous matches and overwriting add/move targets are rejected. The patch is bounded to 1MiB input, 32 source files, 256 chunks per file, 200,000 lines per updated file, a 64MiB prepared byte budget, and a 32KiB diff preview. Existing encoding and LF/CRLF line endings are preserved. All files are prevalidated and locked before the first write; writes are atomic per file, not transactional across files. Commit failures return is_error, per-operation applied flags, and an explanation; a failed move can leave a written destination. SHA checks detect stale edits but are not an OS compare-and-swap against external writers.

The shared filesystem layer serializes cooperating writes across tool instances, enforces a 10MiB write/read/edit limit, and reads through validated file handles with a bound on actual bytes. Unix traversal and replacement use directory descriptors and no-follow operations. Windows checks file handles for reparse points and hardlink counts and pins parent directories during access. Locks do not serialize arbitrary shell scripts or external editors. Metadata workspace hints only prioritize permitted roots; they do not revoke access to the other configured roots.

SandboxPolicy::workspace(root) is an optional, host-owned process policy with explicit read/write roots and network denied by default. macOS uses /usr/bin/sandbox-exec; Linux requires /usr/bin/bwrap and user-namespace support. Common OS runtime paths are readable. Linux provides a private /tmp; on macOS, explicitly grant a dedicated scratch directory and configure TMPDIR when needed. Paths outside grants are unavailable, so build caches and SDKs may need explicit host grants. Missing or failing backends never trigger an unrestricted retry. Windows currently requires a custom isolated Executor; the built-in sandbox constructor fails there. Sandbox enforcement does not add application approval workflows or destination-level network filtering.

A compilable registration and polling example is available in examples/workspace_tools.rs:

cargo run -p anda_engine --example workspace_tools

Skill catalogs and resources

SkillManager keeps skills_manager({"name":"my-skill"}) for complete, small SKILL.md reads. Register manager.tools() to include skills_list and skills_read as a normal skills capability group. See examples/skills.rs for a runnable registration example. The manager does not grant filesystem writes, shell execution, or install dependencies. To expose skills declaring execution: subagent, additionally insert the same Arc<SkillManager> into engine.sub_agents_manager(); inline remains the default.

  • skills_list({"query":null,"cursor":null}) returns at most 20 compact identities per page. Pass next_cursor back until it is null. Queries search names and descriptions; an exact name or ID also finds explicit-only skills.
  • skills_read({"skill":"my-skill","resource":null,"cursor":null}) reads SKILL.md. Use an ID from the list to distinguish duplicate names. Set resource to a package-relative text path such as references/guide.md. Read every page of an instruction document before acting on it. Changed content, package metadata, or resource identity invalidates a continuation; restart without a cursor. Reads revalidate current admission even when a cursor is supplied.
  • skills_manager returns an error directing the model to skills_read when the complete JSON response would exceed the budget. It never returns partial instructions.

catalog() returns immutable host metadata, a generation and structured diagnostics. Generation is unchanged by an identical reload. reload() / load() rescan membership; invalidate() is a cheap hook for a host-owned filesystem watcher and causes the next async list/read to refresh. Concurrent lazy refreshes coalesce. Reads revalidate the selected SKILL.md and sidecar; a missing name also triggers a scan. Creating a duplicate or changing an unrelated file requires invalidation/reload. Synchronous catalog and callable lookups use the last published generation. Refreshing a completion request's resident tool definitions remains the caller's responsibility; there is no background watcher or automatic history rewrite.

Root order determines name precedence. Duplicate names inside the winning root have no name-based reader or callable; each admitted copy remains readable by ID. Rejected copies are filtered before resolving precedence. IDs are opaque hashes of the canonical file location and survive frontmatter renames and root reordering; moving a file changes its ID. Only unchanged winning identities retain live delegated sessions. A renamed/deleted skill or a vanished root cannot leave a stale callable after reload. Scan failures omit unverified files and report diagnostics rather than silently keeping their old callables. Existing names retain skill_* callables; names longer than 58 characters use a stable skillh_* hash so all valid 64-character skill names fit the function-name contract.

Defaults are: 6 descendant directory levels, 2,000 directories and 20,000 entries per root; 1,024 skill files and 32 MiB decoded skill/sidecar content across roots; 512 KiB per SKILL.md, 32 KiB per sidecar, and 1 MiB per bundled text resource. Hidden descendant directories and directory symlinks are not traversed. Configured root aliases are resolved before use; descendant symlinks, hardlinks, nonregular files, and parent/path escapes are rejected on the opened file handle. Actual bytes and decoded text are both bounded. SkillLimits configures scan limits, an 8,000-byte resident catalog budget, and a 32 KiB serialized JSON response budget, within documented hard ceilings in the builder. Descriptions are shortened before catalog entries are omitted. Host-supplied custom introductory tool descriptions and hook-rewritten outputs are outside these budgets.

Optional agents/openai.yaml sections override corresponding frontmatter metadata sections. Malformed or oversized sidecars reject the skill with a diagnostic, so a broken explicit-only policy cannot silently become permissive. Supported text metadata includes interface.display_name, interface.short_description, and interface.default_prompt; metadata.short-description is also supported. UI hints are not automatically executed.

policy:
  allow_implicit_invocation: false
interface:
  short_description: Publish a release when explicitly requested.
dependencies:
  tools:
    - type: tool
      value: shell
    - type: mcp
      value: releases

Explicit-only policy hides automatic catalog/definition listings; exact selection is still possible. It is not an authorization boundary. SkillFilter controls actual admission. SkillSummary::preflight compares dependencies with host-approved tool and provider name sets and returns missing requirements. Unknown dependency kinds remain missing. Dependency metadata never installs, connects, or grants a capability; launcher, OAuth and approval UX belong to the application. allowed-tools continues to constrain delegated callables; including tools_select also allows subsequent discovery under the completion runner's existing rules. Inline skills retain the calling agent's permissions.

SkillToolHook, SkillsListHook and SkillsReadHook observe/customize ordinary tool calls. The read hook includes the resolved skill identity, resource and content fingerprint, so hosts can record usage without forwarding conversation history. Old catalog snapshots are metadata only and do not authorize later reads. Existing copies of already running subagents are not cancelled by removing their catalog entries; hosts own session shutdown.

Memory

The memory module stores conversations, resources, artifacts, usage, steering messages, and follow-up messages. It uses AndaDB collections and exposes KIP-backed tools for persistent agent memory through the Cognitive Nexus.

Remote Engines

Engines can register other engines by endpoint. Remote metadata is fetched through signed RPC, and exported remote functions are exposed with prefixed names:

  • Tools: RT_{handle}_{tool}
  • Agents: RA_{handle}_{agent}

This lets agents discover and call capabilities hosted by other engines without linking them into the same process.

Hooks

Engine-level hooks can observe or transform agent and tool execution. Typed hooks can be attached through context state for specific extensions, including background task lifecycle events.

SingleThreadHook is included for applications that want to limit each caller to one active prompt at a time.

Security Notes

  • Engines are private by default. Configure Management when exposing an engine to external callers.
  • Direct agent and tool calls validate request metadata and engine identity.
  • Only exported agents and tools appear in Engine::information and are available to non-manager callers.
  • Filesystem tools resolve paths under the configured workspace and reject unsafe writes through symlinks or multiply linked files.
  • Shell output is truncated in responses when it exceeds the inline limit; full output can be written to a temporary file.

Related Crates

  • anda_core: core traits, request/response types, messages, resources, and tool schemas.
  • anda_engine_server: HTTP server for exposing one or more engines.
  • anda_web3_client: Web3 integration for non-TEE environments.

Development

Useful checks while working on this crate:

cargo check -p anda_engine
cargo test -p anda_engine --lib
cargo clippy -p anda_engine --all-targets -- -D warnings

License

Copyright © 2026 LDC Labs.

ldclabs/anda is licensed under the MIT License. See the MIT license for the full license text.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in anda by you, shall be licensed as MIT, without any additional terms or conditions.

MCP runtime policies

Use McpServerConfig constructors, then configure limits, timeouts, concurrency, required, and startup. Stdio forwards only platform essentials plus explicit env by default (inherit_env = true restores full inheritance). routes() preserves original tool metadata; server_statuses() is observational. Raw MCP outputs remain auditable while the runner uses bounded ToolOutput::model_output presentations, including supported images in tool responses. Elicitation needs both a handler and per-server opt-in; resource APIs are separately opt-in. See MCP_INTEGRATION.md for defaults, migration notes, lifecycle guarantees, model compatibility, and an example.