@tangle-network/agent-provider-tangle 0.9.0 → 0.11.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/README.md +91 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/tangle-capabilities.d.ts +22 -1
- package/dist/tangle-capabilities.js +120 -4
- package/dist/tangle-create-options.js +3 -10
- package/dist/tangle-deployment-capabilities.d.ts +7 -0
- package/dist/tangle-deployment-capabilities.js +3 -0
- package/dist/tangle-environment-control.js +4 -0
- package/dist/tangle-environment-session.d.ts +9 -1
- package/dist/tangle-environment-session.js +36 -11
- package/dist/tangle-environment.d.ts +7 -1
- package/dist/tangle-environment.js +74 -3
- package/dist/tangle-events.d.ts +32 -2
- package/dist/tangle-events.js +138 -40
- package/dist/tangle-failure-reason.d.ts +13 -0
- package/dist/tangle-failure-reason.js +46 -0
- package/dist/tangle-interaction-response.d.ts +26 -0
- package/dist/tangle-interaction-response.js +169 -0
- package/dist/tangle-observation.d.ts +57 -0
- package/dist/tangle-observation.js +525 -0
- package/dist/tangle-prompt.js +3 -0
- package/dist/tangle-provider.js +3 -1
- package/dist/tangle-resources.d.ts +22 -0
- package/dist/tangle-resources.js +74 -0
- package/dist/tangle-terminal-frames.d.ts +44 -0
- package/dist/tangle-terminal-frames.js +137 -0
- package/dist/tangle-terminal.d.ts +12 -0
- package/dist/tangle-terminal.js +439 -0
- package/dist/tangle-types.d.ts +181 -1
- package/dist/tangle-usage-log.d.ts +22 -0
- package/dist/tangle-usage-log.js +22 -0
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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
|
|
4
|
+
The peer range is `>=0.23.0 <1.0.0`, and this package is developed and tested against 0.23.0.
|
|
5
|
+
The floor is 0.23.0 because the response path needs `session.respondToInteraction`, which first shipped there.
|
|
6
|
+
The adapter feature-detects that method, so an older SDK claims no interactions rather than failing to load.
|
|
7
|
+
The floor stands anyway: the earlier answer path resolves the session's first outstanding question rather than the one a response names, so a response meant for one ask resolves another and reports success.
|
|
8
|
+
This adapter never falls back to it.
|
|
6
9
|
|
|
7
10
|
```ts
|
|
8
11
|
import { Sandbox } from '@tangle-network/sandbox'
|
|
@@ -20,6 +23,8 @@ Sandbox metadata can include caller-authored values and does not authenticate it
|
|
|
20
23
|
Reconstruct an exact session with `environment.session(reference.id, { controlRef: reference.controlRef })`; replay cursors are exclusive at both the agent interface and Sandbox session stream.
|
|
21
24
|
Result, replay, and cancel operations select that exact execution instead of whichever execution most recently changed the shared session.
|
|
22
25
|
Session status with an exact control reference reports a state only when the payload names that execution; a payload bound to a different or unnamed execution reports `unknown`.
|
|
26
|
+
`AgentTurnInput.interactions` maps unchanged to `PromptOptions.backend.interactions` for the selected Sandbox turn.
|
|
27
|
+
The requested posture is part of the retained request digest, so a retry with changed interaction behavior conflicts instead of reusing prior work.
|
|
23
28
|
|
|
24
29
|
## Two capability documents
|
|
25
30
|
|
|
@@ -47,12 +52,40 @@ Each deployment flag this adapter reads gates the claims it backs, and no flag i
|
|
|
47
52
|
| `cancel.idempotent` | `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
48
53
|
| `runs.eventReplay` | `streaming.replay`, `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
49
54
|
| `runs.executionScopedStatus` | `sessions.continue`, `retainedControl`, `session.cancelRun` |
|
|
55
|
+
| `interactions.responseDedupe` | `interactions`, `environment.respondToInteraction`, `session.respondToInteraction` |
|
|
50
56
|
|
|
51
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.
|
|
52
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.
|
|
53
59
|
A claim takes its operation with it: `streaming.detach` gates `dispatch()`, and `session()` stands while any of `streaming.detach`, `streaming.replay`, or `sessions.continue` stands.
|
|
54
60
|
A missing flag means unknown, and unknown is never a claim.
|
|
55
61
|
|
|
62
|
+
Answering an ask rests on `interactions.responseDedupe` alone, and on nothing retained control needs.
|
|
63
|
+
The adapter keeps no record of the responses it sent, so every replay answer comes from the deployment: repeating a command returns the recorded acknowledgement, and a different answer for a recorded ask is refused as `already_resolved_different`.
|
|
64
|
+
A deployment that does not record what it acknowledges therefore claims no interactions, even where the local method exists, because an unrecorded response cannot be retried without risking a second answer to a running agent.
|
|
65
|
+
The claim and the two methods stand or fall together.
|
|
66
|
+
|
|
67
|
+
## Answering an interaction
|
|
68
|
+
|
|
69
|
+
`respondToInteraction` takes the canonical `InteractionResponseCommand` and returns the canonical `InteractionAcknowledgement`.
|
|
70
|
+
It is offered on the environment and on a session handle; the environment routes the command to the session its binding names.
|
|
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
|
+
|
|
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
|
+
| 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 | `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
|
+
|
|
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
|
+
|
|
56
89
|
Four inputs claim nothing at all: a Sandbox SDK older than 0.22.0, a sandbox that is not running, a `null` document (a deployment predating capability discovery, or one serving a newer schema this SDK cannot read), and a capability read that fails.
|
|
57
90
|
In each case the environment omits `dispatch` and `session`, so a caller never selects an action the deployment will reject.
|
|
58
91
|
A document that leaves a flag unset is not one of them.
|
|
@@ -75,6 +108,62 @@ It also rejects `contextTransfer` and `nativeContinuation` inputs explicitly unt
|
|
|
75
108
|
The adapter never advertises `branching.checkpoint` or `branching.fork`.
|
|
76
109
|
Sandbox exposes `snapshot`, `listSnapshots`, `deleteSnapshot`, and `branch(count)` with different semantics; durable workspace branching stays unadvertised until the full `AgentWorkspaceBranching` contract — retry, lookup, conflict, and cleanup together — is implemented over that surface.
|
|
77
110
|
|
|
111
|
+
## Environment observation
|
|
112
|
+
|
|
113
|
+
`environment.observe()` returns the normalized `AgentEnvironmentObservation`.
|
|
114
|
+
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.
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
| Surface | Sandbox source | State when the source is missing |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `identity` | provider name and `box.id` | always known |
|
|
120
|
+
| `lifecycle.status` | `box.status` after a refresh | `stale` with the refresh failure |
|
|
121
|
+
| `lifecycle.cleanup` | `box.expiresAt` as a scheduled retirement | omitted from the lifecycle value |
|
|
122
|
+
| `endpoint` | scheme, host, and explicit port of `connection.runtimeUrl` | `unavailable` |
|
|
123
|
+
| `placement.verified` | `client.describePlacement(box)` | `unavailable` |
|
|
124
|
+
| `resources.requested` | the `create()` resource request | omitted on an environment rebuilt by id |
|
|
125
|
+
| `resources.effective` | cgroup `memoryLimitMb` and the attached GPU lease | `unavailable` |
|
|
126
|
+
| `resourceUse.current` / `peak` | cgroup `memoryCurrentMb` / `memoryPeakMb` | `unavailable` |
|
|
127
|
+
| `modelUsage` | the newest execution this handle measured | `unavailable` |
|
|
128
|
+
| `computeBilling` | the GPU lease's billed or estimated customer cost | `unavailable` |
|
|
129
|
+
| `accountUsage` | `client.subscription()` and `client.usage()` | `unavailable` |
|
|
130
|
+
|
|
131
|
+
Four quantities have no Sandbox source at all, and the adapter reports them as absent rather than deriving them.
|
|
132
|
+
There is no effective CPU or disk figure anywhere in the SDK, so `resources.effective` carries memory and the accelerator only.
|
|
133
|
+
The cgroup reports cumulative CPU microseconds, which is not a utilization figure, so `resourceUse` carries memory only rather than a rate computed from one sample.
|
|
134
|
+
Sandbox prices an attached GPU lease and publishes no per-sandbox container compute cost, so `computeBilling` is unavailable on an environment with no lease.
|
|
135
|
+
`lifecycle.continuity` and `lifecycle.persistence` have no readable source and are omitted.
|
|
136
|
+
|
|
137
|
+
Two account values are reported as absent on purpose.
|
|
138
|
+
A negative credit balance under an overage plan cannot be stated as non-negative remaining credit, and an account with no concurrent-sandbox ceiling has no quota to state; reporting either as zero would hide a debt or invent a limit.
|
|
139
|
+
|
|
140
|
+
`resources.requested` is the request the adapter sends, not the request the caller typed.
|
|
141
|
+
The contract states CPU, memory, and disk as `cpu`/`memoryMb`/`diskMb` while Sandbox reads `cpuCores`/`memoryMB`/`diskGB`, so the adapter translates them; a disk size that is not a whole number of gibibytes is refused rather than rounded.
|
|
142
|
+
A `resources.gpu` class becomes one accelerator device, which is the Sandbox default device count for a request that names only its class.
|
|
143
|
+
|
|
144
|
+
The observation makes two calls of its own — the account subscription and usage counters — plus the cgroup sample and a refresh.
|
|
145
|
+
A hop that fails does not fail the observation: its surface carries the transport's own message as the reason, and a value the refresh could not renew is reported as `stale` rather than as current.
|
|
146
|
+
|
|
147
|
+
Nothing in the observation can carry a credential.
|
|
148
|
+
The endpoint holds only a scheme, a host, and an explicit port; userinfo, path, and query are dropped, and the bearer beside the runtime URL is never read.
|
|
149
|
+
|
|
150
|
+
## Interactive terminal
|
|
151
|
+
|
|
152
|
+
`environment.attachTerminal(request)` opens or reattaches one PTY over the sandbox terminal WebSocket, and `environment.terminal(id)` returns the live handle.
|
|
153
|
+
The four `interactiveTerminal` flags rest on one fact — the sandbox serves the PTY socket and reports terminal metadata — because all four operations reach the caller through one socket from one linked SDK.
|
|
154
|
+
|
|
155
|
+
Attach uses the request's `connectionId`, else its `terminalSessionId`, else a generated id; reusing an id reattaches the PTY and replays its retained screen, which the runtime reports as `reattached`.
|
|
156
|
+
`mode: "logical"` is refused: Sandbox resumes a terminal only by attaching it, and serving an attach the caller did not ask for would start a PTY behind their back.
|
|
157
|
+
|
|
158
|
+
`events()` replays retained frames from an EXCLUSIVE `since` cursor and then continues until the terminal exits.
|
|
159
|
+
Only `output` frames carry a sequence; `ready`, `resize`, `error`, and `exit` keep their place in the same ordered log.
|
|
160
|
+
A cursor the retained buffer no longer holds is refused, because resuming after the gap would drop output the consumer believes it received.
|
|
161
|
+
|
|
162
|
+
Every reference states an expiry one detach window past the newest activity the adapter can prove, and `input` and `resize` refuse a reference that is expired or no longer running.
|
|
163
|
+
`attachCount` counts the attaches this environment holds, not the runtime's own viewers.
|
|
164
|
+
`close()` reports `closed` only when the socket delivered an exit; Sandbox exposes no terminal delete, so an unproven close reports `unknown` rather than claiming a termination that did not happen.
|
|
165
|
+
An attach whose runtime metadata lacks a name, shell, working directory, geometry, timestamps, running state, or detach window fails closed with `unknown`, because a reference cannot state facts the runtime did not report.
|
|
166
|
+
|
|
78
167
|
Pass `exactProcess: {}` only when the Sandbox deployment supports `agent: false` creates and reports `metadata.runtimeMode: "control"`.
|
|
79
168
|
The optional capability creates an ephemeral sandbox with an authenticated control service but no managed agent workload or agent credentials, explicit resources, exact blocked/domain egress, bounded binary file reads, shell-free launch, and recoverable process output plus terminal reason.
|
|
80
169
|
Set `teamId` inside `exactProcess` to scope create, lookup, and recovery to one team.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export type { TangleExactProcessOptions } from "./tangle-types.js";
|
|
|
2
2
|
export * from "./tangle-types.js";
|
|
3
3
|
export { createTangleProvider } from "./tangle-provider.js";
|
|
4
4
|
export { defaultTangleSandboxCapabilities } from "./tangle-capabilities.js";
|
|
5
|
+
export { safeEndpointFromConnection } from "./tangle-observation.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AgentEnvironmentCapabilities, HarnessType } from "@tangle-network/agent-interface";
|
|
2
2
|
import type { SandboxClientLike, SandboxInstanceLike } from "./tangle-types.js";
|
|
3
3
|
import type { DeploymentCapabilitySupport } from "./tangle-deployment-capabilities.js";
|
|
4
|
+
import type { ResourceProfile } from "@tangle-network/agent-interface";
|
|
5
|
+
import { type ObservationSurfaceSupport } from "./tangle-observation.js";
|
|
4
6
|
/**
|
|
5
7
|
* The full capability document this adapter supports when the Sandbox client
|
|
6
8
|
* implements every optional method.
|
|
@@ -29,8 +31,14 @@ export interface SandboxCapabilitySupport {
|
|
|
29
31
|
placement: boolean;
|
|
30
32
|
destroy: boolean;
|
|
31
33
|
cancelRun: boolean;
|
|
34
|
+
/** The session handle exposes the digest-bound interaction command route. */
|
|
35
|
+
respondToInteraction: boolean;
|
|
36
|
+
/** Per-surface sources for the normalized observation. */
|
|
37
|
+
observation: ObservationSurfaceSupport;
|
|
38
|
+
/** The sandbox serves the PTY socket and reports terminal metadata. */
|
|
39
|
+
interactiveTerminal: boolean;
|
|
32
40
|
}
|
|
33
|
-
export declare function sandboxCapabilitySupport(box: SandboxInstanceLike, client: SandboxClientLike): SandboxCapabilitySupport;
|
|
41
|
+
export declare function sandboxCapabilitySupport(box: SandboxInstanceLike, client: SandboxClientLike, requestedResources?: ResourceProfile): SandboxCapabilitySupport;
|
|
34
42
|
/**
|
|
35
43
|
* Establish client-stage facts before any sandbox exists. Two sources: the
|
|
36
44
|
* client's own members (get, describePlacement) and, for an SDK-backed client,
|
|
@@ -53,6 +61,19 @@ export declare function clientCapabilitySupport(client: SandboxClientLike): Sand
|
|
|
53
61
|
* can call is not a run the service retains.
|
|
54
62
|
*/
|
|
55
63
|
export declare function tangleRetainedControlSupported(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Decide whether answering an interaction may be claimed.
|
|
66
|
+
*
|
|
67
|
+
* Two independent fact sets must agree, as they do for retained control. The
|
|
68
|
+
* adapter surface must be able to send the command: a session handle exposing
|
|
69
|
+
* the digest-bound route. The deployment must record what it acknowledges,
|
|
70
|
+
* because this adapter keeps no resolution record of its own — every replay
|
|
71
|
+
* answer comes from the deployment. A deployment that leaves the flag unset
|
|
72
|
+
* refuses the claim even though the local method exists, since an
|
|
73
|
+
* unrecorded response cannot be retried without risking a second answer to a
|
|
74
|
+
* running agent.
|
|
75
|
+
*/
|
|
76
|
+
export declare function tangleInteractionResponsesSupported(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport): boolean;
|
|
56
77
|
/**
|
|
57
78
|
* Narrow a declared capability document to established facts.
|
|
58
79
|
*
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { harnessSystemPromptIntents } from "@tangle-network/agent-interface";
|
|
2
2
|
import { SandboxInstance } from "@tangle-network/sandbox";
|
|
3
3
|
import { ADAPTER_CEILING_DEPLOYMENT, deploymentBacksRetainedControl, } from "./tangle-deployment-capabilities.js";
|
|
4
|
+
import { clientObservationSurfaceSupport, observationSurfaceSupport, } from "./tangle-observation.js";
|
|
5
|
+
import { sandboxBacksInteractiveTerminal } from "./tangle-terminal.js";
|
|
4
6
|
/**
|
|
5
7
|
* The full capability document this adapter supports when the Sandbox client
|
|
6
8
|
* implements every optional method.
|
|
@@ -42,6 +44,26 @@ export function defaultTangleSandboxCapabilities(harness) {
|
|
|
42
44
|
// together over one Sandbox surface, and the capability schema refuses
|
|
43
45
|
// a partial block, so they stand or fall on the same fact set.
|
|
44
46
|
sessions: { continue: true, list: false, messages: false },
|
|
47
|
+
// Answering an ask is declared as intent and stripped by narrowing unless
|
|
48
|
+
// the session exposes the command route and the deployment discloses its
|
|
49
|
+
// durable response record. Three claims are bounded by what the adapter
|
|
50
|
+
// can establish rather than by what the route carries:
|
|
51
|
+
// - `secretAnswers` false, and no `secret` answer field: the deployment
|
|
52
|
+
// discloses nothing about resolving a one-use secret handle, and an
|
|
53
|
+
// undisclosed fact is not a claim.
|
|
54
|
+
// - `responseScopes` names `interaction` alone, for the same reason: no
|
|
55
|
+
// document states that a broader grant is honored on reuse.
|
|
56
|
+
// - `concurrentRequests` true: the command names the ask it answers, so
|
|
57
|
+
// several outstanding asks each take their own exact response.
|
|
58
|
+
interactions: {
|
|
59
|
+
kinds: ["question", "permission", "plan"],
|
|
60
|
+
answerFieldTypes: ["text", "number", "boolean", "select"],
|
|
61
|
+
responseScopes: ["interaction"],
|
|
62
|
+
secretAnswers: false,
|
|
63
|
+
concurrentRequests: true,
|
|
64
|
+
replay: true,
|
|
65
|
+
responseIdempotency: true,
|
|
66
|
+
},
|
|
45
67
|
retainedControl: {
|
|
46
68
|
exactRunIdentity: true,
|
|
47
69
|
resultIdentity: true,
|
|
@@ -57,11 +79,32 @@ export function defaultTangleSandboxCapabilities(harness) {
|
|
|
57
79
|
// Confidential execution needs verified attestation evidence, which this
|
|
58
80
|
// adapter does not yet obtain, so it is never declared by default.
|
|
59
81
|
confidential: false,
|
|
82
|
+
// Observation surfaces are declared as intent and narrowed per sandbox to
|
|
83
|
+
// the sources that can put a value on each one.
|
|
84
|
+
observation: {
|
|
85
|
+
identity: true,
|
|
86
|
+
lifecycle: true,
|
|
87
|
+
endpoint: true,
|
|
88
|
+
placement: true,
|
|
89
|
+
resources: true,
|
|
90
|
+
resourceUse: true,
|
|
91
|
+
modelUsage: true,
|
|
92
|
+
computeBilling: true,
|
|
93
|
+
accountUsage: true,
|
|
94
|
+
},
|
|
95
|
+
// The four terminal operations rest on one fact: the sandbox serves the
|
|
96
|
+
// PTY socket and reports terminal metadata. They stand or fall together.
|
|
97
|
+
interactiveTerminal: {
|
|
98
|
+
attach: true,
|
|
99
|
+
input: true,
|
|
100
|
+
resize: true,
|
|
101
|
+
reattach: true,
|
|
102
|
+
},
|
|
60
103
|
};
|
|
61
104
|
}
|
|
62
105
|
// One reserved id names both probe handles; neither ever reaches the service.
|
|
63
106
|
const CAPABILITY_PROBE_ID = "__tangle-capability-probe__";
|
|
64
|
-
export function sandboxCapabilitySupport(box, client) {
|
|
107
|
+
export function sandboxCapabilitySupport(box, client, requestedResources) {
|
|
65
108
|
let session;
|
|
66
109
|
if (typeof box.session === "function") {
|
|
67
110
|
try {
|
|
@@ -83,6 +126,9 @@ export function sandboxCapabilitySupport(box, client) {
|
|
|
83
126
|
placement: typeof client.describePlacement === "function",
|
|
84
127
|
destroy: typeof box.delete === "function",
|
|
85
128
|
cancelRun: typeof session?.cancelRun === "function",
|
|
129
|
+
respondToInteraction: typeof session?.respondToInteraction === "function",
|
|
130
|
+
observation: observationSurfaceSupport(box, client, requestedResources),
|
|
131
|
+
interactiveTerminal: sandboxBacksInteractiveTerminal(box),
|
|
86
132
|
};
|
|
87
133
|
}
|
|
88
134
|
/**
|
|
@@ -121,9 +167,14 @@ function linkedSdkProbeInstance(client) {
|
|
|
121
167
|
* re-measures them in `capabilitiesForSandbox`.
|
|
122
168
|
*/
|
|
123
169
|
export function clientCapabilitySupport(client) {
|
|
170
|
+
const observation = clientObservationSurfaceSupport(client);
|
|
124
171
|
const probe = linkedSdkProbeInstance(client);
|
|
125
|
-
if (probe)
|
|
126
|
-
|
|
172
|
+
if (probe) {
|
|
173
|
+
// The probe measures the linked SDK's method surface, so it decides the
|
|
174
|
+
// terminal transport. It carries no sandbox data, so the observation
|
|
175
|
+
// surfaces that rest on one environment's values stay at the ceiling.
|
|
176
|
+
return { ...sandboxCapabilitySupport(probe, client), observation };
|
|
177
|
+
}
|
|
127
178
|
return {
|
|
128
179
|
reconstruct: typeof client.get === "function",
|
|
129
180
|
dispatchPrompt: true,
|
|
@@ -134,6 +185,9 @@ export function clientCapabilitySupport(client) {
|
|
|
134
185
|
placement: typeof client.describePlacement === "function",
|
|
135
186
|
destroy: true,
|
|
136
187
|
cancelRun: false,
|
|
188
|
+
respondToInteraction: false,
|
|
189
|
+
observation,
|
|
190
|
+
interactiveTerminal: true,
|
|
137
191
|
};
|
|
138
192
|
}
|
|
139
193
|
/**
|
|
@@ -158,6 +212,24 @@ export function tangleRetainedControlSupported(declared, support, deployment) {
|
|
|
158
212
|
support.session &&
|
|
159
213
|
support.cancelRun);
|
|
160
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Decide whether answering an interaction may be claimed.
|
|
217
|
+
*
|
|
218
|
+
* Two independent fact sets must agree, as they do for retained control. The
|
|
219
|
+
* adapter surface must be able to send the command: a session handle exposing
|
|
220
|
+
* the digest-bound route. The deployment must record what it acknowledges,
|
|
221
|
+
* because this adapter keeps no resolution record of its own — every replay
|
|
222
|
+
* answer comes from the deployment. A deployment that leaves the flag unset
|
|
223
|
+
* refuses the claim even though the local method exists, since an
|
|
224
|
+
* unrecorded response cannot be retried without risking a second answer to a
|
|
225
|
+
* running agent.
|
|
226
|
+
*/
|
|
227
|
+
export function tangleInteractionResponsesSupported(declared, support, deployment) {
|
|
228
|
+
return (declared.interactions !== undefined &&
|
|
229
|
+
deployment.interactionResponses &&
|
|
230
|
+
support.session &&
|
|
231
|
+
support.respondToInteraction);
|
|
232
|
+
}
|
|
161
233
|
/**
|
|
162
234
|
* Narrow a declared capability document to established facts.
|
|
163
235
|
*
|
|
@@ -214,13 +286,57 @@ export function narrowedTangleCapabilities(declared, support, deployment) {
|
|
|
214
286
|
},
|
|
215
287
|
placement: support.placement ? declared.placement : false,
|
|
216
288
|
usage: false,
|
|
289
|
+
...(declared.observation === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: { observation: narrowedObservation(declared.observation, support.observation) }),
|
|
292
|
+
...(declared.interactiveTerminal === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: {
|
|
295
|
+
interactiveTerminal: narrowedInteractiveTerminal(declared.interactiveTerminal, support.interactiveTerminal),
|
|
296
|
+
}),
|
|
217
297
|
};
|
|
218
|
-
|
|
298
|
+
// A claimed block is passed through whole. Its sub-flags state what the
|
|
299
|
+
// route carries, not what one deployment reports, and the deployment's own
|
|
300
|
+
// fact has already decided whether the block survives at all.
|
|
301
|
+
if (!tangleInteractionResponsesSupported(declared, support, deployment)) {
|
|
302
|
+
delete narrowed.interactions;
|
|
303
|
+
}
|
|
219
304
|
delete narrowed.nativeContinuation;
|
|
220
305
|
if (!supportsRetainedControl)
|
|
221
306
|
delete narrowed.retainedControl;
|
|
222
307
|
return narrowed;
|
|
223
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Clear every observation surface no source backs. The flag states whether a
|
|
311
|
+
* value can be produced; the observation itself always carries the surface
|
|
312
|
+
* with its freshness state, so a cleared flag never turns into a missing key.
|
|
313
|
+
*/
|
|
314
|
+
function narrowedObservation(declared, support) {
|
|
315
|
+
return {
|
|
316
|
+
identity: support.identity ? declared.identity : false,
|
|
317
|
+
lifecycle: support.lifecycle ? declared.lifecycle : false,
|
|
318
|
+
endpoint: support.endpoint ? declared.endpoint : false,
|
|
319
|
+
placement: support.placement ? declared.placement : false,
|
|
320
|
+
resources: support.resources ? declared.resources : false,
|
|
321
|
+
resourceUse: support.resourceUse ? declared.resourceUse : false,
|
|
322
|
+
modelUsage: support.modelUsage ? declared.modelUsage : false,
|
|
323
|
+
computeBilling: support.computeBilling ? declared.computeBilling : false,
|
|
324
|
+
accountUsage: support.accountUsage ? declared.accountUsage : false,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The four terminal operations reach the caller through one PTY socket from
|
|
329
|
+
* one linked SDK, so one fact decides them all: a sandbox that cannot serve
|
|
330
|
+
* the socket claims none of them.
|
|
331
|
+
*/
|
|
332
|
+
function narrowedInteractiveTerminal(declared, supported) {
|
|
333
|
+
return {
|
|
334
|
+
attach: supported ? declared.attach : false,
|
|
335
|
+
input: supported ? declared.input : false,
|
|
336
|
+
resize: supported ? declared.resize : false,
|
|
337
|
+
reattach: supported ? declared.reattach : false,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
224
340
|
/**
|
|
225
341
|
* Narrow provider-level claims to facts the client can prove before any
|
|
226
342
|
* sandbox exists.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertBoundedJson, boundedIdentifier, boundedString, MAX_ARRAY_LENGTH, MAX_MAP_ENTRIES, } from "./tangle-contract-safety.js";
|
|
2
|
+
import { sandboxResourcesFromResourceRequest } from "./tangle-resources.js";
|
|
2
3
|
export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
3
4
|
assertCreateInputShape(input);
|
|
4
5
|
assertNoInlineSecretValues(input);
|
|
@@ -40,8 +41,6 @@ export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
|
40
41
|
}
|
|
41
42
|
if (input.resources?.providerOptions)
|
|
42
43
|
assertBoundedRecord(input.resources.providerOptions, "Tangle resource providerOptions");
|
|
43
|
-
if (input.resources?.gpu !== undefined)
|
|
44
|
-
boundedIdentifier(input.resources.gpu, "Tangle GPU");
|
|
45
44
|
if (input.idempotencyKey !== undefined) {
|
|
46
45
|
boundedIdentifier(input.idempotencyKey, "Tangle idempotency key");
|
|
47
46
|
}
|
|
@@ -51,13 +50,7 @@ export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
|
51
50
|
throw new Error("Tangle workspace image cannot be empty");
|
|
52
51
|
if (workspace.repoUrl === "")
|
|
53
52
|
throw new Error("Tangle repository URL cannot be empty");
|
|
54
|
-
|
|
55
|
-
if (name === "providerOptions" || name === "gpu")
|
|
56
|
-
continue;
|
|
57
|
-
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
|
|
58
|
-
throw new Error(`Tangle resource ${name} must be a positive safe integer`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
53
|
+
const resources = sandboxResourcesFromResourceRequest(input.resources);
|
|
61
54
|
if (workspace.environment !== undefined && workspace.image !== undefined) {
|
|
62
55
|
throw new Error("Tangle workspace cannot specify both environment and image");
|
|
63
56
|
}
|
|
@@ -77,7 +70,7 @@ export function sandboxOptionsFromCreateInput(input, defaultBackend) {
|
|
|
77
70
|
},
|
|
78
71
|
}
|
|
79
72
|
: {}),
|
|
80
|
-
...(
|
|
73
|
+
...(resources ? { resources } : {}),
|
|
81
74
|
...(input.env ? { env: input.env } : {}),
|
|
82
75
|
...(Array.isArray(input.secrets) ? { secrets: input.secrets } : {}),
|
|
83
76
|
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
@@ -21,6 +21,13 @@ export interface DeploymentCapabilitySupport {
|
|
|
21
21
|
readonly eventReplay: boolean;
|
|
22
22
|
/** Status and results select one execution, not the session's latest. */
|
|
23
23
|
readonly executionScopedStatus: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Interaction responses are recorded durably, so repeating one replays the
|
|
26
|
+
* recorded acknowledgement instead of delivering a second answer to a
|
|
27
|
+
* running agent. The adapter holds no record of its own, so this fact alone
|
|
28
|
+
* decides whether a response can be retried safely.
|
|
29
|
+
*/
|
|
30
|
+
readonly interactionResponses: boolean;
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
33
|
* The deployment backs nothing.
|
|
@@ -13,6 +13,7 @@ export const UNPROVEN_DEPLOYMENT = {
|
|
|
13
13
|
canonicalCancellation: false,
|
|
14
14
|
eventReplay: false,
|
|
15
15
|
executionScopedStatus: false,
|
|
16
|
+
interactionResponses: false,
|
|
16
17
|
};
|
|
17
18
|
/**
|
|
18
19
|
* The client stage's deployment input: this adapter's ceiling, not a fact.
|
|
@@ -34,6 +35,7 @@ export const ADAPTER_CEILING_DEPLOYMENT = {
|
|
|
34
35
|
canonicalCancellation: true,
|
|
35
36
|
eventReplay: true,
|
|
36
37
|
executionScopedStatus: true,
|
|
38
|
+
interactionResponses: true,
|
|
37
39
|
};
|
|
38
40
|
/**
|
|
39
41
|
* Read the deployment facts out of a capability document. Every flag this
|
|
@@ -50,6 +52,7 @@ export function deploymentCapabilitySupport(document) {
|
|
|
50
52
|
document.cancel?.idempotent === true,
|
|
51
53
|
eventReplay: document.runs?.eventReplay === true,
|
|
52
54
|
executionScopedStatus: document.runs?.executionScopedStatus === true,
|
|
55
|
+
interactionResponses: document.interactions?.responseDedupe === true,
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
/**
|
|
@@ -15,6 +15,9 @@ export function sessionPromptRequestDigest(input, provider, environmentId, sessi
|
|
|
15
15
|
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
16
16
|
...(input.detach === undefined ? {} : { detach: input.detach }),
|
|
17
17
|
...(input.context === undefined ? {} : { context: input.context }),
|
|
18
|
+
...(input.interactions === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { interactions: input.interactions }),
|
|
18
21
|
});
|
|
19
22
|
}
|
|
20
23
|
export function hasReplayPayload(input) {
|
|
@@ -25,6 +28,7 @@ export function hasReplayPayload(input) {
|
|
|
25
28
|
input.turnId !== undefined ||
|
|
26
29
|
input.detach !== undefined ||
|
|
27
30
|
input.context !== undefined ||
|
|
31
|
+
input.interactions !== undefined ||
|
|
28
32
|
input.providerOptions !== undefined);
|
|
29
33
|
}
|
|
30
34
|
export async function interruptAfterAbort(box, reference) {
|
|
@@ -2,6 +2,7 @@ import type { AgentSession, AgentSessionRef, AgentTurnInput } from "@tangle-netw
|
|
|
2
2
|
import type { AgentExactRunControlRef, AgentRunControlRef } from "@tangle-network/agent-interface";
|
|
3
3
|
import type { SandboxEvent } from "@tangle-network/sandbox";
|
|
4
4
|
import type { SandboxSessionLike } from "./tangle-types.js";
|
|
5
|
+
import type { ExecutionUsageLog } from "./tangle-usage-log.js";
|
|
5
6
|
type ExactExecutionEventStream = (options: {
|
|
6
7
|
sessionId: string;
|
|
7
8
|
executionId: string;
|
|
@@ -14,6 +15,13 @@ type ExactExecutionEventStream = (options: {
|
|
|
14
15
|
* document grants retained control. Canonical cancellation is offered only
|
|
15
16
|
* under that grant: a `cancelRun` method the deployment does not honor is an
|
|
16
17
|
* action the caller selects and finds rejected on the wire.
|
|
18
|
+
* @param interactionResponses Whether the environment's narrowed capability
|
|
19
|
+
* document claims interaction responses. The method is offered only under
|
|
20
|
+
* that claim, so a caller never selects an answer the deployment cannot
|
|
21
|
+
* record.
|
|
22
|
+
* @param usageLog Sink for the token usage each execution measured. The
|
|
23
|
+
* environment observation reads it, because Sandbox reports usage per
|
|
24
|
+
* execution result and never for the environment.
|
|
17
25
|
*/
|
|
18
|
-
export declare function sandboxSessionAsAgentSession(session: SandboxSessionLike, controlRef: AgentRunControlRef | undefined, provider: string, environmentId: string, dispatch: ((input: AgentTurnInput) => Promise<AgentSessionRef>) | undefined, exactExecutionEvents: ExactExecutionEventStream | undefined, retainedControl: boolean): AgentSession;
|
|
26
|
+
export declare function sandboxSessionAsAgentSession(session: SandboxSessionLike, controlRef: AgentRunControlRef | undefined, provider: string, environmentId: string, dispatch: ((input: AgentTurnInput) => Promise<AgentSessionRef>) | undefined, exactExecutionEvents: ExactExecutionEventStream | undefined, retainedControl: boolean, interactionResponses: boolean, usageLog?: ExecutionUsageLog): AgentSession;
|
|
19
27
|
export {};
|
|
@@ -1,24 +1,44 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { AgentTurnInputSchema } from "@tangle-network/agent-interface";
|
|
3
3
|
import { AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, agentRunCancellationAcknowledgementMatchesRequest, } from "@tangle-network/agent-interface";
|
|
4
|
-
import { environmentEventFromSandboxEvent, isSandboxConnectionMarker, sandboxEventIdentity, } from "./tangle-events.js";
|
|
4
|
+
import { carriedSessionIds, environmentEventFromSandboxEvent, isSandboxConnectionMarker, sandboxEventIdentity, } from "./tangle-events.js";
|
|
5
5
|
import { agentTurnResultFromPromptRecord, promptFromTurnInput, promptOptionsFromTurnInput, validatedSandboxPromptResult, } from "./tangle-prompt.js";
|
|
6
6
|
import { retainedSessionControlRef, resolveRetainedSessionControlRef, sameRunControlRef, sessionPromptExecutionId, } from "./tangle-session-control.js";
|
|
7
7
|
import { executionBoundSessionStatus, sessionStatusFromUnknown, } from "./tangle-environment-values.js";
|
|
8
8
|
import { awaitWithSignal, boundedIdentifier, } from "./tangle-contract-safety.js";
|
|
9
9
|
import { assertOptionKeys } from "./tangle-environment-validation.js";
|
|
10
|
+
import { tangleInteractionResponder } from "./tangle-interaction-response.js";
|
|
10
11
|
import { hasReplayPayload, interruptExecutionAfterAbort, sessionPromptRequestDigest, } from "./tangle-environment-control.js";
|
|
11
12
|
/**
|
|
12
13
|
* @param retainedControl Whether the environment's narrowed capability
|
|
13
14
|
* document grants retained control. Canonical cancellation is offered only
|
|
14
15
|
* under that grant: a `cancelRun` method the deployment does not honor is an
|
|
15
16
|
* action the caller selects and finds rejected on the wire.
|
|
17
|
+
* @param interactionResponses Whether the environment's narrowed capability
|
|
18
|
+
* document claims interaction responses. The method is offered only under
|
|
19
|
+
* that claim, so a caller never selects an answer the deployment cannot
|
|
20
|
+
* record.
|
|
21
|
+
* @param usageLog Sink for the token usage each execution measured. The
|
|
22
|
+
* environment observation reads it, because Sandbox reports usage per
|
|
23
|
+
* execution result and never for the environment.
|
|
16
24
|
*/
|
|
17
|
-
export function sandboxSessionAsAgentSession(session, controlRef, provider, environmentId, dispatch, exactExecutionEvents, retainedControl) {
|
|
25
|
+
export function sandboxSessionAsAgentSession(session, controlRef, provider, environmentId, dispatch, exactExecutionEvents, retainedControl, interactionResponses, usageLog) {
|
|
26
|
+
const measured = (executionId, result) => {
|
|
27
|
+
usageLog?.record(executionId, result.usage);
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
18
30
|
let activeControlRef = controlRef
|
|
19
31
|
? resolveRetainedSessionControlRef(controlRef, session.id, provider, environmentId)
|
|
20
32
|
: undefined;
|
|
21
33
|
let promptInFlight = false;
|
|
34
|
+
const respondToInteraction = interactionResponses && typeof session.respondToInteraction === "function"
|
|
35
|
+
? tangleInteractionResponder({
|
|
36
|
+
session,
|
|
37
|
+
sessionId: session.id,
|
|
38
|
+
provider,
|
|
39
|
+
environmentId,
|
|
40
|
+
})
|
|
41
|
+
: undefined;
|
|
22
42
|
const cancelRunMethod = retainedControl ? session.cancelRun : undefined;
|
|
23
43
|
const cancelRun = typeof cancelRunMethod === "function"
|
|
24
44
|
? async (request, options) => {
|
|
@@ -106,9 +126,13 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
|
|
|
106
126
|
markerIdentity.executionId !== executionId) {
|
|
107
127
|
throw new Error("Tangle exact session connection identified a different executionId");
|
|
108
128
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
// Every position that names a session on the marker names the
|
|
130
|
+
// session the stream was opened for. The marker carries no native
|
|
131
|
+
// id, so no position is exempt.
|
|
132
|
+
for (const carried of carriedSessionIds(markerIdentity)) {
|
|
133
|
+
if (carried !== session.id) {
|
|
134
|
+
throw new Error("Tangle exact session connection identified a different sessionId");
|
|
135
|
+
}
|
|
112
136
|
}
|
|
113
137
|
continue;
|
|
114
138
|
}
|
|
@@ -143,10 +167,10 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
|
|
|
143
167
|
const resultRecord = validatedSandboxPromptResult(result);
|
|
144
168
|
if (resultRecord.executionId !== expectedExecutionId)
|
|
145
169
|
throw new Error("Tangle session result did not confirm its exact executionId");
|
|
146
|
-
return agentTurnResultFromPromptRecord(resultRecord, {
|
|
170
|
+
return measured(expectedExecutionId, agentTurnResultFromPromptRecord(resultRecord, {
|
|
147
171
|
sessionId: session.id,
|
|
148
172
|
controlRef: activeControlRef,
|
|
149
|
-
});
|
|
173
|
+
}));
|
|
150
174
|
},
|
|
151
175
|
async prompt(input) {
|
|
152
176
|
AgentTurnInputSchema.parse(input);
|
|
@@ -199,10 +223,10 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
|
|
|
199
223
|
if (resultRecord.executionId !== nextControlRef.executionId) {
|
|
200
224
|
throw new Error("Tangle detached session prompt did not confirm its exact executionId");
|
|
201
225
|
}
|
|
202
|
-
return agentTurnResultFromPromptRecord(resultRecord, {
|
|
226
|
+
return measured(nextControlRef.executionId, agentTurnResultFromPromptRecord(resultRecord, {
|
|
203
227
|
sessionId: session.id,
|
|
204
228
|
controlRef: nextControlRef,
|
|
205
|
-
});
|
|
229
|
+
}));
|
|
206
230
|
}
|
|
207
231
|
const sourceControlRef = requestedControlRef ?? activeControlRef;
|
|
208
232
|
const replay = input.lastEventId !== undefined;
|
|
@@ -277,7 +301,7 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
|
|
|
277
301
|
}
|
|
278
302
|
const nextControlRef = targetControlRef;
|
|
279
303
|
activeControlRef = nextControlRef;
|
|
280
|
-
return agentTurnResultFromPromptRecord(resultRecord, {
|
|
304
|
+
return measured(executionId, agentTurnResultFromPromptRecord(resultRecord, {
|
|
281
305
|
sessionId: session.id,
|
|
282
306
|
controlRef: nextControlRef,
|
|
283
307
|
...(input.contextTransfer
|
|
@@ -286,7 +310,7 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
|
|
|
286
310
|
...(input.contextTransfer
|
|
287
311
|
? { contextTransferRequested: true }
|
|
288
312
|
: {}),
|
|
289
|
-
});
|
|
313
|
+
}));
|
|
290
314
|
}
|
|
291
315
|
catch (error) {
|
|
292
316
|
if (input.signal?.aborted && input.detach !== true) {
|
|
@@ -311,5 +335,6 @@ export function sandboxSessionAsAgentSession(session, controlRef, provider, envi
|
|
|
311
335
|
throw new Error("Tangle sandbox did not confirm cancellation");
|
|
312
336
|
},
|
|
313
337
|
...(cancelRun ? { cancelRun } : {}),
|
|
338
|
+
...(respondToInteraction ? { respondToInteraction } : {}),
|
|
314
339
|
};
|
|
315
340
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentEnvironment, AgentEnvironmentCapabilities } from "@tangle-network/agent-interface/environment-provider";
|
|
1
|
+
import type { AgentEnvironment, AgentEnvironmentCapabilities, ResourceProfile } from "@tangle-network/agent-interface/environment-provider";
|
|
2
2
|
import type { SandboxClientLike, SandboxInstanceLike } from "./tangle-types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Compose one concrete sandbox into an environment.
|
|
@@ -16,7 +16,13 @@ import type { SandboxClientLike, SandboxInstanceLike } from "./tangle-types.js";
|
|
|
16
16
|
* nothing and keeps claiming nothing: the exposed operations and the document
|
|
17
17
|
* are composed together and a caller may already hold either one. Compose the
|
|
18
18
|
* environment again through `provider.get(id)` once the sandbox is running.
|
|
19
|
+
*
|
|
20
|
+
* @param request What the create call asked for. An environment rebuilt by id
|
|
21
|
+
* carries none of it, so its observation reports the requested compute shape
|
|
22
|
+
* as absent instead of restating a request it never saw.
|
|
19
23
|
*/
|
|
20
24
|
export declare function sandboxInstanceAsEnvironment(box: SandboxInstanceLike, providerName: string, client: SandboxClientLike, declaredCapabilities: AgentEnvironmentCapabilities, operation?: {
|
|
21
25
|
signal?: AbortSignal;
|
|
26
|
+
}, request?: {
|
|
27
|
+
resources?: ResourceProfile;
|
|
22
28
|
}): Promise<AgentEnvironment>;
|