Tether
DocsGitHub

Core concepts

Tools

A tool is a capability your product already has, described so a model can decide when to reach for it. The catalog you register is the complete set — Tether cannot call anything else.

Anatomy

{
  "name": "get_open_tickets",
  "description": "Lists open tickets for a site, newest first. Use when the user asks what is outstanding, unresolved, or still open. Does not include tickets closed today.",
  "domain": "orders",
  "is_write": false,
  "requires_scope": true,
  "parameters": {
    "type": "object",
    "properties": {
      "limit": { "type": "integer", "description": "Max rows. Default 20, max 100." }
    },
    "required": []
  }
}

Every field is documented in the tool spec reference.

Descriptions are the interface

The description is the only thing the model uses to decide whether this tool answers the question in front of it. It is not documentation for you; it is a prompt.

Write it to disambiguate against your other tools:

✗"Gets tickets."
✓"Lists open tickets for a site, newest first. Use when the user asks what is outstanding or still open. Does not include tickets closed today — use get_ticket_history for those."

Empty descriptions are rejected at registration. A tool with no description is a tool the model calls at random.

Naming

Names must match ^[a-z][a-z0-9_]{2,63}$ — lowercase, starting with a letter, underscores allowed. This is a provider constraint, not a preference; anything else is rejected upstream.

Prefer verb-first names that read as what happens: get_open_tickets, void_ticket, schedule_visit.

The write flag

is_write decides whether a tool executes inline or gets staged for confirmation. It is declared. Tether never infers it.

Validation

Every registration runs the same checks, and reports all failures at once rather than making you fix them one at a time:

Rejected whenMessage
Name breaks the patternnames must match ^[a-z][a-z0-9_]{2,63}$
Named tether_handoff, which is reserveduse a name reserved for a capability the engine provides itself
Description is emptyno description, so the model would call them at random
Two tools share a namedeclared more than once
Both surfaces disabledregistered but offered on no surface
A read declares a risk leveldeclared a risk level but not declared a write, so nothing would gate them

That last one deserves attention: setting "risk": "high" on something you forgot to mark is_write looks careful and gates nothing. Rather than silently ignoring the field, Tether refuses the catalog.

POST /v1/tools/validate runs identical validation without registering. See Validating your catalog in CI.

Visibility

Not every tool should be offered on every turn. Four flags narrow it:

FlagEffect
chat_eligibleOffered on the conversational surface
agent_eligibleOffered on the agent/actions surface
deep_analysis_onlyOnly in capabilityMode: "Deep"
allow_with_imageMay be offered on a turn carrying an image

Narrowing is not only about permissions. Every tool in the catalog costs tokens on every turn, and a model choosing between eight relevant tools is more accurate than one choosing between sixty. Offering fewer tools is usually a quality decision before it is a cost one.

Versioning

PUT /v1/tools replaces the catalog and increments a version. Previous versions are retained rather than overwritten, for two reasons: a turn already choosing tools keeps the catalog it started with, and a customer who breaks their catalog can see exactly what changed.

Registration is atomic. A rejected catalog leaves the previous one live.