@substrat-run/contracts 0.147.0 → 0.148.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 (49) hide show
  1. package/dist/assistant-stream.d.ts +311 -0
  2. package/dist/assistant-stream.d.ts.map +1 -0
  3. package/dist/assistant-stream.js +360 -0
  4. package/dist/assistant-stream.js.map +1 -0
  5. package/dist/attachments.d.ts +153 -0
  6. package/dist/attachments.d.ts.map +1 -1
  7. package/dist/attachments.js +115 -1
  8. package/dist/attachments.js.map +1 -1
  9. package/dist/control-plane.d.ts +52 -2
  10. package/dist/control-plane.d.ts.map +1 -1
  11. package/dist/control-plane.js +45 -7
  12. package/dist/control-plane.js.map +1 -1
  13. package/dist/deploy.d.ts +74 -11
  14. package/dist/deploy.d.ts.map +1 -1
  15. package/dist/deploy.js +108 -18
  16. package/dist/deploy.js.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/kernel-events.d.ts.map +1 -1
  22. package/dist/kernel-events.js +4 -1
  23. package/dist/kernel-events.js.map +1 -1
  24. package/dist/lake.d.ts +109 -0
  25. package/dist/lake.d.ts.map +1 -0
  26. package/dist/lake.js +85 -0
  27. package/dist/lake.js.map +1 -0
  28. package/dist/manifest.d.ts +4 -0
  29. package/dist/manifest.d.ts.map +1 -1
  30. package/dist/manifest.js +5 -0
  31. package/dist/manifest.js.map +1 -1
  32. package/dist/model.d.ts +4 -0
  33. package/dist/model.d.ts.map +1 -1
  34. package/dist/model.js.map +1 -1
  35. package/dist/scope-store-envelope.d.ts +167 -0
  36. package/dist/scope-store-envelope.d.ts.map +1 -0
  37. package/dist/scope-store-envelope.js +390 -0
  38. package/dist/scope-store-envelope.js.map +1 -0
  39. package/dist/scope-store-transport.d.ts +1 -1
  40. package/dist/scope-store-transport.js +1 -1
  41. package/dist/script-names.d.ts +11 -0
  42. package/dist/script-names.d.ts.map +1 -1
  43. package/dist/script-names.js +16 -0
  44. package/dist/script-names.js.map +1 -1
  45. package/package.json +8 -4
  46. package/dist/scope-store-mac.d.ts +0 -24
  47. package/dist/scope-store-mac.d.ts.map +0 -1
  48. package/dist/scope-store-mac.js +0 -61
  49. package/dist/scope-store-mac.js.map +0 -1
@@ -0,0 +1,311 @@
1
+ import { z } from 'zod';
2
+ /** The version of this vocabulary. A change that is not additive bumps it. */
3
+ export declare const ASSISTANT_STREAM_VERSION = 1;
4
+ /**
5
+ * A single-use value minted per approval request, so two requests for the same call and the
6
+ * same input still digest differently and an old approval never fits a new request.
7
+ */
8
+ export declare const approvalNonce: z.ZodString;
9
+ /** The longest text delta. A producer splits a longer run of text into several. */
10
+ export declare const ASSISTANT_TEXT_DELTA_MAX = 16000;
11
+ /** The largest serialized tool input, in UTF-8 bytes. Inputs are arguments, not documents. */
12
+ export declare const ASSISTANT_TOOL_INPUT_MAX_BYTES: number;
13
+ /**
14
+ * The largest serialized tool output, in UTF-8 bytes. Every event is a row in the record and
15
+ * a Durable Object row has a hard ceiling, so a larger result goes by `ref` (§5.2: the
16
+ * record keeps a reference, not a copy).
17
+ */
18
+ export declare const ASSISTANT_TOOL_OUTPUT_MAX_BYTES: number;
19
+ /**
20
+ * The digest an approval binds to: lower-case hex SHA-256 (`approvalDigest`). 64
21
+ * characters, so a client cannot send a short one.
22
+ */
23
+ export declare const approvalDigestString: z.ZodString;
24
+ /** Why a turn came to rest. `awaiting-approval` is a pause, not an end. */
25
+ export declare const turnFinishReason: z.ZodEnum<{
26
+ "awaiting-approval": "awaiting-approval";
27
+ budget: "budget";
28
+ done: "done";
29
+ stopped: "stopped";
30
+ }>;
31
+ export type TurnFinishReason = z.infer<typeof turnFinishReason>;
32
+ /**
33
+ * Every event a producer can emit, without its `id`. Discriminated on `t`. Strict: a
34
+ * producer and the record parse with this; a client reads with `readAssistantStreamEvent`.
35
+ */
36
+ export declare const assistantStreamBody: z.ZodDiscriminatedUnion<[z.ZodObject<{
37
+ t: z.ZodLiteral<"user-message">;
38
+ text: z.ZodString;
39
+ }, z.core.$strip>, z.ZodObject<{
40
+ t: z.ZodLiteral<"turn-start">;
41
+ v: z.ZodNumber;
42
+ }, z.core.$strip>, z.ZodObject<{
43
+ t: z.ZodLiteral<"turn-finish">;
44
+ reason: z.ZodEnum<{
45
+ "awaiting-approval": "awaiting-approval";
46
+ budget: "budget";
47
+ done: "done";
48
+ stopped: "stopped";
49
+ }>;
50
+ }, z.core.$strip>, z.ZodObject<{
51
+ t: z.ZodLiteral<"turn-error">;
52
+ code: z.ZodString;
53
+ message: z.ZodString;
54
+ }, z.core.$strip>, z.ZodObject<{
55
+ t: z.ZodLiteral<"step-start">;
56
+ step: z.ZodString;
57
+ }, z.core.$strip>, z.ZodObject<{
58
+ t: z.ZodLiteral<"step-end">;
59
+ step: z.ZodString;
60
+ }, z.core.$strip>, z.ZodObject<{
61
+ t: z.ZodLiteral<"step-restart">;
62
+ step: z.ZodString;
63
+ }, z.core.$strip>, z.ZodObject<{
64
+ pid: z.ZodString;
65
+ t: z.ZodLiteral<"text-start">;
66
+ }, z.core.$strip>, z.ZodObject<{
67
+ pid: z.ZodString;
68
+ t: z.ZodLiteral<"text-delta">;
69
+ delta: z.ZodString;
70
+ }, z.core.$strip>, z.ZodObject<{
71
+ pid: z.ZodString;
72
+ t: z.ZodLiteral<"text-end">;
73
+ }, z.core.$strip>, z.ZodObject<{
74
+ t: z.ZodLiteral<"tool-input">;
75
+ callId: z.ZodString;
76
+ tool: z.ZodString;
77
+ input: z.ZodUnknown;
78
+ }, z.core.$strip>, z.ZodObject<{
79
+ t: z.ZodLiteral<"tool-output">;
80
+ callId: z.ZodString;
81
+ tool: z.ZodString;
82
+ output: z.ZodOptional<z.ZodUnknown>;
83
+ ref: z.ZodOptional<z.ZodString>;
84
+ }, z.core.$strip>, z.ZodObject<{
85
+ t: z.ZodLiteral<"tool-error">;
86
+ callId: z.ZodString;
87
+ tool: z.ZodString;
88
+ message: z.ZodString;
89
+ }, z.core.$strip>, z.ZodObject<{
90
+ t: z.ZodLiteral<"approval-request">;
91
+ callId: z.ZodString;
92
+ step: z.ZodString;
93
+ tool: z.ZodString;
94
+ operation: z.ZodString;
95
+ version: z.ZodString;
96
+ input: z.ZodUnknown;
97
+ expiresAt: z.core.$ZodBranded<z.ZodString, "Instant", "out">;
98
+ nonce: z.ZodString;
99
+ digest: z.ZodString;
100
+ preview: z.ZodString;
101
+ }, z.core.$strip>, z.ZodObject<{
102
+ t: z.ZodLiteral<"approval-resolved">;
103
+ callId: z.ZodString;
104
+ approved: z.ZodBoolean;
105
+ approver: z.core.$ZodBranded<z.ZodString, "PrincipalId", "out">;
106
+ digest: z.ZodString;
107
+ }, z.core.$strip>, z.ZodObject<{
108
+ t: z.ZodLiteral<"source">;
109
+ sid: z.ZodString;
110
+ title: z.ZodString;
111
+ url: z.ZodOptional<z.ZodString>;
112
+ ref: z.ZodOptional<z.ZodString>;
113
+ }, z.core.$strip>, z.ZodObject<{
114
+ t: z.ZodLiteral<"data-provenance">;
115
+ producedAt: z.core.$ZodBranded<z.ZodString, "Instant", "out">;
116
+ model: z.ZodOptional<z.ZodString>;
117
+ sources: z.ZodOptional<z.ZodNumber>;
118
+ query: z.ZodOptional<z.ZodString>;
119
+ }, z.core.$strip>, z.ZodObject<{
120
+ t: z.ZodLiteral<"budget-notice">;
121
+ exhausted: z.ZodBoolean;
122
+ resetsAt: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "Instant", "out">>;
123
+ }, z.core.$strip>], "t">;
124
+ export type AssistantStreamBody = z.infer<typeof assistantStreamBody>;
125
+ /** The event type literals, for exhaustive handling on a client. */
126
+ export type AssistantStreamEventType = AssistantStreamBody['t'];
127
+ /**
128
+ * An event as a client receives it: the body plus the id the record gave it. Integers
129
+ * strictly increase per thread and are the resume cursor.
130
+ */
131
+ export declare const assistantStreamEvent: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
132
+ t: z.ZodLiteral<"user-message">;
133
+ text: z.ZodString;
134
+ }, z.core.$strip>, z.ZodObject<{
135
+ t: z.ZodLiteral<"turn-start">;
136
+ v: z.ZodNumber;
137
+ }, z.core.$strip>, z.ZodObject<{
138
+ t: z.ZodLiteral<"turn-finish">;
139
+ reason: z.ZodEnum<{
140
+ "awaiting-approval": "awaiting-approval";
141
+ budget: "budget";
142
+ done: "done";
143
+ stopped: "stopped";
144
+ }>;
145
+ }, z.core.$strip>, z.ZodObject<{
146
+ t: z.ZodLiteral<"turn-error">;
147
+ code: z.ZodString;
148
+ message: z.ZodString;
149
+ }, z.core.$strip>, z.ZodObject<{
150
+ t: z.ZodLiteral<"step-start">;
151
+ step: z.ZodString;
152
+ }, z.core.$strip>, z.ZodObject<{
153
+ t: z.ZodLiteral<"step-end">;
154
+ step: z.ZodString;
155
+ }, z.core.$strip>, z.ZodObject<{
156
+ t: z.ZodLiteral<"step-restart">;
157
+ step: z.ZodString;
158
+ }, z.core.$strip>, z.ZodObject<{
159
+ pid: z.ZodString;
160
+ t: z.ZodLiteral<"text-start">;
161
+ }, z.core.$strip>, z.ZodObject<{
162
+ pid: z.ZodString;
163
+ t: z.ZodLiteral<"text-delta">;
164
+ delta: z.ZodString;
165
+ }, z.core.$strip>, z.ZodObject<{
166
+ pid: z.ZodString;
167
+ t: z.ZodLiteral<"text-end">;
168
+ }, z.core.$strip>, z.ZodObject<{
169
+ t: z.ZodLiteral<"tool-input">;
170
+ callId: z.ZodString;
171
+ tool: z.ZodString;
172
+ input: z.ZodUnknown;
173
+ }, z.core.$strip>, z.ZodObject<{
174
+ t: z.ZodLiteral<"tool-output">;
175
+ callId: z.ZodString;
176
+ tool: z.ZodString;
177
+ output: z.ZodOptional<z.ZodUnknown>;
178
+ ref: z.ZodOptional<z.ZodString>;
179
+ }, z.core.$strip>, z.ZodObject<{
180
+ t: z.ZodLiteral<"tool-error">;
181
+ callId: z.ZodString;
182
+ tool: z.ZodString;
183
+ message: z.ZodString;
184
+ }, z.core.$strip>, z.ZodObject<{
185
+ t: z.ZodLiteral<"approval-request">;
186
+ callId: z.ZodString;
187
+ step: z.ZodString;
188
+ tool: z.ZodString;
189
+ operation: z.ZodString;
190
+ version: z.ZodString;
191
+ input: z.ZodUnknown;
192
+ expiresAt: z.core.$ZodBranded<z.ZodString, "Instant", "out">;
193
+ nonce: z.ZodString;
194
+ digest: z.ZodString;
195
+ preview: z.ZodString;
196
+ }, z.core.$strip>, z.ZodObject<{
197
+ t: z.ZodLiteral<"approval-resolved">;
198
+ callId: z.ZodString;
199
+ approved: z.ZodBoolean;
200
+ approver: z.core.$ZodBranded<z.ZodString, "PrincipalId", "out">;
201
+ digest: z.ZodString;
202
+ }, z.core.$strip>, z.ZodObject<{
203
+ t: z.ZodLiteral<"source">;
204
+ sid: z.ZodString;
205
+ title: z.ZodString;
206
+ url: z.ZodOptional<z.ZodString>;
207
+ ref: z.ZodOptional<z.ZodString>;
208
+ }, z.core.$strip>, z.ZodObject<{
209
+ t: z.ZodLiteral<"data-provenance">;
210
+ producedAt: z.core.$ZodBranded<z.ZodString, "Instant", "out">;
211
+ model: z.ZodOptional<z.ZodString>;
212
+ sources: z.ZodOptional<z.ZodNumber>;
213
+ query: z.ZodOptional<z.ZodString>;
214
+ }, z.core.$strip>, z.ZodObject<{
215
+ t: z.ZodLiteral<"budget-notice">;
216
+ exhausted: z.ZodBoolean;
217
+ resetsAt: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "Instant", "out">>;
218
+ }, z.core.$strip>], "t">, z.ZodObject<{
219
+ id: z.ZodNumber;
220
+ }, z.core.$strip>>;
221
+ export type AssistantStreamEvent = z.infer<typeof assistantStreamEvent>;
222
+ /** What `readAssistantStreamEvent` hands a client. */
223
+ export type ReadAssistantStreamEvent = {
224
+ known: true;
225
+ event: AssistantStreamEvent;
226
+ } | {
227
+ known: false;
228
+ t: string;
229
+ id: number;
230
+ raw: Record<string, unknown>;
231
+ };
232
+ /**
233
+ * How a CLIENT reads an event: forward-compatible where the strict schema is not.
234
+ *
235
+ * - A `t` this version does not know, or a known `t` whose only fault is an enum value this
236
+ * version does not know (a new `turn-finish` reason), is `{ known: false }` with its `id`,
237
+ * so the client still advances its resume cursor and skips it, rather than throwing on the
238
+ * first additive event.
239
+ * - Anything else that does not parse throws: a known event missing a field, or a value of
240
+ * the wrong type (an enum field that is absent, a number or `null` included), is a broken
241
+ * producer, not a newer one.
242
+ * - No `t`, or no valid `id`, throws too: without them the event cannot even be skipped.
243
+ */
244
+ export declare function readAssistantStreamEvent(raw: unknown): ReadAssistantStreamEvent;
245
+ /** The events a turn can come to rest on. A stream that ends on one needs no resume. */
246
+ export declare const ASSISTANT_RESTING_EVENTS: ReadonlySet<AssistantStreamEventType>;
247
+ /** True when `event` ends a run: nothing further will arrive until someone acts. */
248
+ export declare function isRestingEvent(event: {
249
+ t: string;
250
+ }): boolean;
251
+ /** The tag that opens every approval tuple. A new tuple shape is a new tag, never an edit. */
252
+ export declare const APPROVAL_DIGEST_TAG = "substrat.approval/v1";
253
+ /**
254
+ * Everything an approval is bound to. `version` is the deployed version the request was made
255
+ * under, `approver` is the principal who may say yes (the
256
+ * thread's subject), `thread` the thread, `step` and `call` the step and tool call it belongs
257
+ * to, `operation` what will run, `input` its parsed input, `expiresAt` when the request lapses
258
+ * and `nonce` a value minted for this one request.
259
+ */
260
+ export declare const approvalBinding: z.ZodObject<{
261
+ version: z.ZodString;
262
+ approver: z.core.$ZodBranded<z.ZodString, "PrincipalId", "out">;
263
+ thread: z.ZodString;
264
+ step: z.ZodString;
265
+ call: z.ZodString;
266
+ operation: z.ZodString;
267
+ expiresAt: z.ZodString;
268
+ nonce: z.ZodString;
269
+ input: z.ZodUnknown;
270
+ }, z.core.$strip>;
271
+ export type ApprovalBinding = z.input<typeof approvalBinding>;
272
+ /**
273
+ * The encoding the approval digest hashes. Its OWN, deliberately not `canonicalJson`: an
274
+ * idempotency fingerprint may change its encoding between deploys, and an approval digest
275
+ * may not — digests are stored and compared across deploys, so a change here invalidates
276
+ * every pending approval. The test pins vectors for it.
277
+ *
278
+ * A subset of JSON with one spelling per value:
279
+ * - object keys sorted by UTF-16 code unit; a member whose value is `undefined` is left out,
280
+ * so an unset optional and one set to `undefined` agree;
281
+ * - strings as `JSON.stringify` writes them (a lone surrogate escaped, never replaced);
282
+ * - numbers as `Number#toString` writes them (`0.1`, `1e+21`), finite only, `-0` as `0`;
283
+ * - arrays in order.
284
+ *
285
+ * It THROWS on anything else — `undefined` outside an object member (an array hole included), a symbol key, a
286
+ * non-enumerable property, a named property on an array, `NaN`, `±Infinity`, a
287
+ * bigint, a function, a symbol, a `Date`, `Map`, `Set` or any other non-plain object, and a
288
+ * cycle — rather than collapse two different values into one spelling, as JSON does.
289
+ */
290
+ export declare function approvalEncoding(value: unknown): string;
291
+ /**
292
+ * The digest an approval is bound to: SHA-256 over `approvalEncoding` of the tagged tuple
293
+ * `['substrat.approval/v1', version, approver, thread, step, call, operation, expiresAt, nonce, input]`,
294
+ * as 64 lower-case hex characters. Web Crypto, so it is the same function on the host, in a
295
+ * test and in a browser.
296
+ *
297
+ * The engine stores the digest of the request it showed and, when an answer arrives,
298
+ * recomputes it with the principal who ANSWERED as `approver`. So an approval from anyone
299
+ * but the thread's subject, for another thread, step, call or operation, or for an input the
300
+ * model changed afterwards, or under another deployed version, does not match. Same-looking calls never share a digest: one
301
+ * `{ id }` archived and deleted, or one call in two threads, digest differently.
302
+ *
303
+ * What the digest does NOT do, and the engine must: refuse an answer after `expiresAt`
304
+ * (the digest binds the expiry, it cannot read a clock), and accept each request once (the
305
+ * nonce makes requests distinct; single use is the record's conditional append, not a read
306
+ * before it). The answer must also arrive on a user-presence route the loop cannot call.
307
+ *
308
+ * Throws when a binding is malformed or the input holds a value `approvalEncoding` refuses.
309
+ */
310
+ export declare function approvalDigest(binding: ApprovalBinding): Promise<string>;
311
+ //# sourceMappingURL=assistant-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-stream.d.ts","sourceRoot":"","sources":["../src/assistant-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkCxB,8EAA8E;AAC9E,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAc1C;;;GAGG;AACH,eAAO,MAAM,aAAa,aAA6C,CAAC;AAExE,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,8FAA8F;AAC9F,eAAO,MAAM,8BAA8B,QAAY,CAAC;AACxD;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,QAAY,CAAC;AAmCzD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,aAAqC,CAAC;AAEvE,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB;;;;;EAA6D,CAAC;AAC3F,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAIhE;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAyF9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,oEAAoE;AACpE,MAAM,MAAM,wBAAwB,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;AAIhE;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAAiE,CAAC;AACnG,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIxE,sDAAsD;AACtD,MAAM,MAAM,wBAAwB,GAChC;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,GAC5C;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAE1E;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,wBAAwB,CAY/E;AAWD,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,EAAE,WAAW,CAAC,wBAAwB,CAA0C,CAAC;AAEtH,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAE5D;AAID,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;iBAe1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEvD;AA+CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9E"}
@@ -0,0 +1,360 @@
1
+ import { z } from 'zod';
2
+ import { instant, principalId } from './ids.js';
3
+ // The assistant's stream protocol (docs/rfc/assistant.md §5.1, #2283): the events a thread
4
+ // is made of, in the order a client folds them into a conversation.
5
+ //
6
+ // **Experimental.** Published only at the subpath `@substrat-run/contracts/assistant-stream`
7
+ // and kept out of the package root until the RFC (#2284) settles the actor model and a
8
+ // product consumes it. The names may still change, and they change here without a version
9
+ // bump until the root exports it.
10
+ //
11
+ // The vocabulary is shaped like the AI SDK's UI message parts on purpose — text and tool
12
+ // calls as start / delta / end or input / output, an approval request as a state a tool call
13
+ // passes through — so a mapping to and from that format is mechanical. It is OWNED here,
14
+ // not imported from it: the element and every client are published, and must not break when
15
+ // a library ships a major version with a different wire format.
16
+ //
17
+ // Three properties the rest of the design leans on:
18
+ //
19
+ // 1. `id` belongs to the RECORD, never to the producer. An event is written first and
20
+ // delivered second, and `id` is what the record assigned it — a strictly increasing
21
+ // integer per thread, and the cursor a client resumes after (`Last-Event-ID`). So an
22
+ // event has two shapes: the body a producer emits, and the event a client receives.
23
+ // 2. A turn is resumable from its events alone. A step that was cut off is restarted from
24
+ // its beginning, announced by `step-restart`, so a client drops the partial text of
25
+ // that step instead of showing it twice.
26
+ // 3. Additive only, and a client READS through `readAssistantStreamEvent`, not the strict
27
+ // schema. That reader hands back an event type it does not know, and a known type
28
+ // carrying an enum value it does not know, as `{ known: false }` rather than throwing,
29
+ // so a new event type, a new enum value or a new optional field is not a break. A
30
+ // removal, a rename or a retype is `ASSISTANT_STREAM_VERSION` plus one, and so is changing
31
+ // a size bound: a reader at this version throws on a value over its bound (`too_big`),
32
+ // so raising one is not additive. A stream says which version it speaks on `turn-start`.
33
+ /** The version of this vocabulary. A change that is not additive bumps it. */
34
+ export const ASSISTANT_STREAM_VERSION = 1;
35
+ /** Names a text part within a turn, so deltas find their part. */
36
+ const partId = z.string().min(1).max(64);
37
+ /** Names a tool call within a turn, so its input, approval and output find each other. */
38
+ const callId = z.string().min(1).max(64);
39
+ /** What the step is called in the loop (`lookup`, `propose`, …). Opaque to a client. */
40
+ const stepName = z.string().min(1).max(64);
41
+ /** A tool as the model knows it. Not an operation name: the host maps one to the other. */
42
+ const toolName = z.string().min(1).max(128);
43
+ /** The deployed version a request was made under. Opaque: whatever the host's version identity is. */
44
+ const deployedVersion = z.string().min(1).max(128);
45
+ /** The operation a tool call runs as (`todo/archive`). What an approval is for. */
46
+ const operationName = z.string().min(1).max(128);
47
+ /**
48
+ * A single-use value minted per approval request, so two requests for the same call and the
49
+ * same input still digest differently and an old approval never fits a new request.
50
+ */
51
+ export const approvalNonce = z.string().regex(/^[0-9A-Za-z_-]{16,64}$/);
52
+ /** The longest text delta. A producer splits a longer run of text into several. */
53
+ export const ASSISTANT_TEXT_DELTA_MAX = 16_000;
54
+ /** The largest serialized tool input, in UTF-8 bytes. Inputs are arguments, not documents. */
55
+ export const ASSISTANT_TOOL_INPUT_MAX_BYTES = 16 * 1024;
56
+ /**
57
+ * The largest serialized tool output, in UTF-8 bytes. Every event is a row in the record and
58
+ * a Durable Object row has a hard ceiling, so a larger result goes by `ref` (§5.2: the
59
+ * record keeps a reference, not a copy).
60
+ */
61
+ export const ASSISTANT_TOOL_OUTPUT_MAX_BYTES = 32 * 1024;
62
+ /** A value JSON can carry, at most `maxBytes` once serialized. */
63
+ function boundedJson(maxBytes) {
64
+ return z.unknown().superRefine((value, ctx) => {
65
+ let text;
66
+ try {
67
+ text = JSON.stringify(value);
68
+ }
69
+ catch {
70
+ text = undefined;
71
+ }
72
+ if (text === undefined) {
73
+ ctx.addIssue({ code: 'custom', message: 'must be a value JSON can carry' });
74
+ return;
75
+ }
76
+ const bytes = new TextEncoder().encode(text).length;
77
+ if (bytes > maxBytes) {
78
+ ctx.addIssue({ code: 'custom', message: `is ${bytes} bytes serialized; at most ${maxBytes}` });
79
+ }
80
+ });
81
+ }
82
+ /** A link a client may render: `http:` or `https:` only, never `javascript:` or `data:`. */
83
+ const httpUrl = z
84
+ .string()
85
+ .max(2_000)
86
+ .refine((s) => {
87
+ try {
88
+ const { protocol } = new URL(s);
89
+ return protocol === 'http:' || protocol === 'https:';
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }, 'must be an http(s) URL');
95
+ /**
96
+ * The digest an approval binds to: lower-case hex SHA-256 (`approvalDigest`). 64
97
+ * characters, so a client cannot send a short one.
98
+ */
99
+ export const approvalDigestString = z.string().regex(/^[0-9a-f]{64}$/);
100
+ /** Why a turn came to rest. `awaiting-approval` is a pause, not an end. */
101
+ export const turnFinishReason = z.enum(['done', 'awaiting-approval', 'stopped', 'budget']);
102
+ const text = z.object({ pid: partId });
103
+ /**
104
+ * Every event a producer can emit, without its `id`. Discriminated on `t`. Strict: a
105
+ * producer and the record parse with this; a client reads with `readAssistantStreamEvent`.
106
+ */
107
+ export const assistantStreamBody = z.discriminatedUnion('t', [
108
+ // The user's side of a turn, and the start of the turn it opens. `v` is the
109
+ // `ASSISTANT_STREAM_VERSION` the producer speaks, so a client can tell a stream it cannot
110
+ // read from one it can.
111
+ z.object({ t: z.literal('user-message'), text: z.string().max(32_000) }),
112
+ z.object({ t: z.literal('turn-start'), v: z.number().int().positive() }),
113
+ // Rest. `awaiting-approval` ends the run, not the turn: an approval resumes it.
114
+ z.object({ t: z.literal('turn-finish'), reason: turnFinishReason }),
115
+ // A failure the turn ends on. `code` is a stable slug a client branches on, `message` is
116
+ // for the user, in their language, and never a stack trace.
117
+ z.object({ t: z.literal('turn-error'), code: z.string().min(1).max(64), message: z.string().max(2_000) }),
118
+ // Step boundaries. `step-restart` names the step that was cut off and is starting over.
119
+ z.object({ t: z.literal('step-start'), step: stepName }),
120
+ z.object({ t: z.literal('step-end'), step: stepName }),
121
+ z.object({ t: z.literal('step-restart'), step: stepName }),
122
+ // Text, as start / delta / end. Neighbouring deltas of one part may be merged by the
123
+ // record (up to the bound), so a client must not assume one delta per token.
124
+ text.extend({ t: z.literal('text-start') }),
125
+ text.extend({ t: z.literal('text-delta'), delta: z.string().max(ASSISTANT_TEXT_DELTA_MAX) }),
126
+ text.extend({ t: z.literal('text-end') }),
127
+ // A tool call: its input, then its output or its error. A result too large for a row
128
+ // travels as `ref` (what the product resolves it by) with `output` absent or a summary.
129
+ z.object({ t: z.literal('tool-input'), callId, tool: toolName, input: boundedJson(ASSISTANT_TOOL_INPUT_MAX_BYTES) }),
130
+ z.object({
131
+ t: z.literal('tool-output'),
132
+ callId,
133
+ tool: toolName,
134
+ output: boundedJson(ASSISTANT_TOOL_OUTPUT_MAX_BYTES).optional(),
135
+ ref: z.string().min(1).max(500).optional(),
136
+ }),
137
+ z.object({ t: z.literal('tool-error'), callId, tool: toolName, message: z.string().max(2_000) }),
138
+ // A gated write waiting for the user. `preview` is the product's own wording of what will
139
+ // change, never the model's prose. `digest` is `approvalDigest` over this request's
140
+ // bindings plus the thread and its subject (the approver), which the record knows.
141
+ // `version` is the deployed version the request was made under: the operation and its
142
+ // preview may change in a deploy while a request waits, and the user approved what THIS
143
+ // version showed, so an answer arriving under another version is refused.
144
+ z.object({
145
+ t: z.literal('approval-request'),
146
+ callId,
147
+ step: stepName,
148
+ tool: toolName,
149
+ operation: operationName,
150
+ version: deployedVersion,
151
+ input: boundedJson(ASSISTANT_TOOL_INPUT_MAX_BYTES),
152
+ expiresAt: instant,
153
+ nonce: approvalNonce,
154
+ digest: approvalDigestString,
155
+ preview: z.string().max(2_000),
156
+ }),
157
+ // The user's answer. `approver` is who answered, as the host authenticated them on this
158
+ // message; `digest` echoes the request it answers, so the record shows what was agreed to.
159
+ z.object({
160
+ t: z.literal('approval-resolved'),
161
+ callId,
162
+ approved: z.boolean(),
163
+ approver: principalId,
164
+ digest: approvalDigestString,
165
+ }),
166
+ // Where an answer came from. `ref` is whatever the product links to (an entity, a page).
167
+ z.object({
168
+ t: z.literal('source'),
169
+ sid: z.string().min(1).max(64),
170
+ title: z.string().max(500),
171
+ url: httpUrl.optional(),
172
+ ref: z.string().max(500).optional(),
173
+ }),
174
+ // The AI marker and its provenance: when, which model, how many sources, and (for staff
175
+ // only; the host withholds it from anyone else) the query it ran.
176
+ z.object({
177
+ t: z.literal('data-provenance'),
178
+ producedAt: instant,
179
+ model: z.string().max(200).optional(),
180
+ sources: z.number().int().nonnegative().optional(),
181
+ query: z.string().max(4_000).optional(),
182
+ }),
183
+ // A limit reached, or about to be. Always carries enough for the UI to say what happens next.
184
+ z.object({
185
+ t: z.literal('budget-notice'),
186
+ exhausted: z.boolean(),
187
+ resetsAt: instant.optional(),
188
+ }),
189
+ ]);
190
+ const eventId = z.number().int().positive();
191
+ /**
192
+ * An event as a client receives it: the body plus the id the record gave it. Integers
193
+ * strictly increase per thread and are the resume cursor.
194
+ */
195
+ export const assistantStreamEvent = z.intersection(assistantStreamBody, z.object({ id: eventId }));
196
+ const KNOWN_TYPES = new Set(assistantStreamBody.options.map((o) => o.shape.t.value));
197
+ /**
198
+ * How a CLIENT reads an event: forward-compatible where the strict schema is not.
199
+ *
200
+ * - A `t` this version does not know, or a known `t` whose only fault is an enum value this
201
+ * version does not know (a new `turn-finish` reason), is `{ known: false }` with its `id`,
202
+ * so the client still advances its resume cursor and skips it, rather than throwing on the
203
+ * first additive event.
204
+ * - Anything else that does not parse throws: a known event missing a field, or a value of
205
+ * the wrong type (an enum field that is absent, a number or `null` included), is a broken
206
+ * producer, not a newer one.
207
+ * - No `t`, or no valid `id`, throws too: without them the event cannot even be skipped.
208
+ */
209
+ export function readAssistantStreamEvent(raw) {
210
+ const envelope = z.looseObject({ t: z.string().min(1), id: eventId }).parse(raw);
211
+ const record = raw;
212
+ if (!KNOWN_TYPES.has(envelope.t))
213
+ return { known: false, t: envelope.t, id: envelope.id, raw: record };
214
+ const parsed = assistantStreamEvent.safeParse(raw);
215
+ if (parsed.success)
216
+ return { known: true, event: parsed.data };
217
+ // Zod says `invalid_value` for an enum whatever it was handed, so only a present string
218
+ // counts as a value from a newer version; a missing one, a number or a null is broken.
219
+ if (parsed.error.issues.every((issue) => issue.code === 'invalid_value' && typeof valueAt(raw, issue.path) === 'string')) {
220
+ return { known: false, t: envelope.t, id: envelope.id, raw: record };
221
+ }
222
+ throw parsed.error;
223
+ }
224
+ function valueAt(root, path) {
225
+ let at = root;
226
+ for (const key of path) {
227
+ if (at === null || typeof at !== 'object' || !Object.hasOwn(at, key))
228
+ return undefined;
229
+ at = at[key];
230
+ }
231
+ return at;
232
+ }
233
+ /** The events a turn can come to rest on. A stream that ends on one needs no resume. */
234
+ export const ASSISTANT_RESTING_EVENTS = new Set(['turn-finish', 'turn-error']);
235
+ /** True when `event` ends a run: nothing further will arrive until someone acts. */
236
+ export function isRestingEvent(event) {
237
+ return ASSISTANT_RESTING_EVENTS.has(event.t);
238
+ }
239
+ // ── The approval digest ─────────────────────────────────────────────────────────────────
240
+ /** The tag that opens every approval tuple. A new tuple shape is a new tag, never an edit. */
241
+ export const APPROVAL_DIGEST_TAG = 'substrat.approval/v1';
242
+ /**
243
+ * Everything an approval is bound to. `version` is the deployed version the request was made
244
+ * under, `approver` is the principal who may say yes (the
245
+ * thread's subject), `thread` the thread, `step` and `call` the step and tool call it belongs
246
+ * to, `operation` what will run, `input` its parsed input, `expiresAt` when the request lapses
247
+ * and `nonce` a value minted for this one request.
248
+ */
249
+ export const approvalBinding = z.object({
250
+ version: deployedVersion,
251
+ approver: principalId,
252
+ thread: z.string().min(1).max(64),
253
+ step: stepName,
254
+ call: callId,
255
+ operation: operationName,
256
+ // The event's own `instant` rule, and only in `toISOString()`'s form, so one moment has one
257
+ // digest and an expiry the event would refuse cannot be digested.
258
+ expiresAt: z.string().refine((s) => {
259
+ const parsed = instant.safeParse(s);
260
+ return parsed.success && parsed.data === s;
261
+ }, 'must be an instant as toISOString() writes it'),
262
+ nonce: approvalNonce,
263
+ input: z.unknown(),
264
+ });
265
+ /**
266
+ * The encoding the approval digest hashes. Its OWN, deliberately not `canonicalJson`: an
267
+ * idempotency fingerprint may change its encoding between deploys, and an approval digest
268
+ * may not — digests are stored and compared across deploys, so a change here invalidates
269
+ * every pending approval. The test pins vectors for it.
270
+ *
271
+ * A subset of JSON with one spelling per value:
272
+ * - object keys sorted by UTF-16 code unit; a member whose value is `undefined` is left out,
273
+ * so an unset optional and one set to `undefined` agree;
274
+ * - strings as `JSON.stringify` writes them (a lone surrogate escaped, never replaced);
275
+ * - numbers as `Number#toString` writes them (`0.1`, `1e+21`), finite only, `-0` as `0`;
276
+ * - arrays in order.
277
+ *
278
+ * It THROWS on anything else — `undefined` outside an object member (an array hole included), a symbol key, a
279
+ * non-enumerable property, a named property on an array, `NaN`, `±Infinity`, a
280
+ * bigint, a function, a symbol, a `Date`, `Map`, `Set` or any other non-plain object, and a
281
+ * cycle — rather than collapse two different values into one spelling, as JSON does.
282
+ */
283
+ export function approvalEncoding(value) {
284
+ return encode(value, new Set());
285
+ }
286
+ function encode(value, path) {
287
+ if (value === null)
288
+ return 'null';
289
+ switch (typeof value) {
290
+ case 'boolean':
291
+ return value ? 'true' : 'false';
292
+ case 'string':
293
+ return JSON.stringify(value);
294
+ case 'number':
295
+ if (!Number.isFinite(value))
296
+ throw new TypeError(`approvalEncoding: ${value} has no encoding`);
297
+ return String(value); // `String(-0)` is '0'
298
+ case 'object':
299
+ break;
300
+ default:
301
+ throw new TypeError(`approvalEncoding: a ${typeof value} has no encoding`);
302
+ }
303
+ const object = value;
304
+ if (path.has(object))
305
+ throw new TypeError('approvalEncoding: a cycle has no encoding');
306
+ path.add(object);
307
+ try {
308
+ // What `Object.keys` cannot see (a symbol key, a non-enumerable property, a named
309
+ // property on an array) is refused, not dropped: dropping it would give two values one
310
+ // spelling. An array owns its indices and `length`, and nothing else (a hole, one fewer,
311
+ // is refused below as the `undefined` it reads as).
312
+ const own = Reflect.ownKeys(object).length;
313
+ if (Array.isArray(object) ? own > object.length + 1 : own !== Object.keys(object).length) {
314
+ throw new TypeError('approvalEncoding: a property JSON cannot see has no encoding');
315
+ }
316
+ if (Array.isArray(object)) {
317
+ // `Array.from` reads a hole as `undefined`, which is refused like any other.
318
+ return `[${Array.from(object, (entry) => encode(entry, path)).join(',')}]`;
319
+ }
320
+ const proto = Object.getPrototypeOf(object);
321
+ if (proto !== Object.prototype && proto !== null) {
322
+ throw new TypeError(`approvalEncoding: a ${proto?.constructor?.name ?? 'non-plain object'} has no encoding`);
323
+ }
324
+ const members = Object.entries(object)
325
+ .filter(([, v]) => v !== undefined)
326
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
327
+ .map(([k, v]) => `${JSON.stringify(k)}:${encode(v, path)}`);
328
+ return `{${members.join(',')}}`;
329
+ }
330
+ finally {
331
+ path.delete(object);
332
+ }
333
+ }
334
+ /**
335
+ * The digest an approval is bound to: SHA-256 over `approvalEncoding` of the tagged tuple
336
+ * `['substrat.approval/v1', version, approver, thread, step, call, operation, expiresAt, nonce, input]`,
337
+ * as 64 lower-case hex characters. Web Crypto, so it is the same function on the host, in a
338
+ * test and in a browser.
339
+ *
340
+ * The engine stores the digest of the request it showed and, when an answer arrives,
341
+ * recomputes it with the principal who ANSWERED as `approver`. So an approval from anyone
342
+ * but the thread's subject, for another thread, step, call or operation, or for an input the
343
+ * model changed afterwards, or under another deployed version, does not match. Same-looking calls never share a digest: one
344
+ * `{ id }` archived and deleted, or one call in two threads, digest differently.
345
+ *
346
+ * What the digest does NOT do, and the engine must: refuse an answer after `expiresAt`
347
+ * (the digest binds the expiry, it cannot read a clock), and accept each request once (the
348
+ * nonce makes requests distinct; single use is the record's conditional append, not a read
349
+ * before it). The answer must also arrive on a user-presence route the loop cannot call.
350
+ *
351
+ * Throws when a binding is malformed or the input holds a value `approvalEncoding` refuses.
352
+ */
353
+ export async function approvalDigest(binding) {
354
+ const b = approvalBinding.parse(binding);
355
+ const tuple = [APPROVAL_DIGEST_TAG, b.version, b.approver, b.thread, b.step, b.call, b.operation, b.expiresAt, b.nonce, b.input];
356
+ const bytes = new TextEncoder().encode(approvalEncoding(tuple));
357
+ const hash = await globalThis.crypto.subtle.digest('SHA-256', bytes);
358
+ return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
359
+ }
360
+ //# sourceMappingURL=assistant-stream.js.map