pi-codebuddy-sdk 0.4.6 → 0.4.8
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 +9 -9
- package/src/index.ts +28 -22
- package/src/transcript-compat.ts +81 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-codebuddy-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Pi extension that uses CodeBuddy (via Agent SDK) as a model provider and adds an AskCodebuddy tool.",
|
|
5
5
|
"author": "slahser",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,18 +34,18 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@tencent-ai/agent-sdk": "^0.3.259",
|
|
36
36
|
"change-case": "^5.4.4",
|
|
37
|
-
"typebox": "^1.3.
|
|
37
|
+
"typebox": "^1.3.34"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@earendil-works/pi-ai": "^0.
|
|
41
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
42
|
-
"@earendil-works/pi-tui": "^0.
|
|
40
|
+
"@earendil-works/pi-ai": "^0.86.0",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "^0.86.0",
|
|
42
|
+
"@earendil-works/pi-tui": "^0.86.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@earendil-works/pi-ai": "^0.
|
|
46
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
47
|
-
"@earendil-works/pi-tui": "^0.
|
|
48
|
-
"@types/node": "^26.6.
|
|
45
|
+
"@earendil-works/pi-ai": "^0.86.0",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^0.86.0",
|
|
47
|
+
"@earendil-works/pi-tui": "^0.86.0",
|
|
48
|
+
"@types/node": "^26.6.2",
|
|
49
49
|
"tsx": "^4.23.13",
|
|
50
50
|
"typescript": "^7.0.2"
|
|
51
51
|
},
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { dirname, join } from "path";
|
|
|
11
11
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
12
12
|
import { buildModels, codebuddyModelId, FALLBACK_MODELS, rawModelsFromSdk, resolveModel as _resolveModel, type PiModel } from "./models.js";
|
|
13
13
|
import { readModelsCache, writeModelsCache } from "./models-cache.js";
|
|
14
|
+
import { readTranscript } from "./transcript-compat.js";
|
|
14
15
|
import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, buildCodebuddySystemPrompt } from "./skills.js";
|
|
15
16
|
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
16
17
|
import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
|
|
@@ -413,7 +414,8 @@ async function runIsolatedSummary(
|
|
|
413
414
|
};
|
|
414
415
|
|
|
415
416
|
try {
|
|
416
|
-
const
|
|
417
|
+
const view = readTranscript(context);
|
|
418
|
+
const promptText = extractIsolatedSummaryPrompt(view.messages);
|
|
417
419
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
418
420
|
const codebuddyExecutable = loadConfig(cwd).provider?.pathToCodebuddyCode;
|
|
419
421
|
const cliModel = codebuddyModelId(model);
|
|
@@ -428,7 +430,7 @@ async function runIsolatedSummary(
|
|
|
428
430
|
strictMcpConfig: true,
|
|
429
431
|
settingSources: [] as SettingSource[],
|
|
430
432
|
persistSession: false,
|
|
431
|
-
systemPrompt:
|
|
433
|
+
systemPrompt: view.systemPrompt,
|
|
432
434
|
model: cliModel,
|
|
433
435
|
maxTurns: 1,
|
|
434
436
|
...(codebuddyExecutable ? { pathToCodebuddyCode: codebuddyExecutable } : {}),
|
|
@@ -772,7 +774,7 @@ function contextForToolResults(results: McpResult[]): QueryContext | undefined {
|
|
|
772
774
|
return undefined;
|
|
773
775
|
}
|
|
774
776
|
|
|
775
|
-
function resolveMcpTools(
|
|
777
|
+
function resolveMcpTools(declaredTools: Tool[] | undefined, excludeToolName?: string): {
|
|
776
778
|
mcpTools: Tool[];
|
|
777
779
|
customToolNameToSdk: Map<string, string>;
|
|
778
780
|
customToolNameToPi: Map<string, string>;
|
|
@@ -781,9 +783,9 @@ function resolveMcpTools(context: Context, excludeToolName?: string): {
|
|
|
781
783
|
const customToolNameToSdk = new Map<string, string>();
|
|
782
784
|
const customToolNameToPi = new Map<string, string>();
|
|
783
785
|
|
|
784
|
-
if (!
|
|
786
|
+
if (!declaredTools) return { mcpTools, customToolNameToSdk, customToolNameToPi };
|
|
785
787
|
|
|
786
|
-
for (const tool of
|
|
788
|
+
for (const tool of declaredTools) {
|
|
787
789
|
if (tool.name === excludeToolName) continue;
|
|
788
790
|
const sdkName = `${MCP_TOOL_PREFIX}${tool.name}`;
|
|
789
791
|
mcpTools.push(tool);
|
|
@@ -1187,8 +1189,12 @@ async function consumeQuery(
|
|
|
1187
1189
|
function streamCodebuddySdk(model: Model<any>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
1188
1190
|
const stream = newAssistantMessageEventStream();
|
|
1189
1191
|
|
|
1192
|
+
// Upstream #9548: pi-ai >= 0.85.2 passes a normalized transcript (prompt + tools carried by
|
|
1193
|
+
// system messages). Read both shapes so this provider works before and after that change.
|
|
1194
|
+
const { messages: convoMessages, systemPrompt: baseSystemPrompt, tools: declaredTools } = readTranscript(context);
|
|
1195
|
+
|
|
1190
1196
|
// DEBUG: trace followUp message triggering
|
|
1191
|
-
const lastMsgRole =
|
|
1197
|
+
const lastMsgRole = convoMessages[convoMessages.length - 1]?.role;
|
|
1192
1198
|
debug(`provider: streamCodebuddySdk called, activeQuery=${!!ctx().activeQuery}, lastMsgRole=${lastMsgRole}, isReentrant=${ctx().activeQuery !== null}`);
|
|
1193
1199
|
|
|
1194
1200
|
const activeQuery = ctx().activeQuery !== null;
|
|
@@ -1196,7 +1202,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1196
1202
|
const resultCtx = allResults.length > 0 ? contextForToolResults(allResults) : undefined;
|
|
1197
1203
|
const isReentrantUserQuery = activeQuery && lastMsgRole === "user" && allResults.length === 0;
|
|
1198
1204
|
if (isReentrantUserQuery) {
|
|
1199
|
-
debug(`provider: active query user-only call treated as reentrant fresh query, waitingHandlers=${ctx().pendingToolCalls.size}, ctx.msgs=${
|
|
1205
|
+
debug(`provider: active query user-only call treated as reentrant fresh query, waitingHandlers=${ctx().pendingToolCalls.size}, ctx.msgs=${convoMessages.length}`);
|
|
1200
1206
|
}
|
|
1201
1207
|
|
|
1202
1208
|
// --- Tool result delivery ---
|
|
@@ -1206,7 +1212,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1206
1212
|
if (resultCtx) {
|
|
1207
1213
|
claimCurrentPiStream(stream, "tool-result", resultCtx);
|
|
1208
1214
|
resultCtx.resetTurnState(model);
|
|
1209
|
-
debug(`provider: tool results, ${allResults.length} results, ${resultCtx.pendingToolCalls.size} waiting handlers, ctx.msgs=${
|
|
1215
|
+
debug(`provider: tool results, ${allResults.length} results, ${resultCtx.pendingToolCalls.size} waiting handlers, ctx.msgs=${convoMessages.length}`);
|
|
1210
1216
|
for (const result of allResults) {
|
|
1211
1217
|
const id = result.toolCallId;
|
|
1212
1218
|
if (id && resultCtx.pendingToolCalls.has(id)) {
|
|
@@ -1245,25 +1251,25 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1245
1251
|
// The bridge can't forward these mid-query (the SDK query is in progress),
|
|
1246
1252
|
// so we save them for replay as continuation queries after consumeQuery ends.
|
|
1247
1253
|
if (lastMsgRole === "user") {
|
|
1248
|
-
const userPrompt = extractUserPrompt(
|
|
1254
|
+
const userPrompt = extractUserPrompt(convoMessages);
|
|
1249
1255
|
if (userPrompt) {
|
|
1250
1256
|
resultCtx.deferredUserMessages.push(userPrompt);
|
|
1251
1257
|
debug(`provider: deferred user message for replay after query (len=${userPrompt.length})`);
|
|
1252
1258
|
}
|
|
1253
1259
|
}
|
|
1254
1260
|
|
|
1255
|
-
if (sharedSession) sharedSession.cursor =
|
|
1256
|
-
resultCtx.latestCursor = Math.max(resultCtx.latestCursor,
|
|
1261
|
+
if (sharedSession) sharedSession.cursor = convoMessages.length;
|
|
1262
|
+
resultCtx.latestCursor = Math.max(resultCtx.latestCursor, convoMessages.length);
|
|
1257
1263
|
return stream;
|
|
1258
1264
|
}
|
|
1259
1265
|
|
|
1260
1266
|
// --- Orphaned tool result (e.g. user aborted a tool call) ---
|
|
1261
1267
|
// The query is gone but pi still delivered the result. Nothing to do — just
|
|
1262
1268
|
// emit end_turn so pi waits for the next real user message.
|
|
1263
|
-
const lastMsg =
|
|
1269
|
+
const lastMsg = convoMessages[convoMessages.length - 1];
|
|
1264
1270
|
if (lastMsg?.role === "toolResult") {
|
|
1265
1271
|
debug(`provider: orphaned tool result after abort, emitting end_turn`);
|
|
1266
|
-
if (sharedSession) sharedSession.cursor =
|
|
1272
|
+
if (sharedSession) sharedSession.cursor = convoMessages.length;
|
|
1267
1273
|
const c = ctx(); // capture current context for the microtask
|
|
1268
1274
|
queueMicrotask(() => {
|
|
1269
1275
|
c.resetTurnState(model);
|
|
@@ -1291,24 +1297,24 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1291
1297
|
queryCtx.resetTurnState(model);
|
|
1292
1298
|
queryCtx.latestCursor = 0;
|
|
1293
1299
|
|
|
1294
|
-
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(
|
|
1300
|
+
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(declaredTools, askCodebuddyToolName);
|
|
1295
1301
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
1296
|
-
const syncResult = syncSharedSession(
|
|
1302
|
+
const syncResult = syncSharedSession(convoMessages, cwd, customToolNameToSdk, model.id, Boolean(isReentrant));
|
|
1297
1303
|
const { sessionId: resumeSessionId } = syncResult;
|
|
1298
|
-
const promptBlocks = extractUserPromptBlocks(
|
|
1299
|
-
let promptText = extractUserPrompt(
|
|
1304
|
+
const promptBlocks = extractUserPromptBlocks(convoMessages);
|
|
1305
|
+
let promptText = extractUserPrompt(convoMessages) ?? "";
|
|
1300
1306
|
|
|
1301
1307
|
// Guard: empty prompt means the last context message isn't a user message.
|
|
1302
1308
|
// This should never happen with per-query state — dump diagnostics if it does.
|
|
1303
1309
|
if (!promptText && !promptBlocks) {
|
|
1304
1310
|
diagDump("empty_prompt", {
|
|
1305
|
-
contextLength:
|
|
1311
|
+
contextLength: convoMessages.length,
|
|
1306
1312
|
lastMsgRole: lastMsg?.role,
|
|
1307
1313
|
isReentrant,
|
|
1308
1314
|
activeQueryContexts: activeQueryContexts.size,
|
|
1309
1315
|
activeQueryExists: queryCtx.activeQuery !== null,
|
|
1310
1316
|
sharedSession: sharedSession ? { sessionId: sharedSession.sessionId.slice(0, 8), cursor: sharedSession.cursor } : null,
|
|
1311
|
-
messageRoles:
|
|
1317
|
+
messageRoles: convoMessages.map((m, i) => `[${i}]${m.role}`).join(" "),
|
|
1312
1318
|
});
|
|
1313
1319
|
// Recover: use a continuation prompt so the SDK doesn't send an empty text block
|
|
1314
1320
|
promptText = "[continue]";
|
|
@@ -1320,7 +1326,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1320
1326
|
const mcpServers = buildMcpServers(mcpTools, queryCtx);
|
|
1321
1327
|
const appendSystemPrompt = providerSettings.appendSystemPrompt !== false;
|
|
1322
1328
|
const systemPrompt = appendSystemPrompt
|
|
1323
|
-
? buildCodebuddySystemPrompt(
|
|
1329
|
+
? buildCodebuddySystemPrompt(baseSystemPrompt)
|
|
1324
1330
|
: undefined;
|
|
1325
1331
|
|
|
1326
1332
|
// MCP auto-loading suppression: CC reads MCP servers from ~/.claude.json (top-level
|
|
@@ -1366,7 +1372,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1366
1372
|
};
|
|
1367
1373
|
|
|
1368
1374
|
debug("provider: fresh query",
|
|
1369
|
-
`model=${cliModel} msgs=${
|
|
1375
|
+
`model=${cliModel} msgs=${convoMessages.length} tools=${mcpTools.length}`,
|
|
1370
1376
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1371
1377
|
`appendSys=${appendSystemPrompt} strictMcp=${strictMcpConfigEnabled}`,
|
|
1372
1378
|
`promptLen=${promptText.length}${promptBlocks ? " [+images]" : ""}`);
|
|
@@ -1426,7 +1432,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1426
1432
|
}
|
|
1427
1433
|
debug(`provider: query done, ignoring captured session ${capturedSessionId?.slice(0, 8) ?? "none"} to preserve shared session`);
|
|
1428
1434
|
} else if (sessionId) {
|
|
1429
|
-
const cursor = Math.max(
|
|
1435
|
+
const cursor = Math.max(convoMessages.length, queryCtx.latestCursor, sharedSession?.cursor ?? 0);
|
|
1430
1436
|
debug(`provider: query done, session=${sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
1431
1437
|
sharedSession = { sessionId, cursor, cwd };
|
|
1432
1438
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
2
|
+
import type { Message, Tool } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
// Upstream #9548 ("Mid conversation system messages") folds `Context.systemPrompt` and
|
|
5
|
+
// `Context.tools` into the transcript's system messages, so provider-facing code receives a
|
|
6
|
+
// `TranscriptContext` that only carries `messages`. This module reads both shapes so the
|
|
7
|
+
// provider keeps working on pi-ai <= 0.85.1 and >= 0.85.2 without a hard version bump.
|
|
8
|
+
|
|
9
|
+
export interface TranscriptView {
|
|
10
|
+
/** Conversation messages with system messages removed (the pre-#9548 shape). */
|
|
11
|
+
messages: Message[];
|
|
12
|
+
/** Rendered system prompt, or undefined when the request carries none. */
|
|
13
|
+
systemPrompt: string | undefined;
|
|
14
|
+
/** Tool declarations available for this request. */
|
|
15
|
+
tools: Tool[] | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type MaybeTranscript = {
|
|
19
|
+
messages: Message[];
|
|
20
|
+
systemPrompt?: string;
|
|
21
|
+
tools?: Tool[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ai = piAi as unknown as Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
function textOfContent(content: unknown): string {
|
|
27
|
+
if (typeof content === "string") return content;
|
|
28
|
+
if (Array.isArray(content)) {
|
|
29
|
+
return content
|
|
30
|
+
.map((part) =>
|
|
31
|
+
part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string"
|
|
32
|
+
? (part as { text: string }).text
|
|
33
|
+
: "",
|
|
34
|
+
)
|
|
35
|
+
.join("");
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read the system prompt, tool declarations, and conversation from a request context that
|
|
42
|
+
* may be either a legacy `Context` or a normalized `TranscriptContext`.
|
|
43
|
+
*/
|
|
44
|
+
export function readTranscript(context: MaybeTranscript): TranscriptView {
|
|
45
|
+
const messages = Array.isArray(context.messages) ? context.messages : [];
|
|
46
|
+
|
|
47
|
+
// Legacy shape (pi-ai <= 0.85.1): prompt and tools are top-level fields.
|
|
48
|
+
if (typeof context.systemPrompt === "string" || Array.isArray(context.tools)) {
|
|
49
|
+
return { messages, systemPrompt: context.systemPrompt, tools: context.tools };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Normalized shape (pi-ai >= 0.85.2): replay the transcript through upstream helpers.
|
|
53
|
+
const collapseSystemMessages = ai.collapseSystemMessages as
|
|
54
|
+
| ((c: { messages: Message[] }) => { messages: Message[] })
|
|
55
|
+
| undefined;
|
|
56
|
+
const getCurrentSystemPrompt = ai.getCurrentSystemPrompt as ((m: readonly unknown[]) => string) | undefined;
|
|
57
|
+
const getCurrentTools = ai.getCurrentTools as ((m: readonly unknown[]) => Tool[]) | undefined;
|
|
58
|
+
if (
|
|
59
|
+
typeof collapseSystemMessages === "function" &&
|
|
60
|
+
typeof getCurrentSystemPrompt === "function" &&
|
|
61
|
+
typeof getCurrentTools === "function"
|
|
62
|
+
) {
|
|
63
|
+
const collapsed = collapseSystemMessages({ messages });
|
|
64
|
+
return {
|
|
65
|
+
messages: collapsed.messages.filter((message) => (message as { role?: string }).role !== "system"),
|
|
66
|
+
systemPrompt: getCurrentSystemPrompt(collapsed.messages) || undefined,
|
|
67
|
+
tools: getCurrentTools(collapsed.messages),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// No upstream helpers available: best-effort replay of the leading system message.
|
|
72
|
+
const systemMessages = messages.filter((message) => (message as { role?: string }).role === "system");
|
|
73
|
+
const conversation = messages.filter((message) => (message as { role?: string }).role !== "system");
|
|
74
|
+
const head = systemMessages[0] as (Message & { content?: unknown; toolsAdded?: Tool[] }) | undefined;
|
|
75
|
+
if (!head) return { messages: conversation, systemPrompt: undefined, tools: undefined };
|
|
76
|
+
return {
|
|
77
|
+
messages: conversation,
|
|
78
|
+
systemPrompt: textOfContent(head.content) || undefined,
|
|
79
|
+
tools: head.toolsAdded,
|
|
80
|
+
};
|
|
81
|
+
}
|