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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codebuddy-sdk",
3
- "version": "0.4.6",
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.0"
37
+ "typebox": "^1.3.34"
38
38
  },
39
39
  "peerDependencies": {
40
- "@earendil-works/pi-ai": "^0.85.1",
41
- "@earendil-works/pi-coding-agent": "^0.85.1",
42
- "@earendil-works/pi-tui": "^0.85.1"
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.85.1",
46
- "@earendil-works/pi-coding-agent": "^0.85.1",
47
- "@earendil-works/pi-tui": "^0.85.1",
48
- "@types/node": "^26.6.1",
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 promptText = extractIsolatedSummaryPrompt(context.messages);
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: context.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(context: Context, excludeToolName?: string): {
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 (!context.tools) return { mcpTools, customToolNameToSdk, customToolNameToPi };
786
+ if (!declaredTools) return { mcpTools, customToolNameToSdk, customToolNameToPi };
785
787
 
786
- for (const tool of context.tools) {
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 = context.messages[context.messages.length - 1]?.role;
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=${context.messages.length}`);
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=${context.messages.length}`);
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(context.messages);
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 = context.messages.length;
1256
- resultCtx.latestCursor = Math.max(resultCtx.latestCursor, context.messages.length);
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 = context.messages[context.messages.length - 1];
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 = context.messages.length;
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(context, askCodebuddyToolName);
1300
+ const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(declaredTools, askCodebuddyToolName);
1295
1301
  const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
1296
- const syncResult = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id, Boolean(isReentrant));
1302
+ const syncResult = syncSharedSession(convoMessages, cwd, customToolNameToSdk, model.id, Boolean(isReentrant));
1297
1303
  const { sessionId: resumeSessionId } = syncResult;
1298
- const promptBlocks = extractUserPromptBlocks(context.messages);
1299
- let promptText = extractUserPrompt(context.messages) ?? "";
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: context.messages.length,
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: context.messages.map((m, i) => `[${i}]${m.role}`).join(" "),
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(context.systemPrompt)
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=${context.messages.length} tools=${mcpTools.length}`,
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(context.messages.length, queryCtx.latestCursor, sharedSession?.cursor ?? 0);
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
+ }