@tangle-network/agent-provider-cli-bridge 0.4.3 → 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 +31 -1
- package/dist/index.d.ts +5 -27
- package/dist/index.js +46 -614
- 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 +20 -2
package/dist/index.js
CHANGED
|
@@ -1,630 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { harnessSystemPromptIntents, snapshotAgentProfile, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { createCliBridgeEnvironment } from "./retained-environment.js";
|
|
3
|
+
import { assertCliBridgeProviderOptions, } from "./provider-options.js";
|
|
4
4
|
export function createCliBridgeProvider(options) {
|
|
5
|
-
|
|
6
|
-
assertTimeout(options.bodyTimeoutMs, "bodyTimeoutMs");
|
|
7
|
-
assertTimeout(options.cancelWaitMs, "cancelWaitMs");
|
|
5
|
+
assertCliBridgeProviderOptions(options);
|
|
8
6
|
const name = options.name ?? "cli-bridge";
|
|
9
7
|
return {
|
|
10
8
|
name,
|
|
11
9
|
capabilities: () => options.capabilities ?? defaultCliBridgeCapabilities(),
|
|
12
10
|
async create(input) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const runs = new Map();
|
|
16
|
-
const readers = new Set();
|
|
17
|
-
let destroyed = false;
|
|
18
|
-
let closePromise;
|
|
19
|
-
const stream = async function* (turn) {
|
|
20
|
-
if (destroyed)
|
|
21
|
-
throw new Error("cli-bridge environment is destroyed");
|
|
22
|
-
yield* streamTrackedCliBridgeTurn(options, input, turn, transport, environmentId, runs, readers);
|
|
23
|
-
};
|
|
24
|
-
const environment = {
|
|
25
|
-
id: environmentId,
|
|
26
|
-
provider: name,
|
|
27
|
-
...(input.name ? { name: input.name } : {}),
|
|
28
|
-
status: async () => (destroyed ? "stopped" : "running"),
|
|
29
|
-
stream,
|
|
30
|
-
placement: async () => ({
|
|
31
|
-
kind: options.defaultExecution?.kind === "sandbox" ? "sandbox" : "local",
|
|
32
|
-
providerMetadata: { baseUrl: options.baseUrl },
|
|
33
|
-
}),
|
|
34
|
-
async destroy() {
|
|
35
|
-
if (closePromise)
|
|
36
|
-
return closePromise;
|
|
37
|
-
destroyed = true;
|
|
38
|
-
let cancellationsConfirmed = false;
|
|
39
|
-
const attempt = (async () => {
|
|
40
|
-
const cancellations = await Promise.allSettled(Array.from(runs.values()).map(async (run) => {
|
|
41
|
-
const snapshot = await cancelCliBridgeRun(options, transport, run);
|
|
42
|
-
if (runs.get(run.id) === run)
|
|
43
|
-
runs.delete(run.id);
|
|
44
|
-
return snapshot;
|
|
45
|
-
}));
|
|
46
|
-
const failures = cancellations.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
47
|
-
if (failures.length === 1)
|
|
48
|
-
throw failures[0];
|
|
49
|
-
if (failures.length > 1) {
|
|
50
|
-
throw new AggregateError(failures, "cli-bridge environment cancellation failed");
|
|
51
|
-
}
|
|
52
|
-
cancellationsConfirmed = true;
|
|
53
|
-
for (const reader of readers) {
|
|
54
|
-
reader.abort(new DOMException("cli-bridge environment was destroyed", "AbortError"));
|
|
55
|
-
}
|
|
56
|
-
await transport.close();
|
|
57
|
-
})();
|
|
58
|
-
closePromise = attempt;
|
|
59
|
-
try {
|
|
60
|
-
await attempt;
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
closePromise = undefined;
|
|
64
|
-
if (!cancellationsConfirmed)
|
|
65
|
-
destroyed = false;
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
return environment;
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
async function* streamTrackedCliBridgeTurn(options, environmentInput, originalTurn, transport, environmentId, runs, readers) {
|
|
75
|
-
if (originalTurn.detach) {
|
|
76
|
-
throw new Error("cli-bridge provider does not support detached turns");
|
|
77
|
-
}
|
|
78
|
-
const sessionId = originalTurn.sessionId;
|
|
79
|
-
const turnId = originalTurn.turnId ?? crypto.randomUUID();
|
|
80
|
-
const runId = cliBridgeRunId(environmentId, originalTurn, turnId);
|
|
81
|
-
const controller = new AbortController();
|
|
82
|
-
const signals = [
|
|
83
|
-
originalTurn.signal,
|
|
84
|
-
environmentInput.signal,
|
|
85
|
-
controller.signal,
|
|
86
|
-
].filter((signal) => signal !== undefined);
|
|
87
|
-
const turn = {
|
|
88
|
-
...originalTurn,
|
|
89
|
-
turnId,
|
|
90
|
-
...(signals.length > 0 ? { signal: AbortSignal.any(signals) } : {}),
|
|
91
|
-
};
|
|
92
|
-
const requestBody = JSON.stringify(toChatCompletionsBody(options, environmentInput, turn, runId));
|
|
93
|
-
const run = runs.get(runId) ?? {
|
|
94
|
-
id: runId,
|
|
95
|
-
readers: new Set(),
|
|
96
|
-
};
|
|
97
|
-
run.readers.add(controller);
|
|
98
|
-
readers.add(controller);
|
|
99
|
-
runs.set(runId, run);
|
|
100
|
-
let drained = false;
|
|
101
|
-
let threw = false;
|
|
102
|
-
try {
|
|
103
|
-
for await (const event of streamCliBridgeTurn(options, turn, requestBody, transport, runId, originalTurn.lastEventId, signals.length > 0 ? AbortSignal.any(signals) : undefined)) {
|
|
104
|
-
yield event;
|
|
105
|
-
}
|
|
106
|
-
drained = true;
|
|
107
|
-
if (runs.get(run.id) === run)
|
|
108
|
-
runs.delete(run.id);
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
threw = true;
|
|
112
|
-
if (error instanceof CliBridgeRequestRejectedError) {
|
|
113
|
-
if (runs.get(run.id) === run)
|
|
114
|
-
runs.delete(run.id);
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
let snapshot;
|
|
118
|
-
try {
|
|
119
|
-
snapshot = await getCliBridgeRun(options, transport, run.id);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
snapshot = undefined;
|
|
123
|
-
}
|
|
124
|
-
if (snapshot?.terminal) {
|
|
125
|
-
if (runs.get(run.id) === run)
|
|
126
|
-
runs.delete(run.id);
|
|
127
|
-
}
|
|
128
|
-
else if ((originalTurn.signal?.aborted || environmentInput.signal?.aborted)) {
|
|
129
|
-
await cancelCliBridgeRun(options, transport, run);
|
|
130
|
-
if (runs.get(run.id) === run)
|
|
131
|
-
runs.delete(run.id);
|
|
132
|
-
}
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
if (!drained && !threw && runs.get(run.id) === run) {
|
|
137
|
-
await cancelCliBridgeRun(options, transport, run);
|
|
138
|
-
runs.delete(run.id);
|
|
139
|
-
}
|
|
140
|
-
run.readers.delete(controller);
|
|
141
|
-
readers.delete(controller);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async function cancelCliBridgeRun(options, transport, run) {
|
|
145
|
-
if (run.cancellation)
|
|
146
|
-
return run.cancellation;
|
|
147
|
-
run.cancellation = (async () => {
|
|
148
|
-
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(run.id)}/cancel`, {
|
|
149
|
-
method: "POST",
|
|
150
|
-
headers: requestHeaders(options),
|
|
151
|
-
body: "{}",
|
|
152
|
-
});
|
|
153
|
-
if (!response.ok) {
|
|
154
|
-
throw new Error(`cli-bridge cancel ${response.status}: ${await response.text()}`);
|
|
155
|
-
}
|
|
156
|
-
let snapshot = cancelSnapshot(await response.text());
|
|
157
|
-
const waitBudgetMs = options.cancelWaitMs ?? 30_000;
|
|
158
|
-
const deadline = Date.now() + waitBudgetMs;
|
|
159
|
-
while (!snapshot.terminal) {
|
|
160
|
-
const remainingMs = deadline - Date.now();
|
|
161
|
-
if (remainingMs <= 0)
|
|
162
|
-
break;
|
|
163
|
-
snapshot = await getCliBridgeRun(options, transport, run.id, Math.min(remainingMs, 30_000));
|
|
164
|
-
if (snapshot === null) {
|
|
165
|
-
throw new Error(`cli-bridge lost run "${run.id}" before confirming cancellation`);
|
|
11
|
+
if (typeof input.profile === "string") {
|
|
12
|
+
throw new Error(`createCliBridgeProvider requires an inline AgentProfile; named profile "${input.profile}" is unsupported`);
|
|
166
13
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
for (const reader of run.readers) {
|
|
172
|
-
reader.abort(new DOMException(`cli-bridge run ended ${snapshot.status}`, "AbortError"));
|
|
173
|
-
}
|
|
174
|
-
return snapshot;
|
|
175
|
-
})();
|
|
176
|
-
try {
|
|
177
|
-
return await run.cancellation;
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
run.cancellation = undefined;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
async function getCliBridgeRun(options, transport, runId, waitMs) {
|
|
184
|
-
const query = waitMs === undefined ? "" : `?wait_ms=${waitMs}`;
|
|
185
|
-
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(runId)}${query}`, {
|
|
186
|
-
method: "GET",
|
|
187
|
-
headers: requestHeaders(options),
|
|
188
|
-
});
|
|
189
|
-
if (response.status === 404)
|
|
190
|
-
return null;
|
|
191
|
-
if (!response.ok) {
|
|
192
|
-
throw new Error(`cli-bridge run status ${response.status}: ${await response.text()}`);
|
|
193
|
-
}
|
|
194
|
-
return runSnapshot(await response.text());
|
|
195
|
-
}
|
|
196
|
-
async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal) {
|
|
197
|
-
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
198
|
-
method: "POST",
|
|
199
|
-
headers: {
|
|
200
|
-
...requestHeaders(options),
|
|
201
|
-
accept: "text/event-stream",
|
|
202
|
-
...(turn.sessionId ? { "x-session-id": turn.sessionId } : {}),
|
|
203
|
-
...(lastEventId ? { "last-event-id": lastEventId } : {}),
|
|
204
|
-
},
|
|
205
|
-
body: requestBody,
|
|
206
|
-
signal,
|
|
207
|
-
});
|
|
208
|
-
if (!response.ok) {
|
|
209
|
-
let detail = "request rejected";
|
|
210
|
-
try {
|
|
211
|
-
detail = await response.text();
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// The HTTP status already proves this request was rejected.
|
|
215
|
-
}
|
|
216
|
-
throw new CliBridgeRequestRejectedError(response.status, detail);
|
|
217
|
-
}
|
|
218
|
-
if (!response.body)
|
|
219
|
-
throw new Error("cli-bridge response body is empty");
|
|
220
|
-
let text = "";
|
|
221
|
-
const sessionId = turn.sessionId ?? runId;
|
|
222
|
-
const messageId = turn.turnId ?? `${sessionId}:assistant`;
|
|
223
|
-
const emittedToolCalls = new Set();
|
|
224
|
-
let completed = false;
|
|
225
|
-
let terminalCursor;
|
|
226
|
-
for await (const frame of parseSse(response.body)) {
|
|
227
|
-
if (frame.data === "[DONE]")
|
|
228
|
-
continue;
|
|
229
|
-
const parsed = safeJson(frame.data);
|
|
230
|
-
if (!parsed)
|
|
231
|
-
continue;
|
|
232
|
-
if (parsed.error && typeof parsed.error === "object") {
|
|
233
|
-
const error = parsed.error;
|
|
234
|
-
const message = typeof error.message === "string" ? error.message : "cli-bridge error";
|
|
235
|
-
yield {
|
|
236
|
-
type: "status",
|
|
237
|
-
data: { status: "failed", error: message },
|
|
238
|
-
...(frame.id ? { id: frame.id } : {}),
|
|
239
|
-
};
|
|
240
|
-
throw new Error(`cli-bridge: ${message}`);
|
|
241
|
-
}
|
|
242
|
-
const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
|
|
243
|
-
const delta = choice?.delta;
|
|
244
|
-
const chunk = delta && typeof delta.content === "string" ? delta.content : "";
|
|
245
|
-
const nextUsage = usageFromOpenAi(parsed.usage);
|
|
246
|
-
const frameEvents = [];
|
|
247
|
-
if (nextUsage) {
|
|
248
|
-
frameEvents.push({ type: "usage", data: {}, usage: nextUsage });
|
|
249
|
-
}
|
|
250
|
-
if (chunk) {
|
|
251
|
-
text += chunk;
|
|
252
|
-
const part = {
|
|
253
|
-
id: `${messageId}:text`,
|
|
254
|
-
sessionID: sessionId,
|
|
255
|
-
messageID: messageId,
|
|
256
|
-
type: "text",
|
|
257
|
-
text,
|
|
258
|
-
};
|
|
259
|
-
const normalized = {
|
|
260
|
-
type: "message.part.updated",
|
|
261
|
-
part,
|
|
262
|
-
delta: chunk,
|
|
263
|
-
};
|
|
264
|
-
frameEvents.push({
|
|
265
|
-
type: "message.part.updated",
|
|
266
|
-
data: { part, delta: chunk },
|
|
267
|
-
normalized,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
for (const toolCall of toolCallsFromDelta(delta)) {
|
|
271
|
-
const callId = toolCall.id ?? `${messageId}:tool:${toolCall.index}`;
|
|
272
|
-
if (!toolCall.name || emittedToolCalls.has(callId))
|
|
273
|
-
continue;
|
|
274
|
-
emittedToolCalls.add(callId);
|
|
275
|
-
const part = {
|
|
276
|
-
id: callId,
|
|
277
|
-
sessionID: sessionId,
|
|
278
|
-
messageID: messageId,
|
|
279
|
-
type: "tool",
|
|
280
|
-
callID: callId,
|
|
281
|
-
tool: toolCall.name,
|
|
282
|
-
state: { status: "pending", input: {} },
|
|
14
|
+
const environmentInput = {
|
|
15
|
+
...input,
|
|
16
|
+
profile: snapshotAgentProfile(input.profile),
|
|
283
17
|
};
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
18
|
+
const environmentId = input.idempotencyKey ?? crypto.randomUUID();
|
|
19
|
+
return createCliBridgeEnvironment({
|
|
20
|
+
options,
|
|
21
|
+
providerName: name,
|
|
22
|
+
environmentInput,
|
|
23
|
+
environmentId,
|
|
24
|
+
allowDispatch: true,
|
|
25
|
+
cancelRunsOnDestroy: true,
|
|
292
26
|
});
|
|
293
|
-
}
|
|
294
|
-
if (choice?.finish_reason) {
|
|
295
|
-
if (choice.finish_reason === "error") {
|
|
296
|
-
frameEvents.push({
|
|
297
|
-
type: "status",
|
|
298
|
-
data: { status: "failed", error: "cli-bridge returned finish_reason=error" },
|
|
299
|
-
});
|
|
300
|
-
yield* eventsWithCursor(frameEvents, frame.id);
|
|
301
|
-
throw new Error("cli-bridge returned finish_reason=error");
|
|
302
|
-
}
|
|
303
|
-
completed = true;
|
|
304
|
-
if (lastEventId) {
|
|
305
|
-
terminalCursor = frame.id;
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
frameEvents.push({
|
|
309
|
-
type: "result",
|
|
310
|
-
data: {
|
|
311
|
-
finalText: text,
|
|
312
|
-
finishReason: choice.finish_reason,
|
|
313
|
-
status: "completed",
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
yield* eventsWithCursor(frameEvents, choice?.finish_reason && lastEventId ? undefined : frame.id);
|
|
319
|
-
}
|
|
320
|
-
if (!completed && !lastEventId) {
|
|
321
|
-
throw new Error("cli-bridge stream ended without a terminal result");
|
|
322
|
-
}
|
|
323
|
-
if (lastEventId) {
|
|
324
|
-
const result = await readFullCliBridgeResult(options, requestBody, transport, signal);
|
|
325
|
-
yield {
|
|
326
|
-
type: "result",
|
|
327
|
-
data: {
|
|
328
|
-
finalText: result.text,
|
|
329
|
-
finishReason: result.finishReason,
|
|
330
|
-
status: "completed",
|
|
331
|
-
},
|
|
332
|
-
id: terminalCursor ?? lastEventId,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
class CliBridgeRequestRejectedError extends Error {
|
|
337
|
-
status;
|
|
338
|
-
constructor(status, detail) {
|
|
339
|
-
super(`cli-bridge ${status}: ${detail}`);
|
|
340
|
-
this.status = status;
|
|
341
|
-
this.name = "CliBridgeRequestRejectedError";
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
async function readFullCliBridgeResult(options, requestBody, transport, signal) {
|
|
345
|
-
const body = safeJson(requestBody);
|
|
346
|
-
if (!body)
|
|
347
|
-
throw new Error("cli-bridge replay request is not valid JSON");
|
|
348
|
-
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
349
|
-
method: "POST",
|
|
350
|
-
headers: requestHeaders(options),
|
|
351
|
-
body: JSON.stringify({ ...body, stream: false }),
|
|
352
|
-
signal,
|
|
353
|
-
});
|
|
354
|
-
if (!response.ok) {
|
|
355
|
-
throw new Error(`cli-bridge replay result ${response.status}: ${await response.text()}`);
|
|
356
|
-
}
|
|
357
|
-
const parsed = safeJson(await response.text());
|
|
358
|
-
if (parsed?.error && typeof parsed.error === "object") {
|
|
359
|
-
const error = parsed.error;
|
|
360
|
-
const message = typeof error.message === "string" ? error.message : "cli-bridge replay failed";
|
|
361
|
-
throw new Error(`cli-bridge replay result failed: ${message}`);
|
|
362
|
-
}
|
|
363
|
-
const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : undefined;
|
|
364
|
-
const message = choice?.message && typeof choice.message === "object"
|
|
365
|
-
? choice.message
|
|
366
|
-
: undefined;
|
|
367
|
-
if (typeof message?.content !== "string" ||
|
|
368
|
-
typeof choice?.finish_reason !== "string") {
|
|
369
|
-
throw new Error("cli-bridge replay result returned an invalid completion");
|
|
370
|
-
}
|
|
371
|
-
if (choice.finish_reason === "error" || choice.finish_reason === "timeout") {
|
|
372
|
-
throw new Error(`cli-bridge replay result ended ${choice.finish_reason}`);
|
|
373
|
-
}
|
|
374
|
-
return { text: message.content, finishReason: choice.finish_reason };
|
|
375
|
-
}
|
|
376
|
-
function createTransport(options) {
|
|
377
|
-
if (options.fetch) {
|
|
378
|
-
const fetch = options.fetch;
|
|
379
|
-
return {
|
|
380
|
-
fetch: (input, init) => fetch(input, init),
|
|
381
|
-
close: async () => { },
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
const dispatcher = new Agent({
|
|
385
|
-
headersTimeout: options.headersTimeoutMs ?? 0,
|
|
386
|
-
bodyTimeout: options.bodyTimeoutMs ?? 0,
|
|
387
|
-
});
|
|
388
|
-
return {
|
|
389
|
-
fetch: (input, init) => undiciFetch(input, {
|
|
390
|
-
...init,
|
|
391
|
-
dispatcher,
|
|
392
|
-
}),
|
|
393
|
-
close: async () => {
|
|
394
|
-
await dispatcher.close();
|
|
395
27
|
},
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
stream: true,
|
|
409
|
-
...(turn.sessionId ? { session_id: turn.sessionId } : {}),
|
|
410
|
-
run_id: runId,
|
|
411
|
-
...(options.defaultMode ? { mode: options.defaultMode } : {}),
|
|
412
|
-
...(profile ? { agent_profile: profile } : {}),
|
|
413
|
-
...(environmentInput.env ? { env: environmentInput.env } : {}),
|
|
414
|
-
...(environmentInput.workspace?.cwd ? { cwd: environmentInput.workspace.cwd } : {}),
|
|
415
|
-
...(executionFromInput(options, environmentInput) ? { execution: executionFromInput(options, environmentInput) } : {}),
|
|
416
|
-
metadata: {
|
|
417
|
-
...(environmentInput.metadata ?? {}),
|
|
418
|
-
...(turn.context ?? {}),
|
|
419
|
-
...(turn.providerOptions ?? {}),
|
|
28
|
+
async get(id) {
|
|
29
|
+
if (id.length === 0 || id.trim() !== id) {
|
|
30
|
+
throw new Error("cli-bridge environment id must be non-empty and have no outer whitespace");
|
|
31
|
+
}
|
|
32
|
+
return createCliBridgeEnvironment({
|
|
33
|
+
options,
|
|
34
|
+
providerName: name,
|
|
35
|
+
environmentInput: { profile: { name: "reconnected" }, idempotencyKey: id },
|
|
36
|
+
environmentId: id,
|
|
37
|
+
allowDispatch: false,
|
|
38
|
+
cancelRunsOnDestroy: false,
|
|
39
|
+
});
|
|
420
40
|
},
|
|
421
41
|
};
|
|
422
42
|
}
|
|
423
|
-
function resolveBridgeModel(options, environmentInput, turn, profile) {
|
|
424
|
-
const harness = environmentInput.backend ?? profile?.harness;
|
|
425
|
-
const model = turn.model ?? options.defaultModel ?? profile?.model?.default;
|
|
426
|
-
const provider = profile?.model?.provider;
|
|
427
|
-
if (!harness) {
|
|
428
|
-
if (model)
|
|
429
|
-
return model;
|
|
430
|
-
throw new Error("createCliBridgeProvider requires an explicit bridge model or a profile/backend harness");
|
|
431
|
-
}
|
|
432
|
-
if (!model || model === harness)
|
|
433
|
-
return harness;
|
|
434
|
-
if (model.startsWith(`${harness}/`))
|
|
435
|
-
return model;
|
|
436
|
-
if (model.includes("/"))
|
|
437
|
-
return `${harness}/${model}`;
|
|
438
|
-
if (provider)
|
|
439
|
-
return `${harness}/${provider}/${model}`;
|
|
440
|
-
return `${harness}/${model}`;
|
|
441
|
-
}
|
|
442
43
|
/**
|
|
443
|
-
*
|
|
44
|
+
* Describe the bridge backend, not this adapter.
|
|
444
45
|
*
|
|
445
|
-
*
|
|
446
|
-
* the harness's own prompt; lowering it here would have added it on top of that prompt instead —
|
|
447
|
-
* the opposite intent, invisible to the caller. Both prompt intents ride `agent_profile` on the
|
|
448
|
-
* same request (see {@link toChatCompletionsBody}), where the bridge binds each to the control its
|
|
449
|
-
* harness actually owns or refuses it. The bridge additionally rejects any request that carries
|
|
450
|
-
* system-role messages beside `agent_profile`, so synthesizing one here failed the request outright.
|
|
451
|
-
*/
|
|
452
|
-
function messagesFromTurn(turn) {
|
|
453
|
-
return [{ role: "user", content: contentFromTurn(turn) }];
|
|
454
|
-
}
|
|
455
|
-
function contentFromTurn(turn) {
|
|
456
|
-
if (turn.parts)
|
|
457
|
-
return turn.parts;
|
|
458
|
-
return turn.prompt ?? "";
|
|
459
|
-
}
|
|
460
|
-
function inlineProfile(profile) {
|
|
461
|
-
return typeof profile === "string" ? undefined : profile;
|
|
462
|
-
}
|
|
463
|
-
function executionFromInput(options, input) {
|
|
464
|
-
if (options.defaultExecution)
|
|
465
|
-
return options.defaultExecution;
|
|
466
|
-
if (!input.workspace?.repoUrl)
|
|
467
|
-
return undefined;
|
|
468
|
-
return {
|
|
469
|
-
kind: "sandbox",
|
|
470
|
-
repoUrl: input.workspace.repoUrl,
|
|
471
|
-
...(input.workspace.gitRef ? { gitRef: input.workspace.gitRef } : {}),
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
async function* parseSse(body) {
|
|
475
|
-
const decoder = new TextDecoder();
|
|
476
|
-
let buffer = "";
|
|
477
|
-
for await (const value of body) {
|
|
478
|
-
buffer += decoder.decode(value, { stream: true });
|
|
479
|
-
let boundary = findFrameBoundary(buffer);
|
|
480
|
-
while (boundary) {
|
|
481
|
-
const frame = buffer.slice(0, boundary.index);
|
|
482
|
-
buffer = buffer.slice(boundary.index + boundary.length);
|
|
483
|
-
const data = dataFromFrame(frame);
|
|
484
|
-
if (data)
|
|
485
|
-
yield data;
|
|
486
|
-
boundary = findFrameBoundary(buffer);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
if (buffer) {
|
|
490
|
-
const data = dataFromFrame(buffer);
|
|
491
|
-
if (data)
|
|
492
|
-
yield data;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
function findFrameBoundary(value) {
|
|
496
|
-
const lf = value.indexOf("\n\n");
|
|
497
|
-
const crlf = value.indexOf("\r\n\r\n");
|
|
498
|
-
if (lf === -1 && crlf === -1)
|
|
499
|
-
return undefined;
|
|
500
|
-
if (crlf !== -1 && (lf === -1 || crlf < lf))
|
|
501
|
-
return { index: crlf, length: 4 };
|
|
502
|
-
return { index: lf, length: 2 };
|
|
503
|
-
}
|
|
504
|
-
function dataFromFrame(frame) {
|
|
505
|
-
const lines = frame.split(/\r?\n/);
|
|
506
|
-
const data = lines
|
|
507
|
-
.filter((line) => line.startsWith("data:"))
|
|
508
|
-
.map((line) => line.slice("data:".length).trimStart())
|
|
509
|
-
.join("\n");
|
|
510
|
-
if (!data)
|
|
511
|
-
return undefined;
|
|
512
|
-
const id = lines.find((line) => line.startsWith("id:"))?.slice("id:".length).trim();
|
|
513
|
-
return { data, ...(id ? { id } : {}) };
|
|
514
|
-
}
|
|
515
|
-
function* eventsWithCursor(events, cursor) {
|
|
516
|
-
for (let index = 0; index < events.length; index += 1) {
|
|
517
|
-
const event = events[index];
|
|
518
|
-
yield cursor && index === events.length - 1 ? { ...event, id: cursor } : event;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
function toolCallsFromDelta(value) {
|
|
522
|
-
if (!value || typeof value !== "object")
|
|
523
|
-
return [];
|
|
524
|
-
const calls = value.tool_calls;
|
|
525
|
-
if (!Array.isArray(calls))
|
|
526
|
-
return [];
|
|
527
|
-
return calls.flatMap((call, position) => {
|
|
528
|
-
if (!call || typeof call !== "object")
|
|
529
|
-
return [];
|
|
530
|
-
const record = call;
|
|
531
|
-
const fn = record.function;
|
|
532
|
-
const name = fn && typeof fn === "object" && typeof fn.name === "string"
|
|
533
|
-
? fn.name
|
|
534
|
-
: undefined;
|
|
535
|
-
const index = number(record.index) ?? position;
|
|
536
|
-
const id = typeof record.id === "string" ? record.id : undefined;
|
|
537
|
-
return [{ ...(id ? { id } : {}), index, ...(name ? { name } : {}) }];
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
function safeJson(value) {
|
|
541
|
-
try {
|
|
542
|
-
const parsed = JSON.parse(value);
|
|
543
|
-
return parsed && typeof parsed === "object" ? parsed : null;
|
|
544
|
-
}
|
|
545
|
-
catch {
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
function usageFromOpenAi(value) {
|
|
550
|
-
if (!value || typeof value !== "object")
|
|
551
|
-
return undefined;
|
|
552
|
-
const record = value;
|
|
553
|
-
const inputTokens = number(record.prompt_tokens) ?? number(record.input_tokens);
|
|
554
|
-
const outputTokens = number(record.completion_tokens) ?? number(record.output_tokens);
|
|
555
|
-
if (inputTokens === undefined && outputTokens === undefined)
|
|
556
|
-
return undefined;
|
|
557
|
-
const totalTokens = number(record.total_tokens);
|
|
558
|
-
const cacheReadInputTokens = number(record.cache_read_input_tokens);
|
|
559
|
-
const cacheCreationInputTokens = number(record.cache_creation_input_tokens);
|
|
560
|
-
const reasoningTokens = number(record.reasoning_tokens);
|
|
561
|
-
const cost = number(record.cost);
|
|
562
|
-
return {
|
|
563
|
-
inputTokens: inputTokens ?? 0,
|
|
564
|
-
outputTokens: outputTokens ?? 0,
|
|
565
|
-
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
566
|
-
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
567
|
-
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
568
|
-
...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
|
|
569
|
-
...(cost !== undefined ? { cost } : {}),
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
function requestHeaders(options) {
|
|
573
|
-
return {
|
|
574
|
-
"content-type": "application/json",
|
|
575
|
-
...(options.bearerToken ? { authorization: `Bearer ${options.bearerToken}` } : {}),
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
function cliBridgeRunId(environmentId, turn, turnId) {
|
|
579
|
-
if (turn.executionId &&
|
|
580
|
-
turn.executionId.length <= 128 &&
|
|
581
|
-
/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(turn.executionId)) {
|
|
582
|
-
return turn.executionId;
|
|
583
|
-
}
|
|
584
|
-
const digest = createHash("sha256")
|
|
585
|
-
.update(environmentId)
|
|
586
|
-
.update("\0")
|
|
587
|
-
.update(turn.sessionId ?? "")
|
|
588
|
-
.update("\0")
|
|
589
|
-
.update(turn.executionId ?? turnId)
|
|
590
|
-
.digest("hex");
|
|
591
|
-
return `agent-${digest}`;
|
|
592
|
-
}
|
|
593
|
-
function runSnapshot(value) {
|
|
594
|
-
const parsed = safeJson(value);
|
|
595
|
-
if (!parsed)
|
|
596
|
-
throw new Error("cli-bridge run status returned invalid JSON");
|
|
597
|
-
const id = parsed.id;
|
|
598
|
-
const status = parsed.status;
|
|
599
|
-
const terminal = parsed.terminal;
|
|
600
|
-
if (typeof id !== "string" ||
|
|
601
|
-
!["running", "done", "error", "cancelled"].includes(String(status)) ||
|
|
602
|
-
typeof terminal !== "boolean") {
|
|
603
|
-
throw new Error("cli-bridge run status returned an invalid snapshot");
|
|
604
|
-
}
|
|
605
|
-
return {
|
|
606
|
-
id,
|
|
607
|
-
status: status,
|
|
608
|
-
terminal,
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
function cancelSnapshot(value) {
|
|
612
|
-
const parsed = safeJson(value);
|
|
613
|
-
if (!parsed || !parsed.run || typeof parsed.run !== "object") {
|
|
614
|
-
throw new Error("cli-bridge cancel returned an invalid snapshot");
|
|
615
|
-
}
|
|
616
|
-
return runSnapshot(JSON.stringify(parsed.run));
|
|
617
|
-
}
|
|
618
|
-
function number(value) {
|
|
619
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
620
|
-
}
|
|
621
|
-
function trimSlash(value) {
|
|
622
|
-
return value.replace(/\/+$/, "");
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* @param harness The bridge backend these capabilities describe. The prompt intents belong to that
|
|
626
|
-
* harness, not to this adapter — the adapter only forwards `agent_profile`. Omit it and both intents
|
|
627
|
-
* declare `false`: an adapter that cannot name its harness cannot promise either one.
|
|
46
|
+
* The adapter forwards an AgentProfile but does not own harness prompt controls.
|
|
628
47
|
*/
|
|
629
48
|
export function defaultCliBridgeCapabilities(harness) {
|
|
630
49
|
return {
|
|
@@ -649,9 +68,22 @@ export function defaultCliBridgeCapabilities(harness) {
|
|
|
649
68
|
runtimeUpdate: false,
|
|
650
69
|
validation: false,
|
|
651
70
|
},
|
|
652
|
-
streaming: { live: true, replay: true, detach:
|
|
71
|
+
streaming: { live: true, replay: true, detach: true, turnIdempotency: true },
|
|
653
72
|
sessions: { continue: true, list: false, messages: false },
|
|
654
|
-
|
|
73
|
+
retainedControl: {
|
|
74
|
+
exactRunIdentity: true,
|
|
75
|
+
resultIdentity: true,
|
|
76
|
+
eventIdentity: true,
|
|
77
|
+
cancellationIdempotency: true,
|
|
78
|
+
},
|
|
79
|
+
workspace: {
|
|
80
|
+
read: false,
|
|
81
|
+
write: false,
|
|
82
|
+
exec: false,
|
|
83
|
+
git: false,
|
|
84
|
+
upload: false,
|
|
85
|
+
download: false,
|
|
86
|
+
},
|
|
655
87
|
branching: { checkpoint: false, fork: false },
|
|
656
88
|
placement: true,
|
|
657
89
|
usage: true,
|