Skip to main content
Binding every tool to every LLM call wastes context and degrades tool selection. Ziro instead splits tools into three tiers and lets the model discover and activate what it needs, when it needs it.

The three tiers

Deferred tools are registered in the registry but not bound until the model activates them. They are searchable the whole time; they just do not occupy a slot in the tool schema block sent with each request.

The discovery loop

1

Search

The model calls search_tools("fetch a url"). This is a semantic search over indexed tool descriptions, with a keyword fallback.
2

Load

It picks names from the results and calls load_tools(["webfetch:web_fetch"]).
3

Call

The tool is now bound and can be called normally.
Activated tools persist for the rest of the thread, up to max_active. Past that, the least-recently-used activated tool is evicted. The active set is tracked in AgentState.active_tools and rendered in the TUI side pane.
unload_tools([...]) deactivates tools explicitly, to free context before the LRU would.

Namespaces and qualified names

Every tool is registered under a namespace, and its qualified name is <namespace>:<tool>:
Qualified names are what you write in the config files that talk about tools:
  • tool_policy.yamlcore_tools
  • permissions.yamlallow / ask / deny / dangerous_default_deny globs
  • a subagent’s *.agent.mdtools: and namespaces:
  • hooks.yaml → the matcher glob for pre_tool and post_tool
Namespace tokens expand to concrete tools. rag, rag:*, and the bare namespace name all resolve to every tool registered in that namespace, preserving order and de-duplicating. That is how a subagent can declare its rights as whole namespaces rather than listing individual tools.
The model usually calls a tool by its unqualified name (run_shell), which is why hook matchers for it are written *run_shell.
Tools from external MCP servers are registered under the server name as the namespace, and enter the same deferred-discovery surface. See MCP.

A disabled tool is not registered at all

This is the important consequence of the design. When a feature’s policy file says enabled: false, the tool is never registered in the registry. It is therefore:
  • invisible to search_tools and list_tools
  • impossible to activate with load_tools
  • absent from the permission view a subagent inherits
There is no separate runtime gate: the absence is the disable switch.
Registration is a visibility control, not a permission boundary. A registered tool still passes through the F06 permission gate on every call, which can allow, ask (raising a human-in-the-loop interrupt), or deny. See Permissions.

The full tool surface

Why fs and fs_write are separate namespaces

Reads and writes are split so a permission policy can allow fs:* while setting ask on fs_write:*. The read-only trio stays frictionless; every mutation goes through the gate. fs_write also refuses the control plane by default: secrets (.env, *.key, *.pem, ssh keys), *_policy.yaml, permissions.yaml, hooks.yaml, .git, .github, and .ziro.

Configuring the tiers

Namespace tokens work here too: fs or fs:* pulls in the whole namespace.
Keep core_tools small. Every core tool’s schema is sent on every request and counts against the compaction budget. The whole point of the deferred tier is that a tool costs nothing until it is needed.

Subagents inherit, never widen

A spawned subagent’s tool rights are the intersection of the child’s own tool policy and the parent’s current rights: core tools, active tools, and permission grants. This is enforced by a filtered facade over the shared registry plus a predicate threaded into the meta-tools, so even progressive discovery cannot surface or activate an out-of-rights tool for a child. See Subagents.

Next steps

Permissions

The gate every tool call passes through.

Shell and files

Running commands and editing files safely.

MCP

Bringing external tool servers into the same surface.