@tangle-network/agent-provider-cli-bridge 0.6.4 → 0.7.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 CHANGED
@@ -58,3 +58,24 @@ Stopping a direct `environment.stream()` reader or destroying the environment ca
58
58
 
59
59
  Response headers and streamed bodies have no transport timeout by default.
60
60
  For unattended runs, set `headersTimeoutMs`, `bodyTimeoutMs`, or an `AbortSignal` so an unresponsive bridge cannot wait forever.
61
+
62
+ ## Environment observation
63
+
64
+ `environment.observe()` returns the normalized `AgentEnvironmentObservation` when the provider document carries an `observation` block.
65
+ Every surface keeps a freshness discriminator, so a value this bridge cannot report is visibly absent instead of arriving as a measured zero.
66
+
67
+ | Surface | cli-bridge source | State when the source is missing |
68
+ | --- | --- | --- |
69
+ | `identity` | provider name and environment id | always known |
70
+ | `lifecycle.status` | `running` until the environment is destroyed | always known |
71
+ | `endpoint` | scheme, host, and explicit port of `baseUrl` | `unavailable` |
72
+ | `placement.verified` | `defaultExecution.kind` as `sandbox` or `local` | always known |
73
+ | `modelUsage` | the token usage and cost the newest execution measured | `unavailable` |
74
+
75
+ cli-bridge forwards a turn and provisions no compute, so it states no requested or effective compute shape, no resource use, and no compute cost.
76
+ It holds no account, so it states no plan, credit balance, quota, or billing period.
77
+ Those five surfaces are never claimed in the capability document and always carry `unavailable` with the reason.
78
+
79
+ The usage of one run is summed across its events and attributed to that run's execution id, which the observation carries as the provenance source.
80
+ An environment rebuilt with `provider.get(id)` has measured nothing yet, so it reports the usage surface as unavailable until a turn completes through it.
81
+ The endpoint holds only a scheme, a host, and an explicit port; userinfo, path, and query are dropped, and the bearer beside the base URL is never read.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { AgentEnvironmentCapabilities, AgentEnvironmentProvider } from "@ta
2
2
  import { type HarnessType } from "@tangle-network/agent-interface";
3
3
  import { type CliBridgeProviderOptions } from "./provider-options.js";
4
4
  export type { CliBridgeProviderOptions } from "./provider-options.js";
5
+ export { safeEndpointFromBaseUrl } from "./observation.js";
5
6
  export declare function createCliBridgeProvider(options: CliBridgeProviderOptions): AgentEnvironmentProvider;
6
7
  /**
7
8
  * Describe the bridge backend, not this adapter.
package/dist/index.js CHANGED
@@ -1,12 +1,26 @@
1
1
  import { harnessSystemPromptIntents, snapshotAgentProfile, } from "@tangle-network/agent-interface";
2
2
  import { createCliBridgeEnvironment } from "./retained-environment.js";
3
3
  import { assertCliBridgeProviderOptions, } from "./provider-options.js";
4
+ import { narrowedCliBridgeObservation } from "./observation.js";
5
+ export { safeEndpointFromBaseUrl } from "./observation.js";
4
6
  export function createCliBridgeProvider(options) {
5
7
  assertCliBridgeProviderOptions(options);
6
8
  const name = options.name ?? "cli-bridge";
9
+ // The observation surfaces are declared as intent and narrowed to the
10
+ // sources this bridge can put a value on, so the environment offers the
11
+ // operation exactly where the document claims it.
12
+ const resolveCapabilities = () => {
13
+ const declared = options.capabilities ?? defaultCliBridgeCapabilities();
14
+ return declared.observation === undefined
15
+ ? declared
16
+ : {
17
+ ...declared,
18
+ observation: narrowedCliBridgeObservation(declared.observation, options),
19
+ };
20
+ };
7
21
  return {
8
22
  name,
9
- capabilities: () => options.capabilities ?? defaultCliBridgeCapabilities(),
23
+ capabilities: resolveCapabilities,
10
24
  async create(input) {
11
25
  if (typeof input.profile === "string") {
12
26
  throw new Error(`createCliBridgeProvider requires an inline AgentProfile; named profile "${input.profile}" is unsupported`);
@@ -23,6 +37,7 @@ export function createCliBridgeProvider(options) {
23
37
  environmentId,
24
38
  allowDispatch: true,
25
39
  cancelRunsOnDestroy: true,
40
+ capabilities: resolveCapabilities(),
26
41
  });
27
42
  },
28
43
  async get(id) {
@@ -36,6 +51,7 @@ export function createCliBridgeProvider(options) {
36
51
  environmentId: id,
37
52
  allowDispatch: false,
38
53
  cancelRunsOnDestroy: false,
54
+ capabilities: resolveCapabilities(),
39
55
  });
40
56
  },
41
57
  };
@@ -88,5 +104,19 @@ export function defaultCliBridgeCapabilities(harness) {
88
104
  placement: true,
89
105
  usage: true,
90
106
  confidential: false,
107
+ // cli-bridge measures what a turn cost and where it forwarded that turn.
108
+ // It provisions no compute and holds no account, so the surfaces that
109
+ // describe provisioned resources and billing are never claimed.
110
+ observation: {
111
+ identity: true,
112
+ lifecycle: true,
113
+ endpoint: true,
114
+ placement: true,
115
+ resources: false,
116
+ resourceUse: false,
117
+ modelUsage: true,
118
+ computeBilling: false,
119
+ accountUsage: false,
120
+ },
91
121
  };
92
122
  }
@@ -0,0 +1,49 @@
1
+ import type { AgentEnvironmentCapabilities, AgentEnvironmentObservation, SafeEndpoint, TokenUsage } from "@tangle-network/agent-interface";
2
+ import type { CliBridgeProviderOptions } from "./provider-options.js";
3
+ /** One execution's accumulated token usage and cost. */
4
+ interface ExecutionUsageRecord {
5
+ executionId: string;
6
+ usage: TokenUsage;
7
+ observedAt: string;
8
+ }
9
+ /**
10
+ * The newest per-execution usage this environment handle measured.
11
+ *
12
+ * cli-bridge reports usage on the events of one run, so the totals are summed
13
+ * per execution and the newest execution answers the environment-scoped
14
+ * observation. The record lives with the handle: an environment rebuilt by id
15
+ * starts with none, and the observation then reports the usage surface as
16
+ * unavailable rather than as a measured zero.
17
+ */
18
+ export interface ExecutionUsageLog {
19
+ record(executionId: string | undefined, usage: TokenUsage | undefined): void;
20
+ latest(): ExecutionUsageRecord | undefined;
21
+ }
22
+ export declare function createExecutionUsageLog(): ExecutionUsageLog;
23
+ /**
24
+ * Read the credential-free network location of the cli-bridge endpoint.
25
+ *
26
+ * Only the scheme, host, and explicit port are carried. Userinfo, path, and
27
+ * query are dropped, and the bearer beside the base URL is never read, so an
28
+ * endpoint payload cannot transport a credential.
29
+ */
30
+ export declare function safeEndpointFromBaseUrl(baseUrl: string | undefined): SafeEndpoint | undefined;
31
+ /**
32
+ * Clear every observation surface no source backs.
33
+ *
34
+ * cli-bridge forwards a turn to a remote bridge and reports what that turn
35
+ * cost. It never provisions compute, so it states no compute shape, no
36
+ * resource use, no compute cost, and no account plan. The endpoint rests on a
37
+ * base URL that parses.
38
+ */
39
+ export declare function narrowedCliBridgeObservation(declared: NonNullable<AgentEnvironmentCapabilities["observation"]>, options: CliBridgeProviderOptions): NonNullable<AgentEnvironmentCapabilities["observation"]>;
40
+ export interface CliBridgeObservationSources {
41
+ options: CliBridgeProviderOptions;
42
+ provider: string;
43
+ environmentId: string;
44
+ destroyed: boolean;
45
+ usageLog: ExecutionUsageLog;
46
+ }
47
+ /** Build the normalized, freshness-tagged observation of one environment. */
48
+ export declare function observeCliBridgeEnvironment(sources: CliBridgeObservationSources): AgentEnvironmentObservation;
49
+ export {};
@@ -0,0 +1,141 @@
1
+ import { AgentEnvironmentObservationSchema } from "@tangle-network/agent-interface";
2
+ export function createExecutionUsageLog() {
3
+ let latest;
4
+ return {
5
+ record(executionId, usage) {
6
+ // Usage without the execution it belongs to cannot be attributed, and an
7
+ // execution without usage measured nothing. Neither is recorded.
8
+ if (executionId === undefined || executionId.length === 0)
9
+ return;
10
+ if (usage === undefined)
11
+ return;
12
+ latest = {
13
+ executionId,
14
+ usage,
15
+ observedAt: new Date().toISOString(),
16
+ };
17
+ },
18
+ latest() {
19
+ return latest === undefined
20
+ ? undefined
21
+ : { ...latest, usage: { ...latest.usage } };
22
+ },
23
+ };
24
+ }
25
+ /**
26
+ * Read the credential-free network location of the cli-bridge endpoint.
27
+ *
28
+ * Only the scheme, host, and explicit port are carried. Userinfo, path, and
29
+ * query are dropped, and the bearer beside the base URL is never read, so an
30
+ * endpoint payload cannot transport a credential.
31
+ */
32
+ export function safeEndpointFromBaseUrl(baseUrl) {
33
+ if (typeof baseUrl !== "string" || baseUrl.length === 0)
34
+ return undefined;
35
+ let parsed;
36
+ try {
37
+ parsed = new URL(baseUrl);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ const scheme = parsed.protocol.replace(/:$/, "");
43
+ const host = parsed.hostname;
44
+ if (scheme.length === 0 || host.length === 0)
45
+ return undefined;
46
+ const port = parsed.port === "" ? undefined : Number.parseInt(parsed.port, 10);
47
+ if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)) {
48
+ return undefined;
49
+ }
50
+ return { scheme, host, ...(port === undefined ? {} : { port }) };
51
+ }
52
+ /**
53
+ * Clear every observation surface no source backs.
54
+ *
55
+ * cli-bridge forwards a turn to a remote bridge and reports what that turn
56
+ * cost. It never provisions compute, so it states no compute shape, no
57
+ * resource use, no compute cost, and no account plan. The endpoint rests on a
58
+ * base URL that parses.
59
+ */
60
+ export function narrowedCliBridgeObservation(declared, options) {
61
+ const endpoint = safeEndpointFromBaseUrl(options.baseUrl) !== undefined;
62
+ return {
63
+ identity: declared.identity,
64
+ lifecycle: declared.lifecycle,
65
+ endpoint: endpoint ? declared.endpoint : false,
66
+ placement: declared.placement,
67
+ resources: false,
68
+ resourceUse: false,
69
+ modelUsage: declared.modelUsage,
70
+ computeBilling: false,
71
+ accountUsage: false,
72
+ };
73
+ }
74
+ /** Build the normalized, freshness-tagged observation of one environment. */
75
+ export function observeCliBridgeEnvironment(sources) {
76
+ const capturedAt = new Date().toISOString();
77
+ const provenance = {
78
+ origin: "reported",
79
+ observedAt: capturedAt,
80
+ source: "cli-bridge",
81
+ };
82
+ const subject = {
83
+ provider: sources.provider,
84
+ environmentId: sources.environmentId,
85
+ };
86
+ const endpoint = safeEndpointFromBaseUrl(sources.options.baseUrl);
87
+ const latest = sources.usageLog.latest();
88
+ const absent = (reason) => ({
89
+ state: "unavailable",
90
+ reason,
91
+ });
92
+ const noCompute = "cli-bridge forwards a turn and provisions no compute";
93
+ const noAccount = "cli-bridge reports no account plan, credit, or quota";
94
+ const noProbe = "cli-bridge never probes the bridge, so its runtime state is unproven";
95
+ return AgentEnvironmentObservationSchema.parse({
96
+ subject,
97
+ capturedAt,
98
+ identity: { state: "known", value: subject, provenance },
99
+ // A destroyed handle stopped because this adapter stopped it, which is the
100
+ // only lifecycle fact cli-bridge establishes. A live handle holds a base
101
+ // URL and nothing else, and configuration is not a running runtime.
102
+ lifecycle: sources.destroyed
103
+ ? { state: "known", value: { status: "stopped" }, provenance }
104
+ : absent(noProbe),
105
+ endpoint: endpoint === undefined
106
+ ? absent("the cli-bridge base URL states no reachable host")
107
+ : { state: "known", value: endpoint, provenance },
108
+ // The configured execution kind is where the caller asked the turn to run,
109
+ // so it is carried as the request. cli-bridge runs no placement check, so
110
+ // nothing verifies it.
111
+ placement: {
112
+ requested: {
113
+ kind: sources.options.defaultExecution?.kind === "sandbox" ? "sandbox" : "local",
114
+ },
115
+ verified: absent(noProbe),
116
+ },
117
+ resources: { effective: absent(noCompute) },
118
+ resourceUse: { current: absent(noCompute), peak: absent(noCompute) },
119
+ modelUsage: latest === undefined
120
+ ? absent("no execution measured token usage through this environment handle")
121
+ : {
122
+ state: "known",
123
+ value: latest.usage,
124
+ // The provenance source names the execution the usage belongs to,
125
+ // because cli-bridge measures usage per run and never for the
126
+ // environment.
127
+ provenance: {
128
+ origin: "reported",
129
+ observedAt: latest.observedAt,
130
+ source: latest.executionId,
131
+ },
132
+ },
133
+ computeBilling: absent(noCompute),
134
+ accountUsage: {
135
+ plan: absent(noAccount),
136
+ credits: absent(noAccount),
137
+ quota: absent(noAccount),
138
+ period: absent(noAccount),
139
+ },
140
+ });
141
+ }
@@ -1,4 +1,4 @@
1
- import type { AgentEnvironment, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
1
+ import type { AgentEnvironment, AgentEnvironmentCapabilities, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
2
2
  import type { CliBridgeProviderOptions } from "./provider-options.js";
3
3
  export interface CreateCliBridgeEnvironmentArgs {
4
4
  readonly options: CliBridgeProviderOptions;
@@ -7,5 +7,11 @@ export interface CreateCliBridgeEnvironmentArgs {
7
7
  readonly environmentId: string;
8
8
  readonly allowDispatch: boolean;
9
9
  readonly cancelRunsOnDestroy: boolean;
10
+ /**
11
+ * The document this provider publishes. The environment offers an optional
12
+ * operation only where the document claims it, so a caller never selects an
13
+ * action the document did not carry.
14
+ */
15
+ readonly capabilities: AgentEnvironmentCapabilities;
10
16
  }
11
17
  export declare function createCliBridgeEnvironment(args: CreateCliBridgeEnvironmentArgs): AgentEnvironment;
@@ -1,5 +1,6 @@
1
1
  import { agentSessionStatusFromRun, cancelCliBridgeRun, cancelExactCliBridgeRun, getCliBridgeRun, } from "./retained-control.js";
2
- import { collectCliBridgeTurnResult, dispatchCliBridgeTurn, streamCliBridgeSessionEvents, streamTrackedCliBridgeTurn, } from "./retained-execution.js";
2
+ import { addTokenUsage, collectCliBridgeTurnResult, dispatchCliBridgeTurn, streamCliBridgeSessionEvents, streamTrackedCliBridgeTurn, } from "./retained-execution.js";
3
+ import { createExecutionUsageLog, observeCliBridgeEnvironment, } from "./observation.js";
3
4
  import { assertRunMatchesControlRef, exactControlRefForSession, prepareCliBridgeRun, runFromControlRef, } from "./retained-run-state.js";
4
5
  import { createCliBridgeTransport } from "./transport.js";
5
6
  export function createCliBridgeEnvironment(args) {
@@ -8,6 +9,7 @@ export function createCliBridgeEnvironment(args) {
8
9
  const runs = new Map();
9
10
  const sessions = new Map();
10
11
  const readers = new Set();
12
+ const usageLog = createExecutionUsageLog();
11
13
  let destroyed = false;
12
14
  let closePromise;
13
15
  const stream = async function* (turn) {
@@ -17,7 +19,14 @@ export function createCliBridgeEnvironment(args) {
17
19
  throw new Error("a reconstructed cli-bridge environment can only control an existing run");
18
20
  }
19
21
  const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, false);
20
- yield* streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers);
22
+ // A streamed run reports usage across its events, so the totals are summed
23
+ // here and recorded as one execution's measurement.
24
+ let streamedUsage;
25
+ for await (const event of streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers)) {
26
+ streamedUsage = addTokenUsage(streamedUsage, event.usage);
27
+ usageLog.record(prepared.run.executionId, streamedUsage);
28
+ yield event;
29
+ }
21
30
  };
22
31
  return {
23
32
  id: environmentId,
@@ -52,8 +61,23 @@ export function createCliBridgeEnvironment(args) {
52
61
  allowPrompt: args.allowDispatch,
53
62
  requestedControlRef: sessionOptions?.controlRef,
54
63
  isDestroyed: () => destroyed,
64
+ usageLog,
55
65
  });
56
66
  },
67
+ ...(args.capabilities.observation
68
+ ? {
69
+ async observe(observeOptions) {
70
+ observeOptions?.signal?.throwIfAborted();
71
+ return observeCliBridgeEnvironment({
72
+ options,
73
+ provider: providerName,
74
+ environmentId,
75
+ destroyed,
76
+ usageLog,
77
+ });
78
+ },
79
+ }
80
+ : {}),
57
81
  placement: async (placementOptions) => {
58
82
  placementOptions?.signal?.throwIfAborted();
59
83
  return {
@@ -161,7 +185,9 @@ function createCliBridgeSession(args) {
161
185
  },
162
186
  async result(resultOptions) {
163
187
  const run = requireCurrentRun();
164
- return collectCliBridgeTurnResult(streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, { since: "0", signal: resultOptions?.signal }), run, args.options, args.transport, resultOptions?.signal);
188
+ const result = await collectCliBridgeTurnResult(streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, { since: "0", signal: resultOptions?.signal }), run, args.options, args.transport, resultOptions?.signal);
189
+ args.usageLog.record(run.executionId, result.usage);
190
+ return result;
165
191
  },
166
192
  async prompt(input) {
167
193
  if (!args.allowPrompt) {
@@ -175,6 +201,7 @@ function createCliBridgeSession(args) {
175
201
  sessionId: args.id,
176
202
  }, args.environmentId, false);
177
203
  const result = await collectCliBridgeTurnResult(streamTrackedCliBridgeTurn(args.options, args.environmentInput, prepared, args.transport, args.runs, args.sessions, args.readers), prepared.run, args.options, args.transport, input.signal);
204
+ args.usageLog.record(prepared.run.executionId, result.usage);
178
205
  return { ...result, sessionId: args.id };
179
206
  },
180
207
  async cancel(cancelOptions) {
@@ -1,3 +1,4 @@
1
+ import type { TokenUsage } from "@tangle-network/agent-interface";
1
2
  import type { AgentEnvironmentEvent, AgentSessionRef, AgentTurnResult, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
2
3
  import type { CliBridgeProviderOptions } from "./provider-options.js";
3
4
  import { type CliBridgeRun, type CliBridgeSessionState, type PreparedCliBridgeRun } from "./retained-run-state.js";
@@ -10,3 +11,5 @@ export declare function streamCliBridgeSessionEvents(options: CliBridgeProviderO
10
11
  signal?: AbortSignal;
11
12
  }): AsyncIterable<AgentEnvironmentEvent>;
12
13
  export declare function collectCliBridgeTurnResult(source: AsyncIterable<AgentEnvironmentEvent>, run: CliBridgeRun, options: CliBridgeProviderOptions, transport: CliBridgeTransport, signal?: AbortSignal): Promise<AgentTurnResult>;
14
+ /** Sum the usage a run reported across its events into one total. */
15
+ export declare function addTokenUsage(current: TokenUsage | undefined, next: TokenUsage | undefined): TokenUsage | undefined;
@@ -267,7 +267,8 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
267
267
  events,
268
268
  };
269
269
  }
270
- function addTokenUsage(current, next) {
270
+ /** Sum the usage a run reported across its events into one total. */
271
+ export function addTokenUsage(current, next) {
271
272
  if (!next)
272
273
  return current;
273
274
  if (!current)
@@ -9,6 +9,14 @@ export class CliBridgeRequestRejectedError extends Error {
9
9
  this.name = "CliBridgeRequestRejectedError";
10
10
  }
11
11
  }
12
+ class CliBridgeRunCancelledError extends Error {
13
+ detail;
14
+ constructor(detail) {
15
+ super(`cli-bridge run cancelled: ${detail}`);
16
+ this.detail = detail;
17
+ this.name = "CliBridgeRunCancelledError";
18
+ }
19
+ }
12
20
  export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onAccepted) {
13
21
  const reconnect = requestBody === undefined;
14
22
  const replay = retainedReplayRequest(lastEventId);
@@ -53,23 +61,37 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
53
61
  let textBoundaryPending = false;
54
62
  let terminalCursor;
55
63
  let sawProtocolEnd = false;
64
+ let terminalFrameSeen = false;
65
+ let cancelledTerminalSeen = false;
56
66
  for await (const frame of parseSse(response.body)) {
57
67
  if (frame.data === "[DONE]") {
58
68
  sawProtocolEnd = true;
59
69
  continue;
60
70
  }
71
+ if (terminalFrameSeen)
72
+ continue;
61
73
  const parsed = safeJson(frame.data);
62
74
  if (!parsed)
63
75
  continue;
64
76
  if (parsed.error && typeof parsed.error === "object") {
65
77
  const error = parsed.error;
66
78
  const message = typeof error.message === "string" ? error.message : "cli-bridge error";
79
+ const cancelled = error.type === "run_cancelled";
67
80
  yield* retainedEventsWithIdentity([{
68
81
  type: "status",
69
- data: { status: "failed", error: message },
70
- normalized: { type: "status", status: "failed", detail: message },
82
+ data: { status: cancelled ? "cancelled" : "failed", error: message },
83
+ normalized: {
84
+ type: "status",
85
+ status: cancelled ? "cancelled" : "failed",
86
+ detail: message,
87
+ },
71
88
  }], frame.id, runId, sessionId, turn.executionId, replay.anchor);
72
- throw new Error(`cli-bridge: ${message}`);
89
+ terminalFrameSeen = true;
90
+ completed = cancelled;
91
+ cancelledTerminalSeen = cancelled;
92
+ if (!cancelled)
93
+ throw new Error(`cli-bridge: ${message}`);
94
+ continue;
73
95
  }
74
96
  const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
75
97
  const delta = choice?.delta;
@@ -161,6 +183,7 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
161
183
  if (toolCallDeltas.length > 0)
162
184
  textBoundaryPending = true;
163
185
  if (choice?.finish_reason) {
186
+ terminalFrameSeen = true;
164
187
  if (choice.finish_reason === "error") {
165
188
  frameEvents.push({
166
189
  type: "status",
@@ -201,8 +224,42 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
201
224
  if (!completed && lastEventId === undefined) {
202
225
  throw new Error("cli-bridge stream ended without a terminal result");
203
226
  }
204
- if (lastEventId && requestBody !== undefined) {
205
- const result = await readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted);
227
+ if (lastEventId && requestBody !== undefined && !cancelledTerminalSeen) {
228
+ let result;
229
+ try {
230
+ result = await readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted);
231
+ }
232
+ catch (error) {
233
+ if (error instanceof CliBridgeRunCancelledError) {
234
+ // A normal finish frame can race with the bridge's authoritative
235
+ // cancelled state. Replace the withheld result at the same cursor.
236
+ if (terminalCursor !== undefined) {
237
+ yield {
238
+ type: "status",
239
+ data: {
240
+ status: "cancelled",
241
+ error: error.detail,
242
+ runId,
243
+ sessionId,
244
+ ...(turn.executionId === undefined
245
+ ? {}
246
+ : { executionId: turn.executionId }),
247
+ cursor: terminalCursor,
248
+ },
249
+ normalized: {
250
+ type: "status",
251
+ status: "cancelled",
252
+ detail: error.detail,
253
+ },
254
+ id: terminalCursor,
255
+ };
256
+ }
257
+ // If no new terminal frame followed the requested cursor, the caller
258
+ // already consumed the cancellation event. Do not duplicate it.
259
+ return;
260
+ }
261
+ throw error;
262
+ }
206
263
  const finalModelRequests = result.modelRequests ??
207
264
  (modelRequestsKnown ? observedModelRequests : undefined);
208
265
  const finalCursor = terminalCursor ?? lastEventId;
@@ -240,14 +297,21 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal,
240
297
  ...(signal ? { signal } : {}),
241
298
  });
242
299
  if (!response.ok) {
243
- throw new Error(`cli-bridge replay result ${response.status}: ${await response.text()}`);
300
+ const responseText = await response.text();
301
+ const error = cliBridgeError(safeJson(responseText));
302
+ if (error?.type === "run_cancelled") {
303
+ throw new CliBridgeRunCancelledError(cliBridgeErrorMessage(error));
304
+ }
305
+ throw new Error(`cli-bridge replay result ${response.status}: ${responseText}`);
244
306
  }
245
307
  onAccepted?.(response);
246
308
  const parsed = safeJson(await response.text());
247
- if (parsed?.error && typeof parsed.error === "object") {
248
- const error = parsed.error;
249
- const message = typeof error.message === "string" ? error.message : "cli-bridge replay failed";
250
- throw new Error(`cli-bridge replay result failed: ${message}`);
309
+ const error = cliBridgeError(parsed);
310
+ if (error) {
311
+ if (error.type === "run_cancelled") {
312
+ throw new CliBridgeRunCancelledError(cliBridgeErrorMessage(error));
313
+ }
314
+ throw new Error(`cli-bridge replay result failed: ${cliBridgeErrorMessage(error)}`);
251
315
  }
252
316
  const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : undefined;
253
317
  const message = choice?.message && typeof choice.message === "object"
@@ -274,3 +338,12 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal,
274
338
  : { system_fingerprint: responseIdentity.system_fingerprint }),
275
339
  };
276
340
  }
341
+ function cliBridgeError(parsed) {
342
+ const error = parsed?.error;
343
+ return error && typeof error === "object" && !Array.isArray(error)
344
+ ? error
345
+ : undefined;
346
+ }
347
+ function cliBridgeErrorMessage(error) {
348
+ return typeof error.message === "string" ? error.message : "cli-bridge replay failed";
349
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,6 +25,8 @@
25
25
  "files": [
26
26
  "dist/index.d.ts",
27
27
  "dist/index.js",
28
+ "dist/observation.d.ts",
29
+ "dist/observation.js",
28
30
  "dist/provider-options.d.ts",
29
31
  "dist/provider-options.js",
30
32
  "dist/retained-control.d.ts",
@@ -47,7 +49,7 @@
47
49
  "LICENSE"
48
50
  ],
49
51
  "dependencies": {
50
- "@tangle-network/agent-interface": "0.51.0",
52
+ "@tangle-network/agent-interface": "0.52.0",
51
53
  "undici": "7.29.0"
52
54
  },
53
55
  "devDependencies": {