@tangle-network/agent-provider-tangle 0.11.4 → 0.12.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/tangle-capabilities.d.ts +4 -0
- package/dist/tangle-capabilities.js +40 -1
- package/dist/tangle-contract-safety.d.ts +2 -0
- package/dist/tangle-contract-safety.js +29 -0
- package/dist/tangle-deployment-capabilities.d.ts +4 -0
- package/dist/tangle-deployment-capabilities.js +12 -0
- package/dist/tangle-environment.js +15 -0
- package/dist/tangle-failure-reason.d.ts +1 -1
- package/dist/tangle-interactive.d.ts +24 -0
- package/dist/tangle-interactive.js +328 -0
- package/dist/tangle-terminal.d.ts +68 -2
- package/dist/tangle-terminal.js +221 -137
- package/dist/tangle-types.d.ts +70 -2
- package/package.json +7 -5
|
@@ -37,6 +37,8 @@ export interface SandboxCapabilitySupport {
|
|
|
37
37
|
observation: ObservationSurfaceSupport;
|
|
38
38
|
/** The sandbox serves the PTY socket and reports terminal metadata. */
|
|
39
39
|
interactiveTerminal: boolean;
|
|
40
|
+
/** The SDK can drive the existing native TUI and read its terminal metadata. */
|
|
41
|
+
interactiveAgent: boolean;
|
|
40
42
|
}
|
|
41
43
|
export declare function sandboxCapabilitySupport(box: SandboxInstanceLike, client: SandboxClientLike, requestedResources?: ResourceProfile): SandboxCapabilitySupport;
|
|
42
44
|
/**
|
|
@@ -74,6 +76,8 @@ export declare function tangleRetainedControlSupported(declared: AgentEnvironmen
|
|
|
74
76
|
* running agent.
|
|
75
77
|
*/
|
|
76
78
|
export declare function tangleInteractionResponsesSupported(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport): boolean;
|
|
79
|
+
/** Decide whether exact native-TUI control is both callable and deployed. */
|
|
80
|
+
export declare function tangleInteractiveAgentSupported(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport): boolean;
|
|
77
81
|
/**
|
|
78
82
|
* Narrow a declared capability document to established facts.
|
|
79
83
|
*
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { harnessSystemPromptIntents } from "@tangle-network/agent-interface";
|
|
2
2
|
import { SandboxInstance } from "@tangle-network/sandbox";
|
|
3
|
-
import { ADAPTER_CEILING_DEPLOYMENT, deploymentBacksRetainedControl, } from "./tangle-deployment-capabilities.js";
|
|
3
|
+
import { ADAPTER_CEILING_DEPLOYMENT, deploymentBacksInteractiveAgent, deploymentBacksRetainedControl, } from "./tangle-deployment-capabilities.js";
|
|
4
4
|
import { clientObservationSurfaceSupport, observationSurfaceSupport, } from "./tangle-observation.js";
|
|
5
5
|
import { sandboxBacksInteractiveTerminal } from "./tangle-terminal.js";
|
|
6
|
+
import { sandboxBacksInteractiveAgent } from "./tangle-interactive.js";
|
|
6
7
|
/**
|
|
7
8
|
* The full capability document this adapter supports when the Sandbox client
|
|
8
9
|
* implements every optional method.
|
|
@@ -100,6 +101,17 @@ export function defaultTangleSandboxCapabilities(harness) {
|
|
|
100
101
|
resize: true,
|
|
101
102
|
reattach: true,
|
|
102
103
|
},
|
|
104
|
+
interactiveAgent: {
|
|
105
|
+
start: true,
|
|
106
|
+
status: true,
|
|
107
|
+
attach: true,
|
|
108
|
+
reattach: true,
|
|
109
|
+
control: true,
|
|
110
|
+
sendPrompt: true,
|
|
111
|
+
input: true,
|
|
112
|
+
resize: true,
|
|
113
|
+
stop: true,
|
|
114
|
+
},
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
// One reserved id names both probe handles; neither ever reaches the service.
|
|
@@ -129,6 +141,7 @@ export function sandboxCapabilitySupport(box, client, requestedResources) {
|
|
|
129
141
|
respondToInteraction: typeof session?.respondToInteraction === "function",
|
|
130
142
|
observation: observationSurfaceSupport(box, client, requestedResources),
|
|
131
143
|
interactiveTerminal: sandboxBacksInteractiveTerminal(box),
|
|
144
|
+
interactiveAgent: sandboxBacksInteractiveAgent(box),
|
|
132
145
|
};
|
|
133
146
|
}
|
|
134
147
|
/**
|
|
@@ -188,6 +201,7 @@ export function clientCapabilitySupport(client) {
|
|
|
188
201
|
respondToInteraction: false,
|
|
189
202
|
observation,
|
|
190
203
|
interactiveTerminal: true,
|
|
204
|
+
interactiveAgent: false,
|
|
191
205
|
};
|
|
192
206
|
}
|
|
193
207
|
/**
|
|
@@ -230,6 +244,12 @@ export function tangleInteractionResponsesSupported(declared, support, deploymen
|
|
|
230
244
|
support.session &&
|
|
231
245
|
support.respondToInteraction);
|
|
232
246
|
}
|
|
247
|
+
/** Decide whether exact native-TUI control is both callable and deployed. */
|
|
248
|
+
export function tangleInteractiveAgentSupported(declared, support, deployment) {
|
|
249
|
+
return (declared.interactiveAgent !== undefined &&
|
|
250
|
+
support.interactiveAgent &&
|
|
251
|
+
deploymentBacksInteractiveAgent(deployment));
|
|
252
|
+
}
|
|
233
253
|
/**
|
|
234
254
|
* Narrow a declared capability document to established facts.
|
|
235
255
|
*
|
|
@@ -294,6 +314,11 @@ export function narrowedTangleCapabilities(declared, support, deployment) {
|
|
|
294
314
|
: {
|
|
295
315
|
interactiveTerminal: narrowedInteractiveTerminal(declared.interactiveTerminal, support.interactiveTerminal),
|
|
296
316
|
}),
|
|
317
|
+
...(declared.interactiveAgent === undefined
|
|
318
|
+
? {}
|
|
319
|
+
: {
|
|
320
|
+
interactiveAgent: narrowedInteractiveAgent(declared.interactiveAgent, tangleInteractiveAgentSupported(declared, support, deployment)),
|
|
321
|
+
}),
|
|
297
322
|
};
|
|
298
323
|
// A claimed block is passed through whole. Its sub-flags state what the
|
|
299
324
|
// route carries, not what one deployment reports, and the deployment's own
|
|
@@ -337,6 +362,20 @@ function narrowedInteractiveTerminal(declared, supported) {
|
|
|
337
362
|
reattach: supported ? declared.reattach : false,
|
|
338
363
|
};
|
|
339
364
|
}
|
|
365
|
+
/** One fact decides the exact TUI surface because partial control is unsafe. */
|
|
366
|
+
function narrowedInteractiveAgent(declared, supported) {
|
|
367
|
+
return {
|
|
368
|
+
start: supported ? declared.start : false,
|
|
369
|
+
status: supported ? declared.status : false,
|
|
370
|
+
attach: supported ? declared.attach : false,
|
|
371
|
+
reattach: supported ? declared.reattach : false,
|
|
372
|
+
control: supported ? declared.control : false,
|
|
373
|
+
sendPrompt: supported ? declared.sendPrompt : false,
|
|
374
|
+
input: supported ? declared.input : false,
|
|
375
|
+
resize: supported ? declared.resize : false,
|
|
376
|
+
stop: supported ? declared.stop : false,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
340
379
|
/**
|
|
341
380
|
* Narrow provider-level claims to facts the client can prove before any
|
|
342
381
|
* sandbox exists.
|
|
@@ -17,3 +17,5 @@ export declare function attachCleanupHandle(error: unknown, handle: unknown, cle
|
|
|
17
17
|
export declare function exactProcessRequestDigest(input: CreateAgentExactProcessEnvironmentInput, providerName: string, options: TangleExactProcessOptions): `sha256:${string}`;
|
|
18
18
|
export declare function validateExactProcessCreateInput(input: CreateAgentExactProcessEnvironmentInput, providerName: string, options: TangleExactProcessOptions): void;
|
|
19
19
|
export declare function awaitWithSignal<T>(operation: Promise<T> | T | undefined, signal?: AbortSignal): Promise<T>;
|
|
20
|
+
/** Await an operation and clean its result if abort wins before it resolves. */
|
|
21
|
+
export declare function awaitWithSignalAndCleanup<T>(operation: () => Promise<T> | T, signal: AbortSignal | undefined, cleanup: (value: T) => Promise<void> | void): Promise<T>;
|
|
@@ -238,3 +238,32 @@ export async function awaitWithSignal(operation, signal) {
|
|
|
238
238
|
signal.removeEventListener("abort", listener);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
/** Await an operation and clean its result if abort wins before it resolves. */
|
|
242
|
+
export async function awaitWithSignalAndCleanup(operation, signal, cleanup) {
|
|
243
|
+
signal?.throwIfAborted();
|
|
244
|
+
if (!signal)
|
|
245
|
+
return await operation();
|
|
246
|
+
let aborted = false;
|
|
247
|
+
let listener;
|
|
248
|
+
const observed = Promise.resolve().then(operation).then(async (value) => {
|
|
249
|
+
if (aborted)
|
|
250
|
+
await cleanup(value);
|
|
251
|
+
return value;
|
|
252
|
+
});
|
|
253
|
+
try {
|
|
254
|
+
return await Promise.race([
|
|
255
|
+
observed,
|
|
256
|
+
new Promise((_, reject) => {
|
|
257
|
+
listener = () => {
|
|
258
|
+
aborted = true;
|
|
259
|
+
reject(new DOMException("The operation was aborted", "AbortError"));
|
|
260
|
+
};
|
|
261
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
262
|
+
}),
|
|
263
|
+
]);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (listener)
|
|
267
|
+
signal.removeEventListener("abort", listener);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -28,6 +28,8 @@ export interface DeploymentCapabilitySupport {
|
|
|
28
28
|
* decides whether a response can be retried safely.
|
|
29
29
|
*/
|
|
30
30
|
readonly interactionResponses: boolean;
|
|
31
|
+
/** Native TUI start, status, attach, control, prompt, and stop. */
|
|
32
|
+
readonly interactiveAgent: boolean;
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* The deployment backs nothing.
|
|
@@ -88,3 +90,5 @@ export declare function readDeploymentCapabilitySupport(box: SandboxInstanceLike
|
|
|
88
90
|
* cancellation idempotency.
|
|
89
91
|
*/
|
|
90
92
|
export declare function deploymentBacksRetainedControl(deployment: DeploymentCapabilitySupport): boolean;
|
|
93
|
+
/** True only when the deployment proves the complete exact native-TUI path. */
|
|
94
|
+
export declare function deploymentBacksInteractiveAgent(deployment: DeploymentCapabilitySupport): boolean;
|
|
@@ -14,6 +14,7 @@ export const UNPROVEN_DEPLOYMENT = {
|
|
|
14
14
|
eventReplay: false,
|
|
15
15
|
executionScopedStatus: false,
|
|
16
16
|
interactionResponses: false,
|
|
17
|
+
interactiveAgent: false,
|
|
17
18
|
};
|
|
18
19
|
/**
|
|
19
20
|
* The client stage's deployment input: this adapter's ceiling, not a fact.
|
|
@@ -36,6 +37,7 @@ export const ADAPTER_CEILING_DEPLOYMENT = {
|
|
|
36
37
|
eventReplay: true,
|
|
37
38
|
executionScopedStatus: true,
|
|
38
39
|
interactionResponses: true,
|
|
40
|
+
interactiveAgent: true,
|
|
39
41
|
};
|
|
40
42
|
/**
|
|
41
43
|
* Read the deployment facts out of a capability document. Every flag this
|
|
@@ -53,6 +55,12 @@ export function deploymentCapabilitySupport(document) {
|
|
|
53
55
|
eventReplay: document.runs?.eventReplay === true,
|
|
54
56
|
executionScopedStatus: document.runs?.executionScopedStatus === true,
|
|
55
57
|
interactionResponses: document.interactions?.responseDedupe === true,
|
|
58
|
+
interactiveAgent: document.interactiveAgent?.start === true &&
|
|
59
|
+
document.interactiveAgent?.status === true &&
|
|
60
|
+
document.interactiveAgent?.attach === true &&
|
|
61
|
+
document.interactiveAgent?.control === true &&
|
|
62
|
+
document.interactiveAgent?.sendPrompt === true &&
|
|
63
|
+
document.interactiveAgent?.stop === true,
|
|
56
64
|
};
|
|
57
65
|
}
|
|
58
66
|
/**
|
|
@@ -104,3 +112,7 @@ export function deploymentBacksRetainedControl(deployment) {
|
|
|
104
112
|
deployment.eventReplay &&
|
|
105
113
|
deployment.executionScopedStatus);
|
|
106
114
|
}
|
|
115
|
+
/** True only when the deployment proves the complete exact native-TUI path. */
|
|
116
|
+
export function deploymentBacksInteractiveAgent(deployment) {
|
|
117
|
+
return deployment.interactiveAgent;
|
|
118
|
+
}
|
|
@@ -16,6 +16,7 @@ import { tangleInteractionResponder } from "./tangle-interaction-response.js";
|
|
|
16
16
|
import { createExecutionUsageLog } from "./tangle-usage-log.js";
|
|
17
17
|
import { observeTangleEnvironment } from "./tangle-observation.js";
|
|
18
18
|
import { createTangleTerminalRegistry } from "./tangle-terminal.js";
|
|
19
|
+
import { createTangleInteractiveAgentRegistry } from "./tangle-interactive.js";
|
|
19
20
|
/**
|
|
20
21
|
* Compose one concrete sandbox into an environment.
|
|
21
22
|
*
|
|
@@ -59,6 +60,10 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
59
60
|
const terminals = capabilities.interactiveTerminal?.attach === true
|
|
60
61
|
? createTangleTerminalRegistry(box)
|
|
61
62
|
: undefined;
|
|
63
|
+
const interactiveAgents = capabilities.interactiveAgent?.start === true &&
|
|
64
|
+
capabilities.interactiveAgent.control === true
|
|
65
|
+
? createTangleInteractiveAgentRegistry(box, providerName, environmentId)
|
|
66
|
+
: undefined;
|
|
62
67
|
const dispatch = capabilities.streaming.detach && box.dispatchPrompt
|
|
63
68
|
? dispatchEnvironmentRun(box, providerName, environmentId)
|
|
64
69
|
: undefined;
|
|
@@ -264,6 +269,16 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
264
269
|
},
|
|
265
270
|
}
|
|
266
271
|
: {}),
|
|
272
|
+
...(interactiveAgents
|
|
273
|
+
? {
|
|
274
|
+
async startInteractive(interactiveRequest, options) {
|
|
275
|
+
return await interactiveAgents.start(interactiveRequest, options);
|
|
276
|
+
},
|
|
277
|
+
interactive(ref) {
|
|
278
|
+
return interactiveAgents.get(ref);
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
: {}),
|
|
267
282
|
async refresh(options) {
|
|
268
283
|
assertOptionKeys(options, ["signal"], "Tangle refresh");
|
|
269
284
|
options?.signal?.throwIfAborted();
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
* identifier, which cannot express a URL, a bearer, or whitespace.
|
|
9
9
|
*/
|
|
10
10
|
/** Which transport read one reason describes. */
|
|
11
|
-
export type TransportRead = "environment refresh" | "placement lookup" | "resource usage read" | "subscription read" | "account usage read" | "terminal attach" | "terminal acknowledgement" | "terminal metadata read" | "terminal socket";
|
|
11
|
+
export type TransportRead = "environment refresh" | "placement lookup" | "resource usage read" | "subscription read" | "account usage read" | "terminal attach" | "terminal acknowledgement" | "terminal metadata read" | "terminal socket" | "interactive start" | "interactive control" | "interactive control claim" | "interactive status" | "interactive attach" | "interactive prompt" | "interactive stop";
|
|
12
12
|
/** Name the read that failed and the cause, with nothing copied from it. */
|
|
13
13
|
export declare function transportFailureReason(read: TransportRead, error: unknown): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentInteractiveSession, AgentInteractiveSessionRef, AgentInteractiveSessionStart } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { SandboxInstanceLike } from "./tangle-types.js";
|
|
3
|
+
/** Exact coding-agent TUI operations for one sandbox environment. */
|
|
4
|
+
export interface TangleInteractiveAgentRegistry {
|
|
5
|
+
start(request: AgentInteractiveSessionStart, options?: {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}): Promise<AgentInteractiveSessionRef>;
|
|
8
|
+
get(ref: AgentInteractiveSessionRef): AgentInteractiveSession;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Test the linked SDK surface without accepting any older interactive shape.
|
|
12
|
+
*
|
|
13
|
+
* The current public Sandbox release fails this test. That is intentional:
|
|
14
|
+
* it has no receipt, control claim, or mutation acknowledgement protocol.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sandboxBacksInteractiveAgent(box: SandboxInstanceLike): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Adapt the canonical Sandbox exact interactive API.
|
|
19
|
+
*
|
|
20
|
+
* The Sandbox owns process identity, admission receipts, operation replay,
|
|
21
|
+
* and control generations. This adapter validates each returned value and
|
|
22
|
+
* never creates a second record for any of those facts.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createTangleInteractiveAgentRegistry(box: SandboxInstanceLike, providerName: string, environmentId: string): TangleInteractiveAgentRegistry;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStatusSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionStopAcknowledgementMatchesCommand, exactAgentInteractiveSessionStart, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { parseBackendType } from "@tangle-network/sandbox";
|
|
3
|
+
import { assertOptionKeys } from "./tangle-environment-validation.js";
|
|
4
|
+
import { transportFailureReason } from "./tangle-failure-reason.js";
|
|
5
|
+
import { bindTangleInteractiveControl, closeTangleStreamQuietly, createTangleTerminalStreamCapture, prepareTangleTerminalAttachment, } from "./tangle-terminal.js";
|
|
6
|
+
/**
|
|
7
|
+
* Test the linked SDK surface without accepting any older interactive shape.
|
|
8
|
+
*
|
|
9
|
+
* The current public Sandbox release fails this test. That is intentional:
|
|
10
|
+
* it has no receipt, control claim, or mutation acknowledgement protocol.
|
|
11
|
+
*/
|
|
12
|
+
export function sandboxBacksInteractiveAgent(box) {
|
|
13
|
+
if (typeof box.session !== "function")
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
const candidate = box.session("__tangle-interactive-probe__").interactive?.();
|
|
17
|
+
return (isSandboxInteractiveSession(candidate) &&
|
|
18
|
+
typeof box.terminals?.get === "function");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Adapt the canonical Sandbox exact interactive API.
|
|
26
|
+
*
|
|
27
|
+
* The Sandbox owns process identity, admission receipts, operation replay,
|
|
28
|
+
* and control generations. This adapter validates each returned value and
|
|
29
|
+
* never creates a second record for any of those facts.
|
|
30
|
+
*/
|
|
31
|
+
export function createTangleInteractiveAgentRegistry(box, providerName, environmentId) {
|
|
32
|
+
const exactHandle = (sessionId) => {
|
|
33
|
+
const session = box.session?.(sessionId);
|
|
34
|
+
const candidate = session?.interactive?.();
|
|
35
|
+
if (!isSandboxInteractiveSession(candidate)) {
|
|
36
|
+
throw new Error("the linked Sandbox SDK does not expose the exact interactive control API");
|
|
37
|
+
}
|
|
38
|
+
return candidate;
|
|
39
|
+
};
|
|
40
|
+
const exactRef = (value) => {
|
|
41
|
+
const ref = AgentInteractiveSessionRefSchema.parse(value);
|
|
42
|
+
if (ref.run.provider !== providerName ||
|
|
43
|
+
ref.run.environmentId !== environmentId) {
|
|
44
|
+
throw new Error("the interactive agent reference belongs to a different environment");
|
|
45
|
+
}
|
|
46
|
+
return ref;
|
|
47
|
+
};
|
|
48
|
+
const assertRef = (expected, candidate) => {
|
|
49
|
+
if (expected.run.runId !== candidate.run.runId ||
|
|
50
|
+
expected.run.requestDigest !== candidate.run.requestDigest ||
|
|
51
|
+
expected.incarnationId !== candidate.incarnationId ||
|
|
52
|
+
expected.preparationReceipt.digest !== candidate.preparationReceipt.digest) {
|
|
53
|
+
throw new Error("the Sandbox returned a different interactive session ref");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const assertControl = (ref, control) => {
|
|
57
|
+
if (!agentInteractiveSessionControlClaimMatchesRef(ref, control)) {
|
|
58
|
+
throw new Error("the interactive control claim belongs to another process");
|
|
59
|
+
}
|
|
60
|
+
if (Date.parse(control.expiresAt) <= Date.now()) {
|
|
61
|
+
throw new Error("the interactive control claim has expired");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
async start(request, options) {
|
|
66
|
+
assertOptionKeys(options, ["signal"], "Tangle interactive agent start");
|
|
67
|
+
const exactRequest = exactAgentInteractiveSessionStart(request);
|
|
68
|
+
const requestedHarness = exactRequest.profile.harness;
|
|
69
|
+
if (requestedHarness === undefined) {
|
|
70
|
+
throw new Error("interactive agent sessions require AgentProfile.harness");
|
|
71
|
+
}
|
|
72
|
+
let harness;
|
|
73
|
+
try {
|
|
74
|
+
harness = parseBackendType(requestedHarness);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new Error("the requested AgentProfile harness has no Tangle Sandbox runner");
|
|
78
|
+
}
|
|
79
|
+
options?.signal?.throwIfAborted();
|
|
80
|
+
const handle = exactHandle(exactRequest.run.sessionId);
|
|
81
|
+
let observed;
|
|
82
|
+
try {
|
|
83
|
+
observed = await handle.start({
|
|
84
|
+
harness,
|
|
85
|
+
...(exactRequest.profile.model?.default === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { model: exactRequest.profile.model.default }),
|
|
88
|
+
...(exactRequest.cwd === undefined ? {} : { cwd: exactRequest.cwd }),
|
|
89
|
+
...(exactRequest.cols === undefined ? {} : { cols: exactRequest.cols }),
|
|
90
|
+
...(exactRequest.rows === undefined ? {} : { rows: exactRequest.rows }),
|
|
91
|
+
profile: exactRequest.profile,
|
|
92
|
+
...(exactRequest.initialPrompt === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { initialPrompt: exactRequest.initialPrompt }),
|
|
95
|
+
idempotencyKey: exactRequest.run.runId,
|
|
96
|
+
requestDigest: exactRequest.run.requestDigest,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
options?.signal?.throwIfAborted();
|
|
101
|
+
throw new Error(transportFailureReason("interactive start", error));
|
|
102
|
+
}
|
|
103
|
+
options?.signal?.throwIfAborted();
|
|
104
|
+
const ref = refFromStart(exactRequest, observed);
|
|
105
|
+
if (!agentInteractiveSessionRefMatchesStart(exactRequest, ref)) {
|
|
106
|
+
throw new Error("the Sandbox returned an interactive session with a different admission receipt");
|
|
107
|
+
}
|
|
108
|
+
return ref;
|
|
109
|
+
},
|
|
110
|
+
get(value) {
|
|
111
|
+
const ref = exactRef(value);
|
|
112
|
+
const handle = exactHandle(ref.run.sessionId);
|
|
113
|
+
const readStatus = async (options) => {
|
|
114
|
+
assertOptionKeys(options, ["signal"], "Tangle interactive agent status");
|
|
115
|
+
options?.signal?.throwIfAborted();
|
|
116
|
+
let observed;
|
|
117
|
+
try {
|
|
118
|
+
observed = await handle.status();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
options?.signal?.throwIfAborted();
|
|
122
|
+
throw new Error(transportFailureReason("interactive status", error));
|
|
123
|
+
}
|
|
124
|
+
options?.signal?.throwIfAborted();
|
|
125
|
+
return statusFromSandbox(ref, observed);
|
|
126
|
+
};
|
|
127
|
+
const validateControl = async (control, options) => {
|
|
128
|
+
assertControl(ref, control);
|
|
129
|
+
options?.signal?.throwIfAborted();
|
|
130
|
+
try {
|
|
131
|
+
await handle.validateControl(control);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
options?.signal?.throwIfAborted();
|
|
135
|
+
throw new Error(transportFailureReason("interactive control", error));
|
|
136
|
+
}
|
|
137
|
+
options?.signal?.throwIfAborted();
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
ref,
|
|
141
|
+
async claimControl(request, options) {
|
|
142
|
+
assertOptionKeys(options, ["signal"], "Tangle interactive control claim");
|
|
143
|
+
const exactRequest = AgentInteractiveSessionControlClaimRequestSchema.parse(request);
|
|
144
|
+
assertRef(ref, exactRequest.ref);
|
|
145
|
+
options?.signal?.throwIfAborted();
|
|
146
|
+
let raw;
|
|
147
|
+
try {
|
|
148
|
+
raw = await handle.claimControl(exactRequest);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
options?.signal?.throwIfAborted();
|
|
152
|
+
throw new Error(transportFailureReason("interactive control claim", error));
|
|
153
|
+
}
|
|
154
|
+
options?.signal?.throwIfAborted();
|
|
155
|
+
const acknowledgement = AgentInteractiveSessionControlClaimAcknowledgementSchema.parse(raw);
|
|
156
|
+
if (!agentInteractiveSessionControlClaimAcknowledgementMatchesRequest(exactRequest, acknowledgement)) {
|
|
157
|
+
throw new Error("the Sandbox returned a control claim acknowledgement for another request");
|
|
158
|
+
}
|
|
159
|
+
return acknowledgement;
|
|
160
|
+
},
|
|
161
|
+
status: readStatus,
|
|
162
|
+
async attach(request, options) {
|
|
163
|
+
assertOptionKeys(options, ["signal"], "Tangle interactive agent attach");
|
|
164
|
+
const exactRequest = AgentInteractiveSessionAttachSchema.parse(request ?? {});
|
|
165
|
+
await validateControl(exactRequest.control, options);
|
|
166
|
+
const before = await readStatus(options);
|
|
167
|
+
if (before.state !== "running") {
|
|
168
|
+
throw new Error("the exact interactive agent session is not running");
|
|
169
|
+
}
|
|
170
|
+
const terminals = box.terminals;
|
|
171
|
+
if (terminals === undefined || typeof terminals.get !== "function") {
|
|
172
|
+
throw new Error("the Sandbox client cannot read exact interactive terminal metadata");
|
|
173
|
+
}
|
|
174
|
+
const capture = createTangleTerminalStreamCapture(exactRequest);
|
|
175
|
+
let stream;
|
|
176
|
+
try {
|
|
177
|
+
stream = await handle.attach({
|
|
178
|
+
control: exactRequest.control,
|
|
179
|
+
...(exactRequest.cols === undefined ? {} : { cols: exactRequest.cols }),
|
|
180
|
+
...(exactRequest.rows === undefined ? {} : { rows: exactRequest.rows }),
|
|
181
|
+
handlers: capture.handlers,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
options?.signal?.throwIfAborted();
|
|
186
|
+
throw new Error(transportFailureReason("interactive attach", error));
|
|
187
|
+
}
|
|
188
|
+
if (options?.signal?.aborted) {
|
|
189
|
+
await closeTangleStreamQuietly(stream);
|
|
190
|
+
options.signal.throwIfAborted();
|
|
191
|
+
}
|
|
192
|
+
const prepared = await prepareTangleTerminalAttachment({
|
|
193
|
+
stream,
|
|
194
|
+
capture,
|
|
195
|
+
terminals,
|
|
196
|
+
parentExecutionId: ref.run.executionId,
|
|
197
|
+
expectedSessionId: ref.run.sessionId,
|
|
198
|
+
signal: options?.signal,
|
|
199
|
+
beforeMutation: (operation) => {
|
|
200
|
+
if (operation === "detach")
|
|
201
|
+
return Promise.resolve();
|
|
202
|
+
return validateControl(exactRequest.control, options);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
if (prepared.status === "unknown") {
|
|
206
|
+
throw new Error(prepared.message);
|
|
207
|
+
}
|
|
208
|
+
if (prepared.acknowledgement.restored !== true) {
|
|
209
|
+
await prepared.session.detach();
|
|
210
|
+
throw new Error("the exact interactive attach did not restore the coding-agent PTY");
|
|
211
|
+
}
|
|
212
|
+
const after = await readStatus(options);
|
|
213
|
+
if (after.state !== "running") {
|
|
214
|
+
await prepared.session.detach();
|
|
215
|
+
throw new Error("the exact interactive agent exited while its terminal attached");
|
|
216
|
+
}
|
|
217
|
+
const terminal = bindTangleInteractiveControl(prepared.session, exactRequest.control);
|
|
218
|
+
return terminal;
|
|
219
|
+
},
|
|
220
|
+
async sendPrompt(command, options) {
|
|
221
|
+
assertOptionKeys(options, ["signal"], "Tangle interactive agent prompt");
|
|
222
|
+
const exactCommand = AgentInteractiveSessionPromptCommandSchema.parse(command);
|
|
223
|
+
assertRef(ref, exactCommand.ref);
|
|
224
|
+
await validateControl(exactCommand.control, options);
|
|
225
|
+
let raw;
|
|
226
|
+
try {
|
|
227
|
+
raw = await handle.sendPrompt(exactCommand);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
options?.signal?.throwIfAborted();
|
|
231
|
+
throw new Error(transportFailureReason("interactive prompt", error));
|
|
232
|
+
}
|
|
233
|
+
options?.signal?.throwIfAborted();
|
|
234
|
+
const acknowledgement = AgentInteractiveSessionPromptAcknowledgementSchema.parse(raw);
|
|
235
|
+
if (!agentInteractiveSessionPromptAcknowledgementMatchesCommand(exactCommand, acknowledgement)) {
|
|
236
|
+
throw new Error("the Sandbox returned a prompt acknowledgement for another request");
|
|
237
|
+
}
|
|
238
|
+
return acknowledgement;
|
|
239
|
+
},
|
|
240
|
+
async stop(command, options) {
|
|
241
|
+
assertOptionKeys(options, ["signal"], "Tangle interactive agent stop");
|
|
242
|
+
const exactCommand = AgentInteractiveSessionStopCommandSchema.parse(command);
|
|
243
|
+
assertRef(ref, exactCommand.ref);
|
|
244
|
+
await validateControl(exactCommand.control, options);
|
|
245
|
+
let raw;
|
|
246
|
+
try {
|
|
247
|
+
raw = await handle.stop(exactCommand);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
options?.signal?.throwIfAborted();
|
|
251
|
+
throw new Error(transportFailureReason("interactive stop", error));
|
|
252
|
+
}
|
|
253
|
+
options?.signal?.throwIfAborted();
|
|
254
|
+
const acknowledgement = AgentInteractiveSessionStopAcknowledgementSchema.parse(raw);
|
|
255
|
+
if (!agentInteractiveSessionStopAcknowledgementMatchesCommand(exactCommand, acknowledgement)) {
|
|
256
|
+
throw new Error("the Sandbox returned a stop acknowledgement for another request");
|
|
257
|
+
}
|
|
258
|
+
return acknowledgement;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function isSandboxInteractiveSession(value) {
|
|
265
|
+
if (value === null || typeof value !== "object")
|
|
266
|
+
return false;
|
|
267
|
+
const candidate = value;
|
|
268
|
+
return [
|
|
269
|
+
"start",
|
|
270
|
+
"claimControl",
|
|
271
|
+
"status",
|
|
272
|
+
"attach",
|
|
273
|
+
"validateControl",
|
|
274
|
+
"sendPrompt",
|
|
275
|
+
"stop",
|
|
276
|
+
].every((key) => typeof candidate[key] === "function");
|
|
277
|
+
}
|
|
278
|
+
function refFromStart(request, observed) {
|
|
279
|
+
if (observed.sessionId !== request.run.sessionId ||
|
|
280
|
+
observed.harness !== request.profile.harness) {
|
|
281
|
+
throw new Error("the Sandbox returned a different interactive session identity");
|
|
282
|
+
}
|
|
283
|
+
return AgentInteractiveSessionRefSchema.parse({
|
|
284
|
+
run: request.run,
|
|
285
|
+
preparationReceipt: observed.preparationReceipt,
|
|
286
|
+
incarnationId: observed.incarnationId,
|
|
287
|
+
startedAt: observed.startedAt,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function statusFromSandbox(ref, observed) {
|
|
291
|
+
if (observed === null) {
|
|
292
|
+
return AgentInteractiveSessionStatusSchema.parse({
|
|
293
|
+
state: "unknown",
|
|
294
|
+
ref,
|
|
295
|
+
message: "the Sandbox no longer knows this interactive agent session",
|
|
296
|
+
retryable: false,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const observedRef = AgentInteractiveSessionRefSchema.parse({
|
|
300
|
+
run: ref.run,
|
|
301
|
+
preparationReceipt: observed.preparationReceipt,
|
|
302
|
+
incarnationId: observed.incarnationId,
|
|
303
|
+
startedAt: observed.startedAt,
|
|
304
|
+
});
|
|
305
|
+
if (observed.sessionId !== ref.run.sessionId ||
|
|
306
|
+
observed.harness !== ref.preparationReceipt.harness ||
|
|
307
|
+
observedRef.preparationReceipt.digest !== ref.preparationReceipt.digest) {
|
|
308
|
+
throw new Error("the Sandbox reported a different interactive agent session identity");
|
|
309
|
+
}
|
|
310
|
+
if (observed.state === "running") {
|
|
311
|
+
if (observed.streamUrl.length === 0) {
|
|
312
|
+
throw new Error("the Sandbox reported a running session without a stream");
|
|
313
|
+
}
|
|
314
|
+
return AgentInteractiveSessionStatusSchema.parse({ state: "running", ref });
|
|
315
|
+
}
|
|
316
|
+
return AgentInteractiveSessionStatusSchema.parse({
|
|
317
|
+
state: "exited",
|
|
318
|
+
ref,
|
|
319
|
+
endedAt: observed.endedAt,
|
|
320
|
+
reason: observed.reason,
|
|
321
|
+
...(Number.isSafeInteger(observed.exitCode)
|
|
322
|
+
? { exitCode: observed.exitCode }
|
|
323
|
+
: {}),
|
|
324
|
+
...(typeof observed.exitSignal === "string" && observed.exitSignal.length > 0
|
|
325
|
+
? { exitSignal: observed.exitSignal }
|
|
326
|
+
: {}),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AgentTerminalSession, TerminalAttachRequest, TerminalAttachResult } from "@tangle-network/agent-interface";
|
|
2
|
-
import type { SandboxInstanceLike } from "./tangle-types.js";
|
|
1
|
+
import type { AgentInteractiveSessionControlClaim, AgentInteractiveTerminalSession, AgentTerminalSession, TerminalAttachRequest, TerminalAttachResult } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { SandboxInstanceLike, SandboxTerminalReadyLike, SandboxTerminalStreamLike, SandboxTerminalsLike } from "./tangle-types.js";
|
|
3
|
+
import { TerminalFrameLog } from "./tangle-terminal-frames.js";
|
|
3
4
|
/** Terminal handles this environment holds, keyed by terminal session id. */
|
|
4
5
|
export interface TangleTerminalRegistry {
|
|
5
6
|
attach(request: TerminalAttachRequest, options?: {
|
|
@@ -9,4 +10,69 @@ export interface TangleTerminalRegistry {
|
|
|
9
10
|
}
|
|
10
11
|
/** True when this sandbox backs the interactive terminal surface. */
|
|
11
12
|
export declare function sandboxBacksInteractiveTerminal(box: SandboxInstanceLike): boolean;
|
|
13
|
+
/** Stream state captured from handlers that are installed before attach. */
|
|
14
|
+
export interface TangleTerminalStreamCapture {
|
|
15
|
+
readonly handlers: {
|
|
16
|
+
onReady(info: SandboxTerminalReadyLike): void;
|
|
17
|
+
onData(data: Uint8Array): void;
|
|
18
|
+
onExit(info: {
|
|
19
|
+
exitCode?: number;
|
|
20
|
+
exitSignal?: string;
|
|
21
|
+
}): void;
|
|
22
|
+
onError(error: Error): void;
|
|
23
|
+
onClose(): void;
|
|
24
|
+
};
|
|
25
|
+
readonly log: TerminalFrameLog;
|
|
26
|
+
readonly activity: {
|
|
27
|
+
localMs: number;
|
|
28
|
+
};
|
|
29
|
+
readonly exit: {
|
|
30
|
+
exitCode?: number;
|
|
31
|
+
exitSignal?: string;
|
|
32
|
+
seen: boolean;
|
|
33
|
+
};
|
|
34
|
+
readonly ready: SandboxTerminalReadyLike | undefined;
|
|
35
|
+
}
|
|
36
|
+
/** A live stream after its exact identity and runtime metadata are checked. */
|
|
37
|
+
export type PreparedTangleTerminal = {
|
|
38
|
+
status: "ready";
|
|
39
|
+
session: AgentTerminalSession;
|
|
40
|
+
stream: SandboxTerminalStreamLike;
|
|
41
|
+
acknowledgement: SandboxTerminalReadyLike;
|
|
42
|
+
} | {
|
|
43
|
+
status: "unknown";
|
|
44
|
+
message: string;
|
|
45
|
+
retryable: boolean;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Install one ordered capture before a terminal socket opens.
|
|
49
|
+
*
|
|
50
|
+
* Both generic shells and exact coding-agent sessions use this capture. This
|
|
51
|
+
* keeps replay, resize, exit, and transport ordering identical on both paths.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createTangleTerminalStreamCapture(geometry?: {
|
|
54
|
+
cols?: number;
|
|
55
|
+
rows?: number;
|
|
56
|
+
}): TangleTerminalStreamCapture;
|
|
57
|
+
/**
|
|
58
|
+
* Validate one attached socket and adapt it to the shared terminal contract.
|
|
59
|
+
*
|
|
60
|
+
* The runtime acknowledgement and metadata are authoritative. A missing or
|
|
61
|
+
* different session id closes the socket and returns no usable handle.
|
|
62
|
+
*/
|
|
63
|
+
export declare function prepareTangleTerminalAttachment(input: {
|
|
64
|
+
stream: SandboxTerminalStreamLike;
|
|
65
|
+
capture: TangleTerminalStreamCapture;
|
|
66
|
+
terminals: SandboxTerminalsLike;
|
|
67
|
+
parentExecutionId: string;
|
|
68
|
+
expectedSessionId?: string;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
beforeMutation?: (operation: "input" | "resize" | "detach" | "close") => Promise<void>;
|
|
71
|
+
attachCount?: (terminalSessionId: string) => number;
|
|
72
|
+
release?: (terminalSessionId: string, stream: SandboxTerminalStreamLike) => void;
|
|
73
|
+
}): Promise<PreparedTangleTerminal>;
|
|
12
74
|
export declare function createTangleTerminalRegistry(box: SandboxInstanceLike): TangleTerminalRegistry;
|
|
75
|
+
/** Close a stream that has not been handed to a caller. */
|
|
76
|
+
export declare function closeTangleStreamQuietly(stream: SandboxTerminalStreamLike): Promise<void>;
|
|
77
|
+
/** Add the exact claim identity without replacing the terminal implementation. */
|
|
78
|
+
export declare function bindTangleInteractiveControl(session: AgentTerminalSession, control: AgentInteractiveSessionControlClaim): AgentInteractiveTerminalSession;
|
package/dist/tangle-terminal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { TerminalAttachRequestSchema, TerminalInputSchema, TerminalResizeSchema, TerminalSessionRefSchema, terminalSessionUsable, } from "@tangle-network/agent-interface";
|
|
3
3
|
import { TerminalFrameLog } from "./tangle-terminal-frames.js";
|
|
4
|
-
import { awaitWithSignal, MAX_STRING_LENGTH } from "./tangle-contract-safety.js";
|
|
4
|
+
import { awaitWithSignal, awaitWithSignalAndCleanup, MAX_STRING_LENGTH, } from "./tangle-contract-safety.js";
|
|
5
5
|
import { transportFailureReason } from "./tangle-failure-reason.js";
|
|
6
6
|
import { assertOptionKeys } from "./tangle-environment-validation.js";
|
|
7
7
|
const MAX_TERMINAL_DIMENSION = 10_000;
|
|
@@ -19,6 +19,166 @@ export function sandboxBacksInteractiveTerminal(box) {
|
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Install one ordered capture before a terminal socket opens.
|
|
24
|
+
*
|
|
25
|
+
* Both generic shells and exact coding-agent sessions use this capture. This
|
|
26
|
+
* keeps replay, resize, exit, and transport ordering identical on both paths.
|
|
27
|
+
*/
|
|
28
|
+
export function createTangleTerminalStreamCapture(geometry) {
|
|
29
|
+
const log = new TerminalFrameLog();
|
|
30
|
+
const activity = { localMs: Date.now() };
|
|
31
|
+
const exit = {
|
|
32
|
+
seen: false,
|
|
33
|
+
};
|
|
34
|
+
const decoder = new TextDecoder();
|
|
35
|
+
let ready;
|
|
36
|
+
return {
|
|
37
|
+
log,
|
|
38
|
+
activity,
|
|
39
|
+
exit,
|
|
40
|
+
get ready() {
|
|
41
|
+
return ready;
|
|
42
|
+
},
|
|
43
|
+
handlers: {
|
|
44
|
+
onReady(info) {
|
|
45
|
+
ready = info;
|
|
46
|
+
log.append({
|
|
47
|
+
type: "ready",
|
|
48
|
+
...(geometry?.cols === undefined ? {} : { cols: geometry.cols }),
|
|
49
|
+
...(geometry?.rows === undefined ? {} : { rows: geometry.rows }),
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
onData(data) {
|
|
53
|
+
activity.localMs = Date.now();
|
|
54
|
+
log.appendOutput(decoder.decode(data, { stream: true }));
|
|
55
|
+
},
|
|
56
|
+
onExit(info) {
|
|
57
|
+
exit.seen = true;
|
|
58
|
+
exit.exitCode = info.exitCode;
|
|
59
|
+
exit.exitSignal = info.exitSignal;
|
|
60
|
+
log.append({
|
|
61
|
+
type: "exit",
|
|
62
|
+
...(Number.isSafeInteger(info.exitCode)
|
|
63
|
+
? { exitCode: info.exitCode }
|
|
64
|
+
: {}),
|
|
65
|
+
...(typeof info.exitSignal === "string" && info.exitSignal.length > 0
|
|
66
|
+
? { exitSignal: info.exitSignal }
|
|
67
|
+
: {}),
|
|
68
|
+
});
|
|
69
|
+
log.end();
|
|
70
|
+
},
|
|
71
|
+
onError(error) {
|
|
72
|
+
log.append({ type: "error", message: socketErrorFrame(error) });
|
|
73
|
+
},
|
|
74
|
+
onClose() {
|
|
75
|
+
log.end();
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate one attached socket and adapt it to the shared terminal contract.
|
|
82
|
+
*
|
|
83
|
+
* The runtime acknowledgement and metadata are authoritative. A missing or
|
|
84
|
+
* different session id closes the socket and returns no usable handle.
|
|
85
|
+
*/
|
|
86
|
+
export async function prepareTangleTerminalAttachment(input) {
|
|
87
|
+
let streamClosed = false;
|
|
88
|
+
const closePreparedStream = async () => {
|
|
89
|
+
if (streamClosed)
|
|
90
|
+
return;
|
|
91
|
+
streamClosed = true;
|
|
92
|
+
await closeQuietly(input.stream);
|
|
93
|
+
};
|
|
94
|
+
let handedOff = false;
|
|
95
|
+
try {
|
|
96
|
+
input.signal?.throwIfAborted();
|
|
97
|
+
let acknowledgement;
|
|
98
|
+
try {
|
|
99
|
+
acknowledgement = input.capture.ready ?? input.stream.ready;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
await closePreparedStream();
|
|
103
|
+
return {
|
|
104
|
+
status: "unknown",
|
|
105
|
+
message: transportFailureReason("terminal acknowledgement", error),
|
|
106
|
+
retryable: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (acknowledgement === undefined ||
|
|
110
|
+
typeof acknowledgement.sessionId !== "string" ||
|
|
111
|
+
acknowledgement.sessionId.length === 0) {
|
|
112
|
+
await closePreparedStream();
|
|
113
|
+
return {
|
|
114
|
+
status: "unknown",
|
|
115
|
+
message: "the Tangle terminal transport attached without a session id",
|
|
116
|
+
retryable: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (input.expectedSessionId !== undefined &&
|
|
120
|
+
acknowledgement.sessionId !== input.expectedSessionId) {
|
|
121
|
+
await closePreparedStream();
|
|
122
|
+
return {
|
|
123
|
+
status: "unknown",
|
|
124
|
+
message: "the Tangle terminal transport attached a different terminal session",
|
|
125
|
+
retryable: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (!Number.isSafeInteger(acknowledgement.detachTimeoutMs) ||
|
|
129
|
+
acknowledgement.detachTimeoutMs <= 0) {
|
|
130
|
+
await closePreparedStream();
|
|
131
|
+
return {
|
|
132
|
+
status: "unknown",
|
|
133
|
+
message: "the Tangle terminal transport reported no detach window",
|
|
134
|
+
retryable: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
let info;
|
|
138
|
+
try {
|
|
139
|
+
info = await awaitWithSignal(input.terminals.get(acknowledgement.sessionId), input.signal);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
await closePreparedStream();
|
|
143
|
+
input.signal?.throwIfAborted();
|
|
144
|
+
return {
|
|
145
|
+
status: "unknown",
|
|
146
|
+
message: transportFailureReason("terminal metadata read", error),
|
|
147
|
+
retryable: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
input.signal?.throwIfAborted();
|
|
151
|
+
if (info === null || info === undefined) {
|
|
152
|
+
await closePreparedStream();
|
|
153
|
+
return {
|
|
154
|
+
status: "unknown",
|
|
155
|
+
message: "the Tangle runtime reported no metadata for the attached terminal",
|
|
156
|
+
retryable: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const state = terminalStateFromInfo(info, acknowledgement, input.parentExecutionId, input.capture.activity.localMs);
|
|
160
|
+
if (typeof state === "string") {
|
|
161
|
+
await closePreparedStream();
|
|
162
|
+
return { status: "unknown", message: state, retryable: false };
|
|
163
|
+
}
|
|
164
|
+
state.attachCount = input.attachCount?.(state.terminalSessionId) ?? 1;
|
|
165
|
+
const session = createTangleTerminalSession(input.stream, input.capture.log, state, input.capture.activity, input.capture.exit, () => input.release?.(state.terminalSessionId, input.stream), input.beforeMutation);
|
|
166
|
+
input.signal?.throwIfAborted();
|
|
167
|
+
handedOff = true;
|
|
168
|
+
return {
|
|
169
|
+
status: "ready",
|
|
170
|
+
session,
|
|
171
|
+
stream: input.stream,
|
|
172
|
+
acknowledgement,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (!handedOff)
|
|
177
|
+
await closePreparedStream();
|
|
178
|
+
input.signal?.throwIfAborted();
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
22
182
|
export function createTangleTerminalRegistry(box) {
|
|
23
183
|
const attached = new Map();
|
|
24
184
|
return {
|
|
@@ -46,61 +206,21 @@ export function createTangleTerminalRegistry(box) {
|
|
|
46
206
|
};
|
|
47
207
|
}
|
|
48
208
|
const connectionId = exactRequest.connectionId ?? exactRequest.terminalSessionId ?? randomUUID();
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
const decoder = new TextDecoder();
|
|
55
|
-
let ready;
|
|
209
|
+
const capture = createTangleTerminalStreamCapture({
|
|
210
|
+
cols: exactRequest.cols,
|
|
211
|
+
rows: exactRequest.rows,
|
|
212
|
+
});
|
|
56
213
|
let stream;
|
|
57
214
|
try {
|
|
58
|
-
stream = await
|
|
215
|
+
stream = await awaitWithSignalAndCleanup(() => terminals.attach(connectionId, {
|
|
59
216
|
...(exactRequest.cols === undefined ? {} : { cols: exactRequest.cols }),
|
|
60
217
|
...(exactRequest.rows === undefined ? {} : { rows: exactRequest.rows }),
|
|
61
218
|
...(exactRequest.command === undefined
|
|
62
219
|
? {}
|
|
63
220
|
: { command: exactRequest.command }),
|
|
64
221
|
...(exactRequest.cwd === undefined ? {} : { cwd: exactRequest.cwd }),
|
|
65
|
-
handlers:
|
|
66
|
-
|
|
67
|
-
ready = info;
|
|
68
|
-
log.append({
|
|
69
|
-
type: "ready",
|
|
70
|
-
...(exactRequest.cols === undefined ? {} : { cols: exactRequest.cols }),
|
|
71
|
-
...(exactRequest.rows === undefined ? {} : { rows: exactRequest.rows }),
|
|
72
|
-
});
|
|
73
|
-
},
|
|
74
|
-
onData: (data) => {
|
|
75
|
-
activity.localMs = Date.now();
|
|
76
|
-
log.appendOutput(decoder.decode(data, { stream: true }));
|
|
77
|
-
},
|
|
78
|
-
onExit: (info) => {
|
|
79
|
-
exit.seen = true;
|
|
80
|
-
exit.exitCode = info.exitCode;
|
|
81
|
-
exit.exitSignal = info.exitSignal;
|
|
82
|
-
log.append({
|
|
83
|
-
type: "exit",
|
|
84
|
-
...(Number.isSafeInteger(info.exitCode)
|
|
85
|
-
? { exitCode: info.exitCode }
|
|
86
|
-
: {}),
|
|
87
|
-
...(typeof info.exitSignal === "string" && info.exitSignal.length > 0
|
|
88
|
-
? { exitSignal: info.exitSignal }
|
|
89
|
-
: {}),
|
|
90
|
-
});
|
|
91
|
-
log.end();
|
|
92
|
-
},
|
|
93
|
-
onError: (error) => {
|
|
94
|
-
// A frame belongs to the PTY stream, which carries whatever the
|
|
95
|
-
// terminal itself produced, so the socket error is carried as
|
|
96
|
-
// the terminal reports it and only its length is bounded.
|
|
97
|
-
log.append({ type: "error", message: socketErrorFrame(error) });
|
|
98
|
-
},
|
|
99
|
-
onClose: () => {
|
|
100
|
-
log.end();
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
}), options?.signal);
|
|
222
|
+
handlers: capture.handlers,
|
|
223
|
+
}), options?.signal, closeQuietly);
|
|
104
224
|
}
|
|
105
225
|
catch (error) {
|
|
106
226
|
options?.signal?.throwIfAborted();
|
|
@@ -110,103 +230,44 @@ export function createTangleTerminalRegistry(box) {
|
|
|
110
230
|
retryable: true,
|
|
111
231
|
};
|
|
112
232
|
}
|
|
113
|
-
options?.signal?.
|
|
114
|
-
let acknowledgement;
|
|
115
|
-
try {
|
|
116
|
-
// `ready` is an accessor on the SDK stream that throws until the
|
|
117
|
-
// runtime's acknowledgement arrives. Read outside a guard it would
|
|
118
|
-
// replace this attach's result with a raw transport error and abandon
|
|
119
|
-
// the socket the attach opened.
|
|
120
|
-
acknowledgement = ready ?? stream.ready;
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
await closeQuietly(stream);
|
|
124
|
-
return {
|
|
125
|
-
status: "unknown",
|
|
126
|
-
message: transportFailureReason("terminal acknowledgement", error),
|
|
127
|
-
retryable: true,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (acknowledgement === undefined ||
|
|
131
|
-
typeof acknowledgement.sessionId !== "string" ||
|
|
132
|
-
acknowledgement.sessionId.length === 0) {
|
|
133
|
-
await closeQuietly(stream);
|
|
134
|
-
return {
|
|
135
|
-
status: "unknown",
|
|
136
|
-
message: "the Tangle terminal transport attached without a session id",
|
|
137
|
-
retryable: false,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
if (exactRequest.terminalSessionId !== undefined &&
|
|
141
|
-
acknowledgement.sessionId !== exactRequest.terminalSessionId) {
|
|
142
|
-
await closeQuietly(stream);
|
|
143
|
-
return {
|
|
144
|
-
status: "unknown",
|
|
145
|
-
message: "the Tangle terminal transport attached a different terminal session",
|
|
146
|
-
retryable: false,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (!Number.isSafeInteger(acknowledgement.detachTimeoutMs) ||
|
|
150
|
-
acknowledgement.detachTimeoutMs <= 0) {
|
|
151
|
-
// The detach window is the only bound on how long the runtime keeps
|
|
152
|
-
// this PTY, so without it the reference cannot state an expiry and
|
|
153
|
-
// every later call would be unbounded.
|
|
154
|
-
await closeQuietly(stream);
|
|
155
|
-
return {
|
|
156
|
-
status: "unknown",
|
|
157
|
-
message: "the Tangle terminal transport reported no detach window",
|
|
158
|
-
retryable: false,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
let info;
|
|
162
|
-
try {
|
|
163
|
-
info = await awaitWithSignal(terminals.get(acknowledgement.sessionId), options?.signal);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
options?.signal?.throwIfAborted();
|
|
167
|
-
await closeQuietly(stream);
|
|
168
|
-
return {
|
|
169
|
-
status: "unknown",
|
|
170
|
-
message: transportFailureReason("terminal metadata read", error),
|
|
171
|
-
retryable: true,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (info === null || info === undefined) {
|
|
175
|
-
await closeQuietly(stream);
|
|
176
|
-
return {
|
|
177
|
-
status: "unknown",
|
|
178
|
-
message: "the Tangle runtime reported no metadata for the attached terminal",
|
|
179
|
-
retryable: true,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
const state = terminalStateFromInfo(info, acknowledgement, exactRequest.parentExecutionId, activity.localMs);
|
|
183
|
-
if (typeof state === "string") {
|
|
233
|
+
if (options?.signal?.aborted) {
|
|
184
234
|
await closeQuietly(stream);
|
|
185
|
-
|
|
235
|
+
options.signal.throwIfAborted();
|
|
186
236
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
attached.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
237
|
+
const prepared = await prepareTangleTerminalAttachment({
|
|
238
|
+
stream,
|
|
239
|
+
capture,
|
|
240
|
+
terminals,
|
|
241
|
+
parentExecutionId: exactRequest.parentExecutionId,
|
|
242
|
+
expectedSessionId: exactRequest.terminalSessionId,
|
|
243
|
+
signal: options?.signal,
|
|
244
|
+
attachCount: (terminalSessionId) => {
|
|
245
|
+
const previous = attached.get(terminalSessionId);
|
|
246
|
+
return previous === undefined ? 1 : previous.session.ref.attachCount + 1;
|
|
247
|
+
},
|
|
248
|
+
release: (terminalSessionId, heldStream) => {
|
|
249
|
+
if (attached.get(terminalSessionId)?.stream === heldStream) {
|
|
250
|
+
attached.delete(terminalSessionId);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
if (prepared.status === "unknown")
|
|
255
|
+
return prepared;
|
|
256
|
+
const previous = attached.get(prepared.session.ref.terminalSessionId);
|
|
257
|
+
attached.set(prepared.session.ref.terminalSessionId, {
|
|
258
|
+
session: prepared.session,
|
|
259
|
+
stream: prepared.stream,
|
|
260
|
+
});
|
|
200
261
|
// The registry holds one socket per terminal. The socket this attach
|
|
201
262
|
// replaces stays open on the runtime until its own close, so it is
|
|
202
263
|
// closed here rather than abandoned with the handle that owned it.
|
|
203
264
|
if (previous !== undefined)
|
|
204
265
|
await closeQuietly(previous.stream);
|
|
205
266
|
return {
|
|
206
|
-
status: acknowledgement.restored === true ? "reattached" : "attached",
|
|
267
|
+
status: prepared.acknowledgement.restored === true ? "reattached" : "attached",
|
|
207
268
|
mode: "attach",
|
|
208
|
-
ref: session.ref,
|
|
209
|
-
attachCount:
|
|
269
|
+
ref: prepared.session.ref,
|
|
270
|
+
attachCount: prepared.session.ref.attachCount,
|
|
210
271
|
};
|
|
211
272
|
},
|
|
212
273
|
get(terminalSessionId) {
|
|
@@ -272,7 +333,7 @@ function terminalStateFromInfo(info, ready, parentExecutionId, localLastActivity
|
|
|
272
333
|
attachCount: 1,
|
|
273
334
|
};
|
|
274
335
|
}
|
|
275
|
-
function createTangleTerminalSession(stream, log, state, activity, exit, release) {
|
|
336
|
+
function createTangleTerminalSession(stream, log, state, activity, exit, release, beforeMutation) {
|
|
276
337
|
// A reference describes what this handle can do with its own socket. The
|
|
277
338
|
// runtime keeps a detached PTY alive, but a closed socket carries no input
|
|
278
339
|
// and no output, so a detached or closed handle reports the terminal as not
|
|
@@ -330,6 +391,8 @@ function createTangleTerminalSession(stream, log, state, activity, exit, release
|
|
|
330
391
|
assertOptionKeys(options, ["signal"], "Tangle terminal input");
|
|
331
392
|
const exactInput = TerminalInputSchema.parse(input);
|
|
332
393
|
options?.signal?.throwIfAborted();
|
|
394
|
+
await beforeMutation?.("input");
|
|
395
|
+
options?.signal?.throwIfAborted();
|
|
333
396
|
assertUsable("input");
|
|
334
397
|
stream.write(exactInput.data);
|
|
335
398
|
activity.localMs = Date.now();
|
|
@@ -338,6 +401,8 @@ function createTangleTerminalSession(stream, log, state, activity, exit, release
|
|
|
338
401
|
assertOptionKeys(options, ["signal"], "Tangle terminal resize");
|
|
339
402
|
const exactResize = TerminalResizeSchema.parse(resize);
|
|
340
403
|
options?.signal?.throwIfAborted();
|
|
404
|
+
await beforeMutation?.("resize");
|
|
405
|
+
options?.signal?.throwIfAborted();
|
|
341
406
|
assertUsable("resize");
|
|
342
407
|
stream.resize(exactResize.cols, exactResize.rows);
|
|
343
408
|
state.cols = exactResize.cols;
|
|
@@ -351,6 +416,8 @@ function createTangleTerminalSession(stream, log, state, activity, exit, release
|
|
|
351
416
|
async detach(options) {
|
|
352
417
|
assertOptionKeys(options, ["signal"], "Tangle terminal detach");
|
|
353
418
|
options?.signal?.throwIfAborted();
|
|
419
|
+
await beforeMutation?.("detach");
|
|
420
|
+
options?.signal?.throwIfAborted();
|
|
354
421
|
detached = true;
|
|
355
422
|
release();
|
|
356
423
|
await awaitWithSignal(stream.close(), options?.signal);
|
|
@@ -364,6 +431,8 @@ function createTangleTerminalSession(stream, log, state, activity, exit, release
|
|
|
364
431
|
async close(options) {
|
|
365
432
|
assertOptionKeys(options, ["signal"], "Tangle terminal close");
|
|
366
433
|
options?.signal?.throwIfAborted();
|
|
434
|
+
await beforeMutation?.("close");
|
|
435
|
+
options?.signal?.throwIfAborted();
|
|
367
436
|
detached = true;
|
|
368
437
|
release();
|
|
369
438
|
await awaitWithSignal(stream.close(), options?.signal);
|
|
@@ -407,6 +476,21 @@ async function closeQuietly(stream) {
|
|
|
407
476
|
// caller's failure is reported from the attach result it receives.
|
|
408
477
|
}
|
|
409
478
|
}
|
|
479
|
+
/** Close a stream that has not been handed to a caller. */
|
|
480
|
+
export async function closeTangleStreamQuietly(stream) {
|
|
481
|
+
await closeQuietly(stream);
|
|
482
|
+
}
|
|
483
|
+
/** Add the exact claim identity without replacing the terminal implementation. */
|
|
484
|
+
export function bindTangleInteractiveControl(session, control) {
|
|
485
|
+
return Object.create(session, {
|
|
486
|
+
control: {
|
|
487
|
+
configurable: false,
|
|
488
|
+
enumerable: true,
|
|
489
|
+
value: Object.freeze({ ...control }),
|
|
490
|
+
writable: false,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
410
494
|
function boundedLabel(value) {
|
|
411
495
|
return typeof value === "string" && value.length > 0 && value.length <= 512
|
|
412
496
|
? value
|
package/dist/tangle-types.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 { AgentRunCancellationAcknowledgement, AgentRunCancellationRequest, InputPart, InteractionRequest, InteractionResponseCommand } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { AgentRunCancellationAcknowledgement, AgentRunCancellationRequest, AgentExecutionPreparationReceipt, AgentInteractiveSessionControlClaim, AgentInteractiveSessionControlClaimAcknowledgement, AgentInteractiveSessionControlClaimRequest, AgentInteractiveSessionPromptAcknowledgement, AgentInteractiveSessionPromptCommand, AgentInteractiveSessionStopAcknowledgement, AgentInteractiveSessionStopCommand, AgentProfile, InputPart, InteractionRequest, InteractionResponseCommand } from "@tangle-network/agent-interface";
|
|
3
3
|
import type { AgentEnvironmentCapabilities, AgentEnvironmentProvider, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
|
|
4
4
|
export interface TangleExactProcessOptions {
|
|
5
5
|
teamId?: string;
|
|
@@ -103,6 +103,20 @@ export interface SandboxRuntimeCapabilityDocument {
|
|
|
103
103
|
*/
|
|
104
104
|
responseDedupe?: boolean;
|
|
105
105
|
};
|
|
106
|
+
interactiveAgent?: {
|
|
107
|
+
/** Start binds one native TUI to the requested session id. */
|
|
108
|
+
start?: boolean;
|
|
109
|
+
/** Status returns durable running, exited, stopped, or lost state. */
|
|
110
|
+
status?: boolean;
|
|
111
|
+
/** Attach reaches the existing TUI and never creates a shell. */
|
|
112
|
+
attach?: boolean;
|
|
113
|
+
/** The deployment owns exact control claims and generation fencing. */
|
|
114
|
+
control?: boolean;
|
|
115
|
+
/** Prompt input is sent to that existing TUI. */
|
|
116
|
+
sendPrompt?: boolean;
|
|
117
|
+
/** Stop reaps the exact TUI and records its terminal state. */
|
|
118
|
+
stop?: boolean;
|
|
119
|
+
};
|
|
106
120
|
}
|
|
107
121
|
export interface SandboxProcessStatusLike {
|
|
108
122
|
pid: number;
|
|
@@ -227,6 +241,58 @@ export interface SandboxTerminalsLike {
|
|
|
227
241
|
handlers?: SandboxTerminalHandlersLike;
|
|
228
242
|
}): Promise<SandboxTerminalStreamLike>;
|
|
229
243
|
}
|
|
244
|
+
/** Identity returned by the Sandbox exact interactive-session route. */
|
|
245
|
+
export interface SandboxInteractiveSessionIdentityLike {
|
|
246
|
+
sessionId: string;
|
|
247
|
+
harness: BackendType;
|
|
248
|
+
startedAt: string;
|
|
249
|
+
/** Provider-issued process incarnation. Replays return this exact value. */
|
|
250
|
+
incarnationId: string;
|
|
251
|
+
/** Canonical provider admission receipt for the effective route. */
|
|
252
|
+
preparationReceipt: AgentExecutionPreparationReceipt;
|
|
253
|
+
}
|
|
254
|
+
/** Start acknowledgement from the canonical Sandbox interactive API. */
|
|
255
|
+
export interface SandboxInteractiveSessionInfoLike extends SandboxInteractiveSessionIdentityLike {
|
|
256
|
+
streamUrl: string;
|
|
257
|
+
}
|
|
258
|
+
/** Lifecycle returned by the Sandbox exact interactive-session route. */
|
|
259
|
+
export type SandboxInteractiveSessionStatusLike = (SandboxInteractiveSessionInfoLike & {
|
|
260
|
+
state: "running";
|
|
261
|
+
}) | (SandboxInteractiveSessionIdentityLike & {
|
|
262
|
+
state: "exited";
|
|
263
|
+
endedAt: string;
|
|
264
|
+
exitCode?: number;
|
|
265
|
+
exitSignal?: string;
|
|
266
|
+
reason: "exited" | "stopped" | "lost";
|
|
267
|
+
});
|
|
268
|
+
/** Existing Sandbox SDK handle for one coding harness's native TUI. */
|
|
269
|
+
export interface SandboxInteractiveSessionLike {
|
|
270
|
+
start(options: {
|
|
271
|
+
harness: BackendType;
|
|
272
|
+
model?: string;
|
|
273
|
+
cwd?: string;
|
|
274
|
+
cols?: number;
|
|
275
|
+
rows?: number;
|
|
276
|
+
profile: AgentProfile;
|
|
277
|
+
initialPrompt?: string;
|
|
278
|
+
/** Derived from the exact run; owned by the Sandbox creation ledger. */
|
|
279
|
+
idempotencyKey: string;
|
|
280
|
+
/** Exact request digest paired with the idempotency key. */
|
|
281
|
+
requestDigest: `sha256:${string}`;
|
|
282
|
+
}): Promise<SandboxInteractiveSessionInfoLike>;
|
|
283
|
+
claimControl(request: AgentInteractiveSessionControlClaimRequest): Promise<AgentInteractiveSessionControlClaimAcknowledgement>;
|
|
284
|
+
status(): Promise<SandboxInteractiveSessionStatusLike | null>;
|
|
285
|
+
attach(options: {
|
|
286
|
+
control: AgentInteractiveSessionControlClaim;
|
|
287
|
+
cols?: number;
|
|
288
|
+
rows?: number;
|
|
289
|
+
handlers?: SandboxTerminalHandlersLike;
|
|
290
|
+
}): Promise<SandboxTerminalStreamLike>;
|
|
291
|
+
/** Validate the current generation before each PTY mutation. */
|
|
292
|
+
validateControl(control: AgentInteractiveSessionControlClaim): Promise<void>;
|
|
293
|
+
sendPrompt(command: AgentInteractiveSessionPromptCommand): Promise<AgentInteractiveSessionPromptAcknowledgement>;
|
|
294
|
+
stop(command: AgentInteractiveSessionStopCommand): Promise<AgentInteractiveSessionStopAcknowledgement>;
|
|
295
|
+
}
|
|
230
296
|
export interface SandboxInstanceLike {
|
|
231
297
|
id: string;
|
|
232
298
|
name?: string;
|
|
@@ -302,10 +368,12 @@ export interface SandboxInstanceLike {
|
|
|
302
368
|
}): Promise<void>;
|
|
303
369
|
delete?(options?: {
|
|
304
370
|
signal?: AbortSignal;
|
|
305
|
-
}): Promise<
|
|
371
|
+
}): Promise<unknown>;
|
|
306
372
|
}
|
|
307
373
|
export interface SandboxSessionLike {
|
|
308
374
|
readonly id: string;
|
|
375
|
+
/** Exact native coding-agent TUI bound to this session id. */
|
|
376
|
+
interactive?(): unknown;
|
|
309
377
|
status(options?: {
|
|
310
378
|
signal?: AbortSignal;
|
|
311
379
|
}): Promise<unknown | null>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-tangle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"dist/tangle-environment-session.js",
|
|
54
54
|
"dist/tangle-interaction-response.d.ts",
|
|
55
55
|
"dist/tangle-interaction-response.js",
|
|
56
|
+
"dist/tangle-interactive.d.ts",
|
|
57
|
+
"dist/tangle-interactive.js",
|
|
56
58
|
"dist/tangle-environment-control.d.ts",
|
|
57
59
|
"dist/tangle-environment-control.js",
|
|
58
60
|
"dist/tangle-environment-validation.d.ts",
|
|
@@ -94,13 +96,13 @@
|
|
|
94
96
|
}
|
|
95
97
|
},
|
|
96
98
|
"devDependencies": {
|
|
97
|
-
"@tangle-network/agent-eval": "0.145.
|
|
98
|
-
"@tangle-network/agent-runtime": "0.
|
|
99
|
-
"@tangle-network/sandbox": "0.
|
|
99
|
+
"@tangle-network/agent-eval": "0.145.15",
|
|
100
|
+
"@tangle-network/agent-runtime": "0.135.3",
|
|
101
|
+
"@tangle-network/sandbox": "0.27.0",
|
|
100
102
|
"@types/node": "25.6.0",
|
|
101
103
|
"typescript": "^6.0.3",
|
|
102
104
|
"vitest": "^4.1.5",
|
|
103
|
-
"@tangle-network/agent-provider-testkit": "0.
|
|
105
|
+
"@tangle-network/agent-provider-testkit": "0.8.0"
|
|
104
106
|
},
|
|
105
107
|
"scripts": {
|
|
106
108
|
"build": "tsc -p tsconfig.json",
|