@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 +5 -0
- package/dist/retained-canonical-event.d.ts +11 -0
- package/dist/retained-canonical-event.js +135 -0
- package/dist/retained-stream.js +39 -0
- package/dist/wire.d.ts +1 -0
- package/dist/wire.js +5 -1
- package/package.json +3 -1
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
|
+
}
|
package/dist/retained-stream.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|