@tangle-network/agent-provider-cli-bridge 0.2.25 → 0.2.27

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.
Files changed (2) hide show
  1. package/dist/index.js +100 -20
  2. package/package.json +15 -24
package/dist/index.js CHANGED
@@ -40,7 +40,10 @@ async function* streamCliBridgeTurn(fetchImpl, options, environmentInput, turn)
40
40
  if (!response.body)
41
41
  throw new Error("cli-bridge response body is empty");
42
42
  let text = "";
43
- let usage;
43
+ const sessionId = turn.sessionId ?? environmentInput.idempotencyKey ?? environmentInput.name ?? "cli-bridge";
44
+ const messageId = turn.turnId ?? `${sessionId}:assistant`;
45
+ const emittedToolCalls = new Set();
46
+ let completed = false;
44
47
  for await (const event of parseSse(response.body)) {
45
48
  if (event === "[DONE]")
46
49
  break;
@@ -51,35 +54,80 @@ async function* streamCliBridgeTurn(fetchImpl, options, environmentInput, turn)
51
54
  const error = parsed.error;
52
55
  const message = typeof error.message === "string" ? error.message : "cli-bridge error";
53
56
  yield { type: "status", data: { status: "failed", error: message } };
54
- return;
57
+ throw new Error(`cli-bridge: ${message}`);
55
58
  }
56
59
  const choice = Array.isArray(parsed.choices) ? parsed.choices[0] : undefined;
57
60
  const delta = choice?.delta;
58
61
  const chunk = delta && typeof delta.content === "string" ? delta.content : "";
59
62
  const nextUsage = usageFromOpenAi(parsed.usage);
60
63
  if (nextUsage) {
61
- usage = mergeUsage(usage, nextUsage);
62
- yield { type: "usage", data: { usage: nextUsage }, usage: nextUsage };
64
+ yield { type: "usage", data: {}, usage: nextUsage };
63
65
  }
64
66
  if (chunk) {
65
67
  text += chunk;
68
+ const part = {
69
+ id: `${messageId}:text`,
70
+ sessionID: sessionId,
71
+ messageID: messageId,
72
+ type: "text",
73
+ text,
74
+ };
75
+ const normalized = {
76
+ type: "message.part.updated",
77
+ part,
78
+ delta: chunk,
79
+ };
66
80
  yield {
67
81
  type: "message.part.updated",
68
- data: { delta: chunk },
82
+ data: { part, delta: chunk },
83
+ normalized,
84
+ };
85
+ }
86
+ for (const toolCall of toolCallsFromDelta(delta)) {
87
+ const callId = toolCall.id ?? `${messageId}:tool:${toolCall.index}`;
88
+ if (!toolCall.name || emittedToolCalls.has(callId))
89
+ continue;
90
+ emittedToolCalls.add(callId);
91
+ const part = {
92
+ id: callId,
93
+ sessionID: sessionId,
94
+ messageID: messageId,
95
+ type: "tool",
96
+ callID: callId,
97
+ tool: toolCall.name,
98
+ state: { status: "pending", input: {} },
99
+ };
100
+ const normalized = {
101
+ type: "message.part.updated",
102
+ part,
103
+ };
104
+ yield {
105
+ type: "message.part.updated",
106
+ data: { part },
107
+ normalized,
69
108
  };
70
109
  }
71
110
  if (choice?.finish_reason) {
111
+ if (choice.finish_reason === "error") {
112
+ yield {
113
+ type: "status",
114
+ data: { status: "failed", error: "cli-bridge returned finish_reason=error" },
115
+ };
116
+ throw new Error("cli-bridge returned finish_reason=error");
117
+ }
118
+ completed = true;
72
119
  yield {
73
120
  type: "result",
74
121
  data: {
75
122
  finalText: text,
76
123
  finishReason: choice.finish_reason,
77
- status: choice.finish_reason === "error" ? "failed" : "completed",
124
+ status: "completed",
78
125
  },
79
- ...(usage ? { usage } : {}),
80
126
  };
81
127
  }
82
128
  }
129
+ if (!completed)
130
+ throw new Error("cli-bridge stream ended without a terminal result");
83
131
  }
84
132
  function toChatCompletionsBody(options, environmentInput, turn) {
85
133
  const profile = inlineProfile(environmentInput.profile);
@@ -137,14 +185,14 @@ async function* parseSse(body) {
137
185
  if (done)
138
186
  break;
139
187
  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);
188
+ let boundary = findFrameBoundary(buffer);
189
+ while (boundary) {
190
+ const frame = buffer.slice(0, boundary.index);
191
+ buffer = buffer.slice(boundary.index + boundary.length);
144
192
  const data = dataFromFrame(frame);
145
193
  if (data !== undefined)
146
194
  yield data;
147
- boundary = buffer.indexOf("\n\n");
195
+ boundary = findFrameBoundary(buffer);
148
196
  }
149
197
  }
150
198
  if (buffer) {
@@ -157,6 +205,15 @@ async function* parseSse(body) {
157
205
  reader.releaseLock();
158
206
  }
159
207
  }
208
+ function findFrameBoundary(value) {
209
+ const lf = value.indexOf("\n\n");
210
+ const crlf = value.indexOf("\r\n\r\n");
211
+ if (lf === -1 && crlf === -1)
212
+ return undefined;
213
+ if (crlf !== -1 && (lf === -1 || crlf < lf))
214
+ return { index: crlf, length: 4 };
215
+ return { index: lf, length: 2 };
216
+ }
160
217
  function dataFromFrame(frame) {
161
218
  const lines = frame.split(/\r?\n/);
162
219
  const data = lines
@@ -165,6 +222,25 @@ function dataFromFrame(frame) {
165
222
  .join("\n");
166
223
  return data || undefined;
167
224
  }
225
+ function toolCallsFromDelta(value) {
226
+ if (!value || typeof value !== "object")
227
+ return [];
228
+ const calls = value.tool_calls;
229
+ if (!Array.isArray(calls))
230
+ return [];
231
+ return calls.flatMap((call, position) => {
232
+ if (!call || typeof call !== "object")
233
+ return [];
234
+ const record = call;
235
+ const fn = record.function;
236
+ const name = fn && typeof fn === "object" && typeof fn.name === "string"
237
+ ? fn.name
238
+ : undefined;
239
+ const index = number(record.index) ?? position;
240
+ const id = typeof record.id === "string" ? record.id : undefined;
241
+ return [{ ...(id ? { id } : {}), index, ...(name ? { name } : {}) }];
242
+ });
243
+ }
168
244
  function safeJson(value) {
169
245
  try {
170
246
  const parsed = JSON.parse(value);
@@ -182,15 +258,19 @@ function usageFromOpenAi(value) {
182
258
  const outputTokens = number(record.completion_tokens) ?? number(record.output_tokens);
183
259
  if (inputTokens === undefined && outputTokens === undefined)
184
260
  return undefined;
185
- return { inputTokens: inputTokens ?? 0, outputTokens: outputTokens ?? 0 };
186
- }
187
- function mergeUsage(left, right) {
188
- if (!left)
189
- return right;
261
+ const totalTokens = number(record.total_tokens);
262
+ const cacheReadInputTokens = number(record.cache_read_input_tokens);
263
+ const cacheCreationInputTokens = number(record.cache_creation_input_tokens);
264
+ const reasoningTokens = number(record.reasoning_tokens);
265
+ const cost = number(record.cost);
190
266
  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) } : {}),
267
+ inputTokens: inputTokens ?? 0,
268
+ outputTokens: outputTokens ?? 0,
269
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
270
+ ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
271
+ ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
272
+ ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
273
+ ...(cost !== undefined ? { cost } : {}),
194
274
  };
195
275
  }
196
276
  function number(value) {
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge /v1/chat/completions",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "main": "dist/index.js",
8
- "types": "src/index.ts",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
11
  "import": "./dist/index.js",
12
- "types": "./src/index.ts"
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
13
14
  }
14
15
  },
15
16
  "repository": {
@@ -19,16 +20,7 @@
19
20
  },
20
21
  "publishConfig": {
21
22
  "access": "public",
22
- "registry": "https://registry.npmjs.org",
23
- "main": "./dist/index.js",
24
- "types": "./dist/index.d.ts",
25
- "exports": {
26
- ".": {
27
- "import": "./dist/index.js",
28
- "types": "./dist/index.d.ts",
29
- "default": "./dist/index.js"
30
- }
31
- }
23
+ "registry": "https://registry.npmjs.org"
32
24
  },
33
25
  "files": [
34
26
  "dist/index.d.ts",
@@ -36,19 +28,18 @@
36
28
  "README.md",
37
29
  "LICENSE"
38
30
  ],
39
- "scripts": {
40
- "build": "tsc -p tsconfig.json",
41
- "check-types": "tsc --noEmit",
42
- "clean": "rm -rf dist",
43
- "prepublishOnly": "pnpm run build",
44
- "test": "vitest run"
45
- },
46
31
  "dependencies": {
47
32
  "@tangle-network/agent-interface": "0.34.0"
48
33
  },
49
34
  "devDependencies": {
50
- "@types/node": "catalog:",
35
+ "@types/node": "25.6.0",
51
36
  "typescript": "^6.0.3",
52
- "vitest": "catalog:"
37
+ "vitest": "^4.1.5"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "check-types": "tsc --noEmit",
42
+ "clean": "rm -rf dist",
43
+ "test": "vitest run"
53
44
  }
54
- }
45
+ }