pi-codebuddy-sdk 0.1.12 → 0.1.14
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 +186 -163
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,93 +44,149 @@ 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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
55
|
+
// ── extract user message ──
|
|
56
|
+
|
|
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
|
+
|
|
66
|
+
// Pi injects context-mode state as the LAST user message (after the real one).
|
|
67
|
+
// Search from the end, skip obvious context-mode injections.
|
|
68
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
69
|
+
if (msgs[i].role !== "user") continue;
|
|
70
|
+
const raw = contentToText((msgs[i] as any).content);
|
|
71
|
+
if (!raw) continue;
|
|
72
|
+
|
|
73
|
+
// Skip messages that are entirely context-mode injection
|
|
74
|
+
if (isContextModeInjection(raw)) continue;
|
|
75
|
+
|
|
76
|
+
return stripPiInjection(raw);
|
|
76
77
|
}
|
|
77
|
-
return
|
|
78
|
+
return "Continue.";
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
/** Detect if a user message is a Pi context-mode state injection (not a real message). */
|
|
82
|
+
function isContextModeInjection(text: string): boolean {
|
|
83
|
+
const t = text.trim();
|
|
84
|
+
if (t.startsWith("context-mode active.")) return true;
|
|
85
|
+
if (t.includes("<session_state") || t.includes("<session_mode")) return true;
|
|
86
|
+
if (t.startsWith("<summary>") && t.includes("</summary>")) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function contentToText(content: unknown): string {
|
|
91
|
+
if (typeof content === "string") return content;
|
|
92
|
+
if (!Array.isArray(content)) return "";
|
|
93
|
+
return content
|
|
94
|
+
.filter((c: any) => c?.type === "text")
|
|
95
|
+
.map((c: any) => c.text as string)
|
|
96
|
+
.join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Strip context-mode session state that Pi prepends to user messages.
|
|
101
|
+
*
|
|
102
|
+
* Pi injects several formats:
|
|
103
|
+
* context-mode active. Hierarchy: ...
|
|
104
|
+
* <session_state source="compaction"> ... </session_state>
|
|
105
|
+
* <summary>Compacted: ...</summary>
|
|
106
|
+
*
|
|
107
|
+
* Strip all of them, keep only the actual user message.
|
|
108
|
+
*/
|
|
109
|
+
function stripPiInjection(text: string): string {
|
|
110
|
+
// Fast path
|
|
111
|
+
if (!text.includes("context-mode") && !text.includes("<session_state") && !text.includes("<summary>")) {
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 1. Remove <summary> blocks (compaction summaries contain context-mode state)
|
|
116
|
+
text = text.replace(/<summary>[\s\S]*?<\/summary>\s*/g, "");
|
|
117
|
+
|
|
118
|
+
// 2. Remove <session_state> block and everything between "context-mode" and </session_state>
|
|
119
|
+
const sessionEnd = text.lastIndexOf("</session_state>");
|
|
120
|
+
if (sessionEnd >= 0) {
|
|
121
|
+
const after = text.slice(sessionEnd + "</session_state>".length).trimStart();
|
|
122
|
+
if (after) {
|
|
123
|
+
text = after;
|
|
124
|
+
} else {
|
|
125
|
+
// All content was in the session_state block
|
|
126
|
+
const before = text.slice(0, text.indexOf("<session_state")).trim();
|
|
127
|
+
if (before) text = before;
|
|
95
128
|
}
|
|
96
129
|
}
|
|
97
|
-
|
|
130
|
+
|
|
131
|
+
// 3. Remove "context-mode active." line and everything after until a blank line
|
|
132
|
+
// Pattern: "context-mode active. ... Hierarchy: ..." followed by blank line, then real message
|
|
133
|
+
const cmRe = /context-mode active\.[\s\S]*?\n\s*\n([\s\S]*)$/;
|
|
134
|
+
const cmMatch = text.match(cmRe);
|
|
135
|
+
if (cmMatch?.[1]?.trim()) {
|
|
136
|
+
text = cmMatch[1].trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 4. If "context-mode" is still the only content, return empty
|
|
140
|
+
if (text.trim() === "context-mode active." || text.trim().startsWith("context-mode active.")) {
|
|
141
|
+
const afterCm = text.replace(/context-mode active\.[\s\S]*/, "").trim();
|
|
142
|
+
if (afterCm) text = afterCm;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return text.trim() || text;
|
|
98
146
|
}
|
|
99
147
|
|
|
100
|
-
// ── thinking config
|
|
148
|
+
// ── thinking config ──
|
|
101
149
|
|
|
102
|
-
const
|
|
103
|
-
minimal: 1600,
|
|
104
|
-
low: 4000,
|
|
105
|
-
medium: 0,
|
|
106
|
-
high: 0,
|
|
107
|
-
xhigh: 32000,
|
|
150
|
+
const THINKING_BUDGETS: Record<string, number> = {
|
|
151
|
+
minimal: 1600, low: 4000, medium: 0, high: 0, xhigh: 32000,
|
|
108
152
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
minimal: 'low',
|
|
112
|
-
low: 'low',
|
|
113
|
-
medium: 'medium',
|
|
114
|
-
high: 'high',
|
|
115
|
-
xhigh: 'xhigh',
|
|
153
|
+
const EFFORT_MAP: Record<string, Effort | undefined> = {
|
|
154
|
+
minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "xhigh",
|
|
116
155
|
};
|
|
117
156
|
|
|
118
|
-
function
|
|
119
|
-
reasoning: string | undefined,
|
|
120
|
-
thinkingBudgets?: SimpleStreamOptions["thinkingBudgets"],
|
|
121
|
-
): { thinking?: ThinkingConfig; effort?: Effort } {
|
|
157
|
+
function buildThinking(reasoning?: SimpleStreamOptions["reasoning"], budgets?: SimpleStreamOptions["thinkingBudgets"]): { thinking?: ThinkingConfig; effort?: Effort } {
|
|
122
158
|
if (!reasoning) return {};
|
|
123
159
|
if (reasoning === "off") return { thinking: { type: "disabled" } };
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
?? DEFAULT_THINKING_BUDGETS[reasoning] ?? 0;
|
|
127
|
-
const effort = DEFAULT_EFFORT[reasoning];
|
|
128
|
-
|
|
160
|
+
const budget = budgets?.[reasoning as keyof typeof budgets] ?? THINKING_BUDGETS[reasoning] ?? 0;
|
|
161
|
+
const effort = EFFORT_MAP[reasoning];
|
|
129
162
|
if (budget > 0) return { thinking: { type: "enabled", budgetTokens: budget }, effort };
|
|
130
163
|
return { thinking: { type: "adaptive" }, effort };
|
|
131
164
|
}
|
|
132
165
|
|
|
133
|
-
// ──
|
|
166
|
+
// ── usage extraction ──
|
|
167
|
+
|
|
168
|
+
function extractUsage(msg: CbMessage): Partial<AssistantMessage> | null {
|
|
169
|
+
if (msg.type !== "result" || !msg.usage) return null;
|
|
170
|
+
return {
|
|
171
|
+
usage: {
|
|
172
|
+
input: msg.usage.input_tokens ?? 0,
|
|
173
|
+
output: msg.usage.output_tokens ?? 0,
|
|
174
|
+
cacheRead: msg.usage.cache_read_input_tokens ?? 0,
|
|
175
|
+
cacheWrite: msg.usage.cache_creation_input_tokens ?? 0,
|
|
176
|
+
totalTokens:
|
|
177
|
+
(msg.usage.input_tokens ?? 0) +
|
|
178
|
+
(msg.usage.output_tokens ?? 0) +
|
|
179
|
+
(msg.usage.cache_read_input_tokens ?? 0) +
|
|
180
|
+
(msg.usage.cache_creation_input_tokens ?? 0),
|
|
181
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: msg.total_cost_usd ?? 0 },
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── main ──
|
|
134
187
|
|
|
135
188
|
export function streamCodebuddy(
|
|
136
|
-
model: Model<
|
|
189
|
+
model: Model<any>,
|
|
137
190
|
context: Context,
|
|
138
191
|
options?: SimpleStreamOptions,
|
|
139
192
|
): AssistantMessageEventStream {
|
|
@@ -144,13 +197,10 @@ export function streamCodebuddy(
|
|
|
144
197
|
try {
|
|
145
198
|
stream.push({ type: "start", partial });
|
|
146
199
|
|
|
147
|
-
const sdk = await
|
|
148
|
-
const userText =
|
|
200
|
+
const sdk = await loadSdk();
|
|
201
|
+
const userText = extractUserMessage(context);
|
|
149
202
|
|
|
150
|
-
const { thinking, effort } =
|
|
151
|
-
options?.reasoning,
|
|
152
|
-
options?.thinkingBudgets,
|
|
153
|
-
);
|
|
203
|
+
const { thinking, effort } = buildThinking(options?.reasoning, options?.thinkingBudgets);
|
|
154
204
|
|
|
155
205
|
const q = sdk.query({
|
|
156
206
|
prompt: userText,
|
|
@@ -164,7 +214,7 @@ export function streamCodebuddy(
|
|
|
164
214
|
},
|
|
165
215
|
});
|
|
166
216
|
|
|
167
|
-
// ── abort
|
|
217
|
+
// ── abort ──
|
|
168
218
|
let aborted = false;
|
|
169
219
|
const onAbort = async () => {
|
|
170
220
|
if (aborted) return;
|
|
@@ -175,86 +225,74 @@ export function streamCodebuddy(
|
|
|
175
225
|
};
|
|
176
226
|
if (options?.signal) {
|
|
177
227
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
178
|
-
if (options.signal.aborted) {
|
|
179
|
-
await onAbort();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
228
|
+
if (options.signal.aborted) { await onAbort(); return; }
|
|
182
229
|
}
|
|
183
230
|
|
|
184
|
-
// ── per-
|
|
185
|
-
type BlockKind =
|
|
231
|
+
// ── per-block tracking (for stream_event delta accumulation) ──
|
|
232
|
+
type BlockKind = "text" | "thinking" | "tool_use" | "redacted_thinking";
|
|
186
233
|
const blockKind = new Map<number, BlockKind>();
|
|
187
234
|
const blockAccum = new Map<number, string>();
|
|
188
235
|
const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
|
|
189
236
|
|
|
190
237
|
let hasStreamEvents = false;
|
|
191
238
|
|
|
192
|
-
function
|
|
239
|
+
function onStart(idx: number, kind: BlockKind, init: string, meta?: { id: string; name: string; signature?: string }) {
|
|
193
240
|
blockKind.set(idx, kind);
|
|
194
|
-
blockAccum.set(idx,
|
|
241
|
+
blockAccum.set(idx, init);
|
|
195
242
|
if (meta) blockMeta.set(idx, meta);
|
|
196
243
|
|
|
197
|
-
if (kind ===
|
|
198
|
-
const tc: ThinkingContent = { type:
|
|
244
|
+
if (kind === "redacted_thinking") {
|
|
245
|
+
const tc: ThinkingContent = { type: "thinking", thinking: "[redacted]", redacted: true, thinkingSignature: meta?.signature };
|
|
199
246
|
partial.content = [...partial.content, tc];
|
|
200
|
-
stream.push({ type:
|
|
201
|
-
stream.push({ type:
|
|
202
|
-
} else if (kind ===
|
|
203
|
-
const tc: ThinkingContent = { type:
|
|
247
|
+
stream.push({ type: "thinking_start", contentIndex: idx, partial });
|
|
248
|
+
stream.push({ type: "thinking_end", contentIndex: idx, content: "[redacted]", partial });
|
|
249
|
+
} else if (kind === "thinking") {
|
|
250
|
+
const tc: ThinkingContent = { type: "thinking", thinking: init, thinkingSignature: meta?.signature };
|
|
204
251
|
partial.content = [...partial.content, tc];
|
|
205
|
-
stream.push({ type:
|
|
206
|
-
} else if (kind ===
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
partial.content = [...partial.content, toolCall];
|
|
213
|
-
stream.push({ type: 'toolcall_start', contentIndex: idx, partial });
|
|
252
|
+
stream.push({ type: "thinking_start", contentIndex: idx, partial });
|
|
253
|
+
} else if (kind === "text") {
|
|
254
|
+
partial.content = [...partial.content, { type: "text", text: init }];
|
|
255
|
+
stream.push({ type: "text_start", contentIndex: idx, partial });
|
|
256
|
+
} else if (kind === "tool_use" && meta) {
|
|
257
|
+
partial.content = [...partial.content, { type: "toolCall", id: meta.id, name: meta.name, arguments: {} }];
|
|
258
|
+
stream.push({ type: "toolcall_start", contentIndex: idx, partial });
|
|
214
259
|
}
|
|
215
260
|
}
|
|
216
261
|
|
|
217
|
-
function
|
|
262
|
+
function onDelta(idx: number, deltaType: string, value: string) {
|
|
218
263
|
const kind = blockKind.get(idx);
|
|
219
264
|
if (!kind) return;
|
|
220
265
|
|
|
221
|
-
if (deltaType ===
|
|
222
|
-
const
|
|
223
|
-
blockAccum.set(idx,
|
|
266
|
+
if (deltaType === "text_delta" && kind === "text") {
|
|
267
|
+
const next = (blockAccum.get(idx) || "") + value;
|
|
268
|
+
blockAccum.set(idx, next);
|
|
224
269
|
const blocks = [...partial.content];
|
|
225
|
-
blocks[idx] = { type:
|
|
270
|
+
blocks[idx] = { type: "text", text: next };
|
|
226
271
|
partial.content = blocks;
|
|
227
|
-
stream.push({ type:
|
|
228
|
-
} else if (deltaType ===
|
|
229
|
-
const
|
|
230
|
-
blockAccum.set(idx,
|
|
272
|
+
stream.push({ type: "text_delta", contentIndex: idx, delta: value, partial });
|
|
273
|
+
} else if (deltaType === "thinking_delta" && kind === "thinking") {
|
|
274
|
+
const next = (blockAccum.get(idx) || "") + value;
|
|
275
|
+
blockAccum.set(idx, next);
|
|
231
276
|
const blocks = [...partial.content];
|
|
232
|
-
blocks[idx] = { type:
|
|
277
|
+
blocks[idx] = { type: "thinking", thinking: next, thinkingSignature: blockMeta.get(idx)?.signature };
|
|
233
278
|
partial.content = blocks;
|
|
234
|
-
stream.push({ type:
|
|
235
|
-
} else if (deltaType ===
|
|
236
|
-
const
|
|
237
|
-
blockAccum.set(idx,
|
|
279
|
+
stream.push({ type: "thinking_delta", contentIndex: idx, delta: value, partial });
|
|
280
|
+
} else if (deltaType === "input_json_delta" && kind === "tool_use") {
|
|
281
|
+
const json = (blockAccum.get(idx) || "") + value;
|
|
282
|
+
blockAccum.set(idx, json);
|
|
238
283
|
const blocks = [...partial.content];
|
|
239
|
-
try {
|
|
240
|
-
blocks[idx] = { ...blocks[idx], arguments: JSON.parse(newJson) };
|
|
241
|
-
} catch { /* partial JSON */ }
|
|
284
|
+
try { blocks[idx] = { ...blocks[idx], arguments: JSON.parse(json) }; } catch { /* partial */ }
|
|
242
285
|
partial.content = blocks;
|
|
243
|
-
stream.push({ type:
|
|
286
|
+
stream.push({ type: "toolcall_delta", contentIndex: idx, delta: value, partial } as any);
|
|
244
287
|
}
|
|
245
288
|
}
|
|
246
289
|
|
|
247
|
-
function
|
|
290
|
+
function onStop(idx: number) {
|
|
248
291
|
const kind = blockKind.get(idx);
|
|
249
|
-
const accum = blockAccum.get(idx) ||
|
|
250
|
-
if (kind ===
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
stream.push({ type: 'thinking_end', contentIndex: idx, content: accum, partial });
|
|
254
|
-
} else if (kind === 'tool_use') {
|
|
255
|
-
const toolCall = partial.content[idx] as ToolCall;
|
|
256
|
-
stream.push({ type: 'toolcall_end', contentIndex: idx, toolCall, partial });
|
|
257
|
-
}
|
|
292
|
+
const accum = blockAccum.get(idx) || "";
|
|
293
|
+
if (kind === "text") stream.push({ type: "text_end", contentIndex: idx, content: accum, partial });
|
|
294
|
+
else if (kind === "thinking") stream.push({ type: "thinking_end", contentIndex: idx, content: accum, partial });
|
|
295
|
+
else if (kind === "tool_use") stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: partial.content[idx] as ToolCall, partial });
|
|
258
296
|
}
|
|
259
297
|
|
|
260
298
|
// ── iterate ──
|
|
@@ -266,29 +304,24 @@ export function streamCodebuddy(
|
|
|
266
304
|
continue;
|
|
267
305
|
}
|
|
268
306
|
|
|
307
|
+
// Token-level streaming via stream_event (preferred)
|
|
269
308
|
if (msg.type === "stream_event") {
|
|
270
309
|
hasStreamEvents = true;
|
|
271
310
|
const evt = msg.event;
|
|
272
311
|
|
|
273
312
|
if (evt.type === "content_block_start") {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
} else if (block.type === "redacted_thinking") {
|
|
280
|
-
onBlockStart(evt.index, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
281
|
-
} else if (block.type === "tool_use") {
|
|
282
|
-
const initJson = JSON.stringify(block.input || {});
|
|
283
|
-
onBlockStart(evt.index, 'tool_use', initJson, { id: block.id, name: block.name });
|
|
284
|
-
}
|
|
313
|
+
const b = evt.content_block;
|
|
314
|
+
if (b.type === "text") onStart(evt.index, "text", b.text || "");
|
|
315
|
+
else if (b.type === "thinking") onStart(evt.index, "thinking", b.thinking || "", { id: "", name: "", signature: b.signature });
|
|
316
|
+
else if (b.type === "redacted_thinking") onStart(evt.index, "redacted_thinking", "[redacted]", { id: "", name: "", signature: b.data });
|
|
317
|
+
else if (b.type === "tool_use") onStart(evt.index, "tool_use", JSON.stringify(b.input || {}), { id: b.id, name: b.name });
|
|
285
318
|
} else if (evt.type === "content_block_delta") {
|
|
286
319
|
const d = evt.delta;
|
|
287
|
-
if (d.type ===
|
|
288
|
-
else if (d.type ===
|
|
289
|
-
else if (d.type ===
|
|
320
|
+
if (d.type === "text_delta") onDelta(evt.index, "text_delta", d.text);
|
|
321
|
+
else if (d.type === "thinking_delta") onDelta(evt.index, "thinking_delta", d.thinking);
|
|
322
|
+
else if (d.type === "input_json_delta") onDelta(evt.index, "input_json_delta", d.partial_json);
|
|
290
323
|
} else if (evt.type === "content_block_stop") {
|
|
291
|
-
|
|
324
|
+
onStop(evt.index);
|
|
292
325
|
}
|
|
293
326
|
continue;
|
|
294
327
|
}
|
|
@@ -298,25 +331,16 @@ export function streamCodebuddy(
|
|
|
298
331
|
let ci = 0;
|
|
299
332
|
for (const block of msg.message.content) {
|
|
300
333
|
const idx = ci++;
|
|
301
|
-
if (block.type === "thinking") {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
onBlockStart(idx, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
306
|
-
} else if (block.type === "text") {
|
|
307
|
-
onBlockStart(idx, 'text', block.text);
|
|
308
|
-
onBlockStop(idx);
|
|
309
|
-
} else if (block.type === "tool_use") {
|
|
310
|
-
const initJson = JSON.stringify(block.input ?? {});
|
|
311
|
-
onBlockStart(idx, 'tool_use', initJson, { id: block.id, name: block.name });
|
|
312
|
-
onBlockStop(idx);
|
|
313
|
-
}
|
|
334
|
+
if (block.type === "thinking") { onStart(idx, "thinking", block.thinking); onStop(idx); }
|
|
335
|
+
else if (block.type === "redacted_thinking") onStart(idx, "redacted_thinking", "[redacted]", { id: "", name: "", signature: block.data });
|
|
336
|
+
else if (block.type === "text") { onStart(idx, "text", block.text); onStop(idx); }
|
|
337
|
+
else if (block.type === "tool_use") { onStart(idx, "tool_use", JSON.stringify(block.input ?? {}), { id: block.id, name: block.name }); onStop(idx); }
|
|
314
338
|
}
|
|
315
339
|
}
|
|
316
340
|
|
|
317
341
|
if (msg.type === "result") {
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
342
|
+
const u = extractUsage(msg);
|
|
343
|
+
if (u) Object.assign(partial, u);
|
|
320
344
|
|
|
321
345
|
if (msg.subtype === "success") {
|
|
322
346
|
partial.stopReason = "stop";
|
|
@@ -330,8 +354,7 @@ export function streamCodebuddy(
|
|
|
330
354
|
}
|
|
331
355
|
}
|
|
332
356
|
} catch (error) {
|
|
333
|
-
|
|
334
|
-
stream.push({ type: "error", reason: "error", error: errPartial });
|
|
357
|
+
stream.push({ type: "error", reason: "error", error: makeErrorPartial(model, error) });
|
|
335
358
|
}
|
|
336
359
|
});
|
|
337
360
|
|