pi-codebuddy-sdk 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/index.ts +0 -18
- package/src/provider.ts +126 -48
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -94,22 +94,4 @@ export default async function (pi: ExtensionAPI) {
|
|
|
94
94
|
models,
|
|
95
95
|
streamSimple: (await import("./provider.js")).streamCodebuddy,
|
|
96
96
|
});
|
|
97
|
-
|
|
98
|
-
pi.registerCommand("codebuddy-refresh-models", {
|
|
99
|
-
description: "Refresh CodeBuddy model list from SDK",
|
|
100
|
-
handler: async (_args, ctx) => {
|
|
101
|
-
const refreshed = await discoverModels();
|
|
102
|
-
pi.registerProvider("codebuddy", {
|
|
103
|
-
name: "CodeBuddy",
|
|
104
|
-
baseUrl: "https://codebuddy.ai",
|
|
105
|
-
apiKey: "codebuddy-local",
|
|
106
|
-
api: "codebuddy-sdk",
|
|
107
|
-
models: refreshed,
|
|
108
|
-
streamSimple: (await import("./provider.js")).streamCodebuddy,
|
|
109
|
-
});
|
|
110
|
-
if (ctx.hasUI) {
|
|
111
|
-
ctx.ui.notify(`CodeBuddy: ${refreshed.length} models loaded`, "info");
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
97
|
}
|
package/src/provider.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
Message as CbMessage,
|
|
15
15
|
ThinkingConfig,
|
|
16
16
|
Effort,
|
|
17
|
+
RawMessageStreamEvent,
|
|
17
18
|
} from "@tencent-ai/agent-sdk";
|
|
18
19
|
import { buildFullMessage, buildDeltaMessage } from "./context.js";
|
|
19
20
|
import { acquireSession, evictSession } from "./session-pool.js";
|
|
@@ -126,7 +127,9 @@ export function streamCodebuddy(
|
|
|
126
127
|
options?.thinkingBudgets,
|
|
127
128
|
);
|
|
128
129
|
|
|
129
|
-
|
|
130
|
+
// Always request partial messages so we get stream_event deltas for real streaming.
|
|
131
|
+
// Non-streaming-capable models will still emit assistant messages as fallback.
|
|
132
|
+
const includePartial = true;
|
|
130
133
|
|
|
131
134
|
const cwd = process.cwd();
|
|
132
135
|
|
|
@@ -143,8 +146,14 @@ export function streamCodebuddy(
|
|
|
143
146
|
session = pooled;
|
|
144
147
|
isFirst = first;
|
|
145
148
|
|
|
146
|
-
let
|
|
147
|
-
let
|
|
149
|
+
let msgCount = 0; // track content blocks for empty-response detection
|
|
150
|
+
let hasStreamEvents = false; // true once we see first stream_event
|
|
151
|
+
|
|
152
|
+
// Per-index tracking for stream_event delta accumulation
|
|
153
|
+
type BlockKind = 'text' | 'thinking' | 'tool_use' | 'redacted_thinking';
|
|
154
|
+
const blockKind = new Map<number, BlockKind>();
|
|
155
|
+
const blockAccum = new Map<number, string>();
|
|
156
|
+
const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
|
|
148
157
|
|
|
149
158
|
// ── abort handling ──
|
|
150
159
|
let aborted = false;
|
|
@@ -166,6 +175,79 @@ export function streamCodebuddy(
|
|
|
166
175
|
// ── send prompt ──
|
|
167
176
|
await session.send(userMessage);
|
|
168
177
|
|
|
178
|
+
// ── helpers for stream_event block tracking ──
|
|
179
|
+
function onBlockStart(idx: number, kind: BlockKind, initText: string, meta?: { id: string; name: string; signature?: string }) {
|
|
180
|
+
blockKind.set(idx, kind);
|
|
181
|
+
blockAccum.set(idx, initText);
|
|
182
|
+
if (meta) blockMeta.set(idx, meta);
|
|
183
|
+
|
|
184
|
+
if (kind === 'redacted_thinking') {
|
|
185
|
+
const tc: ThinkingContent = { type: 'thinking', thinking: '[redacted]', redacted: true, thinkingSignature: meta?.signature };
|
|
186
|
+
partial.content = [...partial.content, tc];
|
|
187
|
+
stream.push({ type: 'thinking_start', contentIndex: idx, partial });
|
|
188
|
+
stream.push({ type: 'thinking_end', contentIndex: idx, content: '[redacted]', partial });
|
|
189
|
+
msgCount++;
|
|
190
|
+
} else if (kind === 'thinking') {
|
|
191
|
+
const tc: ThinkingContent = { type: 'thinking', thinking: initText, thinkingSignature: meta?.signature };
|
|
192
|
+
partial.content = [...partial.content, tc];
|
|
193
|
+
stream.push({ type: 'thinking_start', contentIndex: idx, partial });
|
|
194
|
+
msgCount++;
|
|
195
|
+
} else if (kind === 'text') {
|
|
196
|
+
const tb: TextContent = { type: 'text', text: initText };
|
|
197
|
+
partial.content = [...partial.content, tb];
|
|
198
|
+
stream.push({ type: 'text_start', contentIndex: idx, partial });
|
|
199
|
+
msgCount++;
|
|
200
|
+
} else if (kind === 'tool_use' && meta) {
|
|
201
|
+
const toolCall: ToolCall = { type: 'toolCall', id: meta.id, name: meta.name, arguments: {} };
|
|
202
|
+
partial.content = [...partial.content, toolCall];
|
|
203
|
+
stream.push({ type: 'toolcall_start', contentIndex: idx, partial });
|
|
204
|
+
msgCount++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function onBlockDelta(idx: number, deltaType: string, deltaValue: string) {
|
|
209
|
+
const kind = blockKind.get(idx);
|
|
210
|
+
if (!kind) return;
|
|
211
|
+
|
|
212
|
+
if (deltaType === 'text_delta' && kind === 'text') {
|
|
213
|
+
const newText = (blockAccum.get(idx) || '') + deltaValue;
|
|
214
|
+
blockAccum.set(idx, newText);
|
|
215
|
+
const blocks = [...partial.content];
|
|
216
|
+
blocks[idx] = { type: 'text', text: newText };
|
|
217
|
+
partial.content = blocks;
|
|
218
|
+
stream.push({ type: 'text_delta', contentIndex: idx, delta: deltaValue, partial });
|
|
219
|
+
} else if (deltaType === 'thinking_delta' && kind === 'thinking') {
|
|
220
|
+
const newThinking = (blockAccum.get(idx) || '') + deltaValue;
|
|
221
|
+
blockAccum.set(idx, newThinking);
|
|
222
|
+
const blocks = [...partial.content];
|
|
223
|
+
blocks[idx] = { type: 'thinking', thinking: newThinking, thinkingSignature: blockMeta.get(idx)?.signature };
|
|
224
|
+
partial.content = blocks;
|
|
225
|
+
stream.push({ type: 'thinking_delta', contentIndex: idx, delta: deltaValue, partial });
|
|
226
|
+
} else if (deltaType === 'input_json_delta' && kind === 'tool_use') {
|
|
227
|
+
const newJson = (blockAccum.get(idx) || '') + deltaValue;
|
|
228
|
+
blockAccum.set(idx, newJson);
|
|
229
|
+
const blocks = [...partial.content];
|
|
230
|
+
try {
|
|
231
|
+
blocks[idx] = { ...blocks[idx], arguments: JSON.parse(newJson) };
|
|
232
|
+
} catch { /* partial JSON — keep previous */ }
|
|
233
|
+
partial.content = blocks;
|
|
234
|
+
stream.push({ type: 'toolcall_delta', contentIndex: idx, delta: deltaValue, partial } as any);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function onBlockStop(idx: number) {
|
|
239
|
+
const kind = blockKind.get(idx);
|
|
240
|
+
const accum = blockAccum.get(idx) || '';
|
|
241
|
+
if (kind === 'text') {
|
|
242
|
+
stream.push({ type: 'text_end', contentIndex: idx, content: accum, partial });
|
|
243
|
+
} else if (kind === 'thinking') {
|
|
244
|
+
stream.push({ type: 'thinking_end', contentIndex: idx, content: accum, partial });
|
|
245
|
+
} else if (kind === 'tool_use') {
|
|
246
|
+
const toolCall = partial.content[idx] as ToolCall;
|
|
247
|
+
stream.push({ type: 'toolcall_end', contentIndex: idx, toolCall, partial });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
169
251
|
// ── iterate responses ──
|
|
170
252
|
for await (const msg of session.stream()) {
|
|
171
253
|
if (aborted) return;
|
|
@@ -175,53 +257,53 @@ export function streamCodebuddy(
|
|
|
175
257
|
continue;
|
|
176
258
|
}
|
|
177
259
|
|
|
178
|
-
|
|
260
|
+
// Primary path: token-level streaming via stream_event
|
|
261
|
+
if (msg.type === "stream_event") {
|
|
262
|
+
hasStreamEvents = true;
|
|
263
|
+
const evt = msg.event;
|
|
264
|
+
|
|
265
|
+
if (evt.type === "content_block_start") {
|
|
266
|
+
const block = evt.content_block;
|
|
267
|
+
if (block.type === "text") {
|
|
268
|
+
onBlockStart(evt.index, 'text', block.text || '');
|
|
269
|
+
} else if (block.type === "thinking") {
|
|
270
|
+
onBlockStart(evt.index, 'thinking', block.thinking || '', { id: '', name: '', signature: block.signature });
|
|
271
|
+
} else if (block.type === "redacted_thinking") {
|
|
272
|
+
onBlockStart(evt.index, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
273
|
+
} else if (block.type === "tool_use") {
|
|
274
|
+
const initJson = JSON.stringify(block.input || {});
|
|
275
|
+
onBlockStart(evt.index, 'tool_use', initJson, { id: block.id, name: block.name });
|
|
276
|
+
}
|
|
277
|
+
} else if (evt.type === "content_block_delta") {
|
|
278
|
+
const d = evt.delta;
|
|
279
|
+
if (d.type === 'text_delta') onBlockDelta(evt.index, 'text_delta', d.text);
|
|
280
|
+
else if (d.type === 'thinking_delta') onBlockDelta(evt.index, 'thinking_delta', d.thinking);
|
|
281
|
+
else if (d.type === 'input_json_delta') onBlockDelta(evt.index, 'input_json_delta', d.partial_json);
|
|
282
|
+
// ponytail: signature_delta not surfaced to Pi — signature only sent at block start
|
|
283
|
+
} else if (evt.type === "content_block_stop") {
|
|
284
|
+
onBlockStop(evt.index);
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Fallback: full assistant messages (used when model doesn't support streaming)
|
|
290
|
+
if (msg.type === "assistant" && !hasStreamEvents) {
|
|
179
291
|
msgCount++;
|
|
292
|
+
let ci = 0;
|
|
180
293
|
for (const block of msg.message.content) {
|
|
181
|
-
|
|
182
|
-
// Treat them as text.
|
|
294
|
+
const idx = ci++;
|
|
183
295
|
if (block.type === "thinking") {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const tc: ThinkingContent = { type: "thinking", thinking: block.thinking };
|
|
187
|
-
partial.content = [...partial.content, tc];
|
|
188
|
-
stream.push({ type: "thinking_start", contentIndex: ci, partial });
|
|
189
|
-
stream.push({ type: "thinking_delta", contentIndex: ci, delta: block.thinking, partial });
|
|
190
|
-
stream.push({ type: "thinking_end", contentIndex: ci, content: block.thinking, partial });
|
|
191
|
-
} else {
|
|
192
|
-
// Fallback: emit as text for non-reasoning models
|
|
193
|
-
const ci = contentIndex++;
|
|
194
|
-
const textBlock: TextContent = { type: "text", text: block.thinking };
|
|
195
|
-
partial.content = [...partial.content, textBlock];
|
|
196
|
-
stream.push({ type: "text_start", contentIndex: ci, partial });
|
|
197
|
-
stream.push({ type: "text_delta", contentIndex: ci, delta: block.thinking, partial });
|
|
198
|
-
stream.push({ type: "text_end", contentIndex: ci, content: block.thinking, partial });
|
|
199
|
-
}
|
|
296
|
+
onBlockStart(idx, 'thinking', block.thinking, { id: '', name: '', signature: undefined });
|
|
297
|
+
onBlockStop(idx);
|
|
200
298
|
} else if (block.type === "redacted_thinking") {
|
|
201
|
-
|
|
202
|
-
const tc: ThinkingContent = {
|
|
203
|
-
type: "thinking", thinking: "[redacted]",
|
|
204
|
-
thinkingSignature: block.data, redacted: true,
|
|
205
|
-
};
|
|
206
|
-
partial.content = [...partial.content, tc];
|
|
207
|
-
stream.push({ type: "thinking_start", contentIndex: ci, partial });
|
|
208
|
-
stream.push({ type: "thinking_end", contentIndex: ci, content: "[redacted]", partial });
|
|
299
|
+
onBlockStart(idx, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
209
300
|
} else if (block.type === "text") {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
partial.content = [...partial.content, textBlock];
|
|
213
|
-
stream.push({ type: "text_start", contentIndex: ci, partial });
|
|
214
|
-
stream.push({ type: "text_delta", contentIndex: ci, delta: block.text, partial });
|
|
215
|
-
stream.push({ type: "text_end", contentIndex: ci, content: block.text, partial });
|
|
301
|
+
onBlockStart(idx, 'text', block.text);
|
|
302
|
+
onBlockStop(idx);
|
|
216
303
|
} else if (block.type === "tool_use") {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
arguments: (block.input ?? {}) as Record<string, unknown>,
|
|
221
|
-
};
|
|
222
|
-
partial.content = [...partial.content, toolCall];
|
|
223
|
-
stream.push({ type: "toolcall_start", contentIndex: ci, partial });
|
|
224
|
-
stream.push({ type: "toolcall_end", contentIndex: ci, toolCall, partial });
|
|
304
|
+
const initJson = JSON.stringify(block.input ?? {});
|
|
305
|
+
onBlockStart(idx, 'tool_use', initJson, { id: block.id, name: block.name });
|
|
306
|
+
onBlockStop(idx);
|
|
225
307
|
}
|
|
226
308
|
}
|
|
227
309
|
}
|
|
@@ -237,10 +319,8 @@ export function streamCodebuddy(
|
|
|
237
319
|
partial.stopReason = "error";
|
|
238
320
|
partial.errorMessage = msg.errors?.join("; ") ?? `CodeBuddy error: ${msg.subtype}`;
|
|
239
321
|
stream.push({ type: "error", reason: "error", error: { ...partial } });
|
|
240
|
-
// Error on a turn: evict session so next turn starts fresh
|
|
241
322
|
evictSession(cwd);
|
|
242
323
|
}
|
|
243
|
-
// break (not return) to let iterator drain naturally
|
|
244
324
|
break;
|
|
245
325
|
}
|
|
246
326
|
}
|
|
@@ -249,8 +329,6 @@ export function streamCodebuddy(
|
|
|
249
329
|
if (msgCount === 0) {
|
|
250
330
|
evictSession(cwd);
|
|
251
331
|
}
|
|
252
|
-
|
|
253
|
-
// Stream ended; no extra done emission (handled in result branch above)
|
|
254
332
|
} catch (error) {
|
|
255
333
|
evictSession(process.cwd());
|
|
256
334
|
const errPartial = makeErrorPartial(model, error);
|