@tangle-network/agent-provider-cli-bridge 0.2.33 → 0.3.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.
- package/README.md +15 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +350 -43
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,9 +8,23 @@ import { createCliBridgeProvider } from '@tangle-network/agent-provider-cli-brid
|
|
|
8
8
|
const provider = createCliBridgeProvider({
|
|
9
9
|
baseUrl: 'http://127.0.0.1:8787',
|
|
10
10
|
bearerToken: process.env.CLI_BRIDGE_TOKEN,
|
|
11
|
-
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const environment = await provider.create({
|
|
14
|
+
profile: {
|
|
15
|
+
name: 'researcher',
|
|
16
|
+
harness: 'codex',
|
|
17
|
+
model: { default: 'gpt-5' },
|
|
18
|
+
},
|
|
12
19
|
})
|
|
13
20
|
```
|
|
14
21
|
|
|
22
|
+
The bridge model is selected from run data in this order: the turn, the provider default, or the profile's `harness` plus `model.default`.
|
|
23
|
+
Execution fails before network use when none is present.
|
|
24
|
+
|
|
25
|
+
Passing the same `sessionId` on later turns continues the same CLI conversation.
|
|
26
|
+
`executionId` gives a turn stable bridge identity, and `lastEventId` reattaches after a reader failure.
|
|
27
|
+
Stopping a reader or destroying the environment cancels every active bridge run and waits for terminal confirmation.
|
|
28
|
+
|
|
15
29
|
Response headers and streamed bodies have no transport timeout by default.
|
|
16
30
|
For unattended runs, set `headersTimeoutMs`, `bodyTimeoutMs`, or an `AbortSignal` so an unresponsive bridge cannot wait forever.
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface CliBridgeProviderOptions {
|
|
|
17
17
|
headersTimeoutMs?: number;
|
|
18
18
|
/** Maximum idle time between response body chunks. Defaults to no timeout. */
|
|
19
19
|
bodyTimeoutMs?: number;
|
|
20
|
+
/** Maximum wait for cli-bridge to confirm cancellation. Defaults to 30 seconds. */
|
|
21
|
+
cancelWaitMs?: number;
|
|
20
22
|
fetch?: typeof fetch;
|
|
21
23
|
name?: string;
|
|
22
24
|
capabilities?: AgentEnvironmentCapabilities;
|
package/dist/index.js
CHANGED
|
@@ -1,78 +1,250 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
2
3
|
export function createCliBridgeProvider(options) {
|
|
3
4
|
assertTimeout(options.headersTimeoutMs, "headersTimeoutMs");
|
|
4
5
|
assertTimeout(options.bodyTimeoutMs, "bodyTimeoutMs");
|
|
6
|
+
assertTimeout(options.cancelWaitMs, "cancelWaitMs");
|
|
5
7
|
const name = options.name ?? "cli-bridge";
|
|
6
8
|
return {
|
|
7
9
|
name,
|
|
8
10
|
capabilities: () => options.capabilities ?? defaultCliBridgeCapabilities(),
|
|
9
11
|
async create(input) {
|
|
10
12
|
const transport = createTransport(options);
|
|
13
|
+
const environmentId = input.idempotencyKey ?? crypto.randomUUID();
|
|
14
|
+
const runs = new Map();
|
|
15
|
+
const readers = new Set();
|
|
11
16
|
let destroyed = false;
|
|
12
17
|
let closePromise;
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
const stream = async function* (turn) {
|
|
19
|
+
if (destroyed)
|
|
20
|
+
throw new Error("cli-bridge environment is destroyed");
|
|
21
|
+
yield* streamTrackedCliBridgeTurn(options, input, turn, transport, environmentId, runs, readers);
|
|
22
|
+
};
|
|
23
|
+
const environment = {
|
|
24
|
+
id: environmentId,
|
|
15
25
|
provider: name,
|
|
16
26
|
...(input.name ? { name: input.name } : {}),
|
|
17
27
|
status: async () => (destroyed ? "stopped" : "running"),
|
|
18
|
-
stream
|
|
19
|
-
if (destroyed)
|
|
20
|
-
throw new Error("cli-bridge environment is destroyed");
|
|
21
|
-
return streamCliBridgeTurn(options, input, turn, transport);
|
|
22
|
-
},
|
|
28
|
+
stream,
|
|
23
29
|
placement: async () => ({
|
|
24
30
|
kind: options.defaultExecution?.kind === "sandbox" ? "sandbox" : "local",
|
|
25
31
|
providerMetadata: { baseUrl: options.baseUrl },
|
|
26
32
|
}),
|
|
27
|
-
destroy
|
|
33
|
+
async destroy() {
|
|
34
|
+
if (closePromise)
|
|
35
|
+
return closePromise;
|
|
28
36
|
destroyed = true;
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
let cancellationsConfirmed = false;
|
|
38
|
+
const attempt = (async () => {
|
|
39
|
+
const cancellations = await Promise.allSettled(Array.from(runs.values()).map(async (run) => {
|
|
40
|
+
const snapshot = await cancelCliBridgeRun(options, transport, run);
|
|
41
|
+
if (runs.get(run.id) === run)
|
|
42
|
+
runs.delete(run.id);
|
|
43
|
+
return snapshot;
|
|
44
|
+
}));
|
|
45
|
+
const failures = cancellations.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
46
|
+
if (failures.length === 1)
|
|
47
|
+
throw failures[0];
|
|
48
|
+
if (failures.length > 1) {
|
|
49
|
+
throw new AggregateError(failures, "cli-bridge environment cancellation failed");
|
|
50
|
+
}
|
|
51
|
+
cancellationsConfirmed = true;
|
|
52
|
+
for (const reader of readers) {
|
|
53
|
+
reader.abort(new DOMException("cli-bridge environment was destroyed", "AbortError"));
|
|
54
|
+
}
|
|
55
|
+
await transport.close();
|
|
56
|
+
})();
|
|
57
|
+
closePromise = attempt;
|
|
58
|
+
try {
|
|
59
|
+
await attempt;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
closePromise = undefined;
|
|
63
|
+
if (!cancellationsConfirmed)
|
|
64
|
+
destroyed = false;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
31
67
|
},
|
|
32
68
|
};
|
|
69
|
+
return environment;
|
|
33
70
|
},
|
|
34
71
|
};
|
|
35
72
|
}
|
|
36
|
-
async function*
|
|
73
|
+
async function* streamTrackedCliBridgeTurn(options, environmentInput, originalTurn, transport, environmentId, runs, readers) {
|
|
74
|
+
if (originalTurn.detach) {
|
|
75
|
+
throw new Error("cli-bridge provider does not support detached turns");
|
|
76
|
+
}
|
|
77
|
+
const sessionId = originalTurn.sessionId;
|
|
78
|
+
const turnId = originalTurn.turnId ?? crypto.randomUUID();
|
|
79
|
+
const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const signals = [
|
|
82
|
+
originalTurn.signal,
|
|
83
|
+
environmentInput.signal,
|
|
84
|
+
controller.signal,
|
|
85
|
+
].filter((signal) => signal !== undefined);
|
|
86
|
+
const turn = {
|
|
87
|
+
...originalTurn,
|
|
88
|
+
turnId,
|
|
89
|
+
...(signals.length > 0 ? { signal: AbortSignal.any(signals) } : {}),
|
|
90
|
+
};
|
|
91
|
+
const requestBody = JSON.stringify(toChatCompletionsBody(options, environmentInput, turn, runId));
|
|
92
|
+
const run = runs.get(runId) ?? {
|
|
93
|
+
id: runId,
|
|
94
|
+
readers: new Set(),
|
|
95
|
+
};
|
|
96
|
+
run.readers.add(controller);
|
|
97
|
+
readers.add(controller);
|
|
98
|
+
runs.set(runId, run);
|
|
99
|
+
let drained = false;
|
|
100
|
+
let threw = false;
|
|
101
|
+
try {
|
|
102
|
+
for await (const event of streamCliBridgeTurn(options, turn, requestBody, transport, runId, originalTurn.lastEventId, signals.length > 0 ? AbortSignal.any(signals) : undefined)) {
|
|
103
|
+
yield event;
|
|
104
|
+
}
|
|
105
|
+
drained = true;
|
|
106
|
+
if (runs.get(run.id) === run)
|
|
107
|
+
runs.delete(run.id);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
threw = true;
|
|
111
|
+
if (error instanceof CliBridgeRequestRejectedError) {
|
|
112
|
+
if (runs.get(run.id) === run)
|
|
113
|
+
runs.delete(run.id);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
let snapshot;
|
|
117
|
+
try {
|
|
118
|
+
snapshot = await getCliBridgeRun(options, transport, run.id);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
snapshot = undefined;
|
|
122
|
+
}
|
|
123
|
+
if (snapshot?.terminal) {
|
|
124
|
+
if (runs.get(run.id) === run)
|
|
125
|
+
runs.delete(run.id);
|
|
126
|
+
}
|
|
127
|
+
else if ((originalTurn.signal?.aborted || environmentInput.signal?.aborted)) {
|
|
128
|
+
await cancelCliBridgeRun(options, transport, run);
|
|
129
|
+
if (runs.get(run.id) === run)
|
|
130
|
+
runs.delete(run.id);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (!drained && !threw && runs.get(run.id) === run) {
|
|
136
|
+
await cancelCliBridgeRun(options, transport, run);
|
|
137
|
+
runs.delete(run.id);
|
|
138
|
+
}
|
|
139
|
+
run.readers.delete(controller);
|
|
140
|
+
readers.delete(controller);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function cancelCliBridgeRun(options, transport, run) {
|
|
144
|
+
if (run.cancellation)
|
|
145
|
+
return run.cancellation;
|
|
146
|
+
run.cancellation = (async () => {
|
|
147
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: requestHeaders(options),
|
|
150
|
+
body: "{}",
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`cli-bridge cancel ${response.status}: ${await response.text()}`);
|
|
154
|
+
}
|
|
155
|
+
let snapshot = cancelSnapshot(await response.text());
|
|
156
|
+
const waitBudgetMs = options.cancelWaitMs ?? 30_000;
|
|
157
|
+
const deadline = Date.now() + waitBudgetMs;
|
|
158
|
+
while (!snapshot.terminal) {
|
|
159
|
+
const remainingMs = deadline - Date.now();
|
|
160
|
+
if (remainingMs <= 0)
|
|
161
|
+
break;
|
|
162
|
+
snapshot = await getCliBridgeRun(options, transport, run.id, Math.min(remainingMs, 30_000));
|
|
163
|
+
if (snapshot === null) {
|
|
164
|
+
throw new Error(`cli-bridge lost run "${run.id}" before confirming cancellation`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!snapshot?.terminal) {
|
|
168
|
+
throw new Error(`cli-bridge run "${run.id}" did not confirm terminal cancellation`);
|
|
169
|
+
}
|
|
170
|
+
for (const reader of run.readers) {
|
|
171
|
+
reader.abort(new DOMException(`cli-bridge run ended ${snapshot.status}`, "AbortError"));
|
|
172
|
+
}
|
|
173
|
+
return snapshot;
|
|
174
|
+
})();
|
|
175
|
+
try {
|
|
176
|
+
return await run.cancellation;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
run.cancellation = undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function getCliBridgeRun(options, transport, runId, waitMs) {
|
|
183
|
+
const query = waitMs === undefined ? "" : `?wait_ms=${waitMs}`;
|
|
184
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(runId)}${query}`, {
|
|
185
|
+
method: "GET",
|
|
186
|
+
headers: requestHeaders(options),
|
|
187
|
+
});
|
|
188
|
+
if (response.status === 404)
|
|
189
|
+
return null;
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error(`cli-bridge run status ${response.status}: ${await response.text()}`);
|
|
192
|
+
}
|
|
193
|
+
return runSnapshot(await response.text());
|
|
194
|
+
}
|
|
195
|
+
async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal) {
|
|
37
196
|
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
38
197
|
method: "POST",
|
|
39
198
|
headers: {
|
|
40
|
-
|
|
199
|
+
...requestHeaders(options),
|
|
41
200
|
accept: "text/event-stream",
|
|
42
|
-
...(options.bearerToken ? { authorization: `Bearer ${options.bearerToken}` } : {}),
|
|
43
201
|
...(turn.sessionId ? { "x-session-id": turn.sessionId } : {}),
|
|
202
|
+
...(lastEventId ? { "last-event-id": lastEventId } : {}),
|
|
44
203
|
},
|
|
45
|
-
body:
|
|
46
|
-
signal
|
|
204
|
+
body: requestBody,
|
|
205
|
+
signal,
|
|
47
206
|
});
|
|
48
207
|
if (!response.ok) {
|
|
49
|
-
|
|
208
|
+
let detail = "request rejected";
|
|
209
|
+
try {
|
|
210
|
+
detail = await response.text();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// The HTTP status already proves this request was rejected.
|
|
214
|
+
}
|
|
215
|
+
throw new CliBridgeRequestRejectedError(response.status, detail);
|
|
50
216
|
}
|
|
51
217
|
if (!response.body)
|
|
52
218
|
throw new Error("cli-bridge response body is empty");
|
|
53
219
|
let text = "";
|
|
54
|
-
const sessionId = turn.sessionId ??
|
|
220
|
+
const sessionId = turn.sessionId ?? runId;
|
|
55
221
|
const messageId = turn.turnId ?? `${sessionId}:assistant`;
|
|
56
222
|
const emittedToolCalls = new Set();
|
|
57
223
|
let completed = false;
|
|
58
|
-
|
|
59
|
-
|
|
224
|
+
let terminalCursor;
|
|
225
|
+
for await (const frame of parseSse(response.body)) {
|
|
226
|
+
if (frame.data === "[DONE]")
|
|
60
227
|
continue;
|
|
61
|
-
const parsed = safeJson(
|
|
228
|
+
const parsed = safeJson(frame.data);
|
|
62
229
|
if (!parsed)
|
|
63
230
|
continue;
|
|
64
231
|
if (parsed.error && typeof parsed.error === "object") {
|
|
65
232
|
const error = parsed.error;
|
|
66
233
|
const message = typeof error.message === "string" ? error.message : "cli-bridge error";
|
|
67
|
-
yield {
|
|
234
|
+
yield {
|
|
235
|
+
type: "status",
|
|
236
|
+
data: { status: "failed", error: message },
|
|
237
|
+
...(frame.id ? { id: frame.id } : {}),
|
|
238
|
+
};
|
|
68
239
|
throw new Error(`cli-bridge: ${message}`);
|
|
69
240
|
}
|
|
70
241
|
const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
|
|
71
242
|
const delta = choice?.delta;
|
|
72
243
|
const chunk = delta && typeof delta.content === "string" ? delta.content : "";
|
|
73
244
|
const nextUsage = usageFromOpenAi(parsed.usage);
|
|
245
|
+
const frameEvents = [];
|
|
74
246
|
if (nextUsage) {
|
|
75
|
-
|
|
247
|
+
frameEvents.push({ type: "usage", data: {}, usage: nextUsage });
|
|
76
248
|
}
|
|
77
249
|
if (chunk) {
|
|
78
250
|
text += chunk;
|
|
@@ -88,11 +260,11 @@ async function* streamCliBridgeTurn(options, environmentInput, turn, transport)
|
|
|
88
260
|
part,
|
|
89
261
|
delta: chunk,
|
|
90
262
|
};
|
|
91
|
-
|
|
263
|
+
frameEvents.push({
|
|
92
264
|
type: "message.part.updated",
|
|
93
265
|
data: { part, delta: chunk },
|
|
94
266
|
normalized,
|
|
95
|
-
};
|
|
267
|
+
});
|
|
96
268
|
}
|
|
97
269
|
for (const toolCall of toolCallsFromDelta(delta)) {
|
|
98
270
|
const callId = toolCall.id ?? `${messageId}:tool:${toolCall.index}`;
|
|
@@ -112,33 +284,93 @@ async function* streamCliBridgeTurn(options, environmentInput, turn, transport)
|
|
|
112
284
|
type: "message.part.updated",
|
|
113
285
|
part,
|
|
114
286
|
};
|
|
115
|
-
|
|
287
|
+
frameEvents.push({
|
|
116
288
|
type: "message.part.updated",
|
|
117
289
|
data: { part },
|
|
118
290
|
normalized,
|
|
119
|
-
};
|
|
291
|
+
});
|
|
120
292
|
}
|
|
121
293
|
if (choice?.finish_reason) {
|
|
122
294
|
if (choice.finish_reason === "error") {
|
|
123
|
-
|
|
295
|
+
frameEvents.push({
|
|
124
296
|
type: "status",
|
|
125
297
|
data: { status: "failed", error: "cli-bridge returned finish_reason=error" },
|
|
126
|
-
};
|
|
298
|
+
});
|
|
299
|
+
yield* eventsWithCursor(frameEvents, frame.id);
|
|
127
300
|
throw new Error("cli-bridge returned finish_reason=error");
|
|
128
301
|
}
|
|
129
302
|
completed = true;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
303
|
+
if (lastEventId) {
|
|
304
|
+
terminalCursor = frame.id;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
frameEvents.push({
|
|
308
|
+
type: "result",
|
|
309
|
+
data: {
|
|
310
|
+
finalText: text,
|
|
311
|
+
finishReason: choice.finish_reason,
|
|
312
|
+
status: "completed",
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
138
316
|
}
|
|
317
|
+
yield* eventsWithCursor(frameEvents, choice?.finish_reason && lastEventId ? undefined : frame.id);
|
|
139
318
|
}
|
|
140
|
-
if (!completed)
|
|
319
|
+
if (!completed && !lastEventId) {
|
|
141
320
|
throw new Error("cli-bridge stream ended without a terminal result");
|
|
321
|
+
}
|
|
322
|
+
if (lastEventId) {
|
|
323
|
+
const result = await readFullCliBridgeResult(options, requestBody, transport, signal);
|
|
324
|
+
yield {
|
|
325
|
+
type: "result",
|
|
326
|
+
data: {
|
|
327
|
+
finalText: result.text,
|
|
328
|
+
finishReason: result.finishReason,
|
|
329
|
+
status: "completed",
|
|
330
|
+
},
|
|
331
|
+
id: terminalCursor ?? lastEventId,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
class CliBridgeRequestRejectedError extends Error {
|
|
336
|
+
status;
|
|
337
|
+
constructor(status, detail) {
|
|
338
|
+
super(`cli-bridge ${status}: ${detail}`);
|
|
339
|
+
this.status = status;
|
|
340
|
+
this.name = "CliBridgeRequestRejectedError";
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function readFullCliBridgeResult(options, requestBody, transport, signal) {
|
|
344
|
+
const body = safeJson(requestBody);
|
|
345
|
+
if (!body)
|
|
346
|
+
throw new Error("cli-bridge replay request is not valid JSON");
|
|
347
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: requestHeaders(options),
|
|
350
|
+
body: JSON.stringify({ ...body, stream: false }),
|
|
351
|
+
signal,
|
|
352
|
+
});
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
throw new Error(`cli-bridge replay result ${response.status}: ${await response.text()}`);
|
|
355
|
+
}
|
|
356
|
+
const parsed = safeJson(await response.text());
|
|
357
|
+
if (parsed?.error && typeof parsed.error === "object") {
|
|
358
|
+
const error = parsed.error;
|
|
359
|
+
const message = typeof error.message === "string" ? error.message : "cli-bridge replay failed";
|
|
360
|
+
throw new Error(`cli-bridge replay result failed: ${message}`);
|
|
361
|
+
}
|
|
362
|
+
const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : undefined;
|
|
363
|
+
const message = choice?.message && typeof choice.message === "object"
|
|
364
|
+
? choice.message
|
|
365
|
+
: undefined;
|
|
366
|
+
if (typeof message?.content !== "string" ||
|
|
367
|
+
typeof choice?.finish_reason !== "string") {
|
|
368
|
+
throw new Error("cli-bridge replay result returned an invalid completion");
|
|
369
|
+
}
|
|
370
|
+
if (choice.finish_reason === "error" || choice.finish_reason === "timeout") {
|
|
371
|
+
throw new Error(`cli-bridge replay result ended ${choice.finish_reason}`);
|
|
372
|
+
}
|
|
373
|
+
return { text: message.content, finishReason: choice.finish_reason };
|
|
142
374
|
}
|
|
143
375
|
function createTransport(options) {
|
|
144
376
|
if (options.fetch) {
|
|
@@ -167,13 +399,14 @@ function assertTimeout(value, name) {
|
|
|
167
399
|
throw new Error(`createCliBridgeProvider ${name} must be a non-negative integer`);
|
|
168
400
|
}
|
|
169
401
|
}
|
|
170
|
-
function toChatCompletionsBody(options, environmentInput, turn) {
|
|
402
|
+
function toChatCompletionsBody(options, environmentInput, turn, runId) {
|
|
171
403
|
const profile = inlineProfile(environmentInput.profile);
|
|
172
404
|
return {
|
|
173
|
-
model:
|
|
405
|
+
model: resolveBridgeModel(options, environmentInput, turn, profile),
|
|
174
406
|
messages: messagesFromTurn(turn, profile),
|
|
175
407
|
stream: true,
|
|
176
408
|
...(turn.sessionId ? { session_id: turn.sessionId } : {}),
|
|
409
|
+
run_id: runId,
|
|
177
410
|
...(options.defaultMode ? { mode: options.defaultMode } : {}),
|
|
178
411
|
...(profile ? { agent_profile: profile } : {}),
|
|
179
412
|
...(environmentInput.env ? { env: environmentInput.env } : {}),
|
|
@@ -186,6 +419,25 @@ function toChatCompletionsBody(options, environmentInput, turn) {
|
|
|
186
419
|
},
|
|
187
420
|
};
|
|
188
421
|
}
|
|
422
|
+
function resolveBridgeModel(options, environmentInput, turn, profile) {
|
|
423
|
+
const harness = environmentInput.backend ?? profile?.harness;
|
|
424
|
+
const model = turn.model ?? options.defaultModel ?? profile?.model?.default;
|
|
425
|
+
const provider = profile?.model?.provider;
|
|
426
|
+
if (!harness) {
|
|
427
|
+
if (model)
|
|
428
|
+
return model;
|
|
429
|
+
throw new Error("createCliBridgeProvider requires an explicit bridge model or a profile/backend harness");
|
|
430
|
+
}
|
|
431
|
+
if (!model || model === harness)
|
|
432
|
+
return harness;
|
|
433
|
+
if (model.startsWith(`${harness}/`))
|
|
434
|
+
return model;
|
|
435
|
+
if (model.includes("/"))
|
|
436
|
+
return `${harness}/${model}`;
|
|
437
|
+
if (provider)
|
|
438
|
+
return `${harness}/${provider}/${model}`;
|
|
439
|
+
return `${harness}/${model}`;
|
|
440
|
+
}
|
|
189
441
|
function messagesFromTurn(turn, profile) {
|
|
190
442
|
const messages = [];
|
|
191
443
|
const systemPrompt = profile?.prompt?.systemPrompt;
|
|
@@ -223,14 +475,14 @@ async function* parseSse(body) {
|
|
|
223
475
|
const frame = buffer.slice(0, boundary.index);
|
|
224
476
|
buffer = buffer.slice(boundary.index + boundary.length);
|
|
225
477
|
const data = dataFromFrame(frame);
|
|
226
|
-
if (data
|
|
478
|
+
if (data)
|
|
227
479
|
yield data;
|
|
228
480
|
boundary = findFrameBoundary(buffer);
|
|
229
481
|
}
|
|
230
482
|
}
|
|
231
483
|
if (buffer) {
|
|
232
484
|
const data = dataFromFrame(buffer);
|
|
233
|
-
if (data
|
|
485
|
+
if (data)
|
|
234
486
|
yield data;
|
|
235
487
|
}
|
|
236
488
|
}
|
|
@@ -249,7 +501,16 @@ function dataFromFrame(frame) {
|
|
|
249
501
|
.filter((line) => line.startsWith("data:"))
|
|
250
502
|
.map((line) => line.slice("data:".length).trimStart())
|
|
251
503
|
.join("\n");
|
|
252
|
-
|
|
504
|
+
if (!data)
|
|
505
|
+
return undefined;
|
|
506
|
+
const id = lines.find((line) => line.startsWith("id:"))?.slice("id:".length).trim();
|
|
507
|
+
return { data, ...(id ? { id } : {}) };
|
|
508
|
+
}
|
|
509
|
+
function* eventsWithCursor(events, cursor) {
|
|
510
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
511
|
+
const event = events[index];
|
|
512
|
+
yield cursor && index === events.length - 1 ? { ...event, id: cursor } : event;
|
|
513
|
+
}
|
|
253
514
|
}
|
|
254
515
|
function toolCallsFromDelta(value) {
|
|
255
516
|
if (!value || typeof value !== "object")
|
|
@@ -302,6 +563,52 @@ function usageFromOpenAi(value) {
|
|
|
302
563
|
...(cost !== undefined ? { cost } : {}),
|
|
303
564
|
};
|
|
304
565
|
}
|
|
566
|
+
function requestHeaders(options) {
|
|
567
|
+
return {
|
|
568
|
+
"content-type": "application/json",
|
|
569
|
+
...(options.bearerToken ? { authorization: `Bearer ${options.bearerToken}` } : {}),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function cliBridgeRunId(environmentId, turn, turnId) {
|
|
573
|
+
if (turn.executionId &&
|
|
574
|
+
turn.executionId.length <= 128 &&
|
|
575
|
+
/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(turn.executionId)) {
|
|
576
|
+
return turn.executionId;
|
|
577
|
+
}
|
|
578
|
+
const digest = createHash("sha256")
|
|
579
|
+
.update(environmentId)
|
|
580
|
+
.update("\0")
|
|
581
|
+
.update(turn.sessionId ?? "")
|
|
582
|
+
.update("\0")
|
|
583
|
+
.update(turn.executionId ?? turnId)
|
|
584
|
+
.digest("hex");
|
|
585
|
+
return `agent-${digest}`;
|
|
586
|
+
}
|
|
587
|
+
function runSnapshot(value) {
|
|
588
|
+
const parsed = safeJson(value);
|
|
589
|
+
if (!parsed)
|
|
590
|
+
throw new Error("cli-bridge run status returned invalid JSON");
|
|
591
|
+
const id = parsed.id;
|
|
592
|
+
const status = parsed.status;
|
|
593
|
+
const terminal = parsed.terminal;
|
|
594
|
+
if (typeof id !== "string" ||
|
|
595
|
+
!["running", "done", "error", "cancelled"].includes(String(status)) ||
|
|
596
|
+
typeof terminal !== "boolean") {
|
|
597
|
+
throw new Error("cli-bridge run status returned an invalid snapshot");
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
id,
|
|
601
|
+
status: status,
|
|
602
|
+
terminal,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function cancelSnapshot(value) {
|
|
606
|
+
const parsed = safeJson(value);
|
|
607
|
+
if (!parsed || !parsed.run || typeof parsed.run !== "object") {
|
|
608
|
+
throw new Error("cli-bridge cancel returned an invalid snapshot");
|
|
609
|
+
}
|
|
610
|
+
return runSnapshot(JSON.stringify(parsed.run));
|
|
611
|
+
}
|
|
305
612
|
function number(value) {
|
|
306
613
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
307
614
|
}
|
|
@@ -331,7 +638,7 @@ export function defaultCliBridgeCapabilities() {
|
|
|
331
638
|
runtimeUpdate: false,
|
|
332
639
|
validation: false,
|
|
333
640
|
},
|
|
334
|
-
streaming: { live: true, replay:
|
|
641
|
+
streaming: { live: true, replay: true, detach: false, turnIdempotency: true },
|
|
335
642
|
sessions: { continue: true, list: false, messages: false },
|
|
336
643
|
workspace: { read: false, write: false, exec: false, git: false, upload: false, download: false },
|
|
337
644
|
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.3.1",
|
|
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.41.0",
|
|
33
33
|
"undici": "7.28.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|