@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 +16 -4
- package/dist/tangle-capabilities.d.ts +32 -14
- package/dist/tangle-capabilities.js +130 -61
- package/dist/tangle-environment-control.d.ts +4 -1
- package/dist/tangle-environment-control.js +11 -10
- package/dist/tangle-environment-dispatch.js +49 -19
- package/dist/tangle-environment-session.d.ts +12 -3
- package/dist/tangle-environment-session.js +213 -60
- package/dist/tangle-environment-validation.d.ts +1 -8
- package/dist/tangle-environment-validation.js +0 -25
- package/dist/tangle-environment-values.d.ts +11 -1
- package/dist/tangle-environment-values.js +48 -13
- package/dist/tangle-environment.js +30 -57
- package/dist/tangle-events.d.ts +8 -0
- package/dist/tangle-events.js +164 -4
- package/dist/tangle-prompt.d.ts +2 -1
- package/dist/tangle-prompt.js +20 -3
- package/dist/tangle-provider.js +15 -8
- package/dist/tangle-session-control.d.ts +6 -5
- package/dist/tangle-session-control.js +31 -16
- package/dist/tangle-types.d.ts +15 -7
- package/package.json +5 -3
|
@@ -1,17 +1,45 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { AgentTurnInputSchema } from "@tangle-network/agent-interface";
|
|
2
|
-
import {
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 (
|
|
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
|
-
|
|
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, {
|
|
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 (
|
|
91
|
-
throw new Error("Tangle
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
-
...(
|
|
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
|
-
|
|
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) {
|
package/dist/tangle-events.d.ts
CHANGED
|
@@ -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;
|