@tangle-network/agent-provider-tangle 0.5.1 → 0.6.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/dist/exact-process.d.ts +1 -4
- package/dist/exact-process.js +123 -206
- package/dist/index.d.ts +4 -126
- package/dist/index.js +3 -687
- package/dist/tangle-capabilities.d.ts +39 -0
- package/dist/tangle-capabilities.js +140 -0
- package/dist/tangle-contract-safety.d.ts +19 -0
- package/dist/tangle-contract-safety.js +240 -0
- package/dist/tangle-create-options.d.ts +9 -0
- package/dist/tangle-create-options.js +243 -0
- package/dist/tangle-environment-control.d.ts +6 -0
- package/dist/tangle-environment-control.js +50 -0
- package/dist/tangle-environment-dispatch.d.ts +3 -0
- package/dist/tangle-environment-dispatch.js +60 -0
- package/dist/tangle-environment-session.d.ts +4 -0
- package/dist/tangle-environment-session.js +156 -0
- package/dist/tangle-environment-validation.d.ts +11 -0
- package/dist/tangle-environment-validation.js +63 -0
- package/dist/tangle-environment-values.d.ts +8 -0
- package/dist/tangle-environment-values.js +84 -0
- package/dist/tangle-environment.d.ts +3 -0
- package/dist/tangle-environment.js +216 -0
- package/dist/tangle-events.d.ts +6 -0
- package/dist/tangle-events.js +111 -0
- package/dist/tangle-exact-process-environment.d.ts +3 -0
- package/dist/tangle-exact-process-environment.js +184 -0
- package/dist/tangle-exact-process-runtime.d.ts +5 -0
- package/dist/tangle-exact-process-runtime.js +150 -0
- package/dist/tangle-exact-process-validation.d.ts +17 -0
- package/dist/tangle-exact-process-validation.js +123 -0
- package/dist/tangle-prompt.d.ts +24 -0
- package/dist/tangle-prompt.js +166 -0
- package/dist/tangle-provider.d.ts +3 -0
- package/dist/tangle-provider.js +192 -0
- package/dist/tangle-result-values.d.ts +5 -0
- package/dist/tangle-result-values.js +94 -0
- package/dist/tangle-session-control.d.ts +7 -0
- package/dist/tangle-session-control.js +89 -0
- package/dist/tangle-types.d.ts +141 -0
- package/dist/tangle-types.js +1 -0
- package/package.json +39 -3
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { assertBoundedJson, boundedIdentifier, boundedString, MAX_ARRAY_LENGTH, MAX_MAP_ENTRIES, } from "./tangle-contract-safety.js";
|
|
2
|
+
export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
3
|
+
assertCreateInputShape(input);
|
|
4
|
+
assertNoInlineSecretValues(input);
|
|
5
|
+
if (input.providerOptions && Object.keys(input.providerOptions).length > 0) {
|
|
6
|
+
throw new Error("Tangle create providerOptions are not supported");
|
|
7
|
+
}
|
|
8
|
+
if (input.providerOptions)
|
|
9
|
+
assertBoundedRecord(input.providerOptions, "Tangle create providerOptions");
|
|
10
|
+
if (input.metadata)
|
|
11
|
+
assertBoundedRecord(input.metadata, "Tangle metadata");
|
|
12
|
+
if (input.name !== undefined) {
|
|
13
|
+
boundedString(input.name, "Tangle environment name");
|
|
14
|
+
if (!input.name)
|
|
15
|
+
throw new Error("Tangle environment name cannot be empty");
|
|
16
|
+
}
|
|
17
|
+
if (input.backend !== undefined)
|
|
18
|
+
boundedIdentifier(input.backend, "Tangle backend");
|
|
19
|
+
if (input.env !== undefined)
|
|
20
|
+
assertStringRecord(input.env, "Tangle");
|
|
21
|
+
const workspace = input.workspace ?? {};
|
|
22
|
+
if (workspace.providerOptions && Object.keys(workspace.providerOptions).length > 0) {
|
|
23
|
+
throw new Error("Tangle workspace providerOptions are not supported");
|
|
24
|
+
}
|
|
25
|
+
if (workspace.providerOptions)
|
|
26
|
+
assertBoundedRecord(workspace.providerOptions, "Tangle workspace providerOptions");
|
|
27
|
+
if (workspace.environment !== undefined) {
|
|
28
|
+
boundedIdentifier(workspace.environment, "Tangle workspace environment");
|
|
29
|
+
}
|
|
30
|
+
if (workspace.image !== undefined)
|
|
31
|
+
boundedString(workspace.image, "Tangle workspace image");
|
|
32
|
+
if (workspace.cwd !== undefined)
|
|
33
|
+
boundedString(workspace.cwd, "Tangle workspace cwd");
|
|
34
|
+
if (workspace.repoUrl !== undefined)
|
|
35
|
+
boundedString(workspace.repoUrl, "Tangle repository URL");
|
|
36
|
+
if (workspace.gitRef !== undefined)
|
|
37
|
+
boundedIdentifier(workspace.gitRef, "Tangle git ref");
|
|
38
|
+
if (input.resources?.providerOptions && Object.keys(input.resources.providerOptions).length > 0) {
|
|
39
|
+
throw new Error("Tangle resource providerOptions are not supported");
|
|
40
|
+
}
|
|
41
|
+
if (input.resources?.providerOptions)
|
|
42
|
+
assertBoundedRecord(input.resources.providerOptions, "Tangle resource providerOptions");
|
|
43
|
+
if (input.resources?.gpu !== undefined)
|
|
44
|
+
boundedIdentifier(input.resources.gpu, "Tangle GPU");
|
|
45
|
+
if (input.idempotencyKey !== undefined) {
|
|
46
|
+
boundedIdentifier(input.idempotencyKey, "Tangle idempotency key");
|
|
47
|
+
}
|
|
48
|
+
if (input.workspace?.cwd === "")
|
|
49
|
+
throw new Error("Tangle workspace cwd cannot be empty");
|
|
50
|
+
if (workspace.image === "")
|
|
51
|
+
throw new Error("Tangle workspace image cannot be empty");
|
|
52
|
+
if (workspace.repoUrl === "")
|
|
53
|
+
throw new Error("Tangle repository URL cannot be empty");
|
|
54
|
+
for (const [name, value] of Object.entries(input.resources ?? {})) {
|
|
55
|
+
if (name === "providerOptions" || name === "gpu")
|
|
56
|
+
continue;
|
|
57
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
|
|
58
|
+
throw new Error(`Tangle resource ${name} must be a positive safe integer`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (workspace.environment !== undefined && workspace.image !== undefined) {
|
|
62
|
+
throw new Error("Tangle workspace cannot specify both environment and image");
|
|
63
|
+
}
|
|
64
|
+
// Sandbox injects secrets by name from its own store. Accepting a name/value
|
|
65
|
+
// record and dropping it would create an environment with no credentials and
|
|
66
|
+
// no error, surfacing later as an unexplained tool failure.
|
|
67
|
+
const environment = workspace.image ?? workspace.environment;
|
|
68
|
+
const base = {};
|
|
69
|
+
return {
|
|
70
|
+
...base,
|
|
71
|
+
...(environment !== undefined ? { environment } : {}),
|
|
72
|
+
...(workspace.repoUrl
|
|
73
|
+
? {
|
|
74
|
+
git: {
|
|
75
|
+
url: boundedString(workspace.repoUrl, "Tangle repository URL"),
|
|
76
|
+
...(workspace.gitRef ? { ref: boundedIdentifier(workspace.gitRef, "Tangle git ref") } : {}),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
: {}),
|
|
80
|
+
...(input.resources ? { resources: input.resources } : {}),
|
|
81
|
+
...(input.env ? { env: input.env } : {}),
|
|
82
|
+
...(Array.isArray(input.secrets) ? { secrets: input.secrets } : {}),
|
|
83
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
84
|
+
...(input.name === undefined ? {} : { name: input.name }),
|
|
85
|
+
...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }),
|
|
86
|
+
backend: {
|
|
87
|
+
...(base.backend ?? {}),
|
|
88
|
+
type: (input.backend ?? defaultBackend),
|
|
89
|
+
profile: inlineAgentProfile(input.profile),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Reject value-bearing secret maps before any custom mapper can drop them. */
|
|
94
|
+
export function assertNoInlineSecretValues(input) {
|
|
95
|
+
if (input.providerOptions !== undefined) {
|
|
96
|
+
if (!input.providerOptions || typeof input.providerOptions !== "object" || Array.isArray(input.providerOptions)) {
|
|
97
|
+
throw new Error("Tangle create providerOptions must be a JSON object");
|
|
98
|
+
}
|
|
99
|
+
assertBoundedJson(input.providerOptions);
|
|
100
|
+
if (Object.keys(input.providerOptions).length > 0) {
|
|
101
|
+
throw new Error("Tangle create providerOptions are not supported");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (input.workspace?.providerOptions !== undefined) {
|
|
105
|
+
if (!input.workspace.providerOptions || typeof input.workspace.providerOptions !== "object" || Array.isArray(input.workspace.providerOptions)) {
|
|
106
|
+
throw new Error("Tangle workspace providerOptions must be a JSON object");
|
|
107
|
+
}
|
|
108
|
+
assertBoundedJson(input.workspace.providerOptions);
|
|
109
|
+
if (Object.keys(input.workspace.providerOptions).length > 0) {
|
|
110
|
+
throw new Error("Tangle workspace providerOptions are not supported");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (input.resources?.providerOptions !== undefined) {
|
|
114
|
+
if (!input.resources.providerOptions || typeof input.resources.providerOptions !== "object" || Array.isArray(input.resources.providerOptions)) {
|
|
115
|
+
throw new Error("Tangle resource providerOptions must be a JSON object");
|
|
116
|
+
}
|
|
117
|
+
assertBoundedJson(input.resources.providerOptions);
|
|
118
|
+
if (Object.keys(input.resources.providerOptions).length > 0) {
|
|
119
|
+
throw new Error("Tangle resource providerOptions are not supported");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (input.secrets !== undefined && !Array.isArray(input.secrets)) {
|
|
123
|
+
throw new Error("Tangle secrets must be an array of names created with client.secrets.create(); inline secret values are not accepted");
|
|
124
|
+
}
|
|
125
|
+
if (input.secrets !== undefined) {
|
|
126
|
+
if (input.secrets.length > MAX_ARRAY_LENGTH) {
|
|
127
|
+
throw new Error("Tangle secret names exceed their bound");
|
|
128
|
+
}
|
|
129
|
+
for (const secret of input.secrets)
|
|
130
|
+
boundedIdentifier(secret, "Tangle secret name");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** A custom mapper must not smuggle a value map into the Sandbox request. */
|
|
134
|
+
export function assertMappedSecretNames(options) {
|
|
135
|
+
if (options.secrets !== undefined && !Array.isArray(options.secrets)) {
|
|
136
|
+
throw new Error("Tangle mapped secrets must be an array of stored secret names");
|
|
137
|
+
}
|
|
138
|
+
if (options.secrets !== undefined) {
|
|
139
|
+
if (options.secrets.length > MAX_ARRAY_LENGTH) {
|
|
140
|
+
throw new Error("Tangle mapped secrets exceed their bound");
|
|
141
|
+
}
|
|
142
|
+
for (const secret of options.secrets)
|
|
143
|
+
boundedIdentifier(secret, "Tangle mapped secret name");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function assertCreateInputShape(input) {
|
|
147
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
148
|
+
throw new Error("Tangle create input must be an object");
|
|
149
|
+
}
|
|
150
|
+
const keys = new Set(Object.keys(input));
|
|
151
|
+
for (const key of [
|
|
152
|
+
"profile",
|
|
153
|
+
"backend",
|
|
154
|
+
"workspace",
|
|
155
|
+
"resources",
|
|
156
|
+
"env",
|
|
157
|
+
"secrets",
|
|
158
|
+
"metadata",
|
|
159
|
+
"name",
|
|
160
|
+
"idempotencyKey",
|
|
161
|
+
"signal",
|
|
162
|
+
"providerOptions",
|
|
163
|
+
])
|
|
164
|
+
keys.delete(key);
|
|
165
|
+
if (keys.size > 0)
|
|
166
|
+
throw new Error("Tangle create input contains unsupported fields");
|
|
167
|
+
if (typeof input.profile === "string") {
|
|
168
|
+
boundedIdentifier(input.profile, "Tangle profile reference");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
if (!input.profile || typeof input.profile !== "object" || Array.isArray(input.profile)) {
|
|
172
|
+
throw new Error("Tangle profile must be an object or bounded reference");
|
|
173
|
+
}
|
|
174
|
+
assertBoundedJson(input.profile);
|
|
175
|
+
}
|
|
176
|
+
if (input.workspace !== undefined) {
|
|
177
|
+
if (!input.workspace || typeof input.workspace !== "object" || Array.isArray(input.workspace)) {
|
|
178
|
+
throw new Error("Tangle workspace must be an object");
|
|
179
|
+
}
|
|
180
|
+
const workspaceKeys = new Set(Object.keys(input.workspace));
|
|
181
|
+
for (const key of ["environment", "image", "repoUrl", "gitRef", "cwd", "providerOptions"])
|
|
182
|
+
workspaceKeys.delete(key);
|
|
183
|
+
if (workspaceKeys.size > 0)
|
|
184
|
+
throw new Error("Tangle workspace contains unsupported fields");
|
|
185
|
+
}
|
|
186
|
+
if (input.resources !== undefined) {
|
|
187
|
+
if (!input.resources || typeof input.resources !== "object" || Array.isArray(input.resources)) {
|
|
188
|
+
throw new Error("Tangle resources must be an object");
|
|
189
|
+
}
|
|
190
|
+
const resourceKeys = new Set(Object.keys(input.resources));
|
|
191
|
+
for (const key of ["cpu", "memoryMb", "diskMb", "gpu", "providerOptions"])
|
|
192
|
+
resourceKeys.delete(key);
|
|
193
|
+
if (resourceKeys.size > 0)
|
|
194
|
+
throw new Error("Tangle resources contain unsupported fields");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export function assertMappedCreateOptions(options) {
|
|
198
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
199
|
+
throw new Error("Tangle mapped create options must be an object");
|
|
200
|
+
}
|
|
201
|
+
assertBoundedJson(options);
|
|
202
|
+
if (Object.hasOwn(options, "providerOptions")) {
|
|
203
|
+
throw new Error("Tangle mapped create providerOptions are not supported");
|
|
204
|
+
}
|
|
205
|
+
if (options.name !== undefined) {
|
|
206
|
+
boundedString(options.name, "Tangle mapped environment name");
|
|
207
|
+
if (!options.name)
|
|
208
|
+
throw new Error("Tangle mapped environment name cannot be empty");
|
|
209
|
+
}
|
|
210
|
+
if (options.idempotencyKey !== undefined)
|
|
211
|
+
boundedIdentifier(options.idempotencyKey, "Tangle mapped idempotency key");
|
|
212
|
+
if (options.env !== undefined)
|
|
213
|
+
assertStringRecord(options.env, "Tangle mapped");
|
|
214
|
+
assertMappedSecretNames(options);
|
|
215
|
+
}
|
|
216
|
+
function inlineAgentProfile(profile) {
|
|
217
|
+
if (typeof profile === "string") {
|
|
218
|
+
throw new Error("Tangle provider requires an inline AgentProfile, not a profile reference");
|
|
219
|
+
}
|
|
220
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
|
|
221
|
+
throw new Error("Tangle inline AgentProfile must be an object");
|
|
222
|
+
}
|
|
223
|
+
assertBoundedJson(profile);
|
|
224
|
+
return profile;
|
|
225
|
+
}
|
|
226
|
+
function assertBoundedRecord(value, label) {
|
|
227
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
228
|
+
throw new Error(`${label} must be a JSON object`);
|
|
229
|
+
}
|
|
230
|
+
assertBoundedJson(value);
|
|
231
|
+
}
|
|
232
|
+
function assertStringRecord(value, label) {
|
|
233
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
234
|
+
throw new Error(`${label} environment must be a JSON object`);
|
|
235
|
+
}
|
|
236
|
+
if (Object.keys(value).length > MAX_MAP_ENTRIES) {
|
|
237
|
+
throw new Error(`${label} environment has too many variables`);
|
|
238
|
+
}
|
|
239
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
240
|
+
boundedIdentifier(key, `${label} environment variable name`);
|
|
241
|
+
boundedString(entry, `${label} environment variable value`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentSessionRef, AgentTurnInput } from "@tangle-network/agent-interface/environment-provider";
|
|
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}`;
|
|
4
|
+
export declare function hasReplayPayload(input: AgentTurnInput): boolean;
|
|
5
|
+
export declare function interruptAfterAbort(box: SandboxInstanceLike, reference: AgentSessionRef): Promise<void>;
|
|
6
|
+
export declare function interruptExecutionAfterAbort(source: SandboxInstanceLike | SandboxSessionLike, sessionId: string, executionId: string): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { canonicalCandidateDigest } from "@tangle-network/agent-interface";
|
|
2
|
+
export function sessionPromptRequestDigest(input, provider, environmentId, sessionId, executionId) {
|
|
3
|
+
return canonicalCandidateDigest({
|
|
4
|
+
provider,
|
|
5
|
+
environmentId,
|
|
6
|
+
sessionId,
|
|
7
|
+
executionId,
|
|
8
|
+
...(input.turnId === undefined ? {} : { turnId: input.turnId }),
|
|
9
|
+
...(input.prompt === undefined ? {} : { prompt: input.prompt }),
|
|
10
|
+
...(input.parts === undefined ? {} : { parts: input.parts }),
|
|
11
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
12
|
+
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
13
|
+
...(input.detach === undefined ? {} : { detach: input.detach }),
|
|
14
|
+
...(input.context === undefined ? {} : { context: input.context }),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function hasReplayPayload(input) {
|
|
18
|
+
return (input.prompt !== undefined ||
|
|
19
|
+
input.parts !== undefined ||
|
|
20
|
+
input.model !== undefined ||
|
|
21
|
+
input.timeoutMs !== undefined ||
|
|
22
|
+
input.turnId !== undefined ||
|
|
23
|
+
input.detach !== undefined ||
|
|
24
|
+
input.context !== undefined ||
|
|
25
|
+
input.providerOptions !== undefined);
|
|
26
|
+
}
|
|
27
|
+
export async function interruptAfterAbort(box, reference) {
|
|
28
|
+
const sessionId = reference.id;
|
|
29
|
+
const executionId = reference.controlRef?.executionId;
|
|
30
|
+
if (!box.session || executionId === undefined)
|
|
31
|
+
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
|
+
}
|
|
40
|
+
export async function interruptExecutionAfterAbort(source, sessionId, executionId) {
|
|
41
|
+
try {
|
|
42
|
+
const target = "interrupt" in source
|
|
43
|
+
? source
|
|
44
|
+
: source.session?.(sessionId);
|
|
45
|
+
await target?.interrupt({ executionId });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// The operation's abort remains the caller-visible result; cleanup is best effort.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AgentSessionRef, AgentTurnInput } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
import type { SandboxInstanceLike } from "./tangle-types.js";
|
|
3
|
+
export declare function dispatchEnvironmentRun(box: SandboxInstanceLike, provider: string, environmentId: string): (input: AgentTurnInput) => Promise<AgentSessionRef>;
|
|
@@ -0,0 +1,60 @@
|
|
|
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";
|
|
4
|
+
import { awaitWithSignal } from "./tangle-contract-safety.js";
|
|
5
|
+
import { interruptAfterAbort, sessionPromptRequestDigest, } from "./tangle-environment-control.js";
|
|
6
|
+
export function dispatchEnvironmentRun(box, provider, environmentId) {
|
|
7
|
+
return async (input) => {
|
|
8
|
+
AgentTurnInputSchema.parse(input);
|
|
9
|
+
input.signal?.throwIfAborted();
|
|
10
|
+
const expectedSessionId = input.sessionId ?? input.controlRef?.sessionId;
|
|
11
|
+
const promise = box.dispatchPrompt?.(promptFromTurnInput(input), promptOptionsFromTurnInput(input, { provider, environmentId }));
|
|
12
|
+
let dispatched;
|
|
13
|
+
try {
|
|
14
|
+
dispatched = await awaitWithSignal(promise, input.signal);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (input.signal?.aborted && promise) {
|
|
18
|
+
void promise
|
|
19
|
+
.then((late) => {
|
|
20
|
+
const lateRef = sessionRefFromSandboxDispatch(late, provider, environmentId, executionIdFromTurnInput(input));
|
|
21
|
+
return interruptAfterAbort(box, lateRef);
|
|
22
|
+
})
|
|
23
|
+
.catch(() => undefined);
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
let reference;
|
|
28
|
+
try {
|
|
29
|
+
reference = sessionRefFromSandboxDispatch(dispatched, provider, environmentId, executionIdFromTurnInput(input), undefined, expectedSessionId);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
try {
|
|
33
|
+
const unexpected = sessionRefFromSandboxDispatch(dispatched, provider, environmentId, undefined);
|
|
34
|
+
if (unexpected.metadata?.dispatched === true &&
|
|
35
|
+
unexpected.metadata.alreadyExisted !== true) {
|
|
36
|
+
await interruptAfterAbort(box, unexpected);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// An invalid receipt cannot identify a safe execution to interrupt.
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
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) {
|
|
55
|
+
await interruptAfterAbort(box, boundReference);
|
|
56
|
+
input.signal.throwIfAborted();
|
|
57
|
+
}
|
|
58
|
+
return boundReference;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentSession } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
import type { AgentRunControlRef } from "@tangle-network/agent-interface";
|
|
3
|
+
import type { SandboxSessionLike } from "./tangle-types.js";
|
|
4
|
+
export declare function sandboxSessionAsAgentSession(session: SandboxSessionLike, controlRef: AgentRunControlRef | undefined, provider: string, environmentId: string): AgentSession;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { AgentTurnInputSchema } from "@tangle-network/agent-interface";
|
|
2
|
+
import { environmentEventFromSandboxEvent } from "./tangle-events.js";
|
|
3
|
+
import { agentTurnResultFromPromptRecord, promptFromTurnInput, promptOptionsFromTurnInput, validatedSandboxPromptResult, } from "./tangle-prompt.js";
|
|
4
|
+
import { retainedSessionControlRef, resolveRetainedSessionControlRef, sameRunControlRef, sessionPromptExecutionId, } from "./tangle-session-control.js";
|
|
5
|
+
import { sessionStatusFromUnknown } from "./tangle-environment-values.js";
|
|
6
|
+
import { awaitWithSignal, boundedIdentifier, } from "./tangle-contract-safety.js";
|
|
7
|
+
import { assertOptionKeys } from "./tangle-environment-validation.js";
|
|
8
|
+
import { hasReplayPayload, interruptExecutionAfterAbort, sessionPromptRequestDigest, } from "./tangle-environment-control.js";
|
|
9
|
+
export function sandboxSessionAsAgentSession(session, controlRef, provider, environmentId) {
|
|
10
|
+
let activeControlRef = controlRef;
|
|
11
|
+
return {
|
|
12
|
+
id: session.id,
|
|
13
|
+
get controlRef() {
|
|
14
|
+
return activeControlRef;
|
|
15
|
+
},
|
|
16
|
+
async status(options) {
|
|
17
|
+
assertOptionKeys(options, ["signal"], "Tangle session status");
|
|
18
|
+
const status = await awaitWithSignal(session.status(options), options?.signal);
|
|
19
|
+
options?.signal?.throwIfAborted();
|
|
20
|
+
if (!status)
|
|
21
|
+
return null;
|
|
22
|
+
return sessionStatusFromUnknown(status.status);
|
|
23
|
+
},
|
|
24
|
+
async *events(options) {
|
|
25
|
+
assertOptionKeys(options, ["since", "executionId", "signal"], "Tangle session events");
|
|
26
|
+
if (options?.since !== undefined)
|
|
27
|
+
boundedIdentifier(options.since, "Tangle event cursor");
|
|
28
|
+
if (options?.executionId !== undefined)
|
|
29
|
+
boundedIdentifier(options.executionId, "Tangle execution id");
|
|
30
|
+
if (options?.executionId !== undefined && activeControlRef?.executionId !== options.executionId) {
|
|
31
|
+
throw new Error("Tangle replay executionId conflicts with the control reference");
|
|
32
|
+
}
|
|
33
|
+
const executionId = activeControlRef?.executionId ?? options?.executionId;
|
|
34
|
+
if (options?.since !== undefined && executionId === undefined) {
|
|
35
|
+
throw new Error("Tangle cursor replay requires an exact executionId from its control reference");
|
|
36
|
+
}
|
|
37
|
+
options?.signal?.throwIfAborted();
|
|
38
|
+
const seenEventIds = new Set();
|
|
39
|
+
const iterator = session.events({
|
|
40
|
+
...(options?.since !== undefined ? { since: options.since } : {}),
|
|
41
|
+
...(executionId !== undefined ? { executionId } : {}),
|
|
42
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
43
|
+
})[Symbol.asyncIterator]();
|
|
44
|
+
let completed = false;
|
|
45
|
+
try {
|
|
46
|
+
while (true) {
|
|
47
|
+
const next = await awaitWithSignal(iterator.next(), options?.signal);
|
|
48
|
+
if (next.done) {
|
|
49
|
+
completed = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
options?.signal?.throwIfAborted();
|
|
53
|
+
if (options?.since !== undefined && next.value.id === options.since)
|
|
54
|
+
continue;
|
|
55
|
+
const converted = environmentEventFromSandboxEvent(next.value, {
|
|
56
|
+
executionId,
|
|
57
|
+
sessionId: session.id,
|
|
58
|
+
});
|
|
59
|
+
if (converted.id === undefined)
|
|
60
|
+
throw new Error("Tangle session event arrived without a stable id");
|
|
61
|
+
if (seenEventIds.has(converted.id))
|
|
62
|
+
throw new Error(`Tangle session replay repeated event id ${converted.id}`);
|
|
63
|
+
seenEventIds.add(converted.id);
|
|
64
|
+
options?.signal?.throwIfAborted();
|
|
65
|
+
yield converted;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
if (!completed) {
|
|
70
|
+
void Promise.resolve(iterator.return?.()).catch(() => undefined);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
options?.signal?.throwIfAborted();
|
|
74
|
+
},
|
|
75
|
+
async result(options) {
|
|
76
|
+
assertOptionKeys(options, ["signal"], "Tangle session result");
|
|
77
|
+
const expectedExecutionId = activeControlRef?.executionId;
|
|
78
|
+
if (expectedExecutionId === undefined)
|
|
79
|
+
throw new Error("Tangle session result requires an exact executionId from its control reference");
|
|
80
|
+
const result = await awaitWithSignal(session.result({ executionId: expectedExecutionId, signal: options?.signal }), options?.signal);
|
|
81
|
+
options?.signal?.throwIfAborted();
|
|
82
|
+
const resultRecord = validatedSandboxPromptResult(result);
|
|
83
|
+
if (resultRecord.executionId !== expectedExecutionId)
|
|
84
|
+
throw new Error("Tangle session result did not confirm its exact executionId");
|
|
85
|
+
return agentTurnResultFromPromptRecord(resultRecord, { sessionId: session.id });
|
|
86
|
+
},
|
|
87
|
+
async prompt(input) {
|
|
88
|
+
AgentTurnInputSchema.parse(input);
|
|
89
|
+
input.signal?.throwIfAborted();
|
|
90
|
+
if (input.sessionId !== undefined && input.sessionId !== session.id)
|
|
91
|
+
throw new Error("Tangle sessionId conflicts with this session");
|
|
92
|
+
const requestedControlRef = resolveRetainedSessionControlRef(input.controlRef, session.id, provider, environmentId);
|
|
93
|
+
if (activeControlRef && requestedControlRef && !sameRunControlRef(activeControlRef, requestedControlRef))
|
|
94
|
+
throw new Error("Tangle prompt control reference conflicts with this session");
|
|
95
|
+
const sourceControlRef = requestedControlRef ?? activeControlRef;
|
|
96
|
+
const replay = input.lastEventId !== undefined;
|
|
97
|
+
if (replay && sourceControlRef?.executionId !== undefined && input.executionId !== undefined && input.executionId !== sourceControlRef.executionId)
|
|
98
|
+
throw new Error("Tangle replay executionId conflicts with the control reference");
|
|
99
|
+
if (replay && sourceControlRef?.requestDigest === undefined) {
|
|
100
|
+
throw new Error("Tangle replay requires an exact request digest from its control reference");
|
|
101
|
+
}
|
|
102
|
+
const executionId = replay ? input.executionId ?? sourceControlRef?.executionId : input.executionId ?? sessionPromptExecutionId(provider, environmentId, session.id, input.turnId);
|
|
103
|
+
if (executionId === undefined)
|
|
104
|
+
throw new Error("Tangle session replay requires the exact executionId from its control reference");
|
|
105
|
+
const computedRequestDigest = sessionPromptRequestDigest(input, provider, environmentId, session.id, executionId);
|
|
106
|
+
const sameExecution = sourceControlRef?.executionId === executionId;
|
|
107
|
+
if (sourceControlRef?.requestDigest !== undefined &&
|
|
108
|
+
sameExecution &&
|
|
109
|
+
(!replay || hasReplayPayload(input)) &&
|
|
110
|
+
sourceControlRef.requestDigest !== computedRequestDigest) {
|
|
111
|
+
throw new Error("Tangle prompt request digest conflicts with the control reference");
|
|
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 };
|
|
122
|
+
try {
|
|
123
|
+
const result = await awaitWithSignal(session.prompt(promptFromTurnInput(input), promptOptionsFromTurnInput(promptInput, { provider, environmentId, sessionId: session.id })), input.signal);
|
|
124
|
+
input.signal?.throwIfAborted();
|
|
125
|
+
const resultRecord = validatedSandboxPromptResult(result);
|
|
126
|
+
if (resultRecord.executionId !== executionId) {
|
|
127
|
+
void interruptExecutionAfterAbort(session, session.id, executionId);
|
|
128
|
+
throw new Error("Tangle session prompt did not confirm its exact executionId");
|
|
129
|
+
}
|
|
130
|
+
activeControlRef = retainedSessionControlRef(session.id, executionId, provider, environmentId, requestDigest);
|
|
131
|
+
return agentTurnResultFromPromptRecord(resultRecord, {
|
|
132
|
+
sessionId: session.id,
|
|
133
|
+
...(input.contextTransfer ? { contextTransferRequest: input.contextTransfer } : {}),
|
|
134
|
+
...(input.contextTransfer ? { contextTransferRequested: true } : {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (input.signal?.aborted) {
|
|
139
|
+
void interruptExecutionAfterAbort(session, session.id, executionId);
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
async cancel(options) {
|
|
145
|
+
assertOptionKeys(options, ["signal"], "Tangle session cancel");
|
|
146
|
+
const executionId = activeControlRef?.executionId;
|
|
147
|
+
if (executionId === undefined)
|
|
148
|
+
throw new Error("Tangle session cancellation requires an exact executionId from its control reference");
|
|
149
|
+
options?.signal?.throwIfAborted();
|
|
150
|
+
const result = await awaitWithSignal(session.interrupt({ executionId, signal: options?.signal }), options?.signal);
|
|
151
|
+
options?.signal?.throwIfAborted();
|
|
152
|
+
if (result.cancelled !== true)
|
|
153
|
+
throw new Error("Tangle sandbox did not confirm cancellation");
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CheckpointRef, CheckpointRequest, ExecRequest, ForkRequest } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
export declare function assertOptionKeys(value: object | undefined, allowed: readonly string[], label: string): void;
|
|
3
|
+
export declare function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown>;
|
|
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;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { assertBoundedJson, boundedIdentifier, boundedString, MAX_MAP_ENTRIES, } from "./tangle-contract-safety.js";
|
|
2
|
+
export function assertOptionKeys(value, allowed, label) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return;
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6
|
+
throw new Error(`${label} options must be an object`);
|
|
7
|
+
}
|
|
8
|
+
const accepted = new Set(allowed);
|
|
9
|
+
for (const key of Object.keys(value)) {
|
|
10
|
+
if (!accepted.has(key))
|
|
11
|
+
throw new Error(`${label} options contain unsupported field ${key}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function assertRecord(value, label) {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
16
|
+
throw new Error(`${label} must be a JSON object`);
|
|
17
|
+
}
|
|
18
|
+
assertBoundedJson(value);
|
|
19
|
+
if (Object.keys(value).length > MAX_MAP_ENTRIES) {
|
|
20
|
+
throw new Error(`${label} has too many entries`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function assertExecOptions(options) {
|
|
24
|
+
assertOptionKeys(options, ["cwd", "env", "timeoutMs", "signal"], "Tangle exec");
|
|
25
|
+
if (options?.cwd !== undefined)
|
|
26
|
+
boundedString(options.cwd, "Tangle exec cwd");
|
|
27
|
+
if (options?.env !== undefined) {
|
|
28
|
+
assertRecord(options.env, "Tangle exec environment");
|
|
29
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
30
|
+
boundedIdentifier(key, "Tangle exec environment key");
|
|
31
|
+
boundedString(value, "Tangle exec environment value");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (options?.timeoutMs !== undefined &&
|
|
35
|
+
(!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1)) {
|
|
36
|
+
throw new Error("Tangle exec timeoutMs must be a positive safe integer");
|
|
37
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentEnvironmentStatus, AgentSessionStatus, PlacementInfo } from "@tangle-network/agent-interface/environment-provider";
|
|
2
|
+
import type { SandboxInstanceLike } from "./tangle-types.js";
|
|
3
|
+
export declare function nonEmptyString(value: unknown): string | undefined;
|
|
4
|
+
export declare function optionalNonEmptyString(value: unknown, label: string): string | undefined;
|
|
5
|
+
export declare function checkpointIdFromResult(result: unknown): string;
|
|
6
|
+
export declare function placementInfoFromLoopPlacement(placement: unknown, box: SandboxInstanceLike): PlacementInfo;
|
|
7
|
+
export declare function statusFromUnknown(status: unknown): AgentEnvironmentStatus;
|
|
8
|
+
export declare function sessionStatusFromUnknown(status: unknown): AgentSessionStatus;
|