@tangle-network/agent-provider-cli-bridge 0.7.7 → 0.7.8

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
@@ -51,6 +51,11 @@ The returned `AgentSession` exposes status, cursor-based event replay, the termi
51
51
  Cancellation returns only after cli-bridge confirms the run is terminal.
52
52
  A direct stream treats a finish frame as provisional until cli-bridge returns the same run's aggregate result.
53
53
  A retained replay confirms its terminal state from the retained run endpoint.
54
+ Retained native runs use canonical runtime envelopes and finish with a terminal status event.
55
+ Pass any emitted event `id` back as `since` to resume after that event.
56
+ The canonical envelope identity remains available as `event.data.eventId`.
57
+ Legacy runs keep the OpenAI chunk format and its `[DONE]` marker.
58
+ The provider uses the explicit SSE event field to select the format and rejects mixed streams.
54
59
  The provider rejects a terminal response when its run id or request digest does not match the accepted stream.
55
60
  Usage events and terminal result metadata include `modelRequests` only when the bridge measured an exact non-negative integer count.
56
61
  Missing, malformed, partial, or estimated request counts remain unknown.
@@ -0,0 +1,11 @@
1
+ import type { AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { CliBridgeSseFrame } from "./wire.js";
3
+ interface CanonicalEventContext {
4
+ readonly runId: string;
5
+ readonly sessionId: string;
6
+ readonly executionId?: string;
7
+ }
8
+ /** Decode one explicitly typed retained-run frame without guessing its wire format. */
9
+ export declare function retainedCanonicalEvent(frame: CliBridgeSseFrame, context: CanonicalEventContext): AgentEnvironmentEvent | undefined;
10
+ export declare function isTerminalCanonicalEvent(event: AgentEnvironmentEvent): boolean;
11
+ export {};
@@ -0,0 +1,135 @@
1
+ import { RuntimeEventEnvelopeSchema, } from "@tangle-network/agent-interface";
2
+ /** Decode one explicitly typed retained-run frame without guessing its wire format. */
3
+ export function retainedCanonicalEvent(frame, context) {
4
+ if (frame.event === undefined)
5
+ return undefined;
6
+ if (frame.event === "error")
7
+ throw retainedCanonicalStreamError(frame.data);
8
+ const envelope = parseEnvelope(frame.data);
9
+ if (envelope.runId !== context.runId) {
10
+ throw new Error("cli-bridge canonical event belongs to another retained run");
11
+ }
12
+ if (frame.event !== envelope.event.type) {
13
+ throw new Error("cli-bridge SSE event type does not match its canonical envelope");
14
+ }
15
+ if (frame.id === undefined) {
16
+ throw new Error("cli-bridge canonical event has no replay cursor");
17
+ }
18
+ if (!isBaseTenInteger(frame.id) ||
19
+ envelope.sequence === 0 ||
20
+ Number(frame.id) !== envelope.sequence) {
21
+ throw new Error("cli-bridge canonical event cursor does not match its sequence");
22
+ }
23
+ const event = envelope.event;
24
+ const usage = usageFromCanonicalEvent(event);
25
+ return {
26
+ type: event.type,
27
+ // AgentSession.events({ since }) accepts a previously emitted event id.
28
+ // The bridge replays by its numeric SSE cursor, while eventId identifies
29
+ // the canonical envelope and is not a replay cursor.
30
+ id: frame.id,
31
+ data: {
32
+ ...eventPayload(event),
33
+ eventId: envelope.eventId,
34
+ sequence: envelope.sequence,
35
+ cursor: frame.id,
36
+ runId: context.runId,
37
+ sessionId: context.sessionId,
38
+ ...(context.executionId === undefined
39
+ ? {}
40
+ : { executionId: context.executionId }),
41
+ ...(envelope.occurredAt === undefined
42
+ ? {}
43
+ : { occurredAt: envelope.occurredAt }),
44
+ receivedAt: envelope.receivedAt,
45
+ },
46
+ normalized: event,
47
+ ...(usage === undefined ? {} : { usage }),
48
+ providerEvent: envelope,
49
+ };
50
+ }
51
+ export function isTerminalCanonicalEvent(event) {
52
+ return event.normalized?.type === "status" &&
53
+ ["completed", "failed", "cancelled"].includes(event.normalized.status);
54
+ }
55
+ function parseEnvelope(data) {
56
+ let value;
57
+ try {
58
+ value = JSON.parse(data);
59
+ }
60
+ catch (error) {
61
+ throw new Error("cli-bridge canonical event is not valid JSON", { cause: error });
62
+ }
63
+ const parsed = RuntimeEventEnvelopeSchema.safeParse(value);
64
+ if (!parsed.success) {
65
+ throw new Error("cli-bridge emitted an invalid canonical event envelope", {
66
+ cause: parsed.error,
67
+ });
68
+ }
69
+ return parsed.data;
70
+ }
71
+ function eventPayload(event) {
72
+ const { type: _type, ...payload } = event;
73
+ return payload;
74
+ }
75
+ function usageFromCanonicalEvent(event) {
76
+ if (event.type !== "raw")
77
+ return undefined;
78
+ const providerEvent = recordValue(event.event);
79
+ if (providerEvent?.type !== "usage")
80
+ return undefined;
81
+ const data = recordValue(providerEvent.data);
82
+ const usage = recordValue(providerEvent.usage) ?? recordValue(data?.usage);
83
+ if (!usage)
84
+ return undefined;
85
+ const inputTokens = nonNegativeInteger(usage.inputTokens);
86
+ const outputTokens = nonNegativeInteger(usage.outputTokens);
87
+ if (inputTokens === undefined || outputTokens === undefined)
88
+ return undefined;
89
+ return {
90
+ inputTokens,
91
+ outputTokens,
92
+ ...optionalInteger(usage, "totalTokens"),
93
+ ...optionalInteger(usage, "cacheReadInputTokens"),
94
+ ...optionalInteger(usage, "cacheCreationInputTokens"),
95
+ ...optionalInteger(usage, "reasoningTokens"),
96
+ ...optionalNumber(usage, "cost"),
97
+ };
98
+ }
99
+ function optionalInteger(source, key) {
100
+ const value = nonNegativeInteger(source[key]);
101
+ return value === undefined ? {} : { [key]: value };
102
+ }
103
+ function optionalNumber(source, key) {
104
+ const value = source[key];
105
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
106
+ ? { [key]: value }
107
+ : {};
108
+ }
109
+ function nonNegativeInteger(value) {
110
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
111
+ ? value
112
+ : undefined;
113
+ }
114
+ function recordValue(value) {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value)
116
+ ? value
117
+ : undefined;
118
+ }
119
+ function isBaseTenInteger(value) {
120
+ return /^(0|[1-9][0-9]*)$/u.test(value) && Number.isSafeInteger(Number(value));
121
+ }
122
+ function retainedCanonicalStreamError(data) {
123
+ let message = "cli-bridge canonical event stream failed";
124
+ try {
125
+ const parsed = recordValue(JSON.parse(data));
126
+ const error = recordValue(parsed?.error);
127
+ if (typeof error?.message === "string" && error.message.length > 0) {
128
+ message = error.message;
129
+ }
130
+ }
131
+ catch {
132
+ // The typed error frame is authoritative even when its detail is malformed.
133
+ }
134
+ return new Error(`cli-bridge: ${message}`);
135
+ }
@@ -1,4 +1,5 @@
1
1
  import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-event-cursor.js";
2
+ import { isTerminalCanonicalEvent, retainedCanonicalEvent, } from "./retained-canonical-event.js";
2
3
  import { detachCliBridgeReader } from "./retained-control.js";
3
4
  import { requestHeaders, trimSlash } from "./transport.js";
4
5
  import { modelRequestsFromOpenAi, parseSse, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
@@ -72,14 +73,46 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
72
73
  let systemFingerprint;
73
74
  let textBoundaryPending = false;
74
75
  let sawProtocolEnd = false;
76
+ let protocol = "unknown";
77
+ let canonicalTerminalSeen = false;
75
78
  let terminalFrameSeen = false;
76
79
  let cancelledTerminalSeen = false;
77
80
  let terminalFrame;
78
81
  for await (const frame of parseSse(response.body)) {
79
82
  if (frame.data === "[DONE]") {
83
+ if (protocol === "canonical") {
84
+ throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
85
+ }
86
+ protocol = "openai";
80
87
  sawProtocolEnd = true;
81
88
  continue;
82
89
  }
90
+ if (frame.event !== undefined) {
91
+ if (protocol === "openai") {
92
+ throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
93
+ }
94
+ protocol = "canonical";
95
+ if (canonicalTerminalSeen) {
96
+ throw new Error("cli-bridge emitted an event after canonical terminal status");
97
+ }
98
+ const event = retainedCanonicalEvent(frame, {
99
+ runId,
100
+ sessionId,
101
+ ...(turn.executionId === undefined
102
+ ? {}
103
+ : { executionId: turn.executionId }),
104
+ });
105
+ if (!event) {
106
+ throw new Error("cli-bridge emitted an empty canonical event");
107
+ }
108
+ canonicalTerminalSeen = isTerminalCanonicalEvent(event);
109
+ yield event;
110
+ continue;
111
+ }
112
+ if (protocol === "canonical") {
113
+ throw new Error("cli-bridge mixed canonical and OpenAI stream formats");
114
+ }
115
+ protocol = "openai";
83
116
  if (terminalFrameSeen)
84
117
  continue;
85
118
  const parsed = safeJson(frame.data);
@@ -219,6 +252,12 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
219
252
  }
220
253
  yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
221
254
  }
255
+ if (protocol === "canonical") {
256
+ if (!canonicalTerminalSeen) {
257
+ throw new Error("cli-bridge canonical stream ended without terminal status");
258
+ }
259
+ return;
260
+ }
222
261
  if (!sawProtocolEnd) {
223
262
  throw new Error("cli-bridge stream ended without the [DONE] protocol marker");
224
263
  }
package/dist/wire.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare function toChatCompletionsBody(options: CliBridgeProviderOptions,
6
6
  export interface CliBridgeSseFrame {
7
7
  readonly data: string;
8
8
  readonly id?: string;
9
+ readonly event?: string;
9
10
  }
10
11
  export declare function parseSse(body: AsyncIterable<Uint8Array>): AsyncIterable<CliBridgeSseFrame>;
11
12
  export declare function toolCallsFromDelta(value: unknown): Array<{
package/dist/wire.js CHANGED
@@ -106,7 +106,11 @@ function dataFromFrame(frame) {
106
106
  if (!data)
107
107
  return undefined;
108
108
  const id = lines.find((line) => line.startsWith("id:"))?.slice("id:".length).trim();
109
- return { data, ...(id ? { id } : {}) };
109
+ const event = lines
110
+ .find((line) => line.startsWith("event:"))
111
+ ?.slice("event:".length)
112
+ .trim();
113
+ return { data, ...(id ? { id } : {}), ...(event ? { event } : {}) };
110
114
  }
111
115
  export function toolCallsFromDelta(value) {
112
116
  if (!value || typeof value !== "object")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,8 @@
29
29
  "dist/observation.js",
30
30
  "dist/provider-options.d.ts",
31
31
  "dist/provider-options.js",
32
+ "dist/retained-canonical-event.d.ts",
33
+ "dist/retained-canonical-event.js",
32
34
  "dist/retained-control.d.ts",
33
35
  "dist/retained-control.js",
34
36
  "dist/retained-environment.d.ts",