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

  1. Model must use structured tool calling (out-of-band tool channel, not text).
  2. No regex/text parsing for tool calls.
  3. Tool names are canonical and namespaced (e.g. choir.fs.read).
  4. 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

  1. Built-in tools: Runtime-backed Go implementations compiled into the choir-agent binary.
  2. 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

  1. Load built-in tools (Go functions)
  2. Scan /choir/tools/global
  3. Scan /choir/tools/agent
  4. Validate JSON schemas
  5. Verify executables exist
  6. Build registry
  7. Register LLM-facing schemas (stripped of runtime metadata)
  8. 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

  1. Send request (system prompt + messages + tool schemas) to LLM.
  2. Model responds with text OR structured tool call(s).
  3. 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.
  4. Send tool result back to model.
  5. 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 | none
  • prompt_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:

  1. Manifest (<tool-name>.json): JSON file with both LLM and runtime views.
  2. Executable (<tool-name> or <tool-name>/): Binary or source directory.

Steps

  1. Create the manifest with LLM view (name, description, parameters) and runtime view (locks, timeout, secrets, side effect classification).
  2. Write the tool implementation. Compiled tools have source directories under tools/ that are built during agent build.
  3. Place in the global repo (choir-global/tools/) for shared tools, or the per-agent repo (choir-agent-<id>/tools/) for agent-specific tools.
  4. If an agent tool has the same name as a global tool, the agent version overrides (resolved at build time).
  5. 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.