@tangle-network/agent-interface 0.40.0 → 0.41.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.
@@ -0,0 +1,741 @@
1
+ import { z } from "zod";
2
+ import { canonicalCandidateDigest, sha256DigestSchema, } from "./agent-candidate-schema-common.js";
3
+ import { AgentRunControlRefSchema, } from "./runtime-control.js";
4
+ const idSchema = z.string().min(1).max(512);
5
+ const jsonRecordSchema = z.record(z.string(), z.json());
6
+ function wireDigest(value) {
7
+ const serialized = JSON.stringify(value);
8
+ if (serialized === undefined) {
9
+ throw new Error("portable context material must be JSON serializable");
10
+ }
11
+ return canonicalCandidateDigest(JSON.parse(serialized));
12
+ }
13
+ const InputPartSchema = z.discriminatedUnion("type", [
14
+ z.strictObject({ type: z.literal("text"), text: z.string() }),
15
+ z.strictObject({
16
+ type: z.literal("file"),
17
+ filename: z.string().optional(),
18
+ mediaType: z.string().optional(),
19
+ url: z.string().optional(),
20
+ path: z.string().optional(),
21
+ content: z.string().optional(),
22
+ }),
23
+ z.strictObject({
24
+ type: z.literal("image"),
25
+ filename: z.string().optional(),
26
+ mediaType: z.string().optional(),
27
+ url: z.string().optional(),
28
+ path: z.string().optional(),
29
+ }),
30
+ ]);
31
+ const BackendMessageSchema = z.strictObject({
32
+ id: idSchema,
33
+ role: z.enum(["user", "assistant", "system"]),
34
+ parts: z.array(z.json()),
35
+ timestamp: z.iso.datetime(),
36
+ metadata: jsonRecordSchema.optional(),
37
+ });
38
+ export const PortableContextSourceBoundarySchema = z.strictObject({
39
+ runId: idSchema,
40
+ messageId: idSchema.optional(),
41
+ provider: idSchema.optional(),
42
+ environmentId: idSchema.optional(),
43
+ sessionId: idSchema.optional(),
44
+ });
45
+ export const PortableContextAttachmentRefSchema = z.strictObject({
46
+ messageId: idSchema,
47
+ partIndex: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
48
+ digest: sha256DigestSchema,
49
+ name: z.string().min(1).optional(),
50
+ mediaType: z.string().min(1).optional(),
51
+ });
52
+ export function portableConversationContextDigest(context) {
53
+ return wireDigest(context);
54
+ }
55
+ export const PortableConversationContextSchema = z
56
+ .strictObject({
57
+ source: PortableContextSourceBoundarySchema,
58
+ completeness: z.enum(["complete", "partial"]),
59
+ messages: z.array(BackendMessageSchema),
60
+ attachments: z.array(PortableContextAttachmentRefSchema),
61
+ digest: sha256DigestSchema,
62
+ })
63
+ .superRefine((context, refinement) => {
64
+ const messageIds = context.messages.map((message) => message.id);
65
+ if (new Set(messageIds).size !== messageIds.length) {
66
+ refinement.addIssue({
67
+ code: "custom",
68
+ path: ["messages"],
69
+ message: "portable context message ids must be unique",
70
+ });
71
+ }
72
+ for (const attachment of context.attachments) {
73
+ const message = context.messages.find((candidate) => candidate.id === attachment.messageId);
74
+ if (!message || attachment.partIndex >= message.parts.length) {
75
+ refinement.addIssue({
76
+ code: "custom",
77
+ path: ["attachments"],
78
+ message: "attachment must reference a part in the portable context",
79
+ });
80
+ }
81
+ }
82
+ if (context.digest !==
83
+ portableConversationContextDigest({
84
+ source: context.source,
85
+ completeness: context.completeness,
86
+ messages: context.messages,
87
+ attachments: context.attachments,
88
+ })) {
89
+ refinement.addIssue({
90
+ code: "custom",
91
+ path: ["digest"],
92
+ message: "portable context digest does not match its content",
93
+ });
94
+ }
95
+ });
96
+ export const PortableContextDestinationSchema = z.strictObject({
97
+ runner: idSchema,
98
+ provider: idSchema.optional(),
99
+ model: idSchema.optional(),
100
+ profileDigest: sha256DigestSchema.optional(),
101
+ });
102
+ export const PortableContextPartPlanSchema = z
103
+ .strictObject({
104
+ partIndex: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
105
+ action: z.enum(["include", "transform", "omit"]),
106
+ output: InputPartSchema.optional(),
107
+ reason: z.string().min(1).optional(),
108
+ })
109
+ .superRefine((part, refinement) => {
110
+ if (part.action === "transform" && part.output === undefined) {
111
+ refinement.addIssue({
112
+ code: "custom",
113
+ path: ["output"],
114
+ message: "a transformed part must include its portable output",
115
+ });
116
+ }
117
+ if (part.action !== "transform" && part.output !== undefined) {
118
+ refinement.addIssue({
119
+ code: "custom",
120
+ path: ["output"],
121
+ message: "only a transformed part may include portable output",
122
+ });
123
+ }
124
+ if (part.action !== "include" && part.reason === undefined) {
125
+ refinement.addIssue({
126
+ code: "custom",
127
+ path: ["reason"],
128
+ message: "a transformed or omitted part must include a reason",
129
+ });
130
+ }
131
+ });
132
+ export const PortableContextMessagePlanSchema = z
133
+ .strictObject({
134
+ messageId: idSchema,
135
+ action: z.enum(["include", "omit"]),
136
+ parts: z.array(PortableContextPartPlanSchema),
137
+ reason: z.string().min(1).optional(),
138
+ })
139
+ .superRefine((message, refinement) => {
140
+ const indexes = message.parts.map((part) => part.partIndex);
141
+ if (new Set(indexes).size !== indexes.length) {
142
+ refinement.addIssue({
143
+ code: "custom",
144
+ path: ["parts"],
145
+ message: "part indexes must be unique within a message plan",
146
+ });
147
+ }
148
+ if (message.action === "omit") {
149
+ if (message.reason === undefined) {
150
+ refinement.addIssue({
151
+ code: "custom",
152
+ path: ["reason"],
153
+ message: "an omitted message must include a reason",
154
+ });
155
+ }
156
+ if (message.parts.some((part) => part.action !== "omit")) {
157
+ refinement.addIssue({
158
+ code: "custom",
159
+ path: ["parts"],
160
+ message: "every part of an omitted message must be omitted",
161
+ });
162
+ }
163
+ }
164
+ });
165
+ export function portableContextPlanDigest(plan) {
166
+ return wireDigest(plan);
167
+ }
168
+ export const PortableContextPlanSchema = z
169
+ .strictObject({
170
+ planId: idSchema,
171
+ source: PortableConversationContextSchema,
172
+ destination: PortableContextDestinationSchema,
173
+ messages: z.array(PortableContextMessagePlanSchema),
174
+ context: PortableConversationContextSchema,
175
+ estimatedTokens: z.number().int().nonnegative().optional(),
176
+ requiresAcceptance: z.boolean(),
177
+ digest: sha256DigestSchema,
178
+ })
179
+ .superRefine((plan, refinement) => {
180
+ const messageIds = plan.messages.map((message) => message.messageId);
181
+ if (new Set(messageIds).size !== messageIds.length) {
182
+ refinement.addIssue({
183
+ code: "custom",
184
+ path: ["messages"],
185
+ message: "message ids must be unique within a context plan",
186
+ });
187
+ }
188
+ const sourceMessageIds = plan.source.messages.map((message) => message.id);
189
+ if (!sameStringSet(messageIds, sourceMessageIds)) {
190
+ refinement.addIssue({
191
+ code: "custom",
192
+ path: ["messages"],
193
+ message: "a context plan must decide every source message exactly once",
194
+ });
195
+ }
196
+ if (wireDigest(plan.context.source) !== wireDigest(plan.source.source)) {
197
+ refinement.addIssue({
198
+ code: "custom",
199
+ path: ["context", "source"],
200
+ message: "output context source must match the immutable source",
201
+ });
202
+ }
203
+ const expectedMessages = [];
204
+ for (const sourceMessage of plan.source.messages) {
205
+ const messagePlan = plan.messages.find((candidate) => candidate.messageId === sourceMessage.id);
206
+ if (!messagePlan)
207
+ continue;
208
+ const expectedIndexes = sourceMessage.parts.map((_, index) => index);
209
+ const decidedIndexes = messagePlan.parts
210
+ .map((part) => part.partIndex)
211
+ .sort((left, right) => left - right);
212
+ if (JSON.stringify(expectedIndexes) !== JSON.stringify(decidedIndexes)) {
213
+ refinement.addIssue({
214
+ code: "custom",
215
+ path: ["messages"],
216
+ message: `message "${sourceMessage.id}" must decide every source part exactly once`,
217
+ });
218
+ continue;
219
+ }
220
+ if (messagePlan.action === "omit")
221
+ continue;
222
+ const outputParts = [];
223
+ for (const partPlan of [...messagePlan.parts].sort((left, right) => left.partIndex - right.partIndex)) {
224
+ if (partPlan.action === "omit")
225
+ continue;
226
+ if (partPlan.action === "transform") {
227
+ if (partPlan.output)
228
+ outputParts.push(partPlan.output);
229
+ continue;
230
+ }
231
+ const parsedPart = InputPartSchema.safeParse(sourceMessage.parts[partPlan.partIndex]);
232
+ if (!parsedPart.success) {
233
+ refinement.addIssue({
234
+ code: "custom",
235
+ path: ["messages"],
236
+ message: `message "${sourceMessage.id}" part ${partPlan.partIndex} must be transformed or omitted`,
237
+ });
238
+ continue;
239
+ }
240
+ outputParts.push(parsedPart.data);
241
+ }
242
+ if (outputParts.length === 0) {
243
+ refinement.addIssue({
244
+ code: "custom",
245
+ path: ["messages"],
246
+ message: `included message "${sourceMessage.id}" must retain at least one part`,
247
+ });
248
+ }
249
+ expectedMessages.push({ ...sourceMessage, parts: outputParts });
250
+ }
251
+ if (wireDigest(expectedMessages) !== wireDigest(plan.context.messages)) {
252
+ refinement.addIssue({
253
+ code: "custom",
254
+ path: ["context", "messages"],
255
+ message: "output messages do not exactly match the source decisions",
256
+ });
257
+ }
258
+ const expectedAttachments = plan.source.attachments.filter((attachment) => {
259
+ const message = plan.messages.find((candidate) => candidate.messageId === attachment.messageId);
260
+ const part = message?.parts.find((candidate) => candidate.partIndex === attachment.partIndex);
261
+ return message?.action === "include" && part?.action === "include";
262
+ });
263
+ if (wireDigest(expectedAttachments) !== wireDigest(plan.context.attachments)) {
264
+ refinement.addIssue({
265
+ code: "custom",
266
+ path: ["context", "attachments"],
267
+ message: "output attachments do not exactly match included source parts",
268
+ });
269
+ }
270
+ const changesContent = plan.messages.some((message) => message.action === "omit" ||
271
+ message.parts.some((part) => part.action !== "include"));
272
+ const requiresAcceptance = changesContent ||
273
+ plan.source.completeness === "partial" ||
274
+ plan.context.completeness === "partial";
275
+ if (plan.requiresAcceptance !== requiresAcceptance) {
276
+ refinement.addIssue({
277
+ code: "custom",
278
+ path: ["requiresAcceptance"],
279
+ message: "requiresAcceptance must be true when content changes or either context is partial",
280
+ });
281
+ }
282
+ const expectedCompleteness = changesContent || plan.source.completeness === "partial"
283
+ ? "partial"
284
+ : "complete";
285
+ if (plan.context.completeness !== expectedCompleteness) {
286
+ refinement.addIssue({
287
+ code: "custom",
288
+ path: ["context", "completeness"],
289
+ message: `output context completeness must be ${expectedCompleteness}`,
290
+ });
291
+ }
292
+ if (plan.digest !==
293
+ portableContextPlanDigest({
294
+ planId: plan.planId,
295
+ source: plan.source,
296
+ destination: plan.destination,
297
+ messages: plan.messages,
298
+ context: plan.context,
299
+ estimatedTokens: plan.estimatedTokens,
300
+ requiresAcceptance: plan.requiresAcceptance,
301
+ })) {
302
+ refinement.addIssue({
303
+ code: "custom",
304
+ path: ["digest"],
305
+ message: "portable context plan digest does not match its content",
306
+ });
307
+ }
308
+ });
309
+ export function portableContextPlanRequestDigest(request) {
310
+ return wireDigest(request);
311
+ }
312
+ export const PortableContextPlanRequestSchema = z
313
+ .strictObject({
314
+ requestId: idSchema,
315
+ requestDigest: sha256DigestSchema,
316
+ source: PortableConversationContextSchema,
317
+ destination: PortableContextDestinationSchema,
318
+ maxInputTokens: z.number().int().positive().optional(),
319
+ })
320
+ .superRefine((request, refinement) => {
321
+ if (request.requestDigest !==
322
+ portableContextPlanRequestDigest({
323
+ source: request.source,
324
+ destination: request.destination,
325
+ maxInputTokens: request.maxInputTokens,
326
+ })) {
327
+ refinement.addIssue({
328
+ code: "custom",
329
+ path: ["requestDigest"],
330
+ message: "portable context plan request digest does not match its content",
331
+ });
332
+ }
333
+ });
334
+ const portableContextPlanResultBinding = {
335
+ requestId: idSchema,
336
+ requestDigest: sha256DigestSchema,
337
+ };
338
+ export const PortableContextPlanResultSchema = z.discriminatedUnion("status", [
339
+ z.strictObject({
340
+ status: z.literal("ready"),
341
+ ...portableContextPlanResultBinding,
342
+ plan: PortableContextPlanSchema,
343
+ }),
344
+ z.strictObject({
345
+ status: z.literal("over_limit"),
346
+ ...portableContextPlanResultBinding,
347
+ estimatedTokens: z.number().int().nonnegative().optional(),
348
+ maxInputTokens: z.number().int().positive(),
349
+ suggestedBoundaryMessageId: idSchema.optional(),
350
+ message: z.string().min(1),
351
+ }),
352
+ z.strictObject({
353
+ status: z.literal("unsupported"),
354
+ ...portableContextPlanResultBinding,
355
+ message: z.string().min(1),
356
+ }),
357
+ ]);
358
+ /** Cross-check a planner result against the exact request and token budget. */
359
+ export function portableContextPlanResultMatchesRequest(request, result) {
360
+ const parsedRequest = PortableContextPlanRequestSchema.safeParse(request);
361
+ const parsedResult = PortableContextPlanResultSchema.safeParse(result);
362
+ if (!parsedRequest.success || !parsedResult.success)
363
+ return false;
364
+ const exactRequest = parsedRequest.data;
365
+ const exactResult = parsedResult.data;
366
+ if (exactResult.requestId !== exactRequest.requestId ||
367
+ exactResult.requestDigest !== exactRequest.requestDigest) {
368
+ return false;
369
+ }
370
+ if (exactResult.status === "ready") {
371
+ if (wireDigest(exactResult.plan.source) !== wireDigest(exactRequest.source) ||
372
+ wireDigest(exactResult.plan.destination) !==
373
+ wireDigest(exactRequest.destination)) {
374
+ return false;
375
+ }
376
+ if (exactRequest.maxInputTokens === undefined)
377
+ return true;
378
+ return (exactResult.plan.estimatedTokens !== undefined &&
379
+ exactResult.plan.estimatedTokens <= exactRequest.maxInputTokens);
380
+ }
381
+ if (exactResult.status === "over_limit") {
382
+ return (exactRequest.maxInputTokens !== undefined &&
383
+ exactResult.maxInputTokens === exactRequest.maxInputTokens &&
384
+ (exactResult.estimatedTokens === undefined ||
385
+ exactResult.estimatedTokens > exactRequest.maxInputTokens) &&
386
+ (exactResult.suggestedBoundaryMessageId === undefined ||
387
+ exactRequest.source.messages.some((message) => message.id === exactResult.suggestedBoundaryMessageId)));
388
+ }
389
+ return true;
390
+ }
391
+ export const ContextPlanAcceptanceSchema = z.strictObject({
392
+ planDigest: sha256DigestSchema,
393
+ acceptedAt: z.iso.datetime(),
394
+ acceptedBy: z.enum(["user", "policy", "system"]),
395
+ });
396
+ export function contextTransferRequestDigest(request) {
397
+ return wireDigest({
398
+ planDigest: request.plan.digest,
399
+ acceptance: request.acceptance,
400
+ });
401
+ }
402
+ export const ContextTransferRequestSchema = z
403
+ .strictObject({
404
+ operationId: idSchema,
405
+ requestDigest: sha256DigestSchema,
406
+ plan: PortableContextPlanSchema,
407
+ acceptance: ContextPlanAcceptanceSchema,
408
+ })
409
+ .superRefine((request, refinement) => {
410
+ if (request.acceptance.planDigest !== request.plan.digest) {
411
+ refinement.addIssue({
412
+ code: "custom",
413
+ path: ["acceptance", "planDigest"],
414
+ message: "accepted plan digest must match the executed plan",
415
+ });
416
+ }
417
+ if (request.requestDigest !== contextTransferRequestDigest(request)) {
418
+ refinement.addIssue({
419
+ code: "custom",
420
+ path: ["requestDigest"],
421
+ message: "context transfer request digest does not match its content",
422
+ });
423
+ }
424
+ if (request.plan.requiresAcceptance &&
425
+ request.acceptance.acceptedBy === "system") {
426
+ refinement.addIssue({
427
+ code: "custom",
428
+ path: ["acceptance", "acceptedBy"],
429
+ message: "a transformed or partial plan requires user or policy acceptance",
430
+ });
431
+ }
432
+ });
433
+ function contextTransferReceiptVariant(status) {
434
+ return z
435
+ .strictObject({
436
+ status: z.literal(status),
437
+ operationId: idSchema,
438
+ requestDigest: sha256DigestSchema,
439
+ planDigest: sha256DigestSchema,
440
+ contextDigest: sha256DigestSchema,
441
+ destination: PortableContextDestinationSchema,
442
+ provider: idSchema,
443
+ environmentId: idSchema,
444
+ sessionId: idSchema,
445
+ /** Provider assertion that this session was created for this transfer. */
446
+ sessionCreatedForOperationId: idSchema,
447
+ sessionCreatedAt: z.iso.datetime(),
448
+ transferredMessageIds: z.array(idSchema),
449
+ omittedMessageIds: z.array(idSchema),
450
+ admittedAt: z.iso.datetime(),
451
+ })
452
+ .superRefine((receipt, refinement) => {
453
+ const transferred = new Set(receipt.transferredMessageIds);
454
+ const omitted = new Set(receipt.omittedMessageIds);
455
+ if (transferred.size !== receipt.transferredMessageIds.length) {
456
+ refinement.addIssue({
457
+ code: "custom",
458
+ path: ["transferredMessageIds"],
459
+ message: "transferred message ids must be unique",
460
+ });
461
+ }
462
+ if (omitted.size !== receipt.omittedMessageIds.length) {
463
+ refinement.addIssue({
464
+ code: "custom",
465
+ path: ["omittedMessageIds"],
466
+ message: "omitted message ids must be unique",
467
+ });
468
+ }
469
+ if ([...transferred].some((messageId) => omitted.has(messageId))) {
470
+ refinement.addIssue({
471
+ code: "custom",
472
+ path: ["omittedMessageIds"],
473
+ message: "a message cannot be both transferred and omitted",
474
+ });
475
+ }
476
+ if (Date.parse(receipt.sessionCreatedAt) > Date.parse(receipt.admittedAt)) {
477
+ refinement.addIssue({
478
+ code: "custom",
479
+ path: ["sessionCreatedAt"],
480
+ message: "the destination session must exist before context admission",
481
+ });
482
+ }
483
+ });
484
+ }
485
+ export const ContextTransferReceiptSchema = z.discriminatedUnion("status", [
486
+ contextTransferReceiptVariant("accepted"),
487
+ contextTransferReceiptVariant("replayed"),
488
+ ]);
489
+ export const ContextTransferResultSchema = z.discriminatedUnion("status", [
490
+ contextTransferReceiptVariant("accepted"),
491
+ contextTransferReceiptVariant("replayed"),
492
+ z.strictObject({
493
+ status: z.literal("conflict"),
494
+ operationId: idSchema,
495
+ requestDigest: sha256DigestSchema,
496
+ existingRequestDigest: sha256DigestSchema,
497
+ }),
498
+ z.strictObject({
499
+ status: z.enum(["unknown", "transport_failure"]),
500
+ operationId: idSchema,
501
+ requestDigest: sha256DigestSchema,
502
+ message: z.string().min(1),
503
+ retryable: z.boolean(),
504
+ }),
505
+ ]).superRefine((result, refinement) => {
506
+ if (result.status === "conflict" &&
507
+ result.existingRequestDigest === result.requestDigest) {
508
+ refinement.addIssue({
509
+ code: "custom",
510
+ path: ["existingRequestDigest"],
511
+ message: "a transfer conflict must identify a different request",
512
+ });
513
+ }
514
+ });
515
+ /** Bind every transfer outcome to the exact operation before acting on it. */
516
+ export function contextTransferResultMatchesRequest(request, result) {
517
+ const parsedRequest = ContextTransferRequestSchema.safeParse(request);
518
+ const parsedResult = ContextTransferResultSchema.safeParse(result);
519
+ if (!parsedRequest.success || !parsedResult.success)
520
+ return false;
521
+ const exactRequest = parsedRequest.data;
522
+ const exactResult = parsedResult.data;
523
+ if (exactResult.operationId !== exactRequest.operationId ||
524
+ exactResult.requestDigest !== exactRequest.requestDigest) {
525
+ return false;
526
+ }
527
+ if (exactResult.status === "accepted" ||
528
+ exactResult.status === "replayed") {
529
+ return contextTransferReceiptMatches(exactRequest, exactResult);
530
+ }
531
+ return true;
532
+ }
533
+ export const NativeContextBoundarySchema = z
534
+ .discriminatedUnion("kind", [
535
+ z.strictObject({ kind: z.literal("token"), token: idSchema }),
536
+ z.strictObject({ kind: z.literal("revision"), revision: idSchema }),
537
+ z.strictObject({ kind: z.literal("digest"), digest: sha256DigestSchema }),
538
+ z.strictObject({
539
+ kind: z.literal("messages"),
540
+ messageIds: z.array(idSchema),
541
+ digest: sha256DigestSchema,
542
+ }),
543
+ ])
544
+ .superRefine((boundary, refinement) => {
545
+ if (boundary.kind === "messages" &&
546
+ new Set(boundary.messageIds).size !== boundary.messageIds.length) {
547
+ refinement.addIssue({
548
+ code: "custom",
549
+ path: ["messageIds"],
550
+ message: "native context boundary message ids must be unique",
551
+ });
552
+ }
553
+ });
554
+ export const NativeContextBoundaryProofSchema = z.strictObject({
555
+ runId: idSchema,
556
+ provider: idSchema,
557
+ environmentId: idSchema,
558
+ sessionId: idSchema,
559
+ boundary: NativeContextBoundarySchema,
560
+ observedAt: z.iso.datetime(),
561
+ });
562
+ export function nativeContextContinuationRequestDigest(request) {
563
+ return wireDigest({
564
+ run: request.run,
565
+ expectedBoundary: request.expectedBoundary,
566
+ });
567
+ }
568
+ export const NativeContextContinuationRequestSchema = z
569
+ .strictObject({
570
+ operationId: idSchema,
571
+ requestDigest: sha256DigestSchema,
572
+ run: AgentRunControlRefSchema,
573
+ expectedBoundary: NativeContextBoundaryProofSchema,
574
+ })
575
+ .superRefine((request, refinement) => {
576
+ const proof = request.expectedBoundary;
577
+ if (request.run.runId !== proof.runId ||
578
+ request.run.provider !== proof.provider ||
579
+ request.run.environmentId !== proof.environmentId ||
580
+ request.run.sessionId !== proof.sessionId) {
581
+ refinement.addIssue({
582
+ code: "custom",
583
+ path: ["expectedBoundary"],
584
+ message: "boundary proof must match the continued run",
585
+ });
586
+ }
587
+ if (request.requestDigest !== nativeContextContinuationRequestDigest(request)) {
588
+ refinement.addIssue({
589
+ code: "custom",
590
+ path: ["requestDigest"],
591
+ message: "native continuation request digest does not match its content",
592
+ });
593
+ }
594
+ });
595
+ export const NativeContextContinuationAcknowledgementSchema = z
596
+ .strictObject({
597
+ operationId: idSchema,
598
+ requestDigest: sha256DigestSchema,
599
+ status: z.enum([
600
+ "accepted",
601
+ "replayed",
602
+ "conflict",
603
+ "boundary_mismatch",
604
+ "unverified",
605
+ "unknown_session",
606
+ "transport_failure",
607
+ ]),
608
+ historyMessagesSent: z.number().int().nonnegative(),
609
+ existingRequestDigest: sha256DigestSchema.optional(),
610
+ actualBoundary: NativeContextBoundaryProofSchema.optional(),
611
+ message: z.string().min(1).optional(),
612
+ retryable: z.boolean().optional(),
613
+ })
614
+ .superRefine((acknowledgement, refinement) => {
615
+ if (acknowledgement.historyMessagesSent !== 0) {
616
+ refinement.addIssue({
617
+ code: "custom",
618
+ path: ["historyMessagesSent"],
619
+ message: "native continuation must never resend portable history",
620
+ });
621
+ }
622
+ if (acknowledgement.status === "conflict" &&
623
+ acknowledgement.existingRequestDigest === undefined) {
624
+ refinement.addIssue({
625
+ code: "custom",
626
+ path: ["existingRequestDigest"],
627
+ message: "a continuation conflict must include the existing digest",
628
+ });
629
+ }
630
+ if (acknowledgement.status === "conflict" &&
631
+ acknowledgement.existingRequestDigest === acknowledgement.requestDigest) {
632
+ refinement.addIssue({
633
+ code: "custom",
634
+ path: ["existingRequestDigest"],
635
+ message: "a continuation conflict must identify a different request",
636
+ });
637
+ }
638
+ if (acknowledgement.status !== "conflict" &&
639
+ acknowledgement.existingRequestDigest !== undefined) {
640
+ refinement.addIssue({
641
+ code: "custom",
642
+ path: ["existingRequestDigest"],
643
+ message: "only a continuation conflict may include an existing digest",
644
+ });
645
+ }
646
+ if (acknowledgement.status === "transport_failure" &&
647
+ acknowledgement.message === undefined) {
648
+ refinement.addIssue({
649
+ code: "custom",
650
+ path: ["message"],
651
+ message: "a continuation transport failure must include a message",
652
+ });
653
+ }
654
+ if (acknowledgement.status === "transport_failure" &&
655
+ acknowledgement.retryable === undefined) {
656
+ refinement.addIssue({
657
+ code: "custom",
658
+ path: ["retryable"],
659
+ message: "a continuation transport failure must state whether retry is safe",
660
+ });
661
+ }
662
+ if (["accepted", "replayed", "boundary_mismatch"].includes(acknowledgement.status) &&
663
+ acknowledgement.actualBoundary === undefined) {
664
+ refinement.addIssue({
665
+ code: "custom",
666
+ path: ["actualBoundary"],
667
+ message: `${acknowledgement.status} must include the observed boundary`,
668
+ });
669
+ }
670
+ });
671
+ /** Exact cross-check required before committing a transfer receipt. */
672
+ export function contextTransferReceiptMatches(request, receipt) {
673
+ const parsedRequest = ContextTransferRequestSchema.safeParse(request);
674
+ const parsedReceipt = ContextTransferReceiptSchema.safeParse(receipt);
675
+ if (!parsedRequest.success || !parsedReceipt.success)
676
+ return false;
677
+ const exactRequest = parsedRequest.data;
678
+ const exactReceipt = parsedReceipt.data;
679
+ const included = exactRequest.plan.messages
680
+ .filter((message) => message.action === "include")
681
+ .map((message) => message.messageId)
682
+ .sort();
683
+ const omitted = exactRequest.plan.messages
684
+ .filter((message) => message.action === "omit")
685
+ .map((message) => message.messageId)
686
+ .sort();
687
+ const source = exactRequest.plan.context.source;
688
+ const repeatsSourceSession = source.sessionId !== undefined &&
689
+ exactReceipt.sessionId === source.sessionId &&
690
+ (source.provider === undefined || exactReceipt.provider === source.provider) &&
691
+ (source.environmentId === undefined ||
692
+ exactReceipt.environmentId === source.environmentId);
693
+ return (exactReceipt.operationId === exactRequest.operationId &&
694
+ exactReceipt.requestDigest === exactRequest.requestDigest &&
695
+ exactReceipt.planDigest === exactRequest.plan.digest &&
696
+ exactReceipt.contextDigest === exactRequest.plan.context.digest &&
697
+ wireDigest(exactReceipt.destination) ===
698
+ wireDigest(exactRequest.plan.destination) &&
699
+ (exactRequest.plan.destination.provider === undefined ||
700
+ exactReceipt.provider === exactRequest.plan.destination.provider) &&
701
+ exactReceipt.sessionCreatedForOperationId === exactRequest.operationId &&
702
+ !repeatsSourceSession &&
703
+ JSON.stringify([...exactReceipt.transferredMessageIds].sort()) ===
704
+ JSON.stringify(included) &&
705
+ JSON.stringify([...exactReceipt.omittedMessageIds].sort()) ===
706
+ JSON.stringify(omitted));
707
+ }
708
+ /** Exact cross-check required before accepting a native continuation result. */
709
+ export function nativeContextContinuationAcknowledgementMatches(request, acknowledgement) {
710
+ const parsedRequest = NativeContextContinuationRequestSchema.safeParse(request);
711
+ const parsedAcknowledgement = NativeContextContinuationAcknowledgementSchema.safeParse(acknowledgement);
712
+ if (!parsedRequest.success || !parsedAcknowledgement.success)
713
+ return false;
714
+ const exactRequest = parsedRequest.data;
715
+ const exactAcknowledgement = parsedAcknowledgement.data;
716
+ if (exactAcknowledgement.operationId !== exactRequest.operationId ||
717
+ exactAcknowledgement.requestDigest !== exactRequest.requestDigest ||
718
+ (exactAcknowledgement.status !== "accepted" &&
719
+ exactAcknowledgement.status !== "replayed")) {
720
+ return false;
721
+ }
722
+ const actual = exactAcknowledgement.actualBoundary;
723
+ if (actual !== undefined && !boundaryProofMatchesRun(exactRequest.run, actual)) {
724
+ return false;
725
+ }
726
+ return (actual !== undefined &&
727
+ Date.parse(actual.observedAt) >=
728
+ Date.parse(exactRequest.expectedBoundary.observedAt) &&
729
+ wireDigest(actual.boundary) ===
730
+ wireDigest(exactRequest.expectedBoundary.boundary));
731
+ }
732
+ function boundaryProofMatchesRun(run, proof) {
733
+ return (proof.runId === run.runId &&
734
+ proof.provider === run.provider &&
735
+ proof.environmentId === run.environmentId &&
736
+ proof.sessionId === run.sessionId);
737
+ }
738
+ function sameStringSet(left, right) {
739
+ return (left.length === right.length &&
740
+ JSON.stringify([...left].sort()) === JSON.stringify([...right].sort()));
741
+ }