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):
| Action | Notes |
|---|---|
search_tools | query / category / no args (list domains) |
get_tool | Full schema |
execute_tool | Execute by name |
run_parallel | tasks: [{tool, args}] |
background_run | Includes optional watch loops |
Also injected when enabled: ask_user, call_app, use_skill,
memory tools, agent + kv.
Conditionally direct (load + grant / role rules):
| Action set | Module | Gated by |
|---|---|---|
| Memory tools | memory | Memory enabled |
Agent spawn (agent + kv) | agent_spawn | Coordinator / spawn enabled |
Session cron (schedule) | scheduler | Module + grant |
| File actions | filesystem | Module loaded (YAML alias workspace → filesystem) |
| Direct modules | listed | runtime.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 window | Direct (≤N tools) | Compact (≤N tools) | Discovery (>N tools) |
|---|---|---|---|
| 8 K | 8 | 53 | 54+ |
| 32 K | 32 | 213 | 214+ |
| 60 K | 60 | 400 | 401+ |
| 128 K | 128 | 853 | 854+ |
| 200 K | 200 | 1 333 | 1 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.
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:
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)
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.
Semantic search
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.
| Category | Action(s) | Gated by |
|---|---|---|
| Parallel | run_parallel | always (when tools enabled) |
| Background / watch loops | background_run | always (when tools enabled) |
| Skills | use_skill | skills enabled |
| App-as-tool | call_app | call_app enabled |
| Human-in-the-loop | ask_user | ask_user enabled |
| Session cron wake-up | scheduler.schedule | module + grant |
| Shared KV | kv | with agent spawn |
| Long-term memory | memory.* | memory enabled |
See Execution Primitives.
Cross-references
- Module configuration block reference: App Configuration → tools
- Built-in tools (delegation, memory, todo): Built-in Tools
- MCP server integration: MCP Servers
- Capabilities (grant / approve / deny): Security
- Per-module reference: modules/index.md