Page
Tool System
Tools are the only way an LLM can affect or observe the workspace. No implicit context, no hidden mounts, no backdoors.
Design Principle
Choir uses an OS-inspired model:
| OS Concept | Choir Concept |
|---|---|
| User process | LLM lane |
| Kernel | choir-agent runtime |
| Syscalls | Tools |
| Filesystem | Workspace |
| Scheduler | Arbiter |
| Process memory | Compacted working memory |
The LLM proposes tool calls; the arbiter validates, acquires locks, executes, and commits results. No lane can commit a side effect without going through the arbiter.
Dual-View Manifests
Every tool has two representations:
LLM View (what the model sees)
Sent via the OpenAI-compatible tools parameter. Contains only what the model needs to make calls.
{
"name": "choir.fs.write",
"description": "Write content to a file in the workspace.",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string" },
"content": { "type": "string" },
"mode": { "type": "string", "enum": ["overwrite", "append"] }
},
"required": ["path", "content"]
}
}
Runtime View (internal metadata)
Contains lock requirements, security policies, timeout, and execution details. Never exposed to the LLM.
{
"llm": { "name": "...", "description": "...", "input_schema": "..." },
"runtime": {
"exec_path": "/choir/tools/global/git_commit",
"timeout_ms": 15000,
"locks": [
{ "resource": "workspace", "mode": "X" }
],
"network": false,
"secret_resources": ["git_identity"],
"side_effect": "write",
"idempotent": false,
"version": "1.0"
}
}
Invocation Contract
- Model must use structured tool calling (out-of-band tool channel, not text).
- No regex/text parsing for tool calls.
- Tool names are canonical and namespaced (e.g.
choir.fs.read). - All tool inputs validated against schema before execution.
Tool Taxonomy
Read-Only
Safe to parallelize, require shared (S) locks.
choir.fs.read,choir.memory.search,choir.repo.status
Write/Mutating
Require exclusive (X) locks.
choir.fs.write,choir.repo.commit,choir.memory.upsert
External Side Effects
Require X locks + audit logging.
choir.http.post,choir.email.send,choir.web.browse
Built-In Tools (v1)
| # | Name | ID | Lock | Host-Executed | Notes |
|---|---|---|---|---|---|
| 1 | Shell | choir.exec |
workspace:X |
No | Arbitrary commands; secret_resources: [] |
| 2 | Read File | choir.fs.read |
file:<path>:S |
No | Supports chunking via head/tail |
| 3 | Edit File | choir.fs.write |
file:<path>:X |
No | Patch-based (replace range, append, insert at line) |
| 4 | Ripgrep | choir.fs.search |
file:<path>:S |
No | rg binary shipped in image |
| 5 | TTS | choir.tts.speak |
choirtmp:X |
No | TTS via agent’s voice profile; writes to .choirtmp/send/ |
| 6 | Brave Search | choir.web.search |
none | No | Brave Search API (structured results) |
| 7 | Browse | choir.web.browse |
browser_tab:X |
Yes | Playwright via host EXECUTE_HOST_TOOL |
| 8 | Notion | choir.notion.query |
none | No | Notion API integration |
| 9 | Email Send | choir.email.send |
none | No | SMTP email send |
| 10 | Email Receive | choir.email.receive |
none | No | IMAP fetch; returns message list/content |
| 11 | Email Check | choir.email.check |
none | No | IMAP check for new messages; returns count/summary |
| 12 | Memory Query | choir.memory.query |
none | Yes | Search working/session/knowledge via host |
| 13 | Memory Write | choir.memory.upsert |
none | Yes | Write to knowledge store via host |
| 14 | Memory Compact | choir.memory.compact |
none | No | Force reference summary update |
Host-executed tools use the EXECUTE_HOST_TOOL RPC verb to delegate work to choird (e.g., Playwright for browsing, Postgres for memory operations).
Tool Registration and Loading
Registry Sources
- Built-in tools: Runtime-backed Go implementations compiled into the
choir-agentbinary. - External tools: Manifest (
.json) + executable in/choir/tools/.
On-Disk Structure
/choir/tools/
global/ # baked into image, shared across agents
foo.json # tool manifest (LLM + runtime views)
foo # executable binary
agent/ # agent-specific tools
bar.json
bar
Loading Sequence
- Load built-in tools (Go functions)
- Scan
/choir/tools/global - Scan
/choir/tools/agent - Validate JSON schemas
- Verify executables exist
- Build registry
- Register LLM-facing schemas (stripped of runtime metadata)
- If any JSON is invalid, fail startup (fail fast)
Tool Interface
Built-in and external tools share a unified Go interface:
type Tool interface {
Name() string
Schema() JSONSchema
Execute(ctx context.Context, input json.RawMessage) (json.RawMessage, error)
}
No dynamic runtime installs. Tool registries are immutable at runtime.
Execution Pipeline
- Send request (system prompt + messages + tool schemas) to LLM.
- Model responds with text OR structured tool call(s).
- Arbiter receives tool calls; for each:
- Validate input against schema.
- Enforce path constraints, lease, resource locks.
- Acquire locks (atomic, all-or-nothing).
- Execute tool (or delegate via
EXECUTE_HOST_TOOL). - Log event to commit log.
- Release locks.
- Send tool result back to model.
- Call model again.
Tool Output Format
Tools return structured JSON:
{
"status": "success",
"summary": "...",
"artifact_ref": "hash123"
}
Per-tool output tagging controls visibility:
exposure: edge | core | both | noneprompt_mask: true/false
Tool Surface Minimization
Tools are grouped logically rather than exposing many granular variants. For example, choir.memory.query accepts a mode parameter (semantic, hash, range) rather than having separate tools for each.
Meta-Tools and Approvals
Two classes of tools:
Runtime tools: Execute immediately, governed by locks.
Control plane tools (meta): Proposal-only, never execute immediately, always require manual approval.
| Meta-Tool | Purpose |
|---|---|
choir.propose.tool |
Propose a new tool addition/change |
choir.propose.skill |
Propose a new skill addition/change |
choir.propose.config_change |
Propose a configuration change |
Proposal Pipeline
LLM -> propose_tool_change
-> choir-agent logs proposal
-> choird stores pending proposal
-> human approval (via choirctl or gateway)
-> choird mutates registry, rebuilds image
Hard prohibitions: LLM never injects executable code. No auto-apply.
Creating Custom Tools
Custom tools live in git repositories (global or per-agent). Each tool consists of:
- Manifest (
<tool-name>.json): JSON file with both LLM and runtime views. - Executable (
<tool-name>or<tool-name>/): Binary or source directory.
Steps
- Create the manifest with LLM view (name, description, parameters) and runtime view (locks, timeout, secrets, side effect classification).
- Write the tool implementation. Compiled tools have source directories under
tools/that are built duringagent build. - Place in the global repo (
choir-global/tools/) for shared tools, or the per-agent repo (choir-agent-<id>/tools/) for agent-specific tools. - If an agent tool has the same name as a global tool, the agent version overrides (resolved at build time).
- Run
choirctl agent build <agent-id>to bake the tool into the image.
Alternatively, the agent can use the tool-builder skill (see Skills) to propose new tools through the approval pipeline.