@tangle-network/agent-provider-tangle 0.6.2 → 0.7.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.
@@ -1,17 +1,45 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { AgentTurnInputSchema } from "@tangle-network/agent-interface";
2
- import { environmentEventFromSandboxEvent } from "./tangle-events.js";
3
+ import { AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, agentRunCancellationAcknowledgementMatchesRequest, } from "@tangle-network/agent-interface";
4
+ import { environmentEventFromSandboxEvent, isSandboxConnectionMarker, sandboxEventIdentity, } from "./tangle-events.js";
3
5
  import { agentTurnResultFromPromptRecord, promptFromTurnInput, promptOptionsFromTurnInput, validatedSandboxPromptResult, } from "./tangle-prompt.js";
4
6
  import { retainedSessionControlRef, resolveRetainedSessionControlRef, sameRunControlRef, sessionPromptExecutionId, } from "./tangle-session-control.js";
5
- import { sessionStatusFromUnknown } from "./tangle-environment-values.js";
7
+ import { executionBoundSessionStatus, sessionStatusFromUnknown, } from "./tangle-environment-values.js";
6
8
  import { awaitWithSignal, boundedIdentifier, } from "./tangle-contract-safety.js";
7
9
  import { assertOptionKeys } from "./tangle-environment-validation.js";
8
10
  import { hasReplayPayload, interruptExecutionAfterAbort, sessionPromptRequestDigest, } from "./tangle-environment-control.js";
9
- export function sandboxSessionAsAgentSession(session, controlRef, provider, environmentId) {
10
- let activeControlRef = controlRef;
11
+ export function sandboxSessionAsAgentSession(session, controlRef, provider, environmentId, dispatch, exactExecutionEvents) {
12
+ let activeControlRef = controlRef
13
+ ? resolveRetainedSessionControlRef(controlRef, session.id, provider, environmentId)
14
+ : undefined;
15
+ let promptInFlight = false;
16
+ const cancelRunMethod = session.cancelRun;
17
+ const cancelRun = typeof cancelRunMethod === "function"
18
+ ? async (request, options) => {
19
+ assertOptionKeys(options, ["signal"], "Tangle exact session cancellation");
20
+ const exactRequest = AgentRunCancellationRequestSchema.parse(request);
21
+ const expectedRun = activeControlRef;
22
+ if (expectedRun === undefined) {
23
+ throw new Error("Tangle exact session cancellation requires an exact run control reference");
24
+ }
25
+ if (!sameRunControlRef(exactRequest.run, expectedRun)) {
26
+ throw new Error("Tangle exact session cancellation targets another run, session, execution, or request");
27
+ }
28
+ options?.signal?.throwIfAborted();
29
+ const acknowledgement = await awaitWithSignal(cancelRunMethod.call(session, exactRequest, options?.signal ? { signal: options.signal } : undefined), options?.signal);
30
+ options?.signal?.throwIfAborted();
31
+ const exactAcknowledgement = AgentRunCancellationAcknowledgementSchema.parse(acknowledgement);
32
+ if (!agentRunCancellationAcknowledgementMatchesRequest(exactRequest, exactAcknowledgement) ||
33
+ !sameRunControlRef(exactAcknowledgement.run, expectedRun)) {
34
+ throw new Error("Tangle exact session cancellation returned an acknowledgement for another run, session, execution, or request");
35
+ }
36
+ return exactAcknowledgement;
37
+ }
38
+ : undefined;
11
39
  return {
12
40
  id: session.id,
13
41
  get controlRef() {
14
- return activeControlRef;
42
+ return activeControlRef === undefined ? undefined : { ...activeControlRef };
15
43
  },
16
44
  async status(options) {
17
45
  assertOptionKeys(options, ["signal"], "Tangle session status");
@@ -19,6 +47,12 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
19
47
  options?.signal?.throwIfAborted();
20
48
  if (!status)
21
49
  return null;
50
+ const expectedExecutionId = activeControlRef?.executionId;
51
+ // Sandbox reports session-wide status. With an exact control reference,
52
+ // the answer is only valid when the payload binds to that execution.
53
+ if (expectedExecutionId !== undefined) {
54
+ return executionBoundSessionStatus(status, expectedExecutionId);
55
+ }
22
56
  return sessionStatusFromUnknown(status.status);
23
57
  },
24
58
  async *events(options) {
@@ -36,11 +70,20 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
36
70
  }
37
71
  options?.signal?.throwIfAborted();
38
72
  const seenEventIds = new Set();
39
- const iterator = session.events({
40
- ...(options?.since !== undefined ? { since: options.since } : {}),
41
- ...(executionId !== undefined ? { executionId } : {}),
42
- ...(options?.signal ? { signal: options.signal } : {}),
43
- })[Symbol.asyncIterator]();
73
+ const useExactExecutionStream = exactExecutionEvents !== undefined && executionId !== undefined;
74
+ const iterator = (useExactExecutionStream
75
+ ? exactExecutionEvents({
76
+ sessionId: session.id,
77
+ executionId,
78
+ ...(options?.since !== undefined ? { since: options.since } : {}),
79
+ ...(options?.signal ? { signal: options.signal } : {}),
80
+ ...(activeControlRef ? { controlRef: activeControlRef } : {}),
81
+ })
82
+ : session.events({
83
+ ...(options?.since !== undefined ? { since: options.since } : {}),
84
+ ...(executionId !== undefined ? { executionId } : {}),
85
+ ...(options?.signal ? { signal: options.signal } : {}),
86
+ }))[Symbol.asyncIterator]();
44
87
  let completed = false;
45
88
  try {
46
89
  while (true) {
@@ -50,16 +93,28 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
50
93
  break;
51
94
  }
52
95
  options?.signal?.throwIfAborted();
53
- if (options?.since !== undefined && next.value.id === options.since)
96
+ if (isSandboxConnectionMarker(next.value)) {
97
+ const markerIdentity = sandboxEventIdentity(next.value);
98
+ if (executionId !== undefined &&
99
+ markerIdentity.executionId !== undefined &&
100
+ markerIdentity.executionId !== executionId) {
101
+ throw new Error("Tangle exact session connection identified a different executionId");
102
+ }
103
+ if (markerIdentity.sessionId !== undefined &&
104
+ markerIdentity.sessionId !== session.id) {
105
+ throw new Error("Tangle exact session connection identified a different sessionId");
106
+ }
54
107
  continue;
108
+ }
55
109
  const converted = environmentEventFromSandboxEvent(next.value, {
56
110
  executionId,
57
111
  sessionId: session.id,
112
+ ...(useExactExecutionStream ? { streamBound: true } : {}),
58
113
  });
59
114
  if (converted.id === undefined)
60
115
  throw new Error("Tangle session event arrived without a stable id");
61
116
  if (seenEventIds.has(converted.id))
62
- throw new Error(`Tangle session replay repeated event id ${converted.id}`);
117
+ continue;
63
118
  seenEventIds.add(converted.id);
64
119
  options?.signal?.throwIfAborted();
65
120
  yield converted;
@@ -82,63 +137,160 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
82
137
  const resultRecord = validatedSandboxPromptResult(result);
83
138
  if (resultRecord.executionId !== expectedExecutionId)
84
139
  throw new Error("Tangle session result did not confirm its exact executionId");
85
- return agentTurnResultFromPromptRecord(resultRecord, { sessionId: session.id });
140
+ return agentTurnResultFromPromptRecord(resultRecord, {
141
+ sessionId: session.id,
142
+ controlRef: activeControlRef,
143
+ });
86
144
  },
87
145
  async prompt(input) {
88
146
  AgentTurnInputSchema.parse(input);
89
147
  input.signal?.throwIfAborted();
90
- if (input.sessionId !== undefined && input.sessionId !== session.id)
91
- throw new Error("Tangle sessionId conflicts with this session");
92
- const requestedControlRef = resolveRetainedSessionControlRef(input.controlRef, session.id, provider, environmentId);
93
- if (activeControlRef && requestedControlRef && !sameRunControlRef(activeControlRef, requestedControlRef))
94
- throw new Error("Tangle prompt control reference conflicts with this session");
95
- const sourceControlRef = requestedControlRef ?? activeControlRef;
96
- const replay = input.lastEventId !== undefined;
97
- if (replay && sourceControlRef?.executionId !== undefined && input.executionId !== undefined && input.executionId !== sourceControlRef.executionId)
98
- throw new Error("Tangle replay executionId conflicts with the control reference");
99
- if (replay && sourceControlRef?.requestDigest === undefined) {
100
- throw new Error("Tangle replay requires an exact request digest from its control reference");
148
+ if (promptInFlight) {
149
+ throw new Error("Tangle session already has a prompt in flight");
101
150
  }
102
- const executionId = replay ? input.executionId ?? sourceControlRef?.executionId : input.executionId ?? sessionPromptExecutionId(provider, environmentId, session.id, input.turnId);
103
- if (executionId === undefined)
104
- throw new Error("Tangle session replay requires the exact executionId from its control reference");
105
- const computedRequestDigest = sessionPromptRequestDigest(input, provider, environmentId, session.id, executionId);
106
- const sameExecution = sourceControlRef?.executionId === executionId;
107
- if (sourceControlRef?.requestDigest !== undefined &&
108
- sameExecution &&
109
- (!replay || hasReplayPayload(input)) &&
110
- sourceControlRef.requestDigest !== computedRequestDigest) {
111
- throw new Error("Tangle prompt request digest conflicts with the control reference");
112
- }
113
- const requestDigest = replay || sameExecution
114
- ? sourceControlRef?.requestDigest
115
- : computedRequestDigest;
116
- if (requestDigest === undefined) {
117
- throw new Error("Tangle prompt could not establish an exact request digest");
118
- }
119
- const promptInput = replay
120
- ? { ...input, sessionId: session.id, executionId, controlRef: sourceControlRef }
121
- : { ...input, sessionId: session.id, executionId, controlRef: undefined };
151
+ promptInFlight = true;
122
152
  try {
123
- const result = await awaitWithSignal(session.prompt(promptFromTurnInput(input), promptOptionsFromTurnInput(promptInput, { provider, environmentId, sessionId: session.id })), input.signal);
124
- input.signal?.throwIfAborted();
125
- const resultRecord = validatedSandboxPromptResult(result);
126
- if (resultRecord.executionId !== executionId) {
127
- void interruptExecutionAfterAbort(session, session.id, executionId);
128
- throw new Error("Tangle session prompt did not confirm its exact executionId");
153
+ if (input.sessionId !== undefined && input.sessionId !== session.id) {
154
+ throw new Error("Tangle sessionId conflicts with this session");
129
155
  }
130
- activeControlRef = retainedSessionControlRef(session.id, executionId, provider, environmentId, requestDigest);
131
- return agentTurnResultFromPromptRecord(resultRecord, {
132
- sessionId: session.id,
133
- ...(input.contextTransfer ? { contextTransferRequest: input.contextTransfer } : {}),
134
- ...(input.contextTransfer ? { contextTransferRequested: true } : {}),
156
+ const requestedControlRef = resolveRetainedSessionControlRef(input.controlRef, session.id, provider, environmentId);
157
+ if (activeControlRef &&
158
+ requestedControlRef &&
159
+ !sameRunControlRef(activeControlRef, requestedControlRef)) {
160
+ throw new Error("Tangle prompt control reference conflicts with this session");
161
+ }
162
+ if (requestedControlRef?.executionId !== undefined &&
163
+ input.executionId !== undefined &&
164
+ requestedControlRef.executionId !== input.executionId) {
165
+ throw new Error("Tangle executionId conflicts with the control reference");
166
+ }
167
+ if (input.detach === true && input.lastEventId === undefined) {
168
+ if (dispatch === undefined) {
169
+ throw new Error("Tangle detached session prompt requires the sandbox dispatch primitive");
170
+ }
171
+ const detachedInput = {
172
+ ...input,
173
+ signal: undefined,
174
+ controlRef: undefined,
175
+ sessionId: session.id,
176
+ detach: true,
177
+ ...(input.turnId === undefined ? { turnId: randomUUID() } : {}),
178
+ };
179
+ const reference = await dispatch(detachedInput);
180
+ const nextControlRef = resolveRetainedSessionControlRef(reference.controlRef, session.id, provider, environmentId);
181
+ if (nextControlRef === undefined) {
182
+ throw new Error("Tangle detached session dispatch returned no exact control reference");
183
+ }
184
+ // The admission receipt is the durability boundary. Store it before
185
+ // waiting for the result so an aborted caller can reconnect later.
186
+ activeControlRef = nextControlRef;
187
+ const result = await awaitWithSignal(session.result({
188
+ executionId: nextControlRef.executionId,
189
+ ...(input.signal ? { signal: input.signal } : {}),
190
+ }), input.signal);
191
+ input.signal?.throwIfAborted();
192
+ const resultRecord = validatedSandboxPromptResult(result);
193
+ if (resultRecord.executionId !== nextControlRef.executionId) {
194
+ throw new Error("Tangle detached session prompt did not confirm its exact executionId");
195
+ }
196
+ return agentTurnResultFromPromptRecord(resultRecord, {
197
+ sessionId: session.id,
198
+ controlRef: nextControlRef,
199
+ });
200
+ }
201
+ const sourceControlRef = requestedControlRef ?? activeControlRef;
202
+ const replay = input.lastEventId !== undefined;
203
+ if (replay &&
204
+ sourceControlRef?.executionId !== undefined &&
205
+ input.executionId !== undefined &&
206
+ input.executionId !== sourceControlRef.executionId) {
207
+ throw new Error("Tangle replay executionId conflicts with the control reference");
208
+ }
209
+ if (replay && sourceControlRef?.requestDigest === undefined) {
210
+ throw new Error("Tangle replay requires an exact request digest from its control reference");
211
+ }
212
+ const requestedExecutionId = input.executionId ?? requestedControlRef?.executionId;
213
+ const nonce = !replay &&
214
+ input.turnId === undefined &&
215
+ requestedExecutionId === undefined &&
216
+ requestedControlRef === undefined
217
+ ? randomUUID()
218
+ : undefined;
219
+ const baseRequestDigest = sessionPromptRequestDigest(input, provider, environmentId, session.id, nonce === undefined ? {} : { nonce });
220
+ const executionId = replay
221
+ ? input.executionId ?? sourceControlRef?.executionId
222
+ : requestedExecutionId ?? sessionPromptExecutionId(baseRequestDigest);
223
+ if (executionId === undefined) {
224
+ throw new Error("Tangle session replay requires the exact executionId from its control reference");
225
+ }
226
+ const explicitRequestDigest = sessionPromptRequestDigest(input, provider, environmentId, session.id, {
227
+ executionId,
228
+ ...(nonce === undefined ? {} : { nonce }),
135
229
  });
136
- }
137
- catch (error) {
138
- if (input.signal?.aborted) {
139
- void interruptExecutionAfterAbort(session, session.id, executionId);
230
+ if (!replay &&
231
+ requestedControlRef?.requestDigest !== undefined &&
232
+ requestedControlRef.requestDigest !== explicitRequestDigest) {
233
+ throw new Error("Tangle prompt request digest conflicts with the control reference");
234
+ }
235
+ if (replay &&
236
+ hasReplayPayload(input) &&
237
+ sourceControlRef?.requestDigest !== explicitRequestDigest) {
238
+ throw new Error("Tangle prompt request digest conflicts with the control reference");
239
+ }
240
+ const requestDigest = replay
241
+ ? sourceControlRef?.requestDigest
242
+ : requestedControlRef?.requestDigest ?? explicitRequestDigest;
243
+ if (requestDigest === undefined) {
244
+ throw new Error("Tangle prompt could not establish an exact request digest");
245
+ }
246
+ const targetControlRef = retainedSessionControlRef(session.id, executionId, provider, environmentId, requestDigest, requestedControlRef?.runId);
247
+ const promptInput = replay
248
+ ? {
249
+ ...input,
250
+ sessionId: session.id,
251
+ executionId,
252
+ controlRef: sourceControlRef,
253
+ }
254
+ : {
255
+ ...input,
256
+ sessionId: session.id,
257
+ executionId,
258
+ controlRef: targetControlRef,
259
+ };
260
+ try {
261
+ const result = await awaitWithSignal(session.prompt(promptFromTurnInput(input), promptOptionsFromTurnInput(promptInput, {
262
+ provider,
263
+ environmentId,
264
+ sessionId: session.id,
265
+ })), input.signal);
266
+ input.signal?.throwIfAborted();
267
+ const resultRecord = validatedSandboxPromptResult(result);
268
+ if (resultRecord.executionId !== executionId) {
269
+ void interruptExecutionAfterAbort(session, session.id, executionId);
270
+ throw new Error("Tangle session prompt did not confirm its exact executionId");
271
+ }
272
+ const nextControlRef = targetControlRef;
273
+ activeControlRef = nextControlRef;
274
+ return agentTurnResultFromPromptRecord(resultRecord, {
275
+ sessionId: session.id,
276
+ controlRef: nextControlRef,
277
+ ...(input.contextTransfer
278
+ ? { contextTransferRequest: input.contextTransfer }
279
+ : {}),
280
+ ...(input.contextTransfer
281
+ ? { contextTransferRequested: true }
282
+ : {}),
283
+ });
284
+ }
285
+ catch (error) {
286
+ if (input.signal?.aborted && input.detach !== true) {
287
+ void interruptExecutionAfterAbort(session, session.id, executionId);
288
+ }
289
+ throw error;
140
290
  }
141
- throw error;
291
+ }
292
+ finally {
293
+ promptInFlight = false;
142
294
  }
143
295
  },
144
296
  async cancel(options) {
@@ -152,5 +304,6 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
152
304
  if (result.cancelled !== true)
153
305
  throw new Error("Tangle sandbox did not confirm cancellation");
154
306
  },
307
+ ...(cancelRun ? { cancelRun } : {}),
155
308
  };
156
309
  }
@@ -1,11 +1,4 @@
1
- import type { CheckpointRef, CheckpointRequest, ExecRequest, ForkRequest } from "@tangle-network/agent-interface/environment-provider";
1
+ import type { ExecRequest } from "@tangle-network/agent-interface/environment-provider";
2
2
  export declare function assertOptionKeys(value: object | undefined, allowed: readonly string[], label: string): void;
3
3
  export declare function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown>;
4
4
  export declare function assertExecOptions(options: ExecRequest | undefined): void;
5
- export declare function assertCheckpointOptions(options: (CheckpointRequest & {
6
- signal?: AbortSignal;
7
- }) | undefined): void;
8
- export declare function assertCheckpointRef(checkpoint: CheckpointRef): void;
9
- export declare function assertForkOptions(options: (ForkRequest & {
10
- signal?: AbortSignal;
11
- }) | undefined): void;
@@ -36,28 +36,3 @@ export function assertExecOptions(options) {
36
36
  throw new Error("Tangle exec timeoutMs must be a positive safe integer");
37
37
  }
38
38
  }
39
- export function assertCheckpointOptions(options) {
40
- assertOptionKeys(options, ["name", "metadata", "signal"], "Tangle checkpoint");
41
- if (options?.name !== undefined)
42
- boundedString(options.name, "Tangle checkpoint name");
43
- if (options?.metadata !== undefined)
44
- assertRecord(options.metadata, "Tangle checkpoint metadata");
45
- }
46
- export function assertCheckpointRef(checkpoint) {
47
- if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) {
48
- throw new Error("Tangle fork checkpoint must be an object");
49
- }
50
- assertOptionKeys(checkpoint, ["id", "provider", "metadata"], "Tangle fork checkpoint");
51
- boundedIdentifier(checkpoint.id, "Tangle checkpoint id");
52
- if (checkpoint.provider !== undefined)
53
- boundedIdentifier(checkpoint.provider, "Tangle checkpoint provider");
54
- if (checkpoint.metadata !== undefined)
55
- assertRecord(checkpoint.metadata, "Tangle checkpoint metadata");
56
- }
57
- export function assertForkOptions(options) {
58
- assertOptionKeys(options, ["name", "metadata", "signal"], "Tangle fork");
59
- if (options?.name !== undefined)
60
- boundedString(options.name, "Tangle fork name");
61
- if (options?.metadata !== undefined)
62
- assertRecord(options.metadata, "Tangle fork metadata");
63
- }
@@ -2,7 +2,17 @@ import type { AgentEnvironmentStatus, AgentSessionStatus, PlacementInfo } from "
2
2
  import type { SandboxInstanceLike } from "./tangle-types.js";
3
3
  export declare function nonEmptyString(value: unknown): string | undefined;
4
4
  export declare function optionalNonEmptyString(value: unknown, label: string): string | undefined;
5
- export declare function checkpointIdFromResult(result: unknown): string;
6
5
  export declare function placementInfoFromLoopPlacement(placement: unknown, box: SandboxInstanceLike): PlacementInfo;
7
6
  export declare function statusFromUnknown(status: unknown): AgentEnvironmentStatus;
8
7
  export declare function sessionStatusFromUnknown(status: unknown): AgentSessionStatus;
8
+ /**
9
+ * Bind a session-wide status payload to one exact execution.
10
+ *
11
+ * The Sandbox status endpoint reports the whole session. Its execution
12
+ * identity fields — activeExecutionId, latestExecutionId, runControlRef,
13
+ * failureReason.executionId — are the only proof of which execution the
14
+ * lifecycle state describes. A payload that names a different execution, or
15
+ * none, must not be attributed to the exact run, so the result degrades to
16
+ * "unknown" instead of fabricating an execution-scoped answer.
17
+ */
18
+ export declare function executionBoundSessionStatus(payload: unknown, executionId: string): AgentSessionStatus;
@@ -1,4 +1,3 @@
1
- import { assertBoundedJson } from "./tangle-contract-safety.js";
2
1
  const MAX_IDENTIFIER_LENGTH = 512;
3
2
  export function nonEmptyString(value) {
4
3
  return typeof value === "string" &&
@@ -19,18 +18,6 @@ export function optionalNonEmptyString(value, label) {
19
18
  }
20
19
  return value;
21
20
  }
22
- export function checkpointIdFromResult(result) {
23
- assertBoundedJson(result);
24
- const record = result && typeof result === "object" ? result : {};
25
- const id = record.checkpointId ?? record.id;
26
- if (typeof id !== "string" ||
27
- id.length === 0 ||
28
- id.length > MAX_IDENTIFIER_LENGTH ||
29
- id.trim() !== id) {
30
- throw new Error("sandbox checkpoint returned no checkpoint id");
31
- }
32
- return id;
33
- }
34
21
  export function placementInfoFromLoopPlacement(placement, box) {
35
22
  if (!placement || typeof placement !== "object") {
36
23
  return { kind: "sandbox", sandboxId: boundedId(box.id, "sandbox id") };
@@ -75,6 +62,10 @@ export function statusFromUnknown(status) {
75
62
  return status;
76
63
  if (status === "completed" || status === "cancelled")
77
64
  return "stopped";
65
+ // A queued session is admitted work that has not started: pending, not
66
+ // unknown — "unknown" also means "not attributable" in exact-status binding.
67
+ if (status === "queued")
68
+ return "pending";
78
69
  return "unknown";
79
70
  }
80
71
  export function sessionStatusFromUnknown(status) {
@@ -82,3 +73,47 @@ export function sessionStatusFromUnknown(status) {
82
73
  return status;
83
74
  return statusFromUnknown(status);
84
75
  }
76
+ /**
77
+ * Bind a session-wide status payload to one exact execution.
78
+ *
79
+ * The Sandbox status endpoint reports the whole session. Its execution
80
+ * identity fields — activeExecutionId, latestExecutionId, runControlRef,
81
+ * failureReason.executionId — are the only proof of which execution the
82
+ * lifecycle state describes. A payload that names a different execution, or
83
+ * none, must not be attributed to the exact run, so the result degrades to
84
+ * "unknown" instead of fabricating an execution-scoped answer.
85
+ */
86
+ export function executionBoundSessionStatus(payload, executionId) {
87
+ const record = payload && typeof payload === "object" && !Array.isArray(payload)
88
+ ? payload
89
+ : {};
90
+ const sessionStatus = sessionStatusFromUnknown(record.status);
91
+ const active = nonEmptyString(record.activeExecutionId);
92
+ const latest = nonEmptyString(record.latestExecutionId);
93
+ const admitted = record.runControlRef && typeof record.runControlRef === "object"
94
+ ? nonEmptyString(record.runControlRef.executionId)
95
+ : undefined;
96
+ const failed = record.failureReason && typeof record.failureReason === "object"
97
+ ? nonEmptyString(record.failureReason.executionId)
98
+ : undefined;
99
+ // An attributed failure outranks liveness: failureReason.executionId is the
100
+ // only field that names the execution its state describes, so it decides
101
+ // the failed case in both directions. Naming this execution proves the
102
+ // failure; naming another execution proves the failed state is not this
103
+ // run's, even when a contradictory payload also marks this execution live.
104
+ if (sessionStatus === "failed" && failed !== undefined) {
105
+ return failed === executionId ? "failed" : "unknown";
106
+ }
107
+ // A live execution owns the session's current state; any other live
108
+ // execution means this payload says nothing exact about the bound run.
109
+ if (active !== undefined) {
110
+ return active === executionId ? sessionStatus : "unknown";
111
+ }
112
+ // With nothing live, the state belongs to the newest execution. Trust the
113
+ // admitted-run reference only when no newer execution is named.
114
+ if (latest === executionId)
115
+ return sessionStatus;
116
+ if (latest === undefined && admitted === executionId)
117
+ return sessionStatus;
118
+ return "unknown";
119
+ }
@@ -2,11 +2,11 @@ import { AgentTurnInputSchema } from "@tangle-network/agent-interface";
2
2
  import { environmentEventFromSandboxEvent } from "./tangle-events.js";
3
3
  import { executionIdFromTurnInput, promptFromTurnInput, promptOptionsFromTurnInput, } from "./tangle-prompt.js";
4
4
  import { resolveRetainedSessionControlRef } from "./tangle-session-control.js";
5
- import { checkpointIdFromResult, placementInfoFromLoopPlacement, statusFromUnknown, } from "./tangle-environment-values.js";
5
+ import { placementInfoFromLoopPlacement, statusFromUnknown, } from "./tangle-environment-values.js";
6
6
  import { execResultFromSandboxExecResult } from "./tangle-result-values.js";
7
7
  import { capabilitiesForSandbox, sandboxCapabilitySupport } from "./tangle-capabilities.js";
8
- import { attachCleanupHandle, awaitWithSignal, assertBoundedJson, boundedIdentifier, boundedString, } from "./tangle-contract-safety.js";
9
- import { assertCheckpointOptions, assertCheckpointRef, assertExecOptions, assertForkOptions, assertOptionKeys, } from "./tangle-environment-validation.js";
8
+ import { awaitWithSignal, assertBoundedJson, boundedIdentifier, boundedString, } from "./tangle-contract-safety.js";
9
+ import { assertExecOptions, assertOptionKeys, } from "./tangle-environment-validation.js";
10
10
  import { interruptExecutionAfterAbort, } from "./tangle-environment-control.js";
11
11
  import { dispatchEnvironmentRun } from "./tangle-environment-dispatch.js";
12
12
  import { sandboxSessionAsAgentSession } from "./tangle-environment-session.js";
@@ -21,6 +21,18 @@ export function sandboxInstanceAsEnvironment(box, providerName, client, declared
21
21
  }
22
22
  const support = sandboxCapabilitySupport(box, client);
23
23
  const capabilities = capabilitiesForSandbox(declaredCapabilities, support);
24
+ const dispatch = capabilities.streaming.detach && box.dispatchPrompt
25
+ ? dispatchEnvironmentRun(box, providerName, environmentId)
26
+ : undefined;
27
+ const exactExecutionEvents = (options) => box.streamPrompt("", {
28
+ sessionId: options.sessionId,
29
+ executionId: options.executionId,
30
+ // The execution replay endpoint owns stable event IDs. A missing cursor
31
+ // starts at the beginning so the caller can journal a complete run.
32
+ lastEventId: options.since ?? "0",
33
+ ...(options.signal ? { signal: options.signal } : {}),
34
+ ...(options.controlRef ? { runControlRef: options.controlRef } : {}),
35
+ });
24
36
  return {
25
37
  id: environmentId,
26
38
  provider: providerName,
@@ -51,6 +63,9 @@ export function sandboxInstanceAsEnvironment(box, providerName, client, declared
51
63
  const converted = environmentEventFromSandboxEvent(next.value, {
52
64
  executionId: expectedExecutionId,
53
65
  sessionId: expectedSessionId,
66
+ ...(expectedExecutionId !== undefined || expectedSessionId !== undefined
67
+ ? { streamBound: true }
68
+ : {}),
54
69
  });
55
70
  input.signal?.throwIfAborted();
56
71
  yield converted;
@@ -58,6 +73,7 @@ export function sandboxInstanceAsEnvironment(box, providerName, client, declared
58
73
  }
59
74
  catch (error) {
60
75
  if (input.signal?.aborted &&
76
+ input.detach !== true &&
61
77
  expectedSessionId !== undefined &&
62
78
  expectedExecutionId !== undefined) {
63
79
  void interruptExecutionAfterAbort(box, expectedSessionId, expectedExecutionId);
@@ -71,9 +87,7 @@ export function sandboxInstanceAsEnvironment(box, providerName, client, declared
71
87
  }
72
88
  input.signal?.throwIfAborted();
73
89
  },
74
- ...(capabilities.streaming.detach && box.dispatchPrompt
75
- ? { dispatch: dispatchEnvironmentRun(box, providerName, environmentId) }
76
- : {}),
90
+ ...(dispatch ? { dispatch } : {}),
77
91
  ...((capabilities.sessions.continue || capabilities.streaming.replay || capabilities.streaming.detach) &&
78
92
  box.session
79
93
  ? {
@@ -89,7 +103,16 @@ export function sandboxInstanceAsEnvironment(box, providerName, client, declared
89
103
  if (session.id !== id) {
90
104
  throw new Error("sandbox session(id) returned an unrelated session");
91
105
  }
92
- return sandboxSessionAsAgentSession(session, resolveRetainedSessionControlRef(options?.controlRef, id, providerName, environmentId), providerName, environmentId);
106
+ const agentSession = sandboxSessionAsAgentSession(session, resolveRetainedSessionControlRef(options?.controlRef, id, providerName, environmentId), providerName, environmentId, dispatch, exactExecutionEvents);
107
+ // sessions.continue was granted from a probe-session fact; this
108
+ // backstop holds every concrete session to that fact, so a client
109
+ // whose sessions diverge from its probe surface fails loud here
110
+ // instead of failing at the first cancellation.
111
+ if (capabilities.sessions.continue &&
112
+ typeof agentSession.cancelRun !== "function") {
113
+ throw new Error("Tangle retained session support requires SandboxSession.cancelRun");
114
+ }
115
+ return agentSession;
93
116
  },
94
117
  }
95
118
  : {}),
@@ -133,56 +156,6 @@ export function sandboxInstanceAsEnvironment(box, providerName, client, declared
133
156
  },
134
157
  }
135
158
  : {}),
136
- ...(capabilities.branching.checkpoint && box.checkpoint
137
- ? {
138
- async checkpoint(options) {
139
- assertCheckpointOptions(options);
140
- options?.signal?.throwIfAborted();
141
- const result = await awaitWithSignal(box.checkpoint?.(options), options?.signal);
142
- options?.signal?.throwIfAborted();
143
- return { id: checkpointIdFromResult(result), provider: providerName };
144
- },
145
- }
146
- : {}),
147
- ...(capabilities.branching.fork && box.fork
148
- ? {
149
- async fork(checkpoint, options) {
150
- assertCheckpointRef(checkpoint);
151
- assertForkOptions(options);
152
- if (checkpoint.provider !== undefined && checkpoint.provider !== providerName) {
153
- throw new Error("Tangle fork checkpoint belongs to another provider");
154
- }
155
- boundedIdentifier(checkpoint.id, "Tangle checkpoint id");
156
- options?.signal?.throwIfAborted();
157
- const forked = await awaitWithSignal(box.fork?.(checkpoint.id, options), options?.signal);
158
- if (!forked)
159
- throw new Error("sandbox fork returned no environment");
160
- try {
161
- options?.signal?.throwIfAborted();
162
- if (boundedIdentifier(forked.id, "Tangle fork environment id") === environmentId) {
163
- throw new Error("Tangle fork returned the source environment");
164
- }
165
- return sandboxInstanceAsEnvironment(forked, providerName, client, capabilities);
166
- }
167
- catch (error) {
168
- if (!forked.delete) {
169
- const baseError = error instanceof Error ? error : new Error(String(error));
170
- attachCleanupHandle(baseError, forked);
171
- throw baseError;
172
- }
173
- try {
174
- await forked.delete();
175
- }
176
- catch (cleanupError) {
177
- const combined = new AggregateError([error, cleanupError], "Tangle fork validation and cleanup both failed");
178
- attachCleanupHandle(combined, forked, cleanupError);
179
- throw combined;
180
- }
181
- throw error;
182
- }
183
- },
184
- }
185
- : {}),
186
159
  ...(capabilities.placement
187
160
  ? {
188
161
  async placement(options) {
@@ -1,6 +1,14 @@
1
1
  import type { SandboxEvent } from "@tangle-network/sandbox";
2
2
  import type { AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider";
3
+ /** The sidecar sends this envelope when an SSE connection is ready. */
4
+ export declare function isSandboxConnectionMarker(event: SandboxEvent): boolean;
5
+ /** Read identity from both run frames and /agents/events session envelopes. */
6
+ export declare function sandboxEventIdentity(event: SandboxEvent): {
7
+ executionId?: string;
8
+ sessionId?: string;
9
+ };
3
10
  export declare function environmentEventFromSandboxEvent(event: SandboxEvent, expected?: {
4
11
  executionId?: string;
5
12
  sessionId?: string;
13
+ streamBound?: boolean;
6
14
  }): AgentEnvironmentEvent;