@tangle-network/agent-provider-cli-bridge 0.5.0 → 0.6.1

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.
@@ -0,0 +1,304 @@
1
+ import { cancelCliBridgeRun, captureCliBridgeRunIdentity, detachCliBridgeReader, getCliBridgeRun, } from "./retained-control.js";
2
+ import { bindCliBridgeRun, bindCliBridgeSession, exactControlRefForRun, restoreCliBridgeRun, restoreCliBridgeSession, } from "./retained-run-state.js";
3
+ import { CliBridgeRequestRejectedError, streamCliBridgeTurn, } from "./retained-stream.js";
4
+ import { requestHeaders, trimSlash } from "./transport.js";
5
+ import { modelRequestCount } from "./wire.js";
6
+ export async function* streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers) {
7
+ const originalTurn = prepared.turn;
8
+ if (originalTurn.detach) {
9
+ throw new Error("cli-bridge provider does not support detached turns");
10
+ }
11
+ const controller = new AbortController();
12
+ const signals = [
13
+ originalTurn.signal,
14
+ environmentInput.signal,
15
+ controller.signal,
16
+ ].filter((signal) => signal !== undefined);
17
+ const signal = signals.length > 0 ? AbortSignal.any(signals) : undefined;
18
+ const turn = {
19
+ ...originalTurn,
20
+ ...(signal ? { signal } : {}),
21
+ };
22
+ const run = prepared.run;
23
+ const previousRun = bindCliBridgeRun(run, runs);
24
+ const previousSessionRun = bindCliBridgeSession(run, sessions);
25
+ run.readers.add(controller);
26
+ readers.add(controller);
27
+ let drained = false;
28
+ let threw = false;
29
+ try {
30
+ for await (const event of streamCliBridgeTurn(options, turn, run.requestBody, transport, run.id, originalTurn.lastEventId, signal, (response) => captureCliBridgeRunIdentity(response, run, false))) {
31
+ yield event;
32
+ }
33
+ drained = true;
34
+ if (runs.get(run.id) === run)
35
+ runs.delete(run.id);
36
+ }
37
+ catch (error) {
38
+ threw = true;
39
+ if (error instanceof CliBridgeRequestRejectedError) {
40
+ restoreCliBridgeRun(run, previousRun, runs);
41
+ restoreCliBridgeSession(run, previousSessionRun, sessions);
42
+ throw error;
43
+ }
44
+ let snapshot;
45
+ try {
46
+ snapshot = await getCliBridgeRun(options, transport, run);
47
+ }
48
+ catch {
49
+ snapshot = undefined;
50
+ }
51
+ if (snapshot?.terminal) {
52
+ if (runs.get(run.id) === run)
53
+ runs.delete(run.id);
54
+ }
55
+ else if (originalTurn.signal?.aborted || environmentInput.signal?.aborted) {
56
+ await cancelCliBridgeRun(options, transport, run);
57
+ if (runs.get(run.id) === run)
58
+ runs.delete(run.id);
59
+ }
60
+ throw error;
61
+ }
62
+ finally {
63
+ if (!drained && !threw && runs.get(run.id) === run) {
64
+ await cancelCliBridgeRun(options, transport, run);
65
+ runs.delete(run.id);
66
+ }
67
+ run.readers.delete(controller);
68
+ readers.delete(controller);
69
+ }
70
+ }
71
+ export async function dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions) {
72
+ const run = prepared.run;
73
+ const previousRun = bindCliBridgeRun(run, runs);
74
+ const previousSessionRun = bindCliBridgeSession(run, sessions);
75
+ const signals = [
76
+ prepared.turn.signal,
77
+ environmentInput.signal,
78
+ ].filter((signal) => signal !== undefined);
79
+ const signal = signals.length > 0 ? AbortSignal.any(signals) : undefined;
80
+ let accepted = false;
81
+ let responseBody;
82
+ try {
83
+ const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
84
+ method: "POST",
85
+ headers: {
86
+ ...requestHeaders(options),
87
+ accept: "text/event-stream",
88
+ ...(run.sessionId ? { "x-session-id": run.sessionId } : {}),
89
+ },
90
+ body: run.requestBody,
91
+ ...(signal ? { signal } : {}),
92
+ });
93
+ if (!response.ok) {
94
+ let detail = "request rejected";
95
+ try {
96
+ detail = await response.text();
97
+ }
98
+ catch {
99
+ // The HTTP status already proves this request was rejected.
100
+ }
101
+ throw new CliBridgeRequestRejectedError(response.status, detail);
102
+ }
103
+ accepted = true;
104
+ if (!response.body)
105
+ throw new Error("cli-bridge response body is empty");
106
+ responseBody = response.body;
107
+ captureCliBridgeRunIdentity(response, run, true);
108
+ const controlRef = exactControlRefForRun(run, providerName);
109
+ run.controlRef = controlRef;
110
+ await detachCliBridgeReader(responseBody);
111
+ responseBody = undefined;
112
+ return {
113
+ id: run.sessionId,
114
+ provider: providerName,
115
+ controlRef,
116
+ metadata: {
117
+ runId: run.id,
118
+ requestDigest: run.requestDigest,
119
+ },
120
+ };
121
+ }
122
+ catch (error) {
123
+ let failure = error;
124
+ if (responseBody) {
125
+ try {
126
+ await detachCliBridgeReader(responseBody);
127
+ }
128
+ catch (detachError) {
129
+ failure = new AggregateError([failure, detachError], `cli-bridge dispatch "${run.id}" failed and its reader did not detach`);
130
+ }
131
+ }
132
+ if (failure instanceof CliBridgeRequestRejectedError) {
133
+ restoreCliBridgeRun(run, previousRun, runs);
134
+ restoreCliBridgeSession(run, previousSessionRun, sessions);
135
+ throw failure;
136
+ }
137
+ if (accepted || signal?.aborted) {
138
+ try {
139
+ await cancelCliBridgeRun(options, transport, run);
140
+ if (runs.get(run.id) === run)
141
+ runs.delete(run.id);
142
+ }
143
+ catch (cancellationError) {
144
+ throw new AggregateError([failure, cancellationError], `cli-bridge dispatch "${run.id}" failed and cancellation was not confirmed`);
145
+ }
146
+ }
147
+ else {
148
+ restoreCliBridgeRun(run, previousRun, runs);
149
+ }
150
+ restoreCliBridgeSession(run, previousSessionRun, sessions);
151
+ throw failure;
152
+ }
153
+ }
154
+ export async function* streamCliBridgeSessionEvents(options, environmentInput, run, transport, runs, readers, eventOptions) {
155
+ if (eventOptions?.executionId !== undefined &&
156
+ eventOptions.executionId !== run.executionId) {
157
+ throw new Error("cli-bridge event request targets another retained execution");
158
+ }
159
+ const controller = new AbortController();
160
+ const signals = [
161
+ eventOptions?.signal,
162
+ environmentInput.signal,
163
+ controller.signal,
164
+ ].filter((signal) => signal !== undefined);
165
+ const signal = signals.length > 0 ? AbortSignal.any(signals) : undefined;
166
+ run.readers.add(controller);
167
+ readers.add(controller);
168
+ let drained = false;
169
+ try {
170
+ yield* streamCliBridgeTurn(options, {
171
+ ...(run.sessionId ? { sessionId: run.sessionId } : {}),
172
+ executionId: run.executionId,
173
+ turnId: run.turnId,
174
+ }, undefined, transport, run.id, eventOptions?.since ?? "0", signal, (response) => captureCliBridgeRunIdentity(response, run, false));
175
+ drained = true;
176
+ if (runs.get(run.id) === run)
177
+ runs.delete(run.id);
178
+ }
179
+ finally {
180
+ if (!drained) {
181
+ controller.abort(new DOMException("cli-bridge session event reader detached", "AbortError"));
182
+ }
183
+ run.readers.delete(controller);
184
+ readers.delete(controller);
185
+ }
186
+ }
187
+ export async function collectCliBridgeTurnResult(source, run, options, transport, signal) {
188
+ const events = [];
189
+ let text = "";
190
+ let usage;
191
+ let observedModelRequests = 0;
192
+ let modelRequestsKnown = false;
193
+ let terminalModelRequests;
194
+ let servedModel;
195
+ let systemFingerprint;
196
+ let streamError;
197
+ try {
198
+ for await (const event of source) {
199
+ events.push(event);
200
+ const finalText = event.data.finalText;
201
+ if (typeof finalText === "string") {
202
+ text = finalText;
203
+ }
204
+ else if (typeof event.data.delta === "string") {
205
+ text += event.data.delta;
206
+ }
207
+ usage = addTokenUsage(usage, event.usage);
208
+ const eventModel = event.data.model;
209
+ const eventFingerprint = event.data.system_fingerprint;
210
+ if (typeof eventModel === "string" && eventModel.length > 0)
211
+ servedModel = eventModel;
212
+ if (typeof eventFingerprint === "string" && eventFingerprint.length > 0) {
213
+ systemFingerprint = eventFingerprint;
214
+ }
215
+ const modelRequests = modelRequestCount(event.data.modelRequests);
216
+ if (modelRequests !== undefined) {
217
+ if (event.type === "result") {
218
+ terminalModelRequests = modelRequests;
219
+ }
220
+ else if (event.type === "usage") {
221
+ observedModelRequests += modelRequests;
222
+ modelRequestsKnown = true;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ catch (error) {
228
+ streamError = error;
229
+ }
230
+ const snapshot = await getCliBridgeRun(options, transport, run, undefined, signal);
231
+ if (!snapshot) {
232
+ if (streamError)
233
+ throw streamError;
234
+ throw new Error(`cli-bridge lost run "${run.id}" before reading its result`);
235
+ }
236
+ if (!snapshot.terminal) {
237
+ if (streamError)
238
+ throw streamError;
239
+ throw new Error(`cli-bridge run "${run.id}" has no terminal result`);
240
+ }
241
+ const success = snapshot.status === "done" && streamError === undefined;
242
+ return {
243
+ text,
244
+ success,
245
+ ...(!success
246
+ ? {
247
+ error: streamError instanceof Error
248
+ ? streamError.message
249
+ : `cli-bridge run ended ${snapshot.status}`,
250
+ }
251
+ : {}),
252
+ ...(run.sessionId ? { sessionId: run.sessionId } : {}),
253
+ ...(usage ? { usage } : {}),
254
+ metadata: {
255
+ runId: run.id,
256
+ executionId: run.executionId,
257
+ status: snapshot.status,
258
+ ...(run.requestDigest ? { requestDigest: run.requestDigest } : {}),
259
+ ...(terminalModelRequests !== undefined
260
+ ? { modelRequests: terminalModelRequests }
261
+ : modelRequestsKnown
262
+ ? { modelRequests: observedModelRequests }
263
+ : {}),
264
+ ...(servedModel === undefined ? {} : { model: servedModel }),
265
+ ...(systemFingerprint === undefined ? {} : { system_fingerprint: systemFingerprint }),
266
+ },
267
+ events,
268
+ };
269
+ }
270
+ function addTokenUsage(current, next) {
271
+ if (!next)
272
+ return current;
273
+ if (!current)
274
+ return { ...next };
275
+ return {
276
+ inputTokens: current.inputTokens + next.inputTokens,
277
+ outputTokens: current.outputTokens + next.outputTokens,
278
+ ...(current.totalTokens !== undefined || next.totalTokens !== undefined
279
+ ? { totalTokens: (current.totalTokens ?? 0) + (next.totalTokens ?? 0) }
280
+ : {}),
281
+ ...(current.cacheReadInputTokens !== undefined ||
282
+ next.cacheReadInputTokens !== undefined
283
+ ? {
284
+ cacheReadInputTokens: (current.cacheReadInputTokens ?? 0) +
285
+ (next.cacheReadInputTokens ?? 0),
286
+ }
287
+ : {}),
288
+ ...(current.cacheCreationInputTokens !== undefined ||
289
+ next.cacheCreationInputTokens !== undefined
290
+ ? {
291
+ cacheCreationInputTokens: (current.cacheCreationInputTokens ?? 0) +
292
+ (next.cacheCreationInputTokens ?? 0),
293
+ }
294
+ : {}),
295
+ ...(current.reasoningTokens !== undefined || next.reasoningTokens !== undefined
296
+ ? {
297
+ reasoningTokens: (current.reasoningTokens ?? 0) + (next.reasoningTokens ?? 0),
298
+ }
299
+ : {}),
300
+ ...(current.cost !== undefined || next.cost !== undefined
301
+ ? { cost: (current.cost ?? 0) + (next.cost ?? 0) }
302
+ : {}),
303
+ };
304
+ }
@@ -0,0 +1,38 @@
1
+ import { type AgentExactRunControlRef, type AgentRunControlRef, type Sha256Digest } from "@tangle-network/agent-interface";
2
+ import type { AgentTurnInput, CreateAgentEnvironmentInput } from "@tangle-network/agent-interface/environment-provider";
3
+ import type { CliBridgeProviderOptions } from "./provider-options.js";
4
+ export interface CliBridgeRun {
5
+ readonly id: string;
6
+ readonly environmentId: string;
7
+ readonly sessionId?: string;
8
+ readonly executionId: string;
9
+ readonly turnId: string;
10
+ readonly requestBody: string;
11
+ readonly readers: Set<AbortController>;
12
+ requestDigest?: Sha256Digest;
13
+ controlRef?: AgentExactRunControlRef;
14
+ cancellation?: Promise<CliBridgeRunSnapshot>;
15
+ }
16
+ export interface CliBridgeSessionState {
17
+ readonly id: string;
18
+ current: CliBridgeRun;
19
+ }
20
+ export interface CliBridgeRunSnapshot {
21
+ readonly id: string;
22
+ readonly requestDigest?: Sha256Digest;
23
+ readonly status: "running" | "done" | "error" | "cancelled";
24
+ readonly terminal: boolean;
25
+ }
26
+ export interface PreparedCliBridgeRun {
27
+ readonly run: CliBridgeRun;
28
+ readonly turn: AgentTurnInput;
29
+ }
30
+ export declare function exactControlRefForRun(run: CliBridgeRun, providerName: string): AgentExactRunControlRef;
31
+ export declare function exactControlRefForSession(value: AgentRunControlRef, providerName: string, environmentId: string, sessionId: string): AgentExactRunControlRef;
32
+ export declare function runFromControlRef(controlRef: AgentExactRunControlRef): CliBridgeRun;
33
+ export declare function assertRunMatchesControlRef(run: CliBridgeRun, controlRef: AgentExactRunControlRef): void;
34
+ export declare function prepareCliBridgeRun(options: CliBridgeProviderOptions, environmentInput: CreateAgentEnvironmentInput, originalTurn: AgentTurnInput, environmentId: string, requireSession: boolean): PreparedCliBridgeRun;
35
+ export declare function bindCliBridgeRun(prepared: CliBridgeRun, runs: Map<string, CliBridgeRun>): CliBridgeRun | undefined;
36
+ export declare function restoreCliBridgeRun(run: CliBridgeRun, previous: CliBridgeRun | undefined, runs: Map<string, CliBridgeRun>): void;
37
+ export declare function bindCliBridgeSession(run: CliBridgeRun, sessions: Map<string, CliBridgeSessionState>): CliBridgeRun | undefined;
38
+ export declare function restoreCliBridgeSession(run: CliBridgeRun, previous: CliBridgeRun | undefined, sessions: Map<string, CliBridgeSessionState>): void;
@@ -0,0 +1,97 @@
1
+ import { AgentExactRunControlRefSchema, } from "@tangle-network/agent-interface";
2
+ import { cliBridgeRunId, toChatCompletionsBody } from "./wire.js";
3
+ export function exactControlRefForRun(run, providerName) {
4
+ if (!run.sessionId || !run.requestDigest) {
5
+ throw new Error("cli-bridge did not return complete retained run coordinates");
6
+ }
7
+ return Object.freeze(AgentExactRunControlRefSchema.parse({
8
+ runId: run.id,
9
+ provider: providerName,
10
+ environmentId: run.environmentId,
11
+ sessionId: run.sessionId,
12
+ executionId: run.executionId,
13
+ requestDigest: run.requestDigest,
14
+ }));
15
+ }
16
+ export function exactControlRefForSession(value, providerName, environmentId, sessionId) {
17
+ const controlRef = Object.freeze(AgentExactRunControlRefSchema.parse(value));
18
+ if (controlRef.provider !== providerName ||
19
+ controlRef.environmentId !== environmentId ||
20
+ controlRef.sessionId !== sessionId) {
21
+ throw new Error("cli-bridge control reference does not bind to this session");
22
+ }
23
+ return controlRef;
24
+ }
25
+ export function runFromControlRef(controlRef) {
26
+ return {
27
+ id: controlRef.runId,
28
+ environmentId: controlRef.environmentId,
29
+ sessionId: controlRef.sessionId,
30
+ executionId: controlRef.executionId,
31
+ turnId: controlRef.executionId,
32
+ requestBody: "",
33
+ readers: new Set(),
34
+ requestDigest: controlRef.requestDigest,
35
+ controlRef,
36
+ };
37
+ }
38
+ export function assertRunMatchesControlRef(run, controlRef) {
39
+ if (run.id !== controlRef.runId ||
40
+ run.environmentId !== controlRef.environmentId ||
41
+ run.sessionId !== controlRef.sessionId ||
42
+ run.executionId !== controlRef.executionId ||
43
+ run.requestDigest !== controlRef.requestDigest) {
44
+ throw new Error("cli-bridge control reference conflicts with the retained run");
45
+ }
46
+ run.controlRef = controlRef;
47
+ }
48
+ export function prepareCliBridgeRun(options, environmentInput, originalTurn, environmentId, requireSession) {
49
+ const turnId = originalTurn.turnId ?? crypto.randomUUID();
50
+ const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
51
+ const sessionId = originalTurn.sessionId ?? (requireSession ? runId : undefined);
52
+ const turn = {
53
+ ...originalTurn,
54
+ turnId,
55
+ ...(sessionId ? { sessionId } : {}),
56
+ };
57
+ return {
58
+ turn,
59
+ run: {
60
+ id: runId,
61
+ environmentId,
62
+ ...(sessionId ? { sessionId } : {}),
63
+ executionId: originalTurn.executionId ?? runId,
64
+ turnId,
65
+ requestBody: JSON.stringify(toChatCompletionsBody(options, environmentInput, turn, runId)),
66
+ readers: new Set(),
67
+ },
68
+ };
69
+ }
70
+ export function bindCliBridgeRun(prepared, runs) {
71
+ const previous = runs.get(prepared.id);
72
+ runs.set(prepared.id, prepared);
73
+ return previous;
74
+ }
75
+ export function restoreCliBridgeRun(run, previous, runs) {
76
+ if (runs.get(run.id) !== run)
77
+ return;
78
+ if (previous)
79
+ runs.set(run.id, previous);
80
+ else
81
+ runs.delete(run.id);
82
+ }
83
+ export function bindCliBridgeSession(run, sessions) {
84
+ if (!run.sessionId)
85
+ return undefined;
86
+ const previous = sessions.get(run.sessionId)?.current;
87
+ sessions.set(run.sessionId, { id: run.sessionId, current: run });
88
+ return previous;
89
+ }
90
+ export function restoreCliBridgeSession(run, previous, sessions) {
91
+ if (!run.sessionId || sessions.get(run.sessionId)?.current !== run)
92
+ return;
93
+ if (previous)
94
+ sessions.set(run.sessionId, { id: run.sessionId, current: previous });
95
+ else
96
+ sessions.delete(run.sessionId);
97
+ }
@@ -0,0 +1,8 @@
1
+ import type { AgentEnvironmentEvent, AgentTurnInput } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { CliBridgeProviderOptions } from "./provider-options.js";
3
+ import type { CliBridgeResponse, CliBridgeTransport } from "./transport.js";
4
+ export declare class CliBridgeRequestRejectedError extends Error {
5
+ readonly status: number;
6
+ constructor(status: number, detail: string);
7
+ }
8
+ export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal, onAccepted?: (response: CliBridgeResponse) => void): AsyncIterable<AgentEnvironmentEvent>;