stitchkit 0.59.4 → 0.60.0
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/README.md +7 -3
- package/dist/agent-runtime/admission-lanes.d.ts +35 -0
- package/dist/agent-runtime/admission-lanes.d.ts.map +1 -0
- package/dist/agent-runtime/coordinator.d.ts +36 -2
- package/dist/agent-runtime/coordinator.d.ts.map +1 -1
- package/dist/agent-runtime/events.d.ts +7 -0
- package/dist/agent-runtime/events.d.ts.map +1 -1
- package/dist/agent-runtime/history.d.ts +7 -0
- package/dist/agent-runtime/history.d.ts.map +1 -1
- package/dist/agent-runtime/models.d.ts +2 -3
- package/dist/agent-runtime/models.d.ts.map +1 -1
- package/dist/agent-runtime/observability.d.ts.map +1 -1
- package/dist/agent-runtime/run-execution.d.ts +31 -0
- package/dist/agent-runtime/run-execution.d.ts.map +1 -0
- package/dist/agent-runtime/runtime-internals.d.ts +25 -0
- package/dist/agent-runtime/runtime-internals.d.ts.map +1 -0
- package/dist/agent-runtime/runtime-result.d.ts +11 -0
- package/dist/agent-runtime/runtime-result.d.ts.map +1 -0
- package/dist/agent-runtime/runtime.d.ts +16 -15
- package/dist/agent-runtime/runtime.d.ts.map +1 -1
- package/dist/agent-runtime/schemas.d.ts.map +1 -1
- package/dist/agent-runtime/store-driver.d.ts.map +1 -1
- package/dist/agent-runtime/store.d.ts +9 -2
- package/dist/agent-runtime/store.d.ts.map +1 -1
- package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
- package/dist/agent-runtime/terminal-status.d.ts +13 -0
- package/dist/agent-runtime/terminal-status.d.ts.map +1 -0
- package/dist/agent-runtime.d.ts +5 -3
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +211 -156
- package/dist/application/activity.d.ts +7 -2
- package/dist/application/activity.d.ts.map +1 -1
- package/dist/application/grammy.d.ts +6 -0
- package/dist/application/grammy.d.ts.map +1 -1
- package/dist/application/health.d.ts +9 -2
- package/dist/application/health.d.ts.map +1 -1
- package/dist/application/kernel.d.ts +49 -3
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/schemas.d.ts +42 -0
- package/dist/application/schemas.d.ts.map +1 -1
- package/dist/application-opentelemetry.js +2 -2
- package/dist/application.d.ts +3 -3
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +16 -8
- package/dist/cli.js +6 -6
- package/dist/contract/errors.d.ts +6 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/declaration.d.ts +345 -0
- package/dist/declaration.d.ts.map +1 -0
- package/dist/declaration.js +183 -0
- package/dist/{index-5c8n2123.js → index-1zencgcb.js} +64 -30
- package/dist/{index-sa2mbwa7.js → index-2akar0rq.js} +2 -2
- package/dist/{index-41wm56v0.js → index-3hn10n4c.js} +1 -1
- package/dist/{index-f6pymtqe.js → index-3w74r08v.js} +2 -2
- package/dist/{index-7c0gkyvj.js → index-6fbp58g8.js} +2 -2
- package/dist/{index-gnvzyw0k.js → index-8es1hrv4.js} +2 -2
- package/dist/{index-mgf6cdkm.js → index-8eywc9zv.js} +35 -1
- package/dist/{index-3nwng7hs.js → index-8pc5s3pz.js} +4 -4
- package/dist/{index-28cqssm3.js → index-9h1vba8n.js} +6 -0
- package/dist/{index-0nc0cddp.js → index-a59da114.js} +17 -28
- package/dist/{index-s8w2y7kr.js → index-f7521k0x.js} +22 -16
- package/dist/{index-y2rb7dwx.js → index-pzyt11ch.js} +5 -1
- package/dist/{index-mr617n62.js → index-svqhcrrj.js} +1 -1
- package/dist/{index-ps5pxp3h.js → index-v6e8hnkx.js} +7 -1
- package/dist/{index-1tkyng3g.js → index-vkk06pv1.js} +1 -9
- package/dist/{index-2nzpcfbp.js → index-x8v8b7rc.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/internal/observability-sink.d.ts.map +1 -1
- package/dist/internal/route-pattern.d.ts +9 -0
- package/dist/internal/route-pattern.d.ts.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +5 -5
- package/dist/observability/index.js +5 -5
- package/dist/observability/sanitize.d.ts +3 -1
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/remote.js +3 -3
- package/dist/server/error-hook.d.ts +10 -3
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -12
- package/dist/server/middleware/cors.d.ts +5 -2
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io-config.d.ts +68 -3
- package/dist/server/socket-io-config.d.ts.map +1 -1
- package/dist/server/socket-io-node.d.ts +1 -1
- package/dist/server/socket-io-node.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +1 -1
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.js +8 -10
- package/dist/tools/flatten-join.d.ts +2 -0
- package/dist/tools/flatten-join.d.ts.map +1 -1
- package/dist/tools/flatten.d.ts.map +1 -1
- package/dist/tools/mcp-prepare.d.ts +0 -2
- package/dist/tools/mcp-prepare.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +0 -3
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/schema.d.ts +0 -4
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools.js +9 -9
- package/llms-full.txt +924 -68
- package/llms.txt +1 -0
- package/package.json +6 -2
package/llms-full.txt
CHANGED
|
@@ -33,24 +33,36 @@ realtime, `@tanstack/react-query` + `react-query-kit` for React). See
|
|
|
33
33
|
|
|
34
34
|
## Entrypoints
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
| `stitchkit
|
|
50
|
-
| `stitchkit/
|
|
51
|
-
| `stitchkit/
|
|
52
|
-
| `stitchkit/
|
|
53
|
-
| `stitchkit/
|
|
36
|
+
Every entrypoint is import-safe for one environment — keeping server-only code
|
|
37
|
+
(`Bun.serve`, the MCP SDK) out of browser bundles.
|
|
38
|
+
|
|
39
|
+
Each also declares how settled it is. **Stable** means the shape changes rarely
|
|
40
|
+
and only with a reason worth a migration. **Evolving** means the shape is still
|
|
41
|
+
being found and may be redefined in any minor — always with a
|
|
42
|
+
`### ⚠️ Breaking changes` entry and a migration section, never silently. Both
|
|
43
|
+
are legitimate choices; the difference is how often you should expect to read
|
|
44
|
+
the changelog. Moving an entrypoint from evolving to stable is a decision on its
|
|
45
|
+
own, recorded as an ADR.
|
|
46
|
+
|
|
47
|
+
| Import | Use in | Maturity | Holds |
|
|
48
|
+
|--------|--------|----------|-------|
|
|
49
|
+
| `stitchkit` | browser **and** server | stable | `defineContract`, `createClient`, `createHttpClient`, `createSocketIOClient`, `parseSSE`, the error model |
|
|
50
|
+
| `stitchkit/contract` | browser **and** server | stable | the contract layer alone — `defineContract`, errors, pagination |
|
|
51
|
+
| `stitchkit/server` | server (Bun) | stable | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
|
|
52
|
+
| `stitchkit/node` | server (Node ≥ 22) | stable | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
|
|
53
|
+
| `stitchkit/tools` | server | stable | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
|
|
54
|
+
| `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
|
|
55
|
+
| `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
|
|
56
|
+
| `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
|
|
57
|
+
| `stitchkit/observability` | server | stable | request/tool event projections — `createObservability`, trace context, sanitisation |
|
|
58
|
+
| `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
|
|
59
|
+
| `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
|
|
60
|
+
| `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
|
|
61
|
+
| `stitchkit/agent-runtime` | server | evolving | optional durable conversation/run loop, history, models, prompts, fencing and events |
|
|
62
|
+
| `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
|
|
63
|
+
| `stitchkit/application` | server | evolving | managed resource graph, readiness, admission, schedules and bounded shutdown |
|
|
64
|
+
| `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
|
|
65
|
+
| `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
|
|
54
66
|
|
|
55
67
|
Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
|
|
56
68
|
code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
|
|
@@ -155,7 +167,9 @@ map — feature → packages:
|
|
|
155
167
|
| React data layer (`stitchkit/react`) | `@tanstack/react-query` `react-query-kit` |
|
|
156
168
|
| **Socket.IO server on Bun** | `socket.io` `@socket.io/bun-engine` |
|
|
157
169
|
| **Socket.IO server on Node** | `socket.io` |
|
|
158
|
-
| Socket.IO client | `socket.io-client` |
|
|
170
|
+
| Socket.IO client | `socket.io-client` (brings `@socket.io/component-emitter`, which the browser declarations reference for types only) |
|
|
171
|
+
| grammY lifecycle adapters (`stitchkit/application/grammy`) | `grammy` |
|
|
172
|
+
| OpenTelemetry gauges (`stitchkit/application/opentelemetry`) | `@opentelemetry/api` |
|
|
159
173
|
|
|
160
174
|
```bash
|
|
161
175
|
bun add socket.io @socket.io/bun-engine # e.g. the Socket.IO server on Bun
|
|
@@ -375,12 +389,13 @@ With this policy, omitting `expose` materializes `['HTTP']` on the returned
|
|
|
375
389
|
endpoint. MCP, Agent and CLI then require an explicit endpoint array. The plain
|
|
376
390
|
factory and `defineContract` keep the default-on behaviour above.
|
|
377
391
|
|
|
378
|
-
Tool transports (`MCP`, `AGENT`) skip
|
|
379
|
-
`multipart` (a file upload is not a tool call)
|
|
392
|
+
Tool transports (`MCP`, `AGENT`) skip four kinds of endpoint automatically:
|
|
393
|
+
`multipart` (a file upload is not a tool call),
|
|
380
394
|
[`rawResponse`](./server.md#raw-response-endpoints) (its answer is bytes, which
|
|
381
|
-
a tool result cannot carry — it would serialize to `{}`),
|
|
395
|
+
a tool result cannot carry — it would serialize to `{}`),
|
|
382
396
|
[`responseMeta`](./server.md#typed-json-response-metadata) (outbound HTTP
|
|
383
|
-
headers have no meaning on a tool call)
|
|
397
|
+
headers have no meaning on a tool call) and `rawBody` (HTTP-only by
|
|
398
|
+
construction).
|
|
384
399
|
|
|
385
400
|
## `toolName`
|
|
386
401
|
|
|
@@ -395,11 +410,13 @@ derivation from the method key + prefix (`users` + `create` ⇒ `create_user`,
|
|
|
395
410
|
**Every tool name — derived or explicit — must match `[a-zA-Z0-9_-]` and be at
|
|
396
411
|
most 64 characters**, the character class every major provider accepts.
|
|
397
412
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
`
|
|
401
|
-
`
|
|
402
|
-
|
|
413
|
+
Derivation normalises **per half**, and the halves differ (→ ADR 0035). The
|
|
414
|
+
*service* half collapses everything outside `[a-zA-Z0-9_]` to `_`, hyphen
|
|
415
|
+
included: `bot-status` + `get` ⇒ `get_bot_status`, `admin/analytics` + `get` ⇒
|
|
416
|
+
`get_admin_analytics`. The *method* half keeps its hyphen and normalises only
|
|
417
|
+
what no provider accepts (`[^a-zA-Z0-9_-]`), because a hyphenated method key has
|
|
418
|
+
always shipped a legal name that a client config may already pin: `notes` +
|
|
419
|
+
`get-user` ⇒ `get-user_notes`. An explicit `toolName` is taken verbatim.
|
|
403
420
|
|
|
404
421
|
A prefix with *no* usable character at all (`'///'`, `'_'`, a fully non-ASCII
|
|
405
422
|
prefix) and any explicit `toolName` outside the accepted class **throw at
|
|
@@ -1740,14 +1757,21 @@ without repeating transport configuration.
|
|
|
1740
1757
|
`headers` as a function is the hook for runtime tokens — a bearer token or any
|
|
1741
1758
|
short-lived credential — re-evaluated on every request.
|
|
1742
1759
|
|
|
1743
|
-
Expected 401 policy is explicit and contract-driven.
|
|
1744
|
-
|
|
1745
|
-
|
|
1760
|
+
Expected 401 policy is explicit and contract-driven. Name the operations you
|
|
1761
|
+
mean with `contractEndpointMatchers(contract, ['login'])`, or omit the second
|
|
1762
|
+
argument to select every HTTP operation in that contract. Pass the same
|
|
1746
1763
|
`ContractClientConfig` as the typed client when routes use a static or dynamic
|
|
1747
1764
|
`pathPrefix`; dynamic matchers require `stripPrefixKeys`, so the helper can
|
|
1748
|
-
compile the prefix structure without a concrete tenant id.
|
|
1749
|
-
|
|
1750
|
-
|
|
1765
|
+
compile the prefix structure without a concrete tenant id.
|
|
1766
|
+
|
|
1767
|
+
**Matching is by PATH, not by operation.** Each named operation compiles to its
|
|
1768
|
+
path pattern, matched exactly by segments including params and trailing
|
|
1769
|
+
wildcards — so a shared *prefix* never suppresses a neighbour, but two
|
|
1770
|
+
operations on the **same path** are one matcher. If `login` is `POST /session`
|
|
1771
|
+
and `session` is `GET /session`, naming `login` suppresses the expected-401
|
|
1772
|
+
signal for both, and a real expired session stops raising `unauthorized`. Give
|
|
1773
|
+
an operation you must distinguish a path of its own, or narrow the policy with
|
|
1774
|
+
your own `(pathname) => boolean`.
|
|
1751
1775
|
|
|
1752
1776
|
### Unix domain sockets
|
|
1753
1777
|
|
|
@@ -2179,8 +2203,10 @@ By default every endpoint is a tool on every transport. `expose` narrows it:
|
|
|
2179
2203
|
{ method: 'GET', path: '/lookup', desc: 'Look up a price', expose: ['MCP'] } // MCP tool only
|
|
2180
2204
|
```
|
|
2181
2205
|
|
|
2182
|
-
|
|
2183
|
-
upload (not a tool call),
|
|
2206
|
+
Four kinds of endpoint are **never** tools, whatever `expose` says: a
|
|
2207
|
+
`multipart` upload (not a tool call), a `rawBody` endpoint (HTTP-only by
|
|
2208
|
+
construction), a [`responseMeta`](./server.md#typed-json-response-metadata)
|
|
2209
|
+
endpoint (outbound HTTP headers mean nothing on a tool call), and a
|
|
2184
2210
|
[`rawResponse`](./server.md#raw-response-endpoints) endpoint (its answer is
|
|
2185
2211
|
bytes — a tool result cannot carry them, and it would reach the model as `{}`).
|
|
2186
2212
|
Pin the full list with `listToolNames` in a snapshot test.
|
|
@@ -2188,10 +2214,12 @@ Pin the full list with `listToolNames` in a snapshot test.
|
|
|
2188
2214
|
`desc` is the tool description the model reads — write it for the model, not
|
|
2189
2215
|
just for a human. The tool name defaults
|
|
2190
2216
|
to a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
|
|
2191
|
-
`get_widget`); set `toolName` for an explicit one. Derivation normalises
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
`
|
|
2217
|
+
`get_widget`); set `toolName` for an explicit one. Derivation normalises **per half**
|
|
2218
|
+
(→ ADR 0035): the service half turns everything outside `[a-zA-Z0-9_]` into `_`,
|
|
2219
|
+
hyphen included (`bot-status` ⇒ `get_bot_status`), while the method half keeps
|
|
2220
|
+
its hyphen (`get-user` ⇒ `get-user_notes`) because such a name has always been
|
|
2221
|
+
legal and may already be pinned in a client config. A name is *accepted* if it
|
|
2222
|
+
matches `[a-zA-Z0-9_-]`. A name that
|
|
2195
2223
|
still cannot be delivered (illegal explicit `toolName`, over 64 characters, or a
|
|
2196
2224
|
prefix with no usable character) throws at mount rather than at the first model
|
|
2197
2225
|
call —
|
|
@@ -3363,6 +3391,11 @@ updated: 2026-08-22
|
|
|
3363
3391
|
|
|
3364
3392
|
# Agent application runtime
|
|
3365
3393
|
|
|
3394
|
+
> **Maturity: evolving.** This surface is still finding its shape and may be
|
|
3395
|
+
> redefined in any minor release — always with a `### ⚠️ Breaking changes` entry
|
|
3396
|
+
> and a migration section, never silently. If your application already owns the
|
|
3397
|
+
> conversation loop, `mountAgent` from `stitchkit/tools` is the stable path.
|
|
3398
|
+
|
|
3366
3399
|
`stitchkit/agent-runtime` is the server-only, opinionated layer above
|
|
3367
3400
|
`mountAgent`. Use it when the application wants Stitchkit to own conversation
|
|
3368
3401
|
mechanics: durable acceptance, history projection, the AI SDK stream loop,
|
|
@@ -3554,17 +3587,38 @@ resolves to the same terminal run. Coalescing never mutates the active run.
|
|
|
3554
3587
|
while the predecessor still owns managed callbacks. A hung predecessor blocks
|
|
3555
3588
|
the lane in the first version.
|
|
3556
3589
|
|
|
3557
|
-
Shutdown is two-phase
|
|
3590
|
+
Shutdown is two-phase, and both budgets carry the names they carry everywhere
|
|
3591
|
+
else in Stitchkit:
|
|
3558
3592
|
|
|
3559
3593
|
```ts
|
|
3560
|
-
await runtime.close({
|
|
3594
|
+
const closed = await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
|
|
3595
|
+
if (!closed.settled) {
|
|
3596
|
+
console.warn(`exiting with ${closed.remaining} run(s) still in flight`)
|
|
3597
|
+
}
|
|
3561
3598
|
```
|
|
3562
3599
|
|
|
3563
|
-
`close` first rejects new process-local admissions and gives active runs
|
|
3564
|
-
|
|
3565
|
-
reason `shutdown`; `forceTimeoutMs` bounds the
|
|
3566
|
-
non-cooperative model or tool
|
|
3567
|
-
|
|
3600
|
+
`close` first rejects new process-local admissions and gives active runs
|
|
3601
|
+
`gracePeriodMs` to finish on their own. Only after that budget expires does it
|
|
3602
|
+
abort them with reason `shutdown`; `forceTimeoutMs` then bounds the settlement
|
|
3603
|
+
wait for a non-cooperative model or tool, measured from the abort — the two
|
|
3604
|
+
budgets add up rather than overlapping.
|
|
3605
|
+
|
|
3606
|
+
**`close()` reports what it achieved rather than promising an outcome it cannot
|
|
3607
|
+
reach.** The result is `{ settled, timedOut, remaining }`: `settled` when every
|
|
3608
|
+
in-flight run finished, `timedOut` with a `remaining` count when the force
|
|
3609
|
+
budget expired first. There is no combination of budgets that is both bounded
|
|
3610
|
+
and guaranteed to leave nothing in flight — that is the trade the budgets exist
|
|
3611
|
+
to make, and the result is where you read which side of it you got:
|
|
3612
|
+
|
|
3613
|
+
| budgets | behaviour | can return with a run in flight |
|
|
3614
|
+
|---------|-----------|---------------------------------|
|
|
3615
|
+
| neither | aborts immediately, waits for settlement | no — unbounded wait |
|
|
3616
|
+
| `gracePeriodMs` only | waits, aborts, then waits for settlement | no — unbounded wait |
|
|
3617
|
+
| `forceTimeoutMs` only | aborts immediately, waits at most that long | **yes** — `timedOut` |
|
|
3618
|
+
| both | waits, aborts, waits at most that long | **yes** — `timedOut` |
|
|
3619
|
+
|
|
3620
|
+
A caller that wants the old shape still writes `await runtime.close(…)` and
|
|
3621
|
+
ignores the result. A caller deciding whether to exit the process reads it.
|
|
3568
3622
|
Durably queued records rejected from the local queue remain recoverable through
|
|
3569
3623
|
`scanRecoverable`; close never marks them terminal on its own.
|
|
3570
3624
|
|
|
@@ -3581,7 +3635,7 @@ terminal event and operator metrics; a loser settles from canonical state withou
|
|
|
3581
3635
|
## Store operations
|
|
3582
3636
|
|
|
3583
3637
|
`AgentRuntimeStore` remains the runtime-facing aggregate. Application adapters
|
|
3584
|
-
implement the smaller `AgentRuntimeStoreDriver
|
|
3638
|
+
implement the smaller `AgentRuntimeStoreDriver` rather than these nine members:
|
|
3585
3639
|
|
|
3586
3640
|
- `acceptInputAndAssignRun`
|
|
3587
3641
|
- `acquireRun`
|
|
@@ -3590,7 +3644,10 @@ implement the smaller `AgentRuntimeStoreDriver`, not these eight transitions:
|
|
|
3590
3644
|
- `recoverRun`
|
|
3591
3645
|
- `commitRunTerminal`
|
|
3592
3646
|
- `replaceCompactedRange`
|
|
3593
|
-
- `loadSnapshot`
|
|
3647
|
+
- `loadSnapshot`
|
|
3648
|
+
- `scanRecoverable` — one **bounded page** of recoverable runs; `recover()`
|
|
3649
|
+
calls this and nothing else, so an adapter that implements the interface has
|
|
3650
|
+
everything recovery needs
|
|
3594
3651
|
|
|
3595
3652
|
Every mutation carries an expected run revision or snapshot version. Input
|
|
3596
3653
|
assignment additionally carries an idempotency identity. A conflict is a
|
|
@@ -3702,9 +3759,13 @@ base64 blobs into the neutral history store.
|
|
|
3702
3759
|
|
|
3703
3760
|
To send a stored attachment back to a multimodal model, configure
|
|
3704
3761
|
`history.resolveFile`. It maps the neutral file reference to AI SDK file data
|
|
3705
|
-
(URL, bytes, provider reference or text). Without a resolver the
|
|
3706
|
-
`unresolvedFile` policy is
|
|
3707
|
-
|
|
3762
|
+
(URL, bytes, provider reference or text). Without a resolver the
|
|
3763
|
+
`unresolvedFile` policy applies, and it is **`omit`** by default: the file part
|
|
3764
|
+
simply does not reach the provider. Choose `text` for a describing placeholder —
|
|
3765
|
+
the filename or media type, never the storage reference, because that string is
|
|
3766
|
+
an address inside your infrastructure and this content travels upstream — or
|
|
3767
|
+
`error` to fail loudly. The `error` message does name the reference: it is
|
|
3768
|
+
thrown into your process, where you are owed the whole story.
|
|
3708
3769
|
|
|
3709
3770
|
## Compaction
|
|
3710
3771
|
|
|
@@ -3759,6 +3820,11 @@ updated: 2026-08-23
|
|
|
3759
3820
|
|
|
3760
3821
|
# Managed application kernel
|
|
3761
3822
|
|
|
3823
|
+
> **Maturity: evolving.** This surface is still finding its shape and may be
|
|
3824
|
+
> redefined in any minor release — always with a `### ⚠️ Breaking changes` entry
|
|
3825
|
+
> and a migration section, never silently. The server, contract and client
|
|
3826
|
+
> surfaces it composes are stable.
|
|
3827
|
+
|
|
3762
3828
|
Use `stitchkit/application` when several process-local resources must become
|
|
3763
3829
|
ready and shut down as one application. Keep using the lower-level server and
|
|
3764
3830
|
signal APIs when one managed server is already the complete lifecycle boundary.
|
|
@@ -3877,10 +3943,35 @@ A required long-lived completion that rejects after startup makes readiness
|
|
|
3877
3943
|
false and health unhealthy. Stitchkit records the failure but does not restart
|
|
3878
3944
|
the resource or process.
|
|
3879
3945
|
|
|
3880
|
-
`
|
|
3946
|
+
`ApplicationResourceShutdown.failures` names the phase that failed — `start`,
|
|
3947
|
+
`ready`, `completion`, `admission`, `drain`, `close`, `force` — and nothing
|
|
3948
|
+
else. To learn *why*, pass `onResourceFailure`:
|
|
3949
|
+
|
|
3950
|
+
```ts
|
|
3951
|
+
createApplication({
|
|
3952
|
+
id: 'app',
|
|
3953
|
+
resources,
|
|
3954
|
+
onResourceFailure: ({ resourceId, phase, error }) => {
|
|
3955
|
+
log.error({ resourceId, phase, err: error }, 'managed resource failed')
|
|
3956
|
+
},
|
|
3957
|
+
})
|
|
3958
|
+
```
|
|
3959
|
+
|
|
3960
|
+
It fires for every phase, including the one place the cause would otherwise be
|
|
3961
|
+
lost entirely: an **optional** resource failing to start. A required one
|
|
3962
|
+
rethrows, so its cause reaches you anyway; an optional one is swallowed on
|
|
3963
|
+
purpose, because the application keeps running. A throwing observer cannot
|
|
3964
|
+
break the lifecycle it observes.
|
|
3965
|
+
|
|
3966
|
+
`createApplicationHealthHandler` publishes an `ApplicationStatusProjection`
|
|
3881
3967
|
through a Fetch-compatible handler, suitable for a raw route on Bun or Node.
|
|
3882
|
-
|
|
3883
|
-
|
|
3968
|
+
The projection carries the verdict — the application's own `id`, `lifecycle`,
|
|
3969
|
+
`health`, `ready`, `capturedAt` and resource **counts** — and never the internal
|
|
3970
|
+
topology: no per-resource ids, no `dependsOn` edges, no process `epoch`, no
|
|
3971
|
+
admission counters.
|
|
3972
|
+
Those stay in `getSnapshot()`, which never leaves the process, and in whatever
|
|
3973
|
+
telemetry you wire to it. Product-specific probes may be composed beside it; do
|
|
3974
|
+
not put secrets or raw provider failures in the response.
|
|
3884
3975
|
|
|
3885
3976
|
For the conventional three-route surface, reuse the same semantics instead of
|
|
3886
3977
|
copying them into the application:
|
|
@@ -3895,8 +3986,11 @@ const rawRoutes = [
|
|
|
3895
3986
|
]
|
|
3896
3987
|
```
|
|
3897
3988
|
|
|
3898
|
-
`status` always returns the current
|
|
3899
|
-
while starting, draining or stopped.
|
|
3989
|
+
`status` always returns the current published projection with HTTP 200,
|
|
3990
|
+
including while starting, draining or stopped. To read the full snapshot — every
|
|
3991
|
+
resource, its dependency edges and the live counters — call `app.getSnapshot()`
|
|
3992
|
+
in-process; there is no option that publishes it over HTTP, because these routes
|
|
3993
|
+
are meant to be reachable. The two probes retain the existing
|
|
3900
3994
|
readiness/liveness status and `Retry-After` policy.
|
|
3901
3995
|
|
|
3902
3996
|
Applications that already own an OpenTelemetry SDK may inject its `Meter` into
|
|
@@ -4733,6 +4827,12 @@ createServer({
|
|
|
4733
4827
|
realtime.emit('note:created', note)
|
|
4734
4828
|
```
|
|
4735
4829
|
|
|
4830
|
+
`socket.io` and, on Bun, `@socket.io/bun-engine` are optional peers, resolved
|
|
4831
|
+
lazily so a project that never opens a socket does not have to install them. If
|
|
4832
|
+
you ship **one self-contained file** to a machine with no `node_modules`, tell
|
|
4833
|
+
the framework how to load them so your bundler can put them inside — see
|
|
4834
|
+
[shipping one self-contained artifact](./testing-and-deployment.md#shipping-one-self-contained-artifact).
|
|
4835
|
+
|
|
4736
4836
|
The canonical room-broadcast example below is executed by the test suite. Its
|
|
4737
4837
|
body is kept byte-identical to `packages/core/examples/realtime-room.ts`.
|
|
4738
4838
|
|
|
@@ -4768,6 +4868,12 @@ await createSocketIOServer({
|
|
|
4768
4868
|
})
|
|
4769
4869
|
```
|
|
4770
4870
|
|
|
4871
|
+
`cors` is **optional**. Omit it when the browser reaches this server on its
|
|
4872
|
+
own origin — Socket.IO then emits no CORS headers, which is same-origin only.
|
|
4873
|
+
Supply it only for a genuinely cross-origin browser: naming a foreign origin is
|
|
4874
|
+
naming where the code will run, and a repository that does not have to know
|
|
4875
|
+
should not say.
|
|
4876
|
+
|
|
4771
4877
|
The wrapper-owned fields (`cors` / `path` / `transports` / `ping*` /
|
|
4772
4878
|
`allowRequest`) take
|
|
4773
4879
|
precedence over the same keys in `serverOptions`. On Bun the engine-level options
|
|
@@ -5676,11 +5782,19 @@ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
|
|
|
5676
5782
|
`CONFLICT`, `RATE_LIMITED`, `VALIDATION_ERROR`, `FILE_INVALID_PATH`,
|
|
5677
5783
|
`FILE_OUTSIDE_ROOT`, `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`,
|
|
5678
5784
|
`FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
|
|
5679
|
-
`REALTIME_CONTRACT_VIOLATION`, `
|
|
5785
|
+
`REALTIME_CONTRACT_VIOLATION`, `APPLICATION_NOT_ACCEPTING`, `WAIT_TIMEOUT`,
|
|
5786
|
+
`WAIT_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
|
|
5787
|
+
`OPERATION_NOT_SUCCEEDED`, `INTERNAL_SERVER_ERROR` — a set that grows in
|
|
5680
5788
|
ordinary releases, each addition named in the changelog. They are
|
|
5681
5789
|
published as **`STITCH_ERROR_STATUS`** (the `code → status` map) and
|
|
5682
5790
|
**`StitchErrorCode`** (its `keyof`), with **`isStitchErrorCode()`** (→ ADR 0026).
|
|
5683
5791
|
|
|
5792
|
+
> This list is enumerated here for reading, and the enumeration is what goes
|
|
5793
|
+
> stale — it once dropped `APPLICATION_NOT_ACCEPTING`, which made the map below
|
|
5794
|
+
> stop compiling. `Object.keys(STITCH_ERROR_STATUS)` is the answer that cannot;
|
|
5795
|
+
> `packages/core/tests/error-registry-completeness.test.ts` keeps the registry
|
|
5796
|
+
> itself complete against the source.
|
|
5797
|
+
|
|
5684
5798
|
If you translate stitch's framework errors into your own wire codes in an
|
|
5685
5799
|
`onError` hook, you choose how the set's growth reaches you. Keying the map by
|
|
5686
5800
|
`StitchErrorCode` makes it exhaustive, so a code stitch adds or renames becomes
|
|
@@ -5691,7 +5805,10 @@ envelope is a published contract, at the cost of an edit on those releases:
|
|
|
5691
5805
|
// Exhaustive on purpose: the annotation is what turns a new stitch code into a
|
|
5692
5806
|
// compile error here. Drop it (or use `satisfies` on a partial map) to let an
|
|
5693
5807
|
// unmapped code travel as itself instead.
|
|
5694
|
-
|
|
5808
|
+
// `Partial`, not an exhaustive `Record`: the set grows in ordinary releases, and
|
|
5809
|
+
// a map written exhaustively stops compiling on every addition. Codes you leave
|
|
5810
|
+
// out fall through to `unmappedCode`.
|
|
5811
|
+
const STITCH_TO_APP: Partial<Record<StitchErrorCode, AppCode>> = {
|
|
5695
5812
|
NOT_FOUND: 'NOT_FOUND', METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED',
|
|
5696
5813
|
BAD_REQUEST: 'VALIDATION_ERROR', VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
5697
5814
|
UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', CONFLICT: 'CONFLICT',
|
|
@@ -6114,10 +6231,18 @@ want the row to say something other than what the framework derived (a domain
|
|
|
6114
6231
|
code, a curated message, structured issues). The framework writes only when the
|
|
6115
6232
|
context carries nothing yet, so your value always wins.
|
|
6116
6233
|
|
|
6117
|
-
**Endpoint identity is automatic
|
|
6118
|
-
`(serviceName, action)` into the context at
|
|
6119
|
-
|
|
6120
|
-
pre-handler 400. Nothing to
|
|
6234
|
+
**Endpoint identity is automatic — for contract operations.** The framework
|
|
6235
|
+
writes the matched operation's `(serviceName, action)` into the context at
|
|
6236
|
+
route-match, *before* validation, so both are present on every event for a
|
|
6237
|
+
request that matched a contract route, including a pre-handler 400. Nothing to
|
|
6238
|
+
wire.
|
|
6239
|
+
|
|
6240
|
+
They are **absent** on events that never reached a contract route: a raw route
|
|
6241
|
+
(`openApiRoute`, an MCP mount, any `rawRoutes` entry), a request short-circuited
|
|
6242
|
+
by `onRequest`, and an unmatched 404 or 405. Both fields are optional in
|
|
6243
|
+
`RequestEvent` for exactly that reason — a sink with a `NOT NULL` column on
|
|
6244
|
+
either, or a dashboard grouping by `(serviceName, action)`, has to allow for
|
|
6245
|
+
it.
|
|
6121
6246
|
|
|
6122
6247
|
When failure attribution itself is asynchronous, use `createErrorHook`'s
|
|
6123
6248
|
`onError(error, info, ctx, endpoint)` observer. The framework awaits it before
|
|
@@ -6269,7 +6394,9 @@ A payload goes into an audit row only after `sanitizePayload`:
|
|
|
6269
6394
|
- **secret-named keys are masked** — a key whose words include a secret term is
|
|
6270
6395
|
redacted (`password`, `sessionToken`, `X-Api-Key`, `authorization`, `cookie`,
|
|
6271
6396
|
…), while identifiers that merely contain one (`authorId`, `sessionCount`,
|
|
6272
|
-
`tokenizer`) survive
|
|
6397
|
+
`tokenizer`) survive. This applies wherever a key exists — an object field and
|
|
6398
|
+
a `Map` entry alike, at any depth. A `Set` member has no key, so nothing there
|
|
6399
|
+
is masked by name;
|
|
6273
6400
|
- **binary blobs** (`Uint8Array`, `Blob`, `FormData`) collapse to metadata —
|
|
6274
6401
|
never the bytes;
|
|
6275
6402
|
- the result is **capped** — anything over the byte limit becomes a preview.
|
|
@@ -6712,6 +6839,41 @@ runtime or type-only package outside a case's budget fails with both the case id
|
|
|
6712
6839
|
and package name, so an accidental eager import cannot hide behind another
|
|
6713
6840
|
fixture's transitive dependency.
|
|
6714
6841
|
|
|
6842
|
+
Beside the matrix, one case is about the ARTIFACT rather than the inventory: a
|
|
6843
|
+
program that injects the Socket.IO peer loaders is bundled and started in a
|
|
6844
|
+
directory with no `node_modules`, under Node and under Bun with auto-install
|
|
6845
|
+
off. It is checked as a pair — the same program without the loaders must fail
|
|
6846
|
+
there — because a positive result alone would only show that the machine
|
|
6847
|
+
running the check happens to have the package, which is precisely how this class
|
|
6848
|
+
of defect stays invisible in development.
|
|
6849
|
+
|
|
6850
|
+
### Shipping one self-contained artifact
|
|
6851
|
+
|
|
6852
|
+
Stitchkit resolves the optional Socket.IO peers through a variable, so a
|
|
6853
|
+
consumer bundling an unrelated `stitchkit/server` export never has to resolve
|
|
6854
|
+
them. A bundler cannot follow that, which matters if you ship **one file** to a
|
|
6855
|
+
machine with no `node_modules`: the package never enters the artifact and the
|
|
6856
|
+
failure arrives at start-up, not at build time.
|
|
6857
|
+
|
|
6858
|
+
Pass the loaders, and the literal lives in your source where your bundler sees
|
|
6859
|
+
it:
|
|
6860
|
+
|
|
6861
|
+
```ts
|
|
6862
|
+
const socket = await createSocketIOServer({
|
|
6863
|
+
cors: { origin: env.CORS_ORIGIN },
|
|
6864
|
+
peers: {
|
|
6865
|
+
server: () => import('socket.io'),
|
|
6866
|
+
// Bun only — on Node the engine is never asked for, and bundling a
|
|
6867
|
+
// Bun-only package for Node asks the bundler to carry `bun:` imports.
|
|
6868
|
+
bunEngine: () => import('@socket.io/bun-engine'),
|
|
6869
|
+
},
|
|
6870
|
+
})
|
|
6871
|
+
```
|
|
6872
|
+
|
|
6873
|
+
Omit `peers` and nothing changes: the peers stay lazy and are resolved from the
|
|
6874
|
+
machine. Both peers must be listed as dependencies of the package you bundle,
|
|
6875
|
+
because they now genuinely are.
|
|
6876
|
+
|
|
6715
6877
|
## Deployment
|
|
6716
6878
|
|
|
6717
6879
|
### Build
|
|
@@ -6855,6 +7017,43 @@ domain run-state remain application resources and close explicitly after server
|
|
|
6855
7017
|
drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
|
|
6856
7018
|
`shutdown()`.
|
|
6857
7019
|
|
|
7020
|
+
### Let the supervisor start the role, not a launcher
|
|
7021
|
+
|
|
7022
|
+
A shutdown chain treats a **second** signal as "stop waiting, force it now".
|
|
7023
|
+
That is deliberate — it is how an operator escalates. It also means an extra
|
|
7024
|
+
process between the supervisor and your role can escalate for you, by
|
|
7025
|
+
accident.
|
|
7026
|
+
|
|
7027
|
+
Point the supervisor at the process that serves, not at a script runner that
|
|
7028
|
+
starts it:
|
|
7029
|
+
|
|
7030
|
+
```js
|
|
7031
|
+
// supervisor entry — the role's own process
|
|
7032
|
+
script: 'bun', args: ['dist/index.js']
|
|
7033
|
+
|
|
7034
|
+
// NOT this: a launcher in between
|
|
7035
|
+
script: 'bun', args: ['run', 'start']
|
|
7036
|
+
```
|
|
7037
|
+
|
|
7038
|
+
Measured under PM2 with the launcher form: the stop signal reaches both
|
|
7039
|
+
processes, the launcher forwards its copy, and the server sees two presses in
|
|
7040
|
+
two turns. A declared 15 s grace period ended in 1.3 ms with
|
|
7041
|
+
`outcome: "forced"`, `reason: "signal"` — no requests drained, and the only
|
|
7042
|
+
visible trace was a non-zero exit code. With the direct form, the same stop is
|
|
7043
|
+
`outcome: "clean"`.
|
|
7044
|
+
|
|
7045
|
+
A workspace-filtering launcher (`bun run --filter <pkg> start`) fails harder:
|
|
7046
|
+
the signal never reaches the role at all, so no shutdown runs and the process
|
|
7047
|
+
is torn down when the launcher dies.
|
|
7048
|
+
|
|
7049
|
+
Two habits make this visible instead of silent:
|
|
7050
|
+
|
|
7051
|
+
- log the outcome in `onComplete`, not just the exit code —
|
|
7052
|
+
`result.outcome`, `result.reason` and `result.durationMs` are the difference
|
|
7053
|
+
between "drained" and "was cut off";
|
|
7054
|
+
- keep the supervisor's kill timeout **at or above** the grace period you
|
|
7055
|
+
declared, so a slow drain is not racing its own supervisor.
|
|
7056
|
+
|
|
6858
7057
|
### Stdio process signals
|
|
6859
7058
|
|
|
6860
7059
|
An MCP stdio handle has `close()`, not managed HTTP `shutdown()` with a force
|
|
@@ -7208,6 +7407,12 @@ current one *up to* your target, and apply each snippet.
|
|
|
7208
7407
|
change at each call site. There are no deprecation shims to lean on; the old
|
|
7209
7408
|
shape is gone, so every site must move.
|
|
7210
7409
|
|
|
7410
|
+
Then read this file's **`## Released migration: X.Y.Z`** section for each of
|
|
7411
|
+
those versions. That is the half the changelog does not carry: what else
|
|
7412
|
+
stops working because of the change, and what changes *silently* rather than
|
|
7413
|
+
at compile time — projected history returning fewer messages, a dashboard
|
|
7414
|
+
field going blank. `bun run check` in step 6 cannot see either.
|
|
7415
|
+
|
|
7211
7416
|
5. **Bump and install.** `bun add stitchkit@<target>` (or update the range), then
|
|
7212
7417
|
`bun install`. Note the caret: `^0.7.0` is `< 0.8.0`, so crossing a breaking
|
|
7213
7418
|
minor is always an explicit version bump, never automatic.
|
|
@@ -7217,7 +7422,193 @@ current one *up to* your target, and apply each snippet.
|
|
|
7217
7422
|
runtime): bootstrap the server, one HTTP request, and any feature you rely on
|
|
7218
7423
|
(Socket.IO connect, an MCP tool call, a multipart upload, …).
|
|
7219
7424
|
|
|
7220
|
-
##
|
|
7425
|
+
## Released migration: 0.60.0
|
|
7426
|
+
|
|
7427
|
+
### close() says what it achieved
|
|
7428
|
+
|
|
7429
|
+
`AgentRuntime.close()` returned `Promise<void>` and its contract promised three
|
|
7430
|
+
things that cannot hold together. It now returns
|
|
7431
|
+
`{ settled, timedOut, remaining }`.
|
|
7432
|
+
|
|
7433
|
+
Nothing breaks if you ignore the value:
|
|
7434
|
+
|
|
7435
|
+
```ts
|
|
7436
|
+
await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
|
|
7437
|
+
```
|
|
7438
|
+
|
|
7439
|
+
What DOES change is any code that relied on the old sentence "`close()` never
|
|
7440
|
+
returns while a run is still in flight" while naming a `forceTimeoutMs`. That
|
|
7441
|
+
was never true — the force budget exists precisely to stop waiting — so if your
|
|
7442
|
+
process exits straight after `close()`, read the result:
|
|
7443
|
+
|
|
7444
|
+
```ts
|
|
7445
|
+
const closed = await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
|
|
7446
|
+
if (!closed.settled) {
|
|
7447
|
+
// `closed.remaining` runs were still in flight. They stay recoverable
|
|
7448
|
+
// through `scanRecoverable`; close never marks them terminal on its own.
|
|
7449
|
+
logger.warn({ remaining: closed.remaining }, 'exiting with runs in flight')
|
|
7450
|
+
}
|
|
7451
|
+
```
|
|
7452
|
+
|
|
7453
|
+
If you want the old guarantee, omit `forceTimeoutMs`: that is the one
|
|
7454
|
+
combination in which `close()` cannot return with a run in flight — and its wait
|
|
7455
|
+
is unbounded, which is the trade.
|
|
7456
|
+
|
|
7457
|
+
A TypeScript consumer that wrote `const done: void = await runtime.close()` is
|
|
7458
|
+
the only shape that stops compiling.
|
|
7459
|
+
|
|
7460
|
+
### one name per concept
|
|
7461
|
+
|
|
7462
|
+
`AgentModelDeclaration` was a bare alias of `AgentModelDescriptor`, so the same
|
|
7463
|
+
type reached consumers under two exported names:
|
|
7464
|
+
|
|
7465
|
+
```ts
|
|
7466
|
+
// before
|
|
7467
|
+
defineModelRegistry<Record<string, AgentModelDeclaration>>({ … })
|
|
7468
|
+
// after
|
|
7469
|
+
defineModelRegistry<Record<string, AgentModelDescriptor>>({ … })
|
|
7470
|
+
```
|
|
7471
|
+
|
|
7472
|
+
A find-and-replace covers it; there is no behavioural change. Kept name is
|
|
7473
|
+
`AgentModelDescriptor`, the one paired with `AgentModelDescriptorSchema` and
|
|
7474
|
+
returned by `registry.descriptor()`.
|
|
7475
|
+
|
|
7476
|
+
Two names that look like the same case and are **not** being merged:
|
|
7477
|
+
`SocketEventMap` aliases Socket.IO's `EventsMap` so a vendor's name stays out of
|
|
7478
|
+
our signatures, and `AgentRun.ownerId` holds the same value a runtime publishes
|
|
7479
|
+
as `runtimeEpoch` on its events. The latter is one identity in two roles — "who
|
|
7480
|
+
owns this run" and "which runtime emitted this event" — and both declarations
|
|
7481
|
+
now say so. Renaming either would make the fencing comparison read worse.
|
|
7482
|
+
|
|
7483
|
+
### a reachable public surface
|
|
7484
|
+
|
|
7485
|
+
Three exports that the public API required and did not provide, plus one option
|
|
7486
|
+
it accepted and ignored.
|
|
7487
|
+
|
|
7488
|
+
```ts
|
|
7489
|
+
// before — identified by string, because the class was exported nowhere
|
|
7490
|
+
if (error instanceof Error && error.name === 'AgentRuntimeConflictError') …
|
|
7491
|
+
// after
|
|
7492
|
+
import { AgentRuntimeConflictError } from 'stitchkit/agent-runtime'
|
|
7493
|
+
if (error instanceof AgentRuntimeConflictError) …
|
|
7494
|
+
```
|
|
7495
|
+
|
|
7496
|
+
`ActivityTokenBrand` is exported, so `ActivityProjection` can be implemented by
|
|
7497
|
+
a test double. `STITCH_ERROR_STATUS` gained `APPLICATION_NOT_ACCEPTING` (503) —
|
|
7498
|
+
only an exhaustive `satisfies Record<StitchErrorCode, …>` map stops compiling,
|
|
7499
|
+
and the fix is one line. `GRAMMY_WEBHOOK_NOT_ACCEPTING` is deliberately *not*
|
|
7500
|
+
registered: the registry belongs to the generic core, and a provider name has no
|
|
7501
|
+
place in a union every consumer imports. It travels as itself through a partial
|
|
7502
|
+
`codeMap`.
|
|
7503
|
+
|
|
7504
|
+
`application.shutdown()` no longer accepts `retryAfterSeconds`. Delete it from
|
|
7505
|
+
the call — the kernel never read it. If you meant the HTTP `Retry-After` a
|
|
7506
|
+
draining server sends, that lives on `managedServerResource({ retryAfterSeconds })`,
|
|
7507
|
+
where it always did the work.
|
|
7508
|
+
|
|
7509
|
+
### one shutdown vocabulary
|
|
7510
|
+
|
|
7511
|
+
```ts
|
|
7512
|
+
// before
|
|
7513
|
+
await runtime.close({ drainTimeoutMs: 30_000, forceTimeoutMs: 5_000 })
|
|
7514
|
+
// after
|
|
7515
|
+
await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
|
|
7516
|
+
```
|
|
7517
|
+
|
|
7518
|
+
The rename is mechanical. The behaviour change under it is not, and it is the
|
|
7519
|
+
reason the rename waited: two combinations were traps.
|
|
7520
|
+
`close({ drainTimeoutMs })` with no force budget aborted the runs and returned
|
|
7521
|
+
**without waiting for them to settle** — so naming a budget gave a weaker
|
|
7522
|
+
guarantee than naming none — and `close({ forceTimeoutMs })` with no drain
|
|
7523
|
+
budget never read the force budget, leaving an unbounded wait. Both now behave
|
|
7524
|
+
as their names say.
|
|
7525
|
+
|
|
7526
|
+
> **Superseded.** This section once ended "and `close()` never returns while a
|
|
7527
|
+
> run is in flight". That was never true with a force budget — the budget exists
|
|
7528
|
+
> to stop waiting — and `close()` now returns `{ settled, timedOut, remaining }`
|
|
7529
|
+
> instead of promising it. See *Released migration: 0.60.0 → close() says what
|
|
7530
|
+
> it achieved*, above.
|
|
7531
|
+
|
|
7532
|
+
If your shutdown path measured how long `close()` took, expect it to take
|
|
7533
|
+
longer in exactly the case where it used to return early — that is the fix, not
|
|
7534
|
+
a regression. Defaults stay per-surface: `ShutdownOptions.gracePeriodMs`
|
|
7535
|
+
defaults to 30 seconds, and the runtime's omitted budget still means "abort
|
|
7536
|
+
immediately", the behaviour `close()` has always had.
|
|
7537
|
+
|
|
7538
|
+
### unresolved attachments are omitted
|
|
7539
|
+
|
|
7540
|
+
`history.unresolvedFile` defaulted to `text`, and the placeholder it produced
|
|
7541
|
+
carried the storage reference:
|
|
7542
|
+
|
|
7543
|
+
```ts
|
|
7544
|
+
// before — the provider received your object key
|
|
7545
|
+
// "[attachment: s3://bucket/tenants/42/invoice.pdf]"
|
|
7546
|
+
history: {}
|
|
7547
|
+
// after — omitted entirely by default; ask for a placeholder explicitly
|
|
7548
|
+
history: { unresolvedFile: 'text' } // "[attachment: invoice.pdf]"
|
|
7549
|
+
```
|
|
7550
|
+
|
|
7551
|
+
Two things to check. If a prompt relied on the model seeing *something* where an
|
|
7552
|
+
unresolved file was, set `unresolvedFile: 'text'` — behaviour otherwise changes
|
|
7553
|
+
silently, since an omitted part produces no error. And if any stored transcript
|
|
7554
|
+
or provider log contains the old placeholder, it contains your storage layout;
|
|
7555
|
+
treat those as disclosed. The `error` policy still names the reference, because
|
|
7556
|
+
it is thrown into your process rather than sent upstream.
|
|
7557
|
+
|
|
7558
|
+
### one bounded recoverable scan
|
|
7559
|
+
|
|
7560
|
+
`AgentRuntimeStore` had two scans: a mandatory unbounded one and an optional
|
|
7561
|
+
paged one. The runtime only ever called the optional one, so implementing the
|
|
7562
|
+
interface as written produced a store that threw on its first `recover()`:
|
|
7563
|
+
|
|
7564
|
+
```ts
|
|
7565
|
+
// before — the mandatory member was dead, the needed one was optional
|
|
7566
|
+
{
|
|
7567
|
+
scanRecoverable: () => loadEveryRecoverableSnapshot(),
|
|
7568
|
+
scanRecoverablePage: ({ cursor, limit }) => page(cursor, limit),
|
|
7569
|
+
}
|
|
7570
|
+
// after — one member, the bounded signature the driver already used
|
|
7571
|
+
{ scanRecoverable: ({ cursor, limit }) => page(cursor, limit) }
|
|
7572
|
+
```
|
|
7573
|
+
|
|
7574
|
+
Delete the unbounded implementation rather than porting it: loading every
|
|
7575
|
+
recoverable conversation to start is the shape 0.59.0 and ADR 0101 moved away
|
|
7576
|
+
from, and nothing calls it now. If you built on `createAgentRuntimeStore()` you
|
|
7577
|
+
have nothing to do **if you only pass it to the runtime** — it implements the
|
|
7578
|
+
bounded page for you from the same driver member. If you CALL it yourself, the
|
|
7579
|
+
member changed shape: `scanRecoverable()` took no argument and returned
|
|
7580
|
+
snapshots; it now takes `{ cursor?, limit }` — `limit` is required — and returns
|
|
7581
|
+
one page of descriptors.
|
|
7582
|
+
```ts
|
|
7583
|
+
// before: const stale = await store.scanRecoverable()
|
|
7584
|
+
// after: const { items, nextCursor } = await store.scanRecoverable({ limit: 100 })
|
|
7585
|
+
```
|
|
7586
|
+
|
|
7587
|
+
### published application status
|
|
7588
|
+
|
|
7589
|
+
`createApplicationHealthHandler` and `createApplicationOperationalHandlers` no
|
|
7590
|
+
longer serialise the whole `ApplicationSnapshot`. They publish
|
|
7591
|
+
`ApplicationStatusProjection` — the verdict plus resource counts:
|
|
7592
|
+
|
|
7593
|
+
```ts
|
|
7594
|
+
// before — the response named every resource and its dependency edges
|
|
7595
|
+
const { resources } = await fetch('/status').then((r) => r.json())
|
|
7596
|
+
resources[0].dependsOn
|
|
7597
|
+
// after — the topology is read in-process, where it always belonged
|
|
7598
|
+
app.getSnapshot().resources[0].dependsOn
|
|
7599
|
+
```
|
|
7600
|
+
|
|
7601
|
+
The consequence to check is not compilation — it is whatever already consumes
|
|
7602
|
+
these routes. A dashboard that drew the dependency graph from `/status`, or an
|
|
7603
|
+
alert keyed on `admission.pending`, goes blank rather than red: the fields are
|
|
7604
|
+
absent, not zero. Both are available from `getSnapshot()`, so the fix is to read
|
|
7605
|
+
them in the process that owns the application and publish them on a channel you
|
|
7606
|
+
control. If a route was reachable from outside your network, treat the previous
|
|
7607
|
+
payload as disclosed and rotate nothing but assume the topology is known.
|
|
7608
|
+
|
|
7609
|
+
## Released migration: 0.59.0
|
|
7610
|
+
|
|
7611
|
+
### Normalized agent runtime persistence
|
|
7221
7612
|
|
|
7222
7613
|
`AgentRuntimeStoreDriver` no longer reads and rewrites a lifetime `AgentStoredState` JSON
|
|
7223
7614
|
aggregate. Migrate that row and its recoverable/archive projections once:
|
|
@@ -7255,6 +7646,85 @@ createAgentRuntimeStore({
|
|
|
7255
7646
|
If an application implements `AgentRuntimeStore` directly, its duplicate result must also include
|
|
7256
7647
|
the canonical `run` and the retained `assistant` for a terminal run.
|
|
7257
7648
|
|
|
7649
|
+
## Released migration: 0.58.0
|
|
7650
|
+
|
|
7651
|
+
### The default history projection rejects invalid chronology
|
|
7652
|
+
|
|
7653
|
+
`projectAgentHistory` no longer forwards records a provider contract cannot
|
|
7654
|
+
accept. A completed assistant record before the first user message, and an
|
|
7655
|
+
assistant record whose tool calls have no matching results, are omitted with an
|
|
7656
|
+
inspectable decision instead of being sent upstream:
|
|
7657
|
+
|
|
7658
|
+
```ts
|
|
7659
|
+
// before — a leading assistant record was forwarded as-is
|
|
7660
|
+
projectAgentHistory(messages)
|
|
7661
|
+
// after — opt in explicitly, and only where the provider contract permits it
|
|
7662
|
+
projectAgentHistory(messages, { leadingAssistant: 'allow' })
|
|
7663
|
+
```
|
|
7664
|
+
|
|
7665
|
+
Two consequences the changelog does not spell out. First, the projection can now
|
|
7666
|
+
return **fewer** messages than the history holds, so any assertion or metric
|
|
7667
|
+
that compared projected length against stored length will move; read the
|
|
7668
|
+
detailed projection instead — it reports what was omitted and why, which is the
|
|
7669
|
+
supported way to see the difference. Second, the omission is silent to the
|
|
7670
|
+
provider but not to you: if a conversation suddenly loses its leading context,
|
|
7671
|
+
the decision record is where that shows up, not the transcript.
|
|
7672
|
+
|
|
7673
|
+
### Operator events redact `internalCause` by default
|
|
7674
|
+
|
|
7675
|
+
Raw provider and tool failures no longer travel in operator events unless the
|
|
7676
|
+
sink asks for them:
|
|
7677
|
+
|
|
7678
|
+
```ts
|
|
7679
|
+
// before — internalCause was present
|
|
7680
|
+
createAgentObservability({ write })
|
|
7681
|
+
// after — an explicit operator-only opt-in
|
|
7682
|
+
createAgentObservability({ write, includeInternalCause: true })
|
|
7683
|
+
```
|
|
7684
|
+
|
|
7685
|
+
The consequence to check before upgrading: any dashboard, alert or log
|
|
7686
|
+
processor keyed on `internalCause` goes blind the moment you upgrade, and it
|
|
7687
|
+
goes blind quietly — the field is absent, not empty. Set the flag on the
|
|
7688
|
+
operator sink you own. Product delivery stays redacted regardless of the flag;
|
|
7689
|
+
this option cannot widen what reaches a user.
|
|
7690
|
+
|
|
7691
|
+
## Released migration: 0.57.0
|
|
7692
|
+
|
|
7693
|
+
### Duplicate admission results carry the complete identity
|
|
7694
|
+
|
|
7695
|
+
Custom `AgentRuntimeStore` adapters must persist and return the input and
|
|
7696
|
+
assistant identities associated with an idempotency key:
|
|
7697
|
+
|
|
7698
|
+
```ts
|
|
7699
|
+
// before
|
|
7700
|
+
return { outcome: 'duplicate', runId, snapshot }
|
|
7701
|
+
|
|
7702
|
+
// after
|
|
7703
|
+
return { outcome: 'duplicate', input, inputMessageId, runId, assistantMessageId, snapshot }
|
|
7704
|
+
```
|
|
7705
|
+
|
|
7706
|
+
Prefer replacing the custom aggregate reducer with `createAgentRuntimeStore()`;
|
|
7707
|
+
its admission record and transaction driver implement this contract
|
|
7708
|
+
automatically. Historical note: 0.59.0 reshaped this driver again, so an adapter
|
|
7709
|
+
crossing both versions should read that section first and migrate once.
|
|
7710
|
+
|
|
7711
|
+
### `AgentRuntimeEvent` adds a post-commit `admission` variant
|
|
7712
|
+
|
|
7713
|
+
Add it to any exhaustive publisher switch. Its `assistant` is either the pending
|
|
7714
|
+
placeholder for a new assignment or the canonical persisted assistant for a
|
|
7715
|
+
duplicate:
|
|
7716
|
+
|
|
7717
|
+
```ts
|
|
7718
|
+
case 'admission':
|
|
7719
|
+
await persistProductProjection(event.input, event.run, event.assistant)
|
|
7720
|
+
break
|
|
7721
|
+
```
|
|
7722
|
+
|
|
7723
|
+
The consequence for an exhaustive switch written without a `default` branch is a
|
|
7724
|
+
compile error, which is the point. The consequence for a switch that *has* a
|
|
7725
|
+
`default` is worse and silent: post-commit admissions fall into it and are
|
|
7726
|
+
projected as an unknown event. Grep for publisher switches before upgrading.
|
|
7727
|
+
|
|
7258
7728
|
## Released migration: 0.56.0
|
|
7259
7729
|
|
|
7260
7730
|
### Surface manifests are version 2
|
|
@@ -7467,6 +7937,116 @@ export const implementFor = createScopedImplement<{
|
|
|
7467
7937
|
endpoints inline in the contract literal: an endpoint hoisted into a variable
|
|
7468
7938
|
widens its `scope` to `string` and is reported as undeclared.
|
|
7469
7939
|
|
|
7940
|
+
## Released migration: 0.49.0
|
|
7941
|
+
|
|
7942
|
+
### The server handle became managed
|
|
7943
|
+
|
|
7944
|
+
`createServer()` and `serveNode()` return a handle that owns admission, HTTP
|
|
7945
|
+
drain, realtime closure and one deadline-bounded runtime stop:
|
|
7946
|
+
|
|
7947
|
+
```ts
|
|
7948
|
+
// before
|
|
7949
|
+
server.stop()
|
|
7950
|
+
await socket.io.close()
|
|
7951
|
+
// after
|
|
7952
|
+
await server.shutdown({ gracePeriodMs: 30_000 })
|
|
7953
|
+
```
|
|
7954
|
+
|
|
7955
|
+
The runtime-specific instance stays reachable at `.runtime`, so an escape hatch
|
|
7956
|
+
you already rely on does not disappear — but code that closed transports itself,
|
|
7957
|
+
in its own order, is now racing the handle. Delete the manual closes rather than
|
|
7958
|
+
keeping both; the handle's result tells you what it drained and what it forced.
|
|
7959
|
+
|
|
7960
|
+
### Socket.IO mounts through the whole handle
|
|
7961
|
+
|
|
7962
|
+
```ts
|
|
7963
|
+
// before
|
|
7964
|
+
createServer({ websocket: socket.websocket, rawRoutes: [socket.route] })
|
|
7965
|
+
// after
|
|
7966
|
+
createServer({ socket })
|
|
7967
|
+
```
|
|
7968
|
+
|
|
7969
|
+
One owner for the route, the WebSocket attachment and the closure. For a raw Bun
|
|
7970
|
+
lane, keep the composed `websocket` handler and pass `socket` beside it.
|
|
7971
|
+
|
|
7972
|
+
### Bun native `routes` are gone
|
|
7973
|
+
|
|
7974
|
+
Native routes run before the Fetch handler, so they bypassed managed admission —
|
|
7975
|
+
which means they also bypassed shutdown, logging and observability, and that is
|
|
7976
|
+
why they had to go rather than be wired up:
|
|
7977
|
+
|
|
7978
|
+
```ts
|
|
7979
|
+
// before
|
|
7980
|
+
createServer({ routes: { '/health': () => Response.json({ ok: true }) } })
|
|
7981
|
+
// after
|
|
7982
|
+
createServer({ rawRoutes: [{ method: 'GET', path: '/health', handler: () => Response.json({ ok: true }) }] })
|
|
7983
|
+
```
|
|
7984
|
+
|
|
7985
|
+
### The handshake policy takes a Web `Request`
|
|
7986
|
+
|
|
7987
|
+
```ts
|
|
7988
|
+
// before
|
|
7989
|
+
createSocketIOServer({ serverOptions: { allowRequest: (req, done) => done(null, allowed(req)) } })
|
|
7990
|
+
// after
|
|
7991
|
+
createSocketIOServer({ allowRequest: (request) => allowed(request) })
|
|
7992
|
+
```
|
|
7993
|
+
|
|
7994
|
+
The Node-shaped callback is gone, and the policy is now composed with shutdown
|
|
7995
|
+
admission on both runtimes: a handshake arriving during drain is refused for
|
|
7996
|
+
you.
|
|
7997
|
+
|
|
7998
|
+
## Released migration: 0.46.0
|
|
7999
|
+
|
|
8000
|
+
### `REALTIME_CONTRACT_VIOLATION` joined the error registry
|
|
8001
|
+
|
|
8002
|
+
Realtime contract failures use the framework error model instead of a bare
|
|
8003
|
+
`ZodError`, so an exhaustive map stops compiling until the code is added:
|
|
8004
|
+
|
|
8005
|
+
```ts
|
|
8006
|
+
// before
|
|
8007
|
+
{ …, INTERNAL_SERVER_ERROR: 'internal' } satisfies Record<StitchErrorCode, string>
|
|
8008
|
+
// after
|
|
8009
|
+
{ …, INTERNAL_SERVER_ERROR: 'internal', REALTIME_CONTRACT_VIOLATION: 'internal' } satisfies Record<StitchErrorCode, string>
|
|
8010
|
+
```
|
|
8011
|
+
|
|
8012
|
+
Only an exhaustive map breaks. Since 0.56.1 `codeMap` itself is partial, so a
|
|
8013
|
+
map without the `satisfies` keeps compiling and lets the code travel as itself.
|
|
8014
|
+
|
|
8015
|
+
### `RealtimeRejectedEvent.error` is an `AppError`
|
|
8016
|
+
|
|
8017
|
+
```ts
|
|
8018
|
+
// before
|
|
8019
|
+
onRejected: ({ error }) => error.issues
|
|
8020
|
+
// after
|
|
8021
|
+
onRejected: ({ error }) => error.details?.issues // the ZodError moves to error.cause
|
|
8022
|
+
```
|
|
8023
|
+
|
|
8024
|
+
The envelope gained `reason` and `fault`. The consequence worth checking: code
|
|
8025
|
+
reading `.issues` directly does not fail to compile if the handler is loosely
|
|
8026
|
+
typed — it silently reads `undefined`. Grep for `.issues` on rejection handlers.
|
|
8027
|
+
|
|
8028
|
+
### CLI construction refuses reserved names
|
|
8029
|
+
|
|
8030
|
+
A contract field or tool named `json`, `wait`, `quiet`, `dry-run`, `help`,
|
|
8031
|
+
`version`, `wait-timeout` or `output-dir` now **throws while the CLI is built**,
|
|
8032
|
+
instead of being silently shadowed:
|
|
8033
|
+
|
|
8034
|
+
```ts
|
|
8035
|
+
// before: app schedule_job --wait 2h → {"path":"2h"}, exit 0
|
|
8036
|
+
// after: building a CLI over a contract with a "wait" field throws
|
|
8037
|
+
```
|
|
8038
|
+
|
|
8039
|
+
This one fires at startup, not at call time, so an application shipping such a
|
|
8040
|
+
field crashes on boot after the upgrade. That is deliberate — the old behaviour
|
|
8041
|
+
corrupted arguments silently — but it means the upgrade is not safe to deploy
|
|
8042
|
+
without building the CLI once locally.
|
|
8043
|
+
|
|
8044
|
+
### `createToolLogger` writes to stderr
|
|
8045
|
+
|
|
8046
|
+
stdout is the JSON-RPC channel of a stdio MCP server, and the previous
|
|
8047
|
+
`console.info` default corrupted it. Pass `log` to redirect if your process
|
|
8048
|
+
collected tool logs from stdout.
|
|
8049
|
+
|
|
7470
8050
|
## Released migration: 0.48.0
|
|
7471
8051
|
|
|
7472
8052
|
### Typed-client request options move to `.withOptions`
|
|
@@ -8286,6 +8866,203 @@ You are on the other side of this flow — see
|
|
|
8286
8866
|
allowed; write the `### ⚠️ Breaking changes` block with a before → after snippet,
|
|
8287
8867
|
bump the minor (pre-1.0), and migrate the controlled consumers in the same pass.
|
|
8288
8868
|
|
|
8869
|
+
### Where the migration section goes while the version has no number
|
|
8870
|
+
|
|
8871
|
+
Write it here, immediately under the flow above, as
|
|
8872
|
+
**`## Unreleased migration: <short slug>`**. The slug matters: several unreleased
|
|
8873
|
+
migrations may sit side by side, and each one belongs to whoever wrote it. Do
|
|
8874
|
+
**not** reuse an existing `Unreleased migration` heading for a different change —
|
|
8875
|
+
that is how the 0.57.0 migration was lost, overwritten by the next author before
|
|
8876
|
+
anyone promoted it.
|
|
8877
|
+
|
|
8878
|
+
At release, the release commit promotes every `Unreleased migration` heading into
|
|
8879
|
+
one `## Released migration: X.Y.Z`, each former heading becoming a `###`
|
|
8880
|
+
subsection under it. This is the same move the changelog makes when `[Unreleased]`
|
|
8881
|
+
becomes `## [X.Y.Z]`, and it happens in the same commit.
|
|
8882
|
+
|
|
8883
|
+
A release carrying `### ⚠️ Breaking changes` and no matching
|
|
8884
|
+
`## Released migration: X.Y.Z` is refused by `bun scripts/release-plan.ts` — in
|
|
8885
|
+
`pre-push` and again in the publishing workflow. The check starts at `0.44.0`;
|
|
8886
|
+
breaking versions older than that are covered by the summary sections near the
|
|
8887
|
+
end of this file.
|
|
8888
|
+
|
|
8889
|
+
|
|
8890
|
+
==============================================================================
|
|
8891
|
+
# Guide: Project declaration (docs/guide/declaration.md)
|
|
8892
|
+
==============================================================================
|
|
8893
|
+
|
|
8894
|
+
---
|
|
8895
|
+
title: Project declaration
|
|
8896
|
+
description: One machine-readable statement a repository makes about itself — identity, roles, build, requirements, release steps and the names of the values a deployment supplies.
|
|
8897
|
+
type: architecture
|
|
8898
|
+
status: active
|
|
8899
|
+
created: 2026-08-25
|
|
8900
|
+
updated: 2026-08-25
|
|
8901
|
+
---
|
|
8902
|
+
|
|
8903
|
+
# Project declaration
|
|
8904
|
+
|
|
8905
|
+
> **Maturity: evolving.** This surface is still finding its shape and may be
|
|
8906
|
+
> redefined in any minor release — always with a `### ⚠️ Breaking changes` entry
|
|
8907
|
+
> and a migration section, never silently.
|
|
8908
|
+
|
|
8909
|
+
> **Declaring yourself is optional.** A project with no `project.json` is a
|
|
8910
|
+
> complete stitchkit project: nothing in `stitchkit`, `stitchkit/server`,
|
|
8911
|
+
> `stitchkit/node`, `stitchkit/tools` or `stitchkit/cli` reads a declaration,
|
|
8912
|
+
> and no build, test or start path looks for one. Read this page as an offer,
|
|
8913
|
+
> not a requirement — it describes what you get if you decide to say these
|
|
8914
|
+
> things in a machine-readable way instead of in a README.
|
|
8915
|
+
|
|
8916
|
+
`stitchkit/declaration` is the schema for what a repository says about itself:
|
|
8917
|
+
what it is, the roles it runs, what it builds, what has to exist before it
|
|
8918
|
+
starts, what must happen once on release, and the **names** of the environment
|
|
8919
|
+
variables a deployment has to supply.
|
|
8920
|
+
|
|
8921
|
+
```ts
|
|
8922
|
+
import { parseProjectDeclaration } from 'stitchkit/declaration';
|
|
8923
|
+
import declaration from '../project.json';
|
|
8924
|
+
|
|
8925
|
+
export const appDeclaration = parseProjectDeclaration(declaration);
|
|
8926
|
+
```
|
|
8927
|
+
|
|
8928
|
+
`parseProjectDeclaration` checks `schemaVersion` **before** it reads any field.
|
|
8929
|
+
A reader that does not recognise the version refuses the repository instead of
|
|
8930
|
+
interpreting it partially — a half-understood declaration is the one failure
|
|
8931
|
+
mode that produces a running, wrong deployment rather than an error.
|
|
8932
|
+
|
|
8933
|
+
## Why declare yourself
|
|
8934
|
+
|
|
8935
|
+
Because the statements exist either way, and without a schema they exist three
|
|
8936
|
+
times. A repository already says how many roles it runs (in a process file),
|
|
8937
|
+
which variables it needs (in a Zod schema), what it builds (in a script) and
|
|
8938
|
+
what has to happen on release (in a README paragraph). Those four copies drift
|
|
8939
|
+
independently, and nothing fails when they do.
|
|
8940
|
+
|
|
8941
|
+
What a declaration buys:
|
|
8942
|
+
|
|
8943
|
+
- **One reader can be outside the tree.** Whatever builds a source into an
|
|
8944
|
+
artifact and binds that artifact into a deployment can validate the project
|
|
8945
|
+
with the same schema the project validates itself with, without being told
|
|
8946
|
+
anything by the author.
|
|
8947
|
+
- **The tool is not the format.** The schema ships from the framework and is
|
|
8948
|
+
plain published TypeScript plus JSON. Any side may read `project.json`
|
|
8949
|
+
through `stitchkit/declaration` — or generate a JSON Schema from it with
|
|
8950
|
+
`z.toJSONSchema` — and serve the project without asking the format's author
|
|
8951
|
+
for anything. A repository that only one specific tool can bring up is a fork,
|
|
8952
|
+
not a dependency.
|
|
8953
|
+
- **Generated instead of hand-kept.** The starter renders its supervision files
|
|
8954
|
+
and its client-safe identity module from the declaration, so a role added in
|
|
8955
|
+
one place cannot be missing in another.
|
|
8956
|
+
|
|
8957
|
+
What it does not buy: nothing here starts, supervises or deploys anything.
|
|
8958
|
+
The declaration is a statement; acting on it belongs to whatever brings a
|
|
8959
|
+
deployment to a source.
|
|
8960
|
+
|
|
8961
|
+
## The boundary rule
|
|
8962
|
+
|
|
8963
|
+
> A declaration must be complete and meaningful **when no machine exists**. A
|
|
8964
|
+
> field that cannot be filled in without knowing where the code will run is a
|
|
8965
|
+
> binding supplied by the deployment, not a declaration made by the repository.
|
|
8966
|
+
|
|
8967
|
+
So ports, hosts, absolute URLs, connection strings, machine paths, routing
|
|
8968
|
+
shape, supervision policy and secrets are never *values* in a declaration. A
|
|
8969
|
+
binding is **named** — by the variable that will carry it — and never valued:
|
|
8970
|
+
|
|
8971
|
+
```jsonc
|
|
8972
|
+
{
|
|
8973
|
+
"listener": { "portVariable": "API_PORT", "bindVariable": "BIND_HOST", "readinessPath": "/health" }
|
|
8974
|
+
}
|
|
8975
|
+
```
|
|
8976
|
+
|
|
8977
|
+
**What is guaranteed is structure**: there is nowhere in the declaration that a
|
|
8978
|
+
value of the place must go. A command is `executable` plus an `args` array — no
|
|
8979
|
+
shell string, no pipe — and no part may be an absolute path or carry an inline
|
|
8980
|
+
value, so `--port=8080` must be written `['--port', '8080']`, where it is
|
|
8981
|
+
refused as a port. A listener's variables must exist in `env.variables` with the
|
|
8982
|
+
right shapes. Unknown keys are refused rather than stripped.
|
|
8983
|
+
|
|
8984
|
+
**What is filtered is the rest**: every remaining free string is checked against
|
|
8985
|
+
`namesAMachine` — a scheme, a protocol-relative host, an absolute or
|
|
8986
|
+
home-relative path, a Windows drive, a `host:port` pair, a bare IPv4 literal.
|
|
8987
|
+
|
|
8988
|
+
The second half is hygiene, not a proof, and it is worth knowing where it stops:
|
|
8989
|
+
a secret written as its own argument and a hostname written as a plain word look
|
|
8990
|
+
like any other argument. The schema will not catch them, and it is not trying
|
|
8991
|
+
to — the point is that a complete declaration can be written before any machine
|
|
8992
|
+
exists, not that nobody can type a value into one.
|
|
8993
|
+
|
|
8994
|
+
## Three kinds of input, not two
|
|
8995
|
+
|
|
8996
|
+
The boundary rule separates code from the values of a place. There is a third
|
|
8997
|
+
kind that is neither: **data read while building**. Pages prerendered from a
|
|
8998
|
+
database depend on bytes that are not in the source and are not a binding, and
|
|
8999
|
+
moving environment variables around does not make such a build portable.
|
|
9000
|
+
|
|
9001
|
+
Decide it **per route**, not per project:
|
|
9002
|
+
|
|
9003
|
+
| what the bytes depend on | what is legitimate |
|
|
9004
|
+
| --- | --- |
|
|
9005
|
+
| only the source | static — built once, and portable |
|
|
9006
|
+
| the place (domain, port) | never static; read it from the request |
|
|
9007
|
+
| data | render at runtime · a declared snapshot with a digest · generate it as a release step |
|
|
9008
|
+
|
|
9009
|
+
The default is **render at runtime**: the build needs no database, and the page
|
|
9010
|
+
reads its data where the data lives. Choose a declared snapshot when you need
|
|
9011
|
+
static bytes — the build then reads a frozen export whose digest is a declared
|
|
9012
|
+
input, not a live database. Choose a release step when the bytes must be
|
|
9013
|
+
produced on the way to the deployment; that is the same kind of step as a
|
|
9014
|
+
migration, not a new concept.
|
|
9015
|
+
|
|
9016
|
+
A build that reads data it has not declared is not hermetic, and the fact is
|
|
9017
|
+
invisible: it succeeds on the machine that happens to have the database.
|
|
9018
|
+
|
|
9019
|
+
## What it carries
|
|
9020
|
+
|
|
9021
|
+
| key | what it says |
|
|
9022
|
+
| --- | --- |
|
|
9023
|
+
| `schemaVersion` | which format this is; checked before anything else |
|
|
9024
|
+
| `kind` | `application` or `library` |
|
|
9025
|
+
| `identity` | `slug`, `name`, `version`, `description` per locale |
|
|
9026
|
+
| `roles` | each with its own working directory, per-mode argv commands, optional listener and `drainFloorMs` |
|
|
9027
|
+
| `build` | the build command and the artifact paths it produces |
|
|
9028
|
+
| `requires` | what the code needs and does not provide, per phase (`release`, `start`) |
|
|
9029
|
+
| `release` | what must happen once before any role starts — migrations declared as bytes |
|
|
9030
|
+
| `env` | the variables a deployment must supply, by name and shape |
|
|
9031
|
+
|
|
9032
|
+
Two details are load-bearing:
|
|
9033
|
+
|
|
9034
|
+
- **A role may declare no listener at all.** A queue consumer, a bot or a
|
|
9035
|
+
scheduler is a role like any other; readiness belongs to a role rather than to
|
|
9036
|
+
the application.
|
|
9037
|
+
- **Migrations are declared as bytes** — `engine`, `root`, `lockfile` — not as a
|
|
9038
|
+
command to run. The side that can see the deployment decides what to do with
|
|
9039
|
+
them: exact contents, admission verdict, whether a preflight can be skipped
|
|
9040
|
+
because nothing touches the database.
|
|
9041
|
+
|
|
9042
|
+
## Narrowing it for your project
|
|
9043
|
+
|
|
9044
|
+
A project that speaks exactly two locales narrows the shipped schema; it does
|
|
9045
|
+
not write its own copy of the rest.
|
|
9046
|
+
|
|
9047
|
+
```ts
|
|
9048
|
+
import { z } from 'zod';
|
|
9049
|
+
import { ProjectDeclarationSchema, parseProjectDeclaration } from 'stitchkit/declaration';
|
|
9050
|
+
|
|
9051
|
+
const Narrowed = ProjectDeclarationSchema.safeExtend({
|
|
9052
|
+
identity: ProjectIdentityNarrowedSchema,
|
|
9053
|
+
});
|
|
9054
|
+
```
|
|
9055
|
+
|
|
9056
|
+
`safeExtend` keeps the refinements the shipped schema carries. Extending the
|
|
9057
|
+
declaration itself is a breaking change to a published schema and moves the
|
|
9058
|
+
minor, with a migration section like any other.
|
|
9059
|
+
|
|
9060
|
+
## Related
|
|
9061
|
+
|
|
9062
|
+
- [ADR 0104](../decisions/0104-the-project-declaration-ships-from-the-framework.md)
|
|
9063
|
+
— why the schema ships from the framework rather than from each project.
|
|
9064
|
+
- [API reference](../api/reference.md#stitchkitdeclaration) — every export.
|
|
9065
|
+
|
|
8289
9066
|
|
|
8290
9067
|
==============================================================================
|
|
8291
9068
|
# API reference (docs/api/reference.md)
|
|
@@ -8619,6 +9396,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
8619
9396
|
| `RealtimeServerHandle` | _type_ | minimal Socket.IO server handle accepted by `bindRealtimeServer`; carries the handshake identity type through to `connection.raw.data` |
|
|
8620
9397
|
| `SocketIORequestPolicy` | _type_ | runtime-neutral async-capable Web `Request` handshake admission policy |
|
|
8621
9398
|
| `SocketIOServerConfig` | _type_ | config for `createSocketIOServer`; `handshake` is the typed identity gate — [guide](../guide/realtime.md#handshake-auth--cookie-or-token) |
|
|
9399
|
+
| `SocketIOPeerLoaders` | _type_ | `peers: { server, bunEngine }` — literal dynamic imports written in YOUR source, so a bundler puts the optional peers inside a self-contained artifact. Omit for the lazy default |
|
|
8622
9400
|
| `SocketIOHandshakeConfig` | _type_ | the `handshake` gate — Zod `schema` over `handshake.auth` plus optional async `verify`; the result lands typed in `socket.data` |
|
|
8623
9401
|
| `SocketIOServerHandle` | _type_ | typed Socket.IO server plus Bun mount fields and idempotent lifecycle |
|
|
8624
9402
|
| `SocketIOServerLifecycle` | _type_ | non-generic Bun mount/shutdown portion accepted by `createServer` |
|
|
@@ -8696,6 +9474,10 @@ cutovers are covered by the executable
|
|
|
8696
9474
|
| Export | Kind | Summary |
|
|
8697
9475
|
|--------|------|---------|
|
|
8698
9476
|
| `createApplication` | function | compose a validated resource DAG into one non-restartable startup, readiness, admission and shutdown state machine |
|
|
9477
|
+
| `ApplicationResourceFailure` | _type_ | one resource failure with the cause its phase label cannot carry — delivered to `onResourceFailure` |
|
|
9478
|
+
| `ApplicationResourcePhase` | _type_ | the phase a managed resource failed in — the vocabulary of `ApplicationResourceShutdown.failures` |
|
|
9479
|
+
| `ApplicationShutdownOptionsSchema` / `ApplicationShutdownOptions` | schema / _type_ | the two shutdown budgets and an abort signal — without the HTTP-only `retryAfterSeconds` |
|
|
9480
|
+
| `ActivityTokenBrand` | const | the brand symbol `ActivityToken` carries, exported so `ActivityProjection` is implementable |
|
|
8699
9481
|
| `defineManagedResource` | function | retain the exact typed resource declaration; every invoked start is rollback-eligible |
|
|
8700
9482
|
| `managedServerResource` | function | adapt an existing managed server without copying its HTTP/WebSocket shutdown machine |
|
|
8701
9483
|
| `createApplicationHealthHandler` | function | build a Fetch-clean liveness or readiness response from the sanitized application snapshot |
|
|
@@ -8754,6 +9536,9 @@ The entrypoint exports each Zod schema beside its inferred type:
|
|
|
8754
9536
|
`ApplicationAdmissionSnapshotSchema` / `ApplicationAdmissionSnapshot`,
|
|
8755
9537
|
`ManagedResourceSnapshotSchema` / `ManagedResourceSnapshot`,
|
|
8756
9538
|
`ApplicationSnapshotSchema` / `ApplicationSnapshot`,
|
|
9539
|
+
`ApplicationStatusProjectionSchema` / `ApplicationStatusProjection` with
|
|
9540
|
+
`projectApplicationStatus` — what a status or probe endpoint may publish, and
|
|
9541
|
+
the function that derives it from a snapshot,
|
|
8757
9542
|
`ApplicationResourceShutdownSchema` / `ApplicationResourceShutdown`, and
|
|
8758
9543
|
`ApplicationShutdownResultSchema` / `ApplicationShutdownResult`.
|
|
8759
9544
|
|
|
@@ -8818,7 +9603,9 @@ Server-only optional application runtime. See the
|
|
|
8818
9603
|
| `AgentAdmissionEventSchema` | schema | post-commit admission projection; removes store rereads but does not imply exactly-once delivery |
|
|
8819
9604
|
| `AgentRunMetricsSchema` | schema | optional provenance-aware usage and timings; `partial` distinguishes checkpoint from terminal totals |
|
|
8820
9605
|
| `AgentRuntimeRecoverOptions` | _type_ | bounded paged startup recovery with context resolver and explicit evidence policy |
|
|
8821
|
-
| `
|
|
9606
|
+
| `AgentRuntimeConflictError` | class | thrown when a store mutation loses to a concurrent writer — catchable by type from `stitchkit/agent-runtime` |
|
|
9607
|
+
| `AgentSessionCloseOptions` | _type_ | `gracePeriodMs` for natural settlement, then abort, then `forceTimeoutMs` for bounded settlement after it |
|
|
9608
|
+
| `AgentSessionCloseResult` | _type_ | what `close()` achieved: `settled`, or `timedOut` with `remaining` runs still in flight. Only omitting `forceTimeoutMs` guarantees nothing is in flight on return |
|
|
8822
9609
|
| `AgentHistoryProjectionOptions` | _type_ | storage-neutral file resolver and explicit unresolved-file behavior |
|
|
8823
9610
|
| `createAgentToolFenceLifecycle` | function | pre-effect and post-effect run ownership fence for `mountAgent` |
|
|
8824
9611
|
| `AgentRuntimeEventSchema` | schema | transient stream lifecycle plus post-commit admission/checkpoint/run-state/terminal projections |
|
|
@@ -8866,7 +9653,7 @@ selection never splits a tool chronology and reports why every canonical record
|
|
|
8866
9653
|
removed.
|
|
8867
9654
|
|
|
8868
9655
|
Model exports are `AgentLanguageModelProvider`, `AgentModelCapability`,
|
|
8869
|
-
`AgentModelCapabilitySchema`, `
|
|
9656
|
+
`AgentModelCapabilitySchema`, `AgentModelDescriptor`,
|
|
8870
9657
|
`AgentModelDescriptorSchema`, `AgentModelRegistry`, `AgentModelRegistryConfig`,
|
|
8871
9658
|
`AgentModelRegistrySnapshot`, `AgentModelRegistrySnapshotSchema`, `AgentModelSnapshotPolicy`,
|
|
8872
9659
|
`AgentResolvedModel` and `validateAgentModelSnapshot`. Registry `preflight` validates availability,
|
|
@@ -9303,6 +10090,75 @@ available from `stitchkit/contract`.
|
|
|
9303
10090
|
|
|
9304
10091
|
---
|
|
9305
10092
|
|
|
10093
|
+
## `stitchkit/declaration`
|
|
10094
|
+
|
|
10095
|
+
Zod-only, dependency-free. The **project declaration**: the single
|
|
10096
|
+
machine-readable statement a repository makes about itself, read by the project,
|
|
10097
|
+
by the scaffolder that writes the first copy, and by whatever builds a source and
|
|
10098
|
+
binds the artifact into a deployment. It ships from the framework so those
|
|
10099
|
+
readers cannot hold different copies of the same schema.
|
|
10100
|
+
|
|
10101
|
+
**Declaring yourself is optional.** A project with no `project.json` is a
|
|
10102
|
+
complete project: nothing else in the framework imports this entrypoint, no
|
|
10103
|
+
build, test or start path looks for a declaration, and its absence is never an
|
|
10104
|
+
error or a warning.
|
|
10105
|
+
|
|
10106
|
+
The rule the schema exists to hold: **a declaration must be complete and
|
|
10107
|
+
meaningful when no machine exists**. A field that cannot be filled in without
|
|
10108
|
+
knowing where the code will run is a binding supplied by the deployment, not a
|
|
10109
|
+
declaration made by the repository.
|
|
10110
|
+
|
|
10111
|
+
**Structure is the guarantee**: nothing here requires a value of the place. A
|
|
10112
|
+
command is `executable` plus an `args` array, no part may be an absolute path or
|
|
10113
|
+
carry an inline value (`--port=8080` must be `['--port', '8080']`), paths are
|
|
10114
|
+
repository-relative, bindings are named by variable and never valued, and a
|
|
10115
|
+
listener's variables must exist in `env.variables` with matching shapes.
|
|
10116
|
+
|
|
10117
|
+
**Hygiene is the filter**: every remaining free string is checked against
|
|
10118
|
+
`namesAMachine` — a scheme, a protocol-relative host, an absolute or
|
|
10119
|
+
home-relative path, a Windows drive, a `host:port` pair, a bare IPv4 literal —
|
|
10120
|
+
and a number after a port flag is refused. It catches known shapes; it is not a
|
|
10121
|
+
secret scanner, and a secret or hostname written as a plain argument passes.
|
|
10122
|
+
|
|
10123
|
+
Unknown keys are **refused**, not stripped. A key one reader does not recognise
|
|
10124
|
+
is a disagreement between programs that never meet, and discarding it silently
|
|
10125
|
+
is how a partially understood declaration becomes a running, wrong deployment.
|
|
10126
|
+
|
|
10127
|
+
| Export | Kind | Summary |
|
|
10128
|
+
|--------|------|---------|
|
|
10129
|
+
| `ProjectDeclarationSchema` / `ProjectDeclaration` | schema / _type_ | the declaration: `schemaVersion`, `kind`, `identity`, `roles`, `build`, `requires`, `release`, `env` |
|
|
10130
|
+
| `parseProjectDeclaration` | function | parse a declaration, refusing an unrecognised `schemaVersion` **before** any field is read |
|
|
10131
|
+
| `PROJECT_DECLARATION_SCHEMA_VERSION` | constant | the declaration format this build understands — the number a reader refuses on |
|
|
10132
|
+
| `namesAMachine` | function | why a string names a particular machine, or `undefined` — the one predicate every free string is checked against |
|
|
10133
|
+
| `findProjectRole` | function | the role with a given name, or `undefined` |
|
|
10134
|
+
| `ProjectIdentitySchema` / `ProjectIdentity` | schema / _type_ | `slug`, `name`, `version` and a per-locale `description` |
|
|
10135
|
+
| `ProjectRoleSchema` / `ProjectRole` | schema / _type_ | one runnable role: a `workingDirectory` inside the source, per-mode `commands`, an optional `listener`, and `drainFloorMs` |
|
|
10136
|
+
| `ProjectCommandSchema` / `ProjectCommand` | schema / _type_ | `executable` plus `args` — argv, never a shell string |
|
|
10137
|
+
| `ProjectRoleCommandSchema` / `ProjectRoleCommand` | schema / _type_ | a command run under a supervisor: additionally never a script runner, because a launcher duplicates the shutdown signal |
|
|
10138
|
+
| `ProjectListenerSchema` | schema | `portVariable`, `bindVariable` and `readinessPath` — **absent means the role has no listener at all** |
|
|
10139
|
+
| `ProjectRunModeSchema` | schema | the run modes a role declares commands for |
|
|
10140
|
+
| `ProjectBuildSchema` | schema | build `command`, the set of `artifacts` it produces — more than one path is normal — and any declared data `inputs` |
|
|
10141
|
+
| `ProjectBuildInputSchema` / `ProjectBuildInput` | schema / _type_ | data the build may read: a `name`, a frozen export `path` inside the source, and the `sha256:` `digest` that pins its bytes. **Absent `inputs` means the build reads no data** — an answer, not a gap |
|
|
10142
|
+
| `ProjectRequirementSchema` / `ProjectRequirementPhaseSchema` | schema | something the code needs but does not provide, and the `phases` — `release`, `start` — it is needed in |
|
|
10143
|
+
| `ProjectReleaseSchema` / `ProjectMigrationsSchema` | schema | what must happen once before roles start; migrations are declared as `engine`, `root` and `lockfile` — **bytes, not a command to run** |
|
|
10144
|
+
| `ProjectEnvVariableSchema` / `ProjectEnvVariable` / `ProjectEnvShapeSchema` | schema / _type_ | a variable a deployment must supply: `name`, `shape`, `required`, and `members` for an enum |
|
|
10145
|
+
| `ProjectSlugSchema` | schema | lowercase hyphen-separated identity everything else is named after |
|
|
10146
|
+
| `ProjectDescriptionSchema` | schema | description per locale tag |
|
|
10147
|
+
| `RepositoryPathSchema` | schema | a path inside the source — absolute, `..`, `~` and Windows paths are refused |
|
|
10148
|
+
| `BindingVariableSchema` | schema | the NAME of an environment variable a deployment fills in |
|
|
10149
|
+
|
|
10150
|
+
Version handling is **fail-closed**: an unrecognised `schemaVersion` is refused,
|
|
10151
|
+
never assumed compatible, and refused before any field is read so a newer
|
|
10152
|
+
declaration reports as a version this build cannot serve rather than as a list of
|
|
10153
|
+
unrecognised keys.
|
|
10154
|
+
|
|
10155
|
+
To compose a stricter declaration, build on the exported member schemas — for
|
|
10156
|
+
example `ProjectDeclarationSchema.safeExtend({ identity: … })`, which keeps the
|
|
10157
|
+
boundary refinements in force. `ProjectDescriptionSchema` is a record, so narrow
|
|
10158
|
+
it by replacing the field rather than extending it.
|
|
10159
|
+
|
|
10160
|
+
---
|
|
10161
|
+
|
|
9306
10162
|
## `stitchkit/node`
|
|
9307
10163
|
|
|
9308
10164
|
Server-only, for Node ≥ 22 (Bun uses `stitchkit/server`). The runtime-agnostic
|
|
@@ -9321,7 +10177,7 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
|
|
|
9321
10177
|
| `NodeRuntimeServer` | _type_ | concrete `srvx/node` runtime escape hatch |
|
|
9322
10178
|
| `NodeSocketLifecycle` | _type_ | Bun-free Socket.IO lifecycle accepted by `serveNode` |
|
|
9323
10179
|
| `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` | _type_ | runtime-neutral handler types; raw routes default their host server to `unknown` |
|
|
9324
|
-
| `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config and Bun-free Node handle with `io`, `attach` and lifecycle |
|
|
10180
|
+
| `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOPeerLoaders` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config, optional-peer loaders and the Bun-free Node handle with `io`, `attach` and lifecycle |
|
|
9325
10181
|
| `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
|
|
9326
10182
|
|
|
9327
10183
|
---
|