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 +1 -1
- package/src/index.ts +90 -86
- package/src/provider-debug.patch +1 -0
- package/src/provider.ts +115 -208
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,97 +1,101 @@
|
|
|
1
|
-
|
|
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
|
-
//
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
}
|
|
71
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
87
|
-
const
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
*
|
|
2
|
+
* Provider entry point — pi-claude-bridge pattern.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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<
|
|
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<
|
|
47
|
+
function makeErrorPartial(model: Model<any>, error: unknown): AssistantMessage {
|
|
51
48
|
return {
|
|
52
49
|
...makePartial(model),
|
|
53
50
|
stopReason: "error",
|
|
54
|
-
errorMessage:
|
|
51
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
55
52
|
};
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
// ──
|
|
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<
|
|
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
|
|
198
|
-
const userText =
|
|
134
|
+
const sdk = await loadSdk();
|
|
135
|
+
const userText = extractUserMessage(context);
|
|
199
136
|
|
|
200
|
-
const { thinking, effort } =
|
|
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
|
|
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-
|
|
235
|
-
type BlockKind =
|
|
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
|
|
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,
|
|
175
|
+
blockAccum.set(idx, init);
|
|
245
176
|
if (meta) blockMeta.set(idx, meta);
|
|
246
177
|
|
|
247
|
-
if (kind ===
|
|
248
|
-
const tc: ThinkingContent = { type:
|
|
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:
|
|
251
|
-
stream.push({ type:
|
|
252
|
-
} else if (kind ===
|
|
253
|
-
const tc: ThinkingContent = { type:
|
|
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:
|
|
256
|
-
} else if (kind ===
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
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 ===
|
|
272
|
-
const
|
|
273
|
-
blockAccum.set(idx,
|
|
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:
|
|
204
|
+
blocks[idx] = { type: "text", text: next };
|
|
276
205
|
partial.content = blocks;
|
|
277
|
-
stream.push({ type:
|
|
278
|
-
} else if (deltaType ===
|
|
279
|
-
const
|
|
280
|
-
blockAccum.set(idx,
|
|
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:
|
|
211
|
+
blocks[idx] = { type: "thinking", thinking: next, thinkingSignature: blockMeta.get(idx)?.signature };
|
|
283
212
|
partial.content = blocks;
|
|
284
|
-
stream.push({ type:
|
|
285
|
-
} else if (deltaType ===
|
|
286
|
-
const
|
|
287
|
-
blockAccum.set(idx,
|
|
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:
|
|
220
|
+
stream.push({ type: "toolcall_delta", contentIndex: idx, delta: value, partial } as any);
|
|
294
221
|
}
|
|
295
222
|
}
|
|
296
223
|
|
|
297
|
-
function
|
|
224
|
+
function onStop(idx: number) {
|
|
298
225
|
const kind = blockKind.get(idx);
|
|
299
|
-
const accum = blockAccum.get(idx) ||
|
|
300
|
-
if (kind ===
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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 ===
|
|
338
|
-
else if (d.type ===
|
|
339
|
-
else if (d.type ===
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
369
|
-
if (
|
|
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
|
-
|
|
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
|
|