@tangle-network/agent-provider-cli-bridge 0.5.0 → 0.6.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 +8 -0
- package/dist/index.d.ts +5 -27
- package/dist/index.js +38 -998
- package/dist/provider-options.d.ts +26 -0
- package/dist/provider-options.js +10 -0
- package/dist/retained-control.d.ts +11 -0
- package/dist/retained-control.js +170 -0
- package/dist/retained-environment.d.ts +11 -0
- package/dist/retained-environment.js +189 -0
- package/dist/retained-event-cursor.d.ts +18 -0
- package/dist/retained-event-cursor.js +68 -0
- package/dist/retained-execution.d.ts +12 -0
- package/dist/retained-execution.js +293 -0
- package/dist/retained-run-state.d.ts +38 -0
- package/dist/retained-run-state.js +97 -0
- package/dist/retained-stream.d.ts +8 -0
- package/dist/retained-stream.js +251 -0
- package/dist/transport.d.ts +23 -0
- package/dist/transport.js +32 -0
- package/dist/wire.d.ts +22 -0
- package/dist/wire.js +226 -0
- package/package.json +19 -1
|
@@ -0,0 +1,293 @@
|
|
|
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 streamError;
|
|
195
|
+
try {
|
|
196
|
+
for await (const event of source) {
|
|
197
|
+
events.push(event);
|
|
198
|
+
const finalText = event.data.finalText;
|
|
199
|
+
if (typeof finalText === "string") {
|
|
200
|
+
text = finalText;
|
|
201
|
+
}
|
|
202
|
+
else if (typeof event.data.delta === "string") {
|
|
203
|
+
text += event.data.delta;
|
|
204
|
+
}
|
|
205
|
+
usage = addTokenUsage(usage, event.usage);
|
|
206
|
+
const modelRequests = modelRequestCount(event.data.modelRequests);
|
|
207
|
+
if (modelRequests !== undefined) {
|
|
208
|
+
if (event.type === "result") {
|
|
209
|
+
terminalModelRequests = modelRequests;
|
|
210
|
+
}
|
|
211
|
+
else if (event.type === "usage") {
|
|
212
|
+
observedModelRequests += modelRequests;
|
|
213
|
+
modelRequestsKnown = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
streamError = error;
|
|
220
|
+
}
|
|
221
|
+
const snapshot = await getCliBridgeRun(options, transport, run, undefined, signal);
|
|
222
|
+
if (!snapshot) {
|
|
223
|
+
if (streamError)
|
|
224
|
+
throw streamError;
|
|
225
|
+
throw new Error(`cli-bridge lost run "${run.id}" before reading its result`);
|
|
226
|
+
}
|
|
227
|
+
if (!snapshot.terminal) {
|
|
228
|
+
if (streamError)
|
|
229
|
+
throw streamError;
|
|
230
|
+
throw new Error(`cli-bridge run "${run.id}" has no terminal result`);
|
|
231
|
+
}
|
|
232
|
+
const success = snapshot.status === "done" && streamError === undefined;
|
|
233
|
+
return {
|
|
234
|
+
text,
|
|
235
|
+
success,
|
|
236
|
+
...(!success
|
|
237
|
+
? {
|
|
238
|
+
error: streamError instanceof Error
|
|
239
|
+
? streamError.message
|
|
240
|
+
: `cli-bridge run ended ${snapshot.status}`,
|
|
241
|
+
}
|
|
242
|
+
: {}),
|
|
243
|
+
...(run.sessionId ? { sessionId: run.sessionId } : {}),
|
|
244
|
+
...(usage ? { usage } : {}),
|
|
245
|
+
metadata: {
|
|
246
|
+
runId: run.id,
|
|
247
|
+
executionId: run.executionId,
|
|
248
|
+
status: snapshot.status,
|
|
249
|
+
...(run.requestDigest ? { requestDigest: run.requestDigest } : {}),
|
|
250
|
+
...(terminalModelRequests !== undefined
|
|
251
|
+
? { modelRequests: terminalModelRequests }
|
|
252
|
+
: modelRequestsKnown
|
|
253
|
+
? { modelRequests: observedModelRequests }
|
|
254
|
+
: {}),
|
|
255
|
+
},
|
|
256
|
+
events,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function addTokenUsage(current, next) {
|
|
260
|
+
if (!next)
|
|
261
|
+
return current;
|
|
262
|
+
if (!current)
|
|
263
|
+
return { ...next };
|
|
264
|
+
return {
|
|
265
|
+
inputTokens: current.inputTokens + next.inputTokens,
|
|
266
|
+
outputTokens: current.outputTokens + next.outputTokens,
|
|
267
|
+
...(current.totalTokens !== undefined || next.totalTokens !== undefined
|
|
268
|
+
? { totalTokens: (current.totalTokens ?? 0) + (next.totalTokens ?? 0) }
|
|
269
|
+
: {}),
|
|
270
|
+
...(current.cacheReadInputTokens !== undefined ||
|
|
271
|
+
next.cacheReadInputTokens !== undefined
|
|
272
|
+
? {
|
|
273
|
+
cacheReadInputTokens: (current.cacheReadInputTokens ?? 0) +
|
|
274
|
+
(next.cacheReadInputTokens ?? 0),
|
|
275
|
+
}
|
|
276
|
+
: {}),
|
|
277
|
+
...(current.cacheCreationInputTokens !== undefined ||
|
|
278
|
+
next.cacheCreationInputTokens !== undefined
|
|
279
|
+
? {
|
|
280
|
+
cacheCreationInputTokens: (current.cacheCreationInputTokens ?? 0) +
|
|
281
|
+
(next.cacheCreationInputTokens ?? 0),
|
|
282
|
+
}
|
|
283
|
+
: {}),
|
|
284
|
+
...(current.reasoningTokens !== undefined || next.reasoningTokens !== undefined
|
|
285
|
+
? {
|
|
286
|
+
reasoningTokens: (current.reasoningTokens ?? 0) + (next.reasoningTokens ?? 0),
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
...(current.cost !== undefined || next.cost !== undefined
|
|
290
|
+
? { cost: (current.cost ?? 0) + (next.cost ?? 0) }
|
|
291
|
+
: {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-event-cursor.js";
|
|
2
|
+
import { requestHeaders, trimSlash } from "./transport.js";
|
|
3
|
+
import { modelRequestsFromOpenAi, parseSse, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
|
|
4
|
+
export class CliBridgeRequestRejectedError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
constructor(status, detail) {
|
|
7
|
+
super(`cli-bridge ${status}: ${detail}`);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = "CliBridgeRequestRejectedError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onAccepted) {
|
|
13
|
+
const reconnect = requestBody === undefined;
|
|
14
|
+
const replay = retainedReplayRequest(lastEventId);
|
|
15
|
+
const response = await transport.fetch(reconnect
|
|
16
|
+
? `${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(runId)}/events`
|
|
17
|
+
: `${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
18
|
+
method: reconnect ? "GET" : "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
...requestHeaders(options),
|
|
21
|
+
accept: "text/event-stream",
|
|
22
|
+
...(!reconnect && turn.sessionId ? { "x-session-id": turn.sessionId } : {}),
|
|
23
|
+
...(replay.serverCursor === undefined
|
|
24
|
+
? {}
|
|
25
|
+
: { "last-event-id": replay.serverCursor }),
|
|
26
|
+
},
|
|
27
|
+
...(requestBody === undefined ? {} : { body: requestBody }),
|
|
28
|
+
...(signal ? { signal } : {}),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
let detail = "request rejected";
|
|
32
|
+
try {
|
|
33
|
+
detail = await response.text();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// The HTTP status already proves this request was rejected.
|
|
37
|
+
}
|
|
38
|
+
throw new CliBridgeRequestRejectedError(response.status, detail);
|
|
39
|
+
}
|
|
40
|
+
onAccepted?.(response);
|
|
41
|
+
if (!response.body)
|
|
42
|
+
throw new Error("cli-bridge response body is empty");
|
|
43
|
+
let text = "";
|
|
44
|
+
const sessionId = turn.sessionId ?? runId;
|
|
45
|
+
const messageId = turn.turnId ?? `${sessionId}:assistant`;
|
|
46
|
+
const emittedToolCalls = new Set();
|
|
47
|
+
let completed = false;
|
|
48
|
+
let sawUsage = false;
|
|
49
|
+
let observedModelRequests = 0;
|
|
50
|
+
let modelRequestsKnown = false;
|
|
51
|
+
let textBoundaryPending = false;
|
|
52
|
+
let terminalCursor;
|
|
53
|
+
let sawProtocolEnd = false;
|
|
54
|
+
for await (const frame of parseSse(response.body)) {
|
|
55
|
+
if (frame.data === "[DONE]") {
|
|
56
|
+
sawProtocolEnd = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const parsed = safeJson(frame.data);
|
|
60
|
+
if (!parsed)
|
|
61
|
+
continue;
|
|
62
|
+
if (parsed.error && typeof parsed.error === "object") {
|
|
63
|
+
const error = parsed.error;
|
|
64
|
+
const message = typeof error.message === "string" ? error.message : "cli-bridge error";
|
|
65
|
+
yield* retainedEventsWithIdentity([{
|
|
66
|
+
type: "status",
|
|
67
|
+
data: { status: "failed", error: message },
|
|
68
|
+
normalized: { type: "status", status: "failed", detail: message },
|
|
69
|
+
}], frame.id, runId, sessionId, turn.executionId, replay.anchor);
|
|
70
|
+
throw new Error(`cli-bridge: ${message}`);
|
|
71
|
+
}
|
|
72
|
+
const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
|
|
73
|
+
const delta = choice?.delta;
|
|
74
|
+
const toolCallDeltas = toolCallsFromDelta(delta);
|
|
75
|
+
const rawChunk = delta && typeof delta.content === "string" ? delta.content : "";
|
|
76
|
+
const chunk = rawChunk &&
|
|
77
|
+
textBoundaryPending &&
|
|
78
|
+
toolCallDeltas.length === 0 &&
|
|
79
|
+
text.length > 0 &&
|
|
80
|
+
!/\s$/u.test(text) &&
|
|
81
|
+
!/^\s/u.test(rawChunk)
|
|
82
|
+
? `\n\n${rawChunk}`
|
|
83
|
+
: rawChunk;
|
|
84
|
+
if (rawChunk && toolCallDeltas.length === 0)
|
|
85
|
+
textBoundaryPending = false;
|
|
86
|
+
const nextUsage = usageFromOpenAi(parsed.usage);
|
|
87
|
+
const nextModelRequests = modelRequestsFromOpenAi(parsed.usage);
|
|
88
|
+
const frameEvents = [];
|
|
89
|
+
if (nextUsage || nextModelRequests !== undefined) {
|
|
90
|
+
if (nextUsage)
|
|
91
|
+
sawUsage = true;
|
|
92
|
+
if (nextModelRequests !== undefined) {
|
|
93
|
+
observedModelRequests += nextModelRequests;
|
|
94
|
+
modelRequestsKnown = true;
|
|
95
|
+
}
|
|
96
|
+
frameEvents.push({
|
|
97
|
+
type: "usage",
|
|
98
|
+
data: nextModelRequests === undefined ? {} : { modelRequests: nextModelRequests },
|
|
99
|
+
...(nextUsage ? { usage: nextUsage } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (chunk) {
|
|
103
|
+
text += chunk;
|
|
104
|
+
const part = {
|
|
105
|
+
id: `${messageId}:text`,
|
|
106
|
+
sessionID: sessionId,
|
|
107
|
+
messageID: messageId,
|
|
108
|
+
type: "text",
|
|
109
|
+
text,
|
|
110
|
+
};
|
|
111
|
+
const normalized = {
|
|
112
|
+
type: "message.part.updated",
|
|
113
|
+
part,
|
|
114
|
+
delta: chunk,
|
|
115
|
+
};
|
|
116
|
+
frameEvents.push({
|
|
117
|
+
type: "message.part.updated",
|
|
118
|
+
data: { part, delta: chunk },
|
|
119
|
+
normalized,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
for (const toolCall of toolCallDeltas) {
|
|
123
|
+
const callId = toolCall.id ?? `${messageId}:tool:${toolCall.index}`;
|
|
124
|
+
if (!toolCall.name || emittedToolCalls.has(callId))
|
|
125
|
+
continue;
|
|
126
|
+
emittedToolCalls.add(callId);
|
|
127
|
+
const part = {
|
|
128
|
+
id: callId,
|
|
129
|
+
sessionID: sessionId,
|
|
130
|
+
messageID: messageId,
|
|
131
|
+
type: "tool",
|
|
132
|
+
callID: callId,
|
|
133
|
+
tool: toolCall.name,
|
|
134
|
+
state: { status: "pending", input: {} },
|
|
135
|
+
};
|
|
136
|
+
const normalized = {
|
|
137
|
+
type: "message.part.updated",
|
|
138
|
+
part,
|
|
139
|
+
};
|
|
140
|
+
frameEvents.push({
|
|
141
|
+
type: "message.part.updated",
|
|
142
|
+
data: { part },
|
|
143
|
+
normalized,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (toolCallDeltas.length > 0)
|
|
147
|
+
textBoundaryPending = true;
|
|
148
|
+
if (choice?.finish_reason) {
|
|
149
|
+
if (choice.finish_reason === "error") {
|
|
150
|
+
frameEvents.push({
|
|
151
|
+
type: "status",
|
|
152
|
+
data: { status: "failed", error: "cli-bridge returned finish_reason=error" },
|
|
153
|
+
normalized: {
|
|
154
|
+
type: "status",
|
|
155
|
+
status: "failed",
|
|
156
|
+
detail: "cli-bridge returned finish_reason=error",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
|
|
160
|
+
throw new Error("cli-bridge returned finish_reason=error");
|
|
161
|
+
}
|
|
162
|
+
completed = true;
|
|
163
|
+
if (lastEventId && requestBody !== undefined) {
|
|
164
|
+
terminalCursor = frame.id === undefined
|
|
165
|
+
? undefined
|
|
166
|
+
: `${frame.id}:${frameEvents.length}`;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
frameEvents.push({
|
|
170
|
+
type: "result",
|
|
171
|
+
data: {
|
|
172
|
+
finalText: text,
|
|
173
|
+
finishReason: choice.finish_reason,
|
|
174
|
+
status: "completed",
|
|
175
|
+
...(modelRequestsKnown ? { modelRequests: observedModelRequests } : {}),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
|
|
181
|
+
}
|
|
182
|
+
if (!sawProtocolEnd) {
|
|
183
|
+
throw new Error("cli-bridge stream ended without the [DONE] protocol marker");
|
|
184
|
+
}
|
|
185
|
+
if (!completed && lastEventId === undefined) {
|
|
186
|
+
throw new Error("cli-bridge stream ended without a terminal result");
|
|
187
|
+
}
|
|
188
|
+
if (lastEventId && requestBody !== undefined) {
|
|
189
|
+
const result = await readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted);
|
|
190
|
+
const finalModelRequests = result.modelRequests ??
|
|
191
|
+
(modelRequestsKnown ? observedModelRequests : undefined);
|
|
192
|
+
const finalCursor = terminalCursor ?? lastEventId;
|
|
193
|
+
yield {
|
|
194
|
+
type: "result",
|
|
195
|
+
data: {
|
|
196
|
+
finalText: result.text,
|
|
197
|
+
finishReason: result.finishReason,
|
|
198
|
+
status: "completed",
|
|
199
|
+
runId,
|
|
200
|
+
sessionId,
|
|
201
|
+
...(turn.executionId === undefined ? {} : { executionId: turn.executionId }),
|
|
202
|
+
...(finalCursor === undefined ? {} : { cursor: finalCursor }),
|
|
203
|
+
...(finalModelRequests === undefined
|
|
204
|
+
? {}
|
|
205
|
+
: { modelRequests: finalModelRequests }),
|
|
206
|
+
},
|
|
207
|
+
...(result.usage && !sawUsage ? { usage: result.usage } : {}),
|
|
208
|
+
...(finalCursor === undefined ? {} : { id: finalCursor }),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted) {
|
|
213
|
+
const body = safeJson(requestBody);
|
|
214
|
+
if (!body)
|
|
215
|
+
throw new Error("cli-bridge replay request is not valid JSON");
|
|
216
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: requestHeaders(options),
|
|
219
|
+
body: JSON.stringify({ ...body, stream: false }),
|
|
220
|
+
...(signal ? { signal } : {}),
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
throw new Error(`cli-bridge replay result ${response.status}: ${await response.text()}`);
|
|
224
|
+
}
|
|
225
|
+
onAccepted?.(response);
|
|
226
|
+
const parsed = safeJson(await response.text());
|
|
227
|
+
if (parsed?.error && typeof parsed.error === "object") {
|
|
228
|
+
const error = parsed.error;
|
|
229
|
+
const message = typeof error.message === "string" ? error.message : "cli-bridge replay failed";
|
|
230
|
+
throw new Error(`cli-bridge replay result failed: ${message}`);
|
|
231
|
+
}
|
|
232
|
+
const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : undefined;
|
|
233
|
+
const message = choice?.message && typeof choice.message === "object"
|
|
234
|
+
? choice.message
|
|
235
|
+
: undefined;
|
|
236
|
+
if (typeof message?.content !== "string" ||
|
|
237
|
+
typeof choice?.finish_reason !== "string") {
|
|
238
|
+
throw new Error("cli-bridge replay result returned an invalid completion");
|
|
239
|
+
}
|
|
240
|
+
if (choice.finish_reason === "error" || choice.finish_reason === "timeout") {
|
|
241
|
+
throw new Error(`cli-bridge replay result ended ${choice.finish_reason}`);
|
|
242
|
+
}
|
|
243
|
+
const usage = usageFromOpenAi(parsed?.usage);
|
|
244
|
+
const modelRequests = modelRequestsFromOpenAi(parsed?.usage);
|
|
245
|
+
return {
|
|
246
|
+
text: message.content,
|
|
247
|
+
finishReason: choice.finish_reason,
|
|
248
|
+
...(usage ? { usage } : {}),
|
|
249
|
+
...(modelRequests === undefined ? {} : { modelRequests }),
|
|
250
|
+
};
|
|
251
|
+
}
|