Tools & Abilities
How Chalie acts — the ability model, tool discovery, the result contract, dispatch, and permission gating.
Reasoning is only half of what Chalie does; the other half is acting. Every action the model can take — checking your calendar, sending an email, browsing the web, recalling a memory — is an ability: a small, self-contained unit with a uniform shape. This page explains that shape, how the model finds the right ability mid-turn, the contract every ability returns on, and the single gate every call passes through before it runs.
Anatomy of an ability
An ability declares what the tool is through five zero-argument descriptors and what it does through one run method:
| Descriptor | Purpose |
|---|---|
| Name | The string the model calls (e.g. weather) |
| Summary | The model-facing description, and the prose the search index embeds |
| Examples | 6–8 natural phrases that drive semantic discovery |
| Search tooltip | A short label shown in discovery results |
| Parameters | Plain JSON Schema for the run method’s inputs |
The run method does the work and returns a tool result — never a string, dict, or raw value. That’s the whole interface. There is no decorator, manifest, or registration call: the registry collects abilities automatically at startup. Drop an ability in, and the tool exists.
The parts that must stay uniform are sealed by the framework. The full tool descriptor the model sees is assembled in exactly one place — a method that individual abilities cannot override — and that same method injects two framework fields into every tool: act_summary (a short tooltip the call shows the user) and, on channels that allow it, async (run this call in the background instead of blocking the turn). An ability that tries to redefine the sealed assembler is rejected at import, so the action-trail and backgrounding contracts can’t silently fork per tool.
Descriptors must return deterministic text when no live request is attached, because the same descriptors are read offline to build the search index. An ability can optionally declare a map of action → required parameters; the dispatcher validates it before anything else, so a malformed call bounces with a self-correcting error instead of running.
Tool discovery
A model that could see every tool on every turn would waste most of its context window on descriptions it doesn’t need. So Chalie pre-loads almost nothing and lets the model discover tools on demand.
Two meta-tools plus memory are the only abilities available on a normal turn by default:
find_tools— activate the tools you need for this turn.find_skills— pull a step-by-step playbook for a complex task.
Every other ability is discoverable by default and is reachable only by the model calling find_tools. An ability that opts out of discoverability is absent from the discovery roster entirely — it can only reach a turn by being pinned directly into a channel’s always-available list. That single flag, plus whether a channel even carries find_tools, is the whole of tool isolation: it’s how the raw web tools stay scoped to the web-search and web-browse delegate channels rather than leaking into a normal chat.
The discovery cascade
Both find_tools and find_skills share one search engine. Both take a query array — one tool name or one described action per entry — so the model can fetch everything it needs in a single call. Each entry runs independently through a precise→broad escalation, and the first rung that yields a result wins for that entry:
- Exact name. The normalised entry is matched against tool names. A direct hit is pinned outright. A bare name owned by more than one source (possible with connected MCP tools) is refused and surfaced, rather than silently guessed.
- Name keyword match. Otherwise, a full-text (FTS5 trigram) match over the tool name only, gated by name-segment alignment —
searchmatchesweb_search, but an incidental substring likeallinsidereview_tool_callsis rejected. The segment gate, not a score floor, is the discriminator. - Vector. Otherwise, a semantic KNN search over the full prose (summary and examples), kept only when the cosine distance is at or under a tuned ceiling. This is the rung that turns “is there a way to check the weather in another city” into the
weathertool.
The union of results is deduplicated but never truncated — there is deliberately no global cap across the array, because the point of the array is to let one call surface every tool the turn requires. Names and titles go to the keyword index; full prose goes to the vector index; the two indexes are single-purpose.
This is the current mechanism. Earlier descriptions of discovery as reciprocal-rank fusion with a relevance floor and a fixed five-result cap describe a previous design and no longer apply.
find_tools returns a structured body the model can act on directly — an injected list of {name, summary} plus a not_found list — with counts in the envelope so even a small model can tell exactly what it got. find_skills returns the full playbook text for each matching skill, along with any personalisation rules derived from your behaviour. Both surface a corrupt or unreadable index as a loud error rather than letting it masquerade as “nothing found”.
The result contract
Every ability returns a tool result — a frozen value with exactly two constructors:
- Success: a body (string shown verbatim, or dict/list rendered as compact JSON), an optional rich-media payload for the chat UI, and flat scalar metadata shown in the result’s opening tag.
- Error: a stable kebab-case machine code (required), a one-line recovery hint for the model, and optional valid values when the model passed an invalid one.
The ability never formats the wire output. The dispatcher renders the one envelope the model sees:
[weather(status=success)]
{"location":"Valletta, MT","condition":"Clear","temperature_c":24.1}
[end:weather]
[memory(status=error, code=no-query-or-location, action=recall)]
recall requires either a query or a location.
hint: pass query= to search by topic, or location= to filter by place.
[end:memory]
This is the design principle that makes errors safe: errors are data for the model, not exceptions. A failed call is returned into the loop as a structured error with a recovery hint, so the model can correct itself and try again. It never crashes the turn. The contract is enforced — an ability that returns anything other than a tool result hard-fails with a non-canonical-result error code, and a raised exception is caught and rendered as an error envelope rather than propagating.
Dispatch and permissions
Every tool call — model-issued, framework seed, or background pass — flows through one chokepoint: the tool dispatcher. It is the only path from the act loop to ability execution. In order, it:
- Resolves the tool to a fresh, per-call ability instance bound to the invoking turn; an MCP-prefixed name resolves to an MCP proxy instead.
- Pre-validates the action-required map, so a hallucinated action or a missing parameter bounces with a self-correcting error before the permission gate.
- Gates the call through the policy manager (below).
- Executes — inline, or on a background thread when the call sets the
asyncflag. - Renders the envelope and records the call on the turn’s action trail.
The permission gate
Because tools take real actions, every call passes a policy gate before it runs. The gate is a flat lookup on (channel, permission), where the permission is the tool name optionally qualified by an action (e.g. calendar.delete_event). Each entry resolves to one of four settings:
| Setting | Behaviour |
|---|---|
allow |
Run without asking |
ask |
Prompt you for confirmation, then run if you approve |
deny |
Block, and tell the model not to retry |
internal |
Always allowed; hidden from the policy surface |
A few read-only and infrastructure tools — discovery, memory, reading, the compactors — always bypass the gate as internal. For everything else, an unseen permission defaults to ask: a brand-new action prompts you the first time rather than running silently. You adjust these in the Brain dashboard’s policy manager, and the same permission can carry a different setting per channel.
Crucially, the channel matters. There are three policy channels — chat, the background subconscious, and external agents. On the two channels with no human present to answer a prompt, an ask automatically becomes a deny — so a background reflection or a connected agent can never silently trigger a confirmation that nobody will see. And the risk class a call is gated on is derived from its inputs through the ability itself, never trusted from a model-supplied field, so a prompt-injected “action” can’t talk its way past the gate.
Adding a tool
The shape above is the whole contract, so adding a tool is small:
- Create an ability with the five descriptors plus a run method returning a tool result.
- Leave discoverability on so the tool joins the global
find_toolsroster, or turn it off and pin it into the always-available list of every channel that should reach it. - Rebuild the search index so discovery can find it.
That’s it — no engine changes, no wiring. The same uniformity is what lets external tools join through Agent & MCP Integration and dispatch through this exact pipeline. For how a turn assembles requests and feeds tool results back to the model, see Message Flow; for the runtime these tools live in, see the Architecture Overview; and for how memory and the background mind use these same abilities, see Memory & Cognition.