@tangle-network/agent-provider-tangle 0.4.5 → 0.4.7
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 +12 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +397 -72
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @tangle-network/agent-provider-tangle
|
|
2
2
|
|
|
3
|
-
Wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`.
|
|
3
|
+
Wraps `@tangle-network/sandbox` 0.17 or newer as an `AgentEnvironmentProvider`.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { Sandbox } from '@tangle-network/sandbox'
|
|
@@ -11,6 +11,17 @@ const provider = createTangleProvider({
|
|
|
11
11
|
})
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
Detached dispatch returns the immutable Sandbox execution receipt in `controlRef`.
|
|
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.
|
|
17
|
+
Result, replay, and cancel operations select that exact execution instead of whichever execution most recently changed the shared session.
|
|
18
|
+
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
|
+
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
|
+
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
|
+
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.
|
|
24
|
+
|
|
14
25
|
Pass `exactProcess: {}` only when the Sandbox deployment supports `agent: false` creates and reports `metadata.runtimeMode: "control"`.
|
|
15
26
|
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.
|
|
16
27
|
Set `teamId` inside `exactProcess` to scope create, lookup, and recovery to one team.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BackendType, CreateSandboxOptions, ExecResult as SandboxExecResult, PromptOptions, PromptResult, SandboxEvent } from "@tangle-network/sandbox";
|
|
2
|
-
import type
|
|
2
|
+
import { type AgentEnvironmentCapabilities, type AgentEnvironmentProvider, type CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
3
3
|
import type { InputPart } from "@tangle-network/agent-interface";
|
|
4
4
|
import { type TangleExactProcessOptions } from "./exact-process.js";
|
|
5
5
|
export type { TangleExactProcessOptions } from "./exact-process.js";
|
|
@@ -93,11 +93,16 @@ export interface SandboxSessionLike {
|
|
|
93
93
|
status(): Promise<unknown | null>;
|
|
94
94
|
events(options?: {
|
|
95
95
|
since?: string;
|
|
96
|
+
executionId?: string;
|
|
96
97
|
signal?: AbortSignal;
|
|
97
98
|
}): AsyncIterable<SandboxEvent>;
|
|
98
|
-
result(
|
|
99
|
+
result(options?: {
|
|
100
|
+
executionId?: string;
|
|
101
|
+
}): Promise<PromptResult>;
|
|
99
102
|
prompt(message: string | InputPart[], options?: PromptOptions): Promise<PromptResult>;
|
|
100
|
-
interrupt(
|
|
103
|
+
interrupt(options?: {
|
|
104
|
+
executionId?: string;
|
|
105
|
+
}): Promise<{
|
|
101
106
|
cancelled: boolean;
|
|
102
107
|
}>;
|
|
103
108
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { AgentEnvironmentCapabilitiesSchema, } from "@tangle-network/agent-interface/environment-provider";
|
|
3
|
+
import { AgentRunControlRefSchema, ContextTransferReceiptSchema, } from "@tangle-network/agent-interface";
|
|
1
4
|
import { createTangleExactProcessProvider, } from "./exact-process.js";
|
|
2
5
|
export function createTangleProvider(options) {
|
|
3
6
|
const providerName = options.name ?? "tangle-sandbox";
|
|
@@ -8,34 +11,41 @@ export function createTangleProvider(options) {
|
|
|
8
11
|
providerName,
|
|
9
12
|
})
|
|
10
13
|
: undefined;
|
|
14
|
+
const resolveCapabilities = async () => {
|
|
15
|
+
const configured = options.capabilities
|
|
16
|
+
? typeof options.capabilities === "function"
|
|
17
|
+
? await options.capabilities()
|
|
18
|
+
: options.capabilities
|
|
19
|
+
: defaultTangleSandboxCapabilities();
|
|
20
|
+
if (!exactProcess && configured.exactProcess) {
|
|
21
|
+
throw new Error("Tangle capabilities cannot advertise exactProcess without exactProcess configuration");
|
|
22
|
+
}
|
|
23
|
+
return AgentEnvironmentCapabilitiesSchema.parse(exactProcess
|
|
24
|
+
? {
|
|
25
|
+
...configured,
|
|
26
|
+
exactProcess: { egress: ["blocked", "strict"] },
|
|
27
|
+
}
|
|
28
|
+
: configured);
|
|
29
|
+
};
|
|
11
30
|
return {
|
|
12
31
|
name: providerName,
|
|
13
32
|
...(exactProcess ? { exactProcess } : {}),
|
|
14
|
-
capabilities:
|
|
15
|
-
const capabilities = options.capabilities
|
|
16
|
-
? typeof options.capabilities === "function"
|
|
17
|
-
? await options.capabilities()
|
|
18
|
-
: options.capabilities
|
|
19
|
-
: defaultTangleSandboxCapabilities();
|
|
20
|
-
if (!exactProcess && capabilities.exactProcess) {
|
|
21
|
-
throw new Error("Tangle capabilities cannot advertise exactProcess without exactProcess configuration");
|
|
22
|
-
}
|
|
23
|
-
return exactProcess
|
|
24
|
-
? { ...capabilities, exactProcess: { egress: ["blocked", "strict"] } }
|
|
25
|
-
: capabilities;
|
|
26
|
-
},
|
|
33
|
+
capabilities: resolveCapabilities,
|
|
27
34
|
...(options.validateProfile ? { validateProfile: options.validateProfile } : {}),
|
|
28
35
|
async create(input) {
|
|
36
|
+
const capabilities = await resolveCapabilities();
|
|
29
37
|
const createOptions = options.mapCreateInput?.(input) ??
|
|
30
38
|
sandboxOptionsFromCreateInput(input, options.defaultBackend ?? "opencode");
|
|
31
39
|
const box = await options.client.create(createOptions, input.signal ? { signal: input.signal } : undefined);
|
|
32
|
-
return sandboxInstanceAsEnvironment(box, providerName, options.client);
|
|
40
|
+
return sandboxInstanceAsEnvironment(box, providerName, options.client, capabilities);
|
|
33
41
|
},
|
|
34
42
|
...(options.client.get
|
|
35
43
|
? {
|
|
36
44
|
async get(id) {
|
|
37
45
|
const box = await options.client.get?.(id);
|
|
38
|
-
return box
|
|
46
|
+
return box
|
|
47
|
+
? sandboxInstanceAsEnvironment(box, providerName, options.client, await resolveCapabilities())
|
|
48
|
+
: null;
|
|
39
49
|
},
|
|
40
50
|
}
|
|
41
51
|
: {}),
|
|
@@ -55,7 +65,7 @@ export function createTangleProvider(options) {
|
|
|
55
65
|
: {}),
|
|
56
66
|
};
|
|
57
67
|
}
|
|
58
|
-
function sandboxInstanceAsEnvironment(box, providerName, client) {
|
|
68
|
+
function sandboxInstanceAsEnvironment(box, providerName, client, capabilities) {
|
|
59
69
|
return {
|
|
60
70
|
id: String(box.id),
|
|
61
71
|
provider: providerName,
|
|
@@ -65,44 +75,60 @@ function sandboxInstanceAsEnvironment(box, providerName, client) {
|
|
|
65
75
|
return statusFromUnknown(box.status);
|
|
66
76
|
},
|
|
67
77
|
async *stream(input) {
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
const expectedExecutionId = executionIdFromTurnInput(input);
|
|
79
|
+
const expectedSessionId = input.sessionId ?? input.controlRef?.sessionId;
|
|
80
|
+
for await (const event of box.streamPrompt(promptFromTurnInput(input), promptOptionsFromTurnInput(input, {
|
|
81
|
+
provider: providerName,
|
|
82
|
+
environmentId: String(box.id),
|
|
83
|
+
}))) {
|
|
84
|
+
yield environmentEventFromSandboxEvent(event, {
|
|
85
|
+
executionId: expectedExecutionId,
|
|
86
|
+
sessionId: expectedSessionId,
|
|
87
|
+
});
|
|
70
88
|
}
|
|
71
89
|
},
|
|
72
|
-
...(box.dispatchPrompt
|
|
90
|
+
...(capabilities.streaming.detach && box.dispatchPrompt
|
|
73
91
|
? {
|
|
74
92
|
async dispatch(input) {
|
|
75
|
-
const dispatched = await box.dispatchPrompt?.(promptFromTurnInput(input), promptOptionsFromTurnInput(input
|
|
76
|
-
|
|
93
|
+
const dispatched = await box.dispatchPrompt?.(promptFromTurnInput(input), promptOptionsFromTurnInput(input, {
|
|
94
|
+
provider: providerName,
|
|
95
|
+
environmentId: String(box.id),
|
|
96
|
+
}));
|
|
97
|
+
return sessionRefFromSandboxDispatch(dispatched, providerName, String(box.id), executionIdFromTurnInput(input));
|
|
77
98
|
},
|
|
78
99
|
}
|
|
79
100
|
: {}),
|
|
80
|
-
...(
|
|
101
|
+
...((capabilities.sessions.continue ||
|
|
102
|
+
capabilities.streaming.replay ||
|
|
103
|
+
capabilities.streaming.detach) &&
|
|
104
|
+
box.session
|
|
81
105
|
? {
|
|
82
|
-
session(id) {
|
|
106
|
+
session(id, options) {
|
|
83
107
|
const session = box.session?.(id);
|
|
84
108
|
if (!session)
|
|
85
109
|
throw new Error("sandbox session(id) returned undefined");
|
|
86
|
-
return sandboxSessionAsAgentSession(session);
|
|
110
|
+
return sandboxSessionAsAgentSession(session, resolveRetainedSessionControlRef(options?.controlRef, session.id, providerName, String(box.id)), providerName, String(box.id));
|
|
87
111
|
},
|
|
88
112
|
}
|
|
89
113
|
: {}),
|
|
90
|
-
...(
|
|
91
|
-
|
|
114
|
+
...(capabilities.workspace.read && box.read
|
|
115
|
+
? { read: box.read.bind(box) }
|
|
116
|
+
: {}),
|
|
117
|
+
...(capabilities.workspace.write && box.write
|
|
92
118
|
? {
|
|
93
119
|
async write(path, content) {
|
|
94
120
|
await box.write?.(path, content);
|
|
95
121
|
},
|
|
96
122
|
}
|
|
97
123
|
: {}),
|
|
98
|
-
...(box.exec
|
|
124
|
+
...(capabilities.workspace.exec && box.exec
|
|
99
125
|
? {
|
|
100
126
|
async exec(command, options) {
|
|
101
127
|
return execResultFromSandboxExecResult(await box.exec?.(command, options));
|
|
102
128
|
},
|
|
103
129
|
}
|
|
104
130
|
: {}),
|
|
105
|
-
...(box.checkpoint
|
|
131
|
+
...(capabilities.branching.checkpoint && box.checkpoint
|
|
106
132
|
? {
|
|
107
133
|
async checkpoint(options) {
|
|
108
134
|
const result = await box.checkpoint?.(options);
|
|
@@ -110,19 +136,23 @@ function sandboxInstanceAsEnvironment(box, providerName, client) {
|
|
|
110
136
|
},
|
|
111
137
|
}
|
|
112
138
|
: {}),
|
|
113
|
-
...(box.fork
|
|
139
|
+
...(capabilities.branching.fork && box.fork
|
|
114
140
|
? {
|
|
115
141
|
async fork(checkpoint, options) {
|
|
116
142
|
const forked = await box.fork?.(checkpoint.id, options);
|
|
117
143
|
if (!forked)
|
|
118
144
|
throw new Error("sandbox fork returned no environment");
|
|
119
|
-
return sandboxInstanceAsEnvironment(forked, providerName, client);
|
|
145
|
+
return sandboxInstanceAsEnvironment(forked, providerName, client, capabilities);
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
...(capabilities.placement
|
|
150
|
+
? {
|
|
151
|
+
async placement() {
|
|
152
|
+
return placementInfoFromLoopPlacement(client.describePlacement?.(box), box);
|
|
120
153
|
},
|
|
121
154
|
}
|
|
122
155
|
: {}),
|
|
123
|
-
async placement() {
|
|
124
|
-
return placementInfoFromLoopPlacement(client.describePlacement?.(box), box);
|
|
125
|
-
},
|
|
126
156
|
async refresh() {
|
|
127
157
|
await box.refresh?.();
|
|
128
158
|
},
|
|
@@ -131,9 +161,13 @@ function sandboxInstanceAsEnvironment(box, providerName, client) {
|
|
|
131
161
|
},
|
|
132
162
|
};
|
|
133
163
|
}
|
|
134
|
-
function sandboxSessionAsAgentSession(session) {
|
|
164
|
+
function sandboxSessionAsAgentSession(session, controlRef, provider, environmentId) {
|
|
165
|
+
let activeControlRef = controlRef;
|
|
135
166
|
return {
|
|
136
167
|
id: session.id,
|
|
168
|
+
get controlRef() {
|
|
169
|
+
return activeControlRef;
|
|
170
|
+
},
|
|
137
171
|
async status() {
|
|
138
172
|
const status = await session.status();
|
|
139
173
|
if (!status)
|
|
@@ -141,17 +175,105 @@ function sandboxSessionAsAgentSession(session) {
|
|
|
141
175
|
return sessionStatusFromUnknown(status.status);
|
|
142
176
|
},
|
|
143
177
|
async *events(options) {
|
|
144
|
-
|
|
145
|
-
|
|
178
|
+
if (options?.executionId !== undefined &&
|
|
179
|
+
activeControlRef?.executionId !== undefined &&
|
|
180
|
+
options.executionId !== activeControlRef.executionId) {
|
|
181
|
+
throw new Error("Tangle replay executionId conflicts with the control reference");
|
|
182
|
+
}
|
|
183
|
+
const executionId = activeControlRef?.executionId ?? options?.executionId;
|
|
184
|
+
if (options?.since !== undefined && executionId === undefined) {
|
|
185
|
+
throw new Error("Tangle cursor replay requires an exact executionId from its control reference");
|
|
186
|
+
}
|
|
187
|
+
const seenEventIds = new Set();
|
|
188
|
+
for await (const event of session.events({
|
|
189
|
+
...(options?.since !== undefined ? { since: options.since } : {}),
|
|
190
|
+
...(executionId !== undefined ? { executionId } : {}),
|
|
191
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
192
|
+
})) {
|
|
193
|
+
if (options?.since !== undefined && event.id === options.since)
|
|
194
|
+
continue;
|
|
195
|
+
const converted = environmentEventFromSandboxEvent(event, {
|
|
196
|
+
executionId,
|
|
197
|
+
sessionId: session.id,
|
|
198
|
+
});
|
|
199
|
+
if (executionId !== undefined && converted.id === undefined) {
|
|
200
|
+
throw new Error("Tangle exact session replay received an event without a stable id");
|
|
201
|
+
}
|
|
202
|
+
if (converted.id !== undefined) {
|
|
203
|
+
if (seenEventIds.has(converted.id)) {
|
|
204
|
+
throw new Error(`Tangle session replay repeated event id ${converted.id}`);
|
|
205
|
+
}
|
|
206
|
+
seenEventIds.add(converted.id);
|
|
207
|
+
}
|
|
208
|
+
yield converted;
|
|
209
|
+
}
|
|
146
210
|
},
|
|
147
211
|
async result() {
|
|
148
|
-
|
|
212
|
+
const expectedExecutionId = activeControlRef?.executionId;
|
|
213
|
+
if (expectedExecutionId === undefined) {
|
|
214
|
+
throw new Error("Tangle session result requires an exact executionId from its control reference");
|
|
215
|
+
}
|
|
216
|
+
const result = await session.result({ executionId: expectedExecutionId });
|
|
217
|
+
const resultRecord = validatedSandboxPromptResult(result);
|
|
218
|
+
if (resultRecord.executionId !== expectedExecutionId) {
|
|
219
|
+
throw new Error("Tangle session result did not confirm its exact executionId");
|
|
220
|
+
}
|
|
221
|
+
return agentTurnResultFromPromptRecord(resultRecord);
|
|
149
222
|
},
|
|
150
223
|
async prompt(input) {
|
|
151
|
-
|
|
224
|
+
if (input.sessionId !== undefined && input.sessionId !== session.id) {
|
|
225
|
+
throw new Error("Tangle sessionId conflicts with this session");
|
|
226
|
+
}
|
|
227
|
+
const requestedControlRef = resolveRetainedSessionControlRef(input.controlRef, session.id, provider, environmentId);
|
|
228
|
+
if (activeControlRef !== undefined &&
|
|
229
|
+
requestedControlRef !== undefined &&
|
|
230
|
+
!sameRunControlRef(activeControlRef, requestedControlRef)) {
|
|
231
|
+
throw new Error("Tangle prompt control reference conflicts with this session");
|
|
232
|
+
}
|
|
233
|
+
const sourceControlRef = requestedControlRef ?? activeControlRef;
|
|
234
|
+
const replay = input.lastEventId !== undefined;
|
|
235
|
+
if (replay &&
|
|
236
|
+
sourceControlRef?.executionId !== undefined &&
|
|
237
|
+
input.executionId !== undefined &&
|
|
238
|
+
input.executionId !== sourceControlRef.executionId) {
|
|
239
|
+
throw new Error("Tangle replay executionId conflicts with the control reference");
|
|
240
|
+
}
|
|
241
|
+
const executionId = replay
|
|
242
|
+
? input.executionId ?? sourceControlRef?.executionId
|
|
243
|
+
: input.executionId ??
|
|
244
|
+
sessionPromptExecutionId(provider, environmentId, session.id, input.turnId);
|
|
245
|
+
if (executionId === undefined) {
|
|
246
|
+
throw new Error("Tangle session replay requires the exact executionId from its control reference");
|
|
247
|
+
}
|
|
248
|
+
const result = await session.prompt(promptFromTurnInput(input), promptOptionsFromTurnInput({
|
|
249
|
+
...input,
|
|
250
|
+
sessionId: session.id,
|
|
251
|
+
executionId,
|
|
252
|
+
controlRef: undefined,
|
|
253
|
+
}, {
|
|
254
|
+
provider,
|
|
255
|
+
environmentId,
|
|
256
|
+
sessionId: session.id,
|
|
257
|
+
}));
|
|
258
|
+
const resultRecord = validatedSandboxPromptResult(result);
|
|
259
|
+
if (resultRecord.executionId !== executionId) {
|
|
260
|
+
throw new Error("Tangle session prompt did not confirm its exact executionId");
|
|
261
|
+
}
|
|
262
|
+
if (replay) {
|
|
263
|
+
activeControlRef =
|
|
264
|
+
sourceControlRef ??
|
|
265
|
+
retainedSessionControlRef(session.id, executionId, provider, environmentId);
|
|
266
|
+
return agentTurnResultFromPromptRecord(resultRecord);
|
|
267
|
+
}
|
|
268
|
+
activeControlRef = retainedSessionControlRef(session.id, executionId, provider, environmentId);
|
|
269
|
+
return agentTurnResultFromPromptRecord(resultRecord);
|
|
152
270
|
},
|
|
153
271
|
async cancel() {
|
|
154
|
-
|
|
272
|
+
const executionId = activeControlRef?.executionId;
|
|
273
|
+
if (executionId === undefined) {
|
|
274
|
+
throw new Error("Tangle session cancellation requires an exact executionId from its control reference");
|
|
275
|
+
}
|
|
276
|
+
await session.interrupt({ executionId });
|
|
155
277
|
},
|
|
156
278
|
};
|
|
157
279
|
}
|
|
@@ -188,14 +310,42 @@ function inlineAgentProfile(profile) {
|
|
|
188
310
|
}
|
|
189
311
|
return profile;
|
|
190
312
|
}
|
|
191
|
-
function environmentEventFromSandboxEvent(event) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
313
|
+
function environmentEventFromSandboxEvent(event, expected = {}) {
|
|
314
|
+
if (!event || typeof event !== "object") {
|
|
315
|
+
throw new Error("Tangle Sandbox emitted a non-object event");
|
|
316
|
+
}
|
|
317
|
+
const record = event;
|
|
318
|
+
if (typeof record.type !== "string" || record.type.length === 0) {
|
|
319
|
+
throw new Error("Tangle Sandbox event omitted its type");
|
|
320
|
+
}
|
|
321
|
+
if (!record.data ||
|
|
322
|
+
typeof record.data !== "object" ||
|
|
323
|
+
Array.isArray(record.data)) {
|
|
324
|
+
throw new Error("Tangle Sandbox event omitted its object data");
|
|
325
|
+
}
|
|
326
|
+
if (record.id !== undefined &&
|
|
327
|
+
(typeof record.id !== "string" || record.id.length === 0)) {
|
|
328
|
+
throw new Error("Tangle Sandbox event contained an invalid event id");
|
|
329
|
+
}
|
|
330
|
+
const data = record.data;
|
|
331
|
+
const eventExecutionId = optionalNonEmptyString(data.executionId, "Tangle Sandbox event executionId");
|
|
332
|
+
const eventSessionId = optionalNonEmptyString(data.sessionId, "Tangle Sandbox event sessionId");
|
|
333
|
+
// Sandbox binds the stream with session.events({ executionId }). Individual
|
|
334
|
+
// event variants do not all repeat that selector, so validate IDs when present.
|
|
335
|
+
if (expected.executionId !== undefined &&
|
|
336
|
+
eventExecutionId !== undefined &&
|
|
337
|
+
eventExecutionId !== expected.executionId) {
|
|
338
|
+
throw new Error("Tangle exact session event identified a different executionId");
|
|
339
|
+
}
|
|
340
|
+
if (expected.sessionId !== undefined &&
|
|
341
|
+
eventSessionId !== undefined &&
|
|
342
|
+
eventSessionId !== expected.sessionId) {
|
|
343
|
+
throw new Error("Tangle exact session event identified a different sessionId");
|
|
344
|
+
}
|
|
195
345
|
return {
|
|
196
|
-
type:
|
|
346
|
+
type: record.type,
|
|
197
347
|
data,
|
|
198
|
-
...(
|
|
348
|
+
...(typeof record.id === "string" ? { id: record.id } : {}),
|
|
199
349
|
usage: tokenUsageFromData(data),
|
|
200
350
|
providerEvent: event,
|
|
201
351
|
};
|
|
@@ -205,21 +355,102 @@ function promptFromTurnInput(input) {
|
|
|
205
355
|
return input.parts;
|
|
206
356
|
return input.prompt ?? "";
|
|
207
357
|
}
|
|
208
|
-
function
|
|
358
|
+
function executionIdFromTurnInput(input) {
|
|
359
|
+
return input.executionId ?? input.controlRef?.executionId;
|
|
360
|
+
}
|
|
361
|
+
function promptOptionsFromTurnInput(input, target) {
|
|
362
|
+
if (input.contextTransfer !== undefined) {
|
|
363
|
+
throw new Error("Tangle provider does not yet support portable context transfer");
|
|
364
|
+
}
|
|
365
|
+
if (input.nativeContinuation !== undefined) {
|
|
366
|
+
throw new Error("Tangle provider does not yet support verified native continuation");
|
|
367
|
+
}
|
|
368
|
+
const controlRef = input.controlRef
|
|
369
|
+
? AgentRunControlRefSchema.parse(input.controlRef)
|
|
370
|
+
: undefined;
|
|
371
|
+
if (controlRef) {
|
|
372
|
+
if (controlRef.provider !== target.provider ||
|
|
373
|
+
controlRef.environmentId !== target.environmentId ||
|
|
374
|
+
(target.sessionId !== undefined &&
|
|
375
|
+
controlRef.sessionId !== target.sessionId)) {
|
|
376
|
+
throw new Error("Tangle control reference does not match this target");
|
|
377
|
+
}
|
|
378
|
+
if (controlRef.sessionId === undefined || controlRef.executionId === undefined) {
|
|
379
|
+
throw new Error("Tangle control reference requires exact sessionId and executionId");
|
|
380
|
+
}
|
|
381
|
+
if (controlRef.runId !== controlRef.executionId) {
|
|
382
|
+
throw new Error("Tangle control reference requires runId to equal executionId");
|
|
383
|
+
}
|
|
384
|
+
if (input.sessionId !== undefined &&
|
|
385
|
+
input.sessionId !== controlRef.sessionId) {
|
|
386
|
+
throw new Error("Tangle sessionId conflicts with the control reference");
|
|
387
|
+
}
|
|
388
|
+
if (input.executionId !== undefined &&
|
|
389
|
+
input.executionId !== controlRef.executionId) {
|
|
390
|
+
throw new Error("Tangle executionId conflicts with the control reference");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const sessionId = input.sessionId ?? controlRef?.sessionId;
|
|
394
|
+
const executionId = input.executionId ?? controlRef?.executionId;
|
|
209
395
|
return {
|
|
210
|
-
...(
|
|
396
|
+
...(sessionId ? { sessionId } : {}),
|
|
211
397
|
...(input.model ? { model: input.model } : {}),
|
|
212
|
-
...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
|
|
398
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
213
399
|
...(input.context ? { context: input.context } : {}),
|
|
214
400
|
...(input.signal ? { signal: input.signal } : {}),
|
|
215
|
-
...(
|
|
401
|
+
...(executionId ? { executionId } : {}),
|
|
216
402
|
...(input.lastEventId ? { lastEventId: input.lastEventId } : {}),
|
|
217
403
|
...(input.turnId ? { turnId: input.turnId } : {}),
|
|
218
404
|
...(input.detach !== undefined ? { detach: input.detach } : {}),
|
|
219
405
|
};
|
|
220
406
|
}
|
|
221
|
-
function
|
|
407
|
+
function validatedSandboxPromptResult(result) {
|
|
408
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
409
|
+
throw new Error("Tangle prompt returned no result object");
|
|
410
|
+
}
|
|
222
411
|
const record = result;
|
|
412
|
+
if (typeof record.success !== "boolean") {
|
|
413
|
+
throw new Error("Tangle prompt result omitted its success status");
|
|
414
|
+
}
|
|
415
|
+
const statuses = new Set([
|
|
416
|
+
"success",
|
|
417
|
+
"failed",
|
|
418
|
+
"blocked_on_approval",
|
|
419
|
+
"awaiting_question",
|
|
420
|
+
"awaiting_plan_decision",
|
|
421
|
+
]);
|
|
422
|
+
if (typeof record.status !== "string" ||
|
|
423
|
+
!statuses.has(record.status)) {
|
|
424
|
+
throw new Error("Tangle prompt result contained an invalid run status");
|
|
425
|
+
}
|
|
426
|
+
if (record.success !== (record.status === "success")) {
|
|
427
|
+
throw new Error("Tangle prompt result success flag conflicts with its run status");
|
|
428
|
+
}
|
|
429
|
+
if (typeof record.durationMs !== "number" ||
|
|
430
|
+
!Number.isFinite(record.durationMs) ||
|
|
431
|
+
record.durationMs < 0) {
|
|
432
|
+
throw new Error("Tangle prompt result contained an invalid duration");
|
|
433
|
+
}
|
|
434
|
+
for (const field of [
|
|
435
|
+
"executionId",
|
|
436
|
+
"response",
|
|
437
|
+
"text",
|
|
438
|
+
"finalText",
|
|
439
|
+
"error",
|
|
440
|
+
"errorCode",
|
|
441
|
+
"traceId",
|
|
442
|
+
]) {
|
|
443
|
+
if (record[field] !== undefined && typeof record[field] !== "string") {
|
|
444
|
+
throw new Error(`Tangle prompt result contained an invalid ${field}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (record.executionId === "") {
|
|
448
|
+
throw new Error("Tangle prompt result contained an empty executionId");
|
|
449
|
+
}
|
|
450
|
+
tokenUsageFromData(record);
|
|
451
|
+
return record;
|
|
452
|
+
}
|
|
453
|
+
function agentTurnResultFromPromptRecord(record) {
|
|
223
454
|
const text = typeof record.response === "string"
|
|
224
455
|
? record.response
|
|
225
456
|
: typeof record.text === "string"
|
|
@@ -227,15 +458,22 @@ function agentTurnResultFromPromptResult(result) {
|
|
|
227
458
|
: typeof record.finalText === "string"
|
|
228
459
|
? record.finalText
|
|
229
460
|
: "";
|
|
230
|
-
const
|
|
461
|
+
const contextTransferReceipt = ContextTransferReceiptSchema.safeParse(record.contextTransferReceipt);
|
|
462
|
+
if (record.contextTransferReceipt !== undefined &&
|
|
463
|
+
!contextTransferReceipt.success) {
|
|
464
|
+
throw new Error("Tangle prompt result contained an invalid context receipt");
|
|
465
|
+
}
|
|
231
466
|
return {
|
|
232
467
|
text,
|
|
233
|
-
success,
|
|
468
|
+
success: record.success,
|
|
234
469
|
...(typeof record.error === "string" ? { error: record.error } : {}),
|
|
235
470
|
usage: tokenUsageFromData(record),
|
|
471
|
+
...(contextTransferReceipt.success
|
|
472
|
+
? { contextTransferReceipt: contextTransferReceipt.data }
|
|
473
|
+
: {}),
|
|
236
474
|
};
|
|
237
475
|
}
|
|
238
|
-
function sessionRefFromSandboxDispatch(dispatched, providerName) {
|
|
476
|
+
function sessionRefFromSandboxDispatch(dispatched, providerName, environmentId, expectedExecutionId) {
|
|
239
477
|
const record = dispatched && typeof dispatched === "object"
|
|
240
478
|
? dispatched
|
|
241
479
|
: undefined;
|
|
@@ -243,21 +481,91 @@ function sessionRefFromSandboxDispatch(dispatched, providerName) {
|
|
|
243
481
|
if (typeof id !== "string" || id.length === 0 || !record) {
|
|
244
482
|
throw new Error("sandbox dispatch returned no session id");
|
|
245
483
|
}
|
|
484
|
+
const executionId = nonEmptyString(record.executionId);
|
|
485
|
+
if (executionId === undefined) {
|
|
486
|
+
throw new Error("sandbox dispatch returned no exact execution id for durable replay");
|
|
487
|
+
}
|
|
488
|
+
if (expectedExecutionId !== undefined &&
|
|
489
|
+
executionId !== expectedExecutionId) {
|
|
490
|
+
throw new Error("sandbox dispatch returned an execution id different from the requested run");
|
|
491
|
+
}
|
|
246
492
|
return {
|
|
247
493
|
id,
|
|
248
494
|
provider: providerName,
|
|
495
|
+
controlRef: retainedSessionControlRef(id, executionId, providerName, environmentId),
|
|
249
496
|
metadata: {
|
|
250
497
|
...(record.status ? { status: record.status } : {}),
|
|
251
498
|
...(record.alreadyExisted !== undefined ? { alreadyExisted: record.alreadyExisted } : {}),
|
|
499
|
+
...(record.dispatched !== undefined ? { dispatched: record.dispatched } : {}),
|
|
252
500
|
},
|
|
253
501
|
};
|
|
254
502
|
}
|
|
503
|
+
function retainedSessionControlRef(sessionId, executionId, provider, environmentId) {
|
|
504
|
+
return AgentRunControlRefSchema.parse({
|
|
505
|
+
runId: executionId,
|
|
506
|
+
provider,
|
|
507
|
+
environmentId,
|
|
508
|
+
sessionId,
|
|
509
|
+
executionId,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function sessionPromptExecutionId(provider, environmentId, sessionId, turnId) {
|
|
513
|
+
if (turnId === undefined)
|
|
514
|
+
return randomUUID();
|
|
515
|
+
const digest = createHash("sha256")
|
|
516
|
+
.update(`${provider}\0${environmentId}\0${sessionId}\0${turnId}`)
|
|
517
|
+
.digest("hex");
|
|
518
|
+
return `session-turn-${digest}`;
|
|
519
|
+
}
|
|
520
|
+
function sameRunControlRef(left, right) {
|
|
521
|
+
return (left.runId === right.runId &&
|
|
522
|
+
left.provider === right.provider &&
|
|
523
|
+
left.environmentId === right.environmentId &&
|
|
524
|
+
left.sessionId === right.sessionId &&
|
|
525
|
+
left.executionId === right.executionId);
|
|
526
|
+
}
|
|
527
|
+
function resolveRetainedSessionControlRef(candidate, sessionId, provider, environmentId) {
|
|
528
|
+
if (candidate === undefined)
|
|
529
|
+
return undefined;
|
|
530
|
+
const controlRef = AgentRunControlRefSchema.parse(candidate);
|
|
531
|
+
if (controlRef.provider !== provider ||
|
|
532
|
+
controlRef.environmentId !== environmentId ||
|
|
533
|
+
controlRef.sessionId !== sessionId) {
|
|
534
|
+
throw new Error("Tangle control reference does not match this session");
|
|
535
|
+
}
|
|
536
|
+
if (controlRef.executionId === undefined ||
|
|
537
|
+
controlRef.runId !== controlRef.executionId) {
|
|
538
|
+
throw new Error("Tangle session control reference requires runId to equal executionId");
|
|
539
|
+
}
|
|
540
|
+
return controlRef;
|
|
541
|
+
}
|
|
542
|
+
function nonEmptyString(value) {
|
|
543
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
544
|
+
}
|
|
545
|
+
function optionalNonEmptyString(value, label) {
|
|
546
|
+
if (value === undefined)
|
|
547
|
+
return undefined;
|
|
548
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
549
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
550
|
+
}
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
255
553
|
function execResultFromSandboxExecResult(result) {
|
|
256
|
-
|
|
554
|
+
if (!result || typeof result !== "object") {
|
|
555
|
+
throw new Error("Tangle Sandbox exec returned no result");
|
|
556
|
+
}
|
|
557
|
+
const record = result;
|
|
558
|
+
if (typeof record.exitCode !== "number" ||
|
|
559
|
+
!Number.isSafeInteger(record.exitCode)) {
|
|
560
|
+
throw new Error("Tangle Sandbox exec returned an invalid exit code");
|
|
561
|
+
}
|
|
562
|
+
if (typeof record.stdout !== "string" || typeof record.stderr !== "string") {
|
|
563
|
+
throw new Error("Tangle Sandbox exec returned invalid output streams");
|
|
564
|
+
}
|
|
257
565
|
return {
|
|
258
|
-
exitCode:
|
|
259
|
-
stdout:
|
|
260
|
-
stderr:
|
|
566
|
+
exitCode: record.exitCode,
|
|
567
|
+
stdout: record.stdout,
|
|
568
|
+
stderr: record.stderr,
|
|
261
569
|
};
|
|
262
570
|
}
|
|
263
571
|
function checkpointIdFromResult(result) {
|
|
@@ -280,22 +588,26 @@ function placementInfoFromLoopPlacement(placement, box) {
|
|
|
280
588
|
};
|
|
281
589
|
}
|
|
282
590
|
function tokenUsageFromData(data) {
|
|
591
|
+
if (data.usage !== undefined &&
|
|
592
|
+
(!data.usage || typeof data.usage !== "object" || Array.isArray(data.usage))) {
|
|
593
|
+
throw new Error("Tangle usage must be an object");
|
|
594
|
+
}
|
|
595
|
+
if (data.tokenUsage !== undefined &&
|
|
596
|
+
(!data.tokenUsage ||
|
|
597
|
+
typeof data.tokenUsage !== "object" ||
|
|
598
|
+
Array.isArray(data.tokenUsage))) {
|
|
599
|
+
throw new Error("Tangle token usage must be an object");
|
|
600
|
+
}
|
|
283
601
|
const usageRecord = data.usage && typeof data.usage === "object"
|
|
284
602
|
? data.usage
|
|
285
603
|
: data.tokenUsage && typeof data.tokenUsage === "object"
|
|
286
604
|
? data.tokenUsage
|
|
287
605
|
: data;
|
|
288
|
-
const inputTokens =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
finiteNumber(usageRecord.completion_tokens);
|
|
294
|
-
const cost = finiteNumber(usageRecord.cost) ??
|
|
295
|
-
finiteNumber(usageRecord.costUsd) ??
|
|
296
|
-
finiteNumber(usageRecord.totalCostUsd) ??
|
|
297
|
-
finiteNumber(data.costUsd) ??
|
|
298
|
-
finiteNumber(data.totalCostUsd);
|
|
606
|
+
const inputTokens = firstValidatedNumber(usageRecord, ["inputTokens", "tokensIn", "prompt_tokens"], "input token count", true);
|
|
607
|
+
const outputTokens = firstValidatedNumber(usageRecord, ["outputTokens", "tokensOut", "completion_tokens"], "output token count", true);
|
|
608
|
+
const nestedCost = firstValidatedNumber(usageRecord, ["cost", "costUsd", "totalCostUsd"], "usage cost", false);
|
|
609
|
+
const topLevelCost = firstValidatedNumber(data, ["costUsd", "totalCostUsd"], "result cost", false);
|
|
610
|
+
const cost = nestedCost ?? topLevelCost;
|
|
299
611
|
if (inputTokens === undefined && outputTokens === undefined && cost === undefined)
|
|
300
612
|
return undefined;
|
|
301
613
|
return {
|
|
@@ -304,6 +616,22 @@ function tokenUsageFromData(data) {
|
|
|
304
616
|
...(cost !== undefined ? { cost } : {}),
|
|
305
617
|
};
|
|
306
618
|
}
|
|
619
|
+
function firstValidatedNumber(record, fields, label, integer) {
|
|
620
|
+
let selected;
|
|
621
|
+
for (const field of fields) {
|
|
622
|
+
const value = record[field];
|
|
623
|
+
if (value === undefined)
|
|
624
|
+
continue;
|
|
625
|
+
if (typeof value !== "number" ||
|
|
626
|
+
!Number.isFinite(value) ||
|
|
627
|
+
value < 0 ||
|
|
628
|
+
(integer && !Number.isSafeInteger(value))) {
|
|
629
|
+
throw new Error(`Tangle ${label} is invalid`);
|
|
630
|
+
}
|
|
631
|
+
selected ??= value;
|
|
632
|
+
}
|
|
633
|
+
return selected;
|
|
634
|
+
}
|
|
307
635
|
function statusFromUnknown(status) {
|
|
308
636
|
if (status === "pending" || status === "provisioning" || status === "running")
|
|
309
637
|
return status;
|
|
@@ -318,9 +646,6 @@ function sessionStatusFromUnknown(status) {
|
|
|
318
646
|
return status;
|
|
319
647
|
return statusFromUnknown(status);
|
|
320
648
|
}
|
|
321
|
-
function finiteNumber(value) {
|
|
322
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
323
|
-
}
|
|
324
649
|
export function defaultTangleSandboxCapabilities() {
|
|
325
650
|
return {
|
|
326
651
|
profile: {
|
|
@@ -347,7 +672,7 @@ export function defaultTangleSandboxCapabilities() {
|
|
|
347
672
|
streaming: { live: true, replay: true, detach: true, turnIdempotency: true },
|
|
348
673
|
sessions: { continue: true, list: true, messages: true },
|
|
349
674
|
workspace: { read: true, write: true, exec: true, git: true, upload: true, download: true },
|
|
350
|
-
branching: { checkpoint:
|
|
675
|
+
branching: { checkpoint: false, fork: false },
|
|
351
676
|
placement: true,
|
|
352
677
|
usage: true,
|
|
353
678
|
confidential: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-tangle",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"LICENSE"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tangle-network/agent-interface": "0.
|
|
34
|
+
"@tangle-network/agent-interface": "0.41.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@tangle-network/sandbox": ">=0.
|
|
37
|
+
"@tangle-network/sandbox": ">=0.17.0 <1.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@tangle-network/sandbox": {
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@tangle-network/sandbox": "0.
|
|
45
|
+
"@tangle-network/sandbox": "0.17.0",
|
|
46
46
|
"@types/node": "25.6.0",
|
|
47
47
|
"typescript": "^6.0.3",
|
|
48
48
|
"vitest": "^4.1.5",
|
|
49
|
-
"@tangle-network/agent-provider-testkit": "0.
|
|
49
|
+
"@tangle-network/agent-provider-testkit": "0.4.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsc -p tsconfig.json",
|