pi-codebuddy-sdk 0.1.18 → 0.1.19

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/src/models.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Model definitions for CodeBuddy provider.
2
+
3
+ export const MODEL_IDS_IN_ORDER = ["hy3-preview-agent-ioa", "claude-sonnet-4.6", "deepseek-v4-pro-ioa", "glm-5.2-ioa"];
4
+
5
+ export function buildModels<T extends { id: string; [key: string]: any }>(models: T[]): T[] {
6
+ return models.map((m) => ({
7
+ ...m,
8
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
9
+ }));
10
+ }
11
+
12
+ export function resolveModel<T extends { id: string }>(models: T[], input: string): T | undefined {
13
+ const lower = input.toLowerCase();
14
+ return models.find((m) => m.id === lower || m.id.includes(lower));
15
+ }
16
+
17
+ export function codebuddyModelId(model: { id: string }): string {
18
+ return model.id;
19
+ }
@@ -0,0 +1,80 @@
1
+ // Query state: QueryContext class + context stack.
2
+ //
3
+ // All per-query and per-turn mutable state lives here. Reentrant queries
4
+ // (subagents) push the parent context onto a stack and get a fresh instance.
5
+
6
+ import type { AssistantMessage, AssistantMessageEventStream, Model } from "@earendil-works/pi-ai";
7
+
8
+ // ponytail: inline McpResult type since extract-tool-results.ts is removed
9
+ export interface McpResult {
10
+ toolCallId: string;
11
+ content: unknown;
12
+ isError?: boolean;
13
+ }
14
+
15
+ export interface PendingToolCall {
16
+ toolName: string;
17
+ resolve: (result: McpResult) => void;
18
+ }
19
+
20
+ export class QueryContext {
21
+ // Query-scoped (fully isolated per query)
22
+ activeQuery: unknown | null = null;
23
+ currentPiStream: AssistantMessageEventStream | null = null;
24
+ latestCursor = 0;
25
+ pendingToolCalls = new Map<string, PendingToolCall>();
26
+ pendingResults = new Map<string, McpResult>();
27
+ turnToolCallIds: string[] = [];
28
+ nextHandlerIdx = 0;
29
+ deferredUserMessages: string[] = [];
30
+
31
+ // Per-turn (reset together)
32
+ turnOutput: AssistantMessage | null = null;
33
+ turnStarted = false;
34
+ turnSawStreamEvent = false;
35
+ turnSawToolCall = false;
36
+
37
+ get turnBlocks(): Array<any> {
38
+ if (!this.turnOutput) throw new Error("turnBlocks accessed before resetTurnState");
39
+ return this.turnOutput.content;
40
+ }
41
+
42
+ resetTurnState(model: Model<any>): void {
43
+ this.turnOutput = {
44
+ role: "assistant", content: [],
45
+ api: model.api, provider: model.provider, model: model.id,
46
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
47
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
48
+ stopReason: "stop", timestamp: Date.now(),
49
+ };
50
+ this.turnStarted = false;
51
+ this.turnSawStreamEvent = false;
52
+ this.turnSawToolCall = false;
53
+ }
54
+ }
55
+
56
+ let _ctx = new QueryContext();
57
+ const contextStack: QueryContext[] = [];
58
+
59
+ export function ctx(): QueryContext { return _ctx; }
60
+
61
+ export function stackDepth(): number { return contextStack.length; }
62
+
63
+ export function pushContext(): void {
64
+ if (!_ctx.activeQuery) throw new Error("pushContext() called with no active query");
65
+ contextStack.push(_ctx);
66
+ _ctx = new QueryContext();
67
+ }
68
+
69
+ export function popContext(): void {
70
+ if (contextStack.length === 0) throw new Error("popContext() called with empty stack");
71
+ const parent = contextStack[contextStack.length - 1];
72
+ parent.deferredUserMessages.push(..._ctx.deferredUserMessages);
73
+ _ctx = contextStack.pop()!;
74
+ }
75
+
76
+ // Test-only: drop all state so test files can start from a clean module.
77
+ export function resetStack(): void {
78
+ _ctx = new QueryContext();
79
+ contextStack.length = 0;
80
+ }
package/src/skills.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Skills block extraction.
2
+ // Extracted from index.ts so tests can import without activating the extension.
3
+
4
+ // Extract skills block from pi's system prompt for forwarding to CodeBuddy.
5
+ export function extractSkillsBlock(systemPrompt?: string): string | undefined {
6
+ if (!systemPrompt) return undefined;
7
+ const startMarker = "The following skills provide specialized instructions for specific tasks.";
8
+ const endMarker = "</available_skills>";
9
+ const start = systemPrompt.indexOf(startMarker);
10
+ if (start === -1) return undefined;
11
+ const end = systemPrompt.indexOf(endMarker, start);
12
+ if (end === -1) return undefined;
13
+ return rewriteSkillsBlock(systemPrompt.slice(start, end + endMarker.length).trim());
14
+ }
15
+
16
+ export function rewriteSkillsBlock(skillsBlock: string): string {
17
+ return skillsBlock.replace(
18
+ "Use the read tool to load a skill's file",
19
+ "Use the read tool to load a skill's file",
20
+ );
21
+ }
package/src/provider.ts DELETED
@@ -1,289 +0,0 @@
1
- /**
2
- * Provider entry point — pi-claude-bridge pattern.
3
- *
4
- * For each Pi turn:
5
- * 1. Extract the latest user message
6
- * 2. Call sdk.query() — stateless, CodeBuddy uses its own system prompt
7
- * 3. Stream events back to Pi (text, thinking, toolcalls)
8
- */
9
- import {
10
- createAssistantMessageEventStream,
11
- type AssistantMessage,
12
- type AssistantMessageEventStream,
13
- type Context,
14
- type Model,
15
- type SimpleStreamOptions,
16
- type TextContent,
17
- type ThinkingContent,
18
- type ToolCall,
19
- } from "@earendil-works/pi-ai/compat";
20
- import type {
21
- Effort,
22
- Message as CbMessage,
23
- ThinkingConfig,
24
- } from "@tencent-ai/agent-sdk";
25
-
26
- type CodebuddySdk = typeof import("@tencent-ai/agent-sdk");
27
-
28
- async function loadSdk(): Promise<CodebuddySdk> {
29
- return import("@tencent-ai/agent-sdk");
30
- }
31
-
32
- // ── helpers ──
33
-
34
- function makePartial(model: Model<any>): AssistantMessage {
35
- return {
36
- role: "assistant",
37
- content: [],
38
- api: model.api,
39
- provider: model.provider,
40
- model: model.id,
41
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
42
- stopReason: "stop",
43
- timestamp: Date.now(),
44
- };
45
- }
46
-
47
- function makeErrorPartial(model: Model<any>, error: unknown): AssistantMessage {
48
- return {
49
- ...makePartial(model),
50
- stopReason: "error",
51
- errorMessage: error instanceof Error ? error.message : String(error),
52
- };
53
- }
54
-
55
- // ── extract user message ──
56
-
57
- function extractUserMessage(context: Context): string {
58
- const msgs = context.messages;
59
- for (let i = msgs.length - 1; i >= 0; i--) {
60
- if (msgs[i].role !== "user") continue;
61
- const raw = contentToText((msgs[i] as any).content);
62
- if (raw) return raw.trim();
63
- }
64
- return "Continue.";
65
- }
66
-
67
- function contentToText(content: unknown): string {
68
- if (typeof content === "string") return content;
69
- if (!Array.isArray(content)) return "";
70
- return content
71
- .filter((c: any) => c?.type === "text")
72
- .map((c: any) => c.text as string)
73
- .join("\n");
74
- }
75
-
76
- // ── thinking config ──
77
-
78
- const THINKING_BUDGETS: Record<string, number> = {
79
- minimal: 1600, low: 4000, medium: 0, high: 0, xhigh: 32000,
80
- };
81
- const EFFORT_MAP: Record<string, Effort | undefined> = {
82
- minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "xhigh",
83
- };
84
-
85
- function buildThinking(reasoning?: SimpleStreamOptions["reasoning"], budgets?: SimpleStreamOptions["thinkingBudgets"]): { thinking?: ThinkingConfig; effort?: Effort } {
86
- if (!reasoning) return {};
87
- if (reasoning === "off") return { thinking: { type: "disabled" } };
88
- const budget = budgets?.[reasoning as keyof typeof budgets] ?? THINKING_BUDGETS[reasoning] ?? 0;
89
- const effort = EFFORT_MAP[reasoning];
90
- if (budget > 0) return { thinking: { type: "enabled", budgetTokens: budget }, effort };
91
- return { thinking: { type: "adaptive" }, effort };
92
- }
93
-
94
- // ── usage extraction ──
95
-
96
- function extractUsage(msg: CbMessage): Partial<AssistantMessage> | null {
97
- if (msg.type !== "result" || !msg.usage) return null;
98
- return {
99
- usage: {
100
- input: msg.usage.input_tokens ?? 0,
101
- output: msg.usage.output_tokens ?? 0,
102
- cacheRead: msg.usage.cache_read_input_tokens ?? 0,
103
- cacheWrite: msg.usage.cache_creation_input_tokens ?? 0,
104
- totalTokens:
105
- (msg.usage.input_tokens ?? 0) +
106
- (msg.usage.output_tokens ?? 0) +
107
- (msg.usage.cache_read_input_tokens ?? 0) +
108
- (msg.usage.cache_creation_input_tokens ?? 0),
109
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: msg.total_cost_usd ?? 0 },
110
- },
111
- };
112
- }
113
-
114
- // ── main ──
115
-
116
- export function streamCodebuddy(
117
- model: Model<any>,
118
- context: Context,
119
- options?: SimpleStreamOptions,
120
- ): AssistantMessageEventStream {
121
- const stream = createAssistantMessageEventStream();
122
- const partial = makePartial(model);
123
-
124
- queueMicrotask(async () => {
125
- try {
126
- stream.push({ type: "start", partial });
127
-
128
- const sdk = await loadSdk();
129
- const userText = extractUserMessage(context);
130
-
131
- const { thinking, effort } = buildThinking(options?.reasoning, options?.thinkingBudgets);
132
-
133
- const q = sdk.query({
134
- prompt: userText,
135
- options: {
136
- model: model.id,
137
- permissionMode: "bypassPermissions",
138
- maxTurns: 100,
139
- thinking,
140
- effort,
141
- includePartialMessages: true,
142
- },
143
- });
144
-
145
- // ── abort ──
146
- let aborted = false;
147
- const onAbort = async () => {
148
- if (aborted) return;
149
- aborted = true;
150
- try { await q.interrupt(); } catch { /* best-effort */ }
151
- partial.stopReason = "aborted";
152
- stream.push({ type: "error", reason: "aborted", error: partial });
153
- };
154
- if (options?.signal) {
155
- options.signal.addEventListener("abort", onAbort, { once: true });
156
- if (options.signal.aborted) { await onAbort(); return; }
157
- }
158
-
159
- // ── per-block tracking ──
160
- type BlockKind = "text" | "thinking" | "tool_use" | "redacted_thinking";
161
- const blockKind = new Map<number, BlockKind>();
162
- const blockAccum = new Map<number, string>();
163
- const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
164
-
165
- let hasStreamEvents = false;
166
-
167
- function onStart(idx: number, kind: BlockKind, init: string, meta?: { id: string; name: string; signature?: string }) {
168
- blockKind.set(idx, kind);
169
- blockAccum.set(idx, init);
170
- if (meta) blockMeta.set(idx, meta);
171
-
172
- if (kind === "redacted_thinking") {
173
- const tc: ThinkingContent = { type: "thinking", thinking: "[redacted]", redacted: true, thinkingSignature: meta?.signature };
174
- partial.content = [...partial.content, tc];
175
- stream.push({ type: "thinking_start", contentIndex: idx, partial });
176
- stream.push({ type: "thinking_end", contentIndex: idx, content: "[redacted]", partial });
177
- } else if (kind === "thinking") {
178
- const tc: ThinkingContent = { type: "thinking", thinking: init, thinkingSignature: meta?.signature };
179
- partial.content = [...partial.content, tc];
180
- stream.push({ type: "thinking_start", contentIndex: idx, partial });
181
- } else if (kind === "text") {
182
- partial.content = [...partial.content, { type: "text", text: init }];
183
- stream.push({ type: "text_start", contentIndex: idx, partial });
184
- } else if (kind === "tool_use" && meta) {
185
- partial.content = [...partial.content, { type: "toolCall", id: meta.id, name: meta.name, arguments: {} }];
186
- stream.push({ type: "toolcall_start", contentIndex: idx, partial });
187
- }
188
- }
189
-
190
- function onDelta(idx: number, deltaType: string, value: string) {
191
- const kind = blockKind.get(idx);
192
- if (!kind) return;
193
-
194
- if (deltaType === "text_delta" && kind === "text") {
195
- const next = (blockAccum.get(idx) || "") + value;
196
- blockAccum.set(idx, next);
197
- const blocks = [...partial.content];
198
- blocks[idx] = { type: "text", text: next };
199
- partial.content = blocks;
200
- stream.push({ type: "text_delta", contentIndex: idx, delta: value, partial });
201
- } else if (deltaType === "thinking_delta" && kind === "thinking") {
202
- const next = (blockAccum.get(idx) || "") + value;
203
- blockAccum.set(idx, next);
204
- const blocks = [...partial.content];
205
- blocks[idx] = { type: "thinking", thinking: next, thinkingSignature: blockMeta.get(idx)?.signature };
206
- partial.content = blocks;
207
- stream.push({ type: "thinking_delta", contentIndex: idx, delta: value, partial });
208
- } else if (deltaType === "input_json_delta" && kind === "tool_use") {
209
- const json = (blockAccum.get(idx) || "") + value;
210
- blockAccum.set(idx, json);
211
- const blocks = [...partial.content];
212
- try { blocks[idx] = { ...blocks[idx], arguments: JSON.parse(json) }; } catch { /* partial */ }
213
- partial.content = blocks;
214
- stream.push({ type: "toolcall_delta", contentIndex: idx, delta: value, partial } as any);
215
- }
216
- }
217
-
218
- function onStop(idx: number) {
219
- const kind = blockKind.get(idx);
220
- const accum = blockAccum.get(idx) || "";
221
- if (kind === "text") stream.push({ type: "text_end", contentIndex: idx, content: accum, partial });
222
- else if (kind === "thinking") stream.push({ type: "thinking_end", contentIndex: idx, content: accum, partial });
223
- else if (kind === "tool_use") stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: partial.content[idx] as ToolCall, partial });
224
- }
225
-
226
- // ── iterate ──
227
- for await (const msg of q) {
228
- if (aborted) return;
229
-
230
- if (msg.type === "system") {
231
- if (msg.model) partial.responseModel = msg.model;
232
- continue;
233
- }
234
-
235
- if (msg.type === "stream_event") {
236
- hasStreamEvents = true;
237
- const evt = msg.event;
238
-
239
- if (evt.type === "content_block_start") {
240
- const b = evt.content_block;
241
- if (b.type === "text") onStart(evt.index, "text", b.text || "");
242
- else if (b.type === "thinking") onStart(evt.index, "thinking", b.thinking || "", { id: "", name: "", signature: b.signature });
243
- else if (b.type === "redacted_thinking") onStart(evt.index, "redacted_thinking", "[redacted]", { id: "", name: "", signature: b.data });
244
- else if (b.type === "tool_use") onStart(evt.index, "tool_use", JSON.stringify(b.input || {}), { id: b.id, name: b.name });
245
- } else if (evt.type === "content_block_delta") {
246
- const d = evt.delta;
247
- if (d.type === "text_delta") onDelta(evt.index, "text_delta", d.text);
248
- else if (d.type === "thinking_delta") onDelta(evt.index, "thinking_delta", d.thinking);
249
- else if (d.type === "input_json_delta") onDelta(evt.index, "input_json_delta", d.partial_json);
250
- } else if (evt.type === "content_block_stop") {
251
- onStop(evt.index);
252
- }
253
- continue;
254
- }
255
-
256
- // Fallback: full assistant messages (when model doesn't support streaming)
257
- if (msg.type === "assistant" && !hasStreamEvents) {
258
- let ci = 0;
259
- for (const block of msg.message.content) {
260
- const idx = ci++;
261
- if (block.type === "thinking") { onStart(idx, "thinking", block.thinking); onStop(idx); }
262
- else if (block.type === "redacted_thinking") onStart(idx, "redacted_thinking", "[redacted]", { id: "", name: "", signature: block.data });
263
- else if (block.type === "text") { onStart(idx, "text", block.text); onStop(idx); }
264
- else if (block.type === "tool_use") { onStart(idx, "tool_use", JSON.stringify(block.input ?? {}), { id: block.id, name: block.name }); onStop(idx); }
265
- }
266
- }
267
-
268
- if (msg.type === "result") {
269
- const u = extractUsage(msg);
270
- if (u) Object.assign(partial, u);
271
-
272
- if (msg.subtype === "success") {
273
- partial.stopReason = "stop";
274
- stream.push({ type: "done", reason: "stop", message: { ...partial } });
275
- } else {
276
- partial.stopReason = "error";
277
- partial.errorMessage = msg.errors?.join("; ") ?? `CodeBuddy error: ${msg.subtype}`;
278
- stream.push({ type: "error", reason: "error", error: { ...partial } });
279
- }
280
- break;
281
- }
282
- }
283
- } catch (error) {
284
- stream.push({ type: "error", reason: "error", error: makeErrorPartial(model, error) });
285
- }
286
- });
287
-
288
- return stream;
289
- }