@tangle-network/agent-interface 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,9 +33,15 @@ The public `AgentInstanceRecord` contains a credential-free profile identity, no
33
33
 
34
34
  `AgentRunControlRef` identifies a retained run without depending on a live JavaScript object and may carry the provider's admission digest so reconstruction can reject changed-input reuse.
35
35
  `RuntimeEventEnvelope` adds stable run, event, sequence, cursor, and timestamp fields around the existing `StreamEvent` union, and its runtime schema validates every canonical event variant.
36
+ The `child-task` event reports one update of a provider-native child task (a subagent, worker, or delegated task) with a stable `childId`, an optional `parentChildId`, a lifecycle status, start and update times, and the runner, model, usage, and terminal reason when the provider reports them.
37
+ Its `sourceEventId` identifies the exact update, so a consumer applies the first event with a given `sourceEventId` and ignores later copies during replay or reconnect.
38
+ Identity never depends on the bounded `raw` payload, and a provider that cannot report a stable `childId` emits no `child-task` event.
36
39
  The canonical `cancelled` status identifies caller cancellation and remains distinct from `failed`.
37
40
  Providers advertise `retainedControl` only when exact run, result, event, cancellation, replay, detach, turn, and session identity are all implemented together.
38
41
  `AgentEnvironment.metadata` is the detached snapshot returned by create or get, so recovery can check persisted annotations without listing environments.
42
+ `AgentEnvironment.creation` reports what the create call that returned the object did: `created` when the call provisioned the environment, `replayed` when an existing environment matched the idempotency key.
43
+ It is a per-call fact, so a same-key replay returns a view of the same environment with `creation: "replayed"`, and the value is absent when the provider cannot prove either outcome.
44
+ A consumer never destroys an environment whose creation it cannot prove, because another caller can hold it.
39
45
  Metadata can include caller-authored values and does not prove authorization or authorship.
40
46
  `AgentSession.cancelRun()` accepts a canonical request digest bound to one operation and `AgentExactRunControlRef`, so a caller can safely repeat the same cancellation after losing the first acknowledgement.
41
47
  Its acknowledgement repeats the operation, digest, and run coordinates and distinguishes a known cancellation effect from conflict or unknown state.
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Evidence that one captured artifact is the canonical bytes of its material.
4
+ *
5
+ * The artifact's hash must equal the digest the material canonicalizes to, and
6
+ * the artifact must actually carry bytes. Both are integrity claims: a record
7
+ * whose artifact hash names something other than its material is evidence for
8
+ * a different value, and a zero-length artifact is a claim with nothing behind
9
+ * it. `label` names the evidence in the refusal so a reader knows which record
10
+ * failed.
11
+ */
12
+ export declare function agentCandidateEvidenceSchema<TKind extends string, TMaterial>(kind: TKind, material: z.ZodType<TMaterial>, label: string): z.ZodObject<{
13
+ kind: z.ZodLiteral<TKind>;
14
+ digest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
15
+ material: z.ZodType<TMaterial, unknown, z.core.$ZodTypeInternals<TMaterial, unknown>>;
16
+ artifact: z.ZodUnion<readonly [z.ZodObject<{
17
+ locator: z.ZodDiscriminatedUnion<[z.ZodObject<{
18
+ kind: z.ZodLiteral<"s3">;
19
+ bucket: z.ZodString;
20
+ key: z.ZodString;
21
+ region: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strict>, z.ZodObject<{
23
+ kind: z.ZodLiteral<"ipfs">;
24
+ cid: z.ZodString;
25
+ path: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strict>], "kind">;
27
+ sha256: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
28
+ byteLength: z.ZodNumber;
29
+ }, z.core.$strict>, z.ZodObject<{
30
+ encoding: z.ZodLiteral<"base64">;
31
+ content: z.ZodString;
32
+ sha256: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
33
+ byteLength: z.ZodNumber;
34
+ }, z.core.$strict>]>;
35
+ }, z.core.$strict>;
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { agentCandidateCapturedArtifactSchema } from "./agent-candidate-artifact-schema.js";
3
+ import { sha256DigestSchema } from "./agent-candidate-schema-common.js";
4
+ /**
5
+ * Evidence that one captured artifact is the canonical bytes of its material.
6
+ *
7
+ * The artifact's hash must equal the digest the material canonicalizes to, and
8
+ * the artifact must actually carry bytes. Both are integrity claims: a record
9
+ * whose artifact hash names something other than its material is evidence for
10
+ * a different value, and a zero-length artifact is a claim with nothing behind
11
+ * it. `label` names the evidence in the refusal so a reader knows which record
12
+ * failed.
13
+ */
14
+ export function agentCandidateEvidenceSchema(kind, material, label) {
15
+ return z
16
+ .object({
17
+ kind: z.literal(kind),
18
+ digest: sha256DigestSchema,
19
+ material,
20
+ artifact: agentCandidateCapturedArtifactSchema,
21
+ })
22
+ .strict()
23
+ .superRefine((evidence, ctx) => {
24
+ if (evidence.artifact.sha256 !== evidence.digest) {
25
+ ctx.addIssue({
26
+ code: "custom",
27
+ path: ["artifact", "sha256"],
28
+ message: `${label} artifact hash must equal its canonical material digest`,
29
+ });
30
+ }
31
+ if (evidence.artifact.byteLength === 0) {
32
+ ctx.addIssue({
33
+ code: "custom",
34
+ path: ["artifact", "byteLength"],
35
+ message: `${label} artifact must contain canonical material bytes`,
36
+ });
37
+ }
38
+ });
39
+ }
@@ -1,3 +1,4 @@
1
+ import { agentCandidateEvidenceSchema } from "./agent-candidate-evidence-schema.js";
1
2
  import { z } from "zod";
2
3
  import { agentCandidateArtifactRefSchema, agentCandidateCapturedArtifactSchema, agentCandidateWorkspaceSnapshotEvidenceSchema, } from "./agent-candidate-artifact-schema.js";
3
4
  import { agentCandidateContainerSchema, agentCandidateInstructionDeliverySchema, agentCandidateWorkingDirectorySchema, } from "./agent-candidate-code-schema.js";
@@ -448,30 +449,7 @@ export const agentCandidateExecutionPlanMaterialSchema = z
448
449
  }
449
450
  });
450
451
  function planEvidenceSchema(kind, material) {
451
- return z
452
- .object({
453
- kind: z.literal(kind),
454
- digest: sha256DigestSchema,
455
- material,
456
- artifact: agentCandidateCapturedArtifactSchema,
457
- })
458
- .strict()
459
- .superRefine((evidence, ctx) => {
460
- if (evidence.artifact.sha256 !== evidence.digest) {
461
- ctx.addIssue({
462
- code: "custom",
463
- path: ["artifact", "sha256"],
464
- message: "plan artifact hash must equal its canonical material digest",
465
- });
466
- }
467
- if (evidence.artifact.byteLength === 0) {
468
- ctx.addIssue({
469
- code: "custom",
470
- path: ["artifact", "byteLength"],
471
- message: "plan artifact must contain canonical material bytes",
472
- });
473
- }
474
- });
452
+ return agentCandidateEvidenceSchema(kind, material, "plan");
475
453
  }
476
454
  export const agentCandidateProfilePlanEvidenceSchema = planEvidenceSchema("agent-profile-workspace-plan", agentCandidateProfilePlanMaterialSchema);
477
455
  export const agentCandidateProfileActivationSchema = createAgentProfileActivationEvidenceSchema(agentCandidateProfilePlanEvidenceSchema)
@@ -1,5 +1,6 @@
1
+ import { agentCandidateEvidenceSchema } from "./agent-candidate-evidence-schema.js";
1
2
  import { z } from "zod";
2
- import { agentCandidateArtifactRefSchema, agentCandidateCapturedArtifactSchema, agentCandidateWorkspaceSnapshotEvidenceSchema, } from "./agent-candidate-artifact-schema.js";
3
+ import { agentCandidateArtifactRefSchema, agentCandidateWorkspaceSnapshotEvidenceSchema, } from "./agent-candidate-artifact-schema.js";
3
4
  import { agentCandidateBenchmarkGraderIdentitySchema, agentCandidateResolvedModelSchema, } from "./agent-candidate-execution-plan-schema.js";
4
5
  import { agentCandidateMediaTypeSchema, gitObjectSchema, isCanonicalJsonValue, sameGitObjectFormat, sha256DigestSchema, } from "./agent-candidate-schema-common.js";
5
6
  const safeCountSchema = z
@@ -174,7 +175,7 @@ function refineModelSettlementMaterial(material, ctx) {
174
175
  });
175
176
  }
176
177
  }
177
- export const agentCandidateModelSettlementEvidenceSchema = evidenceSchema("agent-candidate-model-settlement", agentCandidateModelSettlementMaterialSchema, "model settlement");
178
+ export const agentCandidateModelSettlementEvidenceSchema = agentCandidateEvidenceSchema("agent-candidate-model-settlement", agentCandidateModelSettlementMaterialSchema, "model settlement");
178
179
  export const agentCandidateRepositoryStateSchema = z
179
180
  .object({
180
181
  identity: z.string().min(1),
@@ -278,7 +279,7 @@ export const agentCandidateTaskOutcomeMaterialSchema = z
278
279
  });
279
280
  }
280
281
  });
281
- export const agentCandidateTaskOutcomeEvidenceSchema = evidenceSchema("agent-candidate-task-outcome", agentCandidateTaskOutcomeMaterialSchema, "task outcome");
282
+ export const agentCandidateTaskOutcomeEvidenceSchema = agentCandidateEvidenceSchema("agent-candidate-task-outcome", agentCandidateTaskOutcomeMaterialSchema, "task outcome");
282
283
  export const agentCandidateBenchmarkDimensionSchema = z
283
284
  .object({
284
285
  name: normalizedDimensionNameSchema,
@@ -346,7 +347,7 @@ export const agentCandidateBenchmarkResultMaterialSchema = z
346
347
  });
347
348
  }
348
349
  });
349
- export const agentCandidateBenchmarkResultEvidenceSchema = evidenceSchema("agent-candidate-benchmark-result", agentCandidateBenchmarkResultMaterialSchema, "benchmark result");
350
+ export const agentCandidateBenchmarkResultEvidenceSchema = agentCandidateEvidenceSchema("agent-candidate-benchmark-result", agentCandidateBenchmarkResultMaterialSchema, "benchmark result");
350
351
  function sameFixedSpend(left, right) {
351
352
  return (left.inputTokens === right.inputTokens &&
352
353
  left.outputTokens === right.outputTokens &&
@@ -356,29 +357,3 @@ function sameFixedSpend(left, right) {
356
357
  left.costUsdNanos === right.costUsdNanos &&
357
358
  left.costProvenance === right.costProvenance);
358
359
  }
359
- function evidenceSchema(kind, material, label) {
360
- return z
361
- .object({
362
- kind: z.literal(kind),
363
- digest: sha256DigestSchema,
364
- material,
365
- artifact: agentCandidateCapturedArtifactSchema,
366
- })
367
- .strict()
368
- .superRefine((evidence, ctx) => {
369
- if (evidence.artifact.sha256 !== evidence.digest) {
370
- ctx.addIssue({
371
- code: "custom",
372
- path: ["artifact", "sha256"],
373
- message: `${label} artifact hash must equal its canonical material digest`,
374
- });
375
- }
376
- if (evidence.artifact.byteLength === 0) {
377
- ctx.addIssue({
378
- code: "custom",
379
- path: ["artifact", "byteLength"],
380
- message: `${label} artifact must contain canonical material bytes`,
381
- });
382
- }
383
- });
384
- }
@@ -1,4 +1,5 @@
1
1
  import { canonicalAgentProfileValue } from "./agent-profile-canonical.js";
2
+ import { deepFreeze } from "./deep-freeze.js";
2
3
  import { agentProfileSchema } from "./profile-schema.js";
3
4
  /**
4
5
  * Detach, validate, and recursively freeze one AgentProfile at an intake boundary.
@@ -12,13 +13,3 @@ export function snapshotAgentProfile(value) {
12
13
  canonicalAgentProfileValue(parsed);
13
14
  return deepFreeze(parsed);
14
15
  }
15
- function deepFreeze(value, seen = new Set()) {
16
- if (value === null || typeof value !== "object" || seen.has(value)) {
17
- return value;
18
- }
19
- seen.add(value);
20
- for (const child of Object.values(value)) {
21
- deepFreeze(child, seen);
22
- }
23
- return Object.freeze(value);
24
- }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { deepFreeze } from "./deep-freeze.js";
2
3
  import { canonicalCandidateDigest, isSafeRelativePath, sha256DigestSchema, } from "./agent-candidate-schema-common.js";
3
4
  const MAX_INLINE_CONTEXT_BYTES = 65_536;
4
5
  const MAX_TOTAL_INLINE_CONTEXT_BYTES = 131_072;
@@ -203,15 +204,6 @@ export const certifiedContextSchema = z
203
204
  });
204
205
  }
205
206
  });
206
- function deepFreeze(value) {
207
- if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
208
- for (const child of Object.values(value)) {
209
- deepFreeze(child);
210
- }
211
- Object.freeze(value);
212
- }
213
- return value;
214
- }
215
207
  const _certifiedContextDeliverySchemaMatches = true;
216
208
  const _certifiedContextProvenanceSchemaMatches = true;
217
209
  const _certifiedContextEntrySchemaMatches = true;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Make a validated value immutable before it is handed out.
3
+ *
4
+ * A profile snapshot, a certified context, and a capability document are each
5
+ * evidence: something already checked, that a caller then reads and acts on.
6
+ * If the copy the caller holds can be written to, the evidence can be changed
7
+ * after the check that made it trustworthy, and a mutated capability flag
8
+ * describes a surface the environment does not have.
9
+ *
10
+ * Values already visited are skipped, so a value that refers back to itself is
11
+ * frozen once instead of recursing until the stack runs out.
12
+ */
13
+ export declare function deepFreeze<T>(value: T, seen?: Set<object>): T;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Make a validated value immutable before it is handed out.
3
+ *
4
+ * A profile snapshot, a certified context, and a capability document are each
5
+ * evidence: something already checked, that a caller then reads and acts on.
6
+ * If the copy the caller holds can be written to, the evidence can be changed
7
+ * after the check that made it trustworthy, and a mutated capability flag
8
+ * describes a surface the environment does not have.
9
+ *
10
+ * Values already visited are skipped, so a value that refers back to itself is
11
+ * frozen once instead of recursing until the stack runs out.
12
+ */
13
+ export function deepFreeze(value, seen = new Set()) {
14
+ if (value === null || typeof value !== "object" || seen.has(value)) {
15
+ return value;
16
+ }
17
+ seen.add(value);
18
+ for (const child of Object.values(value)) {
19
+ deepFreeze(child, seen);
20
+ }
21
+ return Object.freeze(value);
22
+ }
@@ -0,0 +1,48 @@
1
+ import type { ExecResult } from "./environment-requests.js";
2
+ import type { AgentEnvironment, AgentEnvironmentCapabilities, AgentEnvironmentEvent, AgentTurnInput } from "./environment-runtime.js";
3
+ /**
4
+ * Normalize the untyped return of a sandbox SDK's command call into
5
+ * {@link ExecResult}.
6
+ *
7
+ * Sandbox SDKs name the same three facts differently: an exit status under
8
+ * `exitCode` or `code`, captured output under `stdout` or `output`, and
9
+ * captured errors under `stderr` or `error`. Reading both names in one place
10
+ * keeps every adapter's exec surface answering with the same shape.
11
+ *
12
+ * A result carrying no exit status is refused. Exit zero is the one value that
13
+ * means the command succeeded, so reading an absent status as zero reports a
14
+ * command nobody watched as a command that worked, and the turn that ran it
15
+ * completes on evidence that was never measured. `source` names the SDK call
16
+ * whose answer could not be read, because that is where the fix belongs.
17
+ */
18
+ export declare function execResultFromUnknown(value: unknown, source: string): ExecResult;
19
+ export interface CommandTurnOptions {
20
+ /** The turn the caller asked this environment to run. */
21
+ input: AgentTurnInput;
22
+ /** The environment whose `exec` runs the command. */
23
+ environment: AgentEnvironment;
24
+ /** The adapter name the refusal names when no command can be resolved. */
25
+ providerLabel: string;
26
+ /** The caller's explicit command for this turn, chosen before any default. */
27
+ turnCommand?: (input: AgentTurnInput, environment: AgentEnvironment) => string | Promise<string>;
28
+ }
29
+ /**
30
+ * Run one turn as a single command in an environment whose only surface is a
31
+ * workspace, and emit the turn's events.
32
+ *
33
+ * The command comes from the caller's `turnCommand`, then
34
+ * `providerOptions.command` or `providerOptions.agentCommand`, then the
35
+ * prompt. An environment that reaches none of them cannot run the turn and
36
+ * the turn is refused rather than executing an empty command.
37
+ */
38
+ export declare function commandTurnEvents(options: CommandTurnOptions): AsyncIterable<AgentEnvironmentEvent>;
39
+ /**
40
+ * The capability document of a provider whose only surface is a workspace.
41
+ *
42
+ * Such an adapter runs commands and moves files in a sandbox. It owns no
43
+ * agent profile, serves no live or replayable stream, keeps no provider
44
+ * session, and branches no environment, so every surface outside `workspace`
45
+ * and `placement` reads false. The caller states its own workspace facts
46
+ * because they are the only ones that differ between these adapters.
47
+ */
48
+ export declare function execOnlyEnvironmentCapabilities(workspace: AgentEnvironmentCapabilities["workspace"]): AgentEnvironmentCapabilities;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Read one non-empty string from an untyped provider-options bag.
3
+ *
4
+ * An absent key and a present key holding a non-string or an empty string are
5
+ * the same answer: the caller named nothing.
6
+ */
7
+ function stringOption(value) {
8
+ return typeof value === "string" && value.length > 0 ? value : undefined;
9
+ }
10
+ function number(value) {
11
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
12
+ }
13
+ /**
14
+ * Normalize the untyped return of a sandbox SDK's command call into
15
+ * {@link ExecResult}.
16
+ *
17
+ * Sandbox SDKs name the same three facts differently: an exit status under
18
+ * `exitCode` or `code`, captured output under `stdout` or `output`, and
19
+ * captured errors under `stderr` or `error`. Reading both names in one place
20
+ * keeps every adapter's exec surface answering with the same shape.
21
+ *
22
+ * A result carrying no exit status is refused. Exit zero is the one value that
23
+ * means the command succeeded, so reading an absent status as zero reports a
24
+ * command nobody watched as a command that worked, and the turn that ran it
25
+ * completes on evidence that was never measured. `source` names the SDK call
26
+ * whose answer could not be read, because that is where the fix belongs.
27
+ */
28
+ export function execResultFromUnknown(value, source) {
29
+ const record = value && typeof value === "object" ? value : {};
30
+ const exitCode = number(record.exitCode) ?? number(record.code);
31
+ if (exitCode === undefined) {
32
+ throw new Error(`${source} returned no exit status: a command result must carry a finite exitCode or code`);
33
+ }
34
+ return {
35
+ exitCode,
36
+ stdout: typeof record.stdout === "string"
37
+ ? record.stdout
38
+ : typeof record.output === "string"
39
+ ? record.output
40
+ : "",
41
+ stderr: typeof record.stderr === "string"
42
+ ? record.stderr
43
+ : typeof record.error === "string"
44
+ ? record.error
45
+ : "",
46
+ };
47
+ }
48
+ /**
49
+ * Run one turn as a single command in an environment whose only surface is a
50
+ * workspace, and emit the turn's events.
51
+ *
52
+ * The command comes from the caller's `turnCommand`, then
53
+ * `providerOptions.command` or `providerOptions.agentCommand`, then the
54
+ * prompt. An environment that reaches none of them cannot run the turn and
55
+ * the turn is refused rather than executing an empty command.
56
+ */
57
+ export async function* commandTurnEvents(options) {
58
+ const { input, environment, providerLabel } = options;
59
+ const command = (await options.turnCommand?.(input, environment)) ??
60
+ stringOption(input.providerOptions?.command) ??
61
+ stringOption(input.providerOptions?.agentCommand) ??
62
+ input.prompt;
63
+ if (!command) {
64
+ throw new Error(`${providerLabel} provider requires turnCommand, providerOptions.command, or prompt`);
65
+ }
66
+ const result = await environment.exec?.(command, {
67
+ cwd: stringOption(input.providerOptions?.cwd),
68
+ timeoutMs: input.timeoutMs,
69
+ signal: input.signal,
70
+ });
71
+ const text = result?.stdout ?? "";
72
+ yield { type: "message.part.updated", data: { delta: text } };
73
+ yield {
74
+ type: "result",
75
+ data: {
76
+ finalText: text,
77
+ status: result?.exitCode === 0 ? "completed" : "failed",
78
+ exitCode: result?.exitCode ?? 1,
79
+ stderr: result?.stderr ?? "",
80
+ },
81
+ };
82
+ }
83
+ /**
84
+ * The capability document of a provider whose only surface is a workspace.
85
+ *
86
+ * Such an adapter runs commands and moves files in a sandbox. It owns no
87
+ * agent profile, serves no live or replayable stream, keeps no provider
88
+ * session, and branches no environment, so every surface outside `workspace`
89
+ * and `placement` reads false. The caller states its own workspace facts
90
+ * because they are the only ones that differ between these adapters.
91
+ */
92
+ export function execOnlyEnvironmentCapabilities(workspace) {
93
+ return {
94
+ profile: {
95
+ namedProfiles: false,
96
+ systemPrompt: { replace: false, append: false },
97
+ instructions: false,
98
+ tools: false,
99
+ permissions: false,
100
+ mcp: false,
101
+ subagents: false,
102
+ resources: {
103
+ files: true,
104
+ instructions: false,
105
+ tools: false,
106
+ skills: false,
107
+ agents: false,
108
+ commands: false,
109
+ },
110
+ hooks: false,
111
+ modes: false,
112
+ runtimeUpdate: false,
113
+ validation: false,
114
+ },
115
+ streaming: {
116
+ live: false,
117
+ replay: false,
118
+ detach: false,
119
+ turnIdempotency: false,
120
+ },
121
+ sessions: { continue: false, list: false, messages: false },
122
+ workspace: { ...workspace },
123
+ branching: { checkpoint: false, fork: false },
124
+ placement: true,
125
+ usage: false,
126
+ confidential: false,
127
+ };
128
+ }
@@ -4,3 +4,4 @@ export * from "./environment-interactive.js";
4
4
  export * from "./environment-observation.js";
5
5
  export * from "./environment-terminal.js";
6
6
  export * from "./environment-runtime.js";
7
+ export * from "./environment-command-turn.js";
@@ -4,3 +4,4 @@ export * from "./environment-interactive.js";
4
4
  export * from "./environment-observation.js";
5
5
  export * from "./environment-terminal.js";
6
6
  export * from "./environment-runtime.js";
7
+ export * from "./environment-command-turn.js";
@@ -624,10 +624,34 @@ export interface AgentSession {
624
624
  signal?: AbortSignal;
625
625
  }): Promise<void>;
626
626
  }
627
+ /**
628
+ * What one {@link AgentEnvironmentProvider.create} call did for the
629
+ * environment it returned.
630
+ *
631
+ * - `created`: this call provisioned the environment.
632
+ * - `replayed`: an existing environment that matched the idempotency key was
633
+ * returned. This call provisioned nothing.
634
+ *
635
+ * Absent when the provider cannot distinguish the two. A consumer treats an
636
+ * absent value as unknown and fails closed: it never destroys an environment
637
+ * whose creation it cannot prove, because another caller can hold it.
638
+ */
639
+ export type AgentEnvironmentCreation = "created" | "replayed";
640
+ export declare const AgentEnvironmentCreationSchema: z.ZodEnum<{
641
+ replayed: "replayed";
642
+ created: "created";
643
+ }>;
627
644
  export interface AgentEnvironment {
628
645
  readonly id: string;
629
646
  readonly provider: string;
630
647
  readonly name?: string;
648
+ /**
649
+ * The verdict of the create call that returned this object. It is a
650
+ * per-call fact: a same-key replay returns a view of the same environment
651
+ * with `creation: "replayed"`. Absent on `get()` results and when the
652
+ * provider cannot prove which outcome happened.
653
+ */
654
+ readonly creation?: AgentEnvironmentCreation;
631
655
  /**
632
656
  * Detached metadata returned by the provider.
633
657
  * It can contain caller-authored annotations and is not authorization evidence.
@@ -945,15 +969,31 @@ export interface AgentEnvironmentCreateIdempotencyRecord<T> {
945
969
  readonly pending: Promise<T>;
946
970
  environment?: T;
947
971
  }
972
+ /**
973
+ * Return the per-call view of an environment that a same-key create replayed.
974
+ *
975
+ * The view shares every member of the environment, so operations act on the
976
+ * one environment, and it states `creation: "replayed"` because this call
977
+ * provisioned nothing. The copy is shallow, so the environment must be a plain
978
+ * object whose members do not read `this`; a class instance loses its
979
+ * prototype members in a copy and is rejected.
980
+ * @internal
981
+ */
982
+ export declare function replayedAgentEnvironmentView<T extends object>(environment: T): T;
948
983
  /**
949
984
  * Apply the generic create contract to one provider adapter's keyed requests.
950
985
  *
951
986
  * The provider's backing service remains responsible for retaining the key
952
987
  * across adapter reconstruction. This helper coalesces concurrent retries and
953
988
  * rejects collisions before the provider performs another create effect.
989
+ *
990
+ * The call that runs `create` receives the environment the provider built,
991
+ * with the creation verdict the provider could prove. Every same-key call
992
+ * after it, including one that awaited the same pending create, receives
993
+ * {@link replayedAgentEnvironmentView} of that environment.
954
994
  * @internal
955
995
  */
956
- export declare function createAgentEnvironmentWithIdempotency<T>(records: Map<string, AgentEnvironmentCreateIdempotencyRecord<T>>, input: CreateAgentEnvironmentInput, create: () => Promise<T>): Promise<T>;
996
+ export declare function createAgentEnvironmentWithIdempotency<T extends object>(records: Map<string, AgentEnvironmentCreateIdempotencyRecord<T>>, input: CreateAgentEnvironmentInput, create: () => Promise<T>): Promise<T>;
957
997
  export interface AgentEnvironmentProvider {
958
998
  readonly name: string;
959
999
  readonly exactProcess?: AgentExactProcessProvider;
@@ -113,6 +113,10 @@ export function agentNativeContextContinuationResultMatchesRequest(request, outc
113
113
  (exactOutcome.result.sessionId === undefined ||
114
114
  exactOutcome.result.sessionId === current.sessionId));
115
115
  }
116
+ export const AgentEnvironmentCreationSchema = z.enum([
117
+ "created",
118
+ "replayed",
119
+ ]);
116
120
  /** Strict runtime validator for provider capability negotiation. */
117
121
  export const AgentEnvironmentCapabilitiesSchema = z
118
122
  .strictObject({
@@ -325,12 +329,34 @@ export function agentEnvironmentCreateInputDigest(input) {
325
329
  input: material,
326
330
  });
327
331
  }
332
+ /**
333
+ * Return the per-call view of an environment that a same-key create replayed.
334
+ *
335
+ * The view shares every member of the environment, so operations act on the
336
+ * one environment, and it states `creation: "replayed"` because this call
337
+ * provisioned nothing. The copy is shallow, so the environment must be a plain
338
+ * object whose members do not read `this`; a class instance loses its
339
+ * prototype members in a copy and is rejected.
340
+ * @internal
341
+ */
342
+ export function replayedAgentEnvironmentView(environment) {
343
+ const prototype = Object.getPrototypeOf(environment);
344
+ if (prototype !== Object.prototype && prototype !== null) {
345
+ throw new Error("a replayed agent environment view requires a plain object environment");
346
+ }
347
+ return { ...environment, creation: "replayed" };
348
+ }
328
349
  /**
329
350
  * Apply the generic create contract to one provider adapter's keyed requests.
330
351
  *
331
352
  * The provider's backing service remains responsible for retaining the key
332
353
  * across adapter reconstruction. This helper coalesces concurrent retries and
333
354
  * rejects collisions before the provider performs another create effect.
355
+ *
356
+ * The call that runs `create` receives the environment the provider built,
357
+ * with the creation verdict the provider could prove. Every same-key call
358
+ * after it, including one that awaited the same pending create, receives
359
+ * {@link replayedAgentEnvironmentView} of that environment.
334
360
  * @internal
335
361
  */
336
362
  export async function createAgentEnvironmentWithIdempotency(records, input, create) {
@@ -344,7 +370,7 @@ export async function createAgentEnvironmentWithIdempotency(records, input, crea
344
370
  if (existing.digest !== digest) {
345
371
  throw new Error("agent environment create idempotency key conflicts with a different create input");
346
372
  }
347
- return existing.environment ?? existing.pending;
373
+ return replayedAgentEnvironmentView(existing.environment ?? (await existing.pending));
348
374
  }
349
375
  const pending = Promise.resolve().then(create);
350
376
  const record = {
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export * from "./host-services.js";
8
8
  export * from "./mcp.js";
9
9
  export * from "./provider-adapter.js";
10
10
  export type * from "./environment-provider.js";
11
- export { AgentEnvironmentCapabilitiesSchema, AgentNativeContextContinuationResultSchema, AgentTurnInputSchema, AgentTurnResultSchema, agentEnvironmentCreateInputDigest, agentNativeContextContinuationResultMatchesRequest, AccountUsageSchema, AgentEnvironmentObservationSchema, AgentEnvironmentStatusSchema, ComputeBillingSchema, EnvironmentLifecycleSchema, ModelUsageSchema, ObservationProvenanceSchema, ObservationStateSchema, PlacementDescriptorSchema, ProviderIdentitySchema, ResourceProfileSchema, ResourceUseSampleSchema, SafeEndpointSchema, agentEnvironmentObservationIdentityMatches, assertObservationCredentialFree, observationContainsCredential, observationOf, AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStartSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, AgentInteractiveSessionStatusSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimRequestDigest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionControlClaimIsNewer, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionPromptRequestDigest, agentInteractiveSessionStopAcknowledgementMatchesCommand, agentInteractiveSessionStopRequestDigest, agentInteractiveSessionRequestDigest, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionRunRef, agentInteractiveSessionStatusMatchesRef, exactAgentInteractiveSessionStart, createAgentEnvironmentWithIdempotency, TerminalAttachRequestSchema, TerminalAttachResultSchema, TerminalDetachAckSchema, TerminalInputSchema, TerminalOutputEventSchema, TerminalReplayWindowSchema, TerminalResizeSchema, TerminalSessionRefSchema, terminalAttachResultMatchesRequest, terminalSessionUsable, } from "./environment-provider.js";
11
+ export { AgentEnvironmentCapabilitiesSchema, AgentNativeContextContinuationResultSchema, AgentTurnInputSchema, AgentTurnResultSchema, agentEnvironmentCreateInputDigest, agentNativeContextContinuationResultMatchesRequest, AccountUsageSchema, AgentEnvironmentObservationSchema, AgentEnvironmentStatusSchema, ComputeBillingSchema, EnvironmentLifecycleSchema, ModelUsageSchema, ObservationProvenanceSchema, ObservationStateSchema, PlacementDescriptorSchema, ProviderIdentitySchema, ResourceProfileSchema, ResourceUseSampleSchema, SafeEndpointSchema, agentEnvironmentObservationIdentityMatches, assertObservationCredentialFree, observationContainsCredential, observationOf, AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStartSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, AgentInteractiveSessionStatusSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimRequestDigest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionControlClaimIsNewer, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionPromptRequestDigest, agentInteractiveSessionStopAcknowledgementMatchesCommand, agentInteractiveSessionStopRequestDigest, agentInteractiveSessionRequestDigest, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionRunRef, agentInteractiveSessionStatusMatchesRef, exactAgentInteractiveSessionStart, AgentEnvironmentCreationSchema, createAgentEnvironmentWithIdempotency, replayedAgentEnvironmentView, TerminalAttachRequestSchema, TerminalAttachResultSchema, TerminalDetachAckSchema, TerminalInputSchema, TerminalOutputEventSchema, TerminalReplayWindowSchema, TerminalResizeSchema, TerminalSessionRefSchema, terminalAttachResultMatchesRequest, terminalSessionUsable, } from "./environment-provider.js";
12
12
  export * from "./plan.js";
13
13
  export * from "./runtime-control.js";
14
14
  export * from "./portable-context.js";
@@ -33,6 +33,7 @@ export * from "./agent-profile-materialization.js";
33
33
  export * from "./agent-execution-preparation.js";
34
34
  export * from "./agent-workspace-lease.js";
35
35
  export * from "./certified-context.js";
36
+ export * from "./deep-freeze.js";
36
37
  export * from "./profile-diff.js";
37
38
  export * from "./harness.js";
38
39
  export * from "./harness-capabilities.js";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export * from "./provider-config.js";
7
7
  export * from "./host-services.js";
8
8
  export * from "./mcp.js";
9
9
  export * from "./provider-adapter.js";
10
- export { AgentEnvironmentCapabilitiesSchema, AgentNativeContextContinuationResultSchema, AgentTurnInputSchema, AgentTurnResultSchema, agentEnvironmentCreateInputDigest, agentNativeContextContinuationResultMatchesRequest, AccountUsageSchema, AgentEnvironmentObservationSchema, AgentEnvironmentStatusSchema, ComputeBillingSchema, EnvironmentLifecycleSchema, ModelUsageSchema, ObservationProvenanceSchema, ObservationStateSchema, PlacementDescriptorSchema, ProviderIdentitySchema, ResourceProfileSchema, ResourceUseSampleSchema, SafeEndpointSchema, agentEnvironmentObservationIdentityMatches, assertObservationCredentialFree, observationContainsCredential, observationOf, AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStartSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, AgentInteractiveSessionStatusSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimRequestDigest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionControlClaimIsNewer, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionPromptRequestDigest, agentInteractiveSessionStopAcknowledgementMatchesCommand, agentInteractiveSessionStopRequestDigest, agentInteractiveSessionRequestDigest, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionRunRef, agentInteractiveSessionStatusMatchesRef, exactAgentInteractiveSessionStart, createAgentEnvironmentWithIdempotency, TerminalAttachRequestSchema, TerminalAttachResultSchema, TerminalDetachAckSchema, TerminalInputSchema, TerminalOutputEventSchema, TerminalReplayWindowSchema, TerminalResizeSchema, TerminalSessionRefSchema, terminalAttachResultMatchesRequest, terminalSessionUsable, } from "./environment-provider.js";
10
+ export { AgentEnvironmentCapabilitiesSchema, AgentNativeContextContinuationResultSchema, AgentTurnInputSchema, AgentTurnResultSchema, agentEnvironmentCreateInputDigest, agentNativeContextContinuationResultMatchesRequest, AccountUsageSchema, AgentEnvironmentObservationSchema, AgentEnvironmentStatusSchema, ComputeBillingSchema, EnvironmentLifecycleSchema, ModelUsageSchema, ObservationProvenanceSchema, ObservationStateSchema, PlacementDescriptorSchema, ProviderIdentitySchema, ResourceProfileSchema, ResourceUseSampleSchema, SafeEndpointSchema, agentEnvironmentObservationIdentityMatches, assertObservationCredentialFree, observationContainsCredential, observationOf, AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStartSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, AgentInteractiveSessionStatusSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimRequestDigest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionControlClaimIsNewer, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionPromptRequestDigest, agentInteractiveSessionStopAcknowledgementMatchesCommand, agentInteractiveSessionStopRequestDigest, agentInteractiveSessionRequestDigest, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionRunRef, agentInteractiveSessionStatusMatchesRef, exactAgentInteractiveSessionStart, AgentEnvironmentCreationSchema, createAgentEnvironmentWithIdempotency, replayedAgentEnvironmentView, TerminalAttachRequestSchema, TerminalAttachResultSchema, TerminalDetachAckSchema, TerminalInputSchema, TerminalOutputEventSchema, TerminalReplayWindowSchema, TerminalResizeSchema, TerminalSessionRefSchema, terminalAttachResultMatchesRequest, terminalSessionUsable, } from "./environment-provider.js";
11
11
  export * from "./plan.js";
12
12
  export * from "./runtime-control.js";
13
13
  export * from "./portable-context.js";
@@ -31,6 +31,7 @@ export * from "./agent-profile-materialization.js";
31
31
  export * from "./agent-execution-preparation.js";
32
32
  export * from "./agent-workspace-lease.js";
33
33
  export * from "./certified-context.js";
34
+ export * from "./deep-freeze.js";
34
35
  export * from "./profile-diff.js";
35
36
  export * from "./harness.js";
36
37
  export * from "./harness-capabilities.js";
@@ -33,6 +33,15 @@ export declare const AgentExactRunControlRefSchema: z.ZodObject<{
33
33
  executionId: z.ZodString;
34
34
  requestDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
35
35
  }, z.core.$strict>;
36
+ /**
37
+ * Whether two references name the same run.
38
+ *
39
+ * Comparison is over the reference's canonical form rather than a list of
40
+ * field names, so a reference that gains a field is compared on it without a
41
+ * caller having to remember: two runs that differ only in a field nobody
42
+ * enumerated must not read as one run.
43
+ */
44
+ export declare function sameAgentRunControlRef(left: AgentRunControlRef, right: AgentRunControlRef): boolean;
36
45
  export type AgentRunCancellationEffect = "cancel_requested" | "cancelled" | "not_live" | "unknown";
37
46
  export interface AgentRunCancellationRequestMaterial {
38
47
  operationId: string;
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { canonicalCandidateDigest, sha256DigestSchema, } from "./agent-candidate-schema-common.js";
3
3
  import { boundedIdentifierSchema, boundedJsonRecordSchema, boundedJsonSchema, boundedStringSchema, } from "./contract-limits.js";
4
+ import { ModelUsageSchema } from "./environment-observation.js";
4
5
  import { InteractionRequestSchema } from "./interaction.js";
5
6
  import { DurablePlanSchema } from "./plan.js";
6
7
  const stableIdSchema = boundedIdentifierSchema;
@@ -17,6 +18,17 @@ export const AgentExactRunControlRefSchema = AgentRunControlRefSchema.extend({
17
18
  executionId: stableIdSchema,
18
19
  requestDigest: sha256DigestSchema,
19
20
  });
21
+ /**
22
+ * Whether two references name the same run.
23
+ *
24
+ * Comparison is over the reference's canonical form rather than a list of
25
+ * field names, so a reference that gains a field is compared on it without a
26
+ * caller having to remember: two runs that differ only in a field nobody
27
+ * enumerated must not read as one run.
28
+ */
29
+ export function sameAgentRunControlRef(left, right) {
30
+ return canonicalCandidateDigest(left) === canonicalCandidateDigest(right);
31
+ }
20
32
  export function agentRunCancellationRequestDigest(request) {
21
33
  const parsed = AgentRunCancellationRequestMaterialSchema.parse(request);
22
34
  return canonicalCandidateDigest({
@@ -110,8 +122,7 @@ export function agentRunCancellationAcknowledgementMatchesRequest(request, ackno
110
122
  return false;
111
123
  return (exactAcknowledgement.data.operationId === exactRequest.data.operationId &&
112
124
  exactAcknowledgement.data.requestDigest === exactRequest.data.requestDigest &&
113
- canonicalCandidateDigest(exactAcknowledgement.data.run) ===
114
- canonicalCandidateDigest(exactRequest.data.run));
125
+ sameAgentRunControlRef(exactAcknowledgement.data.run, exactRequest.data.run));
115
126
  }
116
127
  export function agentRunControlRequestDigest(request) {
117
128
  const parsed = AgentRunControlRequestMaterialSchema.parse(request);
@@ -187,8 +198,7 @@ export function agentRunControlAcknowledgementMatchesRequest(request, acknowledg
187
198
  return false;
188
199
  return (exactAcknowledgement.data.operationId === exactRequest.data.operationId &&
189
200
  exactAcknowledgement.data.requestDigest === exactRequest.data.requestDigest &&
190
- canonicalCandidateDigest(exactAcknowledgement.data.run) ===
191
- canonicalCandidateDigest(exactRequest.data.run));
201
+ sameAgentRunControlRef(exactAcknowledgement.data.run, exactRequest.data.run));
192
202
  }
193
203
  const unknownRecordSchema = boundedJsonRecordSchema;
194
204
  const partBase = {
@@ -263,6 +273,70 @@ const partSchema = z.discriminatedUnion("type", [
263
273
  agent: stableIdSchema,
264
274
  }),
265
275
  ]);
276
+ const TERMINAL_CHILD_TASK_STATUSES = new Set(["completed", "failed", "cancelled"]);
277
+ const epochMillisecondsSchema = z.number().finite().nonnegative();
278
+ /**
279
+ * Provider-native child task lifecycle. Identity comes only from `childId`,
280
+ * `parentChildId`, and `sourceEventId`; `raw` is opaque and bounded. A provider
281
+ * without a stable `childId` emits no `child-task` event.
282
+ */
283
+ const ChildTaskEventSchema = z
284
+ .strictObject({
285
+ type: z.literal("child-task"),
286
+ childId: stableIdSchema,
287
+ parentChildId: stableIdSchema.optional(),
288
+ status: z.enum(["started", "running", "completed", "failed", "cancelled"]),
289
+ title: boundedStringSchema.optional(),
290
+ time: z.strictObject({
291
+ started: epochMillisecondsSchema,
292
+ updated: epochMillisecondsSchema,
293
+ ended: epochMillisecondsSchema.optional(),
294
+ }),
295
+ runner: stableIdSchema.optional(),
296
+ model: stableIdSchema.optional(),
297
+ usage: ModelUsageSchema.optional(),
298
+ terminalReason: boundedStringSchema.optional(),
299
+ sourceEventId: stableIdSchema,
300
+ raw: boundedJsonRecordSchema.optional(),
301
+ })
302
+ .superRefine((event, refinement) => {
303
+ const terminal = TERMINAL_CHILD_TASK_STATUSES.has(event.status);
304
+ if (!terminal && event.time.ended !== undefined) {
305
+ refinement.addIssue({
306
+ code: "custom",
307
+ path: ["time", "ended"],
308
+ message: "only a terminal child task status may carry an end time",
309
+ });
310
+ }
311
+ if (!terminal && event.terminalReason !== undefined) {
312
+ refinement.addIssue({
313
+ code: "custom",
314
+ path: ["terminalReason"],
315
+ message: "only a terminal child task status may carry a terminal reason",
316
+ });
317
+ }
318
+ if (event.time.updated < event.time.started) {
319
+ refinement.addIssue({
320
+ code: "custom",
321
+ path: ["time", "updated"],
322
+ message: "a child task update time cannot precede its start time",
323
+ });
324
+ }
325
+ if (event.time.ended !== undefined && event.time.ended < event.time.started) {
326
+ refinement.addIssue({
327
+ code: "custom",
328
+ path: ["time", "ended"],
329
+ message: "a child task end time cannot precede its start time",
330
+ });
331
+ }
332
+ if (event.parentChildId === event.childId) {
333
+ refinement.addIssue({
334
+ code: "custom",
335
+ path: ["parentChildId"],
336
+ message: "a child task cannot be its own parent",
337
+ });
338
+ }
339
+ });
266
340
  /** Runtime validator for every member of the existing canonical event union. */
267
341
  export const CanonicalStreamEventSchema = z.discriminatedUnion("type", [
268
342
  z.strictObject({
@@ -332,6 +406,7 @@ export const CanonicalStreamEventSchema = z.discriminatedUnion("type", [
332
406
  type: z.literal("plan.submitted"),
333
407
  plan: DurablePlanSchema,
334
408
  }),
409
+ ChildTaskEventSchema,
335
410
  ]);
336
411
  export const RuntimeEventEnvelopeSchema = z.strictObject({
337
412
  runId: stableIdSchema,
@@ -1,3 +1,4 @@
1
+ import type { TokenUsage } from "./execution-types.js";
1
2
  import type { InteractionRequest } from "./interaction.js";
2
3
  import type { DurablePlan } from "./plan.js";
3
4
  import type { Part } from "./parts.js";
@@ -7,6 +8,63 @@ export type MessagePartUpdatedEvent = {
7
8
  delta?: string;
8
9
  };
9
10
  export type StreamStatus = "started" | "processing" | "completed" | "failed" | "cancelled";
11
+ export type ChildTaskStatus = "started" | "running" | "completed" | "failed" | "cancelled";
12
+ /**
13
+ * One observed update of a provider-native child task: a subagent, worker, or
14
+ * delegated task that the runner started inside the same run.
15
+ *
16
+ * Identity rules:
17
+ * - `childId` is the provider's stable identifier for the child task. Every
18
+ * update of one child repeats the same `childId`. A provider that cannot
19
+ * report a stable `childId` emits no `child-task` event.
20
+ * - `parentChildId` names the parent child task. It is absent when the parent
21
+ * is the run itself.
22
+ * - `sourceEventId` is the provider's identifier for this exact update. Two
23
+ * events with the same `sourceEventId` are the same update, so a consumer
24
+ * applies the first and ignores the rest during replay or reconnect.
25
+ * - Identity never depends on `raw`. `raw` is an opaque, bounded copy of
26
+ * provider fields that have no canonical position.
27
+ *
28
+ * Certainty rules:
29
+ * - `time.ended` and `terminalReason` are present only with a terminal status
30
+ * (`completed`, `failed`, `cancelled`).
31
+ * - `time.updated` and `time.ended` are never earlier than `time.started`.
32
+ *
33
+ * Dedupe example for a consumer that rebuilds the child tree from a replayed
34
+ * stream. Live and replayed streams produce the same tree because identity
35
+ * comes only from `childId`, `parentChildId`, and `sourceEventId`:
36
+ *
37
+ * ```ts
38
+ * const applied = new Set<string>();
39
+ * const children = new Map<string, ChildTaskEvent>();
40
+ * for (const event of events) {
41
+ * if (event.type !== "child-task") continue;
42
+ * if (applied.has(event.sourceEventId)) continue;
43
+ * applied.add(event.sourceEventId);
44
+ * children.set(event.childId, event);
45
+ * }
46
+ * ```
47
+ */
48
+ export type ChildTaskEvent = {
49
+ type: "child-task";
50
+ childId: string;
51
+ parentChildId?: string;
52
+ status: ChildTaskStatus;
53
+ title?: string;
54
+ /** Epoch milliseconds reported by the provider. */
55
+ time: {
56
+ started: number;
57
+ updated: number;
58
+ ended?: number;
59
+ };
60
+ /** Runner that executes the child, for example `claude-code`. */
61
+ runner?: string;
62
+ model?: string;
63
+ usage?: TokenUsage;
64
+ terminalReason?: string;
65
+ sourceEventId: string;
66
+ raw?: Record<string, unknown>;
67
+ };
10
68
  export type StreamEvent = MessagePartUpdatedEvent | {
11
69
  type: "tool-heartbeat";
12
70
  toolName: string;
@@ -53,4 +111,4 @@ export type StreamEvent = MessagePartUpdatedEvent | {
53
111
  } | {
54
112
  type: "plan.submitted";
55
113
  plan: DurablePlan;
56
- };
114
+ } | ChildTaskEvent;
@@ -1,7 +1,45 @@
1
+ import { z } from "zod";
1
2
  import type { Sha256Digest } from "./agent-candidate.js";
2
3
  import { sha256DigestSchema } from "./agent-candidate-schema-common.js";
3
4
  export { sha256DigestSchema };
4
- export declare const idSchema: import("zod").ZodString;
5
- export declare const jsonRecordSchema: import("zod").ZodCustom<Record<string, unknown>, Record<string, unknown>>;
5
+ export declare const idSchema: z.ZodString;
6
+ export declare const jsonRecordSchema: z.ZodCustom<Record<string, unknown>, Record<string, unknown>>;
6
7
  export declare function wireDigest(value: unknown): Sha256Digest;
7
8
  export declare function sameOptionalWireValue(left: unknown, right: unknown): boolean;
9
+ /**
10
+ * The two fields that identify one durable workspace operation.
11
+ *
12
+ * Every result and lookup answer carries them, so a caller can bind an answer
13
+ * to the exact request it asked before using the resource it names.
14
+ */
15
+ export declare const operationIdentityShape: {
16
+ idempotencyKey: z.ZodString;
17
+ requestDigest: z.ZodType<`sha256:${string}`, unknown, z.core.$ZodTypeInternals<`sha256:${string}`, unknown>>;
18
+ };
19
+ /**
20
+ * Refuse a conflict answer that names the request it was asked about.
21
+ *
22
+ * A conflict states that the key is already held by a different request. An
23
+ * answer whose `existingRequestDigest` equals the request's own digest states
24
+ * a conflict with itself, which is a replay, so the caller would retry a
25
+ * request the service already accepted.
26
+ */
27
+ export declare function refuseSelfConflict(result: {
28
+ status: string;
29
+ requestDigest: string;
30
+ existingRequestDigest?: string;
31
+ }, ctx: z.RefinementCtx): void;
32
+ /**
33
+ * Whether the resource an answer carries was produced by that same operation.
34
+ *
35
+ * A checkpoint or forked environment repeats the key and digest of the
36
+ * operation that made it. An answer carrying a resource stamped with anything
37
+ * else names a resource the caller did not ask for.
38
+ */
39
+ export declare function operationResourceIdentityMatches(operation: {
40
+ idempotencyKey: string;
41
+ requestDigest: string;
42
+ }, resource: {
43
+ idempotencyKey: string;
44
+ requestDigest: string;
45
+ }): boolean;
@@ -18,3 +18,42 @@ export function sameOptionalWireValue(left, right) {
18
18
  return left === right;
19
19
  return wireDigest(left) === wireDigest(right);
20
20
  }
21
+ /**
22
+ * The two fields that identify one durable workspace operation.
23
+ *
24
+ * Every result and lookup answer carries them, so a caller can bind an answer
25
+ * to the exact request it asked before using the resource it names.
26
+ */
27
+ export const operationIdentityShape = {
28
+ idempotencyKey: idSchema,
29
+ requestDigest: sha256DigestSchema,
30
+ };
31
+ /**
32
+ * Refuse a conflict answer that names the request it was asked about.
33
+ *
34
+ * A conflict states that the key is already held by a different request. An
35
+ * answer whose `existingRequestDigest` equals the request's own digest states
36
+ * a conflict with itself, which is a replay, so the caller would retry a
37
+ * request the service already accepted.
38
+ */
39
+ export function refuseSelfConflict(result, ctx) {
40
+ if (result.status === "conflict" &&
41
+ result.existingRequestDigest === result.requestDigest) {
42
+ ctx.addIssue({
43
+ code: "custom",
44
+ path: ["existingRequestDigest"],
45
+ message: "a conflict must identify a different existing request",
46
+ });
47
+ }
48
+ }
49
+ /**
50
+ * Whether the resource an answer carries was produced by that same operation.
51
+ *
52
+ * A checkpoint or forked environment repeats the key and digest of the
53
+ * operation that made it. An answer carrying a resource stamped with anything
54
+ * else names a resource the caller did not ask for.
55
+ */
56
+ export function operationResourceIdentityMatches(operation, resource) {
57
+ return (resource.idempotencyKey === operation.idempotencyKey &&
58
+ resource.requestDigest === operation.requestDigest);
59
+ }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { AgentExactRunControlRefSchema } from "./runtime-control.js";
3
- import { idSchema, jsonRecordSchema, sameOptionalWireValue, sha256DigestSchema, wireDigest } from "./workspace-branching-shared.js";
3
+ import { idSchema, jsonRecordSchema, operationIdentityShape, operationResourceIdentityMatches, refuseSelfConflict, sameOptionalWireValue, sha256DigestSchema, wireDigest } from "./workspace-branching-shared.js";
4
4
  import { boundedStringSchema } from "./contract-limits.js";
5
5
  const WorkspaceCheckpointMaterialSchema = z.strictObject({
6
6
  source: AgentExactRunControlRefSchema,
@@ -56,45 +56,33 @@ export const WorkspaceCheckpointRefSchema = z
56
56
  });
57
57
  }
58
58
  });
59
- const checkpointOperationBase = {
60
- idempotencyKey: idSchema,
61
- requestDigest: sha256DigestSchema,
62
- };
63
59
  export const WorkspaceCheckpointResultSchema = z.discriminatedUnion("status", [
64
60
  z.strictObject({
65
61
  status: z.enum(["created", "replayed"]),
66
- ...checkpointOperationBase,
62
+ ...operationIdentityShape,
67
63
  checkpoint: WorkspaceCheckpointRefSchema,
68
64
  }),
69
65
  z.strictObject({
70
66
  status: z.literal("conflict"),
71
- ...checkpointOperationBase,
67
+ ...operationIdentityShape,
72
68
  existingRequestDigest: sha256DigestSchema,
73
69
  }),
74
70
  z.strictObject({
75
71
  status: z.literal("unknown"),
76
- ...checkpointOperationBase,
72
+ ...operationIdentityShape,
77
73
  message: boundedStringSchema.min(1),
78
74
  retryable: z.boolean(),
79
75
  }),
80
- ]).superRefine((result, refinement) => {
76
+ ]).superRefine((result, ctx) => {
81
77
  if ((result.status === "created" || result.status === "replayed") &&
82
- (result.checkpoint.idempotencyKey !== result.idempotencyKey ||
83
- result.checkpoint.requestDigest !== result.requestDigest)) {
84
- refinement.addIssue({
78
+ !operationResourceIdentityMatches(result, result.checkpoint)) {
79
+ ctx.addIssue({
85
80
  code: "custom",
86
81
  path: ["checkpoint"],
87
82
  message: "checkpoint identity must match its operation",
88
83
  });
89
84
  }
90
- if (result.status === "conflict" &&
91
- result.existingRequestDigest === result.requestDigest) {
92
- refinement.addIssue({
93
- code: "custom",
94
- path: ["existingRequestDigest"],
95
- message: "a conflict must identify a different existing request",
96
- });
97
- }
85
+ refuseSelfConflict(result, ctx);
98
86
  });
99
87
  export const WorkspaceOperationLookupRequestSchema = z.strictObject({
100
88
  idempotencyKey: idSchema,
@@ -103,42 +91,34 @@ export const WorkspaceOperationLookupRequestSchema = z.strictObject({
103
91
  export const WorkspaceCheckpointLookupResultSchema = z.discriminatedUnion("status", [
104
92
  z.strictObject({
105
93
  status: z.literal("found"),
106
- ...checkpointOperationBase,
94
+ ...operationIdentityShape,
107
95
  checkpoint: WorkspaceCheckpointRefSchema,
108
96
  }),
109
97
  z.strictObject({
110
98
  status: z.literal("not_found"),
111
- ...checkpointOperationBase,
99
+ ...operationIdentityShape,
112
100
  }),
113
101
  z.strictObject({
114
102
  status: z.literal("conflict"),
115
- ...checkpointOperationBase,
103
+ ...operationIdentityShape,
116
104
  existingRequestDigest: sha256DigestSchema,
117
105
  }),
118
106
  z.strictObject({
119
107
  status: z.literal("unknown"),
120
- ...checkpointOperationBase,
108
+ ...operationIdentityShape,
121
109
  message: boundedStringSchema.min(1),
122
110
  retryable: z.boolean(),
123
111
  }),
124
- ]).superRefine((result, refinement) => {
112
+ ]).superRefine((result, ctx) => {
125
113
  if (result.status === "found" &&
126
- (result.checkpoint.idempotencyKey !== result.idempotencyKey ||
127
- result.checkpoint.requestDigest !== result.requestDigest)) {
128
- refinement.addIssue({
114
+ !operationResourceIdentityMatches(result, result.checkpoint)) {
115
+ ctx.addIssue({
129
116
  code: "custom",
130
117
  path: ["checkpoint"],
131
118
  message: "checkpoint identity must match its lookup operation",
132
119
  });
133
120
  }
134
- if (result.status === "conflict" &&
135
- result.existingRequestDigest === result.requestDigest) {
136
- refinement.addIssue({
137
- code: "custom",
138
- path: ["existingRequestDigest"],
139
- message: "a conflict must identify a different existing request",
140
- });
141
- }
121
+ refuseSelfConflict(result, ctx);
142
122
  });
143
123
  /** Bind a checkpoint result to the exact request before using the resource. */
144
124
  export function workspaceCheckpointResultMatchesRequest(request, result) {
@@ -159,10 +139,8 @@ export function workspaceCheckpointResultMatchesRequest(request, result) {
159
139
  return false;
160
140
  }
161
141
  function checkpointResultMatchesParsed(request, result) {
162
- return (result.idempotencyKey === request.idempotencyKey &&
163
- result.requestDigest === request.requestDigest &&
164
- result.checkpoint.idempotencyKey === request.idempotencyKey &&
165
- result.checkpoint.requestDigest === request.requestDigest &&
142
+ return (operationResourceIdentityMatches(request, result) &&
143
+ operationResourceIdentityMatches(request, result.checkpoint) &&
166
144
  wireDigest(result.checkpoint.source) === wireDigest(request.source) &&
167
145
  sameOptionalWireValue(result.checkpoint.metadata, request.metadata) &&
168
146
  result.checkpoint.provider === request.source.provider);
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { AgentExactRunControlRefSchema } from "./runtime-control.js";
3
- import { idSchema, jsonRecordSchema, sameOptionalWireValue, sha256DigestSchema, wireDigest } from "./workspace-branching-shared.js";
3
+ import { idSchema, jsonRecordSchema, operationIdentityShape, operationResourceIdentityMatches, refuseSelfConflict, sameOptionalWireValue, sha256DigestSchema, wireDigest } from "./workspace-branching-shared.js";
4
4
  import { WorkspaceCheckpointRefSchema } from "./workspace-checkpoint.js";
5
5
  import { ConfidentialAttestationSchema, ConfidentialExecutionRequestSchema, confidentialExecutionVerified, } from "./workspace-confidentiality.js";
6
6
  import { boundedStringSchema } from "./contract-limits.js";
@@ -155,82 +155,62 @@ export function forkedEnvironmentConfidentialityVerified(request, environment, v
155
155
  ...(verifyProviderAttestation ? { verifyProviderAttestation } : {}),
156
156
  });
157
157
  }
158
- const forkOperationBase = {
159
- idempotencyKey: idSchema,
160
- requestDigest: sha256DigestSchema,
161
- };
162
158
  export const WorkspaceForkResultSchema = z.discriminatedUnion("status", [
163
159
  z.strictObject({
164
160
  status: z.enum(["created", "replayed"]),
165
- ...forkOperationBase,
161
+ ...operationIdentityShape,
166
162
  environment: ForkedEnvironmentRefSchema,
167
163
  }),
168
164
  z.strictObject({
169
165
  status: z.literal("conflict"),
170
- ...forkOperationBase,
166
+ ...operationIdentityShape,
171
167
  existingRequestDigest: sha256DigestSchema,
172
168
  }),
173
169
  z.strictObject({
174
170
  status: z.literal("unknown"),
175
- ...forkOperationBase,
171
+ ...operationIdentityShape,
176
172
  message: boundedStringSchema.min(1),
177
173
  retryable: z.boolean(),
178
174
  }),
179
- ]).superRefine((result, refinement) => {
175
+ ]).superRefine((result, ctx) => {
180
176
  if ((result.status === "created" || result.status === "replayed") &&
181
- (result.environment.idempotencyKey !== result.idempotencyKey ||
182
- result.environment.requestDigest !== result.requestDigest)) {
183
- refinement.addIssue({
177
+ !operationResourceIdentityMatches(result, result.environment)) {
178
+ ctx.addIssue({
184
179
  code: "custom",
185
180
  path: ["environment"],
186
181
  message: "forked environment identity must match its operation",
187
182
  });
188
183
  }
189
- if (result.status === "conflict" &&
190
- result.existingRequestDigest === result.requestDigest) {
191
- refinement.addIssue({
192
- code: "custom",
193
- path: ["existingRequestDigest"],
194
- message: "a conflict must identify a different existing request",
195
- });
196
- }
184
+ refuseSelfConflict(result, ctx);
197
185
  });
198
186
  export const WorkspaceForkLookupResultSchema = z.discriminatedUnion("status", [
199
187
  z.strictObject({
200
188
  status: z.literal("found"),
201
- ...forkOperationBase,
189
+ ...operationIdentityShape,
202
190
  environment: ForkedEnvironmentRefSchema,
203
191
  }),
204
- z.strictObject({ status: z.literal("not_found"), ...forkOperationBase }),
192
+ z.strictObject({ status: z.literal("not_found"), ...operationIdentityShape }),
205
193
  z.strictObject({
206
194
  status: z.literal("conflict"),
207
- ...forkOperationBase,
195
+ ...operationIdentityShape,
208
196
  existingRequestDigest: sha256DigestSchema,
209
197
  }),
210
198
  z.strictObject({
211
199
  status: z.literal("unknown"),
212
- ...forkOperationBase,
200
+ ...operationIdentityShape,
213
201
  message: boundedStringSchema.min(1),
214
202
  retryable: z.boolean(),
215
203
  }),
216
- ]).superRefine((result, refinement) => {
204
+ ]).superRefine((result, ctx) => {
217
205
  if (result.status === "found" &&
218
- (result.environment.idempotencyKey !== result.idempotencyKey ||
219
- result.environment.requestDigest !== result.requestDigest)) {
220
- refinement.addIssue({
206
+ !operationResourceIdentityMatches(result, result.environment)) {
207
+ ctx.addIssue({
221
208
  code: "custom",
222
209
  path: ["environment"],
223
210
  message: "forked environment identity must match its lookup operation",
224
211
  });
225
212
  }
226
- if (result.status === "conflict" &&
227
- result.existingRequestDigest === result.requestDigest) {
228
- refinement.addIssue({
229
- code: "custom",
230
- path: ["existingRequestDigest"],
231
- message: "a conflict must identify a different existing request",
232
- });
233
- }
213
+ refuseSelfConflict(result, ctx);
234
214
  });
235
215
  export function workspaceForkResultMatchesRequest(request, result) {
236
216
  const parsedRequest = WorkspaceForkRequestSchema.safeParse(request);
@@ -251,10 +231,8 @@ export function workspaceForkResultMatchesRequest(request, result) {
251
231
  }
252
232
  function forkResultMatchesParsed(request, result) {
253
233
  const source = request.checkpoint.source;
254
- return (result.idempotencyKey === request.idempotencyKey &&
255
- result.requestDigest === request.requestDigest &&
256
- result.environment.idempotencyKey === request.idempotencyKey &&
257
- result.environment.requestDigest === request.requestDigest &&
234
+ return (operationResourceIdentityMatches(request, result) &&
235
+ operationResourceIdentityMatches(request, result.environment) &&
258
236
  result.environment.sourceCheckpointId === request.checkpoint.checkpointId &&
259
237
  result.environment.provider === request.checkpoint.provider &&
260
238
  result.environment.sourceEnvironmentId === source.environmentId &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-interface",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",