stitchkit 0.118.1 → 0.119.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.
Files changed (144) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/contract/client-types.d.ts +6 -3
  4. package/dist/contract/client-types.d.ts.map +1 -1
  5. package/dist/contract/define-validation.d.ts.map +1 -1
  6. package/dist/contract/define.d.ts +18 -12
  7. package/dist/contract/define.d.ts.map +1 -1
  8. package/dist/contract/endpoint-capabilities.d.ts +28 -0
  9. package/dist/contract/endpoint-capabilities.d.ts.map +1 -0
  10. package/dist/contract/factory.d.ts +49 -19
  11. package/dist/contract/factory.d.ts.map +1 -1
  12. package/dist/contract/tool-view.d.ts +1 -1
  13. package/dist/contract/tool-view.d.ts.map +1 -1
  14. package/dist/contract/transports.d.ts +5 -0
  15. package/dist/contract/transports.d.ts.map +1 -0
  16. package/dist/entrypoints/agent-runtime/coding-tools.js +6 -5
  17. package/dist/entrypoints/agent-runtime/harness.js +6 -5
  18. package/dist/entrypoints/agent-runtime/sandbox.js +6 -5
  19. package/dist/entrypoints/agent-runtime/sqlite/bun.js +9 -8
  20. package/dist/entrypoints/agent-runtime/sqlite/node.js +9 -8
  21. package/dist/entrypoints/agent-runtime/testing.js +9 -8
  22. package/dist/entrypoints/agent-runtime.js +14 -13
  23. package/dist/entrypoints/application/diagnostic-journal.js +3 -3
  24. package/dist/entrypoints/application/directory-inbox.js +4 -4
  25. package/dist/entrypoints/application/opentelemetry.js +1 -1
  26. package/dist/entrypoints/application.js +5 -5
  27. package/dist/entrypoints/cli/publish.js +4 -4
  28. package/dist/entrypoints/cli.js +8 -7
  29. package/dist/entrypoints/contract.d.ts +1 -1
  30. package/dist/entrypoints/contract.d.ts.map +1 -1
  31. package/dist/entrypoints/contract.js +6 -4
  32. package/dist/entrypoints/files.js +3 -3
  33. package/dist/entrypoints/geo/contract.d.ts +7 -0
  34. package/dist/entrypoints/geo/contract.d.ts.map +1 -0
  35. package/dist/entrypoints/geo/contract.js +6 -0
  36. package/dist/entrypoints/geo.js +60 -23
  37. package/dist/entrypoints/http.js +2 -2
  38. package/dist/entrypoints/index.js +8 -6
  39. package/dist/entrypoints/live.js +1 -1
  40. package/dist/entrypoints/node.js +7 -6
  41. package/dist/entrypoints/primitives.js +1 -1
  42. package/dist/entrypoints/process/owner-loss.js +1 -1
  43. package/dist/entrypoints/process.js +3 -3
  44. package/dist/entrypoints/react.js +1 -1
  45. package/dist/entrypoints/remote.js +4 -3
  46. package/dist/entrypoints/server.js +11 -10
  47. package/dist/entrypoints/telegram.js +2 -2
  48. package/dist/entrypoints/testing.js +7 -6
  49. package/dist/entrypoints/tools/connections.js +3 -2
  50. package/dist/entrypoints/tools/contract.js +1 -1
  51. package/dist/entrypoints/tools/invoker.js +5 -4
  52. package/dist/entrypoints/tools/mcp.js +7 -6
  53. package/dist/entrypoints/tools.js +17 -15
  54. package/dist/entrypoints/tracking/contract.js +5 -3
  55. package/dist/entrypoints/tracking/performance.js +3 -3
  56. package/dist/entrypoints/tracking.d.ts +1 -1
  57. package/dist/entrypoints/tracking.d.ts.map +1 -1
  58. package/dist/entrypoints/tracking.js +63 -83
  59. package/dist/entrypoints/xlsx.js +1 -1
  60. package/dist/geo/index.d.ts +2 -1
  61. package/dist/geo/index.d.ts.map +1 -1
  62. package/dist/geo/record.d.ts +8 -0
  63. package/dist/geo/record.d.ts.map +1 -0
  64. package/dist/geo/resolver.d.ts +1 -3
  65. package/dist/geo/resolver.d.ts.map +1 -1
  66. package/dist/geo/schema.d.ts +44 -0
  67. package/dist/geo/schema.d.ts.map +1 -0
  68. package/dist/geo/types.d.ts +2 -13
  69. package/dist/geo/types.d.ts.map +1 -1
  70. package/dist/{index-n5sv4z0w.js → index-1k18qzqa.js} +13 -15
  71. package/dist/{index-tsngj2b5.js → index-2dfvv1jg.js} +206 -206
  72. package/dist/{index-d9hq291n.js → index-4jpykzvw.js} +70 -81
  73. package/dist/{index-ghtpfz4z.js → index-57gqaq8b.js} +2 -2
  74. package/dist/index-69t14s4a.js +39 -0
  75. package/dist/{index-6p3frkv5.js → index-6a69xzrx.js} +4 -4
  76. package/dist/{index-dky0dcmf.js → index-72r9k0av.js} +6 -2
  77. package/dist/{index-vv1sch8a.js → index-8vzh5wpv.js} +13 -8
  78. package/dist/{index-xx1j310k.js → index-8yxgcyt6.js} +171 -169
  79. package/dist/{index-p6apg0jg.js → index-9frf01t9.js} +16 -9
  80. package/dist/{index-q0th8q0z.js → index-9mjrnqxs.js} +1 -3
  81. package/dist/{index-ega3yz4c.js → index-aqwg2dm5.js} +1 -1
  82. package/dist/{index-073nw9qr.js → index-ba7are04.js} +524 -522
  83. package/dist/{index-095ya5hm.js → index-bbvc59gw.js} +13 -11
  84. package/dist/{index-txg1npmg.js → index-cyxp8rbf.js} +1 -1
  85. package/dist/index-d3xdyjt6.js +38 -0
  86. package/dist/{index-n8z3rvzw.js → index-f4njz9j0.js} +3 -5
  87. package/dist/{index-90p34rts.js → index-fcqj44mb.js} +3 -1
  88. package/dist/{index-86nbfysb.js → index-ff39f9dr.js} +3 -3
  89. package/dist/{index-75ns2ybj.js → index-fqd4yqwd.js} +1 -1
  90. package/dist/{index-nqxrm176.js → index-g444vfz2.js} +1 -1
  91. package/dist/{index-vwhrnp67.js → index-kz9734jc.js} +3 -3
  92. package/dist/{index-dr3t5n1v.js → index-nn9qvkww.js} +46 -46
  93. package/dist/{index-qezhmp51.js → index-nz06qw5a.js} +3 -1
  94. package/dist/{index-1bx1xv1k.js → index-pfvde2fz.js} +5 -2
  95. package/dist/{index-s80awwsw.js → index-qch1vcaa.js} +3 -1
  96. package/dist/{index-cycp3r3e.js → index-rxcgp4b9.js} +2 -2
  97. package/dist/{index-w7jq3fz9.js → index-s0qjs6ee.js} +1198 -1196
  98. package/dist/{index-p0rf7sq9.js → index-s20s2mad.js} +209 -209
  99. package/dist/{index-v6vb5qx8.js → index-s3yxw6kt.js} +1 -1
  100. package/dist/{index-cqmqyjax.js → index-sa99p98w.js} +1 -1
  101. package/dist/{index-0gxvns6s.js → index-sba3e6m4.js} +1 -0
  102. package/dist/{index-f24bdt13.js → index-v1swr26p.js} +3 -1
  103. package/dist/{index-rhzkndr5.js → index-x4p3kw1p.js} +533 -530
  104. package/dist/{index-hdjmdwsj.js → index-x8d9fdeg.js} +513 -507
  105. package/dist/{index-htxh46ej.js → index-ycqhyars.js} +2 -2
  106. package/dist/{index-9bwcthxw.js → index-yj6saztf.js} +31 -13
  107. package/dist/{index-tdpmfks8.js → index-ynrf908v.js} +1 -1
  108. package/dist/{index-f6m4c545.js → index-zrdcmg6j.js} +4 -6
  109. package/dist/process/bun-child.d.ts +3 -2
  110. package/dist/process/bun-child.d.ts.map +1 -1
  111. package/dist/server/contract-method.d.ts.map +1 -1
  112. package/dist/tools/internal/surface-projector.d.ts.map +1 -1
  113. package/dist/tools/operations/async-operation-canonical-types.d.ts +1 -1
  114. package/dist/tracking/attribution.d.ts +21 -25
  115. package/dist/tracking/attribution.d.ts.map +1 -1
  116. package/dist/tracking/client-visit.d.ts +5 -7
  117. package/dist/tracking/client-visit.d.ts.map +1 -1
  118. package/dist/tracking/client.d.ts +2 -2
  119. package/dist/tracking/client.d.ts.map +1 -1
  120. package/dist/tracking/contract.d.ts +1 -1
  121. package/dist/tracking/host.d.ts +0 -1
  122. package/dist/tracking/host.d.ts.map +1 -1
  123. package/dist/tracking/privacy.d.ts +6 -0
  124. package/dist/tracking/privacy.d.ts.map +1 -1
  125. package/dist/tracking/schemas.d.ts +2 -2
  126. package/dist/tracking/schemas.d.ts.map +1 -1
  127. package/llms/contract.txt +46 -24
  128. package/llms/geo-contract.txt +23 -0
  129. package/llms/geo.txt +40 -4
  130. package/llms/stitchkit.txt +4 -3
  131. package/llms/tools.2.txt +2 -2
  132. package/llms/tools.3.txt +1 -1
  133. package/llms/tracking-contract.txt +1 -1
  134. package/llms/tracking.txt +27 -5
  135. package/llms/{upgrading-0.105.0-0.118.0.txt → upgrading-0.106.0-0.119.0.txt} +121 -410
  136. package/llms/upgrading-0.37.0-0.53.0.txt +969 -0
  137. package/llms/{upgrading-0.44.0-0.62.0.txt → upgrading-0.55.0-0.68.0.txt} +509 -528
  138. package/llms/{upgrading-0.63.0-0.83.0.txt → upgrading-0.69.0-0.95.0.txt} +506 -509
  139. package/llms/{upgrading-0.84.0-0.104.0.txt → upgrading-0.96.0-0.105.0.txt} +410 -506
  140. package/llms/upgrading.txt +5 -5
  141. package/llms-full.txt +247 -38
  142. package/llms.txt +10 -9
  143. package/package.json +23 -19
  144. package/llms/upgrading-0.37.0-0.44.0.txt +0 -444
@@ -1,14 +1,520 @@
1
- # Upgrading 0.44.0 → 0.62.0 — stitchkit docs slice
1
+ # Upgrading 0.55.0 → 0.68.0 — stitchkit docs slice
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
5
5
 
6
- Contains: Guide: Upgrading, 0.44.0 → 0.62.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.55.0 → 0.68.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.44.0 → 0.62.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.55.0 → 0.68.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.68.0
13
+
14
+ ### Make Unix transport selection explicit outside Bun
15
+
16
+ The legacy `unix` option remains a Bun convenience, but on Node or another
17
+ runtime it now fails before dispatch. Replace it with the owned adapter and close
18
+ that adapter with the application:
19
+
20
+ ```ts
21
+ // before — unsafe outside Bun: an unsupported fetch could dial baseUrl over TCP
22
+ const http = createHttpClient({ baseUrl, unix: '/run/service.sock' })
23
+
24
+ // after — Bun and Node; every dispatch and redirect stays on the socket
25
+ import { createUnixClientTransport } from 'stitchkit/server' // or stitchkit/node
26
+ const transport = createUnixClientTransport({ socketPath: '/run/service.sock' })
27
+ const http = createHttpClient({ baseUrl, fetch: transport.fetch })
28
+ // during shutdown
29
+ await transport.close()
30
+ ```
31
+
32
+ Do not automatically replay `possibly-dispatched`: a timeout or connection loss
33
+ after bytes left the process does not prove that a write did not happen.
34
+
35
+ ### Choose tolerant stream parsing explicitly
36
+
37
+ `parseNDJSON` and `parseSSE` now throw on malformed JSON, invalid UTF-8 and an
38
+ over-limit line. The default line ceiling is 1 MiB. If a feed deliberately skips
39
+ bad records, retain that policy explicitly:
40
+
41
+ ```ts
42
+ // before — malformed input disappeared implicitly
43
+ parseNDJSON(response)
44
+
45
+ // after — ordinary fail-closed path
46
+ parseNDJSON(response, { maxLineBytes: 256 * 1024 })
47
+
48
+ // after — deliberately tolerant path
49
+ parseNDJSON(response, { onParseError: (raw, error) => report(raw, error) })
50
+ ```
51
+
52
+ ### Extend exhaustive framework-error handling
53
+
54
+ If a switch makes `StitchErrorCode` exhaustive, add
55
+ `STREAM_ITEM_INVALID`, `STREAM_FRAME_TOO_LARGE`,
56
+ `STREAM_TERMINAL_MISSING` and `STREAM_LIFETIME_EXCEEDED`. A partial application
57
+ status map needs no change (ADR 0105).
58
+
59
+ The admission/channel APIs and endpoint `stream` descriptor are additive; raw
60
+ responses and raw `streamingRoute` remain supported.
61
+
62
+ ## Released migration: 0.67.0
63
+
64
+ Three application-kernel changes. Two of them fix silent failures, so the most
65
+ important part of this migration is not what the compiler points at — it is the
66
+ two things that change with no compile error at all: **when your server is
67
+ created**, and **which shutdown budget a signal spends**.
68
+
69
+ ### If you gave `managedServerResource` a thunk
70
+
71
+ It is now called during `start`, after the resource's dependencies are ready —
72
+ the reading the type always suggested. Before, it was called on the way *down*,
73
+ so `app.start()` resolved, the snapshot said `healthy` and `ready`, and nothing
74
+ was bound to the port.
75
+
76
+ If your application looked like this, it was never listening:
77
+
78
+ ```ts
79
+ // before — resolves clean, listens on nothing
80
+ managedServerResource({ id: 'http', dependsOn: ['database'], server: () => createServer(config) })
81
+ ```
82
+
83
+ Nothing to change: the same code now binds the port. Check your startup logs for
84
+ a healthy report you never actually verified with a request.
85
+
86
+ If you used the spread workaround — this resource's phases over your own
87
+ `start` — it keeps working, and you can now delete it:
88
+
89
+ ```ts
90
+ // before
91
+ let handle: ManagedServerHandle<T> | null = null
92
+ const shutdown = managedServerResource({ id: 'http', server: () => handle! })
93
+ const http = defineManagedResource({
94
+ ...shutdown,
95
+ dependsOn: ['database', 'socket-io'],
96
+ start: () => { handle = createServer(config) },
97
+ })
98
+
99
+ // after
100
+ const http = managedServerResource({
101
+ id: 'http',
102
+ dependsOn: [database, socketIo],
103
+ server: () => createServer(config),
104
+ })
105
+ ```
106
+
107
+ An already-created handle is adopted exactly as before.
108
+
109
+ ### If you passed `shutdown` to `bindProcessSignals`
110
+
111
+ `bindProcessSignals` used to fill in the schema's defaults for every budget you
112
+ omitted, which made `createApplication({ shutdown })` unreachable on the signal
113
+ path — the one path production stops through. An application declaring five
114
+ seconds took thirty-five.
115
+
116
+ Declare the budget once, on the application, and delete the repetition:
117
+
118
+ ```ts
119
+ // before — the same two numbers in two places, and only one of them applied
120
+ const app = createApplication({ id: 'svc', resources, shutdown: SHUTDOWN_BUDGET })
121
+ bindProcessSignals(app, { shutdown: SHUTDOWN_BUDGET })
122
+
123
+ // after
124
+ const app = createApplication({ id: 'svc', resources, shutdown: SHUTDOWN_BUDGET })
125
+ bindProcessSignals(app)
126
+ ```
127
+
128
+ **Check this even if you change nothing.** If you declared a budget on the
129
+ application and did not repeat it on the binding, your process has been stopping
130
+ on 30 s / 5 s and will now stop on what you declared. Compare it against the
131
+ supervisor timeout that watches it — `kill_timeout`, `TimeoutStopSec` — because
132
+ that number was probably calculated from the declaration.
133
+
134
+ Passing `shutdown` to the binding still works and still wins, key by key: one
135
+ key overrides that key alone and leaves the other at the declaration.
136
+
137
+ ### If you read `resource.dependsOn`
138
+
139
+ Its type widened from `readonly string[]` to `readonly ManagedResourceDependency[]`
140
+ — `string | ManagedResource` — so a dependency can be declared as the resource
141
+ itself. Declaring stays compatible; reading needs one call:
142
+
143
+ ```ts
144
+ // before
145
+ const ids: readonly string[] = resource.dependsOn ?? []
146
+
147
+ // after
148
+ import { managedResourceDependencyId } from 'stitchkit/application'
149
+ const ids = (resource.dependsOn ?? []).map(managedResourceDependencyId)
150
+ ```
151
+
152
+ ### If you thread a handle through a module-local
153
+
154
+ This is the pattern the change exists to remove, and it is opt-in — nothing
155
+ breaks if you keep yours:
156
+
157
+ ```ts
158
+ // before
159
+ let socket: SocketHandle | null = null
160
+ const socketIo = defineManagedResource({
161
+ id: 'socket-io',
162
+ start: async () => { socket = await createSocketServer(config) },
163
+ })
164
+ const http = defineManagedResource({
165
+ id: 'http',
166
+ dependsOn: ['socket-io'],
167
+ start: () => {
168
+ if (!socket) throw new Error('socket is not initialized') // unreachable
169
+ server = createServer({ socket })
170
+ },
171
+ })
172
+
173
+ // after
174
+ const socketIo = defineManagedResource({
175
+ id: 'socket-io',
176
+ start: async () => ({ value: await createSocketServer(config) }),
177
+ })
178
+ const http = defineManagedResource({
179
+ id: 'http',
180
+ dependsOn: [socketIo],
181
+ start: (context) => {
182
+ server = createServer({ socket: context.use(socketIo) }) // SocketHandle
183
+ },
184
+ })
185
+ ```
186
+
187
+ Declare the dependency with the **resource** when you intend to read from it —
188
+ that is the form `use` can type. `use` refuses a resource missing from
189
+ `dependsOn`, and refuses one that published no value; the second refusal is a
190
+ compile error too.
191
+
192
+ ## Released migration: 0.66.0
193
+
194
+ Three changes, and only one of them is a feature. The other two are shapes that
195
+ would have had to break later: a vocabulary that described one fact with two
196
+ words, and a store whose only read was the whole conversation.
197
+
198
+ The compiler points at most of it. Two things change with no compile error —
199
+ what a **total** says about its own provenance, and the fact that token counts
200
+ are now validated where they were not.
201
+
202
+ ### An input that joins a run in flight
203
+
204
+ #### If you matched exhaustively on `AgentTerminalReason`
205
+
206
+ `'absorbed'` is new. A run ends this way when a run already in flight took its
207
+ input on and answered it; its state is `'superseded'`, and `absorbedIntoRunId`
208
+ names the run that has the answer.
209
+
210
+ ```ts
211
+ // after
212
+ switch (run.terminalReason) {
213
+ // …
214
+ case 'absorbed':
215
+ // no assistant message of its own — follow run.absorbedIntoRunId
216
+ break
217
+ }
218
+ ```
219
+
220
+ A run record with `terminalReason: 'absorbed'` and no `absorbedIntoRunId` is
221
+ refused at parse time, and so is `absorbedIntoRunId` on any other reason.
222
+
223
+ #### If you render or export runs
224
+
225
+ An absorbed run has **no assistant message**. Anything that assumes "every
226
+ terminal run has one" needs the `absorbed` case — the answer is on the run
227
+ `absorbedIntoRunId` names, and `store.loadRun` resolves it.
228
+
229
+ #### If you want the policy
230
+
231
+ ```ts
232
+ // after
233
+ runs: { inputPolicy: 'inject' }
234
+ ```
235
+
236
+ It was withdrawn in 0.65.0 and is back with the ordering corrected: the
237
+ absorption commits with the terminal record, not at the step boundary. Read
238
+ *An input that joins a run in flight* in the agent-runtime guide before turning
239
+ it on — in particular what happens when the absorbing run does not complete, and
240
+ how it composes with `coalescePending`.
241
+
242
+ #### If you implement a store driver
243
+
244
+ Nothing to do, but know what changed underneath: one terminal commit can now
245
+ save **two** run records, and they must land in the same transaction. A driver
246
+ that persists one of the pair fails `runAgentStoreConformance`.
247
+
248
+ ### A run read without its conversation
249
+
250
+ #### If you implement `AgentRuntimeStore` by hand
251
+
252
+ Two members to add. **If your adapter is an `AgentRuntimeStoreDriver` passed to
253
+ `createAgentRuntimeStore`, there is nothing to do** — the driver already had
254
+ everything both need.
255
+
256
+ ```ts
257
+ // after
258
+ loadRun(input: { conversationId: string; runId: string }): Promise<AgentRunView | undefined>
259
+ listActiveRuns(conversationId: string): Promise<readonly AgentRun[]>
260
+ ```
261
+
262
+ `AgentRunView` is `{ snapshotVersion, run, assistant? }`. `assistant` is the
263
+ retained terminal answer, so it is present exactly when the run has ended and
264
+ absent while it is live — a store that returns a live run's draft here lets the
265
+ terminal path resolve a run that has not finished. `listActiveRuns` orders by
266
+ `createdAt` then `id`, and must not report a run that has ended.
267
+ `runAgentStoreConformance` covers both, including the boundary cases.
268
+
269
+ #### If you have a store **double** in your tests
270
+
271
+ The runtime now reads `loadRun` where it used to read `loadSnapshot`. A double
272
+ that simulates a condition — a run drifting to another owner, a stale fencing
273
+ token — must apply it to both reads, or the code under test will not see it.
274
+ This is not hypothetical: it turned one of this repository's own fixtures into
275
+ an infinite retry loop, which is how the bounded retry below was found.
276
+
277
+ #### Nothing else changes
278
+
279
+ `loadSnapshot` behaves exactly as before, and still returns the whole
280
+ conversation — as does every mutation result. What it costs, and what bounds it,
281
+ is now written down in *Reading a conversation* in the agent-runtime guide.
282
+
283
+ ### One provenance vocabulary, and integral tokens
284
+
285
+ #### If you match on `'measured'`
286
+
287
+ A **total** now says `computed`, because it is a sum this code performed rather
288
+ than a count it took — the same rule `AgentUsage` has always applied to a run's
289
+ spend. Two values change with no compile error:
290
+
291
+ ```ts
292
+ // before
293
+ if (result.totalTokens.provenance === 'measured') { /* exact */ }
294
+ // after
295
+ if (result.totalTokens.provenance === 'computed') { /* exact, and derived */ }
296
+ ```
297
+
298
+ It affects `AgentHistoryBudgetResult.totalTokens` and
299
+ `ComposedAgentPrompt.instructionTokens`. A per-message or per-section count is
300
+ still `measured` — only the totals moved. When any part was estimated the total
301
+ is still `estimated`: an estimate survives arithmetic, and that is the weaker
302
+ claim, so it wins.
303
+
304
+ #### If you produce token counts
305
+
306
+ They are validated now, in the places they were not. A fractional count throws
307
+ where it used to flow into the context-window arithmetic:
308
+
309
+ ```ts
310
+ // refused from this release on
311
+ estimateTokens: (text) => ({ value: text.length / 4, provenance: 'estimated' })
312
+ // after
313
+ estimateTokens: (text) => ({ value: Math.ceil(text.length / 4), provenance: 'estimated' })
314
+ ```
315
+
316
+ The same applies to `ComposeAgentPromptOptions.estimateFallback` and
317
+ `.historyTokens`, to `AgentPromptBudget.toolSchemas` / `.attachments` /
318
+ `.providerOverhead`, and to `AgentPromptBudget.contextWindow` /
319
+ `.reservedOutput`, which must now be non-negative safe integers.
320
+
321
+ `AgentUsageValue.value` is `z.int()` too, which matters if you build usage
322
+ records by hand or in a store double. A figure arriving from a **provider** is
323
+ not thrown — `normalizeSdkUsage` and the OpenRouter adapter turn a non-integer
324
+ into `{ provenance: 'unavailable' }`, so a run that already answered is not
325
+ failed over its own bookkeeping.
326
+
327
+ #### The new export
328
+
329
+ `AgentProvenanceSchema` / `AgentProvenance` is the whole vocabulary:
330
+ `provider-reported`, `measured`, `computed`, `estimated`, `unavailable`. Each
331
+ surface declares its subset, so nothing widened — `AgentUsageValue` still refuses
332
+ `measured` and `AgentTokenCount` still refuses `provider-reported`. Use it when
333
+ you want one switch over the question instead of two.
334
+
335
+ ## Released migration: 0.65.0
336
+
337
+ The largest migration of the pre-1.0 line, and most of it is the compiler
338
+ pointing at things. One item is a feature withdrawal and two change behaviour
339
+ with no compile error at all.
340
+
341
+ ### A projection a provider accepts, and a policy withdrawn
342
+
343
+ ### If you compact, or use `system-note`, you were broken and now are not
344
+
345
+ No code change to adopt the fix — but **check your own code if you call
346
+ `projectAgentHistory` yourself.** System and summary records no longer appear in
347
+ `messages`; they come back in `system` and belong in the provider's instructions
348
+ channel, which is the only place `ai` accepts them:
349
+
350
+ ```ts
351
+ // before — the provider refuses this outright
352
+ const messages = await projectAgentHistory(snapshot.messages)
353
+ streamText({ model, messages })
354
+
355
+ // after
356
+ const { messages, system } = await projectAgentHistoryDetailed(snapshot.messages)
357
+ streamText({
358
+ model,
359
+ instructions: system.map((content) => ({ role: 'system', content })),
360
+ messages,
361
+ })
362
+ ```
363
+
364
+ If you use `createAgentRuntime`, this is handled for you.
365
+
366
+ ### `inputPolicy: 'inject'` is withdrawn
367
+
368
+ ```ts
369
+ // before
370
+ runs: { inputPolicy: 'inject' }
371
+ // after
372
+ runs: { inputPolicy: 'queue' }
373
+ ```
374
+
375
+ `'absorbed'`, `AgentRun.absorbedIntoRunId` and `AgentRuntimeStore.absorbQueuedRun`
376
+ go with it. A driver built on `AgentRuntimeStoreDriver` needs no change.
377
+
378
+ ### Records must agree with themselves
379
+
380
+ If you construct `AgentRun` values — a store double, a fixture, a migration —
381
+ derive the state instead of setting it:
382
+
383
+ ```ts
384
+ // after
385
+ import { runStateForTerminalReason } from 'stitchkit/agent-runtime'
386
+ AgentRunSchema.parse({ …run, terminalReason: reason, state: runStateForTerminalReason(reason) })
387
+ ```
388
+
389
+ A terminal state with no reason, a queued run carrying one, and `policy_stop`
390
+ without a `terminalPolicyName` are now all refused at parse time.
391
+
392
+ ### Enum and field changes the compiler will point at
393
+
394
+ - `AgentTerminalReason`: `'tool_failure'` removed (never produced),
395
+ `'context_overflow'` added — this runtime's own refusal when the prompt does
396
+ not fit, which used to report `provider_failure`.
397
+ - `AgentRunState`: `'absorbed'` removed.
398
+ - `AgentRunMetrics.usage` is required.
399
+ - `AgentHistoryBudgetDecision['reason']`: `'superseded'` → `'unspeakable'`.
400
+
401
+ ### Two behaviour changes with no compile error
402
+
403
+ - **`loop.idleTimeoutMs` now defaults to 60 000.** A run whose provider stream
404
+ goes quiet for a minute ends as `timeout` instead of holding the lane forever.
405
+ Pass `null` for the old behaviour, and think about why you want it.
406
+ - **`advanceAgentRuntimeEventCursor` stops returning `gap` for durable events.**
407
+ If you reload a conversation on `gap`, you were reloading after essentially
408
+ every run. Transient events still report it, and there it is real.
409
+
410
+ ## Released migration: 0.64.0
411
+
412
+ Two changes, and only one of them can break a build. Nothing was removed.
413
+
414
+ ### An event that says which kind it is
415
+
416
+ ### Narrow `AgentRunEvent` on `type`
417
+
418
+ ```ts
419
+ // before
420
+ for (const event of events) record(event.usage?.cost?.value ?? 0)
421
+
422
+ // after — and the compiler will point at every site
423
+ for (const event of events) {
424
+ if (event.type !== 'run-terminal') continue
425
+ record(event.usage.cost.value ?? null) // `usage` is present; unknown says so
426
+ }
427
+ ```
428
+
429
+ `step` is now required on `step-finished`, `terminalReason` on `run-terminal`,
430
+ and `usage` on both. `queueWaitMs` exists only on `run-started`. Nothing was
431
+ removed — the fields that were optional because a *different* kind of event
432
+ lacked them are now simply on the kinds that have them.
433
+
434
+ Import `AgentRunTerminalEventSchema` (or the sibling schemas) if you construct
435
+ events in tests.
436
+
437
+ ### If you implement `AgentRuntimeStore` directly, move to the driver
438
+
439
+ Not urgent and nothing breaks today — but the aggregate is no longer the
440
+ supported target, so its future growth will not be announced as breaking
441
+ (→ ADR 0111). The supported shape is one line:
442
+
443
+ ```ts
444
+ const driver: AgentRuntimeStoreDriver<TransactionClient> = { /* six primitives */ }
445
+ export const store = createAgentRuntimeStore(driver)
446
+ ```
447
+
448
+ Run `runAgentStoreConformance` against your adapter before and after any bump —
449
+ see *Before you bump, if you implement an agent store* above.
450
+
451
+ ## Released migration: 0.63.0
452
+
453
+ Four changes to what a running system reports and how an input reaches a run
454
+ in flight. Nothing moves an export except one added store member, and only an
455
+ application implementing `AgentRuntimeStore` directly has to touch code.
456
+
457
+ ### A spend figure that survives, and an input that joins
458
+
459
+ ### `AgentRuntimeStore` has a ninth member
460
+
461
+ Only if you implement the aggregate interface directly — an adapter built on
462
+ `AgentRuntimeStoreDriver` needs no change, and the public conformance kit covers
463
+ the new operation.
464
+
465
+ ```ts
466
+ // after
467
+ absorbQueuedRun(input: AbsorbQueuedRun): Promise<AgentStoreMutationResult>
468
+ ```
469
+
470
+ It moves a queued successor's inputs into the run already answering, in one
471
+ mutation, and marks the successor `absorbed`. Two run records change in one
472
+ transaction — if your driver persists them one at a time, persist both.
473
+
474
+ ### A provider failure says so
475
+
476
+ ```ts
477
+ // before — an upstream error arrived as a policy stop with no policy
478
+ if (terminal.reason === 'policy_stop') retryLater()
479
+
480
+ // after
481
+ if (terminal.reason === 'provider_failure') retryLater()
482
+ if (terminal.reason === 'provider_stop') { /* a length cap or content filter */ }
483
+ ```
484
+
485
+ `policy_stop` now always carries the `policyName` that caused it. If you switch
486
+ exhaustively on `AgentTerminalReason` or `AgentRunState`, add `'provider_stop'`
487
+ and `'absorbed'`.
488
+
489
+ ### `partial` changed meaning
490
+
491
+ It used to tell you which event kind you were holding. It now tells you whether
492
+ the figure beside it is a confirmed total:
493
+
494
+ ```ts
495
+ // after — true when the provider never reported the run finished
496
+ if (metrics.partial) treatAsFloor(metrics.usage)
497
+ ```
498
+
499
+ Terminal events for runs that were superseded, interrupted, timed out or failed
500
+ before the provider finished now report `partial: true` where they reported
501
+ `false`.
502
+
503
+ ### Checkpoint metrics are a running total
504
+
505
+ ```ts
506
+ // wrong, and now wrong by more than it used to be
507
+ const spent = checkpoints.reduce((total, c) => total + (c.metrics.usage?.cost?.value ?? 0), 0)
508
+ // right
509
+ const spent = checkpoints.at(-1)?.metrics.usage?.cost?.value
510
+ ```
511
+
512
+ ### You can read a run's spend back from the store
513
+
514
+ `AgentRun.usage` is written at every checkpoint and with the terminal record, so
515
+ a dropped observability event no longer loses the number, and a process that dies
516
+ mid-stream leaves behind what it had already spent.
517
+
12
518
  ## Released migration: 0.62.0
13
519
 
14
520
  Two groups of behaviour changes. Nothing moves an export — the surface is
@@ -694,528 +1200,3 @@ Downloaded `path` is now relative to the boundary and MIME metadata is named
694
1200
  same way. Auth predicates must explicitly return `true`, `false`, or a plain
695
1201
  object of context fields; replace accidental `undefined` fallthroughs with the
696
1202
  intended boolean.
697
-
698
- ## Released migration: 0.53.0
699
-
700
- ### Realtime `emit` returns `boolean` instead of `void`
701
-
702
- Every **call site** compiles and behaves exactly as before — the return value
703
- is new information (`true` = accepted by the transport, `false` = dropped
704
- while the browser client was disconnected), not a behavior change. What breaks
705
- is **implementing** the interfaces: a test mock or app-side adapter of
706
- `SocketIOClient` / `RealtimeClient` / `ValidatedRealtimeSocket` /
707
- `RealtimeServer` whose `emit` returns `void` no longer typechecks.
708
-
709
- ```ts
710
- // before — a void mock satisfied the interface
711
- const mock: Pick<RealtimeClient<S, C>, 'emit'> = { emit: () => {} }
712
- // after — report acceptance (true is what a live server-side emit reports)
713
- const mock: Pick<RealtimeClient<S, C>, 'emit'> = { emit: () => true }
714
- ```
715
-
716
- While migrating, consider replacing hand-rolled `if (client.connected)` guards
717
- with the new honest surface: check `client.emit(...) === false`, or observe
718
- drops centrally with `onDroppedEmit`.
719
-
720
- ## Released migration: 0.50.0
721
-
722
- ### The factory's scope union now covers per-endpoint overrides
723
-
724
- `createContractFactory<Scope>()` already required a typed `scope` on the
725
- contract. It now holds a per-endpoint `scope` override to the same union. Nothing
726
- to migrate unless an override is outside the union — which was always a bug: the
727
- scope reached no auth rule, and `createAuthHook` threw
728
- `[stitchkit] auth: no rule for scope "…"` on the first request to it.
729
-
730
- ```ts
731
- const { defineContract } = createContractFactory<'public' | 'user' | 'admin'>()
732
-
733
- defineContract({ prefix: 'posts', scope: 'user' }, {
734
- // before: compiled; failed at request time
735
- // after: compile error naming the scope (TypeScript even suggests 'admin')
736
- purge: { method: 'DELETE', path: '/all', desc: 'Purge', scope: 'admn', output },
737
- })
738
- ```
739
-
740
- Fix the typo, or widen the factory union if the scope is real. Contracts built
741
- with plain `defineContract` are unaffected.
742
-
743
- ### A declared `defineErrors` message is now used
744
-
745
- `ErrorDefinition` accepts `message`. Nothing to migrate unless a registry already
746
- wrote that key: it type-checked before (excess-property checking does not fire
747
- through a `const` generic) and was ignored, so the code itself went on the wire.
748
-
749
- ```ts
750
- const { errors } = defineErrors({ GONE: { status: 410, message: 'Long gone' } })
751
- // before: errors.GONE().message === 'GONE'
752
- // after: errors.GONE().message === 'Long gone'
753
- ```
754
-
755
- If a registry carried `message` as a note to the reader rather than as
756
- user-facing text, either fix the text or drop the key. A code with no `details`
757
- schema also shows that text to a model (its tool `details` is `{ message }`).
758
-
759
- ### Optional: adopt `createScopedImplement`
760
-
761
- If the app declares one superset handler context because different scopes inject
762
- different fields, replace it with a scope map. This is additive — `createImplement`
763
- still works.
764
-
765
- ```ts
766
- // before — one context for every scope; `ctx.userId` is typed even in a
767
- // `public` handler, where the runtime never injects it
768
- interface AppContext extends RuntimeContext { userId: string; isAdmin: boolean }
769
- export const implement = createImplement<AppContext>()
770
-
771
- // after — each handler typed by its endpoint's effective scope
772
- export const implementFor = createScopedImplement<{
773
- public: object
774
- user: { userId: string }
775
- admin: { userId: string; isAdmin: boolean }
776
- }>()
777
- ```
778
-
779
- `'public'` must be a key (a contract with no `scope` is `'public'`). Write
780
- endpoints inline in the contract literal: an endpoint hoisted into a variable
781
- widens its `scope` to `string` and is reported as undeclared.
782
-
783
- ## Released migration: 0.49.0
784
-
785
- ### The server handle became managed
786
-
787
- `createServer()` and `serveNode()` return a handle that owns admission, HTTP
788
- drain, realtime closure and one deadline-bounded runtime stop:
789
-
790
- ```ts
791
- // before
792
- server.stop()
793
- await socket.io.close()
794
- // after
795
- await server.shutdown({ gracePeriodMs: 30_000 })
796
- ```
797
-
798
- The runtime-specific instance stays reachable at `.runtime`, so an escape hatch
799
- you already rely on does not disappear — but code that closed transports itself,
800
- in its own order, is now racing the handle. Delete the manual closes rather than
801
- keeping both; the handle's result tells you what it drained and what it forced.
802
-
803
- ### Socket.IO mounts through the whole handle
804
-
805
- ```ts
806
- // before
807
- createServer({ websocket: socket.websocket, rawRoutes: [socket.route] })
808
- // after
809
- createServer({ socket })
810
- ```
811
-
812
- One owner for the route, the WebSocket attachment and the closure. For a raw Bun
813
- lane, keep the composed `websocket` handler and pass `socket` beside it.
814
-
815
- ### Bun native `routes` are gone
816
-
817
- Native routes run before the Fetch handler, so they bypassed managed admission —
818
- which means they also bypassed shutdown, logging and observability, and that is
819
- why they had to go rather than be wired up:
820
-
821
- ```ts
822
- // before
823
- createServer({ routes: { '/health': () => Response.json({ ok: true }) } })
824
- // after
825
- createServer({ rawRoutes: [{ method: 'GET', path: '/health', handler: () => Response.json({ ok: true }) }] })
826
- ```
827
-
828
- ### The handshake policy takes a Web `Request`
829
-
830
- ```ts
831
- // before
832
- createSocketIOServer({ serverOptions: { allowRequest: (req, done) => done(null, allowed(req)) } })
833
- // after
834
- createSocketIOServer({ allowRequest: (request) => allowed(request) })
835
- ```
836
-
837
- The Node-shaped callback is gone, and the policy is now composed with shutdown
838
- admission on both runtimes: a handshake arriving during drain is refused for
839
- you.
840
-
841
- ## Released migration: 0.48.0
842
-
843
- ### Typed-client request options move to `.withOptions`
844
-
845
- Generated endpoint methods reserve their ordinary call signature for contract
846
- variables. This keeps them directly assignable to callback APIs whose runtime
847
- supplies its own second context argument, including `react-query-kit` and
848
- TanStack Query. Move imperative cancellation to the callable's explicit method:
849
-
850
- ```ts
851
- // before — endpoint with arguments
852
- await api.create({ name: 'Max' }, { signal })
853
-
854
- // after
855
- await api.create.withOptions({ name: 'Max' }, { signal })
856
-
857
- // before — endpoint without arguments
858
- await api.health({ signal })
859
-
860
- // after
861
- await api.health.withOptions({ signal })
862
- ```
863
-
864
- Direct query and mutation composition remains unchanged:
865
-
866
- ```ts
867
- createMutation({ mutationFn: api.create })
868
- createQuery({ queryKey: ['search'], fetcher: api.search })
869
- ```
870
-
871
- There is no positional-options alias. Ordinary generated methods ignore extra
872
- runtime callback arguments; only `.withOptions` reads `ClientRequestOptions`.
873
-
874
- ## Released migration: 0.47.0
875
-
876
- ### HTTP auth moves to the pre-body `authorize` phase
877
-
878
- Move the HTTP wiring of `createAuthHook` from `beforeHandle` to `authorize`.
879
- This lets Stitchkit reject an unauthorized JSON or multipart request after path
880
- parameter validation but before reading a body chunk. Keep application
881
- preconditions that depend on validated input in `beforeHandle`.
882
-
883
- ```ts
884
- const auth = createAuthHook({ authenticate, authorize })
885
-
886
- // before
887
- createServer({ services, hooks: { beforeHandle: auth } })
888
-
889
- // after
890
- createServer({ services, hooks: { authorize: auth } })
891
- ```
892
-
893
- Tool transports already receive parsed input, so their wiring does not move:
894
-
895
- ```ts
896
- createMcpHandler({ services, lifecycle: { beforeHandle: auth } })
897
- ```
898
-
899
- If a custom HTTP authorization hook read `ctx.input`, `ctx.files` or raw body
900
- state, split it: identity/scope checks belong in `authorize`; validated payload
901
- preconditions belong in `beforeHandle` or the domain service.
902
-
903
- ### Multipart uses a typed descriptor and `ctx.files`
904
-
905
- Replace every string multipart declaration, top-level `maxUploadBytes` and
906
- `ctx.file`. The descriptor is now the only source of request, per-file,
907
- cardinality and declared media-type policy.
908
-
909
- ```ts
910
- // before
911
- upload: {
912
- method: 'POST',
913
- path: '/',
914
- multipart: 'file',
915
- maxUploadBytes: 25 * 1024 * 1024,
916
- }
917
- upload: ({ file, input }) => store(file, input)
918
-
919
- // after
920
- upload: {
921
- method: 'POST',
922
- path: '/',
923
- multipart: {
924
- maxRequestBytes: 25 * 1024 * 1024,
925
- files: {
926
- file: {
927
- maxBytes: 20 * 1024 * 1024,
928
- contentTypes: ['image/*', 'application/pdf'],
929
- },
930
- },
931
- },
932
- }
933
- upload: ({ files, input }) => store(files.file, input)
934
- ```
935
-
936
- Multiple files are repeated under one multipart field name and arrive in the
937
- same order:
938
-
939
- ```ts
940
- files: {
941
- attachments: { multiple: true, maxFiles: 8 },
942
- }
943
-
944
- await api.upload({ attachments: [firstFile, secondFile] })
945
- // handler: files.attachments is File[]
946
- ```
947
-
948
- For direct-to-storage delivery, set `delivery: 'stream'` and implement the
949
- endpoint with `defineMultipartStream`. A receiver must consume its Web Stream
950
- and return `{ value, cleanup }`; the final handler sees only receiver values.
951
- There is no deprecated overload or buffered compatibility path under the old
952
- contract shape.
953
-
954
- ## Released migration: 0.46.0
955
-
956
- ### `REALTIME_CONTRACT_VIOLATION` joined the error registry
957
-
958
- Realtime contract failures use the framework error model instead of a bare
959
- `ZodError`, so an exhaustive map stops compiling until the code is added:
960
-
961
- ```ts
962
- // before
963
- { …, INTERNAL_SERVER_ERROR: 'internal' } satisfies Record<StitchErrorCode, string>
964
- // after
965
- { …, INTERNAL_SERVER_ERROR: 'internal', REALTIME_CONTRACT_VIOLATION: 'internal' } satisfies Record<StitchErrorCode, string>
966
- ```
967
-
968
- Only an exhaustive map breaks. Since 0.56.1 `codeMap` itself is partial, so a
969
- map without the `satisfies` keeps compiling and lets the code travel as itself.
970
-
971
- ### `RealtimeRejectedEvent.error` is an `AppError`
972
-
973
- ```ts
974
- // before
975
- onRejected: ({ error }) => error.issues
976
- // after
977
- onRejected: ({ error }) => error.details?.issues // the ZodError moves to error.cause
978
- ```
979
-
980
- The envelope gained `reason` and `fault`. The consequence worth checking: code
981
- reading `.issues` directly does not fail to compile if the handler is loosely
982
- typed — it silently reads `undefined`. Grep for `.issues` on rejection handlers.
983
-
984
- ### CLI construction refuses reserved names
985
-
986
- A contract field or tool named `json`, `wait`, `quiet`, `dry-run`, `help`,
987
- `version`, `wait-timeout` or `output-dir` now **throws while the CLI is built**,
988
- instead of being silently shadowed:
989
-
990
- ```ts
991
- // before: app schedule_job --wait 2h → {"path":"2h"}, exit 0
992
- // after: building a CLI over a contract with a "wait" field throws
993
- ```
994
-
995
- This one fires at startup, not at call time, so an application shipping such a
996
- field crashes on boot after the upgrade. That is deliberate — the old behaviour
997
- corrupted arguments silently — but it means the upgrade is not safe to deploy
998
- without building the CLI once locally.
999
-
1000
- ### `createToolLogger` writes to stderr
1001
-
1002
- stdout is the JSON-RPC channel of a stdio MCP server, and the previous
1003
- `console.info` default corrupted it. Pass `log` to redirect if your process
1004
- collected tool logs from stdout.
1005
-
1006
- ## Released migration: 0.44.0
1007
-
1008
- ### MCP TypeScript SDK v2 and protocol `2026-07-28`
1009
-
1010
- This is a hard cut: there is one Stitchkit API and no v1 aliases. Applications
1011
- that expose MCP install the split server package; applications that implement an
1012
- MCP host or run client E2E install the split client package.
1013
-
1014
- ```bash
1015
- bun remove @modelcontextprotocol/sdk
1016
- bun add @modelcontextprotocol/server@^2 ai@^7
1017
- # MCP hosts and client-side E2E only:
1018
- bun add -d @modelcontextprotocol/client@^2
1019
- ```
1020
-
1021
- Direct SDK imports move to the split packages too:
1022
-
1023
- ```ts
1024
- // before
1025
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
1026
-
1027
- // after
1028
- import { McpServer } from '@modelcontextprotocol/server'
1029
- ```
1030
-
1031
- MCP Apps additionally install `@modelcontextprotocol/ext-apps`. That adapter may
1032
- carry its own isolated v1-era transitive/peer relationship while the ecosystem
1033
- finishes its cutover; it does not permit application code to import the removed
1034
- monolithic SDK. Application-owned MCP server and client code uses the v2 split
1035
- packages exclusively.
1036
-
1037
- ```ts
1038
- // before
1039
- import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1040
- import { createMcpHandler } from 'stitchkit/tools'
1041
-
1042
- const handleMcp = createMcpHandler({
1043
- serverInfo,
1044
- auth,
1045
- services,
1046
- sessionMode: 'stateless',
1047
- })
1048
- rawRoutes: [{ method: 'ALL', path: '/mcp', handler: handleMcp }]
1049
-
1050
- // after
1051
- import { Client } from '@modelcontextprotocol/client'
1052
- import { createMcpHandler, createMcpHttpRoute } from 'stitchkit/tools'
1053
-
1054
- const mcp = createMcpHandler({
1055
- serverInfo,
1056
- auth,
1057
- services,
1058
- legacy: 'serve',
1059
- })
1060
- rawRoutes: [createMcpHttpRoute({ path: '/mcp', handler: mcp })]
1061
- // graceful shutdown: await mcp.close()
1062
- ```
1063
-
1064
- Keep the owned handle and close it from the same shutdown path as the HTTP
1065
- server. A minimal runtime smoke should list tools before shutdown and prove the
1066
- closed handler no longer serves requests:
1067
-
1068
- ```ts
1069
- const mcp = createMcpHandler(config)
1070
- const route = createMcpHttpRoute({ path: '/mcp', handler: mcp })
1071
-
1072
- const beforeClose = await route.handler(listToolsRequest)
1073
- if (!beforeClose.ok) throw new Error('MCP list-tools smoke failed')
1074
-
1075
- await mcp.close()
1076
- ```
1077
-
1078
- Remove `@modelcontextprotocol/sdk`, `sessionMode`, `McpSessionMode`, session
1079
- stores and all `Mcp-Session-Id` handling. HTTP is always request-isolated and
1080
- stateless. `legacy: 'serve'` (default) lets the official SDK negotiate supported
1081
- pre-2026 stateless clients on the same endpoint; `legacy: 'reject'` makes it
1082
- modern-only. This is not a stateful compatibility transport.
1083
-
1084
- #### Output shape depends on the negotiated protocol era
1085
-
1086
- Stitchkit always validates the handler result against the declared output
1087
- schema. The negotiated MCP era determines only its wire representation:
1088
-
1089
- | Negotiated era | Non-object `structuredContent` |
1090
- |---|---|
1091
- | MCP `2026-07-28` | the exact schema-valid JSON root: array, scalar or `null` |
1092
- | supported legacy era | the official SDK codec adapts it to `{ result: value }` |
1093
-
1094
- Object roots keep their object shape in both eras. Do not change every consumer
1095
- expectation to the modern shape while `legacy: 'serve'` remains enabled. Pin
1096
- both protocol versions in the consumer's transport E2E and assert the boundary
1097
- explicitly:
1098
-
1099
- ```ts
1100
- await expectToolOutputForProtocol('2026-07-28', ['a', 'b'])
1101
- await expectToolOutputForProtocol('2025-11-25', { result: ['a', 'b'] })
1102
-
1103
- async function expectToolOutputForProtocol(
1104
- protocolVersion: '2026-07-28' | '2025-11-25',
1105
- expected: unknown,
1106
- ) {
1107
- const client = await connectConsumerMcpClient({ protocolVersion })
1108
- const result = await client.callTool({ name: 'list_notes', arguments: {} })
1109
- expect(result.structuredContent).toEqual(expected)
1110
- await client.close()
1111
- }
1112
- ```
1113
-
1114
- `connectConsumerMcpClient` represents the consumer's real HTTP or stdio setup;
1115
- configure its official client with `versionNegotiation.mode.pin` so the test
1116
- cannot silently negotiate a different era.
1117
-
1118
- The stdio helper now returns an owned lifecycle handle:
1119
-
1120
- ```ts
1121
- // before
1122
- await createStdioMcpServer(config)
1123
-
1124
- // after
1125
- const stdio = await createStdioMcpServer({ ...config, legacy: 'serve' })
1126
- await stdio.close()
1127
- ```
1128
-
1129
- OAuth client registration is now one explicit policy object. CIMD is secure and
1130
- enabled by default; DCR is disabled unless supplied:
1131
-
1132
- ```ts
1133
- // before
1134
- mountOAuthProvider({ ...oauth, clients, codes, refreshTokens })
1135
-
1136
- // after
1137
- mountOAuthProvider({
1138
- ...oauth,
1139
- clientRegistration: {
1140
- preRegistered: { get: clients.get },
1141
- // optional: dcr: { register: clients.register, get: clients.get }
1142
- },
1143
- codes,
1144
- refreshTokens,
1145
- })
1146
- ```
1147
-
1148
- A URL client id must be HTTPS and serve a document whose `client_id` exactly
1149
- matches that URL, with explicit `redirect_uris` and
1150
- `token_endpoint_auth_method: 'none'`. Do not keep a consumer-side metadata
1151
- fetcher or DCR fallback; Stitchkit owns SSRF-safe resolution and caching.
1152
-
1153
- Multi-round input is opt-in on the operation and does not change Agent, CLI or
1154
- ordinary HTTP handlers:
1155
-
1156
- ```ts
1157
- const ConfirmationSchema = z.object({ confirmed: z.boolean() })
1158
-
1159
- mcp: {
1160
- inputRequired: [{
1161
- key: 'confirmation',
1162
- message: 'Confirm this action',
1163
- schema: ConfirmationSchema,
1164
- }],
1165
- }
1166
- ```
1167
-
1168
- Configure `multiRound.state` on the MCP server with a key of at least 32 bytes
1169
- and a stable authenticated `principal`. Read accepted typed content from
1170
- `ctx.mcpInput.confirmation`. Multiple declarations run in array order, the
1171
- aggregate remains exactly typed by key, keys must be unique, and declarations
1172
- must fit `multiRound.serving.maxRounds` (default `10`). Do not execute a
1173
- destructive side effect before the complete aggregate is accepted. A modern
1174
- request missing the declared elicitation capability receives JSON-RPC error
1175
- code `-32021`. The official per-request legacy HTTP bridge cannot issue
1176
- server-to-client elicitation, so it returns a deterministic failed tool result;
1177
- multi-round input is never silently treated as complete.
1178
-
1179
- #### Compatibility matrix
1180
-
1181
- | Host / transport | Tools and Apps | Multi-round input | Continuity |
1182
- |---|---|---|---|
1183
- | `2026-07-28` HTTP | yes | yes | request-isolated |
1184
- | supported legacy stateless HTTP | yes | unsupported result | request-isolated |
1185
- | `2026-07-28` stdio | yes | yes | one process connection |
1186
- | supported legacy stdio | yes | official SDK bridge | one process connection |
1187
- | Agent / CLI / ordinary HTTP | unchanged | not exposed | unchanged |
1188
-
1189
- Subscriptions, cross-request progress and resumable stateful SSE are not
1190
- implemented or advertised.
1191
-
1192
- #### Consumer checklist
1193
-
1194
- 1. Replace the monolithic SDK with `@modelcontextprotocol/server@^2` and, only
1195
- for hosts/tests, `@modelcontextprotocol/client@^2`.
1196
- 2. Replace raw MCP route wiring with `createMcpHttpRoute`; retain and close the
1197
- returned handler/stdio handle during shutdown.
1198
- 3. Delete all session mode, event-store and session-id code.
1199
- 4. Move OAuth client policy under `clientRegistration`; publish CIMD or enable
1200
- DCR explicitly.
1201
- 5. Make `authorizeUser` return the exact consented scope subset. The framework
1202
- validates that it is a subset of the request before saving the authorization
1203
- code:
1204
-
1205
- ```ts
1206
- // before
1207
- authorizeUser: async () => ({ userId })
1208
-
1209
- // after
1210
- authorizeUser: async (_req, request) => ({
1211
- userId,
1212
- approvedScopes: request.scope?.split(' ') ?? [],
1213
- })
1214
- ```
1215
-
1216
- 6. Snapshot `listToolNames`, run one contract tool, one runtime tool, any raw
1217
- multimodal tool and every MCP App resource you use.
1218
- 7. Exercise modern HTTP and stdio with protocol `2026-07-28`; exercise legacy
1219
- only if `legacy: 'serve'` is part of your support policy.
1220
- 8. Run the consumer's typecheck and runtime gates. A browser/HTTP-only consumer
1221
- must continue to work without either MCP package.