pi-codebuddy-sdk 0.1.19 → 0.3.1
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/LICENSE +21 -0
- package/README.md +87 -61
- package/package.json +30 -23
- package/src/agents-md.ts +39 -24
- package/src/askcodebuddy-ui.ts +3 -3
- package/src/cb-session-io.ts +205 -0
- package/src/config.ts +37 -33
- package/src/convert.ts +79 -83
- package/src/extract-tool-results.ts +47 -0
- package/src/index-types.ts +1 -0
- package/src/index.ts +273 -227
- package/src/models.ts +64 -11
- package/src/query-state.ts +48 -48
- package/src/sdk-gate.ts +17 -0
- package/src/session-verify.ts +38 -0
- package/src/skills.ts +65 -13
- package/src/typebox-to-zod.ts +42 -0
package/src/index.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { calculateCost, StringEnum, type AssistantMessage, type AssistantMessageEventStream, type Context, type Model, type SimpleStreamOptions, type Tool } from "@earendil-works/pi-ai";
|
|
2
2
|
import * as piAi from "@earendil-works/pi-ai";
|
|
3
|
-
|
|
4
3
|
import { buildSessionContext, compact, keyHint, type CompactionEntry, type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { query, type
|
|
6
|
-
import type { Base64ImageSource, ContentBlockParam, MessageParam } from "@tencent-ai/agent-sdk";
|
|
4
|
+
import { createSdkMcpServer, query, type Effort, type Message as CbMessage, type UserMessage as CbUserMessage } from "@tencent-ai/agent-sdk";
|
|
7
5
|
import { Type } from "typebox";
|
|
8
6
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import { createSession, deleteSession } from "./cb-session-io.js";
|
|
9
8
|
import { appendFileSync, mkdirSync, realpathSync, statSync } from "fs";
|
|
10
9
|
import { homedir } from "os";
|
|
11
10
|
import { dirname, join } from "path";
|
|
12
11
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
13
|
-
import { buildModels, codebuddyModelId, resolveModel as _resolveModel } from "./models.js";
|
|
14
|
-
import {
|
|
15
|
-
|
|
12
|
+
import { buildModels, codebuddyModelId, FALLBACK_MODELS, rawModelsFromSdk, resolveModel as _resolveModel, type PiModel } from "./models.js";
|
|
13
|
+
import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, buildCodebuddySystemPrompt } from "./skills.js";
|
|
14
|
+
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
15
|
+
import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
|
|
16
16
|
import { QueryContext, ctx } from "./query-state.js";
|
|
17
17
|
import { loadConfig, type Config } from "./config.js";
|
|
18
|
-
import {
|
|
18
|
+
import { jsonSchemaToZodShape } from "./typebox-to-zod.js";
|
|
19
19
|
import { buildActionSummary, type ToolCallState } from "./askcodebuddy-ui.js";
|
|
20
|
+
import { withSdkGate } from "./sdk-gate.js";
|
|
20
21
|
|
|
21
22
|
// Compat (#2): use factory if available (pi-ai ≥0.66), else fall back to constructor (gsd-pi etc.)
|
|
22
23
|
const _piAi = piAi as any;
|
|
@@ -26,11 +27,28 @@ const newAssistantMessageEventStream: () => AssistantMessageEventStream =
|
|
|
26
27
|
: () => new _piAi.AssistantMessageEventStream();
|
|
27
28
|
|
|
28
29
|
// --- Debug logging ---
|
|
29
|
-
//
|
|
30
|
+
// CODEBUDDY_SDK_DEBUG=1 enables local debug logs (metadata only; paths redacted; no prompt/tool bodies).
|
|
30
31
|
|
|
31
|
-
const DEBUG = process.env.
|
|
32
|
-
const DEBUG_LOG_PATH = process.env.
|
|
32
|
+
const DEBUG = process.env.CODEBUDDY_SDK_DEBUG === "1";
|
|
33
|
+
const DEBUG_LOG_PATH = process.env.CODEBUDDY_SDK_DEBUG_PATH || join(homedir(), ".pi", "agent", "codebuddy-sdk.log");
|
|
33
34
|
const DIAG_LOG_PATH = join(homedir(), ".pi", "agent", "codebuddy-sdk-diag.log");
|
|
35
|
+
const ISSUES_URL = "https://github.com/RealAlexandreAI/pi-codebuddy-sdk/issues/new";
|
|
36
|
+
|
|
37
|
+
function redactForLog(value: string): string {
|
|
38
|
+
const home = homedir();
|
|
39
|
+
return home && value.includes(home) ? value.split(home).join("~") : value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function redactLogValue(value: unknown): unknown {
|
|
43
|
+
if (typeof value === "string") return redactForLog(value);
|
|
44
|
+
if (Array.isArray(value)) return value.map(redactLogValue);
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
const out: Record<string, unknown> = {};
|
|
47
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactLogValue(v);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
34
52
|
|
|
35
53
|
// Ensure log directories exist when debug is enabled
|
|
36
54
|
if (DEBUG) {
|
|
@@ -49,46 +67,36 @@ function debug(...args: unknown[]) {
|
|
|
49
67
|
if (!DEBUG) return;
|
|
50
68
|
const ts = new Date().toISOString();
|
|
51
69
|
const fmt = (a: unknown): string => {
|
|
52
|
-
if (typeof a === "string") return a;
|
|
53
|
-
if (a instanceof Error) return `${a.name}: ${a.message}
|
|
54
|
-
return JSON.stringify(a);
|
|
70
|
+
if (typeof a === "string") return redactForLog(a);
|
|
71
|
+
if (a instanceof Error) return redactForLog(`${a.name}: ${a.message}`);
|
|
72
|
+
return redactForLog(JSON.stringify(a));
|
|
55
73
|
};
|
|
56
74
|
const msg = args.map(fmt).join(" ");
|
|
57
75
|
appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${moduleInstanceId}] ${msg}\n`);
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
// Per-query CLI debug capture. When
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// Options — see @tencent-ai/agent-sdk sdk.d.ts:1245 (debug, debugFile,
|
|
64
|
-
// stderr). Without this, CC's internal view of the world is invisible to us
|
|
65
|
-
// and "No conversation found" / empty-error reports are unactionable.
|
|
78
|
+
// Per-query CLI debug capture. When CODEBUDDY_SDK_DEBUG=1, ask the CodeBuddy CLI
|
|
79
|
+
// subprocess to write its own debug log locally. We do not forward stderr (may
|
|
80
|
+
// contain prompt or credential hints).
|
|
66
81
|
let nextCliDebugSeq = 1;
|
|
67
|
-
function makeCliDebugOptions(tag: string): { debug?: boolean; debugFile?: string
|
|
82
|
+
function makeCliDebugOptions(tag: string): { debug?: boolean; debugFile?: string } {
|
|
68
83
|
if (!DEBUG) return {};
|
|
69
84
|
const seq = nextCliDebugSeq++;
|
|
70
85
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
71
|
-
const logDir = join(dirname(DEBUG_LOG_PATH), "
|
|
86
|
+
const logDir = join(dirname(DEBUG_LOG_PATH), "cb-cli-logs");
|
|
72
87
|
try { mkdirSync(logDir, { recursive: true }); } catch { /* ignore */ }
|
|
73
88
|
const debugFile = join(logDir, `${ts}-${tag}-${seq}.log`);
|
|
74
|
-
debug(`cli-debug: ${tag} #${seq} → ${debugFile}`);
|
|
75
|
-
return {
|
|
76
|
-
debug: true,
|
|
77
|
-
debugFile,
|
|
78
|
-
stderr: (data: string) => {
|
|
79
|
-
for (const line of data.split(/\r?\n/)) {
|
|
80
|
-
if (line) debug(`[cli-stderr ${tag}#${seq}] ${line}`);
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
};
|
|
89
|
+
debug(`cli-debug: ${tag} #${seq} → ${redactForLog(debugFile)}`);
|
|
90
|
+
return { debug: true, debugFile };
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
/**
|
|
93
|
+
/** Diagnostic dump for "should never happen" paths — only when debug is enabled */
|
|
87
94
|
function diagDump(label: string, data: Record<string, unknown>) {
|
|
95
|
+
if (!DEBUG) return;
|
|
88
96
|
const ts = new Date().toISOString();
|
|
89
|
-
const entry = { ts, moduleInstanceId, label, ...data };
|
|
97
|
+
const entry = { ts, moduleInstanceId, label, ...redactLogValue(data) as Record<string, unknown> };
|
|
90
98
|
appendFileSync(DIAG_LOG_PATH, JSON.stringify(entry) + "\n");
|
|
91
|
-
debug(`DIAG: ${label} (see ${DIAG_LOG_PATH})`);
|
|
99
|
+
debug(`DIAG: ${label} (see ${redactForLog(DIAG_LOG_PATH)})`);
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
// --- Constants ---
|
|
@@ -113,18 +121,16 @@ const SDK_TO_PI_TOOL_NAME: Record<string, string> = {
|
|
|
113
121
|
read: "read", write: "write", edit: "edit", bash: "bash",
|
|
114
122
|
};
|
|
115
123
|
|
|
116
|
-
|
|
117
|
-
const MODELS = buildModels([
|
|
118
|
-
{ id: "hy3-preview-agent-ioa", name: "Hunyuan 3", reasoning: false, input: ["text"], contextWindow: 131072, maxTokens: 8192 },
|
|
119
|
-
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192 },
|
|
120
|
-
{ id: "deepseek-v4-pro-ioa", name: "DeepSeek V4 Pro", reasoning: false, input: ["text"], contextWindow: 131072, maxTokens: 8192 },
|
|
121
|
-
{ id: "glm-5.2-ioa", name: "GLM 5.2", reasoning: false, input: ["text"], contextWindow: 131072, maxTokens: 8192 },
|
|
122
|
-
] as any);
|
|
124
|
+
let MODELS: PiModel[] = buildModels(FALLBACK_MODELS);
|
|
123
125
|
let providerSettings: NonNullable<Config["provider"]> = {};
|
|
124
|
-
|
|
126
|
+
|
|
127
|
+
type ContentBlockParam =
|
|
128
|
+
| { type: "text"; text: string }
|
|
129
|
+
| { type: "image"; source: { type: "base64"; media_type: string; data: string } };
|
|
130
|
+
type SettingSource = "user" | "project" | "local";
|
|
125
131
|
|
|
126
132
|
function resolveModel(input: string) {
|
|
127
|
-
return _resolveModel(MODELS, input)
|
|
133
|
+
return _resolveModel(MODELS, input);
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
// --- Error handling ---
|
|
@@ -206,29 +212,25 @@ function convertAndImportMessages(
|
|
|
206
212
|
debug(`convertAndImportMessages: imported roles:`, anthropicMessages.map((m, i) => {
|
|
207
213
|
const c = m.content;
|
|
208
214
|
if (typeof c === "string") return `[${i}]${m.role}:text`;
|
|
209
|
-
if (Array.isArray(c)) return `[${i}]${m.role}:${
|
|
215
|
+
if (Array.isArray(c)) return `[${i}]${m.role}:${c.map((b: { type?: string }) => b.type).join("+")}`;
|
|
210
216
|
return `[${i}]${m.role}:?`;
|
|
211
217
|
}).join(" "));
|
|
212
218
|
if (sanitizedIds.size > 0) {
|
|
213
219
|
debug(`convertAndImportMessages: sanitized ${sanitizedIds.size} tool IDs:`,
|
|
214
220
|
[...sanitizedIds.entries()].map(([orig, clean]) => orig === clean ? orig : `${orig}→${clean}`).join(", "));
|
|
215
221
|
}
|
|
216
|
-
|
|
217
|
-
const repaired = repairToolPairing(anthropicMessages);
|
|
218
|
-
if (repaired.length !== anthropicMessages.length) {
|
|
219
|
-
debug(`convertAndImportMessages: repairToolPairing ${anthropicMessages.length} → ${repaired.length} msgs`);
|
|
220
|
-
}
|
|
221
|
-
if (repaired.length) session.importMessages(repaired);
|
|
222
|
+
if (messages.length) session.importPiMessages(messages);
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// Pi doesn't pass tool results directly — it appends them to the context and calls
|
|
226
|
+
// the provider again. Thin wrapper over extract-tool-results.js that adds per-turn
|
|
225
227
|
// debug logging at the extraction boundary.
|
|
226
228
|
function extractAllToolResults(context: Context): McpResult[] {
|
|
227
229
|
const { results, stopIdx } = _extractAllToolResults(context.messages as unknown as Array<{ role: string; [key: string]: unknown }>);
|
|
228
230
|
debug(`extractAllToolResults: ${results.length} results from ${context.messages.length} msgs, stopped at index ${stopIdx}`);
|
|
229
231
|
debug(`extractAllToolResults: all msg roles:`, context.messages.map((m, i) => `[${i}]${m.role}`).join(" "));
|
|
230
232
|
for (let r = 0; r < results.length; r++) {
|
|
231
|
-
debug(`extractAllToolResults: result[${r}] id=${results[r].toolCallId}${results[r].isError ? " ERROR" : ""}
|
|
233
|
+
debug(`extractAllToolResults: result[${r}] id=${results[r].toolCallId}${results[r].isError ? " ERROR" : ""} contentLen=${JSON.stringify(results[r].content).length}`);
|
|
232
234
|
}
|
|
233
235
|
return results;
|
|
234
236
|
}
|
|
@@ -269,18 +271,19 @@ function extractUserPromptBlocks(messages: Context["messages"]): ContentBlockPar
|
|
|
269
271
|
hasImage = true;
|
|
270
272
|
blocks.push({
|
|
271
273
|
type: "image",
|
|
272
|
-
source: { type: "base64", media_type: block.mimeType
|
|
274
|
+
source: { type: "base64", media_type: block.mimeType, data: block.data },
|
|
273
275
|
});
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
return hasImage ? blocks : null;
|
|
277
279
|
}
|
|
278
280
|
|
|
279
|
-
async function* wrapPromptStream(blocks: ContentBlockParam[]): AsyncIterable<
|
|
281
|
+
async function* wrapPromptStream(blocks: ContentBlockParam[]): AsyncIterable<CbUserMessage> {
|
|
280
282
|
yield {
|
|
281
283
|
type: "user",
|
|
282
|
-
|
|
284
|
+
session_id: "",
|
|
283
285
|
parent_tool_use_id: null,
|
|
286
|
+
message: { role: "user", content: blocks as any },
|
|
284
287
|
};
|
|
285
288
|
}
|
|
286
289
|
|
|
@@ -311,8 +314,8 @@ function extractIsolatedSummaryPrompt(messages: Context["messages"]): string {
|
|
|
311
314
|
return promptText;
|
|
312
315
|
}
|
|
313
316
|
|
|
314
|
-
function resultErrorText(message:
|
|
315
|
-
const result = message as
|
|
317
|
+
function resultErrorText(message: CbMessage): string {
|
|
318
|
+
const result = message as CbMessage & { subtype?: string; errors?: unknown; error?: unknown };
|
|
316
319
|
if (Array.isArray(result.errors)) return result.errors.map(String).join("\n");
|
|
317
320
|
if (typeof result.error === "string") return result.error;
|
|
318
321
|
return `CodeBuddy summary failed: ${result.subtype ?? "unknown result"}`;
|
|
@@ -341,7 +344,7 @@ async function runIsolatedSummary(
|
|
|
341
344
|
try {
|
|
342
345
|
const promptText = extractIsolatedSummaryPrompt(context.messages);
|
|
343
346
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
344
|
-
const
|
|
347
|
+
const codebuddyExecutable = loadConfig(cwd).provider?.pathToCodebuddyCode;
|
|
345
348
|
const cliModel = codebuddyModelId(model);
|
|
346
349
|
debug(`compact summary: spawn model=${cliModel} registeredModel=${model.id} promptLen=${promptText.length}`);
|
|
347
350
|
|
|
@@ -349,16 +352,15 @@ async function runIsolatedSummary(
|
|
|
349
352
|
prompt: promptText,
|
|
350
353
|
options: {
|
|
351
354
|
cwd,
|
|
352
|
-
env: { ...process.env, DISABLE_AUTO_COMPACT: "1"
|
|
355
|
+
env: { ...process.env, DISABLE_AUTO_COMPACT: "1" },
|
|
353
356
|
tools: [],
|
|
354
357
|
strictMcpConfig: true,
|
|
355
|
-
|
|
356
|
-
skills: [],
|
|
358
|
+
settingSources: [] as SettingSource[],
|
|
357
359
|
persistSession: false,
|
|
358
360
|
systemPrompt: context.systemPrompt,
|
|
359
361
|
model: cliModel,
|
|
360
362
|
maxTurns: 1,
|
|
361
|
-
...(
|
|
363
|
+
...(codebuddyExecutable ? { pathToCodebuddyCode: codebuddyExecutable } : {}),
|
|
362
364
|
...makeCliDebugOptions("compact-summary"),
|
|
363
365
|
},
|
|
364
366
|
});
|
|
@@ -459,13 +461,13 @@ function verifyWrittenSession(
|
|
|
459
461
|
for (const msg of warnings) {
|
|
460
462
|
debug(`WARNING session verify: ${msg}`);
|
|
461
463
|
piUI?.notify(
|
|
462
|
-
`Session
|
|
463
|
-
`cwd=${
|
|
464
|
-
`
|
|
465
|
-
|
|
464
|
+
`Session sync issue: ${msg}. ` +
|
|
465
|
+
`cwd=${redactForLog(cwd)}` +
|
|
466
|
+
(DEBUG ? ` (see ${redactForLog(DEBUG_LOG_PATH)})` : ` (set CODEBUDDY_SDK_DEBUG=1 for a local log)`) +
|
|
467
|
+
`. Report: ${ISSUES_URL}`,
|
|
466
468
|
"warning",
|
|
467
469
|
);
|
|
468
|
-
diagDump("session_verify_fail", { msg, jsonlPath, cwd, realpath: safeRealpath(cwd),
|
|
470
|
+
diagDump("session_verify_fail", { msg, jsonlPath, cwd, realpath: safeRealpath(cwd), codebuddyConfigDir: process.env.CODEBUDDY_CONFIG_DIR ?? null });
|
|
469
471
|
}
|
|
470
472
|
}
|
|
471
473
|
|
|
@@ -475,7 +477,7 @@ function safeRealpath(p: string): string {
|
|
|
475
477
|
|
|
476
478
|
// Diagnostic snapshot of where a session file was just written. Catches the
|
|
477
479
|
// class of bugs where pi writes to ~/.claude/projects/<X> but CC SDK reads
|
|
478
|
-
// from ~/.claude/projects/<Y> (symlinks,
|
|
480
|
+
// from ~/.claude/projects/<Y> (symlinks, CODEBUDDY_CONFIG_DIR, hash mismatch).
|
|
479
481
|
function debugSessionPaths(label: string, cwd: string, jsonlPath: string): void {
|
|
480
482
|
const realCwd = safeRealpath(cwd);
|
|
481
483
|
let fileSize: number | null = null;
|
|
@@ -485,11 +487,10 @@ function debugSessionPaths(label: string, cwd: string, jsonlPath: string): void
|
|
|
485
487
|
fileExists = true;
|
|
486
488
|
fileSize = st.size;
|
|
487
489
|
} catch { /* file may not exist yet */ }
|
|
488
|
-
debug(`${label}: cwd=${cwd}`);
|
|
489
|
-
if (realCwd !== cwd) debug(`${label}: realpath(cwd)=${realCwd} (
|
|
490
|
-
debug(`${label}: jsonlPath=${jsonlPath}`);
|
|
490
|
+
debug(`${label}: cwd=${redactForLog(cwd)}`);
|
|
491
|
+
if (realCwd !== cwd) debug(`${label}: realpath(cwd)=${redactForLog(realCwd)} (symlink-resolved)`);
|
|
492
|
+
debug(`${label}: jsonlPath=${redactForLog(jsonlPath)}`);
|
|
491
493
|
debug(`${label}: fileExists=${fileExists}${fileSize != null ? ` size=${fileSize}` : ""}`);
|
|
492
|
-
debug(`${label}: env.CLAUDE_CONFIG_DIR=${process.env.CLAUDE_CONFIG_DIR ?? "(unset)"} HOME=${process.env.HOME ?? "(unset)"}`);
|
|
493
494
|
}
|
|
494
495
|
|
|
495
496
|
// Two semantic paths:
|
|
@@ -570,11 +571,11 @@ function syncSharedSession(
|
|
|
570
571
|
const preserveId = previousSessionId !== undefined && !sharedSession?.forceRotate;
|
|
571
572
|
if (preserveId) {
|
|
572
573
|
// Wipe prior jsonl + companion dir (no-op if nothing to wipe).
|
|
573
|
-
deleteSession(previousSessionId!, cwd, process.env.
|
|
574
|
+
deleteSession(previousSessionId!, cwd, process.env.CODEBUDDY_CONFIG_DIR);
|
|
574
575
|
}
|
|
575
576
|
const session = createSession({
|
|
576
577
|
projectPath: cwd,
|
|
577
|
-
|
|
578
|
+
codebuddyDir: process.env.CODEBUDDY_CONFIG_DIR,
|
|
578
579
|
...(preserveId ? { sessionId: previousSessionId } : {}),
|
|
579
580
|
...(modelId ? { model: modelId } : {}),
|
|
580
581
|
});
|
|
@@ -675,6 +676,29 @@ function contextForToolResults(results: McpResult[]): QueryContext | undefined {
|
|
|
675
676
|
return undefined;
|
|
676
677
|
}
|
|
677
678
|
|
|
679
|
+
function resolveMcpTools(context: Context, excludeToolName?: string): {
|
|
680
|
+
mcpTools: Tool[];
|
|
681
|
+
customToolNameToSdk: Map<string, string>;
|
|
682
|
+
customToolNameToPi: Map<string, string>;
|
|
683
|
+
} {
|
|
684
|
+
const mcpTools: Tool[] = [];
|
|
685
|
+
const customToolNameToSdk = new Map<string, string>();
|
|
686
|
+
const customToolNameToPi = new Map<string, string>();
|
|
687
|
+
|
|
688
|
+
if (!context.tools) return { mcpTools, customToolNameToSdk, customToolNameToPi };
|
|
689
|
+
|
|
690
|
+
for (const tool of context.tools) {
|
|
691
|
+
if (tool.name === excludeToolName) continue;
|
|
692
|
+
const sdkName = `${MCP_TOOL_PREFIX}${tool.name}`;
|
|
693
|
+
mcpTools.push(tool);
|
|
694
|
+
customToolNameToSdk.set(tool.name, sdkName);
|
|
695
|
+
customToolNameToSdk.set(tool.name.toLowerCase(), sdkName);
|
|
696
|
+
customToolNameToPi.set(sdkName, tool.name);
|
|
697
|
+
customToolNameToPi.set(sdkName.toLowerCase(), tool.name);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return { mcpTools, customToolNameToSdk, customToolNameToPi };
|
|
701
|
+
}
|
|
678
702
|
|
|
679
703
|
// Creates an MCP server that bridges pi tools to the SDK. Each tool handler
|
|
680
704
|
// blocks on a Promise until pi delivers the tool result via streamSimple.
|
|
@@ -682,7 +706,31 @@ function contextForToolResults(results: McpResult[]): QueryContext | undefined {
|
|
|
682
706
|
// emits tool_use blocks). Results are matched by ID, not position.
|
|
683
707
|
// Handlers close over the captured `queryCtx`, ensuring they operate on the
|
|
684
708
|
// correct query's state while multiple queries run concurrently.
|
|
685
|
-
|
|
709
|
+
function buildMcpServers(tools: Tool[], queryCtx: QueryContext): Record<string, ReturnType<typeof createSdkMcpServer>> | undefined {
|
|
710
|
+
if (!tools.length) return undefined;
|
|
711
|
+
const mcpTools = tools.map((tool) => ({
|
|
712
|
+
name: tool.name,
|
|
713
|
+
description: tool.description,
|
|
714
|
+
inputSchema: jsonSchemaToZodShape(tool.parameters),
|
|
715
|
+
handler: async () => {
|
|
716
|
+
const toolCallId = queryCtx.turnToolCallIds[queryCtx.nextHandlerIdx++];
|
|
717
|
+
if (!toolCallId) debug(`WARNING: mcp handler ${tool.name} has no toolCallId (idx=${queryCtx.nextHandlerIdx - 1}, available=${queryCtx.turnToolCallIds.length})`);
|
|
718
|
+
if (toolCallId && queryCtx.pendingResults.has(toolCallId)) {
|
|
719
|
+
const result = queryCtx.pendingResults.get(toolCallId)!;
|
|
720
|
+
queryCtx.pendingResults.delete(toolCallId);
|
|
721
|
+
debug(`mcp handler: ${tool.name} [${toolCallId}] → resolved from queue (${queryCtx.pendingResults.size} remaining)`);
|
|
722
|
+
return result;
|
|
723
|
+
}
|
|
724
|
+
debug(`mcp handler: ${tool.name} [${toolCallId}] → waiting`);
|
|
725
|
+
return new Promise<McpResult>((resolve) => {
|
|
726
|
+
queryCtx.pendingToolCalls.set(toolCallId, { toolName: tool.name, resolve });
|
|
727
|
+
});
|
|
728
|
+
},
|
|
729
|
+
}));
|
|
730
|
+
const server = createSdkMcpServer({ name: MCP_SERVER_NAME, version: "1.0.0", tools: mcpTools });
|
|
731
|
+
return { [MCP_SERVER_NAME]: server };
|
|
732
|
+
}
|
|
733
|
+
|
|
686
734
|
// --- Usage helpers ---
|
|
687
735
|
|
|
688
736
|
function updateUsage(output: AssistantMessage, usage: Record<string, number | undefined>, model: Model<any>): void {
|
|
@@ -707,7 +755,7 @@ function updateUsage(output: AssistantMessage, usage: Record<string, number | un
|
|
|
707
755
|
// match the docs — e.g. bare Opus served 200K on Pro, or [1m] not honored.
|
|
708
756
|
// The result message's modelUsage is otherwise discarded; this makes the
|
|
709
757
|
// gap observable. See issue #18.
|
|
710
|
-
function logServedContextWindow(label: string, message:
|
|
758
|
+
function logServedContextWindow(label: string, message: CbMessage, model: Model<any>): void {
|
|
711
759
|
const modelUsage = (message as any).modelUsage as Record<string, { contextWindow?: number; maxOutputTokens?: number }> | undefined;
|
|
712
760
|
if (!modelUsage) return;
|
|
713
761
|
for (const [k, v] of Object.entries(modelUsage)) {
|
|
@@ -718,8 +766,8 @@ function logServedContextWindow(label: string, message: SDKMessage, model: Model
|
|
|
718
766
|
// --- Effort level mapping ---
|
|
719
767
|
// Pi reasoning levels → CC SDK effort levels
|
|
720
768
|
|
|
721
|
-
const REASONING_TO_EFFORT: Record<string,
|
|
722
|
-
minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "
|
|
769
|
+
const REASONING_TO_EFFORT: Record<string, Effort> = {
|
|
770
|
+
minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "xhigh",
|
|
723
771
|
};
|
|
724
772
|
|
|
725
773
|
// --- Provider helpers: misc ---
|
|
@@ -775,7 +823,7 @@ function ensureTurnStarted(c: QueryContext): void {
|
|
|
775
823
|
|
|
776
824
|
function finalizeCurrentStream(c: QueryContext, stopReason?: string): void {
|
|
777
825
|
if (!c.currentPiStream || !c.turnOutput) return;
|
|
778
|
-
debug(`provider: finalizeCurrentStream called, stopReason=${stopReason},
|
|
826
|
+
debug(`provider: finalizeCurrentStream called, stopReason=${stopReason}, outputStop=${c.turnOutput!.stopReason}`);
|
|
779
827
|
if (!c.turnStarted) ensureTurnStarted(c);
|
|
780
828
|
const reason = stopReason === "length" ? "length" : "stop";
|
|
781
829
|
const stream = c.currentPiStream;
|
|
@@ -788,14 +836,14 @@ function finalizeCurrentStream(c: QueryContext, stopReason?: string): void {
|
|
|
788
836
|
/** Maps Anthropic stream events to pi stream events (text, thinking, toolcall).
|
|
789
837
|
* On message_stop with tool_use: ends currentPiStream so pi can execute the tool. */
|
|
790
838
|
function processStreamEvent(
|
|
791
|
-
message:
|
|
839
|
+
message: CbMessage,
|
|
792
840
|
customToolNameToPi: Map<string, string>,
|
|
793
841
|
model: Model<any>,
|
|
794
842
|
c: QueryContext,
|
|
795
843
|
): void {
|
|
796
844
|
if (!c.currentPiStream || !c.turnOutput) return;
|
|
797
845
|
c.turnSawStreamEvent = true;
|
|
798
|
-
const event = (message as
|
|
846
|
+
const event = (message as CbMessage & { event: any }).event;
|
|
799
847
|
|
|
800
848
|
if (event?.type === "message_start") {
|
|
801
849
|
c.turnToolCallIds = [];
|
|
@@ -904,7 +952,7 @@ function processStreamEvent(
|
|
|
904
952
|
// arrives before any stream_events, this is the primary content path. Must maintain
|
|
905
953
|
// the same stream lifecycle as processStreamEvent — including ending the stream on
|
|
906
954
|
// tool_use to prevent deadlock with the MCP handler.
|
|
907
|
-
function processAssistantMessage(message:
|
|
955
|
+
function processAssistantMessage(message: CbMessage, model: Model<any>, customToolNameToPi: Map<string, string>, c: QueryContext): void {
|
|
908
956
|
if (c.turnSawStreamEvent) return;
|
|
909
957
|
const assistantMsg = (message as any).message;
|
|
910
958
|
if (!assistantMsg?.content) return;
|
|
@@ -975,18 +1023,19 @@ async function consumeQuery(
|
|
|
975
1023
|
if (wasAborted()) break;
|
|
976
1024
|
if (!queryCtx.currentPiStream || !queryCtx.turnOutput) continue;
|
|
977
1025
|
|
|
978
|
-
switch (message.type) {
|
|
1026
|
+
switch ((message as { type: string }).type) {
|
|
979
1027
|
case "stream_event":
|
|
980
1028
|
processStreamEvent(message, customToolNameToPi, model, queryCtx);
|
|
981
1029
|
break;
|
|
982
1030
|
case "assistant":
|
|
983
1031
|
processAssistantMessage(message, model, customToolNameToPi, queryCtx);
|
|
984
1032
|
break;
|
|
985
|
-
case "result":
|
|
1033
|
+
case "result": {
|
|
1034
|
+
const resultMsg = message as Extract<CbMessage, { type: "result" }>;
|
|
986
1035
|
logServedContextWindow("result", message, model);
|
|
987
|
-
if (!queryCtx.turnSawStreamEvent &&
|
|
1036
|
+
if (!queryCtx.turnSawStreamEvent && resultMsg.subtype === "success") {
|
|
988
1037
|
ensureTurnStarted(queryCtx);
|
|
989
|
-
const text =
|
|
1038
|
+
const text = resultMsg.result || "";
|
|
990
1039
|
queryCtx.turnBlocks.push({ type: "text", text });
|
|
991
1040
|
const idx = queryCtx.turnBlocks.length - 1;
|
|
992
1041
|
queryCtx.currentPiStream?.push({ type: "text_start", contentIndex: idx, partial: queryCtx.turnOutput });
|
|
@@ -994,21 +1043,23 @@ async function consumeQuery(
|
|
|
994
1043
|
queryCtx.currentPiStream?.push({ type: "text_end", contentIndex: idx, content: text, partial: queryCtx.turnOutput });
|
|
995
1044
|
}
|
|
996
1045
|
break;
|
|
1046
|
+
}
|
|
997
1047
|
case "system":
|
|
998
1048
|
if ((message as any).subtype === "init" && (message as any).session_id) {
|
|
999
1049
|
capturedSessionId = (message as any).session_id;
|
|
1000
1050
|
}
|
|
1001
1051
|
break;
|
|
1002
1052
|
case "user":
|
|
1003
|
-
|
|
1053
|
+
case "file-history-snapshot":
|
|
1054
|
+
break;
|
|
1004
1055
|
case "rate_limit_event": {
|
|
1005
1056
|
const info = (message as any).rate_limit_info;
|
|
1006
1057
|
debug("consumeQuery: rate_limit_event", JSON.stringify(info).slice(0, 300));
|
|
1007
1058
|
if (info?.status === "rejected") {
|
|
1008
1059
|
const resetsAt = info.resetsAt ? new Date(info.resetsAt).toLocaleTimeString() : "unknown";
|
|
1009
|
-
piUI?.notify(`
|
|
1060
|
+
piUI?.notify(`CodeBuddy rate limited (${info.rateLimitType ?? "unknown"}) — resets at ${resetsAt}`, "warning");
|
|
1010
1061
|
} else if (info?.status === "allowed_warning") {
|
|
1011
|
-
piUI?.notify(`
|
|
1062
|
+
piUI?.notify(`CodeBuddy rate limit warning: ${Math.round(info.utilization ?? 0)}% used (${info.rateLimitType ?? ""})`, "warning");
|
|
1012
1063
|
}
|
|
1013
1064
|
break;
|
|
1014
1065
|
}
|
|
@@ -1026,12 +1077,12 @@ async function consumeQuery(
|
|
|
1026
1077
|
|
|
1027
1078
|
/** Provider entry point. Pi calls this for each new prompt and each tool result.
|
|
1028
1079
|
* Two cases: tool result delivery (active query) or fresh query. */
|
|
1029
|
-
function
|
|
1080
|
+
function streamCodebuddySdk(model: Model<any>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
1030
1081
|
const stream = newAssistantMessageEventStream();
|
|
1031
1082
|
|
|
1032
1083
|
// DEBUG: trace followUp message triggering
|
|
1033
1084
|
const lastMsgRole = context.messages[context.messages.length - 1]?.role;
|
|
1034
|
-
debug(`provider:
|
|
1085
|
+
debug(`provider: streamCodebuddySdk called, activeQuery=${!!ctx().activeQuery}, lastMsgRole=${lastMsgRole}, isReentrant=${ctx().activeQuery !== null}`);
|
|
1035
1086
|
|
|
1036
1087
|
const activeQuery = ctx().activeQuery !== null;
|
|
1037
1088
|
const allResults = activeQueryContexts.size > 0 ? extractAllToolResults(context) : [];
|
|
@@ -1054,7 +1105,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1054
1105
|
if (id && resultCtx.pendingToolCalls.has(id)) {
|
|
1055
1106
|
const pending = resultCtx.pendingToolCalls.get(id)!;
|
|
1056
1107
|
resultCtx.pendingToolCalls.delete(id);
|
|
1057
|
-
debug(`provider: resolving ${pending.toolName} [${id}]${result.isError ? " (error)" : ""}
|
|
1108
|
+
debug(`provider: resolving ${pending.toolName} [${id}]${result.isError ? " (error)" : ""} contentLen=${JSON.stringify(result.content).length}`);
|
|
1058
1109
|
pending.resolve(result);
|
|
1059
1110
|
} else if (id) {
|
|
1060
1111
|
resultCtx.pendingResults.set(id, result);
|
|
@@ -1068,7 +1119,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1068
1119
|
}
|
|
1069
1120
|
if (resultCtx.pendingToolCalls.size > 0) {
|
|
1070
1121
|
debug(`WARNING: ${resultCtx.pendingToolCalls.size} MCP handlers still waiting after delivering ${allResults.length} results`);
|
|
1071
|
-
piUI?.notify(`
|
|
1122
|
+
piUI?.notify(`CodeBuddy SDK: ${resultCtx.pendingToolCalls.size} tool handler(s) still waiting — provider may be stuck`, "warning");
|
|
1072
1123
|
}
|
|
1073
1124
|
|
|
1074
1125
|
// Detect user messages (steer/followUp) that pi injected into context
|
|
@@ -1083,7 +1134,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1083
1134
|
const userPrompt = extractUserPrompt(context.messages);
|
|
1084
1135
|
if (userPrompt) {
|
|
1085
1136
|
resultCtx.deferredUserMessages.push(userPrompt);
|
|
1086
|
-
debug(`provider: deferred user message for replay after query
|
|
1137
|
+
debug(`provider: deferred user message for replay after query (len=${userPrompt.length})`);
|
|
1087
1138
|
}
|
|
1088
1139
|
}
|
|
1089
1140
|
|
|
@@ -1126,10 +1177,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1126
1177
|
queryCtx.resetTurnState(model);
|
|
1127
1178
|
queryCtx.latestCursor = 0;
|
|
1128
1179
|
|
|
1129
|
-
|
|
1130
|
-
const mcpTools: any[] = [];
|
|
1131
|
-
const customToolNameToSdk = new Map<string, string>();
|
|
1132
|
-
const customToolNameToPi = new Map<string, string>();
|
|
1180
|
+
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(context, askCodebuddyToolName);
|
|
1133
1181
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
1134
1182
|
const syncResult = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
|
|
1135
1183
|
const { sessionId: resumeSessionId } = syncResult;
|
|
@@ -1152,34 +1200,31 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1152
1200
|
promptText = "[continue]";
|
|
1153
1201
|
}
|
|
1154
1202
|
|
|
1155
|
-
const prompt: string | AsyncIterable<
|
|
1203
|
+
const prompt: string | AsyncIterable<CbUserMessage> = promptBlocks
|
|
1156
1204
|
? wrapPromptStream(promptBlocks)
|
|
1157
1205
|
: promptText;
|
|
1158
|
-
|
|
1159
|
-
const mcpServers = {};
|
|
1206
|
+
const mcpServers = buildMcpServers(mcpTools, queryCtx);
|
|
1160
1207
|
const appendSystemPrompt = providerSettings.appendSystemPrompt !== false;
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const systemPromptAppend = appendParts.length > 0 ? appendParts.join("\n\n") : undefined;
|
|
1208
|
+
const systemPrompt = appendSystemPrompt
|
|
1209
|
+
? buildCodebuddySystemPrompt(context.systemPrompt)
|
|
1210
|
+
: undefined;
|
|
1165
1211
|
|
|
1166
1212
|
// MCP auto-loading suppression: CC reads MCP servers from ~/.claude.json (top-level
|
|
1167
1213
|
// + per-project) and .mcp.json. Since pi executes tools (not CC), those are pure
|
|
1168
1214
|
// token overhead. --strict-mcp-config tells the binary to use ONLY mcpServers passed
|
|
1169
1215
|
// programmatically and ignore filesystem MCP entries — applied unconditionally because
|
|
1170
1216
|
// settingSources=undefined does NOT give isolation (the CC default loads all sources).
|
|
1171
|
-
|
|
1172
|
-
const settingSources = undefined
|
|
1217
|
+
const settingSources: SettingSource[] | undefined = appendSystemPrompt
|
|
1173
1218
|
? undefined
|
|
1174
1219
|
: providerSettings.settingSources ?? ["user", "project"];
|
|
1175
1220
|
const strictMcpConfigEnabled = providerSettings.strictMcpConfig !== false;
|
|
1176
|
-
const
|
|
1221
|
+
const codebuddyExecutable = providerSettings.pathToCodebuddyCode;
|
|
1177
1222
|
|
|
1178
1223
|
// Prefer the model's own thinkingLevelMap when present (pi-ai 0.72+ ships
|
|
1179
1224
|
// per-model overrides — e.g. opus-4-7 wants xhigh→xhigh, not xhigh→max).
|
|
1180
1225
|
// Fall back to our generic table for older pi-ai or unmapped levels.
|
|
1181
1226
|
const effort = options?.reasoning
|
|
1182
|
-
? ((model as any).thinkingLevelMap?.[options.reasoning] as
|
|
1227
|
+
? ((model as any).thinkingLevelMap?.[options.reasoning] as Effort | undefined)
|
|
1183
1228
|
?? REASONING_TO_EFFORT[options.reasoning]
|
|
1184
1229
|
: undefined;
|
|
1185
1230
|
|
|
@@ -1188,35 +1233,21 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1188
1233
|
const cliModel = codebuddyModelId(model);
|
|
1189
1234
|
const extraArgs: Record<string, string | null> = { model: cliModel };
|
|
1190
1235
|
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
if (effort) extraArgs["thinking-display"] = "summarized";
|
|
1194
|
-
|
|
1195
|
-
// Suppress claude.ai cloud MCP servers (Figma/Canva/etc. auto-discovered via OAuth
|
|
1196
|
-
// when the user is logged into Anthropic). These are a separate code path from
|
|
1197
|
-
// filesystem MCP and are NOT blocked by --strict-mcp-config or settingSources=undefined.
|
|
1198
|
-
// The native CC binary gates them on env var ENABLE_CLAUDEAI_MCP_SERVERS: setting it
|
|
1199
|
-
// to "0"/"false"/"no"/"off" makes the loader return early before any cloud fetch.
|
|
1200
|
-
// DISABLE_AUTO_COMPACT=1: pi owns context-management and propagates its own
|
|
1201
|
-
// /compact via session_compact (see handler in default export). Letting CC
|
|
1202
|
-
// also autocompact would double-flush the prompt cache and races pi's
|
|
1203
|
-
// threshold with CC's, including CC's anti-thrashing guard (issue #8).
|
|
1204
|
-
// Manual /compact in CC still works (we never invoke it).
|
|
1205
|
-
const childEnv = { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: "0", DISABLE_AUTO_COMPACT: "1" };
|
|
1236
|
+
|
|
1237
|
+
const childEnv = { ...process.env, DISABLE_AUTO_COMPACT: "1" };
|
|
1206
1238
|
const queryOptions: NonNullable<Parameters<typeof query>[0]["options"]> = {
|
|
1207
1239
|
cwd,
|
|
1208
1240
|
env: childEnv,
|
|
1209
1241
|
tools: [],
|
|
1210
1242
|
permissionMode: "bypassPermissions",
|
|
1211
1243
|
includePartialMessages: true,
|
|
1212
|
-
systemPrompt
|
|
1213
|
-
type: "preset", preset: "codebuddy",
|
|
1214
|
-
append: systemPromptAppend ? systemPromptAppend : undefined,
|
|
1215
|
-
},
|
|
1244
|
+
systemPrompt,
|
|
1216
1245
|
extraArgs,
|
|
1217
1246
|
...(effort ? { effort } : {}),
|
|
1218
|
-
|
|
1219
|
-
...(
|
|
1247
|
+
...(settingSources ? { settingSources } : {}),
|
|
1248
|
+
...(mcpServers ? { mcpServers } : {}),
|
|
1249
|
+
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
1250
|
+
...(codebuddyExecutable ? { pathToCodebuddyCode: codebuddyExecutable } : {}),
|
|
1220
1251
|
...makeCliDebugOptions("provider"),
|
|
1221
1252
|
};
|
|
1222
1253
|
|
|
@@ -1224,26 +1255,18 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1224
1255
|
`model=${cliModel} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
1225
1256
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1226
1257
|
`appendSys=${appendSystemPrompt} strictMcp=${strictMcpConfigEnabled}`,
|
|
1227
|
-
`
|
|
1258
|
+
`promptLen=${promptText.length}${promptBlocks ? " [+images]" : ""}`);
|
|
1228
1259
|
|
|
1229
|
-
// 3. Start SDK query
|
|
1260
|
+
// 3. Start SDK query (wait for model discovery + serialize SDK subprocess access)
|
|
1230
1261
|
let wasAborted = false;
|
|
1231
|
-
|
|
1232
|
-
queryCtx.activeQuery = sdkQuery;
|
|
1233
|
-
activeQueryContexts.add(queryCtx);
|
|
1234
|
-
|
|
1235
|
-
// 4. Capture context for abort handling
|
|
1262
|
+
let sdkQuery: ReturnType<typeof query> | undefined;
|
|
1236
1263
|
const abortCtx = queryCtx;
|
|
1237
1264
|
|
|
1238
1265
|
const requestAbort = () => {
|
|
1239
|
-
|
|
1240
|
-
// Both are needed — interrupt alone lets the current API call finish.
|
|
1241
|
-
void sdkQuery.interrupt().catch(() => {});
|
|
1242
|
-
try { sdkQuery.interrupt(); } catch {}
|
|
1266
|
+
void sdkQuery?.interrupt().catch(() => {});
|
|
1243
1267
|
};
|
|
1244
1268
|
const onAbort = () => {
|
|
1245
1269
|
wasAborted = true;
|
|
1246
|
-
// Prevent stale deferred messages from being replayed by parent on pop
|
|
1247
1270
|
abortCtx.deferredUserMessages = [];
|
|
1248
1271
|
for (const pending of abortCtx.pendingToolCalls.values()) { pending.resolve({ content: [{ type: "text", text: "Operation aborted" }] }); }
|
|
1249
1272
|
abortCtx.pendingToolCalls.clear();
|
|
@@ -1255,12 +1278,16 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1255
1278
|
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1256
1279
|
}
|
|
1257
1280
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1281
|
+
void (async () => {
|
|
1282
|
+
await ensureModelsDiscovered();
|
|
1283
|
+
sdkQuery = query({ prompt, options: queryOptions });
|
|
1284
|
+
queryCtx.activeQuery = sdkQuery;
|
|
1285
|
+
activeQueryContexts.add(queryCtx);
|
|
1286
|
+
|
|
1287
|
+
try {
|
|
1288
|
+
const { capturedSessionId } = await consumeQuery(sdkQuery, customToolNameToPi, model, () => wasAborted, queryCtx);
|
|
1261
1289
|
debug(`provider: consumeQuery completed, stopReason=${queryCtx.turnOutput?.stopReason}, error=${queryCtx.turnOutput?.errorMessage}, aborted=${wasAborted}`);
|
|
1262
1290
|
|
|
1263
|
-
// --- Abort detection in normal completion path ---
|
|
1264
1291
|
if (wasAborted || options?.signal?.aborted) {
|
|
1265
1292
|
if (sharedSession) sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
1266
1293
|
queryCtx.deferredUserMessages = [];
|
|
@@ -1269,19 +1296,18 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1269
1296
|
queryCtx.turnOutput.stopReason = "aborted";
|
|
1270
1297
|
queryCtx.turnOutput.errorMessage = "Operation aborted";
|
|
1271
1298
|
}
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
markStreamComplete(
|
|
1275
|
-
|
|
1299
|
+
const errStream = queryCtx.currentPiStream;
|
|
1300
|
+
errStream?.push({ type: "error", reason: "aborted", error: queryCtx.turnOutput! });
|
|
1301
|
+
markStreamComplete(errStream);
|
|
1302
|
+
errStream?.end();
|
|
1276
1303
|
queryCtx.currentPiStream = null;
|
|
1277
1304
|
return;
|
|
1278
1305
|
}
|
|
1279
1306
|
|
|
1280
|
-
// --- Capture session ID ---
|
|
1281
1307
|
const sessionId = capturedSessionId ?? sharedSession?.sessionId;
|
|
1282
1308
|
if (syncResult.preserveSharedSession) {
|
|
1283
1309
|
if (capturedSessionId && capturedSessionId !== sharedSession?.sessionId) {
|
|
1284
|
-
deleteSession(capturedSessionId, cwd, process.env.
|
|
1310
|
+
deleteSession(capturedSessionId, cwd, process.env.CODEBUDDY_CONFIG_DIR);
|
|
1285
1311
|
debug(`provider: query done, deleted ephemeral session ${capturedSessionId.slice(0, 8)} to preserve shared session`);
|
|
1286
1312
|
}
|
|
1287
1313
|
debug(`provider: query done, ignoring captured session ${capturedSessionId?.slice(0, 8) ?? "none"} to preserve shared session`);
|
|
@@ -1291,49 +1317,42 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1291
1317
|
sharedSession = { sessionId, cursor, cwd };
|
|
1292
1318
|
}
|
|
1293
1319
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
debug(`provider: replaying deferred user message: ${steerPrompt.slice(0, 60)}`);
|
|
1299
|
-
queryCtx.resetTurnState(model);
|
|
1300
|
-
|
|
1301
|
-
const resumeId = sharedSession?.sessionId;
|
|
1302
|
-
if (!resumeId) {
|
|
1303
|
-
debug(`WARNING: no session to resume for deferred message, dropping`);
|
|
1304
|
-
break;
|
|
1305
|
-
}
|
|
1320
|
+
while (queryCtx.deferredUserMessages.length > 0 && !isReentrant && !wasAborted) {
|
|
1321
|
+
const steerPrompt = queryCtx.deferredUserMessages.shift()!;
|
|
1322
|
+
debug(`provider: replaying deferred user message (len=${steerPrompt.length})`);
|
|
1323
|
+
queryCtx.resetTurnState(model);
|
|
1306
1324
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1325
|
+
const resumeId = sharedSession?.sessionId;
|
|
1326
|
+
if (!resumeId) {
|
|
1327
|
+
debug(`WARNING: no session to resume for deferred message, dropping`);
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1310
1330
|
|
|
1311
|
-
|
|
1331
|
+
const contOptions = { ...queryOptions, resume: resumeId, ...makeCliDebugOptions("continuation") };
|
|
1332
|
+
const contQuery = query({ prompt: steerPrompt, options: contOptions });
|
|
1333
|
+
queryCtx.activeQuery = contQuery;
|
|
1334
|
+
debug(`provider: continuation query, model=${cliModel}, resume=${resumeId.slice(0, 8)}, promptLen=${steerPrompt.length}`);
|
|
1312
1335
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
contQuery.interrupt();
|
|
1336
|
+
try {
|
|
1337
|
+
const { capturedSessionId: contSid } = await consumeQuery(contQuery, customToolNameToPi, model, () => wasAborted, queryCtx);
|
|
1338
|
+
const sid = contSid ?? sharedSession?.sessionId;
|
|
1339
|
+
if (sid) sharedSession = { sessionId: sid, cursor: sharedSession?.cursor ?? 0, cwd };
|
|
1340
|
+
} catch (contError) {
|
|
1341
|
+
debug(`provider: continuation query error:`, contError);
|
|
1342
|
+
break;
|
|
1343
|
+
} finally {
|
|
1344
|
+
if (wasAborted || options?.signal?.aborted) {
|
|
1345
|
+
await contQuery.return().catch(() => {});
|
|
1324
1346
|
}
|
|
1325
1347
|
}
|
|
1326
|
-
} finally {
|
|
1327
|
-
queryCtx.activeQuery = sdkQuery;
|
|
1328
1348
|
}
|
|
1329
1349
|
|
|
1330
|
-
if (!isReentrant
|
|
1350
|
+
if (!isReentrant) {
|
|
1331
1351
|
debug("provider: clearing activeQuery before final stream completion");
|
|
1332
1352
|
queryCtx.activeQuery = null;
|
|
1333
1353
|
}
|
|
1334
1354
|
finalizeCurrentStream(queryCtx, queryCtx.turnOutput?.stopReason);
|
|
1335
|
-
})
|
|
1336
|
-
.catch((error) => {
|
|
1355
|
+
} catch (error) {
|
|
1337
1356
|
debug(`provider: query error, model=${cliModel}, aborted=${Boolean(options?.signal?.aborted)}, error=`, error);
|
|
1338
1357
|
if ((wasAborted || options?.signal?.aborted) && sharedSession) {
|
|
1339
1358
|
sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
@@ -1345,31 +1364,28 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1345
1364
|
queryCtx.turnOutput.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
1346
1365
|
queryCtx.turnOutput.errorMessage = error instanceof Error ? error.message : String(error);
|
|
1347
1366
|
}
|
|
1348
|
-
if (!isReentrant
|
|
1367
|
+
if (!isReentrant) {
|
|
1349
1368
|
for (const pending of queryCtx.pendingToolCalls.values()) { pending.resolve({ content: [{ type: "text", text: "Query ended" }] }); }
|
|
1350
1369
|
queryCtx.pendingToolCalls.clear();
|
|
1351
1370
|
queryCtx.pendingResults.clear();
|
|
1352
|
-
debug("provider: clearing activeQuery before error stream completion");
|
|
1353
1371
|
queryCtx.activeQuery = null;
|
|
1354
1372
|
}
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
markStreamComplete(
|
|
1358
|
-
|
|
1373
|
+
const errStream = queryCtx.currentPiStream;
|
|
1374
|
+
errStream?.push({ type: "error", reason: (queryCtx.turnOutput?.stopReason ?? "error") as "aborted" | "error", error: queryCtx.turnOutput! });
|
|
1375
|
+
markStreamComplete(errStream);
|
|
1376
|
+
errStream?.end();
|
|
1359
1377
|
queryCtx.currentPiStream = null;
|
|
1360
|
-
}
|
|
1361
|
-
.finally(() => {
|
|
1378
|
+
} finally {
|
|
1362
1379
|
if (options?.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1363
1380
|
if (queryCtx.activeQuery === sdkQuery) {
|
|
1364
|
-
// Drain pending handlers for this query
|
|
1365
1381
|
for (const pending of queryCtx.pendingToolCalls.values()) { pending.resolve({ content: [{ type: "text", text: "Query ended" }] }); }
|
|
1366
1382
|
queryCtx.pendingToolCalls.clear();
|
|
1367
1383
|
queryCtx.pendingResults.clear();
|
|
1368
1384
|
queryCtx.activeQuery = null;
|
|
1369
1385
|
}
|
|
1370
1386
|
activeQueryContexts.delete(queryCtx);
|
|
1371
|
-
|
|
1372
|
-
|
|
1387
|
+
}
|
|
1388
|
+
})();
|
|
1373
1389
|
|
|
1374
1390
|
return stream;
|
|
1375
1391
|
}
|
|
@@ -1418,43 +1434,42 @@ async function promptAndWait(
|
|
|
1418
1434
|
// Mode → disallowed tools
|
|
1419
1435
|
const disallowedTools = MODE_DISALLOWED_TOOLS[mode] ?? [];
|
|
1420
1436
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1437
|
+
const askSystemPrompt = options?.systemPrompt
|
|
1438
|
+
? buildCodebuddySystemPrompt(options.systemPrompt, {
|
|
1439
|
+
includeSkills: options.appendSkills !== false,
|
|
1440
|
+
})
|
|
1441
|
+
: undefined;
|
|
1424
1442
|
|
|
1425
1443
|
// Effort
|
|
1426
1444
|
const effort = options?.thinking && options.thinking !== "off"
|
|
1427
1445
|
? REASONING_TO_EFFORT[options.thinking] : undefined;
|
|
1428
1446
|
|
|
1429
|
-
const
|
|
1447
|
+
const codebuddyExecutable = providerSettings.pathToCodebuddyCode;
|
|
1430
1448
|
|
|
1431
1449
|
const extraArgs: Record<string, string | null> = {
|
|
1432
1450
|
"strict-mcp-config": null,
|
|
1433
1451
|
model: cliModel,
|
|
1434
1452
|
};
|
|
1435
|
-
if (effort) extraArgs["thinking-display"] = "summarized";
|
|
1436
1453
|
|
|
1437
1454
|
debug("askCodebuddy:",
|
|
1438
1455
|
`mode=${mode} model=${modelId} cliModel=${cliModel} effort=${effort ?? "default"}`,
|
|
1439
1456
|
`isolated=${options?.isolated ?? false} resume=${resumeSessionId?.slice(0, 8) ?? "none"}`,
|
|
1440
|
-
`
|
|
1457
|
+
`sysPrompt=${Boolean(askSystemPrompt)} promptLen=${prompt.length}`);
|
|
1441
1458
|
|
|
1442
1459
|
const sdkQuery = query({
|
|
1443
1460
|
prompt,
|
|
1444
1461
|
options: {
|
|
1445
1462
|
cwd,
|
|
1446
|
-
env: { ...process.env,
|
|
1463
|
+
env: { ...process.env, DISABLE_AUTO_COMPACT: "1" },
|
|
1447
1464
|
permissionMode: "bypassPermissions",
|
|
1448
1465
|
...(disallowedTools.length ? { disallowedTools } : {}),
|
|
1449
1466
|
...(effort ? { effort } : {}),
|
|
1450
|
-
systemPrompt:
|
|
1451
|
-
|
|
1452
|
-
: undefined,
|
|
1453
|
-
|
|
1467
|
+
systemPrompt: askSystemPrompt,
|
|
1468
|
+
settingSources: ["user", "project"] as SettingSource[],
|
|
1454
1469
|
extraArgs,
|
|
1455
1470
|
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
1456
1471
|
...(options?.isolated ? { persistSession: false } : {}),
|
|
1457
|
-
...(
|
|
1472
|
+
...(codebuddyExecutable ? { pathToCodebuddyCode: codebuddyExecutable } : {}),
|
|
1458
1473
|
...makeCliDebugOptions("askclaude"),
|
|
1459
1474
|
},
|
|
1460
1475
|
});
|
|
@@ -1463,7 +1478,7 @@ async function promptAndWait(
|
|
|
1463
1478
|
let wasAborted = false;
|
|
1464
1479
|
const onAbort = () => {
|
|
1465
1480
|
wasAborted = true;
|
|
1466
|
-
sdkQuery.interrupt().catch(() => {
|
|
1481
|
+
sdkQuery.interrupt().catch(() => {});
|
|
1467
1482
|
};
|
|
1468
1483
|
if (signal?.aborted) { onAbort(); throw new Error("Aborted"); }
|
|
1469
1484
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1480,7 +1495,7 @@ async function promptAndWait(
|
|
|
1480
1495
|
|
|
1481
1496
|
switch (message.type) {
|
|
1482
1497
|
case "stream_event": {
|
|
1483
|
-
const event = (message as
|
|
1498
|
+
const event = (message as CbMessage & { event: any }).event;
|
|
1484
1499
|
// Text deltas → accumulate and stream
|
|
1485
1500
|
if (event?.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
|
1486
1501
|
responseText += event.delta.text;
|
|
@@ -1532,7 +1547,7 @@ async function promptAndWait(
|
|
|
1532
1547
|
return { responseText, stopReason };
|
|
1533
1548
|
} finally {
|
|
1534
1549
|
signal?.removeEventListener("abort", onAbort);
|
|
1535
|
-
sdkQuery.interrupt();
|
|
1550
|
+
void sdkQuery.interrupt().catch(() => {});
|
|
1536
1551
|
}
|
|
1537
1552
|
}
|
|
1538
1553
|
|
|
@@ -1545,22 +1560,52 @@ const PREVIEW_MAX_CHARS = 1000;
|
|
|
1545
1560
|
const PREVIEW_MAX_LINES = 6;
|
|
1546
1561
|
|
|
1547
1562
|
let askCodebuddyToolName = "AskCodebuddy";
|
|
1563
|
+
let piApi: ExtensionAPI | null = null;
|
|
1564
|
+
let modelsDiscovered = false;
|
|
1548
1565
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1566
|
+
let discoverInFlight: Promise<void> | null = null;
|
|
1567
|
+
|
|
1568
|
+
async function discoverModels(pi: ExtensionAPI): Promise<void> {
|
|
1569
|
+
await withSdkGate(async () => {
|
|
1570
|
+
try {
|
|
1571
|
+
const q = query({ prompt: " ", options: { maxTurns: 0, permissionMode: "bypassPermissions", tools: [] } });
|
|
1572
|
+
const supported = await q.supportedModels();
|
|
1573
|
+
await q.return().catch(() => {});
|
|
1574
|
+
if (!supported.length) return;
|
|
1575
|
+
MODELS = buildModels(rawModelsFromSdk(supported as any));
|
|
1576
|
+
const g = globalThis as Record<symbol, any>;
|
|
1577
|
+
const streamFn = g[ACTIVE_STREAM_SIMPLE_KEY] ?? streamCodebuddySdk;
|
|
1578
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
1579
|
+
name: "CodeBuddy",
|
|
1580
|
+
baseUrl: PROVIDER_ID,
|
|
1581
|
+
apiKey: "not-used",
|
|
1582
|
+
api: "codebuddy-sdk",
|
|
1583
|
+
models: MODELS as any,
|
|
1584
|
+
streamSimple: streamFn as any,
|
|
1585
|
+
});
|
|
1586
|
+
modelsDiscovered = true;
|
|
1587
|
+
debug(`discoverModels: registered ${MODELS.length} models from SDK`);
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
debug("discoverModels: failed, using fallback models", err);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
async function ensureModelsDiscovered(): Promise<void> {
|
|
1595
|
+
if (modelsDiscovered || !piApi) return;
|
|
1596
|
+
discoverInFlight ??= discoverModels(piApi);
|
|
1597
|
+
await discoverInFlight;
|
|
1598
|
+
}
|
|
1552
1599
|
|
|
1600
|
+
export default function (pi: ExtensionAPI) {
|
|
1601
|
+
piApi = pi;
|
|
1553
1602
|
const config = loadConfig(process.cwd());
|
|
1554
1603
|
debug("loadConfig:", JSON.stringify(config));
|
|
1555
1604
|
providerSettings = config.provider ?? {};
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
longContextExtraUsage: providerSettings.longContextExtraUsage ?? false,
|
|
1560
|
-
};
|
|
1561
|
-
const registeredModels = MODELS; // ponytail: no long-context suffix for CodeBuddy
|
|
1605
|
+
const registeredModels = MODELS;
|
|
1606
|
+
|
|
1607
|
+
discoverInFlight = discoverModels(pi);
|
|
1562
1608
|
|
|
1563
|
-
// Reset shared session on pi session lifecycle events
|
|
1564
1609
|
const clearSession = (event: string) => {
|
|
1565
1610
|
debug(`${event}: clearing session ${sharedSession?.sessionId?.slice(0, 8) ?? "none"}`);
|
|
1566
1611
|
sharedSession = null;
|
|
@@ -1569,7 +1614,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1569
1614
|
// This allows /reload to work — the old instance clears the flag so
|
|
1570
1615
|
// the new instance can register fresh without wrapping stale state.
|
|
1571
1616
|
const g = globalThis as Record<symbol, any>;
|
|
1572
|
-
if (g[ACTIVE_STREAM_SIMPLE_KEY] ===
|
|
1617
|
+
if (g[ACTIVE_STREAM_SIMPLE_KEY] === streamCodebuddySdk) {
|
|
1573
1618
|
debug(`${event}: clearing ACTIVE_STREAM_SIMPLE_KEY`);
|
|
1574
1619
|
g[ACTIVE_STREAM_SIMPLE_KEY] = undefined;
|
|
1575
1620
|
}
|
|
@@ -1583,7 +1628,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1583
1628
|
pi.on("session_shutdown", () => clearSession("session_shutdown"));
|
|
1584
1629
|
|
|
1585
1630
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
1586
|
-
if (ctx.model?.baseUrl !==
|
|
1631
|
+
if (ctx.model?.baseUrl !== PROVIDER_ID) return undefined;
|
|
1587
1632
|
debug(
|
|
1588
1633
|
`session_before_compact: takeover reason=${event.reason} willRetry=${event.willRetry} ` +
|
|
1589
1634
|
`isSplitTurn=${event.preparation.isSplitTurn} messages=${event.preparation.messagesToSummarize.length} ` +
|
|
@@ -1608,7 +1653,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1608
1653
|
const msg = errorMessage(err);
|
|
1609
1654
|
debug("session_before_compact: takeover failed; cancelling to avoid native compact fallback", err);
|
|
1610
1655
|
ctx.ui?.notify?.(
|
|
1611
|
-
`
|
|
1656
|
+
`CodeBuddy SDK compact failed (${redactForLog(msg)}). Retry, switch model, or reduce context.`,
|
|
1612
1657
|
"error",
|
|
1613
1658
|
);
|
|
1614
1659
|
return { cancel: true };
|
|
@@ -1641,14 +1686,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1641
1686
|
const g = globalThis as Record<symbol, any>;
|
|
1642
1687
|
if (!g[ACTIVE_STREAM_SIMPLE_KEY]) {
|
|
1643
1688
|
// First instance: store our streamSimple and register.
|
|
1644
|
-
g[ACTIVE_STREAM_SIMPLE_KEY] =
|
|
1689
|
+
g[ACTIVE_STREAM_SIMPLE_KEY] = streamCodebuddySdk;
|
|
1645
1690
|
pi.registerProvider(PROVIDER_ID, {
|
|
1646
|
-
|
|
1691
|
+
name: "CodeBuddy",
|
|
1692
|
+
baseUrl: PROVIDER_ID,
|
|
1647
1693
|
apiKey: "not-used",
|
|
1648
1694
|
api: "codebuddy-sdk",
|
|
1649
|
-
models: registeredModels,
|
|
1695
|
+
models: registeredModels as any,
|
|
1650
1696
|
// Cast: pi-ai AssistantMessageEventStream diamond dep between pi-coding-agent and pi-agent-core
|
|
1651
|
-
streamSimple:
|
|
1697
|
+
streamSimple: streamCodebuddySdk as any,
|
|
1652
1698
|
});
|
|
1653
1699
|
} else {
|
|
1654
1700
|
// Subsequent instance (subagent session): skip registration entirely.
|
|
@@ -1730,7 +1776,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1730
1776
|
},
|
|
1731
1777
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1732
1778
|
// Guard: circular delegation
|
|
1733
|
-
if (ctx.model?.baseUrl ===
|
|
1779
|
+
if (ctx.model?.baseUrl === PROVIDER_ID) {
|
|
1734
1780
|
debug("askCodebuddy: blocked circular delegation (active provider is codebuddy-sdk)");
|
|
1735
1781
|
return {
|
|
1736
1782
|
content: [{ type: "text" as const, text: "Error: AskCodebuddy cannot be used when the active provider is codebuddy-sdk — you're already running through CodeBuddy." }],
|