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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codebuddy-sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CodeBuddy Agent SDK provider for Pi — native tool execution, 29+ models, thinking controls, session pool",
5
5
  "author": "slahser",
6
6
  "license": "MIT",
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
- const includePartial = !!(model.reasoning && options?.reasoning && options.reasoning !== "off");
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 contentIndex = 0;
147
- let msgCount = 0; // track assistant messages for empty-response detection
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
- if (msg.type === "assistant") {
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
- // Non-reasoning models may emit thinking blocks wrapping text (Hunyuan quirk).
182
- // Treat them as text.
294
+ const idx = ci++;
183
295
  if (block.type === "thinking") {
184
- if (model.reasoning) {
185
- const ci = contentIndex++;
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
- const ci = contentIndex++;
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
- const ci = contentIndex++;
211
- const textBlock: TextContent = { type: "text", text: block.text };
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 ci = contentIndex++;
218
- const toolCall: ToolCall = {
219
- type: "toolCall", id: block.id, name: block.name,
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);