@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.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @tangle-network/agent-provider-tangle
2
2
 
3
- Wraps `@tangle-network/sandbox` 0.17 or newer as an `AgentEnvironmentProvider`.
3
+ Wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`.
4
+ The peer range is `>=0.19.6 <1.0.0`; retained-run cancellation (`session.cancelRun`) first shipped in 0.19.6, and this package is developed and tested against 0.21.1.
4
5
 
5
6
  ```ts
6
7
  import { Sandbox } from '@tangle-network/sandbox'
@@ -13,14 +14,25 @@ const provider = createTangleProvider({
13
14
 
14
15
  Detached dispatch returns the immutable Sandbox execution receipt in `controlRef`.
15
16
  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.
17
+ 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
18
  Result, replay, and cancel operations select that exact execution instead of whichever execution most recently changed the shared session.
19
+ Session status with an exact control reference reports a state only when the payload names that execution; a payload bound to a different or unnamed execution reports `unknown`.
20
+
21
+ The provider claims `retainedControl` only from probed facts.
22
+ A lazy instance handle minted from the linked Sandbox SDK over the client's `fetch` transport must prove `dispatchPrompt`, `session`, and `cancelRun`, and the client must expose `get` for reconstruction; the probe sends no request and creates no resource.
23
+ The probe measures the linked SDK's method surface, not the connected service; service-side truth needs the sidecar capability endpoint and is a follow-up.
24
+ A client that cannot prove those facts gets no claim, so the runtime rejects retained dispatch before any sandbox is created.
25
+ Each concrete sandbox narrows the declared document independently against its own measured method surface, so a capable sandbox keeps retained control even when the provider-level claim failed closed.
26
+
27
+ Pass the SDK client itself when retained control matters.
28
+ An object-spread wrapper (`{ ...client }`) drops class prototype methods, including `fetch`, so the provider treats the wrapper as a non-SDK client and claims no retained control.
29
+ A wrapper must delegate the SDK methods instead of copying properties.
18
30
  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
31
  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
32
  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.
21
33
  It also rejects `contextTransfer` and `nativeContinuation` inputs explicitly until those operations have native Sandbox support instead of silently dropping them.
22
- The default adapter does not advertise legacy checkpoint or fork operations because Sandbox 0.17 exposes snapshots and branches with different semantics.
23
- A custom compatible client may opt into the legacy methods explicitly; durable workspace branching remains unadvertised until checkpoint lookup, retry, conflict, and cleanup are implemented together.
34
+ The adapter never advertises `branching.checkpoint` or `branching.fork`.
35
+ Sandbox exposes `snapshot`, `listSnapshots`, `deleteSnapshot`, and `branch(count)` with different semantics; durable workspace branching stays unadvertised until the full `AgentWorkspaceBranching` contract — retry, lookup, conflict, and cleanup together — is implemented over that surface.
24
36
 
25
37
  Pass `exactProcess: {}` only when the Sandbox deployment supports `agent: false` creates and reports `metadata.runtimeMode: "control"`.
26
38
  The optional capability creates an ephemeral sandbox with an authenticated control service but no managed agent workload or agent credentials, explicit resources, exact blocked/domain egress, bounded binary file reads, shell-free launch, and recoverable process output plus terminal reason.
@@ -4,36 +4,54 @@ import type { SandboxClientLike, SandboxInstanceLike } from "./tangle-types.js";
4
4
  * The full capability document this adapter supports when the Sandbox client
5
5
  * implements every optional method.
6
6
  *
7
- * This is an upper bound, not a claim. `capabilitiesForSandbox()` narrows it
8
- * to what a specific client actually exposes, because a capability the client
9
- * cannot back becomes an action the caller selects and finds missing.
7
+ * This is an upper bound, not a claim. `capabilitiesForClient()` and
8
+ * `capabilitiesForSandbox()` narrow it to what the deployment actually
9
+ * exposes, because a capability the client cannot back becomes an action the
10
+ * caller selects and finds missing.
10
11
  */
11
12
  export declare function defaultTangleSandboxCapabilities(harness?: HarnessType): AgentEnvironmentCapabilities;
12
- /** Optional methods whose absence must clear the matching declared capability. */
13
+ /**
14
+ * Deployment facts that gate declared capabilities. Every fact defaults to
15
+ * false when it cannot be established; a false fact clears the matching
16
+ * declared capability.
17
+ */
13
18
  export interface SandboxCapabilitySupport {
19
+ /** The provider can rebuild an environment by id (`client.get`). */
20
+ reconstruct: boolean;
14
21
  dispatchPrompt: boolean;
15
22
  session: boolean;
16
23
  read: boolean;
17
24
  write: boolean;
18
25
  exec: boolean;
19
- checkpoint: boolean;
20
- fork: boolean;
21
26
  placement: boolean;
22
27
  destroy: boolean;
28
+ cancelRun: boolean;
23
29
  }
24
30
  export declare function sandboxCapabilitySupport(box: SandboxInstanceLike, client: SandboxClientLike): SandboxCapabilitySupport;
25
31
  /**
26
- * Narrow provider-level claims to capabilities the client can actually back.
27
- *
28
- * A client without placement metadata cannot satisfy placement(), and this
29
- * adapter has no durable branching, interaction, or native-continuation
30
- * implementation regardless of an overly broad configured document.
32
+ * Establish client-stage facts before any sandbox exists. Two sources:
33
+ * the client's own members (get, describePlacement) and, for an SDK-backed
34
+ * client, the linked SDK surface via `linkedSdkProbeInstance`. Retained
35
+ * control fails closed: without a probe handle nothing proves `cancelRun`,
36
+ * so the provider must not claim it. Box-scoped workspace and streaming
37
+ * facts stay at the declared upper bound when no handle can be minted —
38
+ * each concrete sandbox re-narrows them in `capabilitiesForSandbox`.
31
39
  */
32
- export declare function capabilitiesForClient(declared: AgentEnvironmentCapabilities, client: SandboxClientLike): AgentEnvironmentCapabilities;
40
+ export declare function clientCapabilitySupport(client: SandboxClientLike): SandboxCapabilitySupport;
33
41
  /**
34
- * Narrow a declared capability document to what this Sandbox instance backs.
42
+ * Narrow a declared capability document to established facts.
35
43
  *
36
44
  * Braid derives product actions from these flags, so an over-claimed flag is
37
- * an offered action that throws at the moment the user selects it.
45
+ * an offered action that throws at the moment the user selects it. Retained
46
+ * control requires the complete fact set: exact dispatch, a session handle,
47
+ * canonical cancellation, and environment reconstruction by id.
38
48
  */
49
+ export declare function narrowedTangleCapabilities(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport): AgentEnvironmentCapabilities;
50
+ /**
51
+ * Narrow provider-level claims to facts the client can prove before any
52
+ * sandbox exists. `clientCapabilitySupport` documents which facts stay at
53
+ * the declared upper bound when the client offers no probe surface.
54
+ */
55
+ export declare function capabilitiesForClient(declared: AgentEnvironmentCapabilities, client: SandboxClientLike): AgentEnvironmentCapabilities;
56
+ /** Narrow a declared capability document to what this Sandbox instance backs. */
39
57
  export declare function capabilitiesForSandbox(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport): AgentEnvironmentCapabilities;
@@ -1,11 +1,13 @@
1
1
  import { harnessSystemPromptIntents } from "@tangle-network/agent-interface";
2
+ import { SandboxInstance } from "@tangle-network/sandbox";
2
3
  /**
3
4
  * The full capability document this adapter supports when the Sandbox client
4
5
  * implements every optional method.
5
6
  *
6
- * This is an upper bound, not a claim. `capabilitiesForSandbox()` narrows it
7
- * to what a specific client actually exposes, because a capability the client
8
- * cannot back becomes an action the caller selects and finds missing.
7
+ * This is an upper bound, not a claim. `capabilitiesForClient()` and
8
+ * `capabilitiesForSandbox()` narrow it to what the deployment actually
9
+ * exposes, because a capability the client cannot back becomes an action the
10
+ * caller selects and finds missing.
9
11
  */
10
12
  export function defaultTangleSandboxCapabilities(harness) {
11
13
  return {
@@ -31,10 +33,22 @@ export function defaultTangleSandboxCapabilities(harness) {
31
33
  validation: true,
32
34
  },
33
35
  streaming: { live: true, replay: true, detach: true, turnIdempotency: true },
34
- // A Sandbox session id is not a Braid context-boundary proof. Native
35
- // continuation stays unavailable until this adapter can verify one.
36
- sessions: { continue: false, list: false, messages: false },
36
+ // Retained control is declared as intent here and stripped by narrowing
37
+ // wherever the facts cannot prove dispatchPrompt, session, cancelRun,
38
+ // and environment reconstruction by id. The four sub-flags are
39
+ // all-or-nothing by design: this adapter implements the identities
40
+ // together over one Sandbox surface, and the capability schema refuses
41
+ // a partial block, so they stand or fall on the same probed fact set.
42
+ sessions: { continue: true, list: false, messages: false },
43
+ retainedControl: {
44
+ exactRunIdentity: true,
45
+ resultIdentity: true,
46
+ eventIdentity: true,
47
+ cancellationIdempotency: true,
48
+ },
37
49
  workspace: { read: true, write: true, exec: true, git: false, upload: true, download: true },
50
+ // Sandbox exposes snapshot/branch, not the checkpoint/fork contract, and
51
+ // durable branching needs retry, lookup, conflict, and cleanup together.
38
52
  branching: { checkpoint: false, fork: false },
39
53
  placement: true,
40
54
  usage: false,
@@ -43,98 +57,153 @@ export function defaultTangleSandboxCapabilities(harness) {
43
57
  confidential: false,
44
58
  };
45
59
  }
60
+ // One reserved id names both probe handles; neither ever reaches the service.
61
+ const CAPABILITY_PROBE_ID = "__tangle-capability-probe__";
46
62
  export function sandboxCapabilitySupport(box, client) {
63
+ let session;
64
+ if (typeof box.session === "function") {
65
+ try {
66
+ // Sandbox session handles are lazy. Inspecting one does not call the
67
+ // service, and keeps retained-control claims tied to the actual handle.
68
+ session = box.session(CAPABILITY_PROBE_ID);
69
+ }
70
+ catch {
71
+ // A client that cannot produce a session handle cannot prove retained control.
72
+ }
73
+ }
47
74
  return {
75
+ reconstruct: typeof client.get === "function",
48
76
  dispatchPrompt: typeof box.dispatchPrompt === "function",
49
77
  session: typeof box.session === "function",
50
78
  read: typeof box.read === "function",
51
79
  write: typeof box.write === "function",
52
80
  exec: typeof box.exec === "function",
53
- checkpoint: typeof box.checkpoint === "function",
54
- fork: typeof box.fork === "function",
55
81
  placement: typeof client.describePlacement === "function",
56
82
  destroy: typeof box.delete === "function",
83
+ cancelRun: typeof session?.cancelRun === "function",
57
84
  };
58
85
  }
59
86
  /**
60
- * Narrow provider-level claims to capabilities the client can actually back.
61
- *
62
- * A client without placement metadata cannot satisfy placement(), and this
63
- * adapter has no durable branching, interaction, or native-continuation
64
- * implementation regardless of an overly broad configured document.
87
+ * Mint a lazy instance handle from the sandbox SDK linked into this process.
88
+ * The handle measures the LINKED SDK's instance and session method surface —
89
+ * an adapter-capability fact, not deployment truth. It is valid exactly when
90
+ * the client is SDK-backed (carries the SDK `fetch` transport), because the
91
+ * sandboxes such a client returns are instances of these same classes.
92
+ * Deployment truth (what the connected service honors) needs the sidecar
93
+ * capability endpoint and is a follow-up. The handle and its probe session
94
+ * never leave the process: construction and `session(id)` are lazy in the
95
+ * SDK, so no request is sent and no billable resource is created.
65
96
  */
66
- export function capabilitiesForClient(declared, client) {
67
- const narrowed = {
68
- ...declared,
69
- streaming: {
70
- ...declared.streaming,
71
- replay: declared.streaming.replay,
72
- detach: declared.streaming.detach,
73
- turnIdempotency: declared.streaming.turnIdempotency,
74
- },
75
- sessions: { ...declared.sessions, continue: false, list: false, messages: false },
76
- workspace: { ...declared.workspace, git: false },
77
- usage: false,
78
- branching: {
79
- ...declared.branching,
80
- checkpoint: false,
81
- fork: false,
82
- ...(declared.branching.retrySafe !== undefined ? { retrySafe: false } : {}),
83
- ...(declared.branching.lookup !== undefined ? { lookup: false } : {}),
84
- ...(declared.branching.cleanup !== undefined ? { cleanup: false } : {}),
85
- },
86
- placement: declared.placement && typeof client.describePlacement === "function",
97
+ function linkedSdkProbeInstance(client) {
98
+ if (typeof client.fetch !== "function")
99
+ return undefined;
100
+ try {
101
+ return new SandboxInstance(client, {
102
+ id: CAPABILITY_PROBE_ID,
103
+ status: "stopped",
104
+ createdAt: new Date(0),
105
+ });
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ /**
112
+ * Establish client-stage facts before any sandbox exists. Two sources:
113
+ * the client's own members (get, describePlacement) and, for an SDK-backed
114
+ * client, the linked SDK surface via `linkedSdkProbeInstance`. Retained
115
+ * control fails closed: without a probe handle nothing proves `cancelRun`,
116
+ * so the provider must not claim it. Box-scoped workspace and streaming
117
+ * facts stay at the declared upper bound when no handle can be minted —
118
+ * each concrete sandbox re-narrows them in `capabilitiesForSandbox`.
119
+ */
120
+ export function clientCapabilitySupport(client) {
121
+ const probe = linkedSdkProbeInstance(client);
122
+ if (probe)
123
+ return sandboxCapabilitySupport(probe, client);
124
+ return {
125
+ reconstruct: typeof client.get === "function",
126
+ dispatchPrompt: true,
127
+ session: true,
128
+ read: true,
129
+ write: true,
130
+ exec: true,
131
+ placement: typeof client.describePlacement === "function",
132
+ destroy: true,
133
+ cancelRun: false,
87
134
  };
88
- delete narrowed.interactions;
89
- delete narrowed.retainedControl;
90
- delete narrowed.nativeContinuation;
91
- return narrowed;
92
135
  }
93
136
  /**
94
- * Narrow a declared capability document to what this Sandbox instance backs.
137
+ * Narrow a declared capability document to established facts.
95
138
  *
96
139
  * Braid derives product actions from these flags, so an over-claimed flag is
97
- * an offered action that throws at the moment the user selects it.
140
+ * an offered action that throws at the moment the user selects it. Retained
141
+ * control requires the complete fact set: exact dispatch, a session handle,
142
+ * canonical cancellation, and environment reconstruction by id.
98
143
  */
99
- export function capabilitiesForSandbox(declared, support) {
100
- const narrowed = { ...declared };
101
- delete narrowed.interactions;
102
- delete narrowed.retainedControl;
103
- delete narrowed.nativeContinuation;
104
- return {
105
- ...narrowed,
144
+ export function narrowedTangleCapabilities(declared, support) {
145
+ const supportsRetainedControl = declared.sessions.continue === true &&
146
+ declared.streaming.detach === true &&
147
+ declared.streaming.replay === true &&
148
+ declared.streaming.turnIdempotency === true &&
149
+ support.reconstruct &&
150
+ support.dispatchPrompt &&
151
+ support.session &&
152
+ support.cancelRun;
153
+ // A cleared fact forces false; a held fact passes the declared value
154
+ // through unchanged, so a malformed declaration still reaches the schema
155
+ // at the provider boundary instead of being laundered into a boolean.
156
+ const narrowed = {
157
+ ...declared,
106
158
  streaming: {
107
159
  ...declared.streaming,
108
- detach: declared.streaming.detach && support.dispatchPrompt,
109
- replay: declared.streaming.replay && support.session,
110
- turnIdempotency: declared.streaming.turnIdempotency && support.session,
160
+ detach: support.dispatchPrompt ? declared.streaming.detach : false,
161
+ replay: support.session ? declared.streaming.replay : false,
162
+ turnIdempotency: support.session
163
+ ? declared.streaming.turnIdempotency
164
+ : false,
111
165
  },
112
166
  sessions: {
113
167
  ...declared.sessions,
114
- continue: false,
168
+ continue: supportsRetainedControl ? declared.sessions.continue : false,
115
169
  list: false,
116
170
  messages: false,
117
171
  },
118
172
  workspace: {
119
173
  ...declared.workspace,
120
- read: declared.workspace.read && support.read,
121
- write: declared.workspace.write && support.write,
122
- exec: declared.workspace.exec && support.exec,
174
+ read: support.read ? declared.workspace.read : false,
175
+ write: support.write ? declared.workspace.write : false,
176
+ exec: support.exec ? declared.workspace.exec : false,
123
177
  git: false,
124
- upload: declared.workspace.upload && support.write,
125
- download: declared.workspace.download && support.read,
178
+ upload: support.write ? declared.workspace.upload : false,
179
+ download: support.read ? declared.workspace.download : false,
126
180
  },
127
181
  branching: {
128
- ...narrowed.branching,
182
+ ...declared.branching,
129
183
  checkpoint: false,
130
184
  fork: false,
131
- ...(declared.branching.retrySafe !== undefined
132
- ? { retrySafe: false }
133
- : {}),
185
+ ...(declared.branching.retrySafe !== undefined ? { retrySafe: false } : {}),
134
186
  ...(declared.branching.lookup !== undefined ? { lookup: false } : {}),
135
187
  ...(declared.branching.cleanup !== undefined ? { cleanup: false } : {}),
136
188
  },
137
- placement: narrowed.placement && support.placement,
189
+ placement: support.placement ? declared.placement : false,
138
190
  usage: false,
139
191
  };
192
+ delete narrowed.interactions;
193
+ delete narrowed.nativeContinuation;
194
+ if (!supportsRetainedControl)
195
+ delete narrowed.retainedControl;
196
+ return narrowed;
197
+ }
198
+ /**
199
+ * Narrow provider-level claims to facts the client can prove before any
200
+ * sandbox exists. `clientCapabilitySupport` documents which facts stay at
201
+ * the declared upper bound when the client offers no probe surface.
202
+ */
203
+ export function capabilitiesForClient(declared, client) {
204
+ return narrowedTangleCapabilities(declared, clientCapabilitySupport(client));
205
+ }
206
+ /** Narrow a declared capability document to what this Sandbox instance backs. */
207
+ export function capabilitiesForSandbox(declared, support) {
208
+ return narrowedTangleCapabilities(declared, support);
140
209
  }
@@ -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,17 +28,15 @@ export function hasReplayPayload(input) {
25
28
  input.providerOptions !== undefined);
26
29
  }
27
30
  export async function interruptAfterAbort(box, reference) {
28
- const sessionId = reference.id;
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;
29
36
  const executionId = reference.controlRef?.executionId;
30
37
  if (!box.session || executionId === undefined)
31
38
  return;
32
- try {
33
- await box.session(sessionId)?.interrupt({ executionId });
34
- }
35
- catch {
36
- // The original abort remains the caller-visible outcome; the provider has
37
- // no stronger cleanup primitive when the late dispatch result is lost.
38
- }
39
+ await interruptExecutionAfterAbort(box, reference.id, executionId);
39
40
  }
40
41
  export async function interruptExecutionAfterAbort(source, sessionId, executionId) {
41
42
  try {
@@ -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 {};