@tangle-network/agent-provider-tangle 0.6.1 → 0.6.3

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
@@ -13,8 +13,9 @@ const provider = createTangleProvider({
13
13
 
14
14
  Detached dispatch returns the immutable Sandbox execution receipt in `controlRef`.
15
15
  The adapter validates its complete capability document and omits optional environment methods whose capabilities are disabled.
16
- Reconstruct an exact session with `environment.session(reference.id, { controlRef: reference.controlRef })`; replay cursors are exclusive at the agent interface even though the Sandbox stream is inclusive.
16
+ Reconstruct an exact session with `environment.session(reference.id, { controlRef: reference.controlRef })`; replay cursors are exclusive at both the agent interface and Sandbox session stream.
17
17
  Result, replay, and cancel operations select that exact execution instead of whichever execution most recently changed the shared session.
18
+ Retained control is advertised only when the Sandbox exposes dispatch, replay, exact results and events, turn idempotency, and `cancelRun`; native continuation remains absent.
18
19
  After `session.prompt()` admits another turn, that session object's `controlRef` advances only when Sandbox returns the requested execution ID; a mismatched receipt fails without advancing local state.
19
20
  Sandbox keeps execution identifiers optional for older or unproven service paths, so this adapter fails closed when a dispatch or prompt does not return one and never falls back to latest-session state.
20
21
  Sessions reconstructed without a control reference may start a new prompt, but result lookup, cancellation, and cursor replay fail before calling Sandbox because those operations could otherwise select the newest unrelated execution.
@@ -20,6 +20,7 @@ export interface SandboxCapabilitySupport {
20
20
  fork: boolean;
21
21
  placement: boolean;
22
22
  destroy: boolean;
23
+ cancelRun: boolean;
23
24
  }
24
25
  export declare function sandboxCapabilitySupport(box: SandboxInstanceLike, client: SandboxClientLike): SandboxCapabilitySupport;
25
26
  /**
@@ -28,6 +29,8 @@ export declare function sandboxCapabilitySupport(box: SandboxInstanceLike, clien
28
29
  * A client without placement metadata cannot satisfy placement(), and this
29
30
  * adapter has no durable branching, interaction, or native-continuation
30
31
  * implementation regardless of an overly broad configured document.
32
+ * Per-sandbox retained-run claims are narrowed later, after a concrete
33
+ * session handle proves that cancelRun is available.
31
34
  */
32
35
  export declare function capabilitiesForClient(declared: AgentEnvironmentCapabilities, client: SandboxClientLike): AgentEnvironmentCapabilities;
33
36
  /**
@@ -33,6 +33,7 @@ export function defaultTangleSandboxCapabilities(harness) {
33
33
  streaming: { live: true, replay: true, detach: true, turnIdempotency: true },
34
34
  // A Sandbox session id is not a Braid context-boundary proof. Native
35
35
  // continuation stays unavailable until this adapter can verify one.
36
+ // Retained control stays opt-in until a concrete session exposes cancelRun.
36
37
  sessions: { continue: false, list: false, messages: false },
37
38
  workspace: { read: true, write: true, exec: true, git: false, upload: true, download: true },
38
39
  branching: { checkpoint: false, fork: false },
@@ -43,7 +44,19 @@ export function defaultTangleSandboxCapabilities(harness) {
43
44
  confidential: false,
44
45
  };
45
46
  }
47
+ const CAPABILITY_PROBE_SESSION_ID = "__tangle-capability-probe__";
46
48
  export function sandboxCapabilitySupport(box, client) {
49
+ let session;
50
+ if (typeof box.session === "function") {
51
+ try {
52
+ // Sandbox session handles are lazy. Inspecting one does not call the
53
+ // service, and keeps retained-control claims tied to the actual handle.
54
+ session = box.session(CAPABILITY_PROBE_SESSION_ID);
55
+ }
56
+ catch {
57
+ // A client that cannot produce a session handle cannot prove retained control.
58
+ }
59
+ }
47
60
  return {
48
61
  dispatchPrompt: typeof box.dispatchPrompt === "function",
49
62
  session: typeof box.session === "function",
@@ -54,6 +67,7 @@ export function sandboxCapabilitySupport(box, client) {
54
67
  fork: typeof box.fork === "function",
55
68
  placement: typeof client.describePlacement === "function",
56
69
  destroy: typeof box.delete === "function",
70
+ cancelRun: typeof session?.cancelRun === "function",
57
71
  };
58
72
  }
59
73
  /**
@@ -62,8 +76,11 @@ export function sandboxCapabilitySupport(box, client) {
62
76
  * A client without placement metadata cannot satisfy placement(), and this
63
77
  * adapter has no durable branching, interaction, or native-continuation
64
78
  * implementation regardless of an overly broad configured document.
79
+ * Per-sandbox retained-run claims are narrowed later, after a concrete
80
+ * session handle proves that cancelRun is available.
65
81
  */
66
82
  export function capabilitiesForClient(declared, client) {
83
+ const canReconstructRetainedEnvironment = typeof client.get === "function";
67
84
  const narrowed = {
68
85
  ...declared,
69
86
  streaming: {
@@ -72,7 +89,12 @@ export function capabilitiesForClient(declared, client) {
72
89
  detach: declared.streaming.detach,
73
90
  turnIdempotency: declared.streaming.turnIdempotency,
74
91
  },
75
- sessions: { ...declared.sessions, continue: false, list: false, messages: false },
92
+ sessions: {
93
+ ...declared.sessions,
94
+ continue: declared.sessions.continue && canReconstructRetainedEnvironment,
95
+ list: false,
96
+ messages: false,
97
+ },
76
98
  workspace: { ...declared.workspace, git: false },
77
99
  usage: false,
78
100
  branching: {
@@ -86,7 +108,8 @@ export function capabilitiesForClient(declared, client) {
86
108
  placement: declared.placement && typeof client.describePlacement === "function",
87
109
  };
88
110
  delete narrowed.interactions;
89
- delete narrowed.retainedControl;
111
+ if (!canReconstructRetainedEnvironment)
112
+ delete narrowed.retainedControl;
90
113
  delete narrowed.nativeContinuation;
91
114
  return narrowed;
92
115
  }
@@ -98,9 +121,17 @@ export function capabilitiesForClient(declared, client) {
98
121
  */
99
122
  export function capabilitiesForSandbox(declared, support) {
100
123
  const narrowed = { ...declared };
124
+ const supportsRetainedControl = declared.sessions.continue &&
125
+ declared.streaming.detach &&
126
+ declared.streaming.replay &&
127
+ declared.streaming.turnIdempotency &&
128
+ support.dispatchPrompt &&
129
+ support.session &&
130
+ support.cancelRun;
101
131
  delete narrowed.interactions;
102
- delete narrowed.retainedControl;
103
132
  delete narrowed.nativeContinuation;
133
+ if (!supportsRetainedControl)
134
+ delete narrowed.retainedControl;
104
135
  return {
105
136
  ...narrowed,
106
137
  streaming: {
@@ -111,7 +142,7 @@ export function capabilitiesForSandbox(declared, support) {
111
142
  },
112
143
  sessions: {
113
144
  ...declared.sessions,
114
- continue: false,
145
+ continue: declared.sessions.continue && supportsRetainedControl,
115
146
  list: false,
116
147
  messages: false,
117
148
  },
@@ -1,6 +1,9 @@
1
1
  import type { AgentSessionRef, AgentTurnInput } from "@tangle-network/agent-interface/environment-provider";
2
2
  import type { SandboxInstanceLike, SandboxSessionLike } from "./tangle-types.js";
3
- export declare function sessionPromptRequestDigest(input: AgentTurnInput, provider: string, environmentId: string, sessionId: string, executionId: string): `sha256:${string}`;
3
+ export declare function sessionPromptRequestDigest(input: AgentTurnInput, provider: string, environmentId: string, sessionId: string, options?: {
4
+ executionId?: string;
5
+ nonce?: string;
6
+ }): `sha256:${string}`;
4
7
  export declare function hasReplayPayload(input: AgentTurnInput): boolean;
5
8
  export declare function interruptAfterAbort(box: SandboxInstanceLike, reference: AgentSessionRef): Promise<void>;
6
9
  export declare function interruptExecutionAfterAbort(source: SandboxInstanceLike | SandboxSessionLike, sessionId: string, executionId: string): Promise<void>;
@@ -1,10 +1,13 @@
1
1
  import { canonicalCandidateDigest } from "@tangle-network/agent-interface";
2
- export function sessionPromptRequestDigest(input, provider, environmentId, sessionId, executionId) {
2
+ export function sessionPromptRequestDigest(input, provider, environmentId, sessionId, options = {}) {
3
3
  return canonicalCandidateDigest({
4
4
  provider,
5
5
  environmentId,
6
6
  sessionId,
7
- executionId,
7
+ ...(options.executionId === undefined
8
+ ? {}
9
+ : { executionId: options.executionId }),
10
+ ...(options.nonce === undefined ? {} : { nonce: options.nonce }),
8
11
  ...(input.turnId === undefined ? {} : { turnId: input.turnId }),
9
12
  ...(input.prompt === undefined ? {} : { prompt: input.prompt }),
10
13
  ...(input.parts === undefined ? {} : { parts: input.parts }),
@@ -25,6 +28,11 @@ export function hasReplayPayload(input) {
25
28
  input.providerOptions !== undefined);
26
29
  }
27
30
  export async function interruptAfterAbort(box, reference) {
31
+ const metadata = reference.metadata;
32
+ // A duplicate dispatch receipt identifies work owned by another caller.
33
+ // Only a receipt that proves this call admitted new work may be interrupted.
34
+ if (metadata?.dispatched !== true || metadata.alreadyExisted === true)
35
+ return;
28
36
  const sessionId = reference.id;
29
37
  const executionId = reference.controlRef?.executionId;
30
38
  if (!box.session || executionId === undefined)
@@ -1,23 +1,61 @@
1
- import { AgentTurnInputSchema } from "@tangle-network/agent-interface";
2
- import { executionIdFromTurnInput, promptFromTurnInput, promptOptionsFromTurnInput, } from "./tangle-prompt.js";
3
- import { retainedSessionControlRef, sessionRefFromSandboxDispatch } from "./tangle-session-control.js";
1
+ import { AgentExactRunControlRefSchema, AgentTurnInputSchema, } from "@tangle-network/agent-interface";
2
+ import { promptFromTurnInput, promptOptionsFromTurnInput, } from "./tangle-prompt.js";
3
+ import { retainedSessionControlRef, sessionPromptExecutionId, sessionPromptSessionId, sessionRefFromSandboxDispatch, } from "./tangle-session-control.js";
4
4
  import { awaitWithSignal } from "./tangle-contract-safety.js";
5
5
  import { interruptAfterAbort, sessionPromptRequestDigest, } from "./tangle-environment-control.js";
6
6
  export function dispatchEnvironmentRun(box, provider, environmentId) {
7
7
  return async (input) => {
8
8
  AgentTurnInputSchema.parse(input);
9
9
  input.signal?.throwIfAborted();
10
- const expectedSessionId = input.sessionId ?? input.controlRef?.sessionId;
11
- const promise = box.dispatchPrompt?.(promptFromTurnInput(input), promptOptionsFromTurnInput(input, { provider, environmentId }));
10
+ const callerControlRef = input.controlRef === undefined
11
+ ? undefined
12
+ : AgentExactRunControlRefSchema.parse(input.controlRef);
13
+ if (callerControlRef !== undefined) {
14
+ if (callerControlRef.provider !== provider ||
15
+ callerControlRef.environmentId !== environmentId) {
16
+ throw new Error("Tangle control reference does not match this environment");
17
+ }
18
+ if (input.sessionId !== undefined &&
19
+ input.sessionId !== callerControlRef.sessionId) {
20
+ throw new Error("Tangle sessionId conflicts with the control reference");
21
+ }
22
+ if (input.executionId !== undefined &&
23
+ input.executionId !== callerControlRef.executionId) {
24
+ throw new Error("Tangle executionId conflicts with the control reference");
25
+ }
26
+ }
27
+ const expectedSessionId = input.sessionId ??
28
+ callerControlRef?.sessionId ??
29
+ sessionPromptSessionId(provider, environmentId, input.turnId);
30
+ if (expectedSessionId === undefined) {
31
+ throw new Error("Tangle detached dispatch requires an exact sessionId or turnId");
32
+ }
33
+ const exactInput = { ...input, sessionId: expectedSessionId };
34
+ const requestedExecutionId = input.executionId ?? callerControlRef?.executionId;
35
+ const baseRequestDigest = sessionPromptRequestDigest(exactInput, provider, environmentId, expectedSessionId);
36
+ const expectedExecutionId = requestedExecutionId ?? sessionPromptExecutionId(baseRequestDigest);
37
+ const requestDigest = sessionPromptRequestDigest(exactInput, provider, environmentId, expectedSessionId, { executionId: expectedExecutionId });
38
+ if (callerControlRef !== undefined) {
39
+ if (callerControlRef.requestDigest !== requestDigest) {
40
+ throw new Error("Tangle prompt request digest conflicts with the control reference");
41
+ }
42
+ }
43
+ const runControlRef = retainedSessionControlRef(expectedSessionId, expectedExecutionId, provider, environmentId, requestDigest, callerControlRef?.runId);
44
+ const dispatchInput = {
45
+ ...exactInput,
46
+ executionId: expectedExecutionId,
47
+ controlRef: runControlRef,
48
+ };
49
+ const promise = box.dispatchPrompt?.(promptFromTurnInput(dispatchInput), promptOptionsFromTurnInput(dispatchInput, { provider, environmentId }));
12
50
  let dispatched;
13
51
  try {
14
52
  dispatched = await awaitWithSignal(promise, input.signal);
15
53
  }
16
54
  catch (error) {
17
- if (input.signal?.aborted && promise) {
55
+ if (input.signal?.aborted && input.detach !== true && promise) {
18
56
  void promise
19
57
  .then((late) => {
20
- const lateRef = sessionRefFromSandboxDispatch(late, provider, environmentId, executionIdFromTurnInput(input));
58
+ const lateRef = sessionRefFromSandboxDispatch(late, provider, environmentId, expectedExecutionId, requestDigest, expectedSessionId, callerControlRef);
21
59
  return interruptAfterAbort(box, lateRef);
22
60
  })
23
61
  .catch(() => undefined);
@@ -26,7 +64,7 @@ export function dispatchEnvironmentRun(box, provider, environmentId) {
26
64
  }
27
65
  let reference;
28
66
  try {
29
- reference = sessionRefFromSandboxDispatch(dispatched, provider, environmentId, executionIdFromTurnInput(input), undefined, expectedSessionId);
67
+ reference = sessionRefFromSandboxDispatch(dispatched, provider, environmentId, expectedExecutionId, requestDigest, expectedSessionId, callerControlRef);
30
68
  }
31
69
  catch (error) {
32
70
  try {
@@ -41,20 +79,12 @@ export function dispatchEnvironmentRun(box, provider, environmentId) {
41
79
  }
42
80
  throw error;
43
81
  }
44
- const executionId = reference.controlRef?.executionId;
45
- const requestDigest = executionId === undefined
46
- ? undefined
47
- : sessionPromptRequestDigest(input, provider, environmentId, reference.id, executionId);
48
- const boundReference = executionId === undefined || requestDigest === undefined
49
- ? reference
50
- : {
51
- ...reference,
52
- controlRef: retainedSessionControlRef(reference.id, executionId, provider, environmentId, requestDigest),
53
- };
54
- if (input.signal?.aborted) {
82
+ const boundReference = reference;
83
+ if (input.signal?.aborted && input.detach !== true) {
55
84
  await interruptAfterAbort(box, boundReference);
56
85
  input.signal.throwIfAborted();
57
86
  }
87
+ input.signal?.throwIfAborted();
58
88
  return boundReference;
59
89
  };
60
90
  }
@@ -1,4 +1,13 @@
1
- import type { AgentSession } from "@tangle-network/agent-interface/environment-provider";
2
- import type { AgentRunControlRef } from "@tangle-network/agent-interface";
1
+ import type { AgentSession, AgentSessionRef, AgentTurnInput } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { AgentExactRunControlRef, AgentRunControlRef } from "@tangle-network/agent-interface";
3
+ import type { SandboxEvent } from "@tangle-network/sandbox";
3
4
  import type { SandboxSessionLike } from "./tangle-types.js";
4
- export declare function sandboxSessionAsAgentSession(session: SandboxSessionLike, controlRef: AgentRunControlRef | undefined, provider: string, environmentId: string): AgentSession;
5
+ type ExactExecutionEventStream = (options: {
6
+ sessionId: string;
7
+ executionId: string;
8
+ since?: string;
9
+ signal?: AbortSignal;
10
+ controlRef?: AgentExactRunControlRef;
11
+ }) => AsyncIterable<SandboxEvent>;
12
+ export declare function sandboxSessionAsAgentSession(session: SandboxSessionLike, controlRef: AgentRunControlRef | undefined, provider: string, environmentId: string, dispatch?: (input: AgentTurnInput) => Promise<AgentSessionRef>, exactExecutionEvents?: ExactExecutionEventStream): AgentSession;
13
+ export {};
@@ -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
7
  import { 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");
@@ -36,11 +64,20 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
36
64
  }
37
65
  options?.signal?.throwIfAborted();
38
66
  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]();
67
+ const useExactExecutionStream = exactExecutionEvents !== undefined && executionId !== undefined;
68
+ const iterator = (useExactExecutionStream
69
+ ? exactExecutionEvents({
70
+ sessionId: session.id,
71
+ executionId,
72
+ ...(options?.since !== undefined ? { since: options.since } : {}),
73
+ ...(options?.signal ? { signal: options.signal } : {}),
74
+ ...(activeControlRef ? { controlRef: activeControlRef } : {}),
75
+ })
76
+ : session.events({
77
+ ...(options?.since !== undefined ? { since: options.since } : {}),
78
+ ...(executionId !== undefined ? { executionId } : {}),
79
+ ...(options?.signal ? { signal: options.signal } : {}),
80
+ }))[Symbol.asyncIterator]();
44
81
  let completed = false;
45
82
  try {
46
83
  while (true) {
@@ -50,16 +87,28 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
50
87
  break;
51
88
  }
52
89
  options?.signal?.throwIfAborted();
53
- if (options?.since !== undefined && next.value.id === options.since)
90
+ if (isSandboxConnectionMarker(next.value)) {
91
+ const markerIdentity = sandboxEventIdentity(next.value);
92
+ if (executionId !== undefined &&
93
+ markerIdentity.executionId !== undefined &&
94
+ markerIdentity.executionId !== executionId) {
95
+ throw new Error("Tangle exact session connection identified a different executionId");
96
+ }
97
+ if (markerIdentity.sessionId !== undefined &&
98
+ markerIdentity.sessionId !== session.id) {
99
+ throw new Error("Tangle exact session connection identified a different sessionId");
100
+ }
54
101
  continue;
102
+ }
55
103
  const converted = environmentEventFromSandboxEvent(next.value, {
56
104
  executionId,
57
105
  sessionId: session.id,
106
+ ...(useExactExecutionStream ? { streamBound: true } : {}),
58
107
  });
59
108
  if (converted.id === undefined)
60
109
  throw new Error("Tangle session event arrived without a stable id");
61
110
  if (seenEventIds.has(converted.id))
62
- throw new Error(`Tangle session replay repeated event id ${converted.id}`);
111
+ continue;
63
112
  seenEventIds.add(converted.id);
64
113
  options?.signal?.throwIfAborted();
65
114
  yield converted;
@@ -82,63 +131,160 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
82
131
  const resultRecord = validatedSandboxPromptResult(result);
83
132
  if (resultRecord.executionId !== expectedExecutionId)
84
133
  throw new Error("Tangle session result did not confirm its exact executionId");
85
- return agentTurnResultFromPromptRecord(resultRecord, { sessionId: session.id });
134
+ return agentTurnResultFromPromptRecord(resultRecord, {
135
+ sessionId: session.id,
136
+ controlRef: activeControlRef,
137
+ });
86
138
  },
87
139
  async prompt(input) {
88
140
  AgentTurnInputSchema.parse(input);
89
141
  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");
101
- }
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");
142
+ if (promptInFlight) {
143
+ throw new Error("Tangle session already has a prompt in flight");
112
144
  }
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 };
145
+ promptInFlight = true;
122
146
  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");
147
+ if (input.sessionId !== undefined && input.sessionId !== session.id) {
148
+ throw new Error("Tangle sessionId conflicts with this session");
129
149
  }
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 } : {}),
150
+ const requestedControlRef = resolveRetainedSessionControlRef(input.controlRef, session.id, provider, environmentId);
151
+ if (activeControlRef &&
152
+ requestedControlRef &&
153
+ !sameRunControlRef(activeControlRef, requestedControlRef)) {
154
+ throw new Error("Tangle prompt control reference conflicts with this session");
155
+ }
156
+ if (requestedControlRef?.executionId !== undefined &&
157
+ input.executionId !== undefined &&
158
+ requestedControlRef.executionId !== input.executionId) {
159
+ throw new Error("Tangle executionId conflicts with the control reference");
160
+ }
161
+ if (input.detach === true && input.lastEventId === undefined) {
162
+ if (dispatch === undefined) {
163
+ throw new Error("Tangle detached session prompt requires the sandbox dispatch primitive");
164
+ }
165
+ const detachedInput = {
166
+ ...input,
167
+ signal: undefined,
168
+ controlRef: undefined,
169
+ sessionId: session.id,
170
+ detach: true,
171
+ ...(input.turnId === undefined ? { turnId: randomUUID() } : {}),
172
+ };
173
+ const reference = await dispatch(detachedInput);
174
+ const nextControlRef = resolveRetainedSessionControlRef(reference.controlRef, session.id, provider, environmentId);
175
+ if (nextControlRef === undefined) {
176
+ throw new Error("Tangle detached session dispatch returned no exact control reference");
177
+ }
178
+ // The admission receipt is the durability boundary. Store it before
179
+ // waiting for the result so an aborted caller can reconnect later.
180
+ activeControlRef = nextControlRef;
181
+ const result = await awaitWithSignal(session.result({
182
+ executionId: nextControlRef.executionId,
183
+ ...(input.signal ? { signal: input.signal } : {}),
184
+ }), input.signal);
185
+ input.signal?.throwIfAborted();
186
+ const resultRecord = validatedSandboxPromptResult(result);
187
+ if (resultRecord.executionId !== nextControlRef.executionId) {
188
+ throw new Error("Tangle detached session prompt did not confirm its exact executionId");
189
+ }
190
+ return agentTurnResultFromPromptRecord(resultRecord, {
191
+ sessionId: session.id,
192
+ controlRef: nextControlRef,
193
+ });
194
+ }
195
+ const sourceControlRef = requestedControlRef ?? activeControlRef;
196
+ const replay = input.lastEventId !== undefined;
197
+ if (replay &&
198
+ sourceControlRef?.executionId !== undefined &&
199
+ input.executionId !== undefined &&
200
+ input.executionId !== sourceControlRef.executionId) {
201
+ throw new Error("Tangle replay executionId conflicts with the control reference");
202
+ }
203
+ if (replay && sourceControlRef?.requestDigest === undefined) {
204
+ throw new Error("Tangle replay requires an exact request digest from its control reference");
205
+ }
206
+ const requestedExecutionId = input.executionId ?? requestedControlRef?.executionId;
207
+ const nonce = !replay &&
208
+ input.turnId === undefined &&
209
+ requestedExecutionId === undefined &&
210
+ requestedControlRef === undefined
211
+ ? randomUUID()
212
+ : undefined;
213
+ const baseRequestDigest = sessionPromptRequestDigest(input, provider, environmentId, session.id, nonce === undefined ? {} : { nonce });
214
+ const executionId = replay
215
+ ? input.executionId ?? sourceControlRef?.executionId
216
+ : requestedExecutionId ?? sessionPromptExecutionId(baseRequestDigest);
217
+ if (executionId === undefined) {
218
+ throw new Error("Tangle session replay requires the exact executionId from its control reference");
219
+ }
220
+ const explicitRequestDigest = sessionPromptRequestDigest(input, provider, environmentId, session.id, {
221
+ executionId,
222
+ ...(nonce === undefined ? {} : { nonce }),
135
223
  });
136
- }
137
- catch (error) {
138
- if (input.signal?.aborted) {
139
- void interruptExecutionAfterAbort(session, session.id, executionId);
224
+ if (!replay &&
225
+ requestedControlRef?.requestDigest !== undefined &&
226
+ requestedControlRef.requestDigest !== explicitRequestDigest) {
227
+ throw new Error("Tangle prompt request digest conflicts with the control reference");
228
+ }
229
+ if (replay &&
230
+ hasReplayPayload(input) &&
231
+ sourceControlRef?.requestDigest !== explicitRequestDigest) {
232
+ throw new Error("Tangle prompt request digest conflicts with the control reference");
140
233
  }
141
- throw error;
234
+ const requestDigest = replay
235
+ ? sourceControlRef?.requestDigest
236
+ : requestedControlRef?.requestDigest ?? explicitRequestDigest;
237
+ if (requestDigest === undefined) {
238
+ throw new Error("Tangle prompt could not establish an exact request digest");
239
+ }
240
+ const targetControlRef = retainedSessionControlRef(session.id, executionId, provider, environmentId, requestDigest, requestedControlRef?.runId);
241
+ const promptInput = replay
242
+ ? {
243
+ ...input,
244
+ sessionId: session.id,
245
+ executionId,
246
+ controlRef: sourceControlRef,
247
+ }
248
+ : {
249
+ ...input,
250
+ sessionId: session.id,
251
+ executionId,
252
+ controlRef: targetControlRef,
253
+ };
254
+ try {
255
+ const result = await awaitWithSignal(session.prompt(promptFromTurnInput(input), promptOptionsFromTurnInput(promptInput, {
256
+ provider,
257
+ environmentId,
258
+ sessionId: session.id,
259
+ })), input.signal);
260
+ input.signal?.throwIfAborted();
261
+ const resultRecord = validatedSandboxPromptResult(result);
262
+ if (resultRecord.executionId !== executionId) {
263
+ void interruptExecutionAfterAbort(session, session.id, executionId);
264
+ throw new Error("Tangle session prompt did not confirm its exact executionId");
265
+ }
266
+ const nextControlRef = targetControlRef;
267
+ activeControlRef = nextControlRef;
268
+ return agentTurnResultFromPromptRecord(resultRecord, {
269
+ sessionId: session.id,
270
+ controlRef: nextControlRef,
271
+ ...(input.contextTransfer
272
+ ? { contextTransferRequest: input.contextTransfer }
273
+ : {}),
274
+ ...(input.contextTransfer
275
+ ? { contextTransferRequested: true }
276
+ : {}),
277
+ });
278
+ }
279
+ catch (error) {
280
+ if (input.signal?.aborted && input.detach !== true) {
281
+ void interruptExecutionAfterAbort(session, session.id, executionId);
282
+ }
283
+ throw error;
284
+ }
285
+ }
286
+ finally {
287
+ promptInFlight = false;
142
288
  }
143
289
  },
144
290
  async cancel(options) {
@@ -152,5 +298,6 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
152
298
  if (result.cancelled !== true)
153
299
  throw new Error("Tangle sandbox did not confirm cancellation");
154
300
  },
301
+ ...(cancelRun ? { cancelRun } : {}),
155
302
  };
156
303
  }
@@ -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,12 @@ 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
+ if (capabilities.sessions.continue &&
108
+ typeof agentSession.cancelRun !== "function") {
109
+ throw new Error("Tangle retained session support requires SandboxSession.cancelRun");
110
+ }
111
+ return agentSession;
93
112
  },
94
113
  }
95
114
  : {}),
@@ -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;
@@ -1,6 +1,48 @@
1
+ import { CanonicalStreamEventSchema, } from "@tangle-network/agent-interface";
1
2
  import { assertBoundedJson } from "./tangle-contract-safety.js";
2
3
  import { optionalNonEmptyString } from "./tangle-environment-values.js";
3
4
  import { tokenUsageFromData } from "./tangle-result-values.js";
5
+ /** The sidecar sends this envelope when an SSE connection is ready. */
6
+ export function isSandboxConnectionMarker(event) {
7
+ if (!event || typeof event !== "object")
8
+ return false;
9
+ const record = event;
10
+ if (record.type === "connection.established")
11
+ return true;
12
+ const data = record.data;
13
+ return (data !== null &&
14
+ typeof data === "object" &&
15
+ !Array.isArray(data) &&
16
+ data.type === "connection.established");
17
+ }
18
+ /** Read identity from both run frames and /agents/events session envelopes. */
19
+ export function sandboxEventIdentity(event) {
20
+ const record = event;
21
+ const data = record.data;
22
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
23
+ return {};
24
+ }
25
+ const dataRecord = data;
26
+ const properties = dataRecord.properties &&
27
+ typeof dataRecord.properties === "object" &&
28
+ !Array.isArray(dataRecord.properties)
29
+ ? dataRecord.properties
30
+ : undefined;
31
+ const info = properties?.info &&
32
+ typeof properties.info === "object" &&
33
+ !Array.isArray(properties.info)
34
+ ? properties.info
35
+ : undefined;
36
+ return {
37
+ executionId: optionalNonEmptyString(dataRecord.executionId ?? properties?.executionId ?? info?.executionId, "Tangle Sandbox event executionId"),
38
+ sessionId: optionalNonEmptyString(dataRecord.sessionId ??
39
+ dataRecord.sessionID ??
40
+ properties?.sessionId ??
41
+ properties?.sessionID ??
42
+ info?.sessionId ??
43
+ info?.sessionID, "Tangle Sandbox event sessionId"),
44
+ };
45
+ }
4
46
  export function environmentEventFromSandboxEvent(event, expected = {}) {
5
47
  if (!event || typeof event !== "object") {
6
48
  throw new Error("Tangle Sandbox emitted a non-object event");
@@ -30,27 +72,145 @@ export function environmentEventFromSandboxEvent(event, expected = {}) {
30
72
  if (Object.prototype.hasOwnProperty.call(data, "contextTransferReceipt")) {
31
73
  throw new Error("Tangle Sandbox emitted an unsolicited context transfer receipt");
32
74
  }
33
- const eventExecutionId = optionalNonEmptyString(data.executionId, "Tangle Sandbox event executionId");
34
- const eventSessionId = optionalNonEmptyString(data.sessionId, "Tangle Sandbox event sessionId");
75
+ const { executionId: eventExecutionId, sessionId: eventSessionId } = sandboxEventIdentity(event);
35
76
  if (expected.executionId !== undefined &&
36
- (eventExecutionId === undefined || eventExecutionId !== expected.executionId)) {
77
+ ((eventExecutionId === undefined && expected.streamBound !== true) ||
78
+ (eventExecutionId !== undefined && eventExecutionId !== expected.executionId))) {
37
79
  throw new Error("Tangle exact session event identified a different executionId");
38
80
  }
39
81
  if (expected.sessionId !== undefined &&
40
- (eventSessionId === undefined || eventSessionId !== expected.sessionId)) {
82
+ ((eventSessionId === undefined && expected.streamBound !== true) ||
83
+ (eventSessionId !== undefined && eventSessionId !== expected.sessionId))) {
41
84
  throw new Error("Tangle exact session event identified a different sessionId");
42
85
  }
43
86
  const usage = tokenUsageFromData(data);
87
+ const normalized = normalizeSandboxEvent(record.type, data);
44
88
  return {
45
89
  type: record.type,
46
90
  data,
47
91
  ...(typeof record.id === "string" ? { id: record.id } : {}),
92
+ ...(normalized ? { normalized } : {}),
48
93
  // Absent rather than zeroed: an event that reported no usage must not
49
94
  // contribute a total to whatever sums these events.
50
95
  ...(usage ? { usage } : {}),
51
96
  providerEvent: event,
52
97
  };
53
98
  }
99
+ function normalizeSandboxEvent(type, data) {
100
+ const supplied = data.normalized;
101
+ if (supplied !== undefined) {
102
+ const parsed = CanonicalStreamEventSchema.safeParse(supplied);
103
+ if (!parsed.success) {
104
+ throw new Error("Tangle Sandbox emitted an invalid normalized event");
105
+ }
106
+ if (parsed.data.type !== type) {
107
+ throw new Error(`Tangle Sandbox normalized event type "${parsed.data.type}" does not match transport type "${type}"`);
108
+ }
109
+ return parsed.data;
110
+ }
111
+ switch (type) {
112
+ case "status": {
113
+ const status = statusFromSandboxValue(data.status);
114
+ if (!status)
115
+ return undefined;
116
+ return {
117
+ type: "status",
118
+ status,
119
+ ...detailFromSandboxData(data),
120
+ };
121
+ }
122
+ case "message.part.updated": {
123
+ const candidate = {
124
+ type,
125
+ ...(data.part !== undefined ? { part: data.part } : {}),
126
+ ...(typeof data.delta === "string" ? { delta: data.delta } : {}),
127
+ };
128
+ const parsed = CanonicalStreamEventSchema.safeParse(candidate);
129
+ return parsed.success ? parsed.data : undefined;
130
+ }
131
+ case "tool-heartbeat":
132
+ return parseCanonical({
133
+ type,
134
+ toolName: data.toolName,
135
+ partId: data.partId,
136
+ elapsedMs: data.elapsedMs,
137
+ });
138
+ case "tool-slow":
139
+ return parseCanonical({
140
+ type,
141
+ toolName: data.toolName,
142
+ partId: data.partId,
143
+ elapsedMs: data.elapsedMs,
144
+ thresholdMs: data.thresholdMs,
145
+ });
146
+ case "model-processing":
147
+ return parseCanonical({
148
+ type,
149
+ phase: data.phase,
150
+ ...(typeof data.toolName === "string" ? { toolName: data.toolName } : {}),
151
+ ...(typeof data.elapsedMs === "number" ? { elapsedMs: data.elapsedMs } : {}),
152
+ });
153
+ case "warning":
154
+ return parseCanonical({
155
+ type,
156
+ code: data.code,
157
+ message: data.message,
158
+ });
159
+ case "session.updated":
160
+ return parseCanonical({
161
+ type,
162
+ sessionId: data.sessionId ?? data.sessionID,
163
+ ...(typeof data.title === "string" ? { title: data.title } : {}),
164
+ ...(data.time !== undefined ? { time: data.time } : {}),
165
+ });
166
+ case "interaction":
167
+ return parseCanonical({ type, request: data.request });
168
+ case "interaction.cancel":
169
+ return parseCanonical({
170
+ type,
171
+ id: data.id,
172
+ ...(typeof data.reason === "string" ? { reason: data.reason } : {}),
173
+ });
174
+ case "plan.submitted":
175
+ return parseCanonical({ type, plan: data.plan });
176
+ case "result":
177
+ case "done":
178
+ default:
179
+ // These transport frames carry terminal/result data, but they are not
180
+ // members of the provider-neutral canonical event union. The exact
181
+ // result endpoint remains the authoritative result surface.
182
+ return undefined;
183
+ }
184
+ }
185
+ function parseCanonical(value) {
186
+ const parsed = CanonicalStreamEventSchema.safeParse(value);
187
+ return parsed.success ? parsed.data : undefined;
188
+ }
189
+ function statusFromSandboxValue(value) {
190
+ if (typeof value !== "string")
191
+ return undefined;
192
+ switch (value) {
193
+ case "started":
194
+ case "queued":
195
+ return "started";
196
+ case "processing":
197
+ case "running":
198
+ return "processing";
199
+ case "completed":
200
+ case "success":
201
+ return "completed";
202
+ case "failed":
203
+ case "error":
204
+ case "cancelled":
205
+ return "failed";
206
+ default:
207
+ return undefined;
208
+ }
209
+ }
210
+ function detailFromSandboxData(data) {
211
+ const detail = [data.detail, data.error, data.message].find((value) => typeof value === "string" && value.length > 0);
212
+ return detail === undefined ? {} : { detail };
213
+ }
54
214
  function assertBoundedRecord(value) {
55
215
  if (Object.keys(value).length > 256) {
56
216
  throw new Error("Tangle Sandbox event data has too many fields");
@@ -1,13 +1,14 @@
1
- import type { PromptOptions, PromptResult } from "@tangle-network/sandbox";
1
+ import type { PromptResult } from "@tangle-network/sandbox";
2
2
  import type { AgentTurnInput, AgentTurnResult } from "@tangle-network/agent-interface/environment-provider";
3
- import type { InputPart } from "@tangle-network/agent-interface";
3
+ import type { AgentExactRunControlRef, InputPart } from "@tangle-network/agent-interface";
4
+ import type { TanglePromptOptions } from "./tangle-types.js";
4
5
  export declare function promptFromTurnInput(input: AgentTurnInput): string | InputPart[];
5
6
  export declare function executionIdFromTurnInput(input: AgentTurnInput): string | undefined;
6
7
  export declare function promptOptionsFromTurnInput(input: AgentTurnInput, target: {
7
8
  provider: string;
8
9
  environmentId: string;
9
10
  sessionId?: string;
10
- }): PromptOptions;
11
+ }): TanglePromptOptions;
11
12
  type SandboxRunStatus = "success" | "failed" | "blocked_on_approval" | "awaiting_question" | "awaiting_plan_decision";
12
13
  type ValidatedSandboxPromptResult = Record<string, unknown> & {
13
14
  success: boolean;
@@ -20,5 +21,6 @@ export declare function agentTurnResultFromPromptRecord(record: ValidatedSandbox
20
21
  contextTransferRequested?: boolean;
21
22
  contextTransferRequest?: import("@tangle-network/agent-interface").ContextTransferRequest;
22
23
  sessionId?: string;
24
+ controlRef?: AgentExactRunControlRef;
23
25
  }): AgentTurnResult;
24
26
  export {};
@@ -31,9 +31,6 @@ export function promptOptionsFromTurnInput(input, target) {
31
31
  if (controlRef.sessionId === undefined || controlRef.executionId === undefined) {
32
32
  throw new Error("Tangle control reference requires exact sessionId and executionId");
33
33
  }
34
- if (controlRef.runId !== controlRef.executionId) {
35
- throw new Error("Tangle control reference requires runId to equal executionId");
36
- }
37
34
  if (input.sessionId !== undefined &&
38
35
  input.sessionId !== controlRef.sessionId) {
39
36
  throw new Error("Tangle sessionId conflicts with the control reference");
@@ -55,6 +52,7 @@ export function promptOptionsFromTurnInput(input, target) {
55
52
  ...(input.context ? { context: input.context } : {}),
56
53
  ...(input.signal ? { signal: input.signal } : {}),
57
54
  ...(executionId ? { executionId } : {}),
55
+ ...(controlRef ? { runControlRef: controlRef } : {}),
58
56
  ...(input.lastEventId ? { lastEventId: input.lastEventId } : {}),
59
57
  ...(input.turnId ? { turnId: input.turnId } : {}),
60
58
  ...(input.detach !== undefined ? { detach: input.detach } : {}),
@@ -118,6 +116,18 @@ const AWAITING_STATUSES = new Set([
118
116
  "awaiting_plan_decision",
119
117
  ]);
120
118
  export function agentTurnResultFromPromptRecord(record, options = {}) {
119
+ const controlRef = options.controlRef
120
+ ? AgentExactRunControlRefSchema.parse(options.controlRef)
121
+ : undefined;
122
+ if (controlRef !== undefined &&
123
+ options.sessionId !== undefined &&
124
+ controlRef.sessionId !== options.sessionId) {
125
+ throw new Error("Tangle prompt result sessionId conflicts with its control reference");
126
+ }
127
+ if (controlRef !== undefined &&
128
+ record.executionId !== controlRef.executionId) {
129
+ throw new Error("Tangle prompt result did not confirm its exact executionId");
130
+ }
121
131
  const text = typeof record.response === "string"
122
132
  ? record.response
123
133
  : typeof record.text === "string"
@@ -155,6 +165,13 @@ export function agentTurnResultFromPromptRecord(record, options = {}) {
155
165
  // "the agent is asking you something" indistinguishable from "the turn
156
166
  // failed", while the sandbox stayed alive waiting for an answer.
157
167
  metadata: {
168
+ ...(controlRef
169
+ ? {
170
+ runId: controlRef.runId,
171
+ executionId: controlRef.executionId,
172
+ requestDigest: controlRef.requestDigest,
173
+ }
174
+ : {}),
158
175
  status: record.status,
159
176
  awaitingInteraction: awaiting,
160
177
  terminal: !awaiting,
@@ -1,7 +1,8 @@
1
- import type { AgentRunControlRef } from "@tangle-network/agent-interface";
1
+ import type { AgentExactRunControlRef, AgentRunControlRef } from "@tangle-network/agent-interface";
2
2
  import type { AgentSessionRef } from "@tangle-network/agent-interface/environment-provider";
3
- export declare function sessionRefFromSandboxDispatch(dispatched: unknown, providerName: string, environmentId: string, expectedExecutionId: string | undefined, requestDigest?: `sha256:${string}` | undefined, expectedSessionId?: string | undefined): AgentSessionRef;
4
- export declare function retainedSessionControlRef(sessionId: string, executionId: string, provider: string, environmentId: string, requestDigest?: `sha256:${string}`): AgentRunControlRef;
5
- export declare function sessionPromptExecutionId(provider: string, environmentId: string, sessionId: string, turnId: string | undefined): string;
3
+ export declare function sessionRefFromSandboxDispatch(dispatched: unknown, providerName: string, environmentId: string, expectedExecutionId: string | undefined, requestDigest?: `sha256:${string}` | undefined, expectedSessionId?: string | undefined, callerControlRef?: AgentExactRunControlRef | undefined): AgentSessionRef;
4
+ export declare function retainedSessionControlRef(sessionId: string, executionId: string, provider: string, environmentId: string, requestDigest?: `sha256:${string}`, runId?: string): AgentExactRunControlRef;
5
+ export declare function sessionPromptExecutionId(requestDigest: `sha256:${string}`): string;
6
+ export declare function sessionPromptSessionId(provider: string, environmentId: string, turnId: string | undefined): string | undefined;
6
7
  export declare function sameRunControlRef(left: AgentRunControlRef, right: AgentRunControlRef): boolean;
7
- export declare function resolveRetainedSessionControlRef(candidate: AgentRunControlRef | undefined, sessionId: string, provider: string, environmentId: string): AgentRunControlRef | undefined;
8
+ export declare function resolveRetainedSessionControlRef(candidate: AgentRunControlRef | undefined, sessionId: string, provider: string, environmentId: string): AgentExactRunControlRef | undefined;
@@ -1,8 +1,8 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { createHash } from "node:crypto";
2
2
  import { AgentExactRunControlRefSchema, canonicalCandidateDigest, } from "@tangle-network/agent-interface";
3
3
  import { nonEmptyString } from "./tangle-environment-values.js";
4
4
  import { assertBoundedJson, boundedIdentifier, } from "./tangle-contract-safety.js";
5
- export function sessionRefFromSandboxDispatch(dispatched, providerName, environmentId, expectedExecutionId, requestDigest = undefined, expectedSessionId = undefined) {
5
+ export function sessionRefFromSandboxDispatch(dispatched, providerName, environmentId, expectedExecutionId, requestDigest = undefined, expectedSessionId = undefined, callerControlRef = undefined) {
6
6
  const record = dispatched && typeof dispatched === "object" && !Array.isArray(dispatched)
7
7
  ? dispatched
8
8
  : undefined;
@@ -34,11 +34,26 @@ export function sessionRefFromSandboxDispatch(dispatched, providerName, environm
34
34
  if (record.dispatched !== undefined && typeof record.dispatched !== "boolean") {
35
35
  throw new Error("sandbox dispatch returned an invalid dispatched flag");
36
36
  }
37
+ const accepted = record.runControlRef === undefined
38
+ ? callerControlRef === undefined
39
+ ? undefined
40
+ : AgentExactRunControlRefSchema.safeParse(callerControlRef)
41
+ : AgentExactRunControlRefSchema.safeParse(record.runControlRef);
42
+ if (accepted === undefined || !accepted.success) {
43
+ throw new Error("sandbox dispatch did not return the exact run control reference");
44
+ }
45
+ if (accepted.data.provider !== providerName ||
46
+ accepted.data.environmentId !== environmentId ||
47
+ accepted.data.sessionId !== id ||
48
+ accepted.data.executionId !== executionId ||
49
+ (requestDigest !== undefined && accepted.data.requestDigest !== requestDigest)) {
50
+ throw new Error("sandbox dispatch returned an exact control reference for another run");
51
+ }
52
+ const controlRef = Object.freeze(accepted.data);
37
53
  return {
38
54
  id,
39
55
  provider: providerName,
40
- controlRef: retainedSessionControlRef(id, executionId, providerName, environmentId, requestDigest ??
41
- canonicalCandidateDigest({ provider: providerName, environmentId, id, executionId })),
56
+ controlRef,
42
57
  metadata: {
43
58
  ...(record.status ? { status: record.status } : {}),
44
59
  ...(record.alreadyExisted !== undefined ? { alreadyExisted: record.alreadyExisted } : {}),
@@ -46,24 +61,27 @@ export function sessionRefFromSandboxDispatch(dispatched, providerName, environm
46
61
  },
47
62
  };
48
63
  }
49
- export function retainedSessionControlRef(sessionId, executionId, provider, environmentId, requestDigest) {
50
- return AgentExactRunControlRefSchema.parse({
51
- runId: executionId,
64
+ export function retainedSessionControlRef(sessionId, executionId, provider, environmentId, requestDigest, runId = executionId) {
65
+ return Object.freeze(AgentExactRunControlRefSchema.parse({
66
+ runId,
52
67
  provider,
53
68
  environmentId,
54
69
  sessionId,
55
70
  executionId,
56
71
  requestDigest: requestDigest ??
57
72
  canonicalCandidateDigest({ provider, environmentId, sessionId, executionId }),
58
- });
73
+ }));
74
+ }
75
+ export function sessionPromptExecutionId(requestDigest) {
76
+ return `session-turn-${requestDigest.slice("sha256:".length)}`;
59
77
  }
60
- export function sessionPromptExecutionId(provider, environmentId, sessionId, turnId) {
78
+ export function sessionPromptSessionId(provider, environmentId, turnId) {
61
79
  if (turnId === undefined)
62
- return randomUUID();
80
+ return undefined;
63
81
  const digest = createHash("sha256")
64
- .update(`${provider}\0${environmentId}\0${sessionId}\0${turnId}`)
82
+ .update(`${provider}\0${environmentId}\0${turnId}`)
65
83
  .digest("hex");
66
- return `session-turn-${digest}`;
84
+ return `session-${digest}`;
67
85
  }
68
86
  export function sameRunControlRef(left, right) {
69
87
  return (left.runId === right.runId &&
@@ -82,8 +100,5 @@ export function resolveRetainedSessionControlRef(candidate, sessionId, provider,
82
100
  controlRef.sessionId !== sessionId) {
83
101
  throw new Error("Tangle control reference does not match this session");
84
102
  }
85
- if (controlRef.runId !== controlRef.executionId) {
86
- throw new Error("Tangle session control reference requires runId to equal executionId");
87
- }
88
- return controlRef;
103
+ return retainedSessionControlRef(sessionId, controlRef.executionId, provider, environmentId, controlRef.requestDigest, controlRef.runId);
89
104
  }
@@ -1,6 +1,9 @@
1
1
  import type { BackendType, CreateSandboxOptions, ExecResult as SandboxExecResult, PromptOptions, PromptResult, SandboxEvent } from "@tangle-network/sandbox";
2
- import type { InputPart } from "@tangle-network/agent-interface";
2
+ import type { AgentExactRunControlRef, AgentRunCancellationAcknowledgement, AgentRunCancellationRequest, InputPart } from "@tangle-network/agent-interface";
3
3
  import type { AgentEnvironmentCapabilities, AgentEnvironmentProvider, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
4
+ export type TanglePromptOptions = PromptOptions & {
5
+ runControlRef?: AgentExactRunControlRef;
6
+ };
4
7
  export interface TangleExactProcessOptions {
5
8
  teamId?: string;
6
9
  }
@@ -53,9 +56,9 @@ export interface SandboxInstanceLike {
53
56
  name?: string;
54
57
  status?: unknown;
55
58
  metadata?: Record<string, unknown>;
56
- streamPrompt(message: string | InputPart[], options?: PromptOptions): AsyncIterable<SandboxEvent>;
57
- prompt?(message: string | InputPart[], options?: PromptOptions): Promise<PromptResult>;
58
- dispatchPrompt?(message: string | InputPart[], options?: PromptOptions): Promise<unknown>;
59
+ streamPrompt(message: string | InputPart[], options?: TanglePromptOptions): AsyncIterable<SandboxEvent>;
60
+ prompt?(message: string | InputPart[], options?: TanglePromptOptions): Promise<PromptResult>;
61
+ dispatchPrompt?(message: string | InputPart[], options?: TanglePromptOptions): Promise<unknown>;
59
62
  session?(id: string, options?: {
60
63
  signal?: AbortSignal;
61
64
  }): SandboxSessionLike;
@@ -122,13 +125,16 @@ export interface SandboxSessionLike {
122
125
  executionId?: string;
123
126
  signal?: AbortSignal;
124
127
  }): Promise<PromptResult>;
125
- prompt(message: string | InputPart[], options?: PromptOptions): Promise<PromptResult>;
128
+ prompt(message: string | InputPart[], options?: TanglePromptOptions): Promise<PromptResult>;
126
129
  interrupt(options?: {
127
130
  executionId?: string;
128
131
  signal?: AbortSignal;
129
132
  }): Promise<{
130
133
  cancelled: boolean;
131
134
  }>;
135
+ cancelRun?(request: AgentRunCancellationRequest, options?: {
136
+ signal?: AbortSignal;
137
+ }): Promise<AgentRunCancellationAcknowledgement>;
132
138
  }
133
139
  export interface TangleProviderOptions {
134
140
  client: SandboxClientLike;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-tangle",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,7 +67,7 @@
67
67
  "LICENSE"
68
68
  ],
69
69
  "dependencies": {
70
- "@tangle-network/agent-interface": "0.46.1"
70
+ "@tangle-network/agent-interface": "0.47.0"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@tangle-network/sandbox": ">=0.17.0 <1.0.0"
@@ -78,11 +78,12 @@
78
78
  }
79
79
  },
80
80
  "devDependencies": {
81
- "@tangle-network/sandbox": "0.17.0",
81
+ "@tangle-network/agent-runtime": "0.132.6",
82
+ "@tangle-network/sandbox": "0.19.4",
82
83
  "@types/node": "25.6.0",
83
84
  "typescript": "^6.0.3",
84
85
  "vitest": "^4.1.5",
85
- "@tangle-network/agent-provider-testkit": "0.6.1"
86
+ "@tangle-network/agent-provider-testkit": "0.6.2"
86
87
  },
87
88
  "scripts": {
88
89
  "build": "tsc -p tsconfig.json",