@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { InteractionAcknowledgementSchema, InteractionRequestSchema, InteractionResponseCommandSchema, validateInteractionResponse, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { awaitWithSignal } from "./tangle-contract-safety.js";
|
|
3
|
+
import { assertOptionKeys } from "./tangle-environment-validation.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build one acknowledgement for a command this adapter refuses on its own.
|
|
6
|
+
*
|
|
7
|
+
* The three identity members echo the command, so a caller matches the answer
|
|
8
|
+
* to the operation it sent. Optional members are written only when a value
|
|
9
|
+
* exists: the schema demands a message for the two statuses that cannot be
|
|
10
|
+
* read without one, and an absent `retryable` is a distinct fact from `false`.
|
|
11
|
+
*/
|
|
12
|
+
function acknowledge(command, status, extras = {}) {
|
|
13
|
+
return InteractionAcknowledgementSchema.parse({
|
|
14
|
+
operationId: command.operationId,
|
|
15
|
+
binding: command.binding,
|
|
16
|
+
commandDigest: command.commandDigest,
|
|
17
|
+
status,
|
|
18
|
+
...(extras.message === undefined ? {} : { message: extras.message }),
|
|
19
|
+
...(extras.retryable === undefined ? {} : { retryable: extras.retryable }),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The coordinates this environment can answer for. A command naming another
|
|
24
|
+
* provider, environment, or session is refused here rather than sent, because
|
|
25
|
+
* the Sandbox SDK rejects such a command by throwing, and a caller holding a
|
|
26
|
+
* durable operation needs the refusal as an acknowledgement it can record.
|
|
27
|
+
*/
|
|
28
|
+
function foreignBinding(command, options) {
|
|
29
|
+
const binding = command.binding;
|
|
30
|
+
if (binding.provider === options.provider &&
|
|
31
|
+
binding.environmentId === options.environmentId &&
|
|
32
|
+
binding.sessionId === options.sessionId) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return acknowledge(command, "binding_mismatch", {
|
|
36
|
+
message: `response command binding names another provider, environment, or session than ${options.provider}/${options.environmentId}/${options.sessionId}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Refuse an answer the outstanding ask does not accept, naming every field the
|
|
41
|
+
* spec rejected.
|
|
42
|
+
*
|
|
43
|
+
* The deployment validates the same answer and is the authority, so this check
|
|
44
|
+
* exists for the message: a refusal that names the missing field reaches the
|
|
45
|
+
* caller instead of a bare rejection. It never invents a value for a field the
|
|
46
|
+
* caller left out — an incomplete answer is refused whole.
|
|
47
|
+
*
|
|
48
|
+
* An ask absent from the outstanding set is not refused here. It is the shape
|
|
49
|
+
* every already-resolved ask takes, and the deployment's durable record is the
|
|
50
|
+
* only thing that can tell a replay from an ask that never existed.
|
|
51
|
+
*/
|
|
52
|
+
async function specRefusal(command, options, signal) {
|
|
53
|
+
if (typeof options.session.interactions !== "function")
|
|
54
|
+
return undefined;
|
|
55
|
+
let outstanding;
|
|
56
|
+
try {
|
|
57
|
+
outstanding = await awaitWithSignal(options.session.interactions(signal ? { signal } : undefined), signal);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
// The outstanding set is unreadable, so the spec cannot be checked here.
|
|
62
|
+
// The command still carries only what the caller supplied, and the
|
|
63
|
+
// deployment validates it against the same spec before any delivery.
|
|
64
|
+
console.warn(`Tangle interaction spec read failed for session ${options.sessionId}: the answer reaches the deployment unchecked`, error);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
signal?.throwIfAborted();
|
|
68
|
+
if (!Array.isArray(outstanding))
|
|
69
|
+
return undefined;
|
|
70
|
+
const interactionId = command.binding.interactionId;
|
|
71
|
+
for (const entry of outstanding) {
|
|
72
|
+
const parsed = InteractionRequestSchema.safeParse(entry);
|
|
73
|
+
if (!parsed.success || parsed.data.id !== interactionId)
|
|
74
|
+
continue;
|
|
75
|
+
const validation = validateInteractionResponse(parsed.data, command.response);
|
|
76
|
+
if (validation.ok)
|
|
77
|
+
return undefined;
|
|
78
|
+
return acknowledge(command, "invalid_response", {
|
|
79
|
+
message: `interaction ${interactionId} rejected this answer: ${validation.errors.join("; ")}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Read the digest a conflicting resolution already holds, when it carries one. */
|
|
85
|
+
function existingResponseDigest(result) {
|
|
86
|
+
const digest = result.serverResult?.existingResponseDigest;
|
|
87
|
+
return typeof digest === "string" && digest.length > 0 ? digest : undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether the deployment recorded the response without confirming it reached
|
|
91
|
+
* the waiting agent. A recorded-but-undelivered resolution is not an accepted
|
|
92
|
+
* one, and reporting it as accepted would tell a caller an agent was released
|
|
93
|
+
* that is still blocked.
|
|
94
|
+
*/
|
|
95
|
+
function unconfirmedDelivery(result) {
|
|
96
|
+
return result.serverResult?.delivered === false;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Answer one exact interaction through the Sandbox interaction command route.
|
|
100
|
+
*
|
|
101
|
+
* The route carries the canonical command whole and returns the deployment's
|
|
102
|
+
* own durable record, so this adapter keeps no resolution record of its own:
|
|
103
|
+
* the deployment decides `accepted`, `already_resolved_same`, and
|
|
104
|
+
* `already_resolved_different`, and its answer survives a restart of this
|
|
105
|
+
* process and a rebuilt environment object. The capability this responder is
|
|
106
|
+
* gated on is exactly that durable record.
|
|
107
|
+
*
|
|
108
|
+
* Two verdicts stay here, because the route cannot produce them as an
|
|
109
|
+
* acknowledgement: a command bound to another environment or session, and an
|
|
110
|
+
* answer the outstanding ask's spec rejects.
|
|
111
|
+
*/
|
|
112
|
+
export function tangleInteractionResponder(options) {
|
|
113
|
+
return async (command, operation) => {
|
|
114
|
+
assertOptionKeys(operation, ["signal"], "Tangle interaction response");
|
|
115
|
+
// A command whose digest does not cover its own content is malformed, not
|
|
116
|
+
// refused: no acknowledgement can bind to a digest that names nothing.
|
|
117
|
+
const exactCommand = InteractionResponseCommandSchema.parse(command);
|
|
118
|
+
operation?.signal?.throwIfAborted();
|
|
119
|
+
const foreign = foreignBinding(exactCommand, options);
|
|
120
|
+
if (foreign)
|
|
121
|
+
return foreign;
|
|
122
|
+
const respond = options.session.respondToInteraction;
|
|
123
|
+
if (typeof respond !== "function") {
|
|
124
|
+
throw new Error("Tangle sandbox session cannot deliver an interaction response command");
|
|
125
|
+
}
|
|
126
|
+
const refusal = await specRefusal(exactCommand, options, operation?.signal);
|
|
127
|
+
if (refusal)
|
|
128
|
+
return refusal;
|
|
129
|
+
const result = await awaitWithSignal(respond.call(options.session, exactCommand, operation?.signal ? { signal: operation.signal } : undefined), operation?.signal);
|
|
130
|
+
operation?.signal?.throwIfAborted();
|
|
131
|
+
const acknowledgement = InteractionAcknowledgementSchema.parse(result?.acknowledgement);
|
|
132
|
+
if (acknowledgement.operationId !== exactCommand.operationId ||
|
|
133
|
+
acknowledgement.commandDigest !== exactCommand.commandDigest ||
|
|
134
|
+
acknowledgement.binding.interactionId !==
|
|
135
|
+
exactCommand.binding.interactionId ||
|
|
136
|
+
acknowledgement.binding.requestDigest !==
|
|
137
|
+
exactCommand.binding.requestDigest ||
|
|
138
|
+
acknowledgement.binding.sessionId !== exactCommand.binding.sessionId ||
|
|
139
|
+
acknowledgement.binding.environmentId !==
|
|
140
|
+
exactCommand.binding.environmentId ||
|
|
141
|
+
acknowledgement.binding.runId !== exactCommand.binding.runId ||
|
|
142
|
+
acknowledgement.binding.executionId !== exactCommand.binding.executionId ||
|
|
143
|
+
acknowledgement.binding.provider !== exactCommand.binding.provider) {
|
|
144
|
+
throw new Error("Tangle interaction response returned an acknowledgement for another operation");
|
|
145
|
+
}
|
|
146
|
+
if (acknowledgement.status === "accepted" && unconfirmedDelivery(result)) {
|
|
147
|
+
// The deployment recorded the resolution and did not confirm it reached
|
|
148
|
+
// the ask. The record makes a retry safe, and the caller must not read
|
|
149
|
+
// this as an answered ask.
|
|
150
|
+
return acknowledge(exactCommand, "transport_failure", {
|
|
151
|
+
message: `interaction ${exactCommand.binding.interactionId} is recorded but delivery is not confirmed`,
|
|
152
|
+
retryable: true,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (acknowledgement.status === "already_resolved_different") {
|
|
156
|
+
const digest = existingResponseDigest(result);
|
|
157
|
+
if (digest !== undefined && !(acknowledgement.message ?? "").includes(digest)) {
|
|
158
|
+
return acknowledge(exactCommand, "already_resolved_different", {
|
|
159
|
+
message: `${acknowledgement.message ??
|
|
160
|
+
`interaction ${exactCommand.binding.interactionId} already holds another response`} (existing response digest ${digest})`,
|
|
161
|
+
...(acknowledgement.retryable === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { retryable: acknowledgement.retryable }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return acknowledgement;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AgentEnvironmentObservation, ResourceProfile, SafeEndpoint } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { SandboxClientLike, SandboxConnectionLike, SandboxInstanceLike } from "./tangle-types.js";
|
|
3
|
+
import type { ExecutionUsageLog } from "./tangle-usage-log.js";
|
|
4
|
+
/** Everything one observation reads, resolved when the environment is composed. */
|
|
5
|
+
export interface TangleObservationSources {
|
|
6
|
+
box: SandboxInstanceLike;
|
|
7
|
+
client: SandboxClientLike;
|
|
8
|
+
provider: string;
|
|
9
|
+
environmentId: string;
|
|
10
|
+
/** Compute shape the caller asked for. Absent on an environment rebuilt by id. */
|
|
11
|
+
requestedResources?: ResourceProfile;
|
|
12
|
+
usageLog: ExecutionUsageLog;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Which observation surfaces this environment can put a value on.
|
|
16
|
+
*
|
|
17
|
+
* A flag is true only when a concrete source backs the surface for THIS
|
|
18
|
+
* sandbox. `identity` and `lifecycle` are unconditional because the sandbox id
|
|
19
|
+
* and status are always readable. `modelUsage` is unconditional for a
|
|
20
|
+
* different reason: the adapter measures the usage of every execution that
|
|
21
|
+
* runs through the handle, and reports the surface as unavailable until one
|
|
22
|
+
* completes. Every other flag rests on a source that can be absent.
|
|
23
|
+
*/
|
|
24
|
+
export interface ObservationSurfaceSupport {
|
|
25
|
+
identity: boolean;
|
|
26
|
+
lifecycle: boolean;
|
|
27
|
+
endpoint: boolean;
|
|
28
|
+
placement: boolean;
|
|
29
|
+
resources: boolean;
|
|
30
|
+
resourceUse: boolean;
|
|
31
|
+
modelUsage: boolean;
|
|
32
|
+
computeBilling: boolean;
|
|
33
|
+
accountUsage: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function observationSurfaceSupport(box: SandboxInstanceLike, client: SandboxClientLike, requestedResources: ResourceProfile | undefined): ObservationSurfaceSupport;
|
|
36
|
+
/**
|
|
37
|
+
* Account usage and the client-side placement surface are the only observation
|
|
38
|
+
* facts a client can establish before a sandbox exists. The rest rest on one
|
|
39
|
+
* environment's data, so a provider-stage document keeps them at the declared
|
|
40
|
+
* ceiling and each concrete sandbox measures them.
|
|
41
|
+
*/
|
|
42
|
+
export declare function clientObservationSurfaceSupport(client: SandboxClientLike): ObservationSurfaceSupport;
|
|
43
|
+
/**
|
|
44
|
+
* Read the credential-free network location of the sandbox runtime.
|
|
45
|
+
*
|
|
46
|
+
* Only the scheme, host, and explicit port of the runtime URL are carried.
|
|
47
|
+
* Userinfo, path, and query are dropped, and the bearer beside the URL is
|
|
48
|
+
* never read, so an endpoint payload cannot transport a credential. A port the
|
|
49
|
+
* URL leaves to its scheme default is omitted rather than inferred. The result
|
|
50
|
+
* is held to the contract's own endpoint schema, so a runtime URL the contract
|
|
51
|
+
* refuses reads as no endpoint instead of failing the whole observation.
|
|
52
|
+
*/
|
|
53
|
+
export declare function safeEndpointFromConnection(connection: SandboxConnectionLike | undefined): SafeEndpoint | undefined;
|
|
54
|
+
/** Build the normalized, freshness-tagged observation of one environment. */
|
|
55
|
+
export declare function observeTangleEnvironment(sources: TangleObservationSources, options?: {
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}): Promise<AgentEnvironmentObservation>;
|