@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 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
- const fetchImpl = options.fetch ?? globalThis.fetch;
3
- if (!fetchImpl)
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) => streamCliBridgeTurn(fetchImpl, options, input, 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: async () => { },
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(fetchImpl, options, environmentInput, turn) {
26
- const response = await fetchImpl(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
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
- let usage;
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
- break;
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
- return;
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
- usage = mergeUsage(usage, nextUsage);
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: { delta: chunk },
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: choice.finish_reason === "error" ? "failed" : "completed",
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
- try {
135
- while (true) {
136
- const { done, value } = await reader.read();
137
- if (done)
138
- break;
139
- buffer += decoder.decode(value, { stream: true });
140
- let boundary = buffer.indexOf("\n\n");
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
- finally {
157
- reader.releaseLock();
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
- return { inputTokens: inputTokens ?? 0, outputTokens: outputTokens ?? 0 };
186
- }
187
- function mergeUsage(left, right) {
188
- if (!left)
189
- return right;
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: left.inputTokens + right.inputTokens,
192
- outputTokens: left.outputTokens + right.outputTokens,
193
- ...(left.cost !== undefined || right.cost !== undefined ? { cost: (left.cost ?? 0) + (right.cost ?? 0) } : {}),
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.26",
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",