@orkestrel/scaffold 0.0.94 → 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.
- package/dist/bin/main.js +5 -5
- package/dist/bin/main.js.map +1 -1
- package/dist/host/claude/agents/orkestrel.md +52 -52
- package/dist/host/guides/agent.md +244 -38
- package/dist/host/guides/browser.md +2371 -2510
- package/dist/host/guides/codec.md +2 -2
- package/dist/host/guides/mcp.md +182 -100
- package/dist/host/guides/msg.md +14 -22
- package/dist/host/guides/ollama.md +172 -11
- package/dist/host/guides/server.md +122 -37
- package/dist/host/guides/toolbox.md +47 -40
- package/dist/host/manifest.json +10 -46
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/src/core/index.cjs +5 -5
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +5 -5
- package/dist/src/core/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -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.
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
`
|
|
275
|
-
signal
|
|
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
|
-
|
|
281
|
-
|
|
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,
|
|
286
|
-
|
|
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
|
|
291
|
-
|
|
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
|
|
330
|
-
|
|
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()`
|
|
538
|
-
|
|
539
|
-
work is requests and claimed upgraded sockets
|
|
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() //
|
|
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
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
connection
|
|
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` |
|
|
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
|
|
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)
|
|
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
|
|
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.
|