@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
|
@@ -12,6 +12,10 @@ import { assertExecOptions, assertOptionKeys, } from "./tangle-environment-valid
|
|
|
12
12
|
import { interruptExecutionAfterAbort, } from "./tangle-environment-control.js";
|
|
13
13
|
import { dispatchEnvironmentRun } from "./tangle-environment-dispatch.js";
|
|
14
14
|
import { sandboxSessionAsAgentSession } from "./tangle-environment-session.js";
|
|
15
|
+
import { tangleInteractionResponder } from "./tangle-interaction-response.js";
|
|
16
|
+
import { createExecutionUsageLog } from "./tangle-usage-log.js";
|
|
17
|
+
import { observeTangleEnvironment } from "./tangle-observation.js";
|
|
18
|
+
import { createTangleTerminalRegistry } from "./tangle-terminal.js";
|
|
15
19
|
/**
|
|
16
20
|
* Compose one concrete sandbox into an environment.
|
|
17
21
|
*
|
|
@@ -28,8 +32,12 @@ import { sandboxSessionAsAgentSession } from "./tangle-environment-session.js";
|
|
|
28
32
|
* nothing and keeps claiming nothing: the exposed operations and the document
|
|
29
33
|
* are composed together and a caller may already hold either one. Compose the
|
|
30
34
|
* environment again through `provider.get(id)` once the sandbox is running.
|
|
35
|
+
*
|
|
36
|
+
* @param request What the create call asked for. An environment rebuilt by id
|
|
37
|
+
* carries none of it, so its observation reports the requested compute shape
|
|
38
|
+
* as absent instead of restating a request it never saw.
|
|
31
39
|
*/
|
|
32
|
-
export async function sandboxInstanceAsEnvironment(box, providerName, client, declaredCapabilities, operation) {
|
|
40
|
+
export async function sandboxInstanceAsEnvironment(box, providerName, client, declaredCapabilities, operation, request) {
|
|
33
41
|
const environmentId = boundedIdentifier(box.id, "Tangle environment id");
|
|
34
42
|
boundedIdentifier(providerName, "Tangle provider name");
|
|
35
43
|
if (box.metadata !== undefined) {
|
|
@@ -38,12 +46,19 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
38
46
|
}
|
|
39
47
|
assertBoundedJson(box.metadata);
|
|
40
48
|
}
|
|
41
|
-
const support = sandboxCapabilitySupport(box, client);
|
|
49
|
+
const support = sandboxCapabilitySupport(box, client, request?.resources);
|
|
42
50
|
const deployment = await readDeploymentCapabilitySupport(box, operation);
|
|
43
51
|
const capabilities = frozenCapabilityDocument(AgentEnvironmentCapabilitiesSchema.parse(capabilitiesForSandbox(declaredCapabilities, support, deployment)));
|
|
44
52
|
// The published document is the single source for what this environment
|
|
45
53
|
// offers, so the session surface reads its grant from there.
|
|
46
54
|
const retainedControl = capabilities.retainedControl !== undefined;
|
|
55
|
+
const interactionResponses = capabilities.interactions !== undefined;
|
|
56
|
+
// Usage is measured per execution, so the log collects what runs through
|
|
57
|
+
// this handle and the observation reports the newest record it holds.
|
|
58
|
+
const usageLog = createExecutionUsageLog();
|
|
59
|
+
const terminals = capabilities.interactiveTerminal?.attach === true
|
|
60
|
+
? createTangleTerminalRegistry(box)
|
|
61
|
+
: undefined;
|
|
47
62
|
const dispatch = capabilities.streaming.detach && box.dispatchPrompt
|
|
48
63
|
? dispatchEnvironmentRun(box, providerName, environmentId)
|
|
49
64
|
: undefined;
|
|
@@ -92,6 +107,7 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
92
107
|
? { streamBound: true }
|
|
93
108
|
: {}),
|
|
94
109
|
});
|
|
110
|
+
usageLog.record(expectedExecutionId, converted.usage);
|
|
95
111
|
input.signal?.throwIfAborted();
|
|
96
112
|
yield converted;
|
|
97
113
|
}
|
|
@@ -128,7 +144,7 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
128
144
|
if (session.id !== id) {
|
|
129
145
|
throw new Error("sandbox session(id) returned an unrelated session");
|
|
130
146
|
}
|
|
131
|
-
const agentSession = sandboxSessionAsAgentSession(session, resolveRetainedSessionControlRef(options?.controlRef, id, providerName, environmentId), providerName, environmentId, dispatch, exactExecutionEvents, retainedControl);
|
|
147
|
+
const agentSession = sandboxSessionAsAgentSession(session, resolveRetainedSessionControlRef(options?.controlRef, id, providerName, environmentId), providerName, environmentId, dispatch, exactExecutionEvents, retainedControl, interactionResponses, usageLog);
|
|
132
148
|
// sessions.continue was granted from the probe session and the
|
|
133
149
|
// deployment document together; this backstop holds every
|
|
134
150
|
// concrete session to that grant, so a client whose sessions
|
|
@@ -142,6 +158,31 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
142
158
|
},
|
|
143
159
|
}
|
|
144
160
|
: {}),
|
|
161
|
+
...(interactionResponses && box.session
|
|
162
|
+
? {
|
|
163
|
+
respondToInteraction(command, options) {
|
|
164
|
+
assertOptionKeys(options, ["signal"], "Tangle interaction response");
|
|
165
|
+
options?.signal?.throwIfAborted();
|
|
166
|
+
// The command names its own session, so the environment answers an
|
|
167
|
+
// ask on any session of this sandbox without the caller holding a
|
|
168
|
+
// session object. The binding is checked against this handle, so a
|
|
169
|
+
// command for another session is refused rather than delivered.
|
|
170
|
+
const sessionId = boundedIdentifier(command?.binding?.sessionId, "Tangle interaction session id");
|
|
171
|
+
const session = box.session?.(sessionId, options?.signal ? { signal: options.signal } : undefined);
|
|
172
|
+
if (!session)
|
|
173
|
+
throw new Error("sandbox session(id) returned undefined");
|
|
174
|
+
if (session.id !== sessionId) {
|
|
175
|
+
throw new Error("sandbox session(id) returned an unrelated session");
|
|
176
|
+
}
|
|
177
|
+
return tangleInteractionResponder({
|
|
178
|
+
session,
|
|
179
|
+
sessionId,
|
|
180
|
+
provider: providerName,
|
|
181
|
+
environmentId,
|
|
182
|
+
})(command, options);
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
145
186
|
...(capabilities.workspace.read && box.read
|
|
146
187
|
? {
|
|
147
188
|
async read(path, options) {
|
|
@@ -193,6 +234,36 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
193
234
|
},
|
|
194
235
|
}
|
|
195
236
|
: {}),
|
|
237
|
+
...(capabilities.observation
|
|
238
|
+
? {
|
|
239
|
+
async observe(options) {
|
|
240
|
+
assertOptionKeys(options, ["signal"], "Tangle observe");
|
|
241
|
+
return await observeTangleEnvironment({
|
|
242
|
+
box,
|
|
243
|
+
client,
|
|
244
|
+
provider: providerName,
|
|
245
|
+
environmentId,
|
|
246
|
+
...(request?.resources === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { requestedResources: request.resources }),
|
|
249
|
+
usageLog,
|
|
250
|
+
}, options);
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
254
|
+
...(terminals
|
|
255
|
+
? {
|
|
256
|
+
async attachTerminal(terminalRequest, options) {
|
|
257
|
+
return await terminals.attach(terminalRequest, options);
|
|
258
|
+
},
|
|
259
|
+
terminal(terminalSessionId, options) {
|
|
260
|
+
boundedIdentifier(terminalSessionId, "Tangle terminal session id");
|
|
261
|
+
assertOptionKeys(options, ["signal"], "Tangle terminal");
|
|
262
|
+
options?.signal?.throwIfAborted();
|
|
263
|
+
return terminals.get(terminalSessionId);
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
: {}),
|
|
196
267
|
async refresh(options) {
|
|
197
268
|
assertOptionKeys(options, ["signal"], "Tangle refresh");
|
|
198
269
|
options?.signal?.throwIfAborted();
|
package/dist/tangle-events.d.ts
CHANGED
|
@@ -2,11 +2,41 @@ import type { SandboxEvent } from "@tangle-network/sandbox";
|
|
|
2
2
|
import type { AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider";
|
|
3
3
|
/** The sidecar sends this envelope when an SSE connection is ready. */
|
|
4
4
|
export declare function isSandboxConnectionMarker(event: SandboxEvent): boolean;
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The session ids a Sandbox event carries, kept separate by field position.
|
|
7
|
+
*
|
|
8
|
+
* `runFrameSessionId` is `data.sessionId`/`data.sessionID`. The run/stream lane
|
|
9
|
+
* copies that value straight from the backend adapter, so on `session.updated`
|
|
10
|
+
* it is the harness-native session id (Claude, Codex, OpenCode) rather than the
|
|
11
|
+
* runtime session id.
|
|
12
|
+
*
|
|
13
|
+
* `envelopeSessionId` is the `properties` and `properties.info` position of an
|
|
14
|
+
* /agents/events frame, plus `properties.part` on a frame type that publishes a
|
|
15
|
+
* part. The sidecar rewrites the backend's own ids to the runtime ids before it
|
|
16
|
+
* publishes there, so that position names the runtime session on every frame
|
|
17
|
+
* type it shapes.
|
|
18
|
+
*
|
|
19
|
+
* `sessionId` is the frame's own session id for a consumer that does not care
|
|
20
|
+
* which position carried it: the run-frame position when present, otherwise the
|
|
21
|
+
* envelope position. It repeats one of those two values and is never a third
|
|
22
|
+
* one, so a caller that must know the position reads the position.
|
|
23
|
+
*/
|
|
24
|
+
export type SandboxEventIdentity = {
|
|
7
25
|
executionId?: string;
|
|
8
26
|
sessionId?: string;
|
|
27
|
+
runFrameSessionId?: string;
|
|
28
|
+
envelopeSessionId?: string;
|
|
9
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* The session ids a frame carries, one per field position.
|
|
32
|
+
*
|
|
33
|
+
* Every position that names a session is an assertion the frame makes about
|
|
34
|
+
* itself, so each one is compared. Reducing them to a single precedence winner
|
|
35
|
+
* leaves the losing position unchecked.
|
|
36
|
+
*/
|
|
37
|
+
export declare function carriedSessionIds(identity: SandboxEventIdentity): readonly string[];
|
|
38
|
+
/** Read identity from both run frames and /agents/events session envelopes. */
|
|
39
|
+
export declare function sandboxEventIdentity(event: SandboxEvent): SandboxEventIdentity;
|
|
10
40
|
export declare function environmentEventFromSandboxEvent(event: SandboxEvent, expected?: {
|
|
11
41
|
executionId?: string;
|
|
12
42
|
sessionId?: string;
|
package/dist/tangle-events.js
CHANGED
|
@@ -15,6 +15,69 @@ export function isSandboxConnectionMarker(event) {
|
|
|
15
15
|
!Array.isArray(data) &&
|
|
16
16
|
data.type === "connection.established");
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The frame types whose `properties.part` the sidecar shapes.
|
|
20
|
+
*
|
|
21
|
+
* A `raw` frame carries a backend event the sidecar does not shape, so any
|
|
22
|
+
* `sessionID` inside it is the backend's own value and names no runtime
|
|
23
|
+
* session. The part position is therefore read only on a frame type that
|
|
24
|
+
* publishes a rewritten part. A type outside this set keeps its data opaque.
|
|
25
|
+
*/
|
|
26
|
+
const PART_BEARING_FRAME_TYPES = new Set([
|
|
27
|
+
"message.part.updated",
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* The session ids a frame carries, one per field position.
|
|
31
|
+
*
|
|
32
|
+
* Every position that names a session is an assertion the frame makes about
|
|
33
|
+
* itself, so each one is compared. Reducing them to a single precedence winner
|
|
34
|
+
* leaves the losing position unchecked.
|
|
35
|
+
*/
|
|
36
|
+
export function carriedSessionIds(identity) {
|
|
37
|
+
return [identity.runFrameSessionId, identity.envelopeSessionId].filter((value) => value !== undefined);
|
|
38
|
+
}
|
|
39
|
+
/** Unwrap the nested identity carriers of a session-bus frame. */
|
|
40
|
+
function sessionEnvelope(data, type) {
|
|
41
|
+
const record = (value) => value && typeof value === "object" && !Array.isArray(value)
|
|
42
|
+
? value
|
|
43
|
+
: undefined;
|
|
44
|
+
const properties = record(data.properties);
|
|
45
|
+
// `data.part` is the run lane's raw backend part, whose ids are the backend's
|
|
46
|
+
// own. Only the part under `properties` has been rewritten to the runtime ids.
|
|
47
|
+
return {
|
|
48
|
+
properties,
|
|
49
|
+
info: record(properties?.info),
|
|
50
|
+
part: type !== undefined && PART_BEARING_FRAME_TYPES.has(type)
|
|
51
|
+
? record(properties?.part)
|
|
52
|
+
: undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The one value a set of alias fields carries.
|
|
57
|
+
*
|
|
58
|
+
* The aliases of an id within one frame are copies of a single value, so two
|
|
59
|
+
* different values across them is a frame claiming two identities. Such a frame
|
|
60
|
+
* is refused rather than resolved by field precedence, which would leave the
|
|
61
|
+
* losing alias unchecked.
|
|
62
|
+
*/
|
|
63
|
+
function agreedIdentifier(values, label) {
|
|
64
|
+
let agreed;
|
|
65
|
+
for (const raw of values) {
|
|
66
|
+
if (raw === undefined || raw === null)
|
|
67
|
+
continue;
|
|
68
|
+
const value = optionalNonEmptyString(raw, label);
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
if (agreed === undefined) {
|
|
72
|
+
agreed = value;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (agreed !== value) {
|
|
76
|
+
throw new Error(`${label} disagreed across the fields that carry it`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return agreed;
|
|
80
|
+
}
|
|
18
81
|
/** Read identity from both run frames and /agents/events session envelopes. */
|
|
19
82
|
export function sandboxEventIdentity(event) {
|
|
20
83
|
const record = event;
|
|
@@ -23,24 +86,29 @@ export function sandboxEventIdentity(event) {
|
|
|
23
86
|
return {};
|
|
24
87
|
}
|
|
25
88
|
const dataRecord = data;
|
|
26
|
-
const properties = dataRecord.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
89
|
+
const { properties, info, part } = sessionEnvelope(dataRecord, typeof record.type === "string" ? record.type : undefined);
|
|
90
|
+
const runFrameSessionId = agreedIdentifier([dataRecord.sessionId, dataRecord.sessionID], "Tangle Sandbox event sessionId");
|
|
91
|
+
const envelopeSessionId = agreedIdentifier([
|
|
92
|
+
properties?.sessionId,
|
|
93
|
+
properties?.sessionID,
|
|
94
|
+
info?.sessionId,
|
|
95
|
+
info?.sessionID,
|
|
96
|
+
part?.sessionId,
|
|
97
|
+
part?.sessionID,
|
|
98
|
+
], "Tangle Sandbox event sessionId");
|
|
36
99
|
return {
|
|
37
|
-
executionId:
|
|
38
|
-
sessionId:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
100
|
+
executionId: agreedIdentifier([dataRecord.executionId, properties?.executionId, info?.executionId], "Tangle Sandbox event executionId"),
|
|
101
|
+
sessionId: runFrameSessionId ?? envelopeSessionId,
|
|
102
|
+
runFrameSessionId,
|
|
103
|
+
envelopeSessionId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Read the `title`/`time` content of a `session.updated` frame in either shape. */
|
|
107
|
+
function sessionUpdateContent(data) {
|
|
108
|
+
const { info } = sessionEnvelope(data, "session.updated");
|
|
109
|
+
return {
|
|
110
|
+
title: data.title ?? info?.title,
|
|
111
|
+
time: data.time ?? info?.time,
|
|
44
112
|
};
|
|
45
113
|
}
|
|
46
114
|
export function environmentEventFromSandboxEvent(event, expected = {}) {
|
|
@@ -72,27 +140,42 @@ export function environmentEventFromSandboxEvent(event, expected = {}) {
|
|
|
72
140
|
if (Object.prototype.hasOwnProperty.call(data, "contextTransferReceipt")) {
|
|
73
141
|
throw new Error("Tangle Sandbox emitted an unsolicited context transfer receipt");
|
|
74
142
|
}
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
143
|
+
// A stream-bound iterator is the response body of the run this call started
|
|
144
|
+
// or the replay of one named execution, so the transport itself excludes
|
|
145
|
+
// another session's frames. On such a stream the run-frame position of
|
|
146
|
+
// `session.updated` carries the harness-native session id (for example an
|
|
147
|
+
// OpenCode session) rather than the runtime session id, so that one position
|
|
148
|
+
// on that one frame type is content. Every other position of that frame, and
|
|
149
|
+
// every position of every other frame type, names the runtime session and is
|
|
150
|
+
// compared to the expected id.
|
|
79
151
|
const identity = sandboxEventIdentity(event);
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
?
|
|
83
|
-
: identity
|
|
84
|
-
if (expected.executionId !== undefined
|
|
85
|
-
(
|
|
86
|
-
(
|
|
87
|
-
|
|
152
|
+
const runFrameCarriesNativeSessionId = expected.streamBound === true && record.type === "session.updated";
|
|
153
|
+
const identityBearingSessionIds = carriedSessionIds(runFrameCarriesNativeSessionId
|
|
154
|
+
? { envelopeSessionId: identity.envelopeSessionId }
|
|
155
|
+
: identity);
|
|
156
|
+
if (expected.executionId !== undefined) {
|
|
157
|
+
if (identity.executionId === undefined && expected.streamBound !== true) {
|
|
158
|
+
throw new Error("Tangle exact session event arrived without an executionId");
|
|
159
|
+
}
|
|
160
|
+
if (identity.executionId !== undefined &&
|
|
161
|
+
identity.executionId !== expected.executionId) {
|
|
162
|
+
throw new Error("Tangle exact session event identified a different executionId");
|
|
163
|
+
}
|
|
88
164
|
}
|
|
89
|
-
if (expected.sessionId !== undefined
|
|
90
|
-
(
|
|
91
|
-
(
|
|
92
|
-
|
|
165
|
+
if (expected.sessionId !== undefined) {
|
|
166
|
+
if (identityBearingSessionIds.length === 0 && expected.streamBound !== true) {
|
|
167
|
+
throw new Error("Tangle exact session event arrived without a sessionId");
|
|
168
|
+
}
|
|
169
|
+
for (const value of identityBearingSessionIds) {
|
|
170
|
+
if (value !== expected.sessionId) {
|
|
171
|
+
throw new Error("Tangle exact session event identified a different sessionId");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
93
174
|
}
|
|
94
175
|
const usage = tokenUsageFromData(data);
|
|
95
|
-
|
|
176
|
+
// The session id reaches the normalized event whichever position carried it,
|
|
177
|
+
// including the native id an execution-bound stream just accepted as content.
|
|
178
|
+
const normalized = normalizeSandboxEvent(record.type, data, identity);
|
|
96
179
|
return {
|
|
97
180
|
type: record.type,
|
|
98
181
|
data,
|
|
@@ -104,7 +187,8 @@ export function environmentEventFromSandboxEvent(event, expected = {}) {
|
|
|
104
187
|
providerEvent: event,
|
|
105
188
|
};
|
|
106
189
|
}
|
|
107
|
-
function normalizeSandboxEvent(type, data) {
|
|
190
|
+
function normalizeSandboxEvent(type, data, identity) {
|
|
191
|
+
const sessionId = identity.sessionId;
|
|
108
192
|
const supplied = data.normalized;
|
|
109
193
|
if (supplied !== undefined) {
|
|
110
194
|
const parsed = CanonicalStreamEventSchema.safeParse(supplied);
|
|
@@ -114,6 +198,17 @@ function normalizeSandboxEvent(type, data) {
|
|
|
114
198
|
if (parsed.data.type !== type) {
|
|
115
199
|
throw new Error(`Tangle Sandbox normalized event type "${parsed.data.type}" does not match transport type "${type}"`);
|
|
116
200
|
}
|
|
201
|
+
// A supplied block is a field of the frame, so it repeats a session id one
|
|
202
|
+
// of the frame's own positions carries and introduces none of its own. Each
|
|
203
|
+
// position it can repeat is already bound: the check above compared every
|
|
204
|
+
// position that names the runtime session, and the run-frame position of a
|
|
205
|
+
// stream-bound `session.updated` holds the native id the computed block
|
|
206
|
+
// carries too. A frame that carries no session id can supply none either.
|
|
207
|
+
if (parsed.data.type === "session.updated") {
|
|
208
|
+
if (!carriedSessionIds(identity).includes(parsed.data.sessionId)) {
|
|
209
|
+
throw new Error("Tangle Sandbox normalized event named a session the frame does not carry");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
117
212
|
return parsed.data;
|
|
118
213
|
}
|
|
119
214
|
switch (type) {
|
|
@@ -164,13 +259,15 @@ function normalizeSandboxEvent(type, data) {
|
|
|
164
259
|
code: data.code,
|
|
165
260
|
message: data.message,
|
|
166
261
|
});
|
|
167
|
-
case "session.updated":
|
|
262
|
+
case "session.updated": {
|
|
263
|
+
const content = sessionUpdateContent(data);
|
|
168
264
|
return parseCanonical({
|
|
169
265
|
type,
|
|
170
|
-
sessionId
|
|
171
|
-
...(typeof
|
|
172
|
-
...(
|
|
266
|
+
sessionId,
|
|
267
|
+
...(typeof content.title === "string" ? { title: content.title } : {}),
|
|
268
|
+
...(content.time !== undefined ? { time: content.time } : {}),
|
|
173
269
|
});
|
|
270
|
+
}
|
|
174
271
|
case "interaction":
|
|
175
272
|
return parseCanonical({ type, request: data.request });
|
|
176
273
|
case "interaction.cancel":
|
|
@@ -209,8 +306,9 @@ function statusFromSandboxValue(value) {
|
|
|
209
306
|
return "completed";
|
|
210
307
|
case "failed":
|
|
211
308
|
case "error":
|
|
212
|
-
case "cancelled":
|
|
213
309
|
return "failed";
|
|
310
|
+
case "cancelled":
|
|
311
|
+
return "cancelled";
|
|
214
312
|
default:
|
|
215
313
|
return undefined;
|
|
216
314
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, credential-free summary of one Sandbox transport failure.
|
|
3
|
+
*
|
|
4
|
+
* A transport message carries the request URL, and that URL can carry userinfo
|
|
5
|
+
* or a query token, so no contract payload ever repeats the message. A summary
|
|
6
|
+
* is built from structured error members only: a status is re-emitted as an
|
|
7
|
+
* integer, and a code or an error name is emitted only when it matches a bare
|
|
8
|
+
* identifier, which cannot express a URL, a bearer, or whitespace.
|
|
9
|
+
*/
|
|
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";
|
|
12
|
+
/** Name the read that failed and the cause, with nothing copied from it. */
|
|
13
|
+
export declare function transportFailureReason(read: TransportRead, error: unknown): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, credential-free summary of one Sandbox transport failure.
|
|
3
|
+
*
|
|
4
|
+
* A transport message carries the request URL, and that URL can carry userinfo
|
|
5
|
+
* or a query token, so no contract payload ever repeats the message. A summary
|
|
6
|
+
* is built from structured error members only: a status is re-emitted as an
|
|
7
|
+
* integer, and a code or an error name is emitted only when it matches a bare
|
|
8
|
+
* identifier, which cannot express a URL, a bearer, or whitespace.
|
|
9
|
+
*/
|
|
10
|
+
/** Codes and error names must match this to be carried into a reason. */
|
|
11
|
+
const BARE_IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
12
|
+
/** Name the read that failed and the cause, with nothing copied from it. */
|
|
13
|
+
export function transportFailureReason(read, error) {
|
|
14
|
+
return `the Sandbox ${read} failed (${failureCause(error)})`;
|
|
15
|
+
}
|
|
16
|
+
function failureCause(error) {
|
|
17
|
+
if (error === null || typeof error !== "object")
|
|
18
|
+
return "cause unreported";
|
|
19
|
+
const detail = error;
|
|
20
|
+
const name = bareIdentifier(detail.name);
|
|
21
|
+
if (name === "AbortError")
|
|
22
|
+
return "aborted";
|
|
23
|
+
if (name === "TimeoutError")
|
|
24
|
+
return "timed out";
|
|
25
|
+
const status = httpStatus(detail.status);
|
|
26
|
+
if (status !== undefined)
|
|
27
|
+
return `HTTP ${status}`;
|
|
28
|
+
const code = bareIdentifier(detail.code);
|
|
29
|
+
if (code !== undefined)
|
|
30
|
+
return `code ${code}`;
|
|
31
|
+
// A bare `Error` names no cause beyond the message, which is never carried.
|
|
32
|
+
if (name !== undefined && name !== "Error")
|
|
33
|
+
return name;
|
|
34
|
+
return "cause unreported";
|
|
35
|
+
}
|
|
36
|
+
function bareIdentifier(value) {
|
|
37
|
+
return typeof value === "string" && BARE_IDENTIFIER.test(value) ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
function httpStatus(value) {
|
|
40
|
+
return typeof value === "number" &&
|
|
41
|
+
Number.isSafeInteger(value) &&
|
|
42
|
+
value >= 100 &&
|
|
43
|
+
value <= 599
|
|
44
|
+
? value
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { InteractionAcknowledgement, InteractionResponseCommand } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { SandboxSessionLike } from "./tangle-types.js";
|
|
3
|
+
export interface TangleInteractionResponderOptions {
|
|
4
|
+
session: SandboxSessionLike;
|
|
5
|
+
/** The session the bound ask must belong to. */
|
|
6
|
+
sessionId: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
environmentId: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Answer one exact interaction through the Sandbox interaction command route.
|
|
12
|
+
*
|
|
13
|
+
* The route carries the canonical command whole and returns the deployment's
|
|
14
|
+
* own durable record, so this adapter keeps no resolution record of its own:
|
|
15
|
+
* the deployment decides `accepted`, `already_resolved_same`, and
|
|
16
|
+
* `already_resolved_different`, and its answer survives a restart of this
|
|
17
|
+
* process and a rebuilt environment object. The capability this responder is
|
|
18
|
+
* gated on is exactly that durable record.
|
|
19
|
+
*
|
|
20
|
+
* Two verdicts stay here, because the route cannot produce them as an
|
|
21
|
+
* acknowledgement: a command bound to another environment or session, and an
|
|
22
|
+
* answer the outstanding ask's spec rejects.
|
|
23
|
+
*/
|
|
24
|
+
export declare function tangleInteractionResponder(options: TangleInteractionResponderOptions): (command: InteractionResponseCommand, operation?: {
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}) => Promise<InteractionAcknowledgement>;
|