Skip to main content

Tools

Digitorn agents call tools through a discovery architecture. The context_builder module builds the tool index at bootstrap, the runtime picks one of three injection modes based on toolset size, and the LLM either receives full schemas, compact listings, or a small set of meta-tools that let it discover the rest on demand.

Adaptive tool injection​

The injection mode is picked per agent at bootstrap based on the brain's context window vs the actual JSON size of every tool schema.

The algorithm​

The result is decided once per agent and reused for every turn of that session. To force a specific mode, set runtime.tool_injection: direct | compact_direct | discovery in the YAML; the algorithm is skipped and the forced mode is used.

Direct mode​

Full OpenAI-compatible tool schemas are passed to the LLM - name, description, complete parameters JSON schema, examples. The LLM calls tools by name with full parameter knowledge.

Best for apps with ~1-3 modules and small total tool counts (every tool fits comfortably in 20% of the context window).

Compact direct mode​

Each tool is listed by name + one-line description (~30 tokens each). The LLM knows which tools exist and can call them directly, but discovers the parameter schema at call time (the runtime fetches it lazily).

Best for apps with 5-12 modules and 60-400 tools.

Discovery mode​

Domain tools are hidden behind meta-tools. The agent sees strategic tools directly and discovers domain tools via semantic search.

Meta-tools (which ones are injected depends on mode):

ActionNotes
search_toolsquery / category / no args (list domains)
get_toolFull schema
execute_toolExecute by name
run_paralleltasks: [{tool, args}]
background_runIncludes optional watch loops

Also injected when enabled: ask_user, call_app, use_skill, memory tools, agent + kv.

Conditionally direct (load + grant / role rules):

Action setModuleGated by
Memory toolsmemoryMemory enabled
Agent spawn (agent + kv)agent_spawnCoordinator / spawn enabled
Session cron (schedule)schedulerModule + grant
File actionsfilesystemModule loaded (YAML alias workspace → filesystem)
Direct moduleslistedruntime.direct_modules

Use background_run(watch=true) for a watch loop.

In discovery mode, domain tools sit behind search_tools / get_tool / execute_tool.

Best for apps with MCP servers, plugin ecosystems, or 400+ tools.

Threshold reference​

The thresholds are deterministic given a context window and the actual tool sizes. With the fallback estimator (200 tokens per tool):

Context windowDirect (≤N tools)Compact (≤N tools)Discovery (>N tools)
8 K85354+
32 K32213214+
60 K60400401+
128 K128853854+
200 K2001 3331 334+

When direct_tools is non-empty, the runtime uses the actual JSON size of every tool schema (4 chars ≈ 1 token), so a small toolset with very long descriptions can still tip into compact mode.

How discovery works​

The semantic index is built at bootstrap from a rich corpus: action FQN + description + tags + parameter names + side effects + aliases (see Semantic search below).

Auto-routing direct calls​

If the LLM calls a tool by its short name directly (filesystem.read({...}) instead of execute_tool(name="filesystem.read", params={...})), the agent loop transparently routes it through execute_tool. This happens in every mode, so the same agent code works whether the LLM saw the full schema, a compact listing, or only the meta-tools.

Module declaration​

Tools come from modules declared under tools.modules. Every entry is a ModuleBlock.

yaml
tools:
modules:
filesystem:
constraints:
allowed_actions: [read, glob, grep]
database:
config: {}
constraints:
allowed_actions: [connect, query, disconnect]

The full ModuleBlock field reference (config, setup, constraints, middleware, credential) is in App Configuration → tools.modules.

Registered and catalog modules are listed in the index; per-module pages live under reference/modules/. context_builder is auto-loaded (do not declare it for ordinary apps).

To inspect any module's actions and parameter schemas, see the per-module reference pages under modules/reference/.

Tool constraints​

Two universal keys on ModuleBlock.constraints:

yaml
tools:
modules:
filesystem:
constraints:
allowed_actions: [read, glob, grep] # whitelist
database:
constraints:
blocked_actions: [disconnect] # example blacklist

The context_builder builds the agent's tool index with these constraints applied - blocked / non-allowed actions are invisible to the LLM. They can still be called from setup: steps, hooks, and channel pipelines because those run with the daemon's identity, not the agent's.

Module-specific constraints (anything beyond allowed_actions / blocked_actions) are validated against the module's ConstraintSpec declarations.

When a model answers in plain text instead of calling a tool​

Whatever the backend, tool schemas always go out the same way: via the API's native tools= parameter, for every request that carries tools - there is no separate code path that inlines schemas into the system prompt text instead. Most models honor that and reply with a structured tool_calls entry.

Some models - small local ones especially - answer with a tool call shaped as plain text anyway (an XML tag, a fenced JSON block, a <tool_call>{...}</tool_call>-style wrapper, ...) even though they were given the real schema. When that happens and the response carries no structured tool_calls, a registry of format-specific parsers tries to recover one from the text before giving up - Anthropic-style XML, DeepSeek tokens, Hermes tags, fenced JSON, Cursor-style tags, ReAct-style actions, Qwen's special tokens, Llama python tags, bracket-style calls, pythonic calls, and a few more.

What the system prompt looks like (discovery mode)​

text
You are agent "<id>" (role: <role>).

You have access to N tools across M domains.

To find and use tools, you have these meta-tools:
- search_tools: Search over the visible tool index
- get_tool: Full schema for one tool
- execute_tool: Execute a tool with parameters
- run_parallel / background_run / use_skill / call_app / ask_user

Workflow:
1. Discover what is available (search)
2. Get the exact parameter schema before calling
3. Execute the tool with the correct parameters

[Your system_prompt from YAML]

This is the discovery-mode shape from Adaptive tool injection above; direct and compact-direct mode skip the meta-tool workflow block since the LLM already has the real tools (or their names) up front.

Tool name sanitization​

OpenAI-compatible APIs require function names to match ^[a-zA-Z0-9_-]+$. Digitorn uses dotted FQNs internally (filesystem.read); the runtime sanitizes both directions:

  • Outbound (to API): filesystem.read → filesystem__read
  • Inbound (from API): filesystem__read → filesystem.read

YAML authors and module developers always write the dotted form; the conversion is invisible.

Discovery mode uses hybrid search combining a semantic index and a keyword inverted index.

  • Semantic - an in-process vector index built at bootstrap (cosine similarity over embeddings, no external vector DB - this is a different, lighter-weight store than the RAG module's). Multilingual model paraphrase-multilingual-MiniLM-L12-v2 (384 dims). Supports ~50 languages.
  • Keyword - inverted index with prefix matching.
  • Hybrid scoring - semantic score (×10 weight) + keyword boost (+2-3) for ranking.

The corpus indexed per tool: FQN + description + tags + parameter names + side effects + aliases + synonym expansion. Aliases are declared on the tool definition non-English search queries find the right tool.

Execution primitives​

context_builder exposes a small set of primitives that wrap any module action.

CategoryAction(s)Gated by
Parallelrun_parallelalways (when tools enabled)
Background / watch loopsbackground_runalways (when tools enabled)
Skillsuse_skillskills enabled
App-as-toolcall_appcall_app enabled
Human-in-the-loopask_userask_user enabled
Session cron wake-upscheduler.schedulemodule + grant
Shared KVkvwith agent spawn
Long-term memorymemory.*memory enabled

See Execution Primitives.

Cross-references​