@tangle-network/agent-provider-cli-bridge 0.4.2 → 0.5.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 +23 -1
- package/dist/index.js +414 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,6 +17,22 @@ const environment = await provider.create({
|
|
|
17
17
|
model: { default: 'gpt-5' },
|
|
18
18
|
},
|
|
19
19
|
})
|
|
20
|
+
|
|
21
|
+
const reference = await environment.dispatch({
|
|
22
|
+
prompt: 'inspect the repository',
|
|
23
|
+
sessionId: 'research-session',
|
|
24
|
+
executionId: 'research-turn-1',
|
|
25
|
+
})
|
|
26
|
+
const session = environment.session(reference.id)
|
|
27
|
+
|
|
28
|
+
for await (const event of session.events({ since: '0' })) {
|
|
29
|
+
// Sequence-numbered bridge events, including normalized usage and result data.
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await session.prompt({
|
|
33
|
+
prompt: 'change direction using the evidence already collected',
|
|
34
|
+
executionId: 'research-turn-2',
|
|
35
|
+
})
|
|
20
36
|
```
|
|
21
37
|
|
|
22
38
|
The bridge model is selected from run data in this order: the turn, the provider default, or the profile's `harness` plus `model.default`.
|
|
@@ -24,7 +40,13 @@ Execution fails before network use when none is present.
|
|
|
24
40
|
|
|
25
41
|
Passing the same `sessionId` on later turns continues the same CLI conversation.
|
|
26
42
|
`executionId` gives a turn stable bridge identity, and `lastEventId` reattaches after a reader failure.
|
|
27
|
-
|
|
43
|
+
`dispatch()` starts a bridge-owned durable run and returns after detaching its HTTP reader.
|
|
44
|
+
The returned `AgentSession` exposes status, cursor-based event replay, the terminal result, continuation, and cancellation.
|
|
45
|
+
Cancellation returns only after cli-bridge confirms the run is terminal.
|
|
46
|
+
When `dispatch()` receives no `sessionId`, it creates one from that turn's stable run id and returns it.
|
|
47
|
+
Replay and result reads fail loudly after cli-bridge's configured replay retention expires.
|
|
48
|
+
Stopping a `session.events()` reader detaches only that replay observer.
|
|
49
|
+
Stopping a direct `environment.stream()` reader or destroying the environment cancels its active bridge runs and waits for terminal confirmation.
|
|
28
50
|
|
|
29
51
|
Response headers and streamed bodies have no transport timeout by default.
|
|
30
52
|
For unattended runs, set `headersTimeoutMs`, `bodyTimeoutMs`, or an `AbortSignal` so an unresponsive bridge cannot wait forever.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { harnessSystemPromptIntents, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { harnessSystemPromptIntents, snapshotAgentProfile, } from "@tangle-network/agent-interface";
|
|
3
3
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
4
4
|
export function createCliBridgeProvider(options) {
|
|
5
5
|
assertTimeout(options.headersTimeoutMs, "headersTimeoutMs");
|
|
@@ -10,16 +10,25 @@ export function createCliBridgeProvider(options) {
|
|
|
10
10
|
name,
|
|
11
11
|
capabilities: () => options.capabilities ?? defaultCliBridgeCapabilities(),
|
|
12
12
|
async create(input) {
|
|
13
|
+
if (typeof input.profile === "string") {
|
|
14
|
+
throw new Error(`createCliBridgeProvider requires an inline AgentProfile; named profile "${input.profile}" is unsupported`);
|
|
15
|
+
}
|
|
16
|
+
const environmentInput = {
|
|
17
|
+
...input,
|
|
18
|
+
profile: snapshotAgentProfile(input.profile),
|
|
19
|
+
};
|
|
13
20
|
const transport = createTransport(options);
|
|
14
21
|
const environmentId = input.idempotencyKey ?? crypto.randomUUID();
|
|
15
22
|
const runs = new Map();
|
|
23
|
+
const sessions = new Map();
|
|
16
24
|
const readers = new Set();
|
|
17
25
|
let destroyed = false;
|
|
18
26
|
let closePromise;
|
|
19
27
|
const stream = async function* (turn) {
|
|
20
28
|
if (destroyed)
|
|
21
29
|
throw new Error("cli-bridge environment is destroyed");
|
|
22
|
-
|
|
30
|
+
const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, false);
|
|
31
|
+
yield* streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers);
|
|
23
32
|
};
|
|
24
33
|
const environment = {
|
|
25
34
|
id: environmentId,
|
|
@@ -27,6 +36,26 @@ export function createCliBridgeProvider(options) {
|
|
|
27
36
|
...(input.name ? { name: input.name } : {}),
|
|
28
37
|
status: async () => (destroyed ? "stopped" : "running"),
|
|
29
38
|
stream,
|
|
39
|
+
async dispatch(turn) {
|
|
40
|
+
if (destroyed)
|
|
41
|
+
throw new Error("cli-bridge environment is destroyed");
|
|
42
|
+
const prepared = prepareCliBridgeRun(options, environmentInput, turn, environmentId, true);
|
|
43
|
+
return dispatchCliBridgeTurn(options, environmentInput, prepared, transport, name, runs, sessions);
|
|
44
|
+
},
|
|
45
|
+
session(id) {
|
|
46
|
+
return createCliBridgeSession({
|
|
47
|
+
id,
|
|
48
|
+
providerName: name,
|
|
49
|
+
options,
|
|
50
|
+
environmentInput,
|
|
51
|
+
environmentId,
|
|
52
|
+
transport,
|
|
53
|
+
runs,
|
|
54
|
+
sessions,
|
|
55
|
+
readers,
|
|
56
|
+
isDestroyed: () => destroyed,
|
|
57
|
+
});
|
|
58
|
+
},
|
|
30
59
|
placement: async () => ({
|
|
31
60
|
kind: options.defaultExecution?.kind === "sandbox" ? "sandbox" : "local",
|
|
32
61
|
providerMetadata: { baseUrl: options.baseUrl },
|
|
@@ -54,6 +83,7 @@ export function createCliBridgeProvider(options) {
|
|
|
54
83
|
reader.abort(new DOMException("cli-bridge environment was destroyed", "AbortError"));
|
|
55
84
|
}
|
|
56
85
|
await transport.close();
|
|
86
|
+
sessions.clear();
|
|
57
87
|
})();
|
|
58
88
|
closePromise = attempt;
|
|
59
89
|
try {
|
|
@@ -71,13 +101,63 @@ export function createCliBridgeProvider(options) {
|
|
|
71
101
|
},
|
|
72
102
|
};
|
|
73
103
|
}
|
|
74
|
-
|
|
104
|
+
function prepareCliBridgeRun(options, environmentInput, originalTurn, environmentId, requireSession) {
|
|
105
|
+
const turnId = originalTurn.turnId ?? crypto.randomUUID();
|
|
106
|
+
const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
|
|
107
|
+
const sessionId = originalTurn.sessionId ?? (requireSession ? runId : undefined);
|
|
108
|
+
const turn = {
|
|
109
|
+
...originalTurn,
|
|
110
|
+
turnId,
|
|
111
|
+
...(sessionId ? { sessionId } : {}),
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
turn,
|
|
115
|
+
run: {
|
|
116
|
+
id: runId,
|
|
117
|
+
...(sessionId ? { sessionId } : {}),
|
|
118
|
+
turnId,
|
|
119
|
+
requestBody: JSON.stringify(toChatCompletionsBody(options, environmentInput, turn, runId)),
|
|
120
|
+
readers: new Set(),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function bindCliBridgeRun(prepared, runs) {
|
|
125
|
+
const previous = runs.get(prepared.id);
|
|
126
|
+
runs.set(prepared.id, prepared);
|
|
127
|
+
return previous;
|
|
128
|
+
}
|
|
129
|
+
function restoreCliBridgeRun(run, previous, runs) {
|
|
130
|
+
if (runs.get(run.id) !== run)
|
|
131
|
+
return;
|
|
132
|
+
if (previous) {
|
|
133
|
+
runs.set(run.id, previous);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
runs.delete(run.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function bindCliBridgeSession(run, sessions) {
|
|
140
|
+
if (!run.sessionId)
|
|
141
|
+
return undefined;
|
|
142
|
+
const previous = sessions.get(run.sessionId)?.current;
|
|
143
|
+
sessions.set(run.sessionId, { id: run.sessionId, current: run });
|
|
144
|
+
return previous;
|
|
145
|
+
}
|
|
146
|
+
function restoreCliBridgeSession(run, previous, sessions) {
|
|
147
|
+
if (!run.sessionId || sessions.get(run.sessionId)?.current !== run)
|
|
148
|
+
return;
|
|
149
|
+
if (previous) {
|
|
150
|
+
sessions.set(run.sessionId, { id: run.sessionId, current: previous });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
sessions.delete(run.sessionId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function* streamTrackedCliBridgeTurn(options, environmentInput, prepared, transport, runs, sessions, readers) {
|
|
157
|
+
const originalTurn = prepared.turn;
|
|
75
158
|
if (originalTurn.detach) {
|
|
76
159
|
throw new Error("cli-bridge provider does not support detached turns");
|
|
77
160
|
}
|
|
78
|
-
const sessionId = originalTurn.sessionId;
|
|
79
|
-
const turnId = originalTurn.turnId ?? crypto.randomUUID();
|
|
80
|
-
const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
|
|
81
161
|
const controller = new AbortController();
|
|
82
162
|
const signals = [
|
|
83
163
|
originalTurn.signal,
|
|
@@ -86,21 +166,17 @@ async function* streamTrackedCliBridgeTurn(options, environmentInput, originalTu
|
|
|
86
166
|
].filter((signal) => signal !== undefined);
|
|
87
167
|
const turn = {
|
|
88
168
|
...originalTurn,
|
|
89
|
-
turnId,
|
|
90
169
|
...(signals.length > 0 ? { signal: AbortSignal.any(signals) } : {}),
|
|
91
170
|
};
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
readers: new Set(),
|
|
96
|
-
};
|
|
171
|
+
const run = prepared.run;
|
|
172
|
+
const previousRun = bindCliBridgeRun(run, runs);
|
|
173
|
+
const previousSessionRun = bindCliBridgeSession(run, sessions);
|
|
97
174
|
run.readers.add(controller);
|
|
98
175
|
readers.add(controller);
|
|
99
|
-
runs.set(runId, run);
|
|
100
176
|
let drained = false;
|
|
101
177
|
let threw = false;
|
|
102
178
|
try {
|
|
103
|
-
for await (const event of streamCliBridgeTurn(options, turn, requestBody, transport,
|
|
179
|
+
for await (const event of streamCliBridgeTurn(options, turn, run.requestBody, transport, run.id, originalTurn.lastEventId, signals.length > 0 ? AbortSignal.any(signals) : undefined, (response) => captureCliBridgeRunIdentity(response, run, false))) {
|
|
104
180
|
yield event;
|
|
105
181
|
}
|
|
106
182
|
drained = true;
|
|
@@ -110,8 +186,8 @@ async function* streamTrackedCliBridgeTurn(options, environmentInput, originalTu
|
|
|
110
186
|
catch (error) {
|
|
111
187
|
threw = true;
|
|
112
188
|
if (error instanceof CliBridgeRequestRejectedError) {
|
|
113
|
-
|
|
114
|
-
|
|
189
|
+
restoreCliBridgeRun(run, previousRun, runs);
|
|
190
|
+
restoreCliBridgeSession(run, previousSessionRun, sessions);
|
|
115
191
|
throw error;
|
|
116
192
|
}
|
|
117
193
|
let snapshot;
|
|
@@ -141,6 +217,299 @@ async function* streamTrackedCliBridgeTurn(options, environmentInput, originalTu
|
|
|
141
217
|
readers.delete(controller);
|
|
142
218
|
}
|
|
143
219
|
}
|
|
220
|
+
async function dispatchCliBridgeTurn(options, environmentInput, prepared, transport, providerName, runs, sessions) {
|
|
221
|
+
const run = prepared.run;
|
|
222
|
+
const previousRun = bindCliBridgeRun(run, runs);
|
|
223
|
+
const previousSessionRun = bindCliBridgeSession(run, sessions);
|
|
224
|
+
const signals = [
|
|
225
|
+
prepared.turn.signal,
|
|
226
|
+
environmentInput.signal,
|
|
227
|
+
].filter((signal) => signal !== undefined);
|
|
228
|
+
const signal = signals.length > 0 ? AbortSignal.any(signals) : undefined;
|
|
229
|
+
let accepted = false;
|
|
230
|
+
let responseBody;
|
|
231
|
+
try {
|
|
232
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: {
|
|
235
|
+
...requestHeaders(options),
|
|
236
|
+
accept: "text/event-stream",
|
|
237
|
+
...(run.sessionId ? { "x-session-id": run.sessionId } : {}),
|
|
238
|
+
},
|
|
239
|
+
body: run.requestBody,
|
|
240
|
+
...(signal ? { signal } : {}),
|
|
241
|
+
});
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
let detail = "request rejected";
|
|
244
|
+
try {
|
|
245
|
+
detail = await response.text();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// The HTTP status already proves this request was rejected.
|
|
249
|
+
}
|
|
250
|
+
throw new CliBridgeRequestRejectedError(response.status, detail);
|
|
251
|
+
}
|
|
252
|
+
accepted = true;
|
|
253
|
+
if (!response.body)
|
|
254
|
+
throw new Error("cli-bridge response body is empty");
|
|
255
|
+
responseBody = response.body;
|
|
256
|
+
captureCliBridgeRunIdentity(response, run, true);
|
|
257
|
+
await detachCliBridgeReader(responseBody);
|
|
258
|
+
responseBody = undefined;
|
|
259
|
+
return {
|
|
260
|
+
id: run.sessionId,
|
|
261
|
+
provider: providerName,
|
|
262
|
+
metadata: {
|
|
263
|
+
runId: run.id,
|
|
264
|
+
requestDigest: run.requestDigest,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
let failure = error;
|
|
270
|
+
if (responseBody) {
|
|
271
|
+
try {
|
|
272
|
+
await detachCliBridgeReader(responseBody);
|
|
273
|
+
}
|
|
274
|
+
catch (detachError) {
|
|
275
|
+
failure = new AggregateError([failure, detachError], `cli-bridge dispatch "${run.id}" failed and its reader did not detach`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (failure instanceof CliBridgeRequestRejectedError) {
|
|
279
|
+
restoreCliBridgeRun(run, previousRun, runs);
|
|
280
|
+
restoreCliBridgeSession(run, previousSessionRun, sessions);
|
|
281
|
+
throw failure;
|
|
282
|
+
}
|
|
283
|
+
if (accepted || signal?.aborted) {
|
|
284
|
+
try {
|
|
285
|
+
await cancelCliBridgeRun(options, transport, run);
|
|
286
|
+
if (runs.get(run.id) === run)
|
|
287
|
+
runs.delete(run.id);
|
|
288
|
+
}
|
|
289
|
+
catch (cancellationError) {
|
|
290
|
+
throw new AggregateError([failure, cancellationError], `cli-bridge dispatch "${run.id}" failed and cancellation was not confirmed`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
restoreCliBridgeRun(run, previousRun, runs);
|
|
295
|
+
}
|
|
296
|
+
restoreCliBridgeSession(run, previousSessionRun, sessions);
|
|
297
|
+
throw failure;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function createCliBridgeSession(args) {
|
|
301
|
+
const currentRun = () => {
|
|
302
|
+
if (args.isDestroyed())
|
|
303
|
+
throw new Error("cli-bridge environment is destroyed");
|
|
304
|
+
return args.sessions.get(args.id)?.current;
|
|
305
|
+
};
|
|
306
|
+
const requireCurrentRun = () => {
|
|
307
|
+
const run = currentRun();
|
|
308
|
+
if (!run)
|
|
309
|
+
throw new Error(`cli-bridge session "${args.id}" has no run`);
|
|
310
|
+
return run;
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
id: args.id,
|
|
314
|
+
async status() {
|
|
315
|
+
const run = currentRun();
|
|
316
|
+
if (!run)
|
|
317
|
+
return null;
|
|
318
|
+
const snapshot = await getCliBridgeRun(args.options, args.transport, run.id);
|
|
319
|
+
if (!snapshot)
|
|
320
|
+
return null;
|
|
321
|
+
if (snapshot.terminal && args.runs.get(run.id) === run) {
|
|
322
|
+
args.runs.delete(run.id);
|
|
323
|
+
}
|
|
324
|
+
return agentSessionStatusFromRun(snapshot);
|
|
325
|
+
},
|
|
326
|
+
async *events(options) {
|
|
327
|
+
const run = requireCurrentRun();
|
|
328
|
+
yield* streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, options);
|
|
329
|
+
},
|
|
330
|
+
async result() {
|
|
331
|
+
const run = requireCurrentRun();
|
|
332
|
+
return collectCliBridgeTurnResult(streamCliBridgeSessionEvents(args.options, args.environmentInput, run, args.transport, args.runs, args.readers, { since: "0" }), run, args.options, args.transport);
|
|
333
|
+
},
|
|
334
|
+
async prompt(input) {
|
|
335
|
+
if (input.sessionId && input.sessionId !== args.id) {
|
|
336
|
+
throw new Error(`cli-bridge session "${args.id}" cannot prompt session "${input.sessionId}"`);
|
|
337
|
+
}
|
|
338
|
+
const prepared = prepareCliBridgeRun(args.options, args.environmentInput, {
|
|
339
|
+
...input,
|
|
340
|
+
sessionId: args.id,
|
|
341
|
+
}, args.environmentId, false);
|
|
342
|
+
const result = await collectCliBridgeTurnResult(streamTrackedCliBridgeTurn(args.options, args.environmentInput, prepared, args.transport, args.runs, args.sessions, args.readers), prepared.run, args.options, args.transport);
|
|
343
|
+
return { ...result, sessionId: args.id };
|
|
344
|
+
},
|
|
345
|
+
async cancel() {
|
|
346
|
+
const run = requireCurrentRun();
|
|
347
|
+
await cancelCliBridgeRun(args.options, args.transport, run);
|
|
348
|
+
if (args.runs.get(run.id) === run)
|
|
349
|
+
args.runs.delete(run.id);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async function* streamCliBridgeSessionEvents(options, environmentInput, run, transport, runs, readers, eventOptions) {
|
|
354
|
+
const controller = new AbortController();
|
|
355
|
+
const signals = [
|
|
356
|
+
eventOptions?.signal,
|
|
357
|
+
environmentInput.signal,
|
|
358
|
+
controller.signal,
|
|
359
|
+
].filter((signal) => signal !== undefined);
|
|
360
|
+
const signal = signals.length > 0 ? AbortSignal.any(signals) : undefined;
|
|
361
|
+
run.readers.add(controller);
|
|
362
|
+
readers.add(controller);
|
|
363
|
+
let drained = false;
|
|
364
|
+
try {
|
|
365
|
+
yield* streamCliBridgeTurn(options, {
|
|
366
|
+
...(run.sessionId ? { sessionId: run.sessionId } : {}),
|
|
367
|
+
turnId: run.turnId,
|
|
368
|
+
}, run.requestBody, transport, run.id, eventOptions?.since ?? "0", signal, (response) => captureCliBridgeRunIdentity(response, run, false));
|
|
369
|
+
drained = true;
|
|
370
|
+
if (runs.get(run.id) === run)
|
|
371
|
+
runs.delete(run.id);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
if (!drained) {
|
|
375
|
+
controller.abort(new DOMException("cli-bridge session event reader detached", "AbortError"));
|
|
376
|
+
}
|
|
377
|
+
run.readers.delete(controller);
|
|
378
|
+
readers.delete(controller);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function collectCliBridgeTurnResult(source, run, options, transport) {
|
|
382
|
+
const events = [];
|
|
383
|
+
let text = "";
|
|
384
|
+
let usage;
|
|
385
|
+
let streamError;
|
|
386
|
+
try {
|
|
387
|
+
for await (const event of source) {
|
|
388
|
+
events.push(event);
|
|
389
|
+
const finalText = event.data.finalText;
|
|
390
|
+
if (typeof finalText === "string") {
|
|
391
|
+
text = finalText;
|
|
392
|
+
}
|
|
393
|
+
else if (typeof event.data.delta === "string") {
|
|
394
|
+
text += event.data.delta;
|
|
395
|
+
}
|
|
396
|
+
usage = addTokenUsage(usage, event.usage);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
streamError = error;
|
|
401
|
+
}
|
|
402
|
+
const snapshot = await getCliBridgeRun(options, transport, run.id);
|
|
403
|
+
if (!snapshot) {
|
|
404
|
+
if (streamError)
|
|
405
|
+
throw streamError;
|
|
406
|
+
throw new Error(`cli-bridge lost run "${run.id}" before reading its result`);
|
|
407
|
+
}
|
|
408
|
+
if (!snapshot.terminal) {
|
|
409
|
+
if (streamError)
|
|
410
|
+
throw streamError;
|
|
411
|
+
throw new Error(`cli-bridge run "${run.id}" has no terminal result`);
|
|
412
|
+
}
|
|
413
|
+
const success = snapshot.status === "done" && streamError === undefined;
|
|
414
|
+
return {
|
|
415
|
+
text,
|
|
416
|
+
success,
|
|
417
|
+
...(!success
|
|
418
|
+
? {
|
|
419
|
+
error: streamError instanceof Error
|
|
420
|
+
? streamError.message
|
|
421
|
+
: `cli-bridge run ended ${snapshot.status}`,
|
|
422
|
+
}
|
|
423
|
+
: {}),
|
|
424
|
+
...(run.sessionId ? { sessionId: run.sessionId } : {}),
|
|
425
|
+
...(usage ? { usage } : {}),
|
|
426
|
+
metadata: {
|
|
427
|
+
runId: run.id,
|
|
428
|
+
status: snapshot.status,
|
|
429
|
+
...(run.requestDigest ? { requestDigest: run.requestDigest } : {}),
|
|
430
|
+
},
|
|
431
|
+
events,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function agentSessionStatusFromRun(snapshot) {
|
|
435
|
+
if (snapshot.status === "done")
|
|
436
|
+
return "completed";
|
|
437
|
+
if (snapshot.status === "error")
|
|
438
|
+
return "failed";
|
|
439
|
+
if (snapshot.status === "cancelled")
|
|
440
|
+
return "cancelled";
|
|
441
|
+
return "running";
|
|
442
|
+
}
|
|
443
|
+
function addTokenUsage(current, next) {
|
|
444
|
+
if (!next)
|
|
445
|
+
return current;
|
|
446
|
+
if (!current)
|
|
447
|
+
return { ...next };
|
|
448
|
+
return {
|
|
449
|
+
inputTokens: current.inputTokens + next.inputTokens,
|
|
450
|
+
outputTokens: current.outputTokens + next.outputTokens,
|
|
451
|
+
...(current.totalTokens !== undefined || next.totalTokens !== undefined
|
|
452
|
+
? { totalTokens: (current.totalTokens ?? 0) + (next.totalTokens ?? 0) }
|
|
453
|
+
: {}),
|
|
454
|
+
...(current.cacheReadInputTokens !== undefined ||
|
|
455
|
+
next.cacheReadInputTokens !== undefined
|
|
456
|
+
? {
|
|
457
|
+
cacheReadInputTokens: (current.cacheReadInputTokens ?? 0) +
|
|
458
|
+
(next.cacheReadInputTokens ?? 0),
|
|
459
|
+
}
|
|
460
|
+
: {}),
|
|
461
|
+
...(current.cacheCreationInputTokens !== undefined ||
|
|
462
|
+
next.cacheCreationInputTokens !== undefined
|
|
463
|
+
? {
|
|
464
|
+
cacheCreationInputTokens: (current.cacheCreationInputTokens ?? 0) +
|
|
465
|
+
(next.cacheCreationInputTokens ?? 0),
|
|
466
|
+
}
|
|
467
|
+
: {}),
|
|
468
|
+
...(current.reasoningTokens !== undefined || next.reasoningTokens !== undefined
|
|
469
|
+
? {
|
|
470
|
+
reasoningTokens: (current.reasoningTokens ?? 0) + (next.reasoningTokens ?? 0),
|
|
471
|
+
}
|
|
472
|
+
: {}),
|
|
473
|
+
...(current.cost !== undefined || next.cost !== undefined
|
|
474
|
+
? { cost: (current.cost ?? 0) + (next.cost ?? 0) }
|
|
475
|
+
: {}),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function captureCliBridgeRunIdentity(response, run, required) {
|
|
479
|
+
const responseRunId = response.headers.get("x-run-id");
|
|
480
|
+
const requestDigest = response.headers.get("x-run-request-digest");
|
|
481
|
+
if (responseRunId !== null && responseRunId !== run.id) {
|
|
482
|
+
throw new Error(`cli-bridge accepted run "${responseRunId}" for requested run "${run.id}"`);
|
|
483
|
+
}
|
|
484
|
+
if ((required || run.requestDigest !== undefined) && responseRunId === null) {
|
|
485
|
+
throw new Error("cli-bridge response omitted X-Run-Id");
|
|
486
|
+
}
|
|
487
|
+
if (required && requestDigest === null) {
|
|
488
|
+
throw new Error("cli-bridge dispatch response omitted X-Run-Request-Digest");
|
|
489
|
+
}
|
|
490
|
+
if (requestDigest !== null &&
|
|
491
|
+
run.requestDigest !== undefined &&
|
|
492
|
+
requestDigest !== run.requestDigest) {
|
|
493
|
+
throw new Error(`cli-bridge changed request digest for run "${run.id}" from "${run.requestDigest}" to "${requestDigest}"`);
|
|
494
|
+
}
|
|
495
|
+
if (run.requestDigest !== undefined && requestDigest === null) {
|
|
496
|
+
throw new Error(`cli-bridge replay response omitted X-Run-Request-Digest for run "${run.id}"`);
|
|
497
|
+
}
|
|
498
|
+
if (requestDigest !== null)
|
|
499
|
+
run.requestDigest = requestDigest;
|
|
500
|
+
}
|
|
501
|
+
async function detachCliBridgeReader(body) {
|
|
502
|
+
const cancellable = body;
|
|
503
|
+
if (cancellable.cancel) {
|
|
504
|
+
await cancellable.cancel();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const iterator = body[Symbol.asyncIterator]();
|
|
508
|
+
if (!iterator.return) {
|
|
509
|
+
throw new Error("cli-bridge response body cannot detach its reader");
|
|
510
|
+
}
|
|
511
|
+
await iterator.return();
|
|
512
|
+
}
|
|
144
513
|
async function cancelCliBridgeRun(options, transport, run) {
|
|
145
514
|
if (run.cancellation)
|
|
146
515
|
return run.cancellation;
|
|
@@ -154,6 +523,7 @@ async function cancelCliBridgeRun(options, transport, run) {
|
|
|
154
523
|
throw new Error(`cli-bridge cancel ${response.status}: ${await response.text()}`);
|
|
155
524
|
}
|
|
156
525
|
let snapshot = cancelSnapshot(await response.text());
|
|
526
|
+
assertCliBridgeRunSnapshotIdentity(snapshot, run.id);
|
|
157
527
|
const waitBudgetMs = options.cancelWaitMs ?? 30_000;
|
|
158
528
|
const deadline = Date.now() + waitBudgetMs;
|
|
159
529
|
while (!snapshot.terminal) {
|
|
@@ -191,9 +561,16 @@ async function getCliBridgeRun(options, transport, runId, waitMs) {
|
|
|
191
561
|
if (!response.ok) {
|
|
192
562
|
throw new Error(`cli-bridge run status ${response.status}: ${await response.text()}`);
|
|
193
563
|
}
|
|
194
|
-
|
|
564
|
+
const snapshot = runSnapshot(await response.text());
|
|
565
|
+
assertCliBridgeRunSnapshotIdentity(snapshot, runId);
|
|
566
|
+
return snapshot;
|
|
567
|
+
}
|
|
568
|
+
function assertCliBridgeRunSnapshotIdentity(snapshot, expectedRunId) {
|
|
569
|
+
if (snapshot.id !== expectedRunId) {
|
|
570
|
+
throw new Error(`cli-bridge returned run "${snapshot.id}" for requested run "${expectedRunId}"`);
|
|
571
|
+
}
|
|
195
572
|
}
|
|
196
|
-
async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal) {
|
|
573
|
+
async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onAccepted) {
|
|
197
574
|
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
198
575
|
method: "POST",
|
|
199
576
|
headers: {
|
|
@@ -215,6 +592,7 @@ async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId
|
|
|
215
592
|
}
|
|
216
593
|
throw new CliBridgeRequestRejectedError(response.status, detail);
|
|
217
594
|
}
|
|
595
|
+
onAccepted?.(response);
|
|
218
596
|
if (!response.body)
|
|
219
597
|
throw new Error("cli-bridge response body is empty");
|
|
220
598
|
let text = "";
|
|
@@ -222,6 +600,7 @@ async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId
|
|
|
222
600
|
const messageId = turn.turnId ?? `${sessionId}:assistant`;
|
|
223
601
|
const emittedToolCalls = new Set();
|
|
224
602
|
let completed = false;
|
|
603
|
+
let sawUsage = false;
|
|
225
604
|
let terminalCursor;
|
|
226
605
|
for await (const frame of parseSse(response.body)) {
|
|
227
606
|
if (frame.data === "[DONE]")
|
|
@@ -245,6 +624,7 @@ async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId
|
|
|
245
624
|
const nextUsage = usageFromOpenAi(parsed.usage);
|
|
246
625
|
const frameEvents = [];
|
|
247
626
|
if (nextUsage) {
|
|
627
|
+
sawUsage = true;
|
|
248
628
|
frameEvents.push({ type: "usage", data: {}, usage: nextUsage });
|
|
249
629
|
}
|
|
250
630
|
if (chunk) {
|
|
@@ -321,7 +701,10 @@ async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId
|
|
|
321
701
|
throw new Error("cli-bridge stream ended without a terminal result");
|
|
322
702
|
}
|
|
323
703
|
if (lastEventId) {
|
|
324
|
-
const result = await readFullCliBridgeResult(options, requestBody, transport, signal);
|
|
704
|
+
const result = await readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted);
|
|
705
|
+
if (result.usage && !sawUsage) {
|
|
706
|
+
yield { type: "usage", data: {}, usage: result.usage };
|
|
707
|
+
}
|
|
325
708
|
yield {
|
|
326
709
|
type: "result",
|
|
327
710
|
data: {
|
|
@@ -341,7 +724,7 @@ class CliBridgeRequestRejectedError extends Error {
|
|
|
341
724
|
this.name = "CliBridgeRequestRejectedError";
|
|
342
725
|
}
|
|
343
726
|
}
|
|
344
|
-
async function readFullCliBridgeResult(options, requestBody, transport, signal) {
|
|
727
|
+
async function readFullCliBridgeResult(options, requestBody, transport, signal, onAccepted) {
|
|
345
728
|
const body = safeJson(requestBody);
|
|
346
729
|
if (!body)
|
|
347
730
|
throw new Error("cli-bridge replay request is not valid JSON");
|
|
@@ -354,6 +737,7 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal)
|
|
|
354
737
|
if (!response.ok) {
|
|
355
738
|
throw new Error(`cli-bridge replay result ${response.status}: ${await response.text()}`);
|
|
356
739
|
}
|
|
740
|
+
onAccepted?.(response);
|
|
357
741
|
const parsed = safeJson(await response.text());
|
|
358
742
|
if (parsed?.error && typeof parsed.error === "object") {
|
|
359
743
|
const error = parsed.error;
|
|
@@ -371,7 +755,12 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal)
|
|
|
371
755
|
if (choice.finish_reason === "error" || choice.finish_reason === "timeout") {
|
|
372
756
|
throw new Error(`cli-bridge replay result ended ${choice.finish_reason}`);
|
|
373
757
|
}
|
|
374
|
-
|
|
758
|
+
const usage = usageFromOpenAi(parsed?.usage);
|
|
759
|
+
return {
|
|
760
|
+
text: message.content,
|
|
761
|
+
finishReason: choice.finish_reason,
|
|
762
|
+
...(usage ? { usage } : {}),
|
|
763
|
+
};
|
|
375
764
|
}
|
|
376
765
|
function createTransport(options) {
|
|
377
766
|
if (options.fetch) {
|
|
@@ -410,6 +799,9 @@ function toChatCompletionsBody(options, environmentInput, turn, runId) {
|
|
|
410
799
|
run_id: runId,
|
|
411
800
|
...(options.defaultMode ? { mode: options.defaultMode } : {}),
|
|
412
801
|
...(profile ? { agent_profile: profile } : {}),
|
|
802
|
+
...(profile?.model?.reasoningEffort
|
|
803
|
+
? { effort: profile.model.reasoningEffort }
|
|
804
|
+
: {}),
|
|
413
805
|
...(environmentInput.env ? { env: environmentInput.env } : {}),
|
|
414
806
|
...(environmentInput.workspace?.cwd ? { cwd: environmentInput.workspace.cwd } : {}),
|
|
415
807
|
...(executionFromInput(options, environmentInput) ? { execution: executionFromInput(options, environmentInput) } : {}),
|
|
@@ -649,7 +1041,7 @@ export function defaultCliBridgeCapabilities(harness) {
|
|
|
649
1041
|
runtimeUpdate: false,
|
|
650
1042
|
validation: false,
|
|
651
1043
|
},
|
|
652
|
-
streaming: { live: true, replay: true, detach:
|
|
1044
|
+
streaming: { live: true, replay: true, detach: true, turnIdempotency: true },
|
|
653
1045
|
sessions: { continue: true, list: false, messages: false },
|
|
654
1046
|
workspace: { read: false, write: false, exec: false, git: false, upload: false, download: false },
|
|
655
1047
|
branching: { checkpoint: false, fork: false },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-cli-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@tangle-network/agent-interface": "0.
|
|
32
|
+
"@tangle-network/agent-interface": "0.47.0",
|
|
33
33
|
"undici": "7.28.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|