@orkestrel/scaffold 0.0.67 → 0.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/main.js +67 -44
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/templates/brief.md +9 -0
- package/dist/host/claude/agents/orkestrel.md +4 -4
- package/dist/host/claude/rules/names.md +15 -0
- package/dist/host/claude/rules/tests.md +33 -4
- package/dist/host/claude/rules/workspace.md +14 -2
- package/dist/host/dotfiles/prettierignore +3 -0
- package/dist/host/guides/README.md +65 -0
- package/dist/host/guides/abort.md +169 -0
- package/dist/host/guides/agent.md +1567 -0
- package/dist/host/guides/brief.md +1266 -0
- package/dist/host/guides/browser.md +2200 -0
- package/dist/host/guides/budget.md +196 -0
- package/dist/host/guides/codec.md +519 -0
- package/dist/host/guides/console.md +785 -0
- package/dist/host/guides/contract.md +1193 -0
- package/dist/host/guides/csv.md +541 -0
- package/dist/host/guides/database.md +2518 -0
- package/dist/host/guides/emitter.md +233 -0
- package/dist/host/guides/form.md +1791 -0
- package/dist/host/guides/html.md +717 -0
- package/dist/host/guides/indexeddb.md +505 -0
- package/dist/host/guides/interpret.md +1029 -0
- package/dist/host/guides/lsp.md +515 -0
- package/dist/host/guides/markdown.md +964 -0
- package/dist/host/guides/mcp.md +5554 -0
- package/dist/host/guides/middleware.md +927 -0
- package/dist/host/guides/msg.md +440 -0
- package/dist/host/guides/ndjson.md +120 -0
- package/dist/host/guides/ollama.md +380 -0
- package/dist/host/guides/pool.md +280 -0
- package/dist/host/guides/probe.md +1210 -0
- package/dist/host/guides/process.md +1620 -0
- package/dist/host/guides/program.md +1110 -0
- package/dist/host/guides/qualifier.md +854 -0
- package/dist/host/guides/queue.md +370 -0
- package/dist/host/guides/rater.md +330 -0
- package/dist/host/guides/reason.md +1122 -0
- package/dist/host/guides/relation.md +373 -0
- package/dist/host/guides/router.md +753 -0
- package/dist/host/guides/scaffold.md +192 -31
- package/dist/host/guides/sea.md +383 -0
- package/dist/host/guides/server.md +752 -0
- package/dist/host/guides/sqlite.md +330 -0
- package/dist/host/guides/sse.md +187 -0
- package/dist/host/guides/supervisor.md +4890 -0
- package/dist/host/guides/table.md +1556 -0
- package/dist/host/guides/template.md +280 -0
- package/dist/host/guides/terminal.md +1145 -0
- package/dist/host/guides/test.md +2969 -0
- package/dist/host/guides/timeout.md +252 -0
- package/dist/host/guides/tool.md +507 -0
- package/dist/host/guides/toolbox.md +1038 -0
- package/dist/host/guides/websocket.md +282 -0
- package/dist/host/guides/worker.md +615 -0
- package/dist/host/guides/workflow.md +1507 -0
- package/dist/host/guides/workspace.md +595 -0
- package/dist/host/manifest.json +1218 -10
- package/dist/host/tests/policy.test.ts +279 -2
- package/dist/host/tests/setupPolicy.ts +445 -6
- package/dist/src/core/index.cjs +38 -16
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +33 -9
- package/dist/src/core/index.d.ts +33 -9
- package/dist/src/core/index.js +37 -17
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1750 -1567
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +106 -24
- package/dist/src/server/index.d.ts +106 -24
- package/dist/src/server/index.js +1751 -1570
- package/dist/src/server/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# Tool
|
|
2
|
+
|
|
3
|
+
> The tool runtime for the `@orkestrel` line: a `Tool` binding an advertised JSON Schema
|
|
4
|
+
> definition to its handler, a `ToolManager` registry that advertises those definitions and
|
|
5
|
+
> executes calls with per-call error isolation, and the correlated `ToolCall` and `ToolResult`
|
|
6
|
+
> pair that travels between a caller and the registry.
|
|
7
|
+
|
|
8
|
+
A tool is a callable function described by a JSON Schema — a `name`, an optional description, an
|
|
9
|
+
optional parameter schema, and the handler that runs it. That is the whole idea: a tool is an API
|
|
10
|
+
call whose shape is data, so whoever calls it can discover it, present it, and invoke it without
|
|
11
|
+
knowing anything about the code behind it.
|
|
12
|
+
|
|
13
|
+
`Tool` and `ToolManager` carry the runtime. A `Tool` is inert — a definition plus a handler, with
|
|
14
|
+
no lifecycle. A configured contract validates arguments before its handler runs. A `ToolManager`
|
|
15
|
+
is the live surface a caller holds: it hands `definitions()` outward, takes a `ToolCall` back, and
|
|
16
|
+
answers with a `ToolResult`, a result rather than a throw for a call whose members are plain
|
|
17
|
+
values. Tools stay in the map by name in insertion order. Everything else in this module is the
|
|
18
|
+
plain data those two exchange.
|
|
19
|
+
|
|
20
|
+
**Anyone can call a tool.** Nothing here is model-specific — `tools.execute(call)` is an ordinary
|
|
21
|
+
async call returning an ordinary result, and plain application code may drive it directly. The
|
|
22
|
+
shape exists because callers that work from descriptions need the description and the handler to
|
|
23
|
+
travel together: an agent loop choosing which function to invoke, an MCP bridge exposing local
|
|
24
|
+
capability to a remote client, a backend dispatching a named operation. `@orkestrel/agent` and
|
|
25
|
+
`@orkestrel/mcp` are two such callers; ready-made tools ship in `@orkestrel/toolbox`.
|
|
26
|
+
|
|
27
|
+
**Mechanism only.** This runtime advertises, dispatches, and contains failure. It transports
|
|
28
|
+
nothing, authorizes no call, and ships no concrete tools. A `contract` derives the advertised
|
|
29
|
+
parameter schema and validates arguments; `parameters` alone remains descriptive. Caller identity
|
|
30
|
+
in the execution context is consumer-asserted and forwarded without verification. Each trust
|
|
31
|
+
decision belongs to the invoking consumer, to a policy layer, or to the tool itself. Progress
|
|
32
|
+
reporting belongs there too: it is a property of the invoking consumer's execution context, one
|
|
33
|
+
layer up — the `@orkestrel/mcp` package's execution context carries a progress reporter — never of
|
|
34
|
+
the tool contract itself.
|
|
35
|
+
|
|
36
|
+
Source: [`src/core`](../src/core). Published through `@orkestrel/tool`.
|
|
37
|
+
|
|
38
|
+
## Surface
|
|
39
|
+
|
|
40
|
+
### Types
|
|
41
|
+
|
|
42
|
+
The data shapes, from [`types.ts`](../src/core/types.ts). Every property is readonly, and an
|
|
43
|
+
optional field the caller did not supply is absent from the value. A `Shape` cell holds an
|
|
44
|
+
interface's data members as bare names in braces, `?` marking an optional member and `plus`
|
|
45
|
+
introducing its call-signature members, and a type alias's own type literal with a union's arms
|
|
46
|
+
escaped as `\|`. An extended interface's name comes before `plus`, with the members it adds after.
|
|
47
|
+
|
|
48
|
+
| Name | Kind | Shape | Summary |
|
|
49
|
+
| ---------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
50
|
+
| `ToolDefinition` | interface | `{ name, title?, description?, parameters?, annotations? }` | Describes a tool as advertised to a caller. |
|
|
51
|
+
| `ToolCall` | interface | `{ id, name, arguments }` | Describes one request to run a named tool. |
|
|
52
|
+
| `ToolSuccess` | interface | `Success<unknown> plus { id, name }` | Reports the successful outcome of executing a `ToolCall`. |
|
|
53
|
+
| `ToolFailure` | interface | `Failure<string> plus { id, name }` | Reports the failed outcome of executing a `ToolCall`. |
|
|
54
|
+
| `ToolOptions` | interface | `{ name, title?, description?, summary?, parameters?, contract?, annotations?, execute }` | Configures an executable tool. |
|
|
55
|
+
| `ToolInterface` | interface | `ToolDefinition plus { summary? } plus execute` | Represents an executable tool: its advertised definition plus its local handler. |
|
|
56
|
+
| `ToolManagerInterface` | interface | `{ count, emitter } plus add, tool, tools, definitions, execute, remove, clear, destroy` | Represents a registry of executable tools with per-call error isolation. |
|
|
57
|
+
| `ToolManagerEventMap` | type | `{ readonly add: readonly [tool: ToolInterface]; readonly remove: readonly [tool: ToolInterface]; readonly clear: readonly [tools: readonly ToolInterface[]] }` | Names the events a tool registry publishes. |
|
|
58
|
+
| `ToolManagerOptions` | interface | `{ on?, error? }` | Configures a tool registry's initial listeners and error handling. |
|
|
59
|
+
| `ToolResult` | type | `ToolSuccess \| ToolFailure` | Represents the outcome of executing a `ToolCall`. |
|
|
60
|
+
| `ToolContext` | interface | `{ signal, caller? }` | Carries the signal and consumer-asserted identity for an execution. |
|
|
61
|
+
| `ToolAnnotations` | interface | `{ pure?, untrusted?, consequential? }` | Describes the observable effects and content of a tool. |
|
|
62
|
+
| `ToolErrorCode` | type | `'SCHEMA' \| 'ARGUMENTS'` | Identifies a schema conflict or an argument validation failure. |
|
|
63
|
+
| `ToolErrorContext` | interface | `{ faults? }` | Carries the structured faults behind an argument validation failure. |
|
|
64
|
+
|
|
65
|
+
`ToolInterface` and `ToolManagerInterface` list every member they declare or inherit. The
|
|
66
|
+
call-signature members of each are documented under [Methods](#methods); the readonly `count` of
|
|
67
|
+
`ToolManagerInterface` reports how many tools are registered and is a Surface member with no
|
|
68
|
+
method row. Its readonly `emitter` publishes `add`, `remove`, and `clear` with the payloads
|
|
69
|
+
declared by `ToolManagerEventMap`. The `ToolManagerOptions` fields supply initial `on` hooks
|
|
70
|
+
and an `error` handler for listener throws.
|
|
71
|
+
|
|
72
|
+
### Validators
|
|
73
|
+
|
|
74
|
+
The call-envelope guard, from [`validators.ts`](../src/core/validators.ts). In a guard
|
|
75
|
+
table a `Shape` cell holds the type the guard narrows to.
|
|
76
|
+
|
|
77
|
+
| Name | Kind | Shape | Summary |
|
|
78
|
+
| ------------ | -------- | ---------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `isToolCall` | function | `ToolCall` | Determines whether an unknown value is structurally a `ToolCall`, staying total for malformed and adversarial input. |
|
|
80
|
+
|
|
81
|
+
### Helpers
|
|
82
|
+
|
|
83
|
+
The advertised-definition projection, from [`helpers.ts`](../src/core/helpers.ts).
|
|
84
|
+
|
|
85
|
+
| Name | Kind | Signature | Summary |
|
|
86
|
+
| ------------------ | -------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| `toolToDefinition` | function | `(tool: ToolInterface) => ToolDefinition` | Projects a tool onto the plain definition advertised to a caller, advertising an authored `summary` in place of the full description and carrying `parameters` and `annotations` by reference. |
|
|
88
|
+
|
|
89
|
+
### Factories
|
|
90
|
+
|
|
91
|
+
From [`factories.ts`](../src/core/factories.ts) — the constructor-free way to reach `Tool` and
|
|
92
|
+
`ToolManager`.
|
|
93
|
+
|
|
94
|
+
| Name | Kind | Signature | Summary |
|
|
95
|
+
| ------------------- | -------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `createTool` | function | `(options: ToolOptions) => ToolInterface` | Creates an executable tool bound to the supplied handler, returned as a `ToolInterface` so a call site holds the published contract rather than the `Tool` class. |
|
|
97
|
+
| `createToolManager` | function | `(options?: ToolManagerOptions) => ToolManagerInterface` | Creates an empty registry that advertises definitions and executes calls with per-call error isolation, returned as a `ToolManagerInterface` so a caller holds the published contract rather than the `ToolManager` class. |
|
|
98
|
+
|
|
99
|
+
### Classes
|
|
100
|
+
|
|
101
|
+
The implementing classes, from [`Tool.ts`](../src/core/tools/Tool.ts) and
|
|
102
|
+
[`ToolManager.ts`](../src/core/tools/ToolManager.ts) — each documented in full under its own
|
|
103
|
+
heading following this table.
|
|
104
|
+
|
|
105
|
+
| Name | Kind | Summary |
|
|
106
|
+
| ------------- | ----- | -------------------------------------------------------------------------------------- |
|
|
107
|
+
| `Tool` | class | Binds an executable tool definition to a handler. |
|
|
108
|
+
| `ToolManager` | class | Represents an insertion-ordered tool registry with per-call error isolation. |
|
|
109
|
+
| `ToolError` | class | Reports a schema conflict or argument validation failure with a machine-readable code. |
|
|
110
|
+
|
|
111
|
+
### `Tool`
|
|
112
|
+
|
|
113
|
+
The implementing class of `ToolInterface`, from [`Tool.ts`](../src/core/tools/Tool.ts). It
|
|
114
|
+
copies the fields it was given — omitting each optional one that was not supplied — and keeps
|
|
115
|
+
the handler in a private field, so a tool's advertised shape cannot drift from what it executes.
|
|
116
|
+
An explicit parameter schema and the execution context are forwarded by reference. Without a
|
|
117
|
+
contract, the argument record retains its identity too. A contract compiles at construction and
|
|
118
|
+
derives the parameter schema through the contract package's projection. The compiled contract
|
|
119
|
+
checks arguments before handler entry and supplies its parsed copy to the handler.
|
|
120
|
+
`Tool` deliberately does not catch: a handler that throws throws, and per-call isolation belongs
|
|
121
|
+
to the registry that dispatched it. See [`## Methods`](#methods) for its public call surface.
|
|
122
|
+
|
|
123
|
+
### `ToolManager`
|
|
124
|
+
|
|
125
|
+
The implementing class of `ToolManagerInterface`, from
|
|
126
|
+
[`ToolManager.ts`](../src/core/tools/ToolManager.ts). It stores tools in a name-keyed map and owns
|
|
127
|
+
an emitter for registry changes. Tools stay in insertion order, `tools()` and `definitions()` return fresh readonly arrays rather
|
|
128
|
+
than a view of that map, and every projection is computed on demand so a mutation can never
|
|
129
|
+
leave a stale copy behind. It is the only place a call can fail into a result instead of an
|
|
130
|
+
exception. See [`## Methods`](#methods) for its public call surface.
|
|
131
|
+
|
|
132
|
+
### `ToolError`
|
|
133
|
+
|
|
134
|
+
The error class from [`errors.ts`](../src/core/errors.ts) extends `Error`. Its constructor takes
|
|
135
|
+
`code`, `message`, and optional `context`. Direct tool execution throws an `ARGUMENTS` error with
|
|
136
|
+
the full fault report in `context.faults`; the manager contains it as a message. Use `isToolError`
|
|
137
|
+
to narrow a caught value. See [Contract validation and errors](#contract-validation-and-errors)
|
|
138
|
+
for an executed example.
|
|
139
|
+
|
|
140
|
+
| Name | Kind | Shape | Summary |
|
|
141
|
+
| ------------- | -------- | ----------- | ---------------------------------------------------------------------------- |
|
|
142
|
+
| `isToolError` | function | `ToolError` | Checks whether a value is a tool error, containing hostile prototype access. |
|
|
143
|
+
|
|
144
|
+
## Methods
|
|
145
|
+
|
|
146
|
+
The public call-signature members of each behavioral interface, one table per interface.
|
|
147
|
+
|
|
148
|
+
#### `ToolInterface`
|
|
149
|
+
|
|
150
|
+
| Method | Returns | Summary |
|
|
151
|
+
| --------- | ----------------------------- | --------------------------------------------------------------------------------- |
|
|
152
|
+
| `execute` | `Promise<unknown> \| unknown` | Runs the tool's handler with the caller-supplied arguments and execution context. |
|
|
153
|
+
|
|
154
|
+
#### `ToolManagerInterface`
|
|
155
|
+
|
|
156
|
+
| Method | Returns | Summary |
|
|
157
|
+
| ------------- | ---------------------------------------------- | -------------------------------------------------------- |
|
|
158
|
+
| `add` | `void` | Registers one tool. |
|
|
159
|
+
| `tool` | `ToolInterface \| undefined` | Finds one registered tool by name. |
|
|
160
|
+
| `tools` | `readonly ToolInterface[]` | Lists the registered tools in insertion order. |
|
|
161
|
+
| `definitions` | `readonly ToolDefinition[]` | Lists the definitions advertised to a caller. |
|
|
162
|
+
| `execute` | `Promise<ToolResult \| readonly ToolResult[]>` | Executes one call with error isolation. |
|
|
163
|
+
| `remove` | `boolean` | Removes one registered tool. |
|
|
164
|
+
| `clear` | `void` | Removes every registered tool. |
|
|
165
|
+
| `destroy` | `void` | Removes every tool and releases the emitter's listeners. |
|
|
166
|
+
|
|
167
|
+
`add`, `execute`, and `remove` each take one value or a readonly batch of them. A batch `add`
|
|
168
|
+
registers every tool, later entries winning over earlier ones with the same name; a batch
|
|
169
|
+
`execute` answers in input order with one result per call; a batch `remove` reports `true` only
|
|
170
|
+
when every named tool was present.
|
|
171
|
+
|
|
172
|
+
## Anatomy of a tool
|
|
173
|
+
|
|
174
|
+
A definition is the part a caller can read; the handler is the part it cannot. Declare both at
|
|
175
|
+
once:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { createTool } from '@orkestrel/tool'
|
|
179
|
+
|
|
180
|
+
const add = createTool({
|
|
181
|
+
name: 'add',
|
|
182
|
+
description: 'Add two numeric values and return their sum. Both operands are required.',
|
|
183
|
+
summary: 'Add two numbers.',
|
|
184
|
+
parameters: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
left: { type: 'number' },
|
|
188
|
+
right: { type: 'number' },
|
|
189
|
+
},
|
|
190
|
+
required: ['left', 'right'],
|
|
191
|
+
},
|
|
192
|
+
execute: (args) => Number(args.left) + Number(args.right),
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`new Tool({ … })` builds the same thing; reach for `createTool` where a call site must not name
|
|
197
|
+
a class.
|
|
198
|
+
|
|
199
|
+
An explicit `parameters` schema is descriptive runtime data, forwarded by reference. Declaring
|
|
200
|
+
`required` in that schema tells the caller what to send; it does not validate the payload. Use the
|
|
201
|
+
`contract` option when the runtime must validate arguments.
|
|
202
|
+
|
|
203
|
+
A handler receives a `Readonly<Record<string, unknown>>` and a required `ToolContext`.
|
|
204
|
+
Without a contract, that record is the original input; with a contract, it is the parsed value.
|
|
205
|
+
The context holds an `AbortSignal` and optional unverified caller identity. Handlers may omit
|
|
206
|
+
unused parameters from their declaration. Every invocation still supplies the arguments and the
|
|
207
|
+
context. A direct `tool.execute(args, context)` call must provide the context; the manager creates
|
|
208
|
+
one when its caller omits it. Handlers may return synchronously or asynchronously.
|
|
209
|
+
|
|
210
|
+
When one was authored, `definitions()` projects the tool's `summary` as `description`, advertising
|
|
211
|
+
it in place of the full description. The full text stays on the tool for direct lookup through
|
|
212
|
+
`tools.tool('add')?.description`.
|
|
213
|
+
|
|
214
|
+
## The registry
|
|
215
|
+
|
|
216
|
+
A registry is a working set, not a global. Build one per caller, fill it with the tools that
|
|
217
|
+
caller is allowed to reach, and hand out its definitions:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { Tool, createToolManager } from '@orkestrel/tool'
|
|
221
|
+
|
|
222
|
+
const tools = createToolManager()
|
|
223
|
+
tools.add(add) // the tool defined earlier
|
|
224
|
+
tools.add([
|
|
225
|
+
new Tool({ name: 'echo', execute: (args) => args.value }),
|
|
226
|
+
new Tool({ name: 'now', description: 'Current epoch milliseconds.', execute: () => Date.now() }),
|
|
227
|
+
])
|
|
228
|
+
|
|
229
|
+
tools.count // 3
|
|
230
|
+
tools.tool('add') // the exact instance that was registered, or undefined
|
|
231
|
+
tools.tools() // a fresh readonly array, in insertion order
|
|
232
|
+
tools.definitions() // the same order, projected to plain ToolDefinition values
|
|
233
|
+
|
|
234
|
+
tools.remove('echo') // true — the tool was present
|
|
235
|
+
tools.remove(['now', 'ghost']) // false — 'ghost' was never registered, so not every name succeeded
|
|
236
|
+
tools.clear() // back to empty
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Order is insertion order, and adding a name that already exists replaces the stored tool without
|
|
240
|
+
moving it — the sequence a caller sees stays stable while a tool behind a name is swapped.
|
|
241
|
+
Remove a name and add it again and it lands at the end, because the name is genuinely new to the
|
|
242
|
+
map. In a batch, later entries win over earlier ones with the same name.
|
|
243
|
+
|
|
244
|
+
`definitions()` projects fresh plain objects on every call: `name`, followed by present `title`,
|
|
245
|
+
`description`, `parameters`, and `annotations` fields. A summary replaces the advertised
|
|
246
|
+
description. The schema and annotations retain their original identities. Nothing that arrives on
|
|
247
|
+
a definition is a live handle on the registry — advertising cannot be used to reach the handlers.
|
|
248
|
+
|
|
249
|
+
## Calls and results
|
|
250
|
+
|
|
251
|
+
A call arrives as unstructured input from somewhere else, so check the envelope before trusting
|
|
252
|
+
it, then execute:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { isToolCall } from '@orkestrel/tool'
|
|
256
|
+
|
|
257
|
+
tools.add(add) // restores the tool removed by the registry example
|
|
258
|
+
|
|
259
|
+
const incoming: unknown = {
|
|
260
|
+
id: 'call-1',
|
|
261
|
+
name: 'add',
|
|
262
|
+
arguments: { left: 2, right: 3 },
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (isToolCall(incoming)) {
|
|
266
|
+
const result = await tools.execute(incoming)
|
|
267
|
+
if (result.success) {
|
|
268
|
+
result.value // 5
|
|
269
|
+
} else {
|
|
270
|
+
result.error // the failure message
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const batch = await tools.execute([
|
|
275
|
+
{ id: '1', name: 'add', arguments: { left: 2, right: 3 } }, // → { id: '1', name: 'add', success: true, value: 5 }
|
|
276
|
+
{ id: '2', name: 'ghost', arguments: {} }, // → { id: '2', name: 'ghost', success: false, error: 'tool not found: ghost' }
|
|
277
|
+
])
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`isToolCall` validates the envelope only: the `id`, the `name`, and that `arguments` is a plain
|
|
281
|
+
record. The guard ignores extra fields without reading them. A call carries no execution context.
|
|
282
|
+
The registered tool's contract, when configured, validates the arguments during execution.
|
|
283
|
+
|
|
284
|
+
Execution context travels as a separate argument. This package adds neither the signal nor caller
|
|
285
|
+
identity to definitions, schemas, calls, or results. A handler can explicitly return a context
|
|
286
|
+
member as its own value. The context and caller identity reach the handler unchanged.
|
|
287
|
+
|
|
288
|
+
Execution always resolves for a call whose members are plain values; a call whose `id` or `name`
|
|
289
|
+
accessor throws when read makes `execute` reject, because no correlated result can be built
|
|
290
|
+
without them. An unknown name becomes `tool not found: <name>`; a synchronous throw and an
|
|
291
|
+
asynchronous rejection are both contained; an `Error` contributes its `message`, and any other
|
|
292
|
+
thrown value is converted with `String`; a value whose conversion itself throws — a hostile
|
|
293
|
+
`toString`, a throwing `message` getter, a null-prototype object — becomes the fixed message
|
|
294
|
+
`Unknown thrown value`. Success and failure never mix in one result: a successful call carries
|
|
295
|
+
`value` even when that value is `undefined`, `null`, `0`, `''`, or `false`, and a failed call
|
|
296
|
+
carries `error`. Narrow on `success` to distinguish the two; a present success value is not
|
|
297
|
+
necessarily meaningful or truthy.
|
|
298
|
+
|
|
299
|
+
An in-process caller needing a typed error can call `tools.tool(name)`, then
|
|
300
|
+
`tool.execute(args, context)` inside its own `try`/`catch`.
|
|
301
|
+
|
|
302
|
+
A batch is dispatched concurrently and answered in input order, with each call whose members are
|
|
303
|
+
plain values isolated from its siblings — a handler failure never voids the batch, a call whose
|
|
304
|
+
`id` or `name` accessor throws when read rejects it, and duplicate ids stay distinct positional
|
|
305
|
+
calls rather than collapsing into one. That isolation is what lets a caller feed every result back
|
|
306
|
+
to whatever produced the calls and let it react to the failures itself.
|
|
307
|
+
|
|
308
|
+
The sections that follow are independent examples.
|
|
309
|
+
|
|
310
|
+
## Execution context
|
|
311
|
+
|
|
312
|
+
For versions from 0.0.15, `caller` lives on `ToolContext` instead of `ToolCall`. A handler whose
|
|
313
|
+
second parameter is annotated `unknown` still compiles and receives the context object rather
|
|
314
|
+
than the caller value. Update such a handler to read `context.caller`.
|
|
315
|
+
|
|
316
|
+
Pass a context when the caller owns cancellation or carries an asserted identity:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import type { ToolContext } from '@orkestrel/tool'
|
|
320
|
+
import { createTool, createToolManager } from '@orkestrel/tool'
|
|
321
|
+
|
|
322
|
+
const controller = new AbortController()
|
|
323
|
+
const context: ToolContext = { signal: controller.signal, caller: { subject: 'reader' } }
|
|
324
|
+
const tools = createToolManager()
|
|
325
|
+
tools.add(createTool({ name: 'signal', execute: (_args, execution) => execution.signal.aborted }))
|
|
326
|
+
const call = { id: 'signal-1', name: 'signal', arguments: {} }
|
|
327
|
+
const result = await tools.execute(call, context)
|
|
328
|
+
result // { id: 'signal-1', name: 'signal', success: true, value: false }
|
|
329
|
+
controller.abort('request ended')
|
|
330
|
+
const aborted = await tools.execute(call, context)
|
|
331
|
+
aborted // { id: 'signal-1', name: 'signal', success: false, error: 'request ended' }
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The manager creates a non-aborted signal for each execution that omits a context. A batch shares
|
|
335
|
+
one context, whether supplied or created. Before entering each registered handler, the manager
|
|
336
|
+
checks the signal. A batch dispatches every call in one synchronous pass, so an abort raised after
|
|
337
|
+
dispatch reaches only handlers that observe the signal. A synchronous abort inside the dispatch
|
|
338
|
+
pass prevents later handler entry. An already-aborted signal produces a failure with
|
|
339
|
+
`String(signal.reason)`, or `aborted` if the reason is `undefined`. An unknown tool still produces
|
|
340
|
+
its not-found failure.
|
|
341
|
+
After handler entry, the handler must observe the signal and stop its own work. The manager awaits
|
|
342
|
+
that handler and contains its throws as usual; an abort does not force a running handler to settle.
|
|
343
|
+
|
|
344
|
+
## Contract validation and errors
|
|
345
|
+
|
|
346
|
+
A contract compiles at construction. Its schema projects to `parameters` through
|
|
347
|
+
`schemaToParameters(createContract(shape).schema)` from `@orkestrel/contract`; an undefined
|
|
348
|
+
projection leaves parameters absent. Supplying `contract` and `parameters` together throws a
|
|
349
|
+
`ToolError` with code `SCHEMA`.
|
|
350
|
+
|
|
351
|
+
The contract's `explain` method reports parse faults before the handler runs. It accepts coercible
|
|
352
|
+
values, such as a numeric string for a number. After a clean report, `Tool.execute` forwards
|
|
353
|
+
`contract.parse(args)`: the owned, normalized copy in the schema's types, with undeclared keys
|
|
354
|
+
dropped. A handler cannot rely on undeclared input keys being present. Without a contract, the
|
|
355
|
+
raw argument record is forwarded unchanged. A parse fault throws `ToolError` with code
|
|
356
|
+
`ARGUMENTS`. The message names the first fault's path and reason, then its expected and received
|
|
357
|
+
values when the fault carries them; a constraint fault also names its constraint and limit when
|
|
358
|
+
present. Array paths join with `.`; string paths stay unchanged. A root array path is empty, so
|
|
359
|
+
its message starts with `: `. The error's `context.faults` holds the full report. A `variant` fault
|
|
360
|
+
carries `variants`, appended as `; variants <n>`; a `oneOf` fault carries `matched`, appended as
|
|
361
|
+
`; matched <n>`. A missing field carries expected alone. Contract construction errors from the
|
|
362
|
+
dependency propagate unchanged. If parsing returns a value that isn't a record after a clean
|
|
363
|
+
explanation, execution throws `ToolError` with code `ARGUMENTS` and message
|
|
364
|
+
`Arguments did not parse`, without `context`, before handler entry.
|
|
365
|
+
|
|
366
|
+
Use the guard to narrow an error at the direct execution boundary:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import type { ToolErrorCode, ToolErrorContext } from '@orkestrel/tool'
|
|
370
|
+
import { numberShape, objectShape } from '@orkestrel/contract'
|
|
371
|
+
import { ToolError, createTool, isToolError } from '@orkestrel/tool'
|
|
372
|
+
|
|
373
|
+
const tool = createTool({
|
|
374
|
+
name: 'amount',
|
|
375
|
+
contract: objectShape({ amount: numberShape() }),
|
|
376
|
+
execute: (args) => args.amount,
|
|
377
|
+
})
|
|
378
|
+
const context = { signal: new AbortController().signal }
|
|
379
|
+
tool.execute({ amount: 3 }, context) // 3
|
|
380
|
+
try {
|
|
381
|
+
tool.execute({ amount: 'invalid' }, context)
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (!isToolError(error)) throw error
|
|
384
|
+
const code: ToolErrorCode = error.code
|
|
385
|
+
code // 'ARGUMENTS'
|
|
386
|
+
const details: ToolErrorContext | undefined = error.context
|
|
387
|
+
details?.faults?.[0]?.reason // 'type'
|
|
388
|
+
error.message // 'amount: type; expected number; received "invalid"'
|
|
389
|
+
}
|
|
390
|
+
const conflict = new ToolError('SCHEMA', 'Choose contract or parameters')
|
|
391
|
+
conflict.code // 'SCHEMA'
|
|
392
|
+
isToolError(conflict) // true
|
|
393
|
+
isToolError(new Error('Unrelated')) // false
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The manager contains this same argument refusal as a `ToolFailure`; it carries the message rather
|
|
397
|
+
than the error instance or fault report. `ToolError` extends `Error`, exposes its readonly `code`
|
|
398
|
+
and optional readonly `context`, and inherits the standard error methods.
|
|
399
|
+
|
|
400
|
+
## Advertising title and annotations
|
|
401
|
+
|
|
402
|
+
A title supplies display text. Annotations describe observable effects and content: `pure` reports
|
|
403
|
+
no state changes the caller can observe, `untrusted` reports that the result can carry content the
|
|
404
|
+
tool did not author, and `consequential` reports an effect the caller must confirm. These are
|
|
405
|
+
claims by the tool author; this runtime neither verifies them nor enforces confirmation.
|
|
406
|
+
|
|
407
|
+
Project the advertising fields while keeping the detailed description on the tool:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import type { ToolAnnotations } from '@orkestrel/tool'
|
|
411
|
+
import { createTool, toolToDefinition } from '@orkestrel/tool'
|
|
412
|
+
|
|
413
|
+
const annotations: ToolAnnotations = { pure: true, untrusted: false, consequential: false }
|
|
414
|
+
const tool = createTool({
|
|
415
|
+
name: 'echo',
|
|
416
|
+
title: 'Echo',
|
|
417
|
+
description: 'Return the supplied value unchanged.',
|
|
418
|
+
summary: 'Echo a value.',
|
|
419
|
+
annotations,
|
|
420
|
+
execute: (args) => args.value,
|
|
421
|
+
})
|
|
422
|
+
const definition = toolToDefinition(tool)
|
|
423
|
+
definition.title // 'Echo'
|
|
424
|
+
definition.description // 'Echo a value.'
|
|
425
|
+
definition.annotations === annotations // true
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Patterns
|
|
429
|
+
|
|
430
|
+
### Observe registry changes
|
|
431
|
+
|
|
432
|
+
Subscribe through the `on` option or `tools.emitter.on`. Each event describes the registry at the
|
|
433
|
+
moment it is published. A listener that mutates the registry re-enters synchronously; its own
|
|
434
|
+
events publish before the outer call resumes.
|
|
435
|
+
|
|
436
|
+
An addition publishes `add` with the map holding that exact tool. A replacement keeps its
|
|
437
|
+
registration position and publishes `remove` with the previous instance while the replacement
|
|
438
|
+
is already installed. A listener must not read absence from the map to confirm that removal.
|
|
439
|
+
After the removal listeners return, `add` publishes only if the map still holds that exact
|
|
440
|
+
replacement. Removing a present name publishes `remove` after deletion; a missing name publishes
|
|
441
|
+
nothing. Batches apply their operations in argument order.
|
|
442
|
+
Each `clear` call publishes one `clear` with the removed tools in registration order, including
|
|
443
|
+
an empty array when the registry was empty. Execution publishes no registry events.
|
|
444
|
+
|
|
445
|
+
Collect event names while registering, replacing, removing, and clearing a tool:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import { createTool, createToolManager } from '@orkestrel/tool'
|
|
449
|
+
|
|
450
|
+
const events: string[] = []
|
|
451
|
+
const tools = createToolManager({
|
|
452
|
+
on: {
|
|
453
|
+
add: () => events.push('add'),
|
|
454
|
+
remove: () => events.push('remove'),
|
|
455
|
+
clear: () => events.push('clear'),
|
|
456
|
+
},
|
|
457
|
+
})
|
|
458
|
+
tools.add(createTool({ name: 'echo', execute: (args) => args.value }))
|
|
459
|
+
tools.add(createTool({ name: 'echo', execute: () => 'replacement' }))
|
|
460
|
+
tools.remove('echo')
|
|
461
|
+
tools.clear()
|
|
462
|
+
events // ['add', 'remove', 'add', 'remove', 'clear']
|
|
463
|
+
tools.destroy()
|
|
464
|
+
tools.emitter.destroyed // true
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Listeners run synchronously. A listener throw reaches the optional `error` handler as
|
|
468
|
+
`(error, event)` and does not prevent sibling listeners. Without an error handler, the emitter
|
|
469
|
+
swallows listener throws. Destruction clears the registry while listeners remain attached,
|
|
470
|
+
destroys the emitter, then empties the map again without publishing. It returns with an empty
|
|
471
|
+
registry even if a `clear` listener added a tool. An emission already underway delivers to its
|
|
472
|
+
remaining snapshotted listeners, even when a listener destroys the registry before its siblings
|
|
473
|
+
run. A destroyed registry publishes nothing; later additions still update its tool map, and
|
|
474
|
+
later subscriptions do nothing.
|
|
475
|
+
|
|
476
|
+
## Callers
|
|
477
|
+
|
|
478
|
+
The registry's two-sided shape — `definitions()` out, `execute()` back — is all a caller needs,
|
|
479
|
+
and it is the same shape whatever sits on the other side.
|
|
480
|
+
|
|
481
|
+
An agent loop advertises `definitions()` to a model, receives tool calls in the model's reply,
|
|
482
|
+
runs them through `execute`, and appends each `ToolResult` to the conversation; because a failure
|
|
483
|
+
comes back as an error result, the model sees what went wrong and can try something else instead
|
|
484
|
+
of the run collapsing. An MCP bridge maps the same definitions onto the protocol's tool listing
|
|
485
|
+
and routes each invocation to `execute`. Plain code skips the discovery half entirely and calls
|
|
486
|
+
`execute` with a call it wrote itself — a scheduled job, an HTTP handler dispatching a named
|
|
487
|
+
operation, a test.
|
|
488
|
+
|
|
489
|
+
Concrete tools are not this package's business. `@orkestrel/toolbox` ships ready-made ones, and
|
|
490
|
+
anything a `ToolInterface` can describe — a local computation, a database query, a remote API —
|
|
491
|
+
registers here unchanged.
|
|
492
|
+
|
|
493
|
+
## Tests
|
|
494
|
+
|
|
495
|
+
- [`guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/core` bijection, the `ToolInterface` ↔ `Tool` and `ToolManagerInterface` ↔ `ToolManager` method bijections, and the equality gate: every `Summary` cell against its declaration's description paragraph, the titled `Anatomy of a tool` fence against the `@example` block of that title (pinned so the titled pair cannot be retired silently), and the README pitch against this guide's tagline. It also runs the flagship fences, including `Observe registry changes`, and asserts the values their comments claim against byte-equal transcriptions.
|
|
496
|
+
- [`Tool.test.ts`](../tests/src/core/tools/Tool.test.ts) — definition binding, optional-field omission, argument and context identity, contract validation, error diagnostics, return values, and direct error propagation.
|
|
497
|
+
- [`ToolManager.test.ts`](../tests/src/core/tools/ToolManager.test.ts) — insertion order, overwrite and removal lifecycle, definition projection, cancellation, context sharing, and isolated single and batch execution. Event proofs cover registration before `add`, ordered batch additions, the installed replacement during `remove`, `remove` before replacement `add`, synchronous replacement re-entry, a third instance a removal listener installs during a replacement, deletion before `remove`, silent missing names, ordered batch removals, populated and empty `clear` snapshots by identity, emitter destruction after clearing, an empty registry after teardown listeners re-add a tool, sibling delivery during mid-emission destruction, silent additions after destruction, and execution without registry events.
|
|
498
|
+
- [`factories.test.ts`](../tests/src/core/factories.test.ts) — factory construction, working instances, initial registry hooks in publication order, sibling listener isolation, and listener-error forwarding.
|
|
499
|
+
- [`helpers.test.ts`](../tests/src/core/helpers.test.ts) — definition projection: summary preference, omitted optional keys, projected key order, schema identity, title and annotations forwarding, and a fresh object per call.
|
|
500
|
+
- [`validators.test.ts`](../tests/src/core/validators.test.ts) — tool-call envelope boundaries: incomplete calls, wrong field types, and non-record arguments.
|
|
501
|
+
- [`errors.test.ts`](../tests/src/core/errors.test.ts) — `isToolError` recognition, unrelated-value rejection, and hostile prototype containment.
|
|
502
|
+
|
|
503
|
+
## See also
|
|
504
|
+
|
|
505
|
+
- [`README.md`](README.md) — the guides index.
|
|
506
|
+
- [`contract.md`](contract.md) — the dependency mirror for `@orkestrel/contract`, whose total guards back `isToolCall` and the registry's overload narrowing.
|
|
507
|
+
- [`AGENTS.md`](../AGENTS.md) — the repository's coding and documentation contract.
|