@orkestrel/scaffold 0.0.93 → 0.0.95

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.
@@ -230,17 +230,17 @@ body before receiving that response.
230
230
  registers a raw protocol-upgrade claimant; `start` binds the listener and
231
231
  resolves the actually-bound port while accepting an optional caller
232
232
  `AbortSignal`; `stop` gracefully drains then closes; `destroy` is the
233
- terminal, idempotent teardown. `stop` and `destroy` always resolve: an upgraded socket a
234
- handler claimed is drained up to the `drain` deadline and then destroyed,
235
- never waited on forever.
236
-
237
- | Method | Returns | Summary |
238
- | --------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
239
- | `use` | `void` | Appends one middleware, or an array of them in order, to the onion, outer-to-inner in call order. |
240
- | `upgrade` | `void` | Registers an `UpgradeHandler` claimant that runs in registration order; a claimed socket is tracked until it closes. |
241
- | `start` | `Promise<number>` | Binds the configured `host` and `port`, or an ephemeral port, under an optional caller `AbortSignal`, and resolves the actually-bound port. |
242
- | `stop` | `Promise<void>` | Stops gracefully: refuses new connections, fires the stop signal, drains in-flight requests and claimed upgraded sockets up to the `drain` deadline, then closes. |
243
- | `destroy` | `Promise<void>` | Tears down for good: force-closes the listener and every socket, then the emitter — terminal and idempotent from any state. |
233
+ terminal, idempotent teardown. An upgraded socket a handler claimed never holds
234
+ `stop` or `destroy` forever: the `stop` method drains it up to the `drain`
235
+ deadline and then destroys it, and the `destroy` method destroys it at once.
236
+
237
+ | Method | Returns | Summary |
238
+ | --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
239
+ | `use` | `void` | Appends one middleware, or an array of them in order, to the onion, outer-to-inner in call order. |
240
+ | `upgrade` | `void` | Registers an `UpgradeHandler` claimant that runs in registration order; a claimed socket is tracked until it closes. |
241
+ | `start` | `Promise<number>` | Binds the configured `host` and `port`, or an ephemeral port, under an optional caller `AbortSignal`, and resolves the actually-bound port. |
242
+ | `stop` | `Promise<void>` | Stops gracefully within the `drain` deadline: fires the stop signal, drains in-flight requests and claimed upgraded sockets, then closes the listener; after a clean drain it ends each connection when no request exchange is open on it, and when the deadline expires it destroys every socket still open. |
243
+ | `destroy` | `Promise<void>` | Tears down for good: force-closes the listener and every socket, then the emitter — terminal and idempotent from any state. |
244
244
 
245
245
  ## Contract
246
246
 
@@ -262,33 +262,93 @@ These invariants hold across `src/server` ↔ `server.md`.
262
262
  server is never born aborted; `address` is the real bound `AddressInfo`
263
263
  after a successful start and `undefined` before start and after stop or
264
264
  destroy; `stop()`/`destroy()` are idempotent no-ops from a state with
265
- nothing to tear down; `EADDRINUSE` rejects `start()` outright — no silent
266
- ephemeral fallback (use `discoverPort` up front for a guaranteed-free
267
- port).
265
+ nothing to tear down, so a `stop()` call from `idle`, `starting`, or
266
+ `stopped` while no stop runs resolves at once; a `stop()` call made while
267
+ a stop runs returns that stop's promise and resolves with it after the
268
+ close, including a call made from a `stop` event listener or from a
269
+ listener on the stop signal; `EADDRINUSE` rejects `start()` outright — no
270
+ silent ephemeral fallback (use `discoverPort` up front for a
271
+ guaranteed-free port). A `start()` call made after `destroy()` while an
272
+ earlier `stop()` call has not yet resolved can be reset to `stopped` when
273
+ that stop finishes.
268
274
  4. **Startup is bounded and caller-cancellable.** `start(signal?)` observes
269
275
  caller cancellation only while binding; `timeouts.start` independently
270
- bounds the bind (`0` permits no startup window). Cancellation or deadline
271
- expiry closes the partial listener, clears the startup deadline, resets
272
- the entity to `idle`, and rejects; expiry rejects with a `DOMException`
273
- named `TimeoutError`, while caller cancellation rejects with that signal's
274
- `reason`. A later `start()` is therefore permitted. Aborting the caller
275
- signal after a successful start does not stop a live server.
276
+ bounds the bind (`0` permits no startup window) and is an integer from 0
277
+ through 2_147_483_647, or construction throws a `TypeError`. Cancellation
278
+ or deadline expiry closes the partial listener, clears the startup
279
+ deadline, resets the entity to `idle`, and rejects; expiry rejects with a
280
+ `DOMException` named `TimeoutError`, while caller cancellation rejects
281
+ with that signal's `reason`. A later `start()` is therefore permitted.
282
+ Aborting the caller signal after a successful start does not stop a live
283
+ server.
276
284
  5. **Graceful drain is event-driven, never a busy-loop.** `stop()` fires the
277
285
  stop signal, arms a `@orkestrel/timeout` deadline, and parks on the
278
286
  drainable count reaching zero or the deadline firing (a wake-park, not
279
- polling); it then emits `drain` with the still-pending counts and closes —
280
- dropping idle keep-alive sockets always, force-closing every open socket
281
- only when the deadline fired with work still pending (or on `destroy()`).
287
+ polling); it then emits `drain` with the still-pending counts and closes
288
+ the listener. The server counts the open request exchanges on each
289
+ connection, because a pipelining client can hold several: an exchange
290
+ opens when a request reaches the handler and stays open until its request
291
+ message has ended or closed and its response has finished or closed.
292
+ The `error` listeners that this tracking attaches to the message and the
293
+ response stay attached, so a later error on either raises no uncaught
294
+ exception.
295
+ After a clean drain the close destroys every connection with no open
296
+ exchange. That covers an idle keep-alive connection. It covers a
297
+ connection that never sent a request: Node doesn't count it as idle, and
298
+ the `closeIdleConnections()` call alone would leave it holding the
299
+ `stop()` call open until the peer drops it. It also covers a connection
300
+ partway through its first or a later header block, because a graceful
301
+ stop serves counted work and a request whose headers never completed is
302
+ not counted. A connection that still carries an open exchange stays open,
303
+ including one whose handler answered, for example with a `413` response,
304
+ while its body still uploads: destroying it can reset the connection and
305
+ lose that response. The server ends that connection when its last
306
+ exchange completes, and the `stop()` call resolves after that connection
307
+ closes. Each body byte restarts Node's keep-alive socket timer, so no Node
308
+ timeout bounds an upload that keeps sending. The deadline that bounds the
309
+ drain therefore stays armed through the clean close's wait: when it
310
+ expires before the server's `close` event, the server destroys every
311
+ socket still open, as an expired drain does, so no connection holds the
312
+ `stop()` call past the `drain` deadline. The `drain` event has already
313
+ reported `[0, 0]` for that stop, because every response had finished at
314
+ the clean drain. The cut ends the unread part of an upload its handler had
315
+ already answered, and destroying that socket can also reset the
316
+ connection and lose the answered response the peer has not yet read. No
317
+ event reports that cut or that loss. An upload ends before the deadline
318
+ only because the `buildRequest` function of the `@orkestrel/router`
319
+ package reads a request body eagerly, whether or not the handler reads
320
+ it. A request or an upgrade that a connection sends after the listener
321
+ closes, such as one pipelined behind the end of that upload, is refused:
322
+ the server destroys that connection before the request is counted or a
323
+ handler sees it, so the `stop()` call waits only on work counted before
324
+ the close. That refusal, and the clean close's destroy of a connection
325
+ with no open exchange, can reset the connection when the kernel still
326
+ holds bytes the server never read, as it does for a peer that pipelines a
327
+ request behind a POST request; see
328
+ [the pipelining section of RFC 9112](https://www.rfc-editor.org/rfc/rfc9112#section-9.3.2).
329
+ After an expired drain the close destroys every socket and cuts the work
330
+ still pending. No I/O callback runs between the drain's settle and the
331
+ close, so a request that reached the handler by the close did so before
332
+ the drain settled and was counted. A request Node answers itself never
333
+ reaches the handler and is not counted: Node answers an unknown `Expect`
334
+ value with a `417` response, so that connection carries no open exchange
335
+ and the clean close destroys it. A request that reaches the server after
336
+ the `stop()` call begins and before the listener closes, on a kept-alive
337
+ connection or on a connection the listener accepts, is in-flight work:
338
+ the drain waits for it, and its `request.signal` property is already
339
+ aborted.
282
340
  Drainable work is every in-flight request plus every upgraded socket a
283
341
  handler claimed, because a long-lived upgraded connection is work a
284
342
  graceful stop lets finish rather than cuts mid-frame. `drain` carries
285
- both counts, so a caller can tell a clean stop from a forced one. This is
286
- also what makes `stop()` and `destroy()` always resolve: node detaches an
343
+ both counts, which say whether a request or a claimed socket was cut; a
344
+ cut during the clean close's wait reports `[0, 0]`. A claimed socket
345
+ never holds `stop()` or `destroy()` open forever: Node detaches an
287
346
  upgraded socket from its own connection set, so neither
288
347
  `closeIdleConnections()` nor `closeAllConnections()` reaches it while
289
348
  `server.close()` still waits on it, and the server therefore tracks each
290
- claimed socket until it closes and destroys the survivors itself on a
291
- forced close. The claimant still owns the socket; tracking only watches it.
349
+ claimed socket until it closes and destroys the survivors itself when the
350
+ server closes. The claimant still owns the socket; tracking only watches
351
+ it.
292
352
  A handler that wants a protocol-clean goodbye — a WebSocket close frame —
293
353
  sends it on the `stop` event, which fires before the drain begins, and the
294
354
  drain then settles on that close instead of running the deadline out. A
@@ -326,8 +386,8 @@ These invariants hold across `src/server` ↔ `server.md`.
326
386
  `Request` exists there, only a raw `IncomingMessage` — and never crashes
327
387
  the process) and the fan-out continues; an upgrade nothing claims destroys
328
388
  the socket so it never leaks a dangling connection. A claimed socket is
329
- tracked until it closes, which is what item 5's drain and forced close
330
- act on — ownership stays with the claimant either way.
389
+ tracked until it closes, which is what item 5's drain and close act on —
390
+ ownership stays with the claimant either way.
331
391
  8. **Body read exactly once, capped, zip-bomb-safe, scrubbed.**
332
392
  `MiddlewareContext.body()` is lazy and cached, so a body-parsing middleware
333
393
  and the eventual handler both reading it consume the underlying stream
@@ -534,9 +594,18 @@ function streamHandler(): Response {
534
594
 
535
595
  ### Graceful shutdown
536
596
 
537
- `stop()` refuses new connections, gives in-flight work up to the `drain`
538
- deadline, then closes; `destroy()` is the final, idempotent teardown. In-flight
539
- work is requests and claimed upgraded sockets, so each call always returns.
597
+ The `stop()` call gives in-flight work up to the `drain` deadline, then
598
+ closes the listener; the `destroy()` call is the final, idempotent
599
+ teardown. In-flight work is requests and claimed upgraded sockets. An idle
600
+ keep-alive connection or a connection that never sent a request costs the
601
+ `stop()` call no wait. A connection whose request exchange is still open,
602
+ such as an upload still sending its body after the response, keeps the
603
+ `stop()` call waiting until that exchange completes or the same `drain`
604
+ deadline expires and cuts it. A request that such a connection sends after
605
+ the listener closes is refused, and its connection is destroyed. A second
606
+ `stop()` call made during the stop returns the first call's promise. The
607
+ `drain` option is an integer from 0 through 2_147_483_647 milliseconds, and
608
+ construction throws a `TypeError` for any other value.
540
609
 
541
610
  ```ts
542
611
  import { createServer } from '@orkestrel/server'
@@ -548,7 +617,7 @@ server.emitter.on('drain', (pending, upgraded) =>
548
617
  console.log(`drained with ${pending} requests and ${upgraded} sockets still open`),
549
618
  )
550
619
  await server.start()
551
- await server.stop() // graceful — waits up to 5s for requests and upgraded sockets
620
+ await server.stop() // drains, then closes; the 5 s deadline cuts whatever still holds it
552
621
  await server.destroy() // idempotent final teardown
553
622
  ```
554
623
 
@@ -727,10 +796,26 @@ new TextDecoder().decode(body) // 'hi' — capped decompression, the zip-bomb de
727
796
  the status matrix, restart-fresh-abort, caller-cancelled / timed-out / clean
728
797
  bounded startup, `EADDRINUSE` honesty, host/port binds, ephemeral default,
729
798
  connection / header / per-socket request caps, graceful-vs-forced drain,
730
- the held-upgraded-socket stop (drained to the deadline then cut, settled
731
- early when the claimant closes it, reported on `drain`, and force-closed by
732
- `destroy()`) against a no-socket control, 20-parallel-none-dropped,
733
- connection facts threaded into state,
799
+ the clean stop that ends a never-used connection inside the deadline after
800
+ the request in flight beside it finishes, the clean stop that counts and
801
+ serves a kept-alive request sent during the drain with its signal aborted
802
+ before it closes that idle connection, the clean stop that serves a
803
+ kept-alive request sent during the drain and keeps that connection open
804
+ until the drain settles, the clean stop that destroys a connection whose
805
+ first or second header block never completed and one whose `417` answer
806
+ left a withheld body pending, the clean stop whose drain waits for a
807
+ response after its POST body ended and then ends that connection, the
808
+ clean stop that keeps a connection whose `413` answer left its body
809
+ uploading and ends it when the upload ends, the `drain` deadline that cuts
810
+ that upload when it sends one more byte and stalls or keeps sending one
811
+ byte at a time, the concurrent `stop()` call that returns the stop in
812
+ flight and resolves with it after the close, the `stop()` call from `idle`
813
+ or `starting` that resolves at once, the clean stop that
814
+ refuses a request or an upgrade pipelined behind the end of that body after
815
+ the listener closes, the held-upgraded-socket stop (drained to
816
+ the deadline then cut, settled early when the claimant closes it, reported
817
+ on `drain`, and force-closed by `destroy()`) against a no-socket control,
818
+ 20-parallel-none-dropped, connection facts threaded into state,
734
819
  `context.body()` caching, boundary mapping (`HTTPError`/other/`expose`), the
735
820
  stop-signal-reaches-handlers case, and the real slow-TCP proof that an SSE
736
821
  producer parks at local queue pressure, resumes on drain, and stays bounded
@@ -70,7 +70,7 @@ In a guard table a `Shape` cell holds the type the guard narrows to.
70
70
  | ---------------------- | -------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
71
  | `isWorkflowLineage` | function | `WorkflowLineage` | Narrows an unknown value to a valid alternating workflow lineage. |
72
72
  | `isAgentFunction` | function | `AgentFunction` | Narrows an unknown callable to Toolbox's frozen contextual agent adapter metadata. |
73
- | `isColumnPrimitive` | function | `ColumnPrimitive` | Narrows an unknown value to a `ColumnPrimitive`. |
73
+ | `isColumnPrimitive` | const | `ColumnPrimitive` | Narrows an unknown value to a `ColumnPrimitive`. |
74
74
  | `isColumnSpec` | function | `ColumnSpec` | Narrows an unknown value to a `ColumnSpec`. |
75
75
  | `isDatabaseDefinition` | function | `DatabaseDefinition` | Narrows an unknown value to a `DatabaseDefinition` — a non-empty `id` and `driver`, a `tables` record whose every value is `{ columns: record of valid ColumnSpec }`, plus optional `primary`, `indexes`, and finite `version` schema configuration. The boundary guard a `DefinitionStoreInterface` applies to an untrusted persisted blob before trusting it as a definition (never an `as`). |
76
76
 
@@ -191,41 +191,41 @@ A `Shape` cell holds the constant's declared type.
191
191
 
192
192
  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 `\|`.
193
193
 
194
- | Type | Kind | Shape | Summary |
195
- | -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
196
- | `TaskDraft` | interface | `{ id?, name?, description?, behavior?, retries?, timeout? }` | Represents a draft task — a `TaskDefinition` (`@orkestrel/workflow`) with optional `id` and `name`. |
197
- | `PhaseDraft` | interface | `{ id?, name?, description?, tasks, concurrency?, bail? }` | Represents a draft phase — a `PhaseDefinition` (`@orkestrel/workflow`) with optional `id` and `name` and `TaskDraft` tasks. |
198
- | `WorkflowDraft` | interface | `{ id?, name?, description?, phases, bail? }` | Represents a draft workflow — a `WorkflowDefinition` (`@orkestrel/workflow`) with optional `id` and `name` at the workflow, phase, and task levels. |
199
- | `WorkflowStep` | interface | `{ name }` | Represents one flat step — `{ name }` — the building block of a `WorkflowSteps` blob. |
200
- | `WorkflowSteps` | interface | `{ name?, steps }` | Represents the flat authoring blob `createWorkflowTool` advertises — `{ name?, steps }` — the simplest surface a small model can fill. |
201
- | `WorkflowToolResult` | interface | `{ status, count, durable?, fault? }` | Represents the JSON-safe run summary returned by `createWorkflowTool`. |
202
- | `WorkflowLineage` | type | `readonly string[]` | Represents one immutable workflow/agent call chain — strictly alternating tags, each unique, beginning with a workflow tag. |
203
- | `WorkflowAgents` | type | `Readonly<Record<string, AgentInterface>>` | Represents raw live agents keyed by the workflow function names that invoke them. |
204
- | `AgentFunction` | type | `WorkflowFunction & { category, lineage }` | Represents a contextual agent adapter carrying immutable metadata for Toolbox composition. |
205
- | `AgentFunctionOptions` | interface | `{ runner?, lineage?, functions?, agents?, store? }` | Represents the options for `createAgentFunction` — the opt-in adapter that wraps a live `AgentInterface` (`@orkestrel/agent`) as an `AgentFunction` with immutable lineage metadata and optional nested-workflow composition. |
206
- | `WorkflowToolOptions` | interface | `{ lineage?, functions?, agents?, store? }` | Represents the options for `createWorkflowTool` and `createWorkflowFunctions` — lineage-aware composition of opaque leaves, raw agents, and native workflow persistence. |
207
- | `WorkspaceToolOptions` | interface | `{ name?, description?, manager?, store? }` | Represents the options for `createWorkspaceTool` — either a caller-built `WorkspaceManagerInterface` to drive directly, or a `WorkspaceStoreInterface` the tool constructs a fresh manager over; neither given constructs a manager over `@orkestrel/workspace`'s in-memory store. |
208
- | `WorkspaceOperation` | type | `{ operation: 'read', path } \| { operation: 'list' } \| { operation: 'has', path } \| { operation: 'search', query, regex?, sensitive?, limit? } \| { operation: 'replace', query, replacement, regex?, sensitive?, limit? } \| { operation: 'write', path, content } \| { operation: 'splice', path, content, fromLine, fromColumn, toLine, toColumn } \| { operation: 'prepend', path, content } \| { operation: 'append', path, content } \| { operation: 'move', from, to } \| { operation: 'remove', path } \| { operation: 'workspaces' } \| { operation: 'switch', id }` | Represents one operation an agent invokes through `createWorkspaceTool` — a flat, descriptive tagged union over the workspace edit, read, and navigation actions, discriminated by the `operation` literal (a discriminant is named for its axis — the action being performed — never `kind`). |
209
- | `AgentToolOptions` | interface | `{ name?, description?, provider?, tools?, system?, depth?, ancestry?, store? }` | Represents the options for `createAgentTool` — the sub-agent delegation defaults, the nesting-depth / cycle guard bookkeeping, and the advertised tool overrides. |
210
- | `AgentToolArguments` | interface | `{ task, provider?, tools?, system? }` | Represents the flat args `createAgentTool` accepts — a delegated `task` plus the minimal optional `AgentJobInput` (`@orkestrel/agent`) fields a caller may override per-call. |
211
- | `ToolboxErrorCode` | type | `'TOOL' \| 'DEPTH' \| 'DEADLOCK' \| 'EXPIRE' \| 'ANSWER' \| 'DATABASE' \| 'RELATION'` | Represents the machine-readable code a thrown `ToolboxError` carries — a thrown, typed, code-bearing error, never a `{ error }` return. |
212
- | `DescribeToolArguments` | interface | `{ name }` | Represents the flat args `createDescribeTool` accepts — the registered tool `name` whose full `description` a model wants back. |
213
- | `PromptToolOptions` | interface | `{ manager, from, name?, description? }` | Represents the options for `createPromptTool` — the live `TerminalManagerInterface` (`@orkestrel/terminal`) to `ask` through, the terminal name `from`, and the advertised tool overrides. |
214
- | `AnswerToolOptions` | interface | `{ manager, to, name?, description? }` | Represents the options for `createAnswerTool` — the live `TerminalManagerInterface` (`@orkestrel/terminal`) to list / answer prompts through, the terminal name `to`, and the advertised tool overrides. |
215
- | `ColumnPrimitive` | type | `'string' \| 'integer' \| 'number' \| 'boolean'` | Represents one column's declared primitive — a shorthand, or `integer` for a whole-number `number`. |
216
- | `ColumnSpec` | type | `ColumnPrimitive \| { primitive, optional? }` | Represents one table column's spec — either a bare `ColumnPrimitive` shorthand, or `{ primitive, optional }` when the column may be absent from a row. |
217
- | `TableSpec` | type | `Readonly<Record<string, { columns }>>` | Represents a database's table layout — one entry per table, each a flat map of column name to `ColumnSpec`. The small-model-facing DSL `expandTables` compiles into an `@orkestrel/database` `TableMap`. |
218
- | `DatabaseDefinition` | interface | `{ id, driver, tables, primary?, indexes?, version? }` | Represents one database's config-only definition — an `id`, a `driver`, and a `TableSpec`, with optional `primary`, `indexes`, and `version` schema configuration. |
219
- | `DatabaseDefinitionRow` | interface | `{ id, definition }` | Represents one opaque persisted row — the shape a definition-row-backed `TableInterface` store reads/writes; `definition` is narrowed with `isDatabaseDefinition` on read. |
220
- | `DefinitionStoreInterface` | interface | `{} plus get, set, delete` | Represents the point-access persistence seam for `DatabaseDefinition` configs — the twin of `@orkestrel/terminal`'s `TerminalStoreInterface`, storing a database's config-only blueprint rather than a live handle. Every primitive is async; `delete` of an absent id is a no-op. |
221
- | `DatabaseQueryInput` | interface | `{ conditions?, order?, limit?, offset? }` | Represents the serialized wire query a database-tool call carries — the parsed form of `queryShape`, which `normalizeQuery` normalizes into a live `@orkestrel/database` `QueryInput`. |
222
- | `ClampedQuery` | interface | `{ query, limit }` | Represents the probe query and effective row limit `clampQuery` returns. |
223
- | `DatabaseToolOptions` | interface | `{ name?, description?, databases?, store?, drivers?, generator?, limit?, timeout?, readonly? }` | Represents the options for `createDatabaseTool` — the live handles, definition store, driver registry, key generator, row cap, timeout, and readonly gate the tool composes. |
224
- | `RelationToolOptions` | interface | `{ name?, description?, managers, limit?, depth? }` | Represents the options for `createRelationTool` — the required live `RelationManagerInterface` registry a call addresses, the row cap, and the `include` depth cap. |
225
- | `InferToolOptions` | interface | `{ name?, description? }` | Represents the options for `createInferTool` — advertised name and description overrides only; `format` and `enum` are runtime call arguments (see `inferToolShape`), not construction-time options, because a model chooses them per call. |
226
- | `EndpointHandler` | type | `(args: Readonly<Record<string, unknown>>) => Promise<unknown> \| unknown` | Represents the handler `EndpointDefinition.execute` implements — it mirrors `@orkestrel/tool`'s `ToolOptions.execute` signature exactly (the same `Readonly<Record<string, unknown>>` argument, the same `Promise<unknown> \| unknown` return), so `execute: (args) => definition.execute(args)` typechecks with zero assertions in `createEndpointTool`. |
227
- | `EndpointDefinition` | interface | `{ name, description, samples, execute }` | Represents one concrete endpoint `createEndpointTool` wraps as an LLM-callable `ToolInterface` — the advertised identity, a non-empty set of example values its `parameters` are inferred from, and the local handler that runs a call. |
228
- | `EndpointToolOptions` | interface | `{ format?, enum?, validate? }` | Represents the construction-time tuning for `createEndpointTool` — the inferred `parameters` schema's `format` and `enum` constraints, and whether that same schema is enforced at `execute` time. |
194
+ | Type | Kind | Shape | Summary |
195
+ | -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
196
+ | `TaskDraft` | interface | `{ id?, name?, description?, behavior?, retries?, timeout? }` | Represents a draft task — a `TaskDefinition` (`@orkestrel/workflow`) with optional `id` and `name`. |
197
+ | `PhaseDraft` | interface | `{ id?, name?, description?, tasks, concurrency?, bail? }` | Represents a draft phase — a `PhaseDefinition` (`@orkestrel/workflow`) with optional `id` and `name` and `TaskDraft` tasks. |
198
+ | `WorkflowDraft` | interface | `{ id?, name?, description?, phases, bail? }` | Represents a draft workflow — a `WorkflowDefinition` (`@orkestrel/workflow`) with optional `id` and `name` at the workflow, phase, and task levels. |
199
+ | `WorkflowStep` | interface | `{ name }` | Represents one flat step — `{ name }` — the building block of a `WorkflowSteps` blob. |
200
+ | `WorkflowSteps` | interface | `{ name?, steps }` | Represents the flat authoring blob `createWorkflowTool` advertises — `{ name?, steps }` — the simplest surface a small model can fill. |
201
+ | `WorkflowToolResult` | interface | `{ status, count, durable?, fault? }` | Represents the JSON-safe run summary returned by `createWorkflowTool`. |
202
+ | `WorkflowLineage` | type | `readonly string[]` | Represents one immutable workflow/agent call chain — strictly alternating tags, each unique, beginning with a workflow tag. |
203
+ | `WorkflowAgents` | type | `Readonly<Record<string, AgentInterface>>` | Represents raw live agents keyed by the workflow function names that invoke them. |
204
+ | `AgentFunction` | type | `WorkflowFunction & { category, lineage }` | Represents a contextual agent adapter carrying immutable metadata for Toolbox composition. |
205
+ | `AgentFunctionOptions` | interface | `{ runner?, lineage?, functions?, agents?, store? }` | Represents the options for `createAgentFunction` — the opt-in adapter that wraps a live `AgentInterface` (`@orkestrel/agent`) as an `AgentFunction` with immutable lineage metadata and optional nested-workflow composition. |
206
+ | `WorkflowToolOptions` | interface | `{ lineage?, functions?, agents?, store? }` | Represents the options for `createWorkflowTool` and `createWorkflowFunctions` — lineage-aware composition of opaque leaves, raw agents, and native workflow persistence. |
207
+ | `WorkspaceToolOptions` | interface | `{ name?, description?, manager?, store? }` | Represents the options for `createWorkspaceTool` — either a caller-built `WorkspaceManagerInterface` to drive directly, or a `WorkspaceStoreInterface` the tool constructs a fresh manager over; neither given constructs a manager over `@orkestrel/workspace`'s in-memory store. |
208
+ | `WorkspaceOperation` | type | `{ operation: 'read', path } \| { operation: 'list' } \| { operation: 'has', path } \| { operation: 'search', query, regex?, sensitive?, limit? } \| { operation: 'replace', query, replacement, regex?, sensitive?, limit? } \| { operation: 'write', path, content } \| { operation: 'splice', path, content, fromLine, fromColumn, toLine, toColumn } \| { operation: 'prepend', path, content } \| { operation: 'append', path, content } \| { operation: 'move', from, to } \| { operation: 'remove', path } \| { operation: 'workspaces' } \| { operation: 'switch', id }` | Represents one operation an agent invokes through `createWorkspaceTool` — a flat, descriptive tagged union over the workspace edit, read, and navigation actions, discriminated by the `operation` literal (a discriminant is named for its axis — the action being performed — never `kind`). |
209
+ | `AgentToolOptions` | interface | `{ name?, description?, provider?, tools?, system?, depth?, ancestry?, store? }` | Represents the options for `createAgentTool` — the sub-agent delegation defaults, the nesting-depth / cycle guard bookkeeping, and the advertised tool overrides. |
210
+ | `AgentToolArguments` | interface | `{ task, provider?, tools?, system? }` | Represents the flat args `createAgentTool` accepts — a delegated `task` plus the minimal optional `AgentJobInput` (`@orkestrel/agent`) fields a caller may override per-call. |
211
+ | `ToolboxErrorCode` | type | `'TOOL' \| 'DEPTH' \| 'DEADLOCK' \| 'EXPIRE' \| 'ANSWER' \| 'DATABASE' \| 'RELATION'` | Represents the machine-readable code a thrown `ToolboxError` carries — a thrown, typed, code-bearing error, never a `{ error }` return. |
212
+ | `DescribeToolArguments` | interface | `{ name }` | Represents the flat args `createDescribeTool` accepts — the registered tool `name` whose full `description` a model wants back. |
213
+ | `PromptToolOptions` | interface | `{ manager, from, name?, description? }` | Represents the options for `createPromptTool` — the live `TerminalManagerInterface` (`@orkestrel/terminal`) to `ask` through, the terminal name `from`, and the advertised tool overrides. |
214
+ | `AnswerToolOptions` | interface | `{ manager, to, name?, description? }` | Represents the options for `createAnswerTool` — the live `TerminalManagerInterface` (`@orkestrel/terminal`) to list / answer prompts through, the terminal name `to`, and the advertised tool overrides. |
215
+ | `ColumnPrimitive` | type | `'string' \| 'integer' \| 'number' \| 'boolean'` | Represents one column's declared primitive — a shorthand, or `integer` for a whole-number `number`. |
216
+ | `ColumnSpec` | type | `ColumnPrimitive \| { primitive, optional? }` | Represents one table column's spec — either a bare `ColumnPrimitive` shorthand, or `{ primitive, optional }` when the column may be absent from a row. |
217
+ | `TableSpec` | type | `Readonly<Record<string, { columns }>>` | Represents a database's table layout — one entry per table, each a flat map of column name to `ColumnSpec`. The small-model-facing DSL `expandTables` compiles into an `@orkestrel/database` `TableMap`. |
218
+ | `DatabaseDefinition` | interface | `{ id, driver, tables, primary?, indexes?, version? }` | Represents one database's config-only definition — an `id`, a `driver`, and a `TableSpec`, with optional `primary`, `indexes`, and `version` schema configuration. |
219
+ | `DatabaseDefinitionRow` | interface | `{ id, definition }` | Represents one opaque persisted row — the shape a definition-row-backed `TableInterface` store reads/writes; `definition` is narrowed with `isDatabaseDefinition` on read. |
220
+ | `DefinitionStoreInterface` | interface | `{} plus get, set, delete` | Represents the point-access persistence seam for `DatabaseDefinition` configs — the twin of `@orkestrel/terminal`'s `TerminalStoreInterface`, storing a database's config-only blueprint rather than a live handle. Every primitive is async; `delete` of an absent id is a no-op. |
221
+ | `DatabaseQueryInput` | interface | `{ conditions?, order?, limit?, offset? }` | Represents the serialized wire query a database-tool call carries — the parsed form of `queryShape`, which `normalizeQuery` normalizes into a live `@orkestrel/database` `QueryInput`. |
222
+ | `ClampedQuery` | interface | `{ query, limit }` | Represents the probe query and effective row limit `clampQuery` returns. |
223
+ | `DatabaseToolOptions` | interface | `{ name?, description?, databases?, store?, drivers?, generator?, limit?, timeout?, readonly? }` | Represents the options for `createDatabaseTool` — the live handles, definition store, driver registry, key generator, row cap, timeout, and readonly gate the tool composes. |
224
+ | `RelationToolOptions` | interface | `{ name?, description?, managers, limit?, depth? }` | Represents the options for `createRelationTool` — the required live `RelationManagerInterface` registry a call addresses, the row cap, and the `include` depth cap. |
225
+ | `InferToolOptions` | interface | `{ name?, description? }` | Represents the options for `createInferTool` — advertised name and description overrides only; `format` and `enum` are runtime call arguments (see `inferToolShape`), not construction-time options, because a model chooses them per call. |
226
+ | `EndpointHandler` | type | `(args: Readonly<Record<string, unknown>>, context: ToolContext) => Promise<unknown> \| unknown` | Represents the handler `EndpointDefinition.execute` implements — it mirrors `@orkestrel/tool`'s `ToolOptions.execute` signature exactly, so `createEndpointTool` forwards the execution context it receives to the handler unchanged and a handler that reads `context.signal` observes the caller's cancellation. |
227
+ | `EndpointDefinition` | interface | `{ name, description, samples, execute }` | Represents one concrete endpoint `createEndpointTool` wraps as an LLM-callable `ToolInterface` — the advertised identity, a non-empty set of example values its `parameters` are inferred from, and the local handler that runs a call. |
228
+ | `EndpointToolOptions` | interface | `{ format?, enum?, validate? }` | Represents the construction-time tuning for `createEndpointTool` — the inferred `parameters` schema's `format` and `enum` constraints, and whether that same schema is enforced at `execute` time. |
229
229
 
230
230
  ### Server routes
231
231
 
@@ -337,7 +337,7 @@ Before shape branching or property reads, the handler snapshots untrusted `args`
337
337
 
338
338
  11. **`ToolboxError` owns Toolbox boundary failures; upstream errors own genuine domain failures.** It carries a machine-readable `ToolboxErrorCode` and an optional `context` and is always thrown, never returned as `{ error }`: `TOOL` covers malformed authoring, missing tool bindings, invalid tool/agent JSON, and the other package-owned resolution/configuration guards; `DEPTH` covers Toolbox workflow/agent nesting refusal; the remaining codes retain their terminal/database/relation meanings. A genuine error thrown by an executed tool passes through unchanged. The native workflow runner's genuine `WorkflowError` also passes through unchanged, including `TRANSITION` for an unresolved named run; Toolbox never invents invalid workflow `TOOL`/`DEPTH` codes and never relabels runner errors. `WorkspaceError` similarly retains its upstream domain ownership. An in-process direct call can inspect typed codes/context; `ToolManagerInterface.execute` flattens the error to its message.
339
339
 
340
- 12. **The workflow-function adapters are opt-in and exact at the JSON boundary.** `createToolFunction(tools, name)` resolves the live tool, awaits `tool.execute(controller.input)`, then deep-gates the unknown return through `parseJSONValue`; a missing binding or non-JSON result throws `ToolboxError('TOOL')`, while a genuine tool throw passes through by identity. It rejects `name === WORKFLOW_TOOL_NAME` at construction so the reserved live workflow tool cannot be adapted back into a workflow registry. `createAgentFunction(agent, options?)` returns a frozen `AgentFunction` with frozen category/lineage metadata. It starts `agent.generate({ signal: controller.signal })` synchronously after any runner-bound tool installation, using Agent's native per-run cancellation seam: an already-aborted signal starts no provider call, and cancellation settles as a partial result. The full `AgentResult` is projected through Agent-owned `agentResultToJSON`; malformed structural results become `ToolboxError('TOOL')`. With a runner, an already-running real agent is rejected before its workflow-tool binding can be replaced, so another Toolbox branch observes the running state. Genuine agent/provider errors retain identity.
340
+ 12. **The workflow-function adapters are opt-in and exact at the JSON boundary.** `createToolFunction(tools, name)` resolves the live tool, awaits `tool.execute(controller.input, { signal: controller.signal })` — the execution context carries the running task's own cancellation, so a handler that reads `context.signal` observes the run's cancellation rather than an uncancellable stand-in — then deep-gates the unknown return through `parseJSONValue`; a missing binding or non-JSON result throws `ToolboxError('TOOL')`, while a genuine tool throw passes through by identity. It rejects `name === WORKFLOW_TOOL_NAME` at construction so the reserved live workflow tool cannot be adapted back into a workflow registry. `createAgentFunction(agent, options?)` returns a frozen `AgentFunction` with frozen category/lineage metadata. It starts `agent.generate({ signal: controller.signal })` synchronously after any runner-bound tool installation, using Agent's native per-run cancellation seam: an already-aborted signal starts no provider call, and cancellation settles as a partial result. The full `AgentResult` is projected through Agent-owned `agentResultToJSON`; malformed structural results become `ToolboxError('TOOL')`. With a runner, an already-running real agent is rejected before its workflow-tool binding can be replaced, so another Toolbox branch observes the running state. Genuine agent/provider errors retain identity.
341
341
 
342
342
  13. **The lean `summary` / full `description` split, and `createDescribeTool`'s expansion seam.** `createWorkflowTool`, `createWorkspaceTool`, and `createAgentTool` each set `ToolInterface.summary` (`@orkestrel/tool`) to a frozen one-sentence constant (`WORKFLOW_TOOL_SUMMARY` / `WORKSPACE_TOOL_SUMMARY` / `AGENT_TOOL_SUMMARY`) alongside their unchanged full teaching `description`; `ToolManagerInterface.definitions()` advertises `summary ?? description`, so a model sees the lean text by default. `createDescribeTool(tools)` is the on-demand expansion: given a registered `name`, it looks the tool up through `tools.tool(name)` and returns its full `tool.description` (falling back to `tool.summary`, then a placeholder, when a tool has neither) — never truncated, never re-derived. Each summary's text points the model at `describe('<name>')` for the full schema.
343
343
 
@@ -349,7 +349,7 @@ Before shape branching or property reads, the handler snapshots untrusted `args`
349
349
 
350
350
  17. **`'records'` / `'find'` / `'links'` truncate against a configured cap, never silently.** `createDatabaseTool`'s `'records'` (cap: `DatabaseToolOptions.limit`, default `DATABASE_TOOL_LIMIT`) and `createRelationTool`'s `'find'` / `'links'` (cap: `RelationToolOptions.limit`, default `RELATION_TOOL_LIMIT`) each probe one row past the effective limit (`clampQuery` for the database tool; `resolveLimit` plus the same probe for `'find'`) to detect truncation without a separate count round trip, returning `{ rows, count, truncated, limit }` (`'links'`: `{ keys, count, truncated, limit }`) — `truncated` is `true` exactly when storage held more than `limit` matching rows/keys. A caller's own `query.limit` / `limit` can only lower the effective cap, never raise it past the configured ceiling.
351
351
 
352
- 18. **A typed upstream failure re-surfaces in-process as a typed `ToolboxError`, never passes through raw.** `createDatabaseTool` catches a `@orkestrel/database` `DatabaseError` and re-throws a typed `DATABASE` `ToolboxError` carrying the original `DatabaseErrorCode` in `context.code` (`inferDatabaseCode`); `createRelationTool` does the same for a `@orkestrel/relation` `RelationError` → `RELATION` (`inferRelationCode`, checked first) and, underneath it, a `DatabaseError` → `DATABASE` (mirroring the database tool's own mapping) — so an in-process catch or direct `tool.execute(args)` call sees exactly one of `TOOL` (this tool's own guards: malformed args, unknown manager/model/database/driver), `RELATION`, or `DATABASE`, never an unwrapped upstream error. A `ToolboxError` already thrown by this tool's own guards passes through unwrapped (never re-mapped a second time). A `ToolManagerInterface.execute` registry caller does not receive that code or context; it receives only the flattened message string.
352
+ 18. **A typed upstream failure re-surfaces in-process as a typed `ToolboxError`, never passes through raw.** `createDatabaseTool` catches a `@orkestrel/database` `DatabaseError` and re-throws a typed `DATABASE` `ToolboxError` carrying the original `DatabaseErrorCode` in `context.code` (`inferDatabaseCode`); `createRelationTool` does the same for a `@orkestrel/relation` `RelationError` → `RELATION` (`inferRelationCode`, checked first) and, underneath it, a `DatabaseError` → `DATABASE` (mirroring the database tool's own mapping) — so an in-process catch or direct `tool.execute(args, context)` call sees exactly one of `TOOL` (this tool's own guards: malformed args, unknown manager/model/database/driver), `RELATION`, or `DATABASE`, never an unwrapped upstream error. A `ToolboxError` already thrown by this tool's own guards passes through unwrapped (never re-mapped a second time). A `ToolManagerInterface.execute` registry caller does not receive that code or context; it receives only the flattened message string.
353
353
 
354
354
  19. **`DatabaseToolOptions.readonly` gates every mutating operation up front.** When `true`, `createDatabaseTool` throws a typed `TOOL` `ToolboxError` for `'create'` / `'add'` / `'set'` / `'update'` / `'remove'` / `'destroy'` before resolving a database or touching storage — the non-mutating operations (`'tables'` / `'get'` / `'records'` / `'count'` / `'aggregate'`) are unaffected. The exported `DATABASE_TOOL_MUTATIONS` membership list is a runtime-frozen readonly array, so a consumer cannot mutate it to bypass this gate. There is no equivalent gate on `createRelationTool` — its `'link'` / `'unlink'` writes are ungated (relation-tool callers rely on the underlying database's own access controls, if any).
355
355
 
@@ -359,7 +359,7 @@ Before shape branching or property reads, the handler snapshots untrusted `args`
359
359
 
360
360
  22. **`createDatabaseTool` durability and ownership are narrower than they look.** A lazily re-minted database over the default in-memory driver yields an empty database — only the `DatabaseDefinition` schema persists in `store`, never rows; durable rows need a persistent driver factory registered in `DatabaseToolOptions.drivers`. A cached live database is never evolved in place. To adopt a new target `version`, create a new database id backed by a versioned persistent driver, or close the old tool lifecycle and construct a new tool whose stored definition carries the new schema metadata and stamp. `'destroy'` closes whatever handle is cached for the id, including an embedder-supplied `DatabaseToolOptions.databases` handle — the embedder relinquishes that handle's lifecycle to this tool for any id it wires in. `timeout`, when present, must be a nonnegative safe integer and is validated when the tool is constructed. Its fresh abort signal is passed only to `records`, `count`, `aggregate`, `add`, `set`, `update`, and `remove`, whose current table APIs accept operation options; it does not bound store resolution, construction, schema inspection, `get`, or `close`, and is not an outer deadline. The tool assumes the single-writer, non-reentrant model `@orkestrel/database` itself assumes — concurrent tool calls against one id are not serialized by this tool. Unlike `'records'` / `'find'` / `'links'`, `'get'` is uncapped by `DatabaseToolOptions.limit` (bounded only by the caller's `key` array size).
361
361
 
362
- 23. **`createEndpointTool` enforces its advertised inferred schema through a normalizing parse by default (`@orkestrel/contract`'s `schemaToShape`), with an explicit `validate: false` opt-out.** `parameters` is inferred once at construction (`samplesToSchema` and `schemaToObject` over `definition.samples`, tuned by `EndpointToolOptions.format`/`enum`) — the same object-rooted schema is compiled once (`schemaToShape` → `createContract`) into the contract the tool's `execute` `parse`s every call's `args` through before `definition.execute` runs. The parse coerces a scalar to its inferred type where the house parsers coerce (a number to/from a numeric string, a boolean from `'1'`/`'0'`/`'true'`/`'false'`/`1`/`0`) — `definition.execute` receives the coerced values (for example `7` sent for a string slot arrives as `'7'`), not the raw call args. A call whose `args` fails to parse into a record — a required key missing, or a value not coercible to its slot's type — throws a typed `TOOL` `ToolboxError` carrying the compiled contract's structured `explain` faults (through `contract.explain(args)`), and `definition.execute` is never called. Beyond that coercion, enforcement is structural: required keys, `enum` membership, and numeric bounds — `format` annotations (`email`, `date-time`, `uuid`, `uri`, ...) are never asserted, mirroring `@orkestrel/contract`'s own widening-only law for `schemaToShape` (a `format: true`-tuned endpoint still accepts a non-conforming string in that slot); a key outside the closed inferred schema is silently dropped, not rejected — `@orkestrel/contract`'s own `parse` grants that same leniency to any closed object. `EndpointToolOptions.validate: false` disables that enforcement: the tool's `execute` calls `definition.execute(args)` with the model-supplied `args` exactly as received, never re-parsed, coerced, or checked against the advertised schema. `createInferTool`'s own call args are, as before, always validated against `inferToolShape` (a hand-written shape) regardless — it is the schema inference's caller, not an inferred schema's consumer. `createEndpointTool`'s and `createInferTool`'s inferred schemas surface sample-derived strings verbatim (property names, and enum entries when opted in), so treat sample data intended for schema inference as untrusted content whenever the resulting schema will be advertised to other agents. `createInferTool`'s optional `candidates` call arg is the opposite seam: it checks values against the same call's freshly inferred schema (compiled per-call, because the schema itself is derived from that call's `samples`) and returns a uniform `{ index, valid, coercible, faults? }` entry per candidate. `valid` is a strict `.is` guard verdict, not a normalizing `.parse` — `7` against an inferred string slot is invalid (no coercion), where the same value would be silently coerced to `'7'` by `createEndpointTool`'s enforcement. `coercible` (`checker.parse(candidate) !== undefined`) answers that separate question directly — would `createEndpointTool`'s default enforcement admit this value — and, by the house parse/guard round-trip, is always `true` on a `valid: true` entry. Because `@orkestrel/contract`'s `.explain` mirrors `.parse`'s leniency, not `.is`'s strictness, a strictly-invalid but coercible candidate (`7` against a string slot) yields `{ valid: false, coercible: true, faults: [] }` — empty faults, because the mismatch normalization would silently fix is not one `.explain` reports; `faults` populates only for a non-coercible mismatch (`coercible: false`). `checker.is` / `.parse` / `.explain` are total over JSON-safe input — a JSON-safe hostile candidate (a `__proto__`-carrying object, deep nesting) reaches each of them and yields a bounded, non-throwing per-candidate verdict; a non-JSON-safe candidate (a throwing-getter `Proxy`) never reaches the checker — it fails the outer `args` parse and rejects the whole call as a `TOOL` error, with no per-candidate verdict. `candidates` is uncapped in count (any array length is accepted), but each individual check is bounded (`.explain`'s fault list and the checker's own schema are both bounded by the inference limits already governing `samples`), so the total per-call cost is linear in `candidates.length`.
362
+ 23. **`createEndpointTool` enforces its advertised inferred schema through a normalizing parse by default (`@orkestrel/contract`'s `schemaToShape`), with an explicit `validate: false` opt-out.** `parameters` is inferred once at construction (`samplesToSchema` and `schemaToObject` over `definition.samples`, tuned by `EndpointToolOptions.format`/`enum`) — the same object-rooted schema is compiled once (`schemaToShape` → `createContract`) into the contract the tool's `execute` `parse`s every call's `args` through before `definition.execute` runs. The parse coerces a scalar to its inferred type where the house parsers coerce (a number to/from a numeric string, a boolean from `'1'`/`'0'`/`'true'`/`'false'`/`1`/`0`) — `definition.execute` receives the coerced values (for example `7` sent for a string slot arrives as `'7'`), not the raw call args. A call whose `args` fails to parse into a record — a required key missing, or a value not coercible to its slot's type — throws a typed `TOOL` `ToolboxError` carrying the compiled contract's structured `explain` faults (through `contract.explain(args)`), and `definition.execute` is never called. Beyond that coercion, enforcement is structural: required keys, `enum` membership, and numeric bounds — `format` annotations (`email`, `date-time`, `uuid`, `uri`, ...) are never asserted, mirroring `@orkestrel/contract`'s own widening-only law for `schemaToShape` (a `format: true`-tuned endpoint still accepts a non-conforming string in that slot); a key outside the closed inferred schema is silently dropped, not rejected — `@orkestrel/contract`'s own `parse` grants that same leniency to any closed object. `EndpointToolOptions.validate: false` disables that enforcement: the tool's `execute` calls `definition.execute` with the model-supplied `args` exactly as received, never re-parsed, coerced, or checked against the advertised schema. The execution context the tool's `execute` receives reaches `definition.execute` unchanged whether or not that enforcement runs, so a handler that reads `context.signal` observes the caller's cancellation. `createInferTool`'s own call args are, as before, always validated against `inferToolShape` (a hand-written shape) regardless — it is the schema inference's caller, not an inferred schema's consumer. `createEndpointTool`'s and `createInferTool`'s inferred schemas surface sample-derived strings verbatim (property names, and enum entries when opted in), so treat sample data intended for schema inference as untrusted content whenever the resulting schema will be advertised to other agents. `createInferTool`'s optional `candidates` call arg is the opposite seam: it checks values against the same call's freshly inferred schema (compiled per-call, because the schema itself is derived from that call's `samples`) and returns a uniform `{ index, valid, coercible, faults? }` entry per candidate. `valid` is a strict `.is` guard verdict, not a normalizing `.parse` — `7` against an inferred string slot is invalid (no coercion), where the same value would be silently coerced to `'7'` by `createEndpointTool`'s enforcement. `coercible` (`checker.parse(candidate) !== undefined`) answers that separate question directly — would `createEndpointTool`'s default enforcement admit this value — and, by the house parse/guard round-trip, is always `true` on a `valid: true` entry. Because `@orkestrel/contract`'s `.explain` mirrors `.parse`'s leniency, not `.is`'s strictness, a strictly-invalid but coercible candidate (`7` against a string slot) yields `{ valid: false, coercible: true, faults: [] }` — empty faults, because the mismatch normalization would silently fix is not one `.explain` reports; `faults` populates only for a non-coercible mismatch (`coercible: false`). `checker.is` / `.parse` / `.explain` are total over JSON-safe input — a JSON-safe hostile candidate (a `__proto__`-carrying object, deep nesting) reaches each of them and yields a bounded, non-throwing per-candidate verdict; a non-JSON-safe candidate (a throwing-getter `Proxy`) never reaches the checker — it fails the outer `args` parse and rejects the whole call as a `TOOL` error, with no per-candidate verdict. `candidates` is uncapped in count (any array length is accepted), but each individual check is bounded (`.explain`'s fault list and the checker's own schema are both bounded by the inference limits already governing `samples`), so the total per-call cost is linear in `candidates.length`.
363
363
 
364
364
  24. **The terminal seam speaks whole forms, and `@orkestrel/form` owns their shape.** `promptToolShape` bounds a call to `{ to, schema }` and `answerToolShape`'s `'answer'` arm to `{ id, values }`, each of `schema` and `values` admitted only as exact JSON — Toolbox does not restate the form schema as a second contract. `createPromptTool` then hands `schema` to `parseForm`; a refusal (an unknown control, a duplicate field name, a `'select'` / `'checkbox'` field without usable choices, any other schema fault) throws a typed `TOOL` `ToolboxError` and nothing parks, so a malformed schema can never become a form nobody can answer. A parsed schema becomes a live form through `createForm` and is passed to `manager.ask(from, to, form)`, whose settled `FormValues` is the tool's return value — one record keyed by field name, however many fields the form declared. `createAnswerTool` narrows `values` with `isFormValues` before `manager.answer`, so a non-form payload is refused as `TOOL` rather than reaching the parked form. Expiry reaches one code from either side: a `TerminalError('EXPIRE')` from the broker and a `FormError('ABANDONED')` from the form itself both re-surface as `EXPIRE`, so a caller branches on Toolbox's own code and never on which layer timed the form out. A failed apply carries the manager's `TerminalAnswerError` through: `context.reason` is `'unknown'` / `'rejected'` / `'target'`, and a `'rejected'` apply also carries the per-field `errors` in `context.errors` so the asking side can name which field failed.
365
365
 
@@ -420,7 +420,7 @@ const store = createMemoryWorkflowStore()
420
420
  const runner = createWorkflowRunner()
421
421
  const tool = createWorkflowTool(definition, runner, { store })
422
422
 
423
- const summary = await tool.execute({}) // native runner checkpoints through `store`
423
+ const summary = await tool.execute({}, { signal: new AbortController().signal }) // native runner checkpoints through `store`
424
424
  // summary: { status: 'completed', count: 1, durable: true }
425
425
  const snapshot = await store.get('ingest')
426
426
  const restored = snapshot === undefined ? undefined : createRestoredWorkflow(snapshot)
@@ -1006,6 +1006,13 @@ const result = await tools.execute({
1006
1006
  // `{ validate: false }` as the second argument to `createEndpointTool` for raw passthrough.
1007
1007
  ```
1008
1008
 
1009
+ The handler takes the caller's execution context as its second parameter, the same one a
1010
+ `@orkestrel/tool` handler takes. `createEndpointTool` forwards that context unchanged whichever way
1011
+ it handled the args — parsed against the inferred schema, or passed through raw under
1012
+ `{ validate: false }` — so a handler that reads `context.signal` observes the caller's cancellation.
1013
+ The preceding fence declares `args` alone, which still satisfies `EndpointHandler`: a handler reads
1014
+ the signal only where it has cancellable work to abandon.
1015
+
1009
1016
  ## Tests
1010
1017
 
1011
1018
  - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/core` and `src/server` bijection (value and type exports, spanning both barrels), this guide's `## Patterns` fences resolving to real exports (per-specifier) with resolving imports, the `DefinitionStoreInterface` and `DatabaseResolver` method bijections, and the equality gate: every `Summary` cell against its declaration's description paragraph, the titled `Authoring and running a workflow through the tool with a real ToolManager` 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.