@orkestrel/scaffold 0.0.67 → 0.0.68

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.
Files changed (74) hide show
  1. package/dist/bin/main.js +67 -44
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/templates/brief.md +9 -0
  4. package/dist/host/claude/agents/orkestrel.md +4 -4
  5. package/dist/host/claude/rules/names.md +15 -0
  6. package/dist/host/claude/rules/tests.md +33 -4
  7. package/dist/host/claude/rules/workspace.md +14 -2
  8. package/dist/host/dotfiles/prettierignore +3 -0
  9. package/dist/host/guides/README.md +65 -0
  10. package/dist/host/guides/abort.md +169 -0
  11. package/dist/host/guides/agent.md +1509 -0
  12. package/dist/host/guides/brief.md +1266 -0
  13. package/dist/host/guides/browser.md +2200 -0
  14. package/dist/host/guides/budget.md +196 -0
  15. package/dist/host/guides/codec.md +519 -0
  16. package/dist/host/guides/console.md +785 -0
  17. package/dist/host/guides/contract.md +1193 -0
  18. package/dist/host/guides/csv.md +541 -0
  19. package/dist/host/guides/database.md +2518 -0
  20. package/dist/host/guides/emitter.md +233 -0
  21. package/dist/host/guides/form.md +1791 -0
  22. package/dist/host/guides/html.md +717 -0
  23. package/dist/host/guides/indexeddb.md +505 -0
  24. package/dist/host/guides/interpret.md +1029 -0
  25. package/dist/host/guides/lsp.md +515 -0
  26. package/dist/host/guides/markdown.md +964 -0
  27. package/dist/host/guides/mcp.md +5554 -0
  28. package/dist/host/guides/middleware.md +927 -0
  29. package/dist/host/guides/msg.md +440 -0
  30. package/dist/host/guides/ndjson.md +120 -0
  31. package/dist/host/guides/ollama.md +380 -0
  32. package/dist/host/guides/pool.md +280 -0
  33. package/dist/host/guides/probe.md +1210 -0
  34. package/dist/host/guides/process.md +1620 -0
  35. package/dist/host/guides/program.md +1110 -0
  36. package/dist/host/guides/qualifier.md +854 -0
  37. package/dist/host/guides/queue.md +370 -0
  38. package/dist/host/guides/rater.md +330 -0
  39. package/dist/host/guides/reason.md +1122 -0
  40. package/dist/host/guides/relation.md +373 -0
  41. package/dist/host/guides/router.md +753 -0
  42. package/dist/host/guides/scaffold.md +192 -31
  43. package/dist/host/guides/sea.md +383 -0
  44. package/dist/host/guides/server.md +752 -0
  45. package/dist/host/guides/sqlite.md +330 -0
  46. package/dist/host/guides/sse.md +187 -0
  47. package/dist/host/guides/supervisor.md +4890 -0
  48. package/dist/host/guides/table.md +1556 -0
  49. package/dist/host/guides/template.md +280 -0
  50. package/dist/host/guides/terminal.md +1145 -0
  51. package/dist/host/guides/test.md +2969 -0
  52. package/dist/host/guides/timeout.md +252 -0
  53. package/dist/host/guides/tool.md +311 -0
  54. package/dist/host/guides/toolbox.md +1038 -0
  55. package/dist/host/guides/websocket.md +282 -0
  56. package/dist/host/guides/worker.md +615 -0
  57. package/dist/host/guides/workflow.md +1507 -0
  58. package/dist/host/guides/workspace.md +595 -0
  59. package/dist/host/manifest.json +1218 -10
  60. package/dist/host/tests/policy.test.ts +279 -2
  61. package/dist/host/tests/setupPolicy.ts +437 -6
  62. package/dist/src/core/index.cjs +38 -16
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +33 -9
  65. package/dist/src/core/index.d.ts +33 -9
  66. package/dist/src/core/index.js +37 -17
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +1750 -1567
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +106 -24
  71. package/dist/src/server/index.d.ts +106 -24
  72. package/dist/src/server/index.js +1751 -1570
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +3 -3
@@ -0,0 +1,252 @@
1
+ # Timeout
2
+
3
+ > The time-bound half of the substrate's time-and-cancellation pair: a
4
+ > controllable `setTimeout` wrapper carrying a trace `id` and a deadline `ms`,
5
+ > whose native `AbortSignal` aborts on expiry.
6
+
7
+ Arm the deadline with `start()`, then race its `signal` against work to bound
8
+ how long that work may run; `clear()` cancels the deadline without firing it,
9
+ and a `start()` after an expiry swaps in a fresh signal, so one handle serves a
10
+ sequence of deadlines without re-construction. The package is deliberately
11
+ thin: not a scheduler, not a debounce, not a retry policy — one `setTimeout`
12
+ made re-armable, clearable, and parent-linkable. The native signal is the
13
+ complete observation surface, so there is no separate event map. Source:
14
+ [`src/core`](../src/core). Surfaced through the `@src/core` barrel.
15
+
16
+ ## Surface
17
+
18
+ Create a deadline handle, arm it, and hand its `signal` to deadline-aware
19
+ work — call `clear()` on the deadline if the work finishes first:
20
+
21
+ ```ts
22
+ import { createTimeout } from '@orkestrel/timeout'
23
+
24
+ const timeout = createTimeout({ ms: 5_000 })
25
+ timeout.start()
26
+
27
+ // `signal` aborts on expiry — pass it anywhere a native AbortSignal is accepted:
28
+ const response = await fetch(url, { signal: timeout.signal })
29
+
30
+ timeout.clear() // work finished first — cancel the deadline
31
+ ```
32
+
33
+ Construction is a strict JavaScript boundary. Options must be a plain readable
34
+ record; a defined `id` must be a string; `ms` must be an integer in the
35
+ inclusive range from `0` through `MAX_TIMEOUT_MS`; and a defined parent
36
+ `signal` must be a genuine native `AbortSignal`. Invalid input throws
37
+ `ContractError` from `@orkestrel/contract`: malformed or unreadable options use
38
+ code `bound`, while invalid `id`, `ms`, and `signal` values use `literal`,
39
+ `range`, and `placement`, respectively. Each error carries safe `path`, `limit`,
40
+ and `received` context. The package does not re-export `ContractError`.
41
+
42
+ An optional parent signal clears the timeout rather than expiring it if it
43
+ aborts before the deadline. An optional `id` labels the handle for tracing and
44
+ defaults to a random UUID.
45
+
46
+ ### Factories
47
+
48
+ | API | Kind | Summary |
49
+ | --------------- | -------- | ------------------------------------------------------------------------------------------------- |
50
+ | `createTimeout` | function | Creates a deadline handle from validated `TimeoutOptions` and returns it as a `TimeoutInterface`. |
51
+
52
+ ### Classes
53
+
54
+ | API | Kind | Summary |
55
+ | --------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `Timeout` | class | Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one owned `AbortController` whose signal aborts when the deadline expires. |
57
+
58
+ ### Constants
59
+
60
+ A `Shape` cell holds the constant's declared type.
61
+
62
+ | API | Kind | Shape | Summary |
63
+ | ---------------- | ----- | -------- | ------------------------------------------------------------------------------------- |
64
+ | `MAX_TIMEOUT_MS` | const | `number` | Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds. |
65
+
66
+ ### Validators
67
+
68
+ In a guard table a `Shape` cell holds the type the guard narrows to.
69
+
70
+ | API | Kind | Shape | Summary |
71
+ | ------------------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
72
+ | `isTimeoutDuration` | function | `number` | Determines whether a value is an integer in the inclusive range from `0` through `MAX_TIMEOUT_MS`, staying total for every input. |
73
+ | `isTimeoutSignal` | function | `AbortSignal` | Determines whether a value is a genuine native `AbortSignal`, staying total for a structural spoof and for a hostile or revoked proxy. |
74
+
75
+ ### Helpers
76
+
77
+ | API | Kind | Summary |
78
+ | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------- |
79
+ | `validateTimeoutOptions` | function | Validates once-read timeout construction options and returns a fresh normalized copy omitting absent optional keys. |
80
+
81
+ ### Types
82
+
83
+ A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
84
+
85
+ | Type | Kind | Shape | Summary |
86
+ | ------------------ | --------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------- |
87
+ | `TimeoutOptions` | interface | `{ id?, ms, signal? }` | Represents the options `createTimeout` and the `Timeout` constructor accept. |
88
+ | `TimeoutInterface` | interface | `{ id, ms, signal, expired } plus start, clear` | Represents a controllable deadline exposing a native `AbortSignal` that aborts on expiry. |
89
+
90
+ The `id`, `ms`, `signal`, and `expired` members of `TimeoutInterface` are
91
+ `readonly` data members (Shape cell, earlier) — its call-signature methods are
92
+ documented under [Methods](#methods). `expired` derives directly from the
93
+ owned signal's `aborted` state rather than storing a duplicate lifecycle flag.
94
+
95
+ ## Methods
96
+
97
+ The public methods of `TimeoutInterface` — every call-signature member listed
98
+ (its `readonly` members `id` / `ms` / `signal` / `expired` have no method row).
99
+ `Timeout` implements the interface exactly, so this doubles as the class's
100
+ instance-method surface (AGENTS.md, Documentation contract).
101
+
102
+ #### `TimeoutInterface`
103
+
104
+ The call-signature members, each with the type it returns:
105
+
106
+ | Method | Returns | Summary |
107
+ | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
108
+ | `start` | `void` | Arms or re-arms the deadline for `ms`, installing a fresh `signal` when the current one has already aborted. |
109
+ | `clear` | `void` | Cancels an armed deadline without aborting its `signal`, and resets expiry by installing a fresh signal when the current one has already aborted. |
110
+
111
+ ## Contract
112
+
113
+ These invariants hold across `src/core` ↔ `timeout.md`:
114
+
115
+ 1. **DOC ↔ SOURCE bijection.** Every `function` / `class` / `const` /
116
+ `interface` row in the `## Surface` tables is a real export of the timeout
117
+ source, and every export appears as a Surface row — exhaustive, both
118
+ directions (AGENTS.md, Documentation contract).
119
+ 2. **Strict construction boundary.** `validateTimeoutOptions` requires a plain
120
+ readable record, reads `id`, `ms`, and `signal` exactly once inside a
121
+ contained boundary, validates them, and returns a fresh normalized copy that
122
+ omits absent optional keys. Defined identifiers must be strings, durations
123
+ must be integers in inclusive `[0, MAX_TIMEOUT_MS]`, and defined parent
124
+ signals must pass the native brand check. `Timeout` calls this helper before
125
+ allocating its controller or listener. Negative zero and zero are valid;
126
+ zero intentionally expires on the next turn. Invalid inputs throw the coded
127
+ `ContractError` taxonomy described under Surface.
128
+ 3. **Deadline signal and derived expiry.** The exposed `signal` fires (aborts)
129
+ on expiry. `expired` derives from that owned signal's `aborted` state, so
130
+ `expired` and `aborted` cannot drift apart.
131
+ 4. **Signal identity swaps only on a real expiry.** A cleared-but-never-fired
132
+ timeout keeps its original `signal` (not aborted); the identity is only
133
+ swapped for a fresh, non-aborted controller after the current controller has
134
+ fired — whether that swap happens inside `clear()` or at the next `start()`.
135
+ 5. **Parent linking clears, never expires.** A parent `options.signal` abort
136
+ clears the timeout — it does not expire the timeout, abort the timeout's own
137
+ signal, or forward the parent reason. The parent listener is attached only
138
+ while a timer is armed (added on `start()`, removed on expiry or `clear()`);
139
+ once the parent has aborted, a later `start()` is a no-op.
140
+ 6. **Identifiers are strict.** Omitted `id` values generate a random UUID and an
141
+ empty string is retained, but every other defined non-string value throws a
142
+ `literal`-coded `ContractError` rather than being coerced or replaced.
143
+ 7. **Native observation.** Consumers observe expiry through the complete native
144
+ `AbortSignal`; `Timeout` adds no `Emitter` or parallel event system.
145
+
146
+ ## Patterns
147
+
148
+ ### Race work against a deadline
149
+
150
+ Builds a deadline handle, arms it, and clears it in a `finally` once the race resolves:
151
+
152
+ ```ts
153
+ import { createTimeout } from '@orkestrel/timeout'
154
+
155
+ async function fetchWithDeadline(url: string, ms: number): Promise<Response> {
156
+ const timeout = createTimeout({ ms })
157
+ timeout.start()
158
+
159
+ try {
160
+ return await fetch(url, { signal: timeout.signal })
161
+ } finally {
162
+ timeout.clear() // cancels the still-armed deadline when the fetch won the race
163
+ }
164
+ }
165
+ ```
166
+
167
+ ### Link a parent signal
168
+
169
+ A parent `AbortSignal` clears the deadline instead of letting it expire — so
170
+ an outer cancellation (a request abort, a shutdown signal) short-circuits the
171
+ timer cleanly without aborting the timeout's own signal:
172
+
173
+ ```ts
174
+ import { createTimeout } from '@orkestrel/timeout'
175
+
176
+ function withDeadline(parent: AbortSignal, ms: number) {
177
+ const timeout = createTimeout({ id: 'request-deadline', ms, signal: parent })
178
+ timeout.start()
179
+
180
+ timeout.signal.addEventListener(
181
+ 'abort',
182
+ () => {
183
+ if (timeout.expired) giveUp() // only a real timeout expiry reaches this listener
184
+ },
185
+ { once: true },
186
+ )
187
+
188
+ return timeout
189
+ }
190
+ ```
191
+
192
+ ### Reuse a handle across deadlines
193
+
194
+ Clears an armed deadline before it fires, then arms the same handle again for a fresh window:
195
+
196
+ ```ts
197
+ import { createTimeout } from '@orkestrel/timeout'
198
+
199
+ const timeout = createTimeout({ ms: 100 })
200
+
201
+ timeout.start()
202
+ timeout.clear() // cancels before firing — expired stays false
203
+
204
+ timeout.start() // re-armed; a fresh deadline window begins
205
+ ```
206
+
207
+ ### Practices
208
+
209
+ - **Race, don't poll** — attach a listener to `signal` (or pass it straight to
210
+ an API that accepts an `AbortSignal`, for example `fetch`) rather than
211
+ polling `expired`.
212
+ - **`clear()` is always safe to call** — clearing an idle or already-cleared
213
+ handle is a no-op; after expiry it installs a fresh non-aborted signal. Call
214
+ it unconditionally in a `finally`.
215
+ - **Keep parent cancellation distinct** — a parent abort clears the timer but
216
+ does not abort the timeout signal or forward the parent reason.
217
+ - **Reuse, don't reconstruct** — call `start()` again on the same handle for a
218
+ new deadline window instead of constructing a fresh `Timeout`.
219
+
220
+ ## Tests
221
+
222
+ - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔
223
+ `src/core` bijection over value and type exports, the `TimeoutInterface` ↔
224
+ `Timeout` method bijection, and the equality gate: every `Summary` cell
225
+ against its declaration's description paragraph, the titled fence against the
226
+ `@example` block of that title (pinned so the titled pair cannot be retired
227
+ silently), and the README pitch against this guide's tagline. It also runs the
228
+ flagship fences and asserts the values their comments claim.
229
+ - [`tests/src/core/Timeout.test.ts`](../tests/src/core/Timeout.test.ts) —
230
+ public-constructor integration, real expiry / clear / replacement / churn
231
+ behavior, signal identity and derived expiry, and the intentional parent-clear
232
+ lifecycle.
233
+ - [`tests/src/core/helpers.test.ts`](../tests/src/core/helpers.test.ts) — fresh
234
+ normalized copies, omitted optional keys, exactly-once property reads, hostile
235
+ getter containment, duration boundaries, and exact structured errors.
236
+ - [`tests/src/core/validators.test.ts`](../tests/src/core/validators.test.ts) —
237
+ total duration and native-signal validation, including spoofed values and a
238
+ revoked proxy.
239
+ - [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) —
240
+ `createTimeout` returns a working `TimeoutInterface` and preserves the strict
241
+ construction boundary.
242
+
243
+ ## See also
244
+
245
+ - [`AGENTS.md`](../AGENTS.md) — the rules, including the documentation contract
246
+ and the fixed lifecycle meanings of `start` and `clear`.
247
+ - [`contract.md`](contract.md) — the mirrored guide for `@orkestrel/contract`,
248
+ the source of the validation primitives and `ContractError` used at the
249
+ construction boundary.
250
+ - [`guide.md`](guide.md) — the mirrored guide for `@orkestrel/guide`, the
251
+ devDependency powering this repo's guides-parity test suite.
252
+ - [`README.md`](README.md) — the guides index.
@@ -0,0 +1,311 @@
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 and no failure handling of its own. A `ToolManager` is the live surface a caller
15
+ holds: it hands `definitions()` outward, takes a `ToolCall` back, and answers with a `ToolResult`,
16
+ a result rather than a throw for a call whose members are plain values. Tools stay in the map by
17
+ name in insertion order. Everything else in this module is the plain data those two exchange.
18
+
19
+ **Anyone can call a tool.** Nothing here is model-specific — `tools.execute(call)` is an ordinary
20
+ async call returning an ordinary result, and plain application code may drive it directly. The
21
+ shape exists because callers that work from descriptions need the description and the handler to
22
+ travel together: an agent loop choosing which function to invoke, an MCP bridge exposing local
23
+ capability to a remote client, a backend dispatching a named operation. `@orkestrel/agent` and
24
+ `@orkestrel/mcp` are two such callers; ready-made tools ship in `@orkestrel/toolbox`.
25
+
26
+ **Mechanism only.** This runtime advertises, dispatches, and contains failure. It transports
27
+ nothing, validates no arguments against a tool's schema, authorizes no call, and ships no concrete
28
+ tools. Optional caller context is consumer-asserted and forwarded without verification. Each trust
29
+ decision belongs to the invoking consumer, to a policy layer, or to the tool itself. Progress
30
+ reporting belongs there too: it is a property of the invoking consumer's execution context, one
31
+ layer up — the `@orkestrel/mcp` package's execution context carries a progress reporter — never of
32
+ the tool contract itself.
33
+
34
+ Source: [`src/core`](../src/core). Published through `@orkestrel/tool`.
35
+
36
+ ## Surface
37
+
38
+ ### Contracts
39
+
40
+ The data shapes, from [`types.ts`](../src/core/types.ts). Every property is readonly, and an
41
+ optional field the caller did not supply is absent from the value. A `Shape` cell holds an
42
+ interface's data members as bare names in braces, `?` marking an optional member and `plus`
43
+ introducing its call-signature members, and a type alias's own type literal with a union's arms
44
+ escaped as `\|`. An extended interface's name comes before `plus`, with the members it adds after.
45
+
46
+ | Name | Kind | Shape | Summary |
47
+ | ---------------------- | --------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
48
+ | `ToolDefinition` | interface | `{ name, description?, parameters? }` | Describes a tool as advertised to a caller. |
49
+ | `ToolCall` | interface | `{ id, name, arguments, caller? }` | Describes one request to run a named tool. |
50
+ | `ToolSuccess` | interface | `Success<unknown> plus { id, name }` | Reports the successful outcome of executing a `ToolCall`. |
51
+ | `ToolFailure` | interface | `Failure<string> plus { id, name }` | Reports the failed outcome of executing a `ToolCall`. |
52
+ | `ToolOptions` | interface | `{ name, description?, summary?, parameters?, execute }` | Configures an executable tool. |
53
+ | `ToolInterface` | interface | `ToolDefinition plus { summary? } plus execute` | Represents an executable tool: its advertised definition plus its local handler. |
54
+ | `ToolManagerInterface` | interface | `{ count } plus add, tool, tools, definitions, execute, remove, clear` | Represents a registry of executable tools with per-call error isolation. |
55
+ | `ToolResult` | type | `ToolSuccess \| ToolFailure` | Represents the outcome of executing a `ToolCall`. |
56
+
57
+ `ToolInterface` and `ToolManagerInterface` list every member they declare or inherit. The
58
+ call-signature members of each are documented under [Methods](#methods); the readonly `count` of
59
+ `ToolManagerInterface` reports how many tools are registered and is a Surface member with no
60
+ method row.
61
+
62
+ ### Validators
63
+
64
+ The call-envelope guard, from [`validators.ts`](../src/core/validators.ts). In a guard table a
65
+ `Shape` cell holds the type the guard narrows to.
66
+
67
+ | Name | Kind | Shape | Summary |
68
+ | ------------ | -------- | ---------- | -------------------------------------------------------------------------------------------------------------------- |
69
+ | `isToolCall` | function | `ToolCall` | Determines whether an unknown value is structurally a `ToolCall`, staying total for malformed and adversarial input. |
70
+
71
+ ### Helpers
72
+
73
+ The advertised-definition projection, from [`helpers.ts`](../src/core/helpers.ts).
74
+
75
+ | Name | Kind | Signature | Summary |
76
+ | ------------------ | -------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
77
+ | `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 the parameter schema by reference. |
78
+
79
+ ### Factories
80
+
81
+ From [`factories.ts`](../src/core/factories.ts) — the constructor-free way to reach `Tool` and
82
+ `ToolManager`.
83
+
84
+ | Name | Kind | Signature | Summary |
85
+ | ------------------- | -------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `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. |
87
+ | `createToolManager` | function | `() => 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. |
88
+
89
+ ### Classes
90
+
91
+ The implementing classes, from [`Tool.ts`](../src/core/tools/Tool.ts) and
92
+ [`ToolManager.ts`](../src/core/tools/ToolManager.ts) — each documented in full under its own
93
+ heading following this table.
94
+
95
+ | Name | Kind | Summary |
96
+ | ------------- | ----- | ---------------------------------------------------------------------------- |
97
+ | `Tool` | class | Binds an executable tool definition to a handler. |
98
+ | `ToolManager` | class | Represents an insertion-ordered tool registry with per-call error isolation. |
99
+
100
+ ### `Tool`
101
+
102
+ The implementing class of `ToolInterface`, from [`Tool.ts`](../src/core/tools/Tool.ts). It
103
+ copies the fields it was given — omitting each optional one that was not supplied — and keeps
104
+ the handler in a private field, so a tool's advertised shape cannot drift from what it executes.
105
+ The parameter schema, argument record, and present caller context are forwarded by reference,
106
+ never cloned. `Tool` deliberately does not catch: a handler that throws throws, and per-call
107
+ isolation belongs to the registry that dispatched it. See [`## Methods`](#methods) for its public
108
+ call surface.
109
+
110
+ ### `ToolManager`
111
+
112
+ The implementing class of `ToolManagerInterface`, from
113
+ [`ToolManager.ts`](../src/core/tools/ToolManager.ts). One name-keyed map is its whole state:
114
+ tools stay in insertion order, `tools()` and `definitions()` return fresh readonly arrays rather
115
+ than a view of that map, and every projection is computed on demand so a mutation can never
116
+ leave a stale copy behind. It is the only place a call can fail into a result instead of an
117
+ exception. See [`## Methods`](#methods) for its public call surface.
118
+
119
+ ## Methods
120
+
121
+ The public call-signature members of each behavioral interface, one table per interface.
122
+
123
+ #### `ToolInterface`
124
+
125
+ | Method | Returns | Summary |
126
+ | --------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
127
+ | `execute` | `Promise<unknown> \| unknown` | Runs the tool's handler with the caller-supplied arguments and any consumer-asserted caller context. |
128
+
129
+ #### `ToolManagerInterface`
130
+
131
+ | Method | Returns | Summary |
132
+ | ------------- | ---------------------------------------------- | ---------------------------------------------- |
133
+ | `add` | `void` | Registers one tool. |
134
+ | `tool` | `ToolInterface \| undefined` | Finds one registered tool by name. |
135
+ | `tools` | `readonly ToolInterface[]` | Lists the registered tools in insertion order. |
136
+ | `definitions` | `readonly ToolDefinition[]` | Lists the definitions advertised to a caller. |
137
+ | `execute` | `Promise<ToolResult \| readonly ToolResult[]>` | Executes one call with error isolation. |
138
+ | `remove` | `boolean` | Removes one registered tool. |
139
+ | `clear` | `void` | Removes every registered tool. |
140
+
141
+ `add`, `execute`, and `remove` each take one value or a readonly batch of them. A batch `add`
142
+ registers every tool, later entries winning over earlier ones with the same name; a batch
143
+ `execute` answers in input order with one result per call; a batch `remove` reports `true` only
144
+ when every named tool was present.
145
+
146
+ ## Anatomy of a tool
147
+
148
+ A definition is the part a caller can read; the handler is the part it cannot. Declare both at
149
+ once:
150
+
151
+ ```ts
152
+ import { createTool } from '@orkestrel/tool'
153
+
154
+ const add = createTool({
155
+ name: 'add',
156
+ description: 'Add two numeric values and return their sum. Both operands are required.',
157
+ summary: 'Add two numbers.',
158
+ parameters: {
159
+ type: 'object',
160
+ properties: {
161
+ left: { type: 'number' },
162
+ right: { type: 'number' },
163
+ },
164
+ required: ['left', 'right'],
165
+ },
166
+ execute: (args) => Number(args.left) + Number(args.right),
167
+ })
168
+ ```
169
+
170
+ `new Tool({ … })` builds the same thing; reach for `createTool` where a call site must not name
171
+ a class.
172
+
173
+ The schema is descriptive runtime data, forwarded by reference and never interpreted here. A
174
+ handler always receives the open `Readonly<Record<string, unknown>>` the caller sent and narrows
175
+ the fields it consumes — declaring `required` tells the caller what to send, not this runtime
176
+ what to reject. Its optional second parameter is the call's `caller` value verbatim. That value
177
+ is asserted by the invoking consumer and is never verified here; a handler or policy layer must
178
+ make every authorization and trust decision. Handlers may be synchronous or asynchronous; the
179
+ registry awaits either. A handler can declare no parameter, one, or two. When the call carries
180
+ no caller context the registry invokes the handler with the arguments record alone, so a handler
181
+ that reads its own arity sees one argument and a handler that declares `caller` sees `undefined`.
182
+
183
+ When one was authored, `definitions()` projects the tool's `summary` as `description`, advertising
184
+ it in place of the full description. The full text stays on the tool for direct lookup through
185
+ `tools.tool('add')?.description`.
186
+
187
+ ## The registry
188
+
189
+ A registry is a working set, not a global. Build one per caller, fill it with the tools that
190
+ caller is allowed to reach, and hand out its definitions:
191
+
192
+ ```ts
193
+ import { Tool, createToolManager } from '@orkestrel/tool'
194
+
195
+ const tools = createToolManager()
196
+ tools.add(add) // the tool defined earlier
197
+ tools.add([
198
+ new Tool({ name: 'echo', execute: (args) => args.value }),
199
+ new Tool({ name: 'now', description: 'Current epoch milliseconds.', execute: () => Date.now() }),
200
+ ])
201
+
202
+ tools.count // 3
203
+ tools.tool('add') // the exact instance that was registered, or undefined
204
+ tools.tools() // a fresh readonly array, in insertion order
205
+ tools.definitions() // the same order, projected to plain ToolDefinition values
206
+
207
+ tools.remove('echo') // true — the tool was present
208
+ tools.remove(['now', 'ghost']) // false — 'ghost' was never registered, so not every name succeeded
209
+ tools.clear() // back to empty
210
+ ```
211
+
212
+ Order is insertion order, and adding a name that already exists replaces the stored tool without
213
+ moving it — the sequence a caller sees stays stable while a tool behind a name is swapped.
214
+ Remove a name and add it again and it lands at the end, because the name is genuinely new to the
215
+ map. In a batch, later entries win over earlier ones with the same name.
216
+
217
+ `definitions()` projects fresh plain objects on every call: `name`, then `description` only when
218
+ a summary or description exists, then `parameters` only when a schema exists, with the schema
219
+ object's original identity preserved. Nothing that arrives on a definition is a live handle on
220
+ the registry — advertising cannot be used to reach the handlers.
221
+
222
+ ## Calls and results
223
+
224
+ A call arrives as unstructured input from somewhere else, so check the envelope before trusting
225
+ it, then execute:
226
+
227
+ ```ts
228
+ import { isToolCall } from '@orkestrel/tool'
229
+
230
+ const incoming: unknown = {
231
+ id: 'call-1',
232
+ name: 'add',
233
+ arguments: { left: 2, right: 3 },
234
+ caller: { subject: 'user-42' },
235
+ }
236
+
237
+ if (isToolCall(incoming)) {
238
+ const result = await tools.execute(incoming)
239
+ if (result.success) {
240
+ result.value // 5
241
+ } else {
242
+ result.error // the failure message
243
+ }
244
+ }
245
+
246
+ const batch = await tools.execute([
247
+ { id: '1', name: 'add', arguments: { left: 2, right: 3 } }, // → { id: '1', name: 'add', success: true, value: 5 }
248
+ { id: '2', name: 'ghost', arguments: {} }, // → { id: '2', name: 'ghost', success: false, error: 'tool not found: ghost' }
249
+ ])
250
+ ```
251
+
252
+ `isToolCall` validates the envelope only: the `id`, the `name`, and that `arguments` is a plain
253
+ record. Optional `caller` remains opaque `unknown`; the guard does not verify it. It never checks
254
+ arguments against a tool's schema, so a well-formed call for a badly shaped payload still reaches
255
+ the handler, which is exactly where the domain knowledge to reject it lives.
256
+
257
+ Caller context exists only on the call and in the tool execution chain. This package never adds
258
+ it to advertised definitions, schemas, results, or logs. When present it is forwarded by
259
+ identity; when absent it is not passed at all.
260
+
261
+ Execution always resolves for a call whose members are plain values; a call whose `id` or `name`
262
+ accessor throws when read makes `execute` reject, because no correlated result can be built
263
+ without them. An unknown name becomes `tool not found: <name>`; a synchronous throw and an
264
+ asynchronous rejection are both contained; an `Error` contributes its `message`, and any other
265
+ thrown value is converted with `String`; a value whose conversion itself throws — a hostile
266
+ `toString`, a throwing `message` getter, a null-prototype object — becomes the fixed message
267
+ `Unknown thrown value`. Success and failure never mix in one result: a successful call carries
268
+ `value` even when that value is `undefined`, `null`, `0`, `''`, or `false`, and a failed call
269
+ carries `error`. Narrow on `success` to distinguish the two; a present success value is not
270
+ necessarily meaningful or truthy.
271
+
272
+ An in-process caller needing a typed error can call `tools.tool(name)`, then
273
+ `tool.execute(args)` inside its own `try`/`catch`.
274
+
275
+ A batch is dispatched concurrently and answered in input order, with each call whose members are
276
+ plain values isolated from its siblings — a handler failure never voids the batch, a call whose
277
+ `id` or `name` accessor throws when read rejects it, and duplicate ids stay distinct positional
278
+ calls rather than collapsing into one. That isolation is what lets a caller feed every result back
279
+ to whatever produced the calls and let it react to the failures itself.
280
+
281
+ ## Callers
282
+
283
+ The registry's two-sided shape — `definitions()` out, `execute()` back — is all a caller needs,
284
+ and it is the same shape whatever sits on the other side.
285
+
286
+ An agent loop advertises `definitions()` to a model, receives tool calls in the model's reply,
287
+ runs them through `execute`, and appends each `ToolResult` to the conversation; because a failure
288
+ comes back as an error result, the model sees what went wrong and can try something else instead
289
+ of the run collapsing. An MCP bridge maps the same definitions onto the protocol's tool listing
290
+ and routes each invocation to `execute`. Plain code skips the discovery half entirely and calls
291
+ `execute` with a call it wrote itself — a scheduled job, an HTTP handler dispatching a named
292
+ operation, a test.
293
+
294
+ Concrete tools are not this package's business. `@orkestrel/toolbox` ships ready-made ones, and
295
+ anything a `ToolInterface` can describe — a local computation, a database query, a remote API —
296
+ registers here unchanged.
297
+
298
+ ## Tests
299
+
300
+ - [`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 and asserts the values their comments claim.
301
+ - [`Tool.test.ts`](../tests/src/core/tools/Tool.test.ts) — definition binding, optional-field omission, argument identity, return values, and the deliberate absence of handler isolation.
302
+ - [`ToolManager.test.ts`](../tests/src/core/tools/ToolManager.test.ts) — insertion order, overwrite and removal lifecycle, definition projection, and isolated single and batch execution.
303
+ - [`factories.test.ts`](../tests/src/core/factories.test.ts) — factory construction and working instances.
304
+ - [`helpers.test.ts`](../tests/src/core/helpers.test.ts) — definition projection: summary preference, omitted optional keys, projected key order, schema identity, and a fresh object per call.
305
+ - [`validators.test.ts`](../tests/src/core/validators.test.ts) — tool-call envelope boundaries: incomplete calls, wrong field types, and non-record arguments.
306
+
307
+ ## See also
308
+
309
+ - [`README.md`](README.md) — the guides index.
310
+ - [`contract.md`](contract.md) — the dependency mirror for `@orkestrel/contract`, whose total guards back `isToolCall` and the registry's overload narrowing.
311
+ - [`AGENTS.md`](../AGENTS.md) — the repository's coding and documentation contract.