@tangle-network/agent-provider-tangle 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -54
- package/dist/tangle-failure-reason.js +4 -1
- package/dist/tangle-interactive.d.ts +2 -2
- package/dist/tangle-interactive.js +95 -140
- package/dist/tangle-types.d.ts +36 -45
- package/dist/tangle-workspace-branching.js +290 -70
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# @tangle-network/agent-provider-tangle
|
|
2
2
|
|
|
3
3
|
Wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`.
|
|
4
|
-
The peer range is `>=0.
|
|
5
|
-
The floor is 0.
|
|
4
|
+
The peer range is `>=0.34.0 <1.0.0`, and this package is developed and tested against 0.34.3.
|
|
5
|
+
The floor is 0.34.0 because workspace branching uses the keyed snapshot, fork,
|
|
6
6
|
lookup, and cleanup operations added to that SDK.
|
|
7
7
|
The provider fails closed when the configured backend or its catalog entry cannot be read.
|
|
8
8
|
Newer SDKs may also provide `getBackend()` as a lookup over the same catalog.
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
|
-
import { Sandbox } from
|
|
12
|
-
import { createTangleProvider } from
|
|
11
|
+
import { Sandbox } from "@tangle-network/sandbox";
|
|
12
|
+
import { createTangleProvider } from "@tangle-network/agent-provider-tangle";
|
|
13
13
|
|
|
14
14
|
const provider = createTangleProvider({
|
|
15
15
|
client: new Sandbox({ apiKey: process.env.TANGLE_API_KEY }),
|
|
16
|
-
})
|
|
16
|
+
});
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Detached dispatch returns the immutable Sandbox execution receipt in `controlRef`.
|
|
@@ -43,16 +43,16 @@ One provider reaches deployments of different ages, which is why the environment
|
|
|
43
43
|
|
|
44
44
|
Each deployment flag this adapter reads gates the claims it backs, and no flag is read that gates nothing:
|
|
45
45
|
|
|
46
|
-
| Deployment flag
|
|
47
|
-
|
|
|
48
|
-
| `dispatch.runControlRef`
|
|
46
|
+
| Deployment flag | Claims it gates |
|
|
47
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
48
|
+
| `dispatch.runControlRef` | `streaming.detach`, `streaming.turnIdempotency`, `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
49
49
|
| `dispatch.executionIdOnAdmission` | `streaming.detach`, `streaming.turnIdempotency`, `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
50
|
-
| `cancel.canonicalRunCancellation` | `sessions.continue`, `retainedControl`, `session.cancelRun`
|
|
51
|
-
| `cancel.digestBound`
|
|
52
|
-
| `cancel.idempotent`
|
|
53
|
-
| `runs.eventReplay`
|
|
54
|
-
| `runs.executionScopedStatus`
|
|
55
|
-
| `interactions.responseDedupe`
|
|
50
|
+
| `cancel.canonicalRunCancellation` | `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
51
|
+
| `cancel.digestBound` | `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
52
|
+
| `cancel.idempotent` | `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
53
|
+
| `runs.eventReplay` | `streaming.replay`, `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
54
|
+
| `runs.executionScopedStatus` | `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
55
|
+
| `interactions.responseDedupe` | `interactions`, `environment.respondToInteraction`, `session.respondToInteraction` |
|
|
56
56
|
|
|
57
57
|
Detached dispatch carries the caller's exact reference and refuses a receipt that does not name the execution back, so it needs both `dispatch` flags and a session handle to reach the run through.
|
|
58
58
|
`sessions.continue`, `retainedControl`, and `session.cancelRun` need every flag in the table, because the capability schema refuses a partial retained-control block and each identity rests on its own flag.
|
|
@@ -70,19 +70,19 @@ The claim and the two methods stand or fall together.
|
|
|
70
70
|
It is offered on the environment and on a session handle; the environment routes the command to the session its binding names.
|
|
71
71
|
The command carries only the answer the caller supplied. No field is filled in on the caller's behalf, and an answer the outstanding ask's spec rejects is refused with the field named.
|
|
72
72
|
|
|
73
|
-
| Result
|
|
74
|
-
|
|
|
75
|
-
| The deployment recorded and delivered the response
|
|
76
|
-
| The deployment already holds this exact response
|
|
73
|
+
| Result | Acknowledgement status |
|
|
74
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
75
|
+
| The deployment recorded and delivered the response | `accepted` |
|
|
76
|
+
| The deployment already holds this exact response | `already_resolved_same` |
|
|
77
77
|
| The deployment already holds a different response for the ask | `already_resolved_different`, message naming the recorded digest |
|
|
78
|
-
| The command names another provider, environment, or session
|
|
79
|
-
| The deployment refuses the binding
|
|
80
|
-
| The ask is unknown to the deployment
|
|
81
|
-
| The session is unknown to the deployment
|
|
82
|
-
| The ask left the outstanding set on a deadline
|
|
83
|
-
| The ask's spec rejects the answer
|
|
84
|
-
| The response is recorded and delivery is unconfirmed
|
|
85
|
-
| The request did not reach the route
|
|
78
|
+
| The command names another provider, environment, or session | `binding_mismatch` |
|
|
79
|
+
| The deployment refuses the binding | `binding_mismatch` |
|
|
80
|
+
| The ask is unknown to the deployment | `unknown_interaction` |
|
|
81
|
+
| The session is unknown to the deployment | `unknown_run` |
|
|
82
|
+
| The ask left the outstanding set on a deadline | `expired` |
|
|
83
|
+
| The ask's spec rejects the answer | `invalid_response`, message naming each field |
|
|
84
|
+
| The response is recorded and delivery is unconfirmed | `transport_failure`, `retryable: true` |
|
|
85
|
+
| The request did not reach the route | `transport_failure`, `retryable: true` |
|
|
86
86
|
|
|
87
87
|
A refusal is never reported as a success, and a recorded response whose delivery the deployment does not confirm is never reported as `accepted`.
|
|
88
88
|
|
|
@@ -150,24 +150,24 @@ Clean branch resources before deleting the source or use a platform reaper.
|
|
|
150
150
|
import {
|
|
151
151
|
workspaceCheckpointRequestDigest,
|
|
152
152
|
workspaceForkRequestDigest,
|
|
153
|
-
} from
|
|
153
|
+
} from "@tangle-network/agent-interface";
|
|
154
154
|
|
|
155
155
|
const checkpoint = await environment.workspaceBranching?.checkpoint({
|
|
156
156
|
source: exactRun,
|
|
157
|
-
idempotencyKey:
|
|
157
|
+
idempotencyKey: "checkpoint-before-analysis",
|
|
158
158
|
requestDigest: workspaceCheckpointRequestDigest({ source: exactRun }),
|
|
159
|
-
})
|
|
159
|
+
});
|
|
160
160
|
|
|
161
|
-
if (checkpoint?.status ===
|
|
161
|
+
if (checkpoint?.status === "created" || checkpoint?.status === "replayed") {
|
|
162
162
|
const fork = await environment.workspaceBranching?.fork({
|
|
163
163
|
checkpoint: checkpoint.checkpoint,
|
|
164
|
-
placement: { kind:
|
|
165
|
-
idempotencyKey:
|
|
164
|
+
placement: { kind: "sandbox", sandboxId: "analysis-worker" },
|
|
165
|
+
idempotencyKey: "analysis-worker",
|
|
166
166
|
requestDigest: workspaceForkRequestDigest({
|
|
167
167
|
checkpoint: checkpoint.checkpoint,
|
|
168
|
-
placement: { kind:
|
|
168
|
+
placement: { kind: "sandbox", sandboxId: "analysis-worker" },
|
|
169
169
|
}),
|
|
170
|
-
})
|
|
170
|
+
});
|
|
171
171
|
}
|
|
172
172
|
```
|
|
173
173
|
|
|
@@ -192,7 +192,7 @@ const provider = createTangleProvider({
|
|
|
192
192
|
client: new Sandbox({ apiKey: process.env.TANGLE_API_KEY }),
|
|
193
193
|
confidentialAttestationVerifier: async ({ report, attestation }) =>
|
|
194
194
|
(await verifyTangleQuote({ report, attestation })) ?? null,
|
|
195
|
-
})
|
|
195
|
+
});
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
The provider stores the raw report in `ConfidentialAttestation.quote` through
|
|
@@ -208,19 +208,19 @@ reports before verification.
|
|
|
208
208
|
Every surface carries a freshness discriminator, so a value the Sandbox SDK does not report is visibly absent instead of arriving as a measured zero.
|
|
209
209
|
The `observation` capability flag for a surface is true only when a source can put a value on it for that environment; the observation itself always carries the surface, with `unavailable` and its reason when there is nothing to report.
|
|
210
210
|
|
|
211
|
-
| Surface
|
|
212
|
-
|
|
|
213
|
-
| `identity`
|
|
214
|
-
| `lifecycle.status`
|
|
215
|
-
| `lifecycle.cleanup`
|
|
216
|
-
| `endpoint`
|
|
217
|
-
| `placement.verified`
|
|
218
|
-
| `resources.requested`
|
|
219
|
-
| `resources.effective`
|
|
220
|
-
| `resourceUse.current` / `peak` | cgroup `memoryCurrentMb` / `memoryPeakMb`
|
|
221
|
-
| `modelUsage`
|
|
222
|
-
| `computeBilling`
|
|
223
|
-
| `accountUsage`
|
|
211
|
+
| Surface | Sandbox source | State when the source is missing |
|
|
212
|
+
| ------------------------------ | ---------------------------------------------------------- | --------------------------------------- |
|
|
213
|
+
| `identity` | provider name and `box.id` | always known |
|
|
214
|
+
| `lifecycle.status` | `box.status` after a refresh | `stale` with the refresh failure |
|
|
215
|
+
| `lifecycle.cleanup` | `box.expiresAt` as a scheduled retirement | omitted from the lifecycle value |
|
|
216
|
+
| `endpoint` | scheme, host, and explicit port of `connection.runtimeUrl` | `unavailable` |
|
|
217
|
+
| `placement.verified` | `client.describePlacement(box)` | `unavailable` |
|
|
218
|
+
| `resources.requested` | the `create()` resource request | omitted on an environment rebuilt by id |
|
|
219
|
+
| `resources.effective` | cgroup `memoryLimitMb` and the attached GPU lease | `unavailable` |
|
|
220
|
+
| `resourceUse.current` / `peak` | cgroup `memoryCurrentMb` / `memoryPeakMb` | `unavailable` |
|
|
221
|
+
| `modelUsage` | the newest execution this handle measured | `unavailable` |
|
|
222
|
+
| `computeBilling` | the GPU lease's billed or estimated customer cost | `unavailable` |
|
|
223
|
+
| `accountUsage` | `client.subscription()` and `client.usage()` | `unavailable` |
|
|
224
224
|
|
|
225
225
|
Four quantities have no Sandbox source at all, and the adapter reports them as absent rather than deriving them.
|
|
226
226
|
There is no effective CPU or disk figure anywhere in the SDK, so `resources.effective` carries memory and the accelerator only.
|
|
@@ -266,16 +266,16 @@ Set `teamId` inside `exactProcess` to scope create, lookup, and recovery to one
|
|
|
266
266
|
const provider = createTangleProvider({
|
|
267
267
|
client: new Sandbox({ apiKey: process.env.TANGLE_API_KEY }),
|
|
268
268
|
exactProcess: {},
|
|
269
|
-
})
|
|
269
|
+
});
|
|
270
270
|
|
|
271
271
|
const environment = await provider.exactProcess!.create({
|
|
272
|
-
image:
|
|
273
|
-
egress: { mode:
|
|
272
|
+
image: "ghcr.io/acme/agent@sha256:<64-hex-manifest-digest>",
|
|
273
|
+
egress: { mode: "blocked" },
|
|
274
274
|
maxLifetimeMs: 120_000,
|
|
275
275
|
resources: { cpu: 1, memoryMb: 1024, diskMb: 1024 },
|
|
276
|
-
metadata: { executionId:
|
|
277
|
-
idempotencyKey:
|
|
278
|
-
})
|
|
276
|
+
metadata: { executionId: "run-1" },
|
|
277
|
+
idempotencyKey: "run-1",
|
|
278
|
+
});
|
|
279
279
|
```
|
|
280
280
|
|
|
281
281
|
The adapter rejects ordinary sandboxes during create, recovery, and list operations.
|
|
@@ -23,9 +23,12 @@ function failureCause(error) {
|
|
|
23
23
|
if (name === "TimeoutError")
|
|
24
24
|
return "timed out";
|
|
25
25
|
const status = httpStatus(detail.status);
|
|
26
|
+
const code = bareIdentifier(detail.code);
|
|
27
|
+
if (status !== undefined && code !== undefined) {
|
|
28
|
+
return `HTTP ${status}; code ${code}`;
|
|
29
|
+
}
|
|
26
30
|
if (status !== undefined)
|
|
27
31
|
return `HTTP ${status}`;
|
|
28
|
-
const code = bareIdentifier(detail.code);
|
|
29
32
|
if (code !== undefined)
|
|
30
33
|
return `code ${code}`;
|
|
31
34
|
// A bare `Error` names no cause beyond the message, which is never carried.
|
|
@@ -10,8 +10,8 @@ export interface TangleInteractiveAgentRegistry {
|
|
|
10
10
|
/**
|
|
11
11
|
* Test the linked SDK surface without accepting any older interactive shape.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Sandbox owns the exact reference, control claim, terminal binding, and
|
|
14
|
+
* mutation acknowledgements. An older handle shape fails this test closed.
|
|
15
15
|
*/
|
|
16
16
|
export declare function sandboxBacksInteractiveAgent(box: SandboxInstanceLike): boolean;
|
|
17
17
|
/**
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStatusSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionStopAcknowledgementMatchesCommand, canonicalCandidateDigest, exactAgentInteractiveSessionStart, } from "@tangle-network/agent-interface";
|
|
2
|
-
import { parseBackendType } from "@tangle-network/sandbox";
|
|
1
|
+
import { AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStatusSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, TerminalReplayWindowSchema, TerminalSessionRefSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionStatusMatchesRef, agentInteractiveSessionStopAcknowledgementMatchesCommand, canonicalCandidateDigest, exactAgentInteractiveSessionStart, } from "@tangle-network/agent-interface";
|
|
3
2
|
import { assertOptionKeys } from "./tangle-environment-validation.js";
|
|
4
3
|
import { transportFailureReason } from "./tangle-failure-reason.js";
|
|
5
|
-
import { bindTangleInteractiveControl, closeTangleStreamQuietly, createTangleTerminalStreamCapture, prepareTangleTerminalAttachment, } from "./tangle-terminal.js";
|
|
6
4
|
/**
|
|
7
5
|
* Test the linked SDK surface without accepting any older interactive shape.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Sandbox owns the exact reference, control claim, terminal binding, and
|
|
8
|
+
* mutation acknowledgements. An older handle shape fails this test closed.
|
|
11
9
|
*/
|
|
12
10
|
export function sandboxBacksInteractiveAgent(box) {
|
|
13
11
|
if (typeof box.session !== "function")
|
|
14
12
|
return false;
|
|
15
13
|
try {
|
|
16
|
-
const candidate = box
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const candidate = box
|
|
15
|
+
.session("__tangle-interactive-probe__")
|
|
16
|
+
.interactive?.();
|
|
17
|
+
return isSandboxInteractiveSession(candidate);
|
|
19
18
|
}
|
|
20
19
|
catch {
|
|
21
20
|
return false;
|
|
@@ -29,9 +28,9 @@ export function sandboxBacksInteractiveAgent(box) {
|
|
|
29
28
|
* never creates a second record for any of those facts.
|
|
30
29
|
*/
|
|
31
30
|
export function createTangleInteractiveAgentRegistry(box, providerName, environmentId) {
|
|
32
|
-
const exactHandle = (sessionId) => {
|
|
31
|
+
const exactHandle = (sessionId, binding) => {
|
|
33
32
|
const session = box.session?.(sessionId);
|
|
34
|
-
const candidate = session?.interactive?.();
|
|
33
|
+
const candidate = session?.interactive?.(binding);
|
|
35
34
|
if (!isSandboxInteractiveSession(candidate)) {
|
|
36
35
|
throw new Error("the linked Sandbox SDK does not expose the exact interactive control API");
|
|
37
36
|
}
|
|
@@ -50,10 +49,13 @@ export function createTangleInteractiveAgentRegistry(box, providerName, environm
|
|
|
50
49
|
throw new Error("the Sandbox returned a different interactive session ref");
|
|
51
50
|
}
|
|
52
51
|
};
|
|
53
|
-
const
|
|
52
|
+
const assertControlMatchesRef = (ref, control) => {
|
|
54
53
|
if (!agentInteractiveSessionControlClaimMatchesRef(ref, control)) {
|
|
55
54
|
throw new Error("the interactive control claim belongs to another process");
|
|
56
55
|
}
|
|
56
|
+
};
|
|
57
|
+
const assertActiveControl = (ref, control) => {
|
|
58
|
+
assertControlMatchesRef(ref, control);
|
|
57
59
|
if (Date.parse(control.expiresAt) <= Date.now()) {
|
|
58
60
|
throw new Error("the interactive control claim has expired");
|
|
59
61
|
}
|
|
@@ -62,70 +64,68 @@ export function createTangleInteractiveAgentRegistry(box, providerName, environm
|
|
|
62
64
|
async start(request, options) {
|
|
63
65
|
assertOptionKeys(options, ["signal"], "Tangle interactive agent start");
|
|
64
66
|
const exactRequest = exactAgentInteractiveSessionStart(request);
|
|
65
|
-
|
|
66
|
-
if (requestedHarness === undefined) {
|
|
67
|
+
if (exactRequest.profile.harness === undefined) {
|
|
67
68
|
throw new Error("interactive agent sessions require AgentProfile.harness");
|
|
68
69
|
}
|
|
69
|
-
let harness;
|
|
70
|
-
try {
|
|
71
|
-
harness = parseBackendType(requestedHarness);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
throw new Error("the requested AgentProfile harness has no Tangle Sandbox runner");
|
|
75
|
-
}
|
|
76
70
|
options?.signal?.throwIfAborted();
|
|
77
71
|
const handle = exactHandle(exactRequest.run.sessionId);
|
|
78
72
|
let observed;
|
|
79
73
|
try {
|
|
80
|
-
observed = await handle.start(
|
|
81
|
-
harness,
|
|
82
|
-
...(exactRequest.profile.model?.default === undefined
|
|
83
|
-
? {}
|
|
84
|
-
: { model: exactRequest.profile.model.default }),
|
|
85
|
-
...(exactRequest.cwd === undefined ? {} : { cwd: exactRequest.cwd }),
|
|
86
|
-
...(exactRequest.cols === undefined ? {} : { cols: exactRequest.cols }),
|
|
87
|
-
...(exactRequest.rows === undefined ? {} : { rows: exactRequest.rows }),
|
|
88
|
-
profile: exactRequest.profile,
|
|
89
|
-
...(exactRequest.initialPrompt === undefined
|
|
90
|
-
? {}
|
|
91
|
-
: { initialPrompt: exactRequest.initialPrompt }),
|
|
92
|
-
idempotencyKey: exactRequest.run.runId,
|
|
93
|
-
requestDigest: exactRequest.run.requestDigest,
|
|
94
|
-
});
|
|
74
|
+
observed = await handle.start(exactRequest, options);
|
|
95
75
|
}
|
|
96
76
|
catch (error) {
|
|
97
77
|
options?.signal?.throwIfAborted();
|
|
98
78
|
throw new Error(transportFailureReason("interactive start", error));
|
|
99
79
|
}
|
|
100
80
|
options?.signal?.throwIfAborted();
|
|
101
|
-
const ref =
|
|
81
|
+
const ref = AgentInteractiveSessionRefSchema.parse(observed.ref);
|
|
82
|
+
const control = AgentInteractiveSessionControlClaimSchema.parse(observed.control);
|
|
102
83
|
if (!agentInteractiveSessionRefMatchesStart(exactRequest, ref)) {
|
|
103
84
|
throw new Error("the Sandbox returned an interactive session with a different admission receipt");
|
|
104
85
|
}
|
|
86
|
+
assertControlMatchesRef(ref, control);
|
|
87
|
+
if (observed.state === "running")
|
|
88
|
+
assertActiveControl(ref, control);
|
|
89
|
+
if (observed.state === "running" && observed.streamUrl.length === 0) {
|
|
90
|
+
throw new Error("the Sandbox returned a running session without a stream");
|
|
91
|
+
}
|
|
105
92
|
return ref;
|
|
106
93
|
},
|
|
107
94
|
get(value) {
|
|
108
95
|
const ref = exactRef(value);
|
|
109
|
-
const handle = exactHandle(ref.run.sessionId);
|
|
96
|
+
const handle = exactHandle(ref.run.sessionId, { ref });
|
|
97
|
+
const mutationHandle = (control) => exactHandle(ref.run.sessionId, { ref, control });
|
|
110
98
|
const readStatus = async (options) => {
|
|
111
99
|
assertOptionKeys(options, ["signal"], "Tangle interactive agent status");
|
|
112
100
|
options?.signal?.throwIfAborted();
|
|
113
101
|
let observed;
|
|
114
102
|
try {
|
|
115
|
-
observed = await handle.status();
|
|
103
|
+
observed = await handle.status(options);
|
|
116
104
|
}
|
|
117
105
|
catch (error) {
|
|
118
106
|
options?.signal?.throwIfAborted();
|
|
119
107
|
throw new Error(transportFailureReason("interactive status", error));
|
|
120
108
|
}
|
|
121
109
|
options?.signal?.throwIfAborted();
|
|
122
|
-
|
|
110
|
+
if (observed === null) {
|
|
111
|
+
return AgentInteractiveSessionStatusSchema.parse({
|
|
112
|
+
state: "unknown",
|
|
113
|
+
ref,
|
|
114
|
+
message: "the Sandbox no longer knows this interactive agent session",
|
|
115
|
+
retryable: false,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const status = AgentInteractiveSessionStatusSchema.parse(observed);
|
|
119
|
+
if (!agentInteractiveSessionStatusMatchesRef(ref, status)) {
|
|
120
|
+
throw new Error("the Sandbox reported a different interactive agent session identity");
|
|
121
|
+
}
|
|
122
|
+
return status;
|
|
123
123
|
};
|
|
124
124
|
const validateControl = async (control, options) => {
|
|
125
|
-
|
|
125
|
+
assertActiveControl(ref, control);
|
|
126
126
|
options?.signal?.throwIfAborted();
|
|
127
127
|
try {
|
|
128
|
-
await
|
|
128
|
+
await mutationHandle(control).validateControl(control, options);
|
|
129
129
|
}
|
|
130
130
|
catch (error) {
|
|
131
131
|
options?.signal?.throwIfAborted();
|
|
@@ -142,7 +142,7 @@ export function createTangleInteractiveAgentRegistry(box, providerName, environm
|
|
|
142
142
|
options?.signal?.throwIfAborted();
|
|
143
143
|
let raw;
|
|
144
144
|
try {
|
|
145
|
-
raw = await handle.claimControl(exactRequest);
|
|
145
|
+
raw = await handle.claimControl(exactRequest, options);
|
|
146
146
|
}
|
|
147
147
|
catch (error) {
|
|
148
148
|
options?.signal?.throwIfAborted();
|
|
@@ -160,59 +160,33 @@ export function createTangleInteractiveAgentRegistry(box, providerName, environm
|
|
|
160
160
|
assertOptionKeys(options, ["signal"], "Tangle interactive agent attach");
|
|
161
161
|
const exactRequest = AgentInteractiveSessionAttachSchema.parse(request ?? {});
|
|
162
162
|
await validateControl(exactRequest.control, options);
|
|
163
|
-
|
|
164
|
-
if (before.state !== "running") {
|
|
165
|
-
throw new Error("the exact interactive agent session is not running");
|
|
166
|
-
}
|
|
167
|
-
const terminals = box.terminals;
|
|
168
|
-
if (terminals === undefined || typeof terminals.get !== "function") {
|
|
169
|
-
throw new Error("the Sandbox client cannot read exact interactive terminal metadata");
|
|
170
|
-
}
|
|
171
|
-
const capture = createTangleTerminalStreamCapture(exactRequest);
|
|
172
|
-
let stream;
|
|
163
|
+
let terminal;
|
|
173
164
|
try {
|
|
174
|
-
|
|
175
|
-
control: exactRequest.control,
|
|
176
|
-
...(exactRequest.cols === undefined ? {} : { cols: exactRequest.cols }),
|
|
177
|
-
...(exactRequest.rows === undefined ? {} : { rows: exactRequest.rows }),
|
|
178
|
-
handlers: capture.handlers,
|
|
179
|
-
});
|
|
165
|
+
terminal = await mutationHandle(exactRequest.control).attachAgentTerminal(exactRequest, options);
|
|
180
166
|
}
|
|
181
167
|
catch (error) {
|
|
182
168
|
options?.signal?.throwIfAborted();
|
|
183
169
|
throw new Error(transportFailureReason("interactive attach", error));
|
|
184
170
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return Promise.resolve();
|
|
199
|
-
return validateControl(exactRequest.control, options);
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
if (prepared.status === "unknown") {
|
|
203
|
-
throw new Error(prepared.message);
|
|
204
|
-
}
|
|
205
|
-
if (prepared.acknowledgement.restored !== true) {
|
|
206
|
-
await prepared.session.detach();
|
|
207
|
-
throw new Error("the exact interactive attach did not restore the coding-agent PTY");
|
|
171
|
+
try {
|
|
172
|
+
options?.signal?.throwIfAborted();
|
|
173
|
+
const returnedControl = AgentInteractiveSessionControlClaimSchema.parse(terminal.control);
|
|
174
|
+
const returnedRef = TerminalSessionRefSchema.parse(terminal.ref);
|
|
175
|
+
TerminalReplayWindowSchema.parse(terminal.cursors);
|
|
176
|
+
if (canonicalCandidateDigest(returnedControl) !==
|
|
177
|
+
canonicalCandidateDigest(exactRequest.control) ||
|
|
178
|
+
returnedRef.terminalSessionId !== ref.run.sessionId ||
|
|
179
|
+
returnedRef.parentExecutionId !== ref.run.executionId ||
|
|
180
|
+
returnedRef.expiresAt !== exactRequest.control.expiresAt ||
|
|
181
|
+
!returnedRef.isRunning) {
|
|
182
|
+
throw new Error("the Sandbox returned a terminal with a different interactive identity");
|
|
183
|
+
}
|
|
208
184
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
throw new Error("the exact interactive agent exited while its terminal attached");
|
|
185
|
+
catch (error) {
|
|
186
|
+
await terminal.detach().catch(() => undefined);
|
|
187
|
+
throw error;
|
|
213
188
|
}
|
|
214
|
-
|
|
215
|
-
return terminal;
|
|
189
|
+
return validatedTerminal(terminal, exactRequest.control, validateControl, ref);
|
|
216
190
|
},
|
|
217
191
|
async sendPrompt(command, options) {
|
|
218
192
|
assertOptionKeys(options, ["signal"], "Tangle interactive agent prompt");
|
|
@@ -221,7 +195,7 @@ export function createTangleInteractiveAgentRegistry(box, providerName, environm
|
|
|
221
195
|
await validateControl(exactCommand.control, options);
|
|
222
196
|
let raw;
|
|
223
197
|
try {
|
|
224
|
-
raw = await
|
|
198
|
+
raw = await mutationHandle(exactCommand.control).sendPrompt(exactCommand, options);
|
|
225
199
|
}
|
|
226
200
|
catch (error) {
|
|
227
201
|
options?.signal?.throwIfAborted();
|
|
@@ -241,7 +215,7 @@ export function createTangleInteractiveAgentRegistry(box, providerName, environm
|
|
|
241
215
|
await validateControl(exactCommand.control, options);
|
|
242
216
|
let raw;
|
|
243
217
|
try {
|
|
244
|
-
raw = await
|
|
218
|
+
raw = await mutationHandle(exactCommand.control).stop(exactCommand, options);
|
|
245
219
|
}
|
|
246
220
|
catch (error) {
|
|
247
221
|
options?.signal?.throwIfAborted();
|
|
@@ -266,62 +240,43 @@ function isSandboxInteractiveSession(value) {
|
|
|
266
240
|
"start",
|
|
267
241
|
"claimControl",
|
|
268
242
|
"status",
|
|
269
|
-
"
|
|
243
|
+
"attachAgentTerminal",
|
|
270
244
|
"validateControl",
|
|
271
245
|
"sendPrompt",
|
|
272
246
|
"stop",
|
|
273
247
|
].every((key) => typeof candidate[key] === "function");
|
|
274
248
|
}
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
if (observed.state === "running") {
|
|
307
|
-
if (observed.streamUrl.length === 0) {
|
|
308
|
-
throw new Error("the Sandbox reported a running session without a stream");
|
|
309
|
-
}
|
|
310
|
-
return AgentInteractiveSessionStatusSchema.parse({ state: "running", ref });
|
|
311
|
-
}
|
|
312
|
-
return AgentInteractiveSessionStatusSchema.parse({
|
|
313
|
-
state: "exited",
|
|
314
|
-
ref,
|
|
315
|
-
endedAt: observed.endedAt,
|
|
316
|
-
reason: observed.reason,
|
|
317
|
-
...(Number.isSafeInteger(observed.exitCode)
|
|
318
|
-
? { exitCode: observed.exitCode }
|
|
319
|
-
: {}),
|
|
320
|
-
...(typeof observed.exitSignal === "string" && observed.exitSignal.length > 0
|
|
321
|
-
? { exitSignal: observed.exitSignal }
|
|
322
|
-
: {}),
|
|
323
|
-
});
|
|
249
|
+
function validatedTerminal(terminal, control, validate, expectedRef) {
|
|
250
|
+
const boundControl = Object.freeze({ ...control });
|
|
251
|
+
return {
|
|
252
|
+
get ref() {
|
|
253
|
+
const ref = TerminalSessionRefSchema.parse(terminal.ref);
|
|
254
|
+
if (ref.terminalSessionId !== expectedRef.run.sessionId ||
|
|
255
|
+
ref.parentExecutionId !== expectedRef.run.executionId) {
|
|
256
|
+
throw new Error("the Sandbox terminal changed its interactive session identity");
|
|
257
|
+
}
|
|
258
|
+
return ref;
|
|
259
|
+
},
|
|
260
|
+
get cursors() {
|
|
261
|
+
return TerminalReplayWindowSchema.parse(terminal.cursors);
|
|
262
|
+
},
|
|
263
|
+
control: boundControl,
|
|
264
|
+
async input(input, options) {
|
|
265
|
+
await validate(boundControl, options);
|
|
266
|
+
await terminal.input(input, options);
|
|
267
|
+
},
|
|
268
|
+
async resize(resize, options) {
|
|
269
|
+
await validate(boundControl, options);
|
|
270
|
+
await terminal.resize(resize, options);
|
|
271
|
+
},
|
|
272
|
+
detach: (options) => terminal.detach(options),
|
|
273
|
+
async close(options) {
|
|
274
|
+
await validate(boundControl, options);
|
|
275
|
+
return terminal.close(options);
|
|
276
|
+
},
|
|
277
|
+
events: (options) => terminal.events(options),
|
|
278
|
+
};
|
|
324
279
|
}
|
|
325
280
|
function sameExactSessionRef(expected, candidate) {
|
|
326
|
-
return canonicalCandidateDigest(expected) === canonicalCandidateDigest(candidate);
|
|
281
|
+
return (canonicalCandidateDigest(expected) === canonicalCandidateDigest(candidate));
|
|
327
282
|
}
|