pi-codebuddy-sdk 0.1.13 → 0.1.15

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.13",
3
+ "version": "0.1.15",
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
@@ -1,97 +1,101 @@
1
- import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
1
+ /**
2
+ * pi-codebuddy-sdk — CodeBuddy Agent SDK as a Pi provider.
3
+ *
4
+ * Architecture follows pi-claude-bridge:
5
+ * - Stateless: one sdk.query() per Pi turn
6
+ * - No Pi systemPrompt forwarding — CodeBuddy uses its own default
7
+ * - No MCP tool bridge — CodeBuddy SDK tools are used natively
8
+ * - Only the latest user message is forwarded (Pi manages conversation)
9
+ *
10
+ * Two files: index.ts (registration + model discovery), provider.ts (streaming).
11
+ */
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import type { ProviderApi, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
14
+ import { streamCodebuddy } from "./provider.js";
2
15
 
3
- // ponytail: hardcoded patterns for capability detection since supportedModels() returns
4
- // only {id, name} without capability metadata.
5
- const THINKING_PATTERNS = [/claude/i, /gemini/i, /gpt-5/i];
6
- const IMAGE_PATTERNS = [/claude/i, /gemini/i, /gpt/i];
7
- // ponytail: rough context window estimates by model family
8
- const CONTEXT_ESTIMATES: Record<string, [number, number]> = {
9
- // [contextWindow, maxTokens]
10
- claude: [200000, 8192],
11
- gemini: [1048576, 8192],
12
- gpt: [200000, 16384],
13
- deepseek: [131072, 8192],
14
- glm: [131072, 8192],
15
- minimax: [131072, 8192],
16
- kimi: [131072, 8192],
17
- hy: [131072, 8192], // Hunyuan
18
- };
16
+ // ── provider registration ──
19
17
 
20
- function detectCapabilities(modelId: string) {
21
- const reasoning = THINKING_PATTERNS.some((p) => p.test(modelId));
22
- const hasImage = IMAGE_PATTERNS.some((p) => p.test(modelId));
23
-
24
- // Match family for context window estimate
25
- const family = Object.keys(CONTEXT_ESTIMATES).find((k) =>
26
- modelId.toLowerCase().startsWith(k),
27
- );
28
- const [contextWindow, maxTokens] = family
29
- ? CONTEXT_ESTIMATES[family]
30
- : [128000, 8192];
31
-
32
- return {
33
- reasoning,
34
- input: (hasImage ? ["text", "image"] : ["text"]) as ("text" | "image")[],
35
- contextWindow,
36
- maxTokens,
37
- };
38
- }
18
+ export default function activate(api: ExtensionAPI) {
19
+ api.registerProvider("codebuddy", {
20
+ name: "CodeBuddy",
21
+ baseUrl: "codebuddy-local",
22
+ apiKey: "codebuddy-local-auth",
23
+ api: "codebuddy-sdk",
24
+ models: [],
25
+ streamSimple: streamCodebuddy,
26
+ });
39
27
 
40
- async function discoverModels(): Promise<ProviderModelConfig[]> {
41
- try {
42
- const sdk = await import("@tencent-ai/agent-sdk");
43
- const q = sdk.query({
44
- prompt: "",
45
- options: {
46
- permissionMode: "bypassPermissions",
47
- maxTurns: 0,
48
- },
49
- });
50
- const models = await Promise.race([
51
- q.supportedModels(),
52
- new Promise<never>((_, reject) =>
53
- setTimeout(() => reject(new Error("CodeBuddy model discovery timed out after 15s")), 15000),
54
- ),
55
- ]);
56
- if (models && models.length > 0) {
57
- return models.map((m) => {
58
- const caps = detectCapabilities(m.id);
59
- return {
60
- id: m.id,
61
- name: m.name || m.id,
62
- reasoning: caps.reasoning,
63
- input: caps.input,
64
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
65
- contextWindow: caps.contextWindow,
66
- maxTokens: caps.maxTokens,
67
- };
28
+ // Lazy model discovery at startup
29
+ queueMicrotask(async () => {
30
+ try {
31
+ const models = await discoverModels();
32
+ api.registerProvider("codebuddy", {
33
+ name: "CodeBuddy",
34
+ baseUrl: "codebuddy-local",
35
+ apiKey: "codebuddy-local-auth",
36
+ api: "codebuddy-sdk",
37
+ models,
38
+ streamSimple: streamCodebuddy,
68
39
  });
40
+ } catch {
41
+ // Model discovery failed — provider registered with empty models,
42
+ // user can still try manually.
69
43
  }
70
- } catch (e) {
71
- // Discovery failed — SDK not installed, CLI missing, or network issue.
72
- // Fall back to hardcoded minimal list so Pi can still show a provider.
73
- console.warn(
74
- `[pi-codebuddy-sdk] Model discovery failed: ${e instanceof Error ? e.message : String(e)}. Using ${3} fallback models.`,
75
- );
76
- }
44
+ });
45
+ }
77
46
 
78
- // ponytail: fallback model list when discovery fails
79
- return [
80
- { id: "hy3-preview-agent-ioa", name: "Hunyuan 3 Preview", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
81
- { id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200000, maxTokens: 8192 },
82
- { id: "deepseek-v4-pro-ioa", name: "DeepSeek V4 Pro", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
83
- ];
47
+ // ── model discovery ──
48
+
49
+ let _sdk: typeof import("@tencent-ai/agent-sdk") | undefined;
50
+ async function getSdk() {
51
+ if (!_sdk) _sdk = await import("@tencent-ai/agent-sdk");
52
+ return _sdk;
84
53
  }
85
54
 
86
- export default async function (pi: ExtensionAPI) {
87
- const models = await discoverModels();
55
+ export async function discoverModels(): Promise<ProviderApi["models"]> {
56
+ const sdk = await getSdk();
57
+ const q = sdk.query({ prompt: " ", options: { maxTurns: 0 } });
58
+ const supported = await q.supportedModels();
59
+ await q.interrupt();
88
60
 
89
- pi.registerProvider("codebuddy", {
90
- name: "CodeBuddy",
91
- baseUrl: "https://codebuddy.ai",
92
- apiKey: "codebuddy-local",
93
- api: "codebuddy-sdk",
94
- models,
95
- streamSimple: (await import("./provider.js")).streamCodebuddy,
61
+ const models = supported.map((m) => {
62
+ const caps = detectCapabilities(m.id);
63
+ return {
64
+ id: m.id,
65
+ name: m.name || m.id,
66
+ api: "codebuddy-sdk" as const,
67
+ provider: "codebuddy" as const,
68
+ reasoning: caps.reasoning,
69
+ input: caps.input,
70
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
71
+ contextWindow: caps.contextWindow,
72
+ maxTokens: caps.maxTokens,
73
+ };
96
74
  });
75
+
76
+ return models;
77
+ }
78
+
79
+ // ── capability detection (pattern-based, like pi-claude-bridge) ──
80
+
81
+ const THINKING_PATTERNS = [/claude/i, /gemini/i, /gpt-5/i];
82
+ const IMAGE_PATTERNS = [/claude/i, /gemini/i, /gpt/i];
83
+ const CONTEXT_ESTIMATES: Record<string, { window: number; maxTokens: number }> = {
84
+ claude: { window: 200000, maxTokens: 8192 },
85
+ gemini: { window: 1048576, maxTokens: 8192 },
86
+ gpt: { window: 200000, maxTokens: 16384 },
87
+ deepseek: { window: 131072, maxTokens: 8192 },
88
+ glm: { window: 131072, maxTokens: 8192 },
89
+ minimax: { window: 131072, maxTokens: 8192 },
90
+ kimi: { window: 131072, maxTokens: 8192 },
91
+ hy: { window: 131072, maxTokens: 8192 },
92
+ };
93
+
94
+ function detectCapabilities(id: string) {
95
+ const reasoning = THINKING_PATTERNS.some((p) => p.test(id));
96
+ const supportsImages = IMAGE_PATTERNS.some((p) => p.test(id));
97
+ const input = supportsImages ? (["text", "image"] as const) : (["text"] as const);
98
+ const family = Object.keys(CONTEXT_ESTIMATES).find((k) => id.toLowerCase().includes(k));
99
+ const est = family ? CONTEXT_ESTIMATES[family] : { window: 131072, maxTokens: 8192 };
100
+ return { reasoning, input, contextWindow: est.window, maxTokens: est.maxTokens };
97
101
  }
@@ -0,0 +1 @@
1
+ # Temporary: add debug logging to extractUserMessage
package/src/provider.ts CHANGED
@@ -1,15 +1,13 @@
1
1
  /**
2
- * CodeBuddy Provider — stateless, lightweight.
2
+ * Provider entry point — pi-claude-bridge pattern.
3
3
  *
4
- * Uses sdk.query() (one-shot) — no context building, no session pool,
5
- * no tool boundary. Pi manages conversation, CodeBuddy is just a model backend.
6
- *
7
- * Architecture follows pi-openai-compatible / pi-anthropic pattern:
8
- * extract latest user message → send to SDK → stream events back to Pi.
4
+ * For each Pi turn:
5
+ * 1. Extract the latest user message (strip context-mode injection)
6
+ * 2. Call sdk.query() — stateless, CodeBuddy uses its own system prompt
7
+ * 3. Stream events back to Pi (text, thinking, toolcalls)
9
8
  */
10
9
  import {
11
10
  createAssistantMessageEventStream,
12
- type Api,
13
11
  type AssistantMessage,
14
12
  type AssistantMessageEventStream,
15
13
  type Context,
@@ -20,21 +18,20 @@ import {
20
18
  type ToolCall,
21
19
  } from "@earendil-works/pi-ai/compat";
22
20
  import type {
21
+ Effort,
23
22
  Message as CbMessage,
24
23
  ThinkingConfig,
25
- Effort,
26
- RawMessageStreamEvent,
27
24
  } from "@tencent-ai/agent-sdk";
28
25
 
29
26
  type CodebuddySdk = typeof import("@tencent-ai/agent-sdk");
30
27
 
31
- async function loadCodebuddySdk(): Promise<CodebuddySdk> {
28
+ async function loadSdk(): Promise<CodebuddySdk> {
32
29
  return import("@tencent-ai/agent-sdk");
33
30
  }
34
31
 
35
32
  // ── helpers ──
36
33
 
37
- function makePartial(model: Model<Api>): AssistantMessage {
34
+ function makePartial(model: Model<any>): AssistantMessage {
38
35
  return {
39
36
  role: "assistant",
40
37
  content: [],
@@ -47,143 +44,83 @@ function makePartial(model: Model<Api>): AssistantMessage {
47
44
  };
48
45
  }
49
46
 
50
- function makeErrorPartial(model: Model<Api>, error: unknown): AssistantMessage {
47
+ function makeErrorPartial(model: Model<any>, error: unknown): AssistantMessage {
51
48
  return {
52
49
  ...makePartial(model),
53
50
  stopReason: "error",
54
- errorMessage: `CodeBuddy SDK error: ${error instanceof Error ? error.message : String(error)}`,
51
+ errorMessage: error instanceof Error ? error.message : String(error),
55
52
  };
56
53
  }
57
54
 
58
- function extractUsageUpdate(msg: CbMessage): Partial<AssistantMessage> | null {
59
- if (msg.type === "result" && msg.usage) {
60
- return {
61
- usage: {
62
- input: msg.usage.input_tokens ?? 0,
63
- output: msg.usage.output_tokens ?? 0,
64
- cacheRead: msg.usage.cache_read_input_tokens ?? 0,
65
- cacheWrite: msg.usage.cache_creation_input_tokens ?? 0,
66
- totalTokens: (msg.usage.input_tokens ?? 0) + (msg.usage.output_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0) + (msg.usage.cache_creation_input_tokens ?? 0),
67
- cost: {
68
- input: 0,
69
- output: 0,
70
- cacheRead: 0,
71
- cacheWrite: 0,
72
- total: msg.total_cost_usd ?? 0,
73
- },
74
- },
75
- };
76
- }
77
- return null;
78
- }
55
+ // ── extract user message ──
79
56
 
80
- // ── extract latest user message ──
81
-
82
- function extractLatestUserText(context: Context): string {
83
- const messages = context.messages;
84
- for (let i = messages.length - 1; i >= 0; i--) {
85
- if (messages[i].role === "user") {
86
- let raw = extractRawContent((messages[i] as any).content);
87
- if (!raw) continue;
88
- // Strip context-mode session state injection prepended to user messages.
89
- // Pi's context-mode adds its hierarchy/state before the actual user input.
90
- raw = stripContextModeInjection(raw);
91
- if (raw) return raw;
92
- }
57
+ /**
58
+ * Extract the latest user message from Pi Context.
59
+ * Strips context-mode session state injection.
60
+ *
61
+ * Like pi-claude-bridge's extractUserPrompt() but with context-mode defense.
62
+ */
63
+ function extractUserMessage(context: Context): string {
64
+ const msgs = context.messages;
65
+ for (let i = msgs.length - 1; i >= 0; i--) {
66
+ if (msgs[i].role !== "user") continue;
67
+ const raw = contentToText((msgs[i] as any).content);
68
+ if (raw) return raw.trim();
93
69
  }
94
70
  return "Continue.";
95
71
  }
96
72
 
97
- function extractRawContent(content: unknown): string {
73
+ function contentToText(content: unknown): string {
98
74
  if (typeof content === "string") return content;
99
75
  if (!Array.isArray(content)) return "";
100
76
  return content
101
77
  .filter((c: any) => c?.type === "text")
102
- .map((c: any) => c.text)
78
+ .map((c: any) => c.text as string)
103
79
  .join("\n");
104
80
  }
105
81
 
106
- /**
107
- * Pi context-mode prepends its session state to user messages.
108
- * Strip it to keep CodeBuddy from treating it as conversation content.
109
- *
110
- * Injected format:
111
- * context-mode active. Hierarchy: ...
112
- * <session_state source="compaction">
113
- * <session_mode>investigate</session_mode>
114
- * </session_state>
115
- * [actual user message]
116
- */
117
- function stripContextModeInjection(text: string): string {
118
- // Find the end of context-mode injection
119
- // Pattern: </session_state> followed by newlines then actual message
120
- const sessionEnd = text.lastIndexOf("</session_state>");
121
- if (sessionEnd >= 0) {
122
- const after = text.slice(sessionEnd + "</session_state>".length).trimStart();
123
- if (after) return after;
124
- }
125
-
126
- // Fallback: look for "context-mode active" header and take everything after it + session_state
127
- const cmStart = text.indexOf("context-mode active.");
128
- if (cmStart >= 0) {
129
- // Find first blank line after </session_state> or end of XML
130
- const afterSession = afterSessionState(text);
131
- if (afterSession) return afterSession;
132
- }
133
-
134
- return text;
135
- }
136
-
137
- function afterSessionState(text: string): string | null {
138
- // Try to find the actual user message after context-mode XML injection
139
- const patterns = [
140
- /context-mode active\.[\s\S]*?<\/session_state>\s*\n+([\s\S]+)$/,
141
- /<session_state[\s\S]*?<\/session_state>\s*\n+([\s\S]+)$/,
142
- ];
143
- for (const re of patterns) {
144
- const m = text.match(re);
145
- if (m?.[1]?.trim()) return m[1].trim();
146
- }
147
- return null;
148
- }
149
-
150
- // ── thinking config mapping ──
82
+ // ── thinking config ──
151
83
 
152
- const DEFAULT_THINKING_BUDGETS: Record<string, number> = {
153
- minimal: 1600,
154
- low: 4000,
155
- medium: 0,
156
- high: 0,
157
- xhigh: 32000,
84
+ const THINKING_BUDGETS: Record<string, number> = {
85
+ minimal: 1600, low: 4000, medium: 0, high: 0, xhigh: 32000,
158
86
  };
159
-
160
- const DEFAULT_EFFORT: Record<string, Effort | undefined> = {
161
- minimal: 'low',
162
- low: 'low',
163
- medium: 'medium',
164
- high: 'high',
165
- xhigh: 'xhigh',
87
+ const EFFORT_MAP: Record<string, Effort | undefined> = {
88
+ minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "xhigh",
166
89
  };
167
90
 
168
- function buildThinkingConfig(
169
- reasoning: string | undefined,
170
- thinkingBudgets?: SimpleStreamOptions["thinkingBudgets"],
171
- ): { thinking?: ThinkingConfig; effort?: Effort } {
91
+ function buildThinking(reasoning?: SimpleStreamOptions["reasoning"], budgets?: SimpleStreamOptions["thinkingBudgets"]): { thinking?: ThinkingConfig; effort?: Effort } {
172
92
  if (!reasoning) return {};
173
93
  if (reasoning === "off") return { thinking: { type: "disabled" } };
174
-
175
- const budget = thinkingBudgets?.[reasoning as keyof typeof thinkingBudgets]
176
- ?? DEFAULT_THINKING_BUDGETS[reasoning] ?? 0;
177
- const effort = DEFAULT_EFFORT[reasoning];
178
-
94
+ const budget = budgets?.[reasoning as keyof typeof budgets] ?? THINKING_BUDGETS[reasoning] ?? 0;
95
+ const effort = EFFORT_MAP[reasoning];
179
96
  if (budget > 0) return { thinking: { type: "enabled", budgetTokens: budget }, effort };
180
97
  return { thinking: { type: "adaptive" }, effort };
181
98
  }
182
99
 
183
- // ── main provider ──
100
+ // ── usage extraction ──
101
+
102
+ function extractUsage(msg: CbMessage): Partial<AssistantMessage> | null {
103
+ if (msg.type !== "result" || !msg.usage) return null;
104
+ return {
105
+ usage: {
106
+ input: msg.usage.input_tokens ?? 0,
107
+ output: msg.usage.output_tokens ?? 0,
108
+ cacheRead: msg.usage.cache_read_input_tokens ?? 0,
109
+ cacheWrite: msg.usage.cache_creation_input_tokens ?? 0,
110
+ totalTokens:
111
+ (msg.usage.input_tokens ?? 0) +
112
+ (msg.usage.output_tokens ?? 0) +
113
+ (msg.usage.cache_read_input_tokens ?? 0) +
114
+ (msg.usage.cache_creation_input_tokens ?? 0),
115
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: msg.total_cost_usd ?? 0 },
116
+ },
117
+ };
118
+ }
119
+
120
+ // ── main ──
184
121
 
185
122
  export function streamCodebuddy(
186
- model: Model<Api>,
123
+ model: Model<any>,
187
124
  context: Context,
188
125
  options?: SimpleStreamOptions,
189
126
  ): AssistantMessageEventStream {
@@ -194,13 +131,10 @@ export function streamCodebuddy(
194
131
  try {
195
132
  stream.push({ type: "start", partial });
196
133
 
197
- const sdk = await loadCodebuddySdk();
198
- const userText = extractLatestUserText(context);
134
+ const sdk = await loadSdk();
135
+ const userText = extractUserMessage(context);
199
136
 
200
- const { thinking, effort } = buildThinkingConfig(
201
- options?.reasoning,
202
- options?.thinkingBudgets,
203
- );
137
+ const { thinking, effort } = buildThinking(options?.reasoning, options?.thinkingBudgets);
204
138
 
205
139
  const q = sdk.query({
206
140
  prompt: userText,
@@ -214,7 +148,7 @@ export function streamCodebuddy(
214
148
  },
215
149
  });
216
150
 
217
- // ── abort handling ──
151
+ // ── abort ──
218
152
  let aborted = false;
219
153
  const onAbort = async () => {
220
154
  if (aborted) return;
@@ -225,86 +159,74 @@ export function streamCodebuddy(
225
159
  };
226
160
  if (options?.signal) {
227
161
  options.signal.addEventListener("abort", onAbort, { once: true });
228
- if (options.signal.aborted) {
229
- await onAbort();
230
- return;
231
- }
162
+ if (options.signal.aborted) { await onAbort(); return; }
232
163
  }
233
164
 
234
- // ── per-index block tracking ──
235
- type BlockKind = 'text' | 'thinking' | 'tool_use' | 'redacted_thinking';
165
+ // ── per-block tracking (for stream_event delta accumulation) ──
166
+ type BlockKind = "text" | "thinking" | "tool_use" | "redacted_thinking";
236
167
  const blockKind = new Map<number, BlockKind>();
237
168
  const blockAccum = new Map<number, string>();
238
169
  const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
239
170
 
240
171
  let hasStreamEvents = false;
241
172
 
242
- function onBlockStart(idx: number, kind: BlockKind, initText: string, meta?: { id: string; name: string; signature?: string }) {
173
+ function onStart(idx: number, kind: BlockKind, init: string, meta?: { id: string; name: string; signature?: string }) {
243
174
  blockKind.set(idx, kind);
244
- blockAccum.set(idx, initText);
175
+ blockAccum.set(idx, init);
245
176
  if (meta) blockMeta.set(idx, meta);
246
177
 
247
- if (kind === 'redacted_thinking') {
248
- const tc: ThinkingContent = { type: 'thinking', thinking: '[redacted]', redacted: true, thinkingSignature: meta?.signature };
178
+ if (kind === "redacted_thinking") {
179
+ const tc: ThinkingContent = { type: "thinking", thinking: "[redacted]", redacted: true, thinkingSignature: meta?.signature };
249
180
  partial.content = [...partial.content, tc];
250
- stream.push({ type: 'thinking_start', contentIndex: idx, partial });
251
- stream.push({ type: 'thinking_end', contentIndex: idx, content: '[redacted]', partial });
252
- } else if (kind === 'thinking') {
253
- const tc: ThinkingContent = { type: 'thinking', thinking: initText, thinkingSignature: meta?.signature };
181
+ stream.push({ type: "thinking_start", contentIndex: idx, partial });
182
+ stream.push({ type: "thinking_end", contentIndex: idx, content: "[redacted]", partial });
183
+ } else if (kind === "thinking") {
184
+ const tc: ThinkingContent = { type: "thinking", thinking: init, thinkingSignature: meta?.signature };
254
185
  partial.content = [...partial.content, tc];
255
- stream.push({ type: 'thinking_start', contentIndex: idx, partial });
256
- } else if (kind === 'text') {
257
- const tb: TextContent = { type: 'text', text: initText };
258
- partial.content = [...partial.content, tb];
259
- stream.push({ type: 'text_start', contentIndex: idx, partial });
260
- } else if (kind === 'tool_use' && meta) {
261
- const toolCall: ToolCall = { type: 'toolCall', id: meta.id, name: meta.name, arguments: {} };
262
- partial.content = [...partial.content, toolCall];
263
- stream.push({ type: 'toolcall_start', contentIndex: idx, partial });
186
+ stream.push({ type: "thinking_start", contentIndex: idx, partial });
187
+ } else if (kind === "text") {
188
+ partial.content = [...partial.content, { type: "text", text: init }];
189
+ stream.push({ type: "text_start", contentIndex: idx, partial });
190
+ } else if (kind === "tool_use" && meta) {
191
+ partial.content = [...partial.content, { type: "toolCall", id: meta.id, name: meta.name, arguments: {} }];
192
+ stream.push({ type: "toolcall_start", contentIndex: idx, partial });
264
193
  }
265
194
  }
266
195
 
267
- function onBlockDelta(idx: number, deltaType: string, deltaValue: string) {
196
+ function onDelta(idx: number, deltaType: string, value: string) {
268
197
  const kind = blockKind.get(idx);
269
198
  if (!kind) return;
270
199
 
271
- if (deltaType === 'text_delta' && kind === 'text') {
272
- const newText = (blockAccum.get(idx) || '') + deltaValue;
273
- blockAccum.set(idx, newText);
200
+ if (deltaType === "text_delta" && kind === "text") {
201
+ const next = (blockAccum.get(idx) || "") + value;
202
+ blockAccum.set(idx, next);
274
203
  const blocks = [...partial.content];
275
- blocks[idx] = { type: 'text', text: newText };
204
+ blocks[idx] = { type: "text", text: next };
276
205
  partial.content = blocks;
277
- stream.push({ type: 'text_delta', contentIndex: idx, delta: deltaValue, partial });
278
- } else if (deltaType === 'thinking_delta' && kind === 'thinking') {
279
- const newThinking = (blockAccum.get(idx) || '') + deltaValue;
280
- blockAccum.set(idx, newThinking);
206
+ stream.push({ type: "text_delta", contentIndex: idx, delta: value, partial });
207
+ } else if (deltaType === "thinking_delta" && kind === "thinking") {
208
+ const next = (blockAccum.get(idx) || "") + value;
209
+ blockAccum.set(idx, next);
281
210
  const blocks = [...partial.content];
282
- blocks[idx] = { type: 'thinking', thinking: newThinking, thinkingSignature: blockMeta.get(idx)?.signature };
211
+ blocks[idx] = { type: "thinking", thinking: next, thinkingSignature: blockMeta.get(idx)?.signature };
283
212
  partial.content = blocks;
284
- stream.push({ type: 'thinking_delta', contentIndex: idx, delta: deltaValue, partial });
285
- } else if (deltaType === 'input_json_delta' && kind === 'tool_use') {
286
- const newJson = (blockAccum.get(idx) || '') + deltaValue;
287
- blockAccum.set(idx, newJson);
213
+ stream.push({ type: "thinking_delta", contentIndex: idx, delta: value, partial });
214
+ } else if (deltaType === "input_json_delta" && kind === "tool_use") {
215
+ const json = (blockAccum.get(idx) || "") + value;
216
+ blockAccum.set(idx, json);
288
217
  const blocks = [...partial.content];
289
- try {
290
- blocks[idx] = { ...blocks[idx], arguments: JSON.parse(newJson) };
291
- } catch { /* partial JSON */ }
218
+ try { blocks[idx] = { ...blocks[idx], arguments: JSON.parse(json) }; } catch { /* partial */ }
292
219
  partial.content = blocks;
293
- stream.push({ type: 'toolcall_delta', contentIndex: idx, delta: deltaValue, partial } as any);
220
+ stream.push({ type: "toolcall_delta", contentIndex: idx, delta: value, partial } as any);
294
221
  }
295
222
  }
296
223
 
297
- function onBlockStop(idx: number) {
224
+ function onStop(idx: number) {
298
225
  const kind = blockKind.get(idx);
299
- const accum = blockAccum.get(idx) || '';
300
- if (kind === 'text') {
301
- stream.push({ type: 'text_end', contentIndex: idx, content: accum, partial });
302
- } else if (kind === 'thinking') {
303
- stream.push({ type: 'thinking_end', contentIndex: idx, content: accum, partial });
304
- } else if (kind === 'tool_use') {
305
- const toolCall = partial.content[idx] as ToolCall;
306
- stream.push({ type: 'toolcall_end', contentIndex: idx, toolCall, partial });
307
- }
226
+ const accum = blockAccum.get(idx) || "";
227
+ if (kind === "text") stream.push({ type: "text_end", contentIndex: idx, content: accum, partial });
228
+ else if (kind === "thinking") stream.push({ type: "thinking_end", contentIndex: idx, content: accum, partial });
229
+ else if (kind === "tool_use") stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: partial.content[idx] as ToolCall, partial });
308
230
  }
309
231
 
310
232
  // ── iterate ──
@@ -316,29 +238,24 @@ export function streamCodebuddy(
316
238
  continue;
317
239
  }
318
240
 
241
+ // Token-level streaming via stream_event (preferred)
319
242
  if (msg.type === "stream_event") {
320
243
  hasStreamEvents = true;
321
244
  const evt = msg.event;
322
245
 
323
246
  if (evt.type === "content_block_start") {
324
- const block = evt.content_block;
325
- if (block.type === "text") {
326
- onBlockStart(evt.index, 'text', block.text || '');
327
- } else if (block.type === "thinking") {
328
- onBlockStart(evt.index, 'thinking', block.thinking || '', { id: '', name: '', signature: block.signature });
329
- } else if (block.type === "redacted_thinking") {
330
- onBlockStart(evt.index, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
331
- } else if (block.type === "tool_use") {
332
- const initJson = JSON.stringify(block.input || {});
333
- onBlockStart(evt.index, 'tool_use', initJson, { id: block.id, name: block.name });
334
- }
247
+ const b = evt.content_block;
248
+ if (b.type === "text") onStart(evt.index, "text", b.text || "");
249
+ else if (b.type === "thinking") onStart(evt.index, "thinking", b.thinking || "", { id: "", name: "", signature: b.signature });
250
+ else if (b.type === "redacted_thinking") onStart(evt.index, "redacted_thinking", "[redacted]", { id: "", name: "", signature: b.data });
251
+ else if (b.type === "tool_use") onStart(evt.index, "tool_use", JSON.stringify(b.input || {}), { id: b.id, name: b.name });
335
252
  } else if (evt.type === "content_block_delta") {
336
253
  const d = evt.delta;
337
- if (d.type === 'text_delta') onBlockDelta(evt.index, 'text_delta', d.text);
338
- else if (d.type === 'thinking_delta') onBlockDelta(evt.index, 'thinking_delta', d.thinking);
339
- else if (d.type === 'input_json_delta') onBlockDelta(evt.index, 'input_json_delta', d.partial_json);
254
+ if (d.type === "text_delta") onDelta(evt.index, "text_delta", d.text);
255
+ else if (d.type === "thinking_delta") onDelta(evt.index, "thinking_delta", d.thinking);
256
+ else if (d.type === "input_json_delta") onDelta(evt.index, "input_json_delta", d.partial_json);
340
257
  } else if (evt.type === "content_block_stop") {
341
- onBlockStop(evt.index);
258
+ onStop(evt.index);
342
259
  }
343
260
  continue;
344
261
  }
@@ -348,25 +265,16 @@ export function streamCodebuddy(
348
265
  let ci = 0;
349
266
  for (const block of msg.message.content) {
350
267
  const idx = ci++;
351
- if (block.type === "thinking") {
352
- onBlockStart(idx, 'thinking', block.thinking);
353
- onBlockStop(idx);
354
- } else if (block.type === "redacted_thinking") {
355
- onBlockStart(idx, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
356
- } else if (block.type === "text") {
357
- onBlockStart(idx, 'text', block.text);
358
- onBlockStop(idx);
359
- } else if (block.type === "tool_use") {
360
- const initJson = JSON.stringify(block.input ?? {});
361
- onBlockStart(idx, 'tool_use', initJson, { id: block.id, name: block.name });
362
- onBlockStop(idx);
363
- }
268
+ if (block.type === "thinking") { onStart(idx, "thinking", block.thinking); onStop(idx); }
269
+ else if (block.type === "redacted_thinking") onStart(idx, "redacted_thinking", "[redacted]", { id: "", name: "", signature: block.data });
270
+ else if (block.type === "text") { onStart(idx, "text", block.text); onStop(idx); }
271
+ else if (block.type === "tool_use") { onStart(idx, "tool_use", JSON.stringify(block.input ?? {}), { id: block.id, name: block.name }); onStop(idx); }
364
272
  }
365
273
  }
366
274
 
367
275
  if (msg.type === "result") {
368
- const usageUpdate = extractUsageUpdate(msg);
369
- if (usageUpdate) Object.assign(partial, usageUpdate);
276
+ const u = extractUsage(msg);
277
+ if (u) Object.assign(partial, u);
370
278
 
371
279
  if (msg.subtype === "success") {
372
280
  partial.stopReason = "stop";
@@ -380,8 +288,7 @@ export function streamCodebuddy(
380
288
  }
381
289
  }
382
290
  } catch (error) {
383
- const errPartial = makeErrorPartial(model, error);
384
- stream.push({ type: "error", reason: "error", error: errPartial });
291
+ stream.push({ type: "error", reason: "error", error: makeErrorPartial(model, error) });
385
292
  }
386
293
  });
387
294