@tangle-network/agent-provider-cli-bridge 0.2.26 → 0.2.28
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 +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +154 -45
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,3 +11,6 @@ const provider = createCliBridgeProvider({
|
|
|
11
11
|
defaultModel: 'codex',
|
|
12
12
|
})
|
|
13
13
|
```
|
|
14
|
+
|
|
15
|
+
Response headers and streamed bodies have no transport timeout by default.
|
|
16
|
+
For unattended runs, set `headersTimeoutMs`, `bodyTimeoutMs`, or an `AbortSignal` so an unresponsive bridge cannot wait forever.
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface CliBridgeProviderOptions {
|
|
|
13
13
|
capability?: string;
|
|
14
14
|
ttlSeconds?: number;
|
|
15
15
|
};
|
|
16
|
+
/** Maximum wait for response headers. Defaults to no timeout. */
|
|
17
|
+
headersTimeoutMs?: number;
|
|
18
|
+
/** Maximum idle time between response body chunks. Defaults to no timeout. */
|
|
19
|
+
bodyTimeoutMs?: number;
|
|
16
20
|
fetch?: typeof fetch;
|
|
17
21
|
name?: string;
|
|
18
22
|
capabilities?: AgentEnvironmentCapabilities;
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,40 @@
|
|
|
1
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
1
2
|
export function createCliBridgeProvider(options) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
throw new Error("createCliBridgeProvider requires fetch");
|
|
3
|
+
assertTimeout(options.headersTimeoutMs, "headersTimeoutMs");
|
|
4
|
+
assertTimeout(options.bodyTimeoutMs, "bodyTimeoutMs");
|
|
5
5
|
const name = options.name ?? "cli-bridge";
|
|
6
6
|
return {
|
|
7
7
|
name,
|
|
8
8
|
capabilities: () => options.capabilities ?? defaultCliBridgeCapabilities(),
|
|
9
9
|
async create(input) {
|
|
10
|
+
const transport = createTransport(options);
|
|
11
|
+
let destroyed = false;
|
|
12
|
+
let closePromise;
|
|
10
13
|
return {
|
|
11
14
|
id: input.idempotencyKey ?? input.name ?? crypto.randomUUID(),
|
|
12
15
|
provider: name,
|
|
13
16
|
...(input.name ? { name: input.name } : {}),
|
|
14
|
-
status: async () => "running",
|
|
15
|
-
stream: (turn) =>
|
|
17
|
+
status: async () => (destroyed ? "stopped" : "running"),
|
|
18
|
+
stream: (turn) => {
|
|
19
|
+
if (destroyed)
|
|
20
|
+
throw new Error("cli-bridge environment is destroyed");
|
|
21
|
+
return streamCliBridgeTurn(options, input, turn, transport);
|
|
22
|
+
},
|
|
16
23
|
placement: async () => ({
|
|
17
24
|
kind: options.defaultExecution?.kind === "sandbox" ? "sandbox" : "local",
|
|
18
25
|
providerMetadata: { baseUrl: options.baseUrl },
|
|
19
26
|
}),
|
|
20
|
-
destroy:
|
|
27
|
+
destroy: () => {
|
|
28
|
+
destroyed = true;
|
|
29
|
+
closePromise ??= transport.close();
|
|
30
|
+
return closePromise;
|
|
31
|
+
},
|
|
21
32
|
};
|
|
22
33
|
},
|
|
23
34
|
};
|
|
24
35
|
}
|
|
25
|
-
async function* streamCliBridgeTurn(
|
|
26
|
-
const response = await
|
|
36
|
+
async function* streamCliBridgeTurn(options, environmentInput, turn, transport) {
|
|
37
|
+
const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
|
|
27
38
|
method: "POST",
|
|
28
39
|
headers: {
|
|
29
40
|
"content-type": "application/json",
|
|
@@ -40,10 +51,13 @@ async function* streamCliBridgeTurn(fetchImpl, options, environmentInput, turn)
|
|
|
40
51
|
if (!response.body)
|
|
41
52
|
throw new Error("cli-bridge response body is empty");
|
|
42
53
|
let text = "";
|
|
43
|
-
|
|
54
|
+
const sessionId = turn.sessionId ?? environmentInput.idempotencyKey ?? environmentInput.name ?? "cli-bridge";
|
|
55
|
+
const messageId = turn.turnId ?? `${sessionId}:assistant`;
|
|
56
|
+
const emittedToolCalls = new Set();
|
|
57
|
+
let completed = false;
|
|
44
58
|
for await (const event of parseSse(response.body)) {
|
|
45
59
|
if (event === "[DONE]")
|
|
46
|
-
|
|
60
|
+
continue;
|
|
47
61
|
const parsed = safeJson(event);
|
|
48
62
|
if (!parsed)
|
|
49
63
|
continue;
|
|
@@ -51,35 +65,107 @@ async function* streamCliBridgeTurn(fetchImpl, options, environmentInput, turn)
|
|
|
51
65
|
const error = parsed.error;
|
|
52
66
|
const message = typeof error.message === "string" ? error.message : "cli-bridge error";
|
|
53
67
|
yield { type: "status", data: { status: "failed", error: message } };
|
|
54
|
-
|
|
68
|
+
throw new Error(`cli-bridge: ${message}`);
|
|
55
69
|
}
|
|
56
70
|
const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
|
|
57
71
|
const delta = choice?.delta;
|
|
58
72
|
const chunk = delta && typeof delta.content === "string" ? delta.content : "";
|
|
59
73
|
const nextUsage = usageFromOpenAi(parsed.usage);
|
|
60
74
|
if (nextUsage) {
|
|
61
|
-
|
|
62
|
-
yield { type: "usage", data: { usage: nextUsage }, usage: nextUsage };
|
|
75
|
+
yield { type: "usage", data: {}, usage: nextUsage };
|
|
63
76
|
}
|
|
64
77
|
if (chunk) {
|
|
65
78
|
text += chunk;
|
|
79
|
+
const part = {
|
|
80
|
+
id: `${messageId}:text`,
|
|
81
|
+
sessionID: sessionId,
|
|
82
|
+
messageID: messageId,
|
|
83
|
+
type: "text",
|
|
84
|
+
text,
|
|
85
|
+
};
|
|
86
|
+
const normalized = {
|
|
87
|
+
type: "message.part.updated",
|
|
88
|
+
part,
|
|
89
|
+
delta: chunk,
|
|
90
|
+
};
|
|
91
|
+
yield {
|
|
92
|
+
type: "message.part.updated",
|
|
93
|
+
data: { part, delta: chunk },
|
|
94
|
+
normalized,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
for (const toolCall of toolCallsFromDelta(delta)) {
|
|
98
|
+
const callId = toolCall.id ?? `${messageId}:tool:${toolCall.index}`;
|
|
99
|
+
if (!toolCall.name || emittedToolCalls.has(callId))
|
|
100
|
+
continue;
|
|
101
|
+
emittedToolCalls.add(callId);
|
|
102
|
+
const part = {
|
|
103
|
+
id: callId,
|
|
104
|
+
sessionID: sessionId,
|
|
105
|
+
messageID: messageId,
|
|
106
|
+
type: "tool",
|
|
107
|
+
callID: callId,
|
|
108
|
+
tool: toolCall.name,
|
|
109
|
+
state: { status: "pending", input: {} },
|
|
110
|
+
};
|
|
111
|
+
const normalized = {
|
|
112
|
+
type: "message.part.updated",
|
|
113
|
+
part,
|
|
114
|
+
};
|
|
66
115
|
yield {
|
|
67
116
|
type: "message.part.updated",
|
|
68
|
-
data: {
|
|
117
|
+
data: { part },
|
|
118
|
+
normalized,
|
|
69
119
|
};
|
|
70
120
|
}
|
|
71
121
|
if (choice?.finish_reason) {
|
|
122
|
+
if (choice.finish_reason === "error") {
|
|
123
|
+
yield {
|
|
124
|
+
type: "status",
|
|
125
|
+
data: { status: "failed", error: "cli-bridge returned finish_reason=error" },
|
|
126
|
+
};
|
|
127
|
+
throw new Error("cli-bridge returned finish_reason=error");
|
|
128
|
+
}
|
|
129
|
+
completed = true;
|
|
72
130
|
yield {
|
|
73
131
|
type: "result",
|
|
74
132
|
data: {
|
|
75
133
|
finalText: text,
|
|
76
134
|
finishReason: choice.finish_reason,
|
|
77
|
-
status:
|
|
135
|
+
status: "completed",
|
|
78
136
|
},
|
|
79
|
-
...(usage ? { usage } : {}),
|
|
80
137
|
};
|
|
81
138
|
}
|
|
82
139
|
}
|
|
140
|
+
if (!completed)
|
|
141
|
+
throw new Error("cli-bridge stream ended without a terminal result");
|
|
142
|
+
}
|
|
143
|
+
function createTransport(options) {
|
|
144
|
+
if (options.fetch) {
|
|
145
|
+
const fetch = options.fetch;
|
|
146
|
+
return {
|
|
147
|
+
fetch: (input, init) => fetch(input, init),
|
|
148
|
+
close: async () => { },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const dispatcher = new Agent({
|
|
152
|
+
headersTimeout: options.headersTimeoutMs ?? 0,
|
|
153
|
+
bodyTimeout: options.bodyTimeoutMs ?? 0,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
fetch: (input, init) => undiciFetch(input, {
|
|
157
|
+
...init,
|
|
158
|
+
dispatcher,
|
|
159
|
+
}),
|
|
160
|
+
close: async () => {
|
|
161
|
+
await dispatcher.close();
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function assertTimeout(value, name) {
|
|
166
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
|
167
|
+
throw new Error(`createCliBridgeProvider ${name} must be a non-negative integer`);
|
|
168
|
+
}
|
|
83
169
|
}
|
|
84
170
|
function toChatCompletionsBody(options, environmentInput, turn) {
|
|
85
171
|
const profile = inlineProfile(environmentInput.profile);
|
|
@@ -128,35 +214,35 @@ function executionFromInput(options, input) {
|
|
|
128
214
|
};
|
|
129
215
|
}
|
|
130
216
|
async function* parseSse(body) {
|
|
131
|
-
const reader = body.getReader();
|
|
132
217
|
const decoder = new TextDecoder();
|
|
133
218
|
let buffer = "";
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
buffer
|
|
140
|
-
|
|
141
|
-
while (boundary !== -1) {
|
|
142
|
-
const frame = buffer.slice(0, boundary);
|
|
143
|
-
buffer = buffer.slice(boundary + 2);
|
|
144
|
-
const data = dataFromFrame(frame);
|
|
145
|
-
if (data !== undefined)
|
|
146
|
-
yield data;
|
|
147
|
-
boundary = buffer.indexOf("\n\n");
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (buffer) {
|
|
151
|
-
const data = dataFromFrame(buffer);
|
|
219
|
+
for await (const value of body) {
|
|
220
|
+
buffer += decoder.decode(value, { stream: true });
|
|
221
|
+
let boundary = findFrameBoundary(buffer);
|
|
222
|
+
while (boundary) {
|
|
223
|
+
const frame = buffer.slice(0, boundary.index);
|
|
224
|
+
buffer = buffer.slice(boundary.index + boundary.length);
|
|
225
|
+
const data = dataFromFrame(frame);
|
|
152
226
|
if (data !== undefined)
|
|
153
227
|
yield data;
|
|
228
|
+
boundary = findFrameBoundary(buffer);
|
|
154
229
|
}
|
|
155
230
|
}
|
|
156
|
-
|
|
157
|
-
|
|
231
|
+
if (buffer) {
|
|
232
|
+
const data = dataFromFrame(buffer);
|
|
233
|
+
if (data !== undefined)
|
|
234
|
+
yield data;
|
|
158
235
|
}
|
|
159
236
|
}
|
|
237
|
+
function findFrameBoundary(value) {
|
|
238
|
+
const lf = value.indexOf("\n\n");
|
|
239
|
+
const crlf = value.indexOf("\r\n\r\n");
|
|
240
|
+
if (lf === -1 && crlf === -1)
|
|
241
|
+
return undefined;
|
|
242
|
+
if (crlf !== -1 && (lf === -1 || crlf < lf))
|
|
243
|
+
return { index: crlf, length: 4 };
|
|
244
|
+
return { index: lf, length: 2 };
|
|
245
|
+
}
|
|
160
246
|
function dataFromFrame(frame) {
|
|
161
247
|
const lines = frame.split(/\r?\n/);
|
|
162
248
|
const data = lines
|
|
@@ -165,6 +251,25 @@ function dataFromFrame(frame) {
|
|
|
165
251
|
.join("\n");
|
|
166
252
|
return data || undefined;
|
|
167
253
|
}
|
|
254
|
+
function toolCallsFromDelta(value) {
|
|
255
|
+
if (!value || typeof value !== "object")
|
|
256
|
+
return [];
|
|
257
|
+
const calls = value.tool_calls;
|
|
258
|
+
if (!Array.isArray(calls))
|
|
259
|
+
return [];
|
|
260
|
+
return calls.flatMap((call, position) => {
|
|
261
|
+
if (!call || typeof call !== "object")
|
|
262
|
+
return [];
|
|
263
|
+
const record = call;
|
|
264
|
+
const fn = record.function;
|
|
265
|
+
const name = fn && typeof fn === "object" && typeof fn.name === "string"
|
|
266
|
+
? fn.name
|
|
267
|
+
: undefined;
|
|
268
|
+
const index = number(record.index) ?? position;
|
|
269
|
+
const id = typeof record.id === "string" ? record.id : undefined;
|
|
270
|
+
return [{ ...(id ? { id } : {}), index, ...(name ? { name } : {}) }];
|
|
271
|
+
});
|
|
272
|
+
}
|
|
168
273
|
function safeJson(value) {
|
|
169
274
|
try {
|
|
170
275
|
const parsed = JSON.parse(value);
|
|
@@ -182,15 +287,19 @@ function usageFromOpenAi(value) {
|
|
|
182
287
|
const outputTokens = number(record.completion_tokens) ?? number(record.output_tokens);
|
|
183
288
|
if (inputTokens === undefined && outputTokens === undefined)
|
|
184
289
|
return undefined;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
290
|
+
const totalTokens = number(record.total_tokens);
|
|
291
|
+
const cacheReadInputTokens = number(record.cache_read_input_tokens);
|
|
292
|
+
const cacheCreationInputTokens = number(record.cache_creation_input_tokens);
|
|
293
|
+
const reasoningTokens = number(record.reasoning_tokens);
|
|
294
|
+
const cost = number(record.cost);
|
|
190
295
|
return {
|
|
191
|
-
inputTokens:
|
|
192
|
-
outputTokens:
|
|
193
|
-
...(
|
|
296
|
+
inputTokens: inputTokens ?? 0,
|
|
297
|
+
outputTokens: outputTokens ?? 0,
|
|
298
|
+
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
299
|
+
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
300
|
+
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
301
|
+
...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
|
|
302
|
+
...(cost !== undefined ? { cost } : {}),
|
|
194
303
|
};
|
|
195
304
|
}
|
|
196
305
|
function number(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-cli-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@tangle-network/agent-interface": "0.34.0"
|
|
32
|
+
"@tangle-network/agent-interface": "0.34.0",
|
|
33
|
+
"undici": "7.28.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@types/node": "25.6.0",
|