@tangle-network/agent-provider-cli-bridge 0.7.0 → 0.7.2
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 +3 -0
- package/dist/retained-execution.js +2 -2
- package/dist/retained-stream.d.ts +2 -1
- package/dist/retained-stream.js +154 -77
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,6 +49,9 @@ Passing the same `sessionId` on later turns continues the same CLI conversation.
|
|
|
49
49
|
`dispatch()` starts a bridge-owned durable run and returns after detaching its HTTP reader.
|
|
50
50
|
The returned `AgentSession` exposes status, cursor-based event replay, the terminal result, continuation, and cancellation.
|
|
51
51
|
Cancellation returns only after cli-bridge confirms the run is terminal.
|
|
52
|
+
A direct stream treats a finish frame as provisional until cli-bridge returns the same run's aggregate result.
|
|
53
|
+
A retained replay confirms its terminal state from the retained run endpoint.
|
|
54
|
+
The provider rejects a terminal response when its run id or request digest does not match the accepted stream.
|
|
52
55
|
Usage events and terminal result metadata include `modelRequests` only when the bridge measured an exact non-negative integer count.
|
|
53
56
|
Missing, malformed, partial, or estimated request counts remain unknown.
|
|
54
57
|
When `dispatch()` receives no `sessionId`, it creates one from that turn's stable run id and returns it.
|
|
@@ -27,7 +27,7 @@ export async function* streamTrackedCliBridgeTurn(options, environmentInput, pre
|
|
|
27
27
|
let drained = false;
|
|
28
28
|
let threw = false;
|
|
29
29
|
try {
|
|
30
|
-
for await (const event of streamCliBridgeTurn(options, turn, run.requestBody, transport, run.id, originalTurn.lastEventId, signal, (response) => captureCliBridgeRunIdentity(response, run,
|
|
30
|
+
for await (const event of streamCliBridgeTurn(options, turn, run.requestBody, transport, run.id, originalTurn.lastEventId, signal, (response) => captureCliBridgeRunIdentity(response, run, true), () => getCliBridgeRun(options, transport, run, 30_000, signal))) {
|
|
31
31
|
yield event;
|
|
32
32
|
}
|
|
33
33
|
drained = true;
|
|
@@ -171,7 +171,7 @@ export async function* streamCliBridgeSessionEvents(options, environmentInput, r
|
|
|
171
171
|
...(run.sessionId ? { sessionId: run.sessionId } : {}),
|
|
172
172
|
executionId: run.executionId,
|
|
173
173
|
turnId: run.turnId,
|
|
174
|
-
}, undefined, transport, run.id, eventOptions?.since ?? "0", signal, (response) => captureCliBridgeRunIdentity(response, run, false));
|
|
174
|
+
}, undefined, transport, run.id, eventOptions?.since ?? "0", signal, (response) => captureCliBridgeRunIdentity(response, run, false), () => getCliBridgeRun(options, transport, run, 30_000, signal));
|
|
175
175
|
drained = true;
|
|
176
176
|
if (runs.get(run.id) === run)
|
|
177
177
|
runs.delete(run.id);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { AgentEnvironmentEvent, AgentTurnInput } from "@tangle-network/agent-interface/environment-provider";
|
|
2
2
|
import type { CliBridgeProviderOptions } from "./provider-options.js";
|
|
3
|
+
import type { CliBridgeRunSnapshot } from "./retained-run-state.js";
|
|
3
4
|
import type { CliBridgeResponse, CliBridgeTransport } from "./transport.js";
|
|
4
5
|
export declare class CliBridgeRequestRejectedError extends Error {
|
|
5
6
|
readonly status: number;
|
|
6
7
|
constructor(status: number, detail: string);
|
|
7
8
|
}
|
|
8
|
-
export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal,
|
|
9
|
+
export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal, onResponse?: (response: CliBridgeResponse) => void, readAuthoritativeRun?: () => Promise<CliBridgeRunSnapshot | null>): AsyncIterable<AgentEnvironmentEvent>;
|
package/dist/retained-stream.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-event-cursor.js";
|
|
2
|
+
import { detachCliBridgeReader } from "./retained-control.js";
|
|
2
3
|
import { requestHeaders, trimSlash } from "./transport.js";
|
|
3
4
|
import { modelRequestsFromOpenAi, parseSse, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
|
|
4
5
|
export class CliBridgeRequestRejectedError extends Error {
|
|
@@ -17,7 +18,7 @@ class CliBridgeRunCancelledError extends Error {
|
|
|
17
18
|
this.name = "CliBridgeRunCancelledError";
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal,
|
|
21
|
+
export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun) {
|
|
21
22
|
const reconnect = requestBody === undefined;
|
|
22
23
|
const replay = retainedReplayRequest(lastEventId);
|
|
23
24
|
const response = await transport.fetch(reconnect
|
|
@@ -45,9 +46,20 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
45
46
|
}
|
|
46
47
|
throw new CliBridgeRequestRejectedError(response.status, detail);
|
|
47
48
|
}
|
|
48
|
-
onAccepted?.(response);
|
|
49
49
|
if (!response.body)
|
|
50
50
|
throw new Error("cli-bridge response body is empty");
|
|
51
|
+
try {
|
|
52
|
+
onResponse?.(response);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
try {
|
|
56
|
+
await detachCliBridgeReader(response.body);
|
|
57
|
+
}
|
|
58
|
+
catch (detachError) {
|
|
59
|
+
throw new AggregateError([error, detachError], `cli-bridge rejected response identity for run "${runId}" and could not detach its reader`);
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
51
63
|
let text = "";
|
|
52
64
|
const sessionId = turn.sessionId ?? runId;
|
|
53
65
|
const messageId = turn.turnId ?? `${sessionId}:assistant`;
|
|
@@ -59,10 +71,10 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
59
71
|
let servedModel;
|
|
60
72
|
let systemFingerprint;
|
|
61
73
|
let textBoundaryPending = false;
|
|
62
|
-
let terminalCursor;
|
|
63
74
|
let sawProtocolEnd = false;
|
|
64
75
|
let terminalFrameSeen = false;
|
|
65
76
|
let cancelledTerminalSeen = false;
|
|
77
|
+
let terminalFrame;
|
|
66
78
|
for await (const frame of parseSse(response.body)) {
|
|
67
79
|
if (frame.data === "[DONE]") {
|
|
68
80
|
sawProtocolEnd = true;
|
|
@@ -198,95 +210,160 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
|
|
|
198
210
|
throw new Error("cli-bridge returned finish_reason=error");
|
|
199
211
|
}
|
|
200
212
|
completed = true;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
frameEvents.push({
|
|
208
|
-
type: "result",
|
|
209
|
-
data: {
|
|
210
|
-
finalText: text,
|
|
211
|
-
finishReason: choice.finish_reason,
|
|
212
|
-
status: "completed",
|
|
213
|
-
...identityData,
|
|
214
|
-
...(modelRequestsKnown ? { modelRequests: observedModelRequests } : {}),
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
}
|
|
213
|
+
terminalFrame = {
|
|
214
|
+
...(frame.id === undefined ? {} : { id: frame.id }),
|
|
215
|
+
events: frameEvents,
|
|
216
|
+
finishReason: choice.finish_reason,
|
|
217
|
+
};
|
|
218
|
+
continue;
|
|
218
219
|
}
|
|
219
220
|
yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
|
|
220
221
|
}
|
|
221
222
|
if (!sawProtocolEnd) {
|
|
222
223
|
throw new Error("cli-bridge stream ended without the [DONE] protocol marker");
|
|
223
224
|
}
|
|
225
|
+
if (cancelledTerminalSeen)
|
|
226
|
+
return;
|
|
224
227
|
if (!completed && lastEventId === undefined) {
|
|
228
|
+
let snapshot;
|
|
229
|
+
try {
|
|
230
|
+
snapshot = await terminalRunSnapshot(runId, readAuthoritativeRun);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
throw new Error("cli-bridge stream ended without a terminal result", {
|
|
234
|
+
cause: error,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (snapshot.status === "cancelled") {
|
|
238
|
+
const event = cancelledEvent(`cli-bridge run "${runId}" was cancelled`);
|
|
239
|
+
yield {
|
|
240
|
+
...event,
|
|
241
|
+
data: {
|
|
242
|
+
...event.data,
|
|
243
|
+
runId,
|
|
244
|
+
sessionId,
|
|
245
|
+
...(turn.executionId === undefined
|
|
246
|
+
? {}
|
|
247
|
+
: { executionId: turn.executionId }),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
225
252
|
throw new Error("cli-bridge stream ended without a terminal result");
|
|
226
253
|
}
|
|
227
|
-
if (
|
|
254
|
+
if (!terminalFrame) {
|
|
255
|
+
if (requestBody !== undefined && lastEventId !== undefined) {
|
|
256
|
+
try {
|
|
257
|
+
await readFullCliBridgeResult(options, requestBody, transport, signal, onResponse);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (error instanceof CliBridgeRunCancelledError)
|
|
261
|
+
return;
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (requestBody === undefined) {
|
|
266
|
+
await terminalRunSnapshot(runId, readAuthoritativeRun);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let terminalEvent;
|
|
271
|
+
let terminalFailure;
|
|
272
|
+
if (requestBody !== undefined) {
|
|
228
273
|
let result;
|
|
229
274
|
try {
|
|
230
|
-
result = await readFullCliBridgeResult(options, requestBody, transport, signal,
|
|
275
|
+
result = await readFullCliBridgeResult(options, requestBody, transport, signal, onResponse);
|
|
231
276
|
}
|
|
232
277
|
catch (error) {
|
|
233
278
|
if (error instanceof CliBridgeRunCancelledError) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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;
|
|
279
|
+
terminalEvent = cancelledEvent(error.detail);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
throw error;
|
|
260
283
|
}
|
|
261
|
-
throw error;
|
|
262
284
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
...(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
if (result !== undefined) {
|
|
286
|
+
const finalModelRequests = result.modelRequests ??
|
|
287
|
+
(modelRequestsKnown ? observedModelRequests : undefined);
|
|
288
|
+
terminalEvent = {
|
|
289
|
+
type: "result",
|
|
290
|
+
data: {
|
|
291
|
+
finalText: lastEventId === undefined ? text : result.text,
|
|
292
|
+
finishReason: lastEventId === undefined ? terminalFrame.finishReason : result.finishReason,
|
|
293
|
+
status: "completed",
|
|
294
|
+
...(result.model === undefined ? {} : { model: result.model }),
|
|
295
|
+
...(result.system_fingerprint === undefined
|
|
296
|
+
? {}
|
|
297
|
+
: { system_fingerprint: result.system_fingerprint }),
|
|
298
|
+
...(finalModelRequests === undefined
|
|
299
|
+
? {}
|
|
300
|
+
: { modelRequests: finalModelRequests }),
|
|
301
|
+
},
|
|
302
|
+
...(result.usage && !sawUsage ? { usage: result.usage } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
const snapshot = await terminalRunSnapshot(runId, readAuthoritativeRun);
|
|
308
|
+
if (snapshot.status === "cancelled") {
|
|
309
|
+
terminalEvent = cancelledEvent(`cli-bridge run "${runId}" was cancelled`);
|
|
310
|
+
}
|
|
311
|
+
else if (snapshot.status === "error") {
|
|
312
|
+
const detail = `cli-bridge run "${runId}" ended with an error`;
|
|
313
|
+
terminalEvent = failedEvent(detail);
|
|
314
|
+
terminalFailure = new Error(detail);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
terminalEvent = {
|
|
318
|
+
type: "result",
|
|
319
|
+
data: {
|
|
320
|
+
finalText: text,
|
|
321
|
+
finishReason: terminalFrame.finishReason,
|
|
322
|
+
status: "completed",
|
|
323
|
+
...(servedModel === undefined ? {} : { model: servedModel }),
|
|
324
|
+
...(systemFingerprint === undefined
|
|
325
|
+
? {}
|
|
326
|
+
: { system_fingerprint: systemFingerprint }),
|
|
327
|
+
...(modelRequestsKnown ? { modelRequests: observedModelRequests } : {}),
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!terminalEvent) {
|
|
333
|
+
throw new Error("cli-bridge terminal state did not produce a terminal event");
|
|
334
|
+
}
|
|
335
|
+
terminalFrame.events.push(terminalEvent);
|
|
336
|
+
yield* retainedEventsWithIdentity(terminalFrame.events, terminalFrame.id, runId, sessionId, turn.executionId, replay.anchor);
|
|
337
|
+
if (terminalFailure)
|
|
338
|
+
throw terminalFailure;
|
|
339
|
+
}
|
|
340
|
+
async function terminalRunSnapshot(runId, readAuthoritativeRun) {
|
|
341
|
+
if (!readAuthoritativeRun) {
|
|
342
|
+
throw new Error("cli-bridge retained stream omitted authoritative run status");
|
|
343
|
+
}
|
|
344
|
+
const snapshot = await readAuthoritativeRun();
|
|
345
|
+
if (!snapshot)
|
|
346
|
+
throw new Error(`cli-bridge lost run "${runId}" before terminal replay`);
|
|
347
|
+
if (!snapshot.terminal || snapshot.status === "running") {
|
|
348
|
+
throw new Error(`cli-bridge run "${runId}" remained active after its stream ended`);
|
|
287
349
|
}
|
|
350
|
+
return snapshot;
|
|
351
|
+
}
|
|
352
|
+
function cancelledEvent(detail) {
|
|
353
|
+
return {
|
|
354
|
+
type: "status",
|
|
355
|
+
data: { status: "cancelled", error: detail },
|
|
356
|
+
normalized: { type: "status", status: "cancelled", detail },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function failedEvent(detail) {
|
|
360
|
+
return {
|
|
361
|
+
type: "status",
|
|
362
|
+
data: { status: "failed", error: detail },
|
|
363
|
+
normalized: { type: "status", status: "failed", detail },
|
|
364
|
+
};
|
|
288
365
|
}
|
|
289
|
-
async function readFullCliBridgeResult(options, requestBody, transport, signal,
|
|
366
|
+
async function readFullCliBridgeResult(options, requestBody, transport, signal, onResponse) {
|
|
290
367
|
const body = safeJson(requestBody);
|
|
291
368
|
if (!body)
|
|
292
369
|
throw new Error("cli-bridge replay request is not valid JSON");
|
|
@@ -296,16 +373,16 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal,
|
|
|
296
373
|
body: JSON.stringify({ ...body, stream: false }),
|
|
297
374
|
...(signal ? { signal } : {}),
|
|
298
375
|
});
|
|
376
|
+
const responseText = await response.text();
|
|
377
|
+
onResponse?.(response);
|
|
299
378
|
if (!response.ok) {
|
|
300
|
-
const responseText = await response.text();
|
|
301
379
|
const error = cliBridgeError(safeJson(responseText));
|
|
302
380
|
if (error?.type === "run_cancelled") {
|
|
303
381
|
throw new CliBridgeRunCancelledError(cliBridgeErrorMessage(error));
|
|
304
382
|
}
|
|
305
383
|
throw new Error(`cli-bridge replay result ${response.status}: ${responseText}`);
|
|
306
384
|
}
|
|
307
|
-
|
|
308
|
-
const parsed = safeJson(await response.text());
|
|
385
|
+
const parsed = safeJson(responseText);
|
|
309
386
|
const error = cliBridgeError(parsed);
|
|
310
387
|
if (error) {
|
|
311
388
|
if (error.type === "run_cancelled") {
|
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.2",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"LICENSE"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@tangle-network/agent-interface": "0.
|
|
52
|
+
"@tangle-network/agent-interface": "0.53.0",
|
|
53
53
|
"undici": "7.29.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|