|
| 1 | +--- |
| 2 | +title: Tool Records |
| 3 | +description: The three ways a capability reaches an agent, and the narrow case where authoring a tool record is the right answer |
| 4 | +--- |
| 5 | + |
| 6 | +# Tool Records |
| 7 | + |
| 8 | +Part of the [AI module](/docs/ai). `tool` is an authorable metadata kind — |
| 9 | +`ToolSchema`, declared as `defineStack({ tools })` — and it is the **least |
| 10 | +likely** answer to "how do I give my agent a new capability". |
| 11 | + |
| 12 | +So this page opens with the decision instead of the shape. If you read only the |
| 13 | +next section and leave, you will be on the right path. The declaration shape is |
| 14 | +further down, for the reader who has already established that they need it. |
| 15 | + |
| 16 | +<Callout type="info"> |
| 17 | +**The default third-party path declares no tool records at all.** That is |
| 18 | +[ADR-0109](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0109-ai-tool-authoring-model.md), |
| 19 | +and the `stack.tools` field says so in its own description: |
| 20 | + |
| 21 | +```text |
| 22 | +AI Tool metadata records — optional refinement layer, never required: the |
| 23 | +default path is skills referencing platform tools or materialised action_<name> |
| 24 | +tools (ADR-0109) |
| 25 | +``` |
| 26 | +</Callout> |
| 27 | + |
| 28 | +## Three ways a capability reaches an agent |
| 29 | + |
| 30 | +An agent's capability set is the union of its surface-compatible **skills'** |
| 31 | +tools ([ADR-0064](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0064-tool-scoping-to-agent.md)), |
| 32 | +so every one of these is ultimately a name in some skill's `tools[]`. What |
| 33 | +differs is where that name comes from: |
| 34 | + |
| 35 | +| What you want the agent to do | How you get there | Tool record? | |
| 36 | +|:---|:---|:---| |
| 37 | +| Read, query, aggregate, or search — what the platform already does for every app | Name the **platform tool** in your skill: `query_records`, `get_record`, `aggregate_data`, `search_knowledge`, `describe_object`, … | **No** | |
| 38 | +| Run something your app already does — a `script` / `api` / `flow` Action | Opt the Action in with `ai.exposed` + `ai.description`; the runtime materialises one `action_<name>` tool per exposed Action and your skill names that. See [Actions as Tools](/docs/ai/actions-as-tools). | **No** | |
| 39 | +| Reach a system outside your app | Connect it over MCP — see [Connect an MCP Client](/docs/ai/connect-mcp) | **No** | |
| 40 | +| Present an executable to the model *differently* from the way your app runs it | A `tool` record — the optional refinement layer described below | **Yes**, and rarely | |
| 41 | + |
| 42 | +There is a fourth thing authors reach for that is not on this list, because it |
| 43 | +is not a tool at all: *reasoning*. "Analyse the pipeline", "draft this email", |
| 44 | +"score this lead" are things the model does with data it already has. Writing |
| 45 | +them as tool names is the most common authoring mistake on this surface, and it |
| 46 | +produces an assistant that claims abilities it does not have. |
| 47 | + |
| 48 | +### The default path, end to end |
| 49 | + |
| 50 | +Two declarations, no tool record — the Action you already ship for your UI, and |
| 51 | +a skill that names its materialised tool: |
| 52 | + |
| 53 | +{/* os:check */} |
| 54 | +```typescript |
| 55 | +import { defineAction } from '@objectstack/spec/ui'; |
| 56 | +import { defineSkill } from '@objectstack/spec/ai'; |
| 57 | + |
| 58 | +// 1. An Action the app already has — opted in to AI. |
| 59 | +export const EscalateCaseAction = defineAction({ |
| 60 | + name: 'escalate_case', |
| 61 | + label: 'Escalate Case', |
| 62 | + objectName: 'support_case', |
| 63 | + type: 'flow', |
| 64 | + target: 'case_escalation_flow', |
| 65 | + ai: { |
| 66 | + exposed: true, |
| 67 | + description: 'Escalates a support case to the on-call queue and notifies the account owner.', |
| 68 | + }, |
| 69 | +}); |
| 70 | + |
| 71 | +// 2. The skill names the materialised tool. No defineTool anywhere. |
| 72 | +export const CaseTriageSkill = defineSkill({ |
| 73 | + name: 'case_triage', |
| 74 | + label: 'Case Triage', |
| 75 | + surface: 'ask', |
| 76 | + instructions: 'Read the case and its recent activity, then escalate when the customer is blocked.', |
| 77 | + tools: ['get_record', 'query_records', 'action_escalate_case'], |
| 78 | +}); |
| 79 | +``` |
| 80 | + |
| 81 | +The full walkthrough — including the three conditions that decide whether |
| 82 | +`action_<name>` exists at all — is in |
| 83 | +[AI Agents](/docs/ai/agents#a-skill-needs-no-tool-records--name-the-action). |
| 84 | + |
| 85 | +### Why that is the default |
| 86 | + |
| 87 | +- **AI capability ≡ application capability.** The executable, its permission |
| 88 | + checks and its audit trail are the Action your UI button already runs. There |
| 89 | + is no second security surface to review, and no way for the agent to do |
| 90 | + something the app cannot. |
| 91 | +- **One less namespace to get wrong.** A tool record is a second place a name |
| 92 | + has to stay consistent — and a second place an AI author can invent one. |
| 93 | +- **Unresolved names surface at authoring time.** `os validate` reports a |
| 94 | + `skill.tools[]` entry that resolves to nothing (`ai-skill-tool-unresolved`, |
| 95 | + advisory). The rule exists because an app once shipped ten fictional tool |
| 96 | + names across six skills, every one of them passing validation. |
| 97 | + |
| 98 | +## When a tool record is the right answer |
| 99 | + |
| 100 | +Reach for one only when the **AI-facing surface must differ from the raw |
| 101 | +executable**. ADR-0109 names the qualifying cases: |
| 102 | + |
| 103 | +- **A different LLM-facing description** — the model needs a contract written |
| 104 | + for it, and the Action's own `ai.description` cannot serve both audiences. |
| 105 | +- **Parameter narrowing** — the Action takes twenty parameters and the agent |
| 106 | + should see three, or an enum should be tighter for the model than for the API. |
| 107 | +- **Exposing a Flow** — there is no materialised family for flows, only for |
| 108 | + Actions. |
| 109 | +- **A stable AI-facing name** — decoupled from the Action's name, so renaming |
| 110 | + the Action does not rename the tool the model was trained against. |
| 111 | +- **Execution policy** — a confirm-before-run flag. Note that |
| 112 | + `requiresConfirmation` was *removed* from `ToolSchema` as unenforced |
| 113 | + ([ADR-0049](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0049-no-unenforced-security-properties.md)); |
| 114 | + it returns only together with its enforcement. |
| 115 | + |
| 116 | +If none of those describe your situation, you do not need a tool record. If one |
| 117 | +of them does, read the next callout before you write it. |
| 118 | + |
| 119 | +<Callout type="warn"> |
| 120 | +**What a tool record does today.** `ToolSchema` has no `implementation` or |
| 121 | +`handler` field, and no framework executor loads a metadata-authored tool — |
| 122 | +authoring one does **not** make anything runnable. The refinement layer above is |
| 123 | +ADR-0109 *Phase 2*, which is gated on a real refinement need and has not landed: |
| 124 | +`stack.tools` has no runtime reader yet. |
| 125 | + |
| 126 | +What a record does do today is narrower and worth knowing: it survives stack |
| 127 | +composition, it is mirrored into the metadata store for Studio and discovery, |
| 128 | +and its name joins the resolution universe for `skill.tools[]` — so a skill |
| 129 | +naming it validates clean. |
| 130 | + |
| 131 | +When Phase 2 lands, a third-party tool record must carry a `binding` |
| 132 | +(`{ type: 'action' | 'flow', name }`) to the executable it refines. Handlers |
| 133 | +never live on the tool. Write the record as a *view* of something your app |
| 134 | +already executes, and it will still be one. |
| 135 | +</Callout> |
| 136 | + |
| 137 | +## The declaration shape |
| 138 | + |
| 139 | +`ToolSchema` and the `defineTool` factory are exported from |
| 140 | +`@objectstack/spec/ai`. The generated field reference is |
| 141 | +[Tool](/docs/references/ai/tool); the authoring-relevant fields are: |
| 142 | + |
| 143 | +| Field | Required | Meaning | |
| 144 | +|:---|:---|:---| |
| 145 | +| `name` | ✅ | Machine name, `snake_case`, globally unique — this is what a skill's `tools[]` names | |
| 146 | +| `label` | ✅ | Human-readable display name | |
| 147 | +| `description` | ✅ | The text the **model** reads to decide when to call the tool | |
| 148 | +| `parameters` | ✅ | JSON Schema for the tool input — the model generates arguments conforming to it | |
| 149 | +| `outputSchema` | optional | ⚠️ **Experimental, not enforced.** Its top-level keys are folded into the description shown to the model; outputs are never validated against it | |
| 150 | +| `objectName` | optional | The object this tool operates on, when there is exactly one | |
| 151 | +| `protection` | optional | Package-author lock policy ([ADR-0010](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0010-metadata-protection-model.md)) | |
| 152 | + |
| 153 | +The shape is **strict**: an undeclared key is rejected at parse time, not |
| 154 | +stripped. Five keys that were once authorable — `permissions`, `active`, |
| 155 | +`category`, `builtIn` and `requiresConfirmation` — were removed because nothing |
| 156 | +read them, and each rejects today with the prescription for what to write |
| 157 | +instead. If you are porting an older record, the parse error is the instruction. |
| 158 | + |
| 159 | +{/* os:check */} |
| 160 | +```typescript |
| 161 | +import { defineTool, defineSkill } from '@objectstack/spec/ai'; |
| 162 | + |
| 163 | +// A refinement: the underlying Action takes the full case payload, but the |
| 164 | +// agent only ever needs the record and a length hint. |
| 165 | +export const SummariseCaseTool = defineTool({ |
| 166 | + name: 'summarise_case', |
| 167 | + label: 'Summarise Case', |
| 168 | + description: |
| 169 | + 'Summarise a support case and its recent activity for a human reader. ' |
| 170 | + + 'Use it before escalating, so the summary can be pasted into the handover note.', |
| 171 | + objectName: 'support_case', |
| 172 | + parameters: { |
| 173 | + type: 'object', |
| 174 | + properties: { |
| 175 | + caseId: { type: 'string', description: 'Record id of the case to summarise' }, |
| 176 | + length: { type: 'string', enum: ['short', 'detailed'] }, |
| 177 | + }, |
| 178 | + required: ['caseId'], |
| 179 | + }, |
| 180 | +}); |
| 181 | + |
| 182 | +// The skill names it exactly the way it names a platform or materialised tool. |
| 183 | +export const CaseHandoverSkill = defineSkill({ |
| 184 | + name: 'case_handover', |
| 185 | + label: 'Case Handover', |
| 186 | + surface: 'ask', |
| 187 | + instructions: 'Summarise before you escalate, and put the summary in the handover note.', |
| 188 | + tools: ['summarise_case', 'action_escalate_case', 'get_record'], |
| 189 | +}); |
| 190 | +``` |
| 191 | + |
| 192 | +## How the name is resolved |
| 193 | + |
| 194 | +A `skill.tools[]` entry resolves against three sources, in order: |
| 195 | + |
| 196 | +1. the stack's own `tools[]` names — the refinement records on this page; |
| 197 | +2. the curated registry of tools the platform runtime registers at boot; |
| 198 | +3. the materialised `action_<name>` family, one per AI-exposed Action declared |
| 199 | + on the stack or on any object. |
| 200 | + |
| 201 | +A trailing wildcard matches every member of that universe sharing the prefix, so |
| 202 | +`action_*` subscribes a skill to all of the app's exposed Actions at once. |
| 203 | + |
| 204 | +**Agents do not name tools.** `agent.tools` was removed in protocol 17: it was |
| 205 | +the one seam that let an agent reach a tool no skill of its surface declared. |
| 206 | +An agent reaches exactly the tools its surface-compatible skills declare |
| 207 | +(ADR-0064), so a tool record becomes reachable by attaching the skill that names |
| 208 | +it, never by listing it on the agent. |
| 209 | + |
| 210 | +--- |
| 211 | + |
| 212 | +## See also |
| 213 | + |
| 214 | +- [Actions as Tools](/docs/ai/actions-as-tools) — the default path: an Action materialised as an `action_<name>` tool |
| 215 | +- [AI Agents](/docs/ai/agents) — the two platform agents, and skills as the extension primitive |
| 216 | +- [Connect an MCP Client](/docs/ai/connect-mcp) — reaching tools outside your app |
| 217 | +- [AI Skills System](/docs/ai/skills) — a different layer: the `SKILL.md` knowledge modules that teach a coding assistant to *write* your metadata |
| 218 | +- [Tool reference](/docs/references/ai/tool) — every field, generated from `ToolSchema` |
0 commit comments