@tangle-network/agent-interface 4.0.0 → 5.1.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.
@@ -198,6 +198,8 @@ export const AgentEnvironmentCapabilitiesSchema = z
198
198
  freshSession: z.boolean(),
199
199
  requestIdempotency: z.boolean(),
200
200
  lookup: z.boolean(),
201
+ attach: z.boolean().optional(),
202
+ workspaceSnapshot: z.boolean().optional(),
201
203
  })
202
204
  .optional(),
203
205
  interactions: InteractionCapabilitiesSchema.optional(),
@@ -245,6 +247,7 @@ export const AgentEnvironmentCapabilitiesSchema = z
245
247
  billingOwner: z.boolean().optional(),
246
248
  runtimeAttachments: z.strictObject({ mcp: z.literal(true) }).optional(),
247
249
  workspaceCheckpoint: z.boolean().optional(),
250
+ workspaceSnapshot: z.boolean().optional(),
248
251
  })
249
252
  .optional(),
250
253
  observation: z
@@ -316,14 +319,16 @@ export const AgentEnvironmentCapabilitiesSchema = z
316
319
  message: "native continuation requires retained control for early admission",
317
320
  });
318
321
  }
319
- if (capabilities.contextTransfer !== undefined &&
320
- (!capabilities.contextTransfer.freshSession ||
321
- !capabilities.contextTransfer.requestIdempotency ||
322
- !capabilities.contextTransfer.lookup)) {
322
+ const contextTransfer = capabilities.contextTransfer;
323
+ if (contextTransfer !== undefined &&
324
+ (!contextTransfer.lookup ||
325
+ (contextTransfer.freshSession && !contextTransfer.requestIdempotency) ||
326
+ ((contextTransfer.attach === true || contextTransfer.workspaceSnapshot === true) &&
327
+ (!contextTransfer.freshSession || !contextTransfer.requestIdempotency)))) {
323
328
  refinement.addIssue({
324
329
  code: "custom",
325
330
  path: ["contextTransfer"],
326
- message: "context transfer requires fresh-session admission, request idempotency, and lookup together",
331
+ message: "context transfer requires durable lookup; fresh admission, attachment, and snapshot transfer require fresh-session admission and request idempotency together",
327
332
  });
328
333
  }
329
334
  const durableBranching = [
@@ -355,7 +360,8 @@ export const AgentEnvironmentCapabilitiesSchema = z
355
360
  create.egress === undefined &&
356
361
  create.billingOwner === undefined &&
357
362
  create.runtimeAttachments === undefined &&
358
- create.workspaceCheckpoint === undefined) {
363
+ create.workspaceCheckpoint === undefined &&
364
+ create.workspaceSnapshot === undefined) {
359
365
  refinement.addIssue({
360
366
  code: "custom",
361
367
  path: ["create"],
@@ -517,3 +523,7 @@ export async function createAgentEnvironmentWithIdempotency(records, input, crea
517
523
  throw error;
518
524
  }
519
525
  }
526
+ export const AgentEnvironmentGetOptionsSchema = z.strictObject({
527
+ signal: z.custom().optional(),
528
+ contextTransfer: ContextTransferRequestSchema.optional(),
529
+ });
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./plan.js";
14
14
  export * from "./runtime-control.js";
15
15
  export * from "./portable-context.js";
16
16
  export * from "./workspace-branching.js";
17
+ export * from "./workspace-snapshot.js";
17
18
  export * from "./interaction.js";
18
19
  export * from "./agent-candidate.js";
19
20
  export * from "./agent-candidate-schema.js";
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * from "./plan.js";
13
13
  export * from "./runtime-control.js";
14
14
  export * from "./portable-context.js";
15
15
  export * from "./workspace-branching.js";
16
+ export * from "./workspace-snapshot.js";
16
17
  export * from "./interaction.js";
17
18
  export * from "./agent-candidate.js";
18
19
  export * from "./agent-candidate-schema.js";
@@ -0,0 +1,47 @@
1
+ import type { BackendMessage } from "./backend-message.js";
2
+ import type { PortableContextPlanMaterial } from "./portable-context-plan.js";
3
+ export declare function portableContextMessageMetadataDigest(message: BackendMessage): `sha256:${string}`;
4
+ /** Bind the full local source and decisions without sending omitted bytes or explanation text. */
5
+ export declare function portableContextPlanCommitmentMaterial(plan: PortableContextPlanMaterial): {
6
+ kind: "portable-context-transfer-plan";
7
+ planId: string;
8
+ reviewDigest: `sha256:${string}`;
9
+ source: {
10
+ source: import("./portable-context-base.js").PortableContextSourceBoundary;
11
+ digest: `sha256:${string}`;
12
+ completeness: "complete" | "partial";
13
+ messages: {
14
+ messageId: string;
15
+ metadataDigest?: `sha256:${string}` | undefined;
16
+ parts: (`sha256:${string}` | null)[];
17
+ }[];
18
+ attachments: {
19
+ messageId: string;
20
+ partIndex: number;
21
+ digest: `sha256:${string}`;
22
+ }[];
23
+ };
24
+ destination: {
25
+ runner: string;
26
+ provider: string;
27
+ environmentId?: string | undefined;
28
+ sessionId: string;
29
+ runId: string;
30
+ executionId: string;
31
+ model?: string | undefined;
32
+ profileDigest: `sha256:${string}`;
33
+ workspaceSnapshotDigest?: `sha256:${string}` | undefined;
34
+ };
35
+ messages: {
36
+ messageId: string;
37
+ action: "include" | "omit";
38
+ parts: {
39
+ partIndex: number;
40
+ action: "include" | "omit" | "transform";
41
+ outputDigest?: `sha256:${string}` | undefined;
42
+ }[];
43
+ }[];
44
+ context: import("./portable-context-base.js").PortableConversationContext;
45
+ estimatedTokens?: number | undefined;
46
+ requiresAcceptance: boolean;
47
+ };
@@ -0,0 +1,48 @@
1
+ import { wireDigest } from "./portable-context-shared.js";
2
+ export function portableContextMessageMetadataDigest(message) {
3
+ const { parts: _parts, ...metadata } = message;
4
+ return wireDigest(metadata);
5
+ }
6
+ /** Bind the full local source and decisions without sending omitted bytes or explanation text. */
7
+ export function portableContextPlanCommitmentMaterial(plan) {
8
+ return {
9
+ kind: "portable-context-transfer-plan",
10
+ planId: plan.planId,
11
+ reviewDigest: wireDigest({ source: plan.source, messages: plan.messages }),
12
+ source: {
13
+ source: plan.source.source,
14
+ digest: plan.source.digest,
15
+ completeness: plan.source.completeness,
16
+ messages: plan.source.messages.map((message) => {
17
+ const decision = plan.messages.find((item) => item.messageId === message.id);
18
+ return {
19
+ messageId: message.id,
20
+ ...(decision?.action === "include" ? { metadataDigest: portableContextMessageMetadataDigest(message) } : {}),
21
+ parts: message.parts.map((part, partIndex) => decision?.action === "include" &&
22
+ decision.parts.find((item) => item.partIndex === partIndex)?.action === "include" ? wireDigest(part) : null),
23
+ };
24
+ }),
25
+ attachments: plan.source.attachments.filter((attachment) => {
26
+ const decision = plan.messages.find((item) => item.messageId === attachment.messageId);
27
+ return decision?.action === "include" && decision.parts.find((part) => part.partIndex === attachment.partIndex)?.action === "include";
28
+ }).map((attachment) => ({
29
+ messageId: attachment.messageId,
30
+ partIndex: attachment.partIndex,
31
+ digest: wireDigest(attachment),
32
+ })),
33
+ },
34
+ destination: plan.destination,
35
+ messages: plan.messages.map((message) => ({
36
+ messageId: message.messageId,
37
+ action: message.action,
38
+ parts: message.parts.map((part) => ({
39
+ partIndex: part.partIndex,
40
+ action: part.action,
41
+ ...(part.output === undefined ? {} : { outputDigest: wireDigest(part.output) }),
42
+ })),
43
+ })),
44
+ context: plan.context,
45
+ ...(plan.estimatedTokens === undefined ? {} : { estimatedTokens: plan.estimatedTokens }),
46
+ requiresAcceptance: plan.requiresAcceptance,
47
+ };
48
+ }
@@ -60,6 +60,7 @@ export declare const PortableContextPlanRequestSchema: z.ZodObject<{
60
60
  executionId: z.ZodString;
61
61
  model: z.ZodOptional<z.ZodString>;
62
62
  profileDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
63
+ workspaceSnapshotDigest: z.ZodOptional<z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>>;
63
64
  }, z.core.$strict>;
64
65
  maxInputTokens: z.ZodOptional<z.ZodNumber>;
65
66
  }, z.core.$strict>;
@@ -0,0 +1,103 @@
1
+ import { z } from "zod";
2
+ import { type PortableContextPlan } from "./portable-context-plan.js";
3
+ /** Destination wire plan. The complete source and human explanations remain at the caller. */
4
+ export declare const PortableContextTransferPlanSchema: z.ZodObject<{
5
+ kind: z.ZodLiteral<"portable-context-transfer-plan">;
6
+ planId: z.ZodString;
7
+ reviewDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
8
+ source: z.ZodObject<{
9
+ source: z.ZodObject<{
10
+ runId: z.ZodString;
11
+ messageId: z.ZodString;
12
+ provider: z.ZodString;
13
+ environmentId: z.ZodString;
14
+ sessionId: z.ZodString;
15
+ executionId: z.ZodString;
16
+ requestDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
17
+ branchId: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strict>;
19
+ digest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
20
+ completeness: z.ZodEnum<{
21
+ complete: "complete";
22
+ partial: "partial";
23
+ }>;
24
+ messages: z.ZodArray<z.ZodObject<{
25
+ messageId: z.ZodString;
26
+ metadataDigest: z.ZodOptional<z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>>;
27
+ parts: z.ZodArray<z.ZodNullable<z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>>>;
28
+ }, z.core.$strict>>;
29
+ attachments: z.ZodArray<z.ZodObject<{
30
+ messageId: z.ZodString;
31
+ partIndex: z.ZodNumber;
32
+ digest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
33
+ }, z.core.$strict>>;
34
+ }, z.core.$strict>;
35
+ destination: z.ZodObject<{
36
+ runner: z.ZodString;
37
+ provider: z.ZodString;
38
+ environmentId: z.ZodOptional<z.ZodString>;
39
+ sessionId: z.ZodString;
40
+ runId: z.ZodString;
41
+ executionId: z.ZodString;
42
+ model: z.ZodOptional<z.ZodString>;
43
+ profileDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
44
+ workspaceSnapshotDigest: z.ZodOptional<z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>>;
45
+ }, z.core.$strict>;
46
+ messages: z.ZodArray<z.ZodObject<{
47
+ messageId: z.ZodString;
48
+ action: z.ZodEnum<{
49
+ include: "include";
50
+ omit: "omit";
51
+ }>;
52
+ parts: z.ZodArray<z.ZodObject<{
53
+ partIndex: z.ZodNumber;
54
+ action: z.ZodEnum<{
55
+ include: "include";
56
+ omit: "omit";
57
+ transform: "transform";
58
+ }>;
59
+ outputDigest: z.ZodOptional<z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>>;
60
+ }, z.core.$strict>>;
61
+ }, z.core.$strict>>;
62
+ context: z.ZodObject<{
63
+ source: z.ZodObject<{
64
+ runId: z.ZodString;
65
+ messageId: z.ZodString;
66
+ provider: z.ZodString;
67
+ environmentId: z.ZodString;
68
+ sessionId: z.ZodString;
69
+ executionId: z.ZodString;
70
+ requestDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
71
+ branchId: z.ZodOptional<z.ZodString>;
72
+ }, z.core.$strict>;
73
+ completeness: z.ZodEnum<{
74
+ complete: "complete";
75
+ partial: "partial";
76
+ }>;
77
+ messages: z.ZodArray<z.ZodObject<{
78
+ id: z.ZodString;
79
+ role: z.ZodEnum<{
80
+ assistant: "assistant";
81
+ system: "system";
82
+ user: "user";
83
+ }>;
84
+ parts: z.ZodArray<z.ZodCustom<unknown, unknown>>;
85
+ timestamp: z.ZodISODateTime;
86
+ metadata: z.ZodOptional<z.ZodCustom<Record<string, unknown>, Record<string, unknown>>>;
87
+ }, z.core.$strict>>;
88
+ attachments: z.ZodArray<z.ZodObject<{
89
+ messageId: z.ZodString;
90
+ partIndex: z.ZodNumber;
91
+ digest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
92
+ name: z.ZodOptional<z.ZodString>;
93
+ mediaType: z.ZodOptional<z.ZodString>;
94
+ }, z.core.$strict>>;
95
+ digest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
96
+ }, z.core.$strict>;
97
+ estimatedTokens: z.ZodOptional<z.ZodNumber>;
98
+ requiresAcceptance: z.ZodBoolean;
99
+ digest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
100
+ }, z.core.$strict>;
101
+ export type PortableContextTransferPlan = z.infer<typeof PortableContextTransferPlanSchema>;
102
+ export declare function portableContextTransferPlanDigest(material: Omit<PortableContextTransferPlan, "digest">): `sha256:${string}`;
103
+ export declare function projectPortableContextPlan(plan: PortableContextPlan): PortableContextTransferPlan;
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+ import { CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js";
3
+ import { PortableContextSourceBoundarySchema, PortableConversationContextSchema } from "./portable-context-base.js";
4
+ import { portableContextMessageMetadataDigest, portableContextPlanCommitmentMaterial } from "./portable-context-commitments.js";
5
+ import { PortableContextDestinationSchema, PortableContextPlanSchema } from "./portable-context-plan.js";
6
+ import { idSchema, InputPartSchema, sameStringSet, sha256DigestSchema, wireDigest } from "./portable-context-shared.js";
7
+ const partDecisionSchema = z.strictObject({
8
+ partIndex: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
9
+ action: z.enum(["include", "transform", "omit"]),
10
+ outputDigest: sha256DigestSchema.optional(),
11
+ }).superRefine((part, refinement) => {
12
+ if ((part.action === "transform") !== (part.outputDigest !== undefined)) {
13
+ refinement.addIssue({ code: "custom", message: "only transformed parts require an output commitment" });
14
+ }
15
+ });
16
+ const messageDecisionSchema = z.strictObject({
17
+ messageId: idSchema,
18
+ action: z.enum(["include", "omit"]),
19
+ parts: z.array(partDecisionSchema).max(CONTRACT_MAX_ARRAY_LENGTH),
20
+ }).superRefine((message, refinement) => {
21
+ if (message.action === "omit" && message.parts.some((part) => part.action !== "omit")) {
22
+ refinement.addIssue({ code: "custom", message: "omitted messages must omit every part" });
23
+ }
24
+ });
25
+ /** Destination wire plan. The complete source and human explanations remain at the caller. */
26
+ export const PortableContextTransferPlanSchema = z.strictObject({
27
+ kind: z.literal("portable-context-transfer-plan"),
28
+ planId: idSchema,
29
+ reviewDigest: sha256DigestSchema,
30
+ source: z.strictObject({
31
+ source: PortableContextSourceBoundarySchema,
32
+ digest: sha256DigestSchema,
33
+ completeness: z.enum(["complete", "partial"]),
34
+ messages: z.array(z.strictObject({
35
+ messageId: idSchema,
36
+ metadataDigest: sha256DigestSchema.optional(),
37
+ parts: z.array(sha256DigestSchema.nullable()).max(CONTRACT_MAX_ARRAY_LENGTH),
38
+ })).max(CONTRACT_MAX_ARRAY_LENGTH),
39
+ attachments: z.array(z.strictObject({
40
+ messageId: idSchema,
41
+ partIndex: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
42
+ digest: sha256DigestSchema,
43
+ })).max(CONTRACT_MAX_ARRAY_LENGTH),
44
+ }),
45
+ destination: PortableContextDestinationSchema,
46
+ messages: z.array(messageDecisionSchema).max(CONTRACT_MAX_ARRAY_LENGTH),
47
+ context: PortableConversationContextSchema,
48
+ estimatedTokens: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
49
+ requiresAcceptance: z.boolean(),
50
+ digest: sha256DigestSchema,
51
+ }).superRefine((plan, refinement) => {
52
+ const invalid = (message) => refinement.addIssue({ code: "custom", message });
53
+ const sourceIds = plan.source.messages.map((message) => message.messageId);
54
+ const decisionIds = plan.messages.map((message) => message.messageId);
55
+ if (new Set(sourceIds).size !== sourceIds.length || new Set(decisionIds).size !== decisionIds.length || !sameStringSet(sourceIds, decisionIds)) {
56
+ invalid("a transfer plan must decide every source message exactly once");
57
+ return;
58
+ }
59
+ if (wireDigest(plan.source.source) !== wireDigest(plan.context.source))
60
+ invalid("output context source must match the immutable source");
61
+ const outputIds = [];
62
+ const expectedAttachments = [];
63
+ for (const source of plan.source.messages) {
64
+ const decision = plan.messages.find((message) => message.messageId === source.messageId);
65
+ const ordered = [...decision.parts].sort((left, right) => left.partIndex - right.partIndex);
66
+ if (ordered.length !== source.parts.length || ordered.some((part, index) => part.partIndex !== index)) {
67
+ invalid("each source part requires exactly one transfer decision");
68
+ continue;
69
+ }
70
+ if ((decision.action === "include") !== (source.metadataDigest !== undefined) ||
71
+ ordered.some((part) => (decision.action === "include" && part.action === "include") !== (source.parts[part.partIndex] !== null))) {
72
+ invalid("only selected source parts and metadata may carry individual commitments");
73
+ }
74
+ if (decision.action === "omit")
75
+ continue;
76
+ outputIds.push(source.messageId);
77
+ const output = plan.context.messages.find((message) => message.id === source.messageId);
78
+ const retained = ordered.filter((part) => part.action !== "omit");
79
+ if (!output || portableContextMessageMetadataDigest(output) !== source.metadataDigest || retained.length === 0 || output.parts.length !== retained.length) {
80
+ invalid("selected message metadata and part count must match the source commitments");
81
+ continue;
82
+ }
83
+ for (const [outputIndex, part] of retained.entries()) {
84
+ const value = output.parts[outputIndex];
85
+ if (!InputPartSchema.safeParse(value).success || wireDigest(value) !== (part.action === "transform" ? part.outputDigest : source.parts[part.partIndex])) {
86
+ invalid("selected part bytes must match the accepted source or transform commitment");
87
+ }
88
+ }
89
+ }
90
+ if (JSON.stringify(outputIds) !== JSON.stringify(plan.context.messages.map((message) => message.id)))
91
+ invalid("selected messages must retain source order");
92
+ for (const attachment of plan.source.attachments) {
93
+ const message = plan.source.messages.find((item) => item.messageId === attachment.messageId);
94
+ if (message === undefined || attachment.partIndex >= message.parts.length) {
95
+ invalid("attachment commitment must reference a source part");
96
+ continue;
97
+ }
98
+ const decision = plan.messages.find((item) => item.messageId === attachment.messageId);
99
+ const retained = [...decision.parts].sort((a, b) => a.partIndex - b.partIndex).filter((part) => part.action !== "omit");
100
+ const outputIndex = retained.findIndex((part) => part.partIndex === attachment.partIndex);
101
+ if (decision.action !== "include" || retained[outputIndex]?.action !== "include") {
102
+ invalid("only selected source attachments may carry individual commitments");
103
+ continue;
104
+ }
105
+ const selected = plan.context.attachments.find((item) => item.messageId === attachment.messageId && item.partIndex === outputIndex && wireDigest({ ...item, partIndex: attachment.partIndex }) === attachment.digest);
106
+ if (selected === undefined)
107
+ invalid("selected attachment must match its source commitment");
108
+ else
109
+ expectedAttachments.push(selected);
110
+ }
111
+ if (wireDigest(expectedAttachments) !== wireDigest(plan.context.attachments))
112
+ invalid("output attachments must exactly match the selected source parts");
113
+ const changes = plan.messages.some((message) => message.action === "omit" || message.parts.some((part) => part.action !== "include"));
114
+ const partial = changes || plan.source.completeness === "partial";
115
+ if (plan.context.completeness !== (partial ? "partial" : "complete") || plan.requiresAcceptance !== partial)
116
+ invalid("changed or partial context requires explicit acceptance");
117
+ const { digest, ...material } = plan;
118
+ if (digest !== wireDigest(material))
119
+ invalid("portable transfer plan digest does not match its commitments");
120
+ });
121
+ export function portableContextTransferPlanDigest(material) {
122
+ return wireDigest(material);
123
+ }
124
+ export function projectPortableContextPlan(plan) {
125
+ const exact = PortableContextPlanSchema.parse(plan);
126
+ return PortableContextTransferPlanSchema.parse({ ...portableContextPlanCommitmentMaterial(exact), digest: exact.digest });
127
+ }
@@ -11,6 +11,7 @@ export declare const PortableContextDestinationSchema: z.ZodObject<{
11
11
  executionId: z.ZodString;
12
12
  model: z.ZodOptional<z.ZodString>;
13
13
  profileDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
14
+ workspaceSnapshotDigest: z.ZodOptional<z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>>;
14
15
  }, z.core.$strict>;
15
16
  export type PortableContextDestination = z.infer<typeof PortableContextDestinationSchema>;
16
17
  export interface PortableContextPartPlan {
@@ -87,7 +88,7 @@ export declare const PortableContextMessagePlanSchema: z.ZodObject<{
87
88
  }, z.core.$strict>;
88
89
  export interface PortableContextPlan {
89
90
  planId: string;
90
- /** Full immutable source so an executor can verify every decision itself. */
91
+ /** Full immutable local source for accepting decisions. Never serialize this plan to a destination. */
91
92
  source: PortableConversationContext;
92
93
  destination: PortableContextDestination;
93
94
  messages: PortableContextMessagePlan[];
@@ -144,6 +145,7 @@ export declare const PortableContextPlanSchema: z.ZodObject<{
144
145
  executionId: z.ZodString;
145
146
  model: z.ZodOptional<z.ZodString>;
146
147
  profileDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
148
+ workspaceSnapshotDigest: z.ZodOptional<z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>>;
147
149
  }, z.core.$strict>;
148
150
  messages: z.ZodArray<z.ZodObject<{
149
151
  messageId: z.ZodString;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { idSchema, InputPartSchema, sha256DigestSchema, sameStringSet, wireDigest, } from "./portable-context-shared.js";
3
3
  import { boundedStringSchema, CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js";
4
4
  import { PortableConversationContextSchema } from "./portable-context-base.js";
5
+ import { portableContextPlanCommitmentMaterial } from "./portable-context-commitments.js";
5
6
  export const PortableContextDestinationSchema = z.strictObject({
6
7
  runner: idSchema,
7
8
  provider: idSchema,
@@ -12,6 +13,8 @@ export const PortableContextDestinationSchema = z.strictObject({
12
13
  executionId: idSchema,
13
14
  model: idSchema.optional(),
14
15
  profileDigest: sha256DigestSchema,
16
+ /** Complete portable workspace reference approved with this destination. */
17
+ workspaceSnapshotDigest: sha256DigestSchema.optional(),
15
18
  });
16
19
  export const PortableContextPartPlanSchema = z
17
20
  .strictObject({
@@ -87,17 +90,7 @@ const PortableContextPlanMaterialSchema = z.strictObject({
87
90
  });
88
91
  export function portableContextPlanDigest(plan) {
89
92
  const parsed = PortableContextPlanMaterialSchema.parse(plan);
90
- return wireDigest({
91
- planId: parsed.planId,
92
- source: parsed.source,
93
- destination: parsed.destination,
94
- messages: parsed.messages,
95
- context: parsed.context,
96
- ...(parsed.estimatedTokens === undefined
97
- ? {}
98
- : { estimatedTokens: parsed.estimatedTokens }),
99
- requiresAcceptance: parsed.requiresAcceptance,
100
- });
93
+ return wireDigest(portableContextPlanCommitmentMaterial(parsed));
101
94
  }
102
95
  export const PortableContextPlanSchema = z
103
96
  .strictObject({
@@ -189,10 +182,14 @@ export const PortableContextPlanSchema = z
189
182
  message: "output messages do not exactly match the source decisions",
190
183
  });
191
184
  }
192
- const expectedAttachments = plan.source.attachments.filter((attachment) => {
185
+ const expectedAttachments = plan.source.attachments.flatMap((attachment) => {
193
186
  const message = plan.messages.find((candidate) => candidate.messageId === attachment.messageId);
194
187
  const part = message?.parts.find((candidate) => candidate.partIndex === attachment.partIndex);
195
- return message?.action === "include" && part?.action === "include";
188
+ if (message?.action !== "include" || part?.action !== "include")
189
+ return [];
190
+ const retained = [...message.parts].sort((left, right) => left.partIndex - right.partIndex)
191
+ .filter((item) => item.action !== "omit");
192
+ return [{ ...attachment, partIndex: retained.findIndex((item) => item.partIndex === attachment.partIndex) }];
196
193
  });
197
194
  if (wireDigest(expectedAttachments) !== wireDigest(plan.context.attachments)) {
198
195
  refinement.addIssue({