@tangle-network/agent-provider-tangle 0.9.0 → 0.10.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.
@@ -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();
@@ -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
- /** Read identity from both run frames and /agents/events session envelopes. */
6
- export declare function sandboxEventIdentity(event: SandboxEvent): {
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;
@@ -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.properties &&
27
- typeof dataRecord.properties === "object" &&
28
- !Array.isArray(dataRecord.properties)
29
- ? dataRecord.properties
30
- : undefined;
31
- const info = properties?.info &&
32
- typeof properties.info === "object" &&
33
- !Array.isArray(properties.info)
34
- ? properties.info
35
- : undefined;
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: optionalNonEmptyString(dataRecord.executionId ?? properties?.executionId ?? info?.executionId, "Tangle Sandbox event executionId"),
38
- sessionId: optionalNonEmptyString(dataRecord.sessionId ??
39
- dataRecord.sessionID ??
40
- properties?.sessionId ??
41
- properties?.sessionID ??
42
- info?.sessionId ??
43
- info?.sessionID, "Tangle Sandbox event sessionId"),
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
- // An exact run stream is already selected by the runtime execution id.
76
- // On that stream, session.updated carries the harness-native session id
77
- // (for example an OpenCode session), not the runtime session id. Keep that
78
- // value as event content while lifecycle frames remain identity-checked.
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 eventExecutionId = identity.executionId;
81
- const eventSessionId = expected.streamBound === true && record.type === "session.updated"
82
- ? undefined
83
- : identity.sessionId;
84
- if (expected.executionId !== undefined &&
85
- ((eventExecutionId === undefined && expected.streamBound !== true) ||
86
- (eventExecutionId !== undefined && eventExecutionId !== expected.executionId))) {
87
- throw new Error("Tangle exact session event identified a different executionId");
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
- ((eventSessionId === undefined && expected.streamBound !== true) ||
91
- (eventSessionId !== undefined && eventSessionId !== expected.sessionId))) {
92
- throw new Error("Tangle exact session event identified a different sessionId");
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
- const normalized = normalizeSandboxEvent(record.type, data);
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: data.sessionId ?? data.sessionID,
171
- ...(typeof data.title === "string" ? { title: data.title } : {}),
172
- ...(data.time !== undefined ? { time: data.time } : {}),
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>;