@tangle-network/agent-provider-tangle 0.5.0 → 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,150 @@
|
|
|
1
|
+
import { awaitWithSignal, boundedIdentifier, boundedString, MAX_ARRAY_LENGTH, MAX_EXACT_FILE_BYTES, } from "./tangle-contract-safety.js";
|
|
2
|
+
import { assertAbsoluteFilePath } from "./tangle-exact-process-validation.js";
|
|
3
|
+
export function validateExactProcessLaunch(input) {
|
|
4
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
5
|
+
throw new Error("Tangle exact process launch must be an object");
|
|
6
|
+
}
|
|
7
|
+
const unsupported = new Set(Object.keys(input));
|
|
8
|
+
for (const key of ["executable", "args", "cwd", "env", "stdin", "timeoutMs"]) {
|
|
9
|
+
unsupported.delete(key);
|
|
10
|
+
}
|
|
11
|
+
if (unsupported.size > 0)
|
|
12
|
+
throw new Error("Tangle exact process launch contains unsupported fields");
|
|
13
|
+
const executable = boundedProcessString(input.executable, "Tangle exact process executable");
|
|
14
|
+
if (!Array.isArray(input.args) || input.args.length > MAX_ARRAY_LENGTH)
|
|
15
|
+
throw new Error("Tangle exact process has too many arguments");
|
|
16
|
+
for (const argument of input.args)
|
|
17
|
+
boundedProcessString(argument, "Tangle exact process argument");
|
|
18
|
+
if (!input.env || typeof input.env !== "object" || Array.isArray(input.env) || Object.keys(input.env).length > MAX_ARRAY_LENGTH)
|
|
19
|
+
throw new Error("Tangle exact process environment has too many entries");
|
|
20
|
+
for (const [key, value] of Object.entries(input.env)) {
|
|
21
|
+
boundedIdentifier(key, "Tangle exact process environment key");
|
|
22
|
+
assertNoProcessNul(key, "Tangle exact process environment key");
|
|
23
|
+
boundedProcessString(value, "Tangle exact process environment value");
|
|
24
|
+
}
|
|
25
|
+
if (!executable || (!executable.startsWith("/") && !input.env.PATH?.trim())) {
|
|
26
|
+
throw new Error("Tangle exact process executable must be absolute unless env.PATH is supplied");
|
|
27
|
+
}
|
|
28
|
+
if (executable.startsWith("/"))
|
|
29
|
+
assertAbsoluteFilePath(executable);
|
|
30
|
+
assertAbsoluteFilePath(input.cwd);
|
|
31
|
+
if (input.stdin !== undefined)
|
|
32
|
+
boundedProcessString(input.stdin, "Tangle exact process stdin");
|
|
33
|
+
if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 0 || input.timeoutMs > MAX_EXACT_FILE_BYTES) {
|
|
34
|
+
throw new Error("Tangle exact process timeoutMs must be a non-negative integer");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function sandboxProcessAsExactProcess(process) {
|
|
38
|
+
assertProcessId(process.pid);
|
|
39
|
+
return {
|
|
40
|
+
pid: process.pid,
|
|
41
|
+
async status(options = {}) {
|
|
42
|
+
assertSignalOptions(options, "Tangle exact process status");
|
|
43
|
+
options.signal?.throwIfAborted();
|
|
44
|
+
const status = await awaitWithSignal(process.status(), options.signal);
|
|
45
|
+
options.signal?.throwIfAborted();
|
|
46
|
+
return exactProcessStatusFromSandbox(status);
|
|
47
|
+
},
|
|
48
|
+
async wait(options = {}) {
|
|
49
|
+
assertSignalOptions(options, "Tangle exact process wait");
|
|
50
|
+
options.signal?.throwIfAborted();
|
|
51
|
+
await awaitWithSignal(process.wait(), options.signal);
|
|
52
|
+
const status = exactProcessStatusFromSandbox(await awaitWithSignal(process.status(), options.signal));
|
|
53
|
+
if (!status.termination)
|
|
54
|
+
throw new Error("Tangle exact process remained running after wait()");
|
|
55
|
+
return status.termination;
|
|
56
|
+
},
|
|
57
|
+
async kill(options = {}) {
|
|
58
|
+
assertSignalOptions(options, "Tangle exact process kill");
|
|
59
|
+
options.signal?.throwIfAborted();
|
|
60
|
+
await awaitWithSignal(process.kill("SIGKILL", { tree: true }), options.signal);
|
|
61
|
+
options.signal?.throwIfAborted();
|
|
62
|
+
},
|
|
63
|
+
async *stdout(options = {}) {
|
|
64
|
+
assertSignalOptions(options, "Tangle exact process stdout");
|
|
65
|
+
options.signal?.throwIfAborted();
|
|
66
|
+
yield* boundedProcessOutput(process.stdout(), options.signal, "Tangle exact process stdout");
|
|
67
|
+
},
|
|
68
|
+
async *stderr(options = {}) {
|
|
69
|
+
assertSignalOptions(options, "Tangle exact process stderr");
|
|
70
|
+
options.signal?.throwIfAborted();
|
|
71
|
+
yield* boundedProcessOutput(process.stderr(), options.signal, "Tangle exact process stderr");
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function* boundedProcessOutput(source, signal, label) {
|
|
76
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
77
|
+
let completed = false;
|
|
78
|
+
let bytes = 0;
|
|
79
|
+
try {
|
|
80
|
+
while (true) {
|
|
81
|
+
const next = await awaitWithSignal(iterator.next(), signal);
|
|
82
|
+
if (next.done) {
|
|
83
|
+
completed = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
const bounded = boundedString(next.value, label);
|
|
88
|
+
bytes += Buffer.byteLength(bounded, "utf8");
|
|
89
|
+
if (bytes > MAX_EXACT_FILE_BYTES) {
|
|
90
|
+
throw new Error(`${label} exceeded its byte bound`);
|
|
91
|
+
}
|
|
92
|
+
yield bounded;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (!completed) {
|
|
97
|
+
void Promise.resolve(iterator.return?.()).catch(() => undefined);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function exactProcessStatusFromSandbox(status) {
|
|
102
|
+
if (!status || typeof status !== "object")
|
|
103
|
+
throw new Error("Tangle exact process returned no status");
|
|
104
|
+
assertProcessId(status.pid);
|
|
105
|
+
if (typeof status.running !== "boolean")
|
|
106
|
+
throw new Error("Tangle exact process status has no running flag");
|
|
107
|
+
if (!Number.isSafeInteger(status.exitCode))
|
|
108
|
+
throw new Error("Tangle exact process status has an invalid exit code");
|
|
109
|
+
if (status.running ? status.exitCode !== -1 : status.exitCode < 0) {
|
|
110
|
+
throw new Error("Tangle exact process status has an invalid exit state");
|
|
111
|
+
}
|
|
112
|
+
if (status.exitSignal !== undefined)
|
|
113
|
+
boundedIdentifier(status.exitSignal, "Tangle exact process exit signal");
|
|
114
|
+
if (status.running && status.exitSignal)
|
|
115
|
+
throw new Error("Tangle exact process reported an exit signal while running");
|
|
116
|
+
const termination = processTermination(status);
|
|
117
|
+
return {
|
|
118
|
+
pid: status.pid,
|
|
119
|
+
running: status.running,
|
|
120
|
+
exitCode: status.exitCode,
|
|
121
|
+
...(status.exitSignal ? { exitSignal: status.exitSignal } : {}),
|
|
122
|
+
...(termination ? { termination } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function processTermination(status) {
|
|
126
|
+
if (status.running)
|
|
127
|
+
return undefined;
|
|
128
|
+
return status.exitSignal ? { kind: "signal", signal: status.exitSignal } : { kind: "exit", exitCode: status.exitCode };
|
|
129
|
+
}
|
|
130
|
+
function assertProcessId(pid) {
|
|
131
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
132
|
+
throw new Error("Tangle exact process pid is invalid");
|
|
133
|
+
}
|
|
134
|
+
function boundedProcessString(value, label) {
|
|
135
|
+
const bounded = boundedString(value, label);
|
|
136
|
+
assertNoProcessNul(bounded, label);
|
|
137
|
+
return bounded;
|
|
138
|
+
}
|
|
139
|
+
function assertNoProcessNul(value, label) {
|
|
140
|
+
if (value.includes("\0"))
|
|
141
|
+
throw new Error(`${label} contains a NUL byte`);
|
|
142
|
+
}
|
|
143
|
+
function assertSignalOptions(value, label) {
|
|
144
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
145
|
+
throw new Error(`${label} options must be an object`);
|
|
146
|
+
for (const key of Object.keys(value)) {
|
|
147
|
+
if (key !== "signal")
|
|
148
|
+
throw new Error(`${label} options contain unsupported field ${key}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SandboxInstanceLike } from "./tangle-types.js";
|
|
2
|
+
export declare const EXACT_PROCESS_METADATA_KEY = "tangle.exactProcess";
|
|
3
|
+
export declare function assertExactProcessSandbox(box: SandboxInstanceLike, providerName: string, teamId?: string, requestDigest?: `sha256:${string}`): void;
|
|
4
|
+
export declare function isExactProcessSandbox(box: SandboxInstanceLike, providerName: string, teamId?: string, requestDigest?: `sha256:${string}`): boolean;
|
|
5
|
+
export declare function isExactProcessRequestConflict(box: SandboxInstanceLike, providerName: string, teamId: string | undefined, idempotencyKey: string, requestDigest: `sha256:${string}`): boolean;
|
|
6
|
+
export declare function assertUnreservedMetadata(metadata: Record<string, unknown>): void;
|
|
7
|
+
export declare function assertSupportedProviderOptions(providerOptions: Record<string, unknown> | undefined): void;
|
|
8
|
+
export declare function assertAbsoluteFilePath(path: string): void;
|
|
9
|
+
export declare function assertSignalOptions(value: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}, label: string): void;
|
|
12
|
+
export declare function assertFileOptions(value: {
|
|
13
|
+
mode?: number;
|
|
14
|
+
maxBytes?: number;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}, label: string): void;
|
|
17
|
+
export declare function metadataMatches(actual: Record<string, unknown> | undefined, expected: Record<string, unknown> | undefined): boolean;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { assertBoundedJson, boundedIdentifier, boundedString, isBoundedJson, } from "./tangle-contract-safety.js";
|
|
3
|
+
export const EXACT_PROCESS_METADATA_KEY = "tangle.exactProcess";
|
|
4
|
+
export function assertExactProcessSandbox(box, providerName, teamId, requestDigest) {
|
|
5
|
+
if (!isExactProcessSandbox(box, providerName, teamId, requestDigest)) {
|
|
6
|
+
throw new Error("Tangle Sandbox did not create the requested process-only runtime");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function isExactProcessSandbox(box, providerName, teamId, requestDigest) {
|
|
10
|
+
try {
|
|
11
|
+
boundedIdentifier(box.id, "exact process environment id");
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (box.metadata !== undefined && !isBoundedJson(box.metadata))
|
|
17
|
+
return false;
|
|
18
|
+
if (!box.metadata ||
|
|
19
|
+
!Object.hasOwn(box.metadata, EXACT_PROCESS_METADATA_KEY) ||
|
|
20
|
+
!Object.hasOwn(box.metadata, "runtimeMode"))
|
|
21
|
+
return false;
|
|
22
|
+
const marker = box.metadata[EXACT_PROCESS_METADATA_KEY];
|
|
23
|
+
if (!marker || typeof marker !== "object" || Array.isArray(marker))
|
|
24
|
+
return false;
|
|
25
|
+
const record = marker;
|
|
26
|
+
return (box.metadata.runtimeMode === "control" &&
|
|
27
|
+
Object.hasOwn(record, "version") && record.version === 1 &&
|
|
28
|
+
Object.hasOwn(record, "provider") && record.provider === providerName &&
|
|
29
|
+
(teamId === undefined
|
|
30
|
+
? !Object.hasOwn(record, "teamId")
|
|
31
|
+
: Object.hasOwn(record, "teamId") && record.teamId === teamId) &&
|
|
32
|
+
Object.hasOwn(record, "idempotencyKey") &&
|
|
33
|
+
typeof record.idempotencyKey === "string" &&
|
|
34
|
+
record.idempotencyKey.length > 0 &&
|
|
35
|
+
record.idempotencyKey.length <= 512 &&
|
|
36
|
+
record.idempotencyKey.trim() === record.idempotencyKey &&
|
|
37
|
+
Object.hasOwn(record, "requestDigest") &&
|
|
38
|
+
typeof record.requestDigest === "string" &&
|
|
39
|
+
/^sha256:[a-f0-9]{64}$/.test(record.requestDigest) &&
|
|
40
|
+
(requestDigest === undefined || record.requestDigest === requestDigest));
|
|
41
|
+
}
|
|
42
|
+
export function isExactProcessRequestConflict(box, providerName, teamId, idempotencyKey, requestDigest) {
|
|
43
|
+
if (!box.metadata ||
|
|
44
|
+
!isBoundedJson(box.metadata) ||
|
|
45
|
+
box.metadata.runtimeMode !== "control")
|
|
46
|
+
return false;
|
|
47
|
+
const marker = box.metadata?.[EXACT_PROCESS_METADATA_KEY];
|
|
48
|
+
if (!marker || typeof marker !== "object" || Array.isArray(marker))
|
|
49
|
+
return false;
|
|
50
|
+
const record = marker;
|
|
51
|
+
if (record.idempotencyKey !== idempotencyKey)
|
|
52
|
+
return false;
|
|
53
|
+
return (record.provider !== providerName ||
|
|
54
|
+
(teamId === undefined
|
|
55
|
+
? Object.hasOwn(record, "teamId")
|
|
56
|
+
: record.teamId !== teamId) ||
|
|
57
|
+
record.requestDigest !== requestDigest);
|
|
58
|
+
}
|
|
59
|
+
export function assertUnreservedMetadata(metadata) {
|
|
60
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
61
|
+
throw new Error("exact process metadata must be a JSON object");
|
|
62
|
+
}
|
|
63
|
+
assertBoundedJson(metadata);
|
|
64
|
+
const reserved = [
|
|
65
|
+
"capabilities",
|
|
66
|
+
"customer_id",
|
|
67
|
+
"exactProcess",
|
|
68
|
+
"integrationLaunch",
|
|
69
|
+
"runtimeMode",
|
|
70
|
+
"teamId",
|
|
71
|
+
EXACT_PROCESS_METADATA_KEY,
|
|
72
|
+
];
|
|
73
|
+
if (reserved.some((name) => Object.hasOwn(metadata, name))) {
|
|
74
|
+
throw new Error("exact process ownership metadata is reserved by Tangle");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function assertSupportedProviderOptions(providerOptions) {
|
|
78
|
+
if (providerOptions !== undefined &&
|
|
79
|
+
(!providerOptions || typeof providerOptions !== "object" || Array.isArray(providerOptions))) {
|
|
80
|
+
throw new Error("Tangle exact process providerOptions must be a JSON object");
|
|
81
|
+
}
|
|
82
|
+
if (providerOptions && !isBoundedJson(providerOptions)) {
|
|
83
|
+
throw new Error("Tangle exact process providerOptions exceed their bound");
|
|
84
|
+
}
|
|
85
|
+
if (providerOptions && Object.keys(providerOptions).length > 0) {
|
|
86
|
+
throw new Error("Tangle exact process providerOptions are not supported");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function assertAbsoluteFilePath(path) {
|
|
90
|
+
boundedString(path, "Tangle exact process file path");
|
|
91
|
+
if (!path.startsWith("/") ||
|
|
92
|
+
path.includes("\0") ||
|
|
93
|
+
path.includes("\\") ||
|
|
94
|
+
path.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
95
|
+
throw new Error("Tangle exact process file path must be absolute");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function assertSignalOptions(value, label) {
|
|
99
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
100
|
+
throw new Error(`${label} options must be an object`);
|
|
101
|
+
}
|
|
102
|
+
for (const key of Object.keys(value)) {
|
|
103
|
+
if (key !== "signal")
|
|
104
|
+
throw new Error(`${label} options contain unsupported field ${key}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function assertFileOptions(value, label) {
|
|
108
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
109
|
+
throw new Error(`${label} options must be an object`);
|
|
110
|
+
}
|
|
111
|
+
const allowed = new Set(["mode", "maxBytes", "signal"]);
|
|
112
|
+
for (const key of Object.keys(value)) {
|
|
113
|
+
if (!allowed.has(key))
|
|
114
|
+
throw new Error(`${label} options contain unsupported field ${key}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function metadataMatches(actual, expected) {
|
|
118
|
+
if (!expected)
|
|
119
|
+
return true;
|
|
120
|
+
if (!actual)
|
|
121
|
+
return false;
|
|
122
|
+
return Object.entries(expected).every(([key, value]) => Object.hasOwn(actual, key) && isDeepStrictEqual(actual[key], value));
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PromptOptions, PromptResult } from "@tangle-network/sandbox";
|
|
2
|
+
import type { AgentTurnInput, AgentTurnResult } from "@tangle-network/agent-interface/environment-provider";
|
|
3
|
+
import type { InputPart } from "@tangle-network/agent-interface";
|
|
4
|
+
export declare function promptFromTurnInput(input: AgentTurnInput): string | InputPart[];
|
|
5
|
+
export declare function executionIdFromTurnInput(input: AgentTurnInput): string | undefined;
|
|
6
|
+
export declare function promptOptionsFromTurnInput(input: AgentTurnInput, target: {
|
|
7
|
+
provider: string;
|
|
8
|
+
environmentId: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
}): PromptOptions;
|
|
11
|
+
type SandboxRunStatus = "success" | "failed" | "blocked_on_approval" | "awaiting_question" | "awaiting_plan_decision";
|
|
12
|
+
type ValidatedSandboxPromptResult = Record<string, unknown> & {
|
|
13
|
+
success: boolean;
|
|
14
|
+
status: SandboxRunStatus;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
executionId?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function validatedSandboxPromptResult(result: PromptResult): ValidatedSandboxPromptResult;
|
|
19
|
+
export declare function agentTurnResultFromPromptRecord(record: ValidatedSandboxPromptResult, options?: {
|
|
20
|
+
contextTransferRequested?: boolean;
|
|
21
|
+
contextTransferRequest?: import("@tangle-network/agent-interface").ContextTransferRequest;
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
}): AgentTurnResult;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { AgentExactRunControlRefSchema, AgentTurnInputSchema, ContextTransferReceiptSchema, contextTransferResultMatchesRequest, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { tokenUsageFromData } from "./tangle-result-values.js";
|
|
3
|
+
import { assertBoundedJson } from "./tangle-contract-safety.js";
|
|
4
|
+
export function promptFromTurnInput(input) {
|
|
5
|
+
AgentTurnInputSchema.parse(input);
|
|
6
|
+
if (input.parts)
|
|
7
|
+
return input.parts;
|
|
8
|
+
return input.prompt ?? "";
|
|
9
|
+
}
|
|
10
|
+
export function executionIdFromTurnInput(input) {
|
|
11
|
+
return input.executionId ?? input.controlRef?.executionId;
|
|
12
|
+
}
|
|
13
|
+
export function promptOptionsFromTurnInput(input, target) {
|
|
14
|
+
if (input.contextTransfer !== undefined) {
|
|
15
|
+
throw new Error("Tangle provider does not yet support portable context transfer");
|
|
16
|
+
}
|
|
17
|
+
if (input.nativeContinuation !== undefined) {
|
|
18
|
+
throw new Error("Tangle provider does not yet support verified native continuation");
|
|
19
|
+
}
|
|
20
|
+
AgentTurnInputSchema.parse(input);
|
|
21
|
+
const controlRef = input.controlRef
|
|
22
|
+
? AgentExactRunControlRefSchema.parse(input.controlRef)
|
|
23
|
+
: undefined;
|
|
24
|
+
if (controlRef) {
|
|
25
|
+
if (controlRef.provider !== target.provider ||
|
|
26
|
+
controlRef.environmentId !== target.environmentId ||
|
|
27
|
+
(target.sessionId !== undefined &&
|
|
28
|
+
controlRef.sessionId !== target.sessionId)) {
|
|
29
|
+
throw new Error("Tangle control reference does not match this target");
|
|
30
|
+
}
|
|
31
|
+
if (controlRef.sessionId === undefined || controlRef.executionId === undefined) {
|
|
32
|
+
throw new Error("Tangle control reference requires exact sessionId and executionId");
|
|
33
|
+
}
|
|
34
|
+
if (controlRef.runId !== controlRef.executionId) {
|
|
35
|
+
throw new Error("Tangle control reference requires runId to equal executionId");
|
|
36
|
+
}
|
|
37
|
+
if (input.sessionId !== undefined &&
|
|
38
|
+
input.sessionId !== controlRef.sessionId) {
|
|
39
|
+
throw new Error("Tangle sessionId conflicts with the control reference");
|
|
40
|
+
}
|
|
41
|
+
if (input.executionId !== undefined &&
|
|
42
|
+
input.executionId !== controlRef.executionId) {
|
|
43
|
+
throw new Error("Tangle executionId conflicts with the control reference");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (input.providerOptions && Object.keys(input.providerOptions).length > 0) {
|
|
47
|
+
throw new Error("Tangle prompt providerOptions are not supported");
|
|
48
|
+
}
|
|
49
|
+
const sessionId = input.sessionId ?? controlRef?.sessionId;
|
|
50
|
+
const executionId = input.executionId ?? controlRef?.executionId;
|
|
51
|
+
return {
|
|
52
|
+
...(sessionId ? { sessionId } : {}),
|
|
53
|
+
...(input.model ? { model: input.model } : {}),
|
|
54
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
55
|
+
...(input.context ? { context: input.context } : {}),
|
|
56
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
57
|
+
...(executionId ? { executionId } : {}),
|
|
58
|
+
...(input.lastEventId ? { lastEventId: input.lastEventId } : {}),
|
|
59
|
+
...(input.turnId ? { turnId: input.turnId } : {}),
|
|
60
|
+
...(input.detach !== undefined ? { detach: input.detach } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function validatedSandboxPromptResult(result) {
|
|
64
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
65
|
+
throw new Error("Tangle prompt returned no result object");
|
|
66
|
+
}
|
|
67
|
+
const record = result;
|
|
68
|
+
if (Object.hasOwn(record, "contextTransferReceipt") &&
|
|
69
|
+
record.contextTransferReceipt === undefined) {
|
|
70
|
+
throw new Error("Tangle prompt result returned a context receipt for a turn that requested no transfer");
|
|
71
|
+
}
|
|
72
|
+
assertBoundedJson(record);
|
|
73
|
+
if (typeof record.success !== "boolean") {
|
|
74
|
+
throw new Error("Tangle prompt result omitted its success status");
|
|
75
|
+
}
|
|
76
|
+
const statuses = new Set([
|
|
77
|
+
"success",
|
|
78
|
+
"failed",
|
|
79
|
+
"blocked_on_approval",
|
|
80
|
+
"awaiting_question",
|
|
81
|
+
"awaiting_plan_decision",
|
|
82
|
+
]);
|
|
83
|
+
if (typeof record.status !== "string" ||
|
|
84
|
+
!statuses.has(record.status)) {
|
|
85
|
+
throw new Error("Tangle prompt result contained an invalid run status");
|
|
86
|
+
}
|
|
87
|
+
if (record.success !== (record.status === "success")) {
|
|
88
|
+
throw new Error("Tangle prompt result success flag conflicts with its run status");
|
|
89
|
+
}
|
|
90
|
+
if (typeof record.durationMs !== "number" ||
|
|
91
|
+
!Number.isFinite(record.durationMs) ||
|
|
92
|
+
record.durationMs < 0) {
|
|
93
|
+
throw new Error("Tangle prompt result contained an invalid duration");
|
|
94
|
+
}
|
|
95
|
+
for (const field of [
|
|
96
|
+
"executionId",
|
|
97
|
+
"response",
|
|
98
|
+
"text",
|
|
99
|
+
"finalText",
|
|
100
|
+
"error",
|
|
101
|
+
"errorCode",
|
|
102
|
+
"traceId",
|
|
103
|
+
]) {
|
|
104
|
+
if (record[field] !== undefined && typeof record[field] !== "string") {
|
|
105
|
+
throw new Error(`Tangle prompt result contained an invalid ${field}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (record.executionId === "") {
|
|
109
|
+
throw new Error("Tangle prompt result contained an empty executionId");
|
|
110
|
+
}
|
|
111
|
+
tokenUsageFromData(record);
|
|
112
|
+
return record;
|
|
113
|
+
}
|
|
114
|
+
/** Statuses that mean the run is waiting for a human, not that it failed. */
|
|
115
|
+
const AWAITING_STATUSES = new Set([
|
|
116
|
+
"blocked_on_approval",
|
|
117
|
+
"awaiting_question",
|
|
118
|
+
"awaiting_plan_decision",
|
|
119
|
+
]);
|
|
120
|
+
export function agentTurnResultFromPromptRecord(record, options = {}) {
|
|
121
|
+
const text = typeof record.response === "string"
|
|
122
|
+
? record.response
|
|
123
|
+
: typeof record.text === "string"
|
|
124
|
+
? record.text
|
|
125
|
+
: typeof record.finalText === "string"
|
|
126
|
+
? record.finalText
|
|
127
|
+
: "";
|
|
128
|
+
// A receipt with no transfer behind it would let the caller record a handoff
|
|
129
|
+
// that never happened, so it is refused rather than passed through.
|
|
130
|
+
const hasContextTransferReceipt = Object.hasOwn(record, "contextTransferReceipt");
|
|
131
|
+
if (hasContextTransferReceipt && options.contextTransferRequested !== true) {
|
|
132
|
+
throw new Error("Tangle prompt result returned a context receipt for a turn that requested no transfer");
|
|
133
|
+
}
|
|
134
|
+
const rawContextTransferReceipt = hasContextTransferReceipt
|
|
135
|
+
? record.contextTransferReceipt
|
|
136
|
+
: undefined;
|
|
137
|
+
const contextTransferReceipt = ContextTransferReceiptSchema.safeParse(rawContextTransferReceipt);
|
|
138
|
+
if (hasContextTransferReceipt && !contextTransferReceipt.success) {
|
|
139
|
+
throw new Error("Tangle prompt result contained an invalid context receipt");
|
|
140
|
+
}
|
|
141
|
+
if (contextTransferReceipt.success &&
|
|
142
|
+
options.contextTransferRequest !== undefined &&
|
|
143
|
+
!contextTransferResultMatchesRequest(options.contextTransferRequest, contextTransferReceipt.data)) {
|
|
144
|
+
throw new Error("Tangle prompt result context receipt does not match its request");
|
|
145
|
+
}
|
|
146
|
+
const usage = tokenUsageFromData(record);
|
|
147
|
+
const awaiting = AWAITING_STATUSES.has(record.status);
|
|
148
|
+
return {
|
|
149
|
+
text,
|
|
150
|
+
success: record.success,
|
|
151
|
+
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
152
|
+
...(typeof record.error === "string" ? { error: record.error } : {}),
|
|
153
|
+
...(usage ? { usage } : {}),
|
|
154
|
+
// A waiting run is not a terminal failure. Dropping the status here made
|
|
155
|
+
// "the agent is asking you something" indistinguishable from "the turn
|
|
156
|
+
// failed", while the sandbox stayed alive waiting for an answer.
|
|
157
|
+
metadata: {
|
|
158
|
+
status: record.status,
|
|
159
|
+
awaitingInteraction: awaiting,
|
|
160
|
+
terminal: !awaiting,
|
|
161
|
+
},
|
|
162
|
+
...(contextTransferReceipt.success
|
|
163
|
+
? { contextTransferReceipt: contextTransferReceipt.data }
|
|
164
|
+
: {}),
|
|
165
|
+
};
|
|
166
|
+
}
|