pi-codebuddy-sdk 0.4.5 → 0.4.7
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 +6 -6
- package/src/index.ts +143 -35
- 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.7",
|
|
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",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@tencent-ai/agent-sdk": "^0.3.
|
|
35
|
+
"@tencent-ai/agent-sdk": "^0.3.259",
|
|
36
36
|
"change-case": "^5.4.4",
|
|
37
|
-
"typebox": "^1.3.
|
|
37
|
+
"typebox": "^1.3.33"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.85.1",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"@earendil-works/pi-ai": "^0.85.1",
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
47
47
|
"@earendil-works/pi-tui": "^0.85.1",
|
|
48
|
-
"@types/node": "^
|
|
49
|
-
"tsx": "^4.
|
|
50
|
-
"typescript": "^
|
|
48
|
+
"@types/node": "^26.6.1",
|
|
49
|
+
"tsx": "^4.23.13",
|
|
50
|
+
"typescript": "^7.0.2"
|
|
51
51
|
},
|
|
52
52
|
"pi": {
|
|
53
53
|
"extensions": [
|
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";
|
|
@@ -100,6 +101,76 @@ function diagDump(label: string, data: Record<string, unknown>) {
|
|
|
100
101
|
debug(`DIAG: ${label} (see ${redactForLog(DIAG_LOG_PATH)})`);
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// --- SDK transport teardown guard (issue #10) ---
|
|
105
|
+
//
|
|
106
|
+
// @tencent-ai/agent-sdk dispatches late `control_request`/`mcp_message` frames
|
|
107
|
+
// fire-and-forget: `handleLine()` calls `this.handleMcpMessageRequest(req)`
|
|
108
|
+
// without awaiting or catching it. When the CLI subprocess has just been torn
|
|
109
|
+
// down (interrupt → retry), the response path throws
|
|
110
|
+
// `Error: Transport not started` from `writeLine()` inside an *unowned* async
|
|
111
|
+
// function → unhandledRejection → the host escalates it to `fatal` and exits.
|
|
112
|
+
//
|
|
113
|
+
// Upstream still ships the unguarded call as of 0.3.259, so we patch the
|
|
114
|
+
// transport prototype once per process: errors from a half-dead transport are
|
|
115
|
+
// undeliverable anyway and must be swallowed, not crash the session.
|
|
116
|
+
const patchedTransportPrototypes = new WeakSet<object>();
|
|
117
|
+
|
|
118
|
+
function noteTransportTeardown(where: string, err: unknown): void {
|
|
119
|
+
debug(`transport guard: swallowed ${where} failure during teardown: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
diagDump("transport_teardown_swallowed", {
|
|
121
|
+
where,
|
|
122
|
+
message: err instanceof Error ? err.message : String(err),
|
|
123
|
+
stack: err instanceof Error ? err.stack?.split("\n").slice(0, 6).join(" | ") ?? null : null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function installTransportTeardownGuard(sdkQuery: unknown): void {
|
|
128
|
+
const transport = (sdkQuery as { transport?: unknown } | null | undefined)?.transport;
|
|
129
|
+
if (!transport || typeof transport !== "object") return;
|
|
130
|
+
const proto = Object.getPrototypeOf(transport) as Record<string, unknown> | null;
|
|
131
|
+
if (!proto || patchedTransportPrototypes.has(proto)) return;
|
|
132
|
+
|
|
133
|
+
const originalHandle = proto.handleMcpMessageRequest;
|
|
134
|
+
if (typeof originalHandle === "function") {
|
|
135
|
+
proto.handleMcpMessageRequest = function (this: unknown, request: unknown) {
|
|
136
|
+
try {
|
|
137
|
+
return Promise.resolve(originalHandle.call(this, request)).catch((err: unknown) => {
|
|
138
|
+
noteTransportTeardown("handleMcpMessageRequest", err);
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
// Synchronous throw before a promise exists — same teardown class.
|
|
142
|
+
noteTransportTeardown("handleMcpMessageRequest(sync)", err);
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const originalSendError = proto.sendControlErrorResponse;
|
|
149
|
+
if (typeof originalSendError === "function") {
|
|
150
|
+
proto.sendControlErrorResponse = function (this: unknown, requestId: unknown, error: unknown) {
|
|
151
|
+
try {
|
|
152
|
+
return (originalSendError as (...a: unknown[]) => unknown).call(this, requestId, error);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
noteTransportTeardown("sendControlErrorResponse", err);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
patchedTransportPrototypes.add(proto);
|
|
160
|
+
debug("transport guard: installed on SDK ProcessTransport prototype");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Create an SDK query and immediately arm the transport teardown guard. */
|
|
164
|
+
function startQuery(args: Parameters<typeof query>[0]): ReturnType<typeof query> {
|
|
165
|
+
const q = query(args);
|
|
166
|
+
try {
|
|
167
|
+
installTransportTeardownGuard(q);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
debug(`transport guard: install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
+
}
|
|
171
|
+
return q;
|
|
172
|
+
}
|
|
173
|
+
|
|
103
174
|
// --- Constants ---
|
|
104
175
|
|
|
105
176
|
// Global key to prevent re-registration of the provider across module reloads.
|
|
@@ -343,13 +414,14 @@ async function runIsolatedSummary(
|
|
|
343
414
|
};
|
|
344
415
|
|
|
345
416
|
try {
|
|
346
|
-
const
|
|
417
|
+
const view = readTranscript(context);
|
|
418
|
+
const promptText = extractIsolatedSummaryPrompt(view.messages);
|
|
347
419
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
348
420
|
const codebuddyExecutable = loadConfig(cwd).provider?.pathToCodebuddyCode;
|
|
349
421
|
const cliModel = codebuddyModelId(model);
|
|
350
422
|
debug(`compact summary: spawn model=${cliModel} registeredModel=${model.id} promptLen=${promptText.length}`);
|
|
351
423
|
|
|
352
|
-
sdkQuery =
|
|
424
|
+
sdkQuery = startQuery({
|
|
353
425
|
prompt: promptText,
|
|
354
426
|
options: {
|
|
355
427
|
cwd,
|
|
@@ -358,7 +430,7 @@ async function runIsolatedSummary(
|
|
|
358
430
|
strictMcpConfig: true,
|
|
359
431
|
settingSources: [] as SettingSource[],
|
|
360
432
|
persistSession: false,
|
|
361
|
-
systemPrompt:
|
|
433
|
+
systemPrompt: view.systemPrompt,
|
|
362
434
|
model: cliModel,
|
|
363
435
|
maxTurns: 1,
|
|
364
436
|
...(codebuddyExecutable ? { pathToCodebuddyCode: codebuddyExecutable } : {}),
|
|
@@ -524,6 +596,7 @@ function syncSharedSession(
|
|
|
524
596
|
cwd: string,
|
|
525
597
|
customToolNameToSdk?: Map<string, string>,
|
|
526
598
|
modelId?: string,
|
|
599
|
+
isReentrant?: boolean,
|
|
527
600
|
): SyncResult {
|
|
528
601
|
const priorMessages = messages.slice(0, -1); // everything before the new user prompt
|
|
529
602
|
|
|
@@ -548,13 +621,37 @@ function syncSharedSession(
|
|
|
548
621
|
}
|
|
549
622
|
}
|
|
550
623
|
// Only reachable when needsRebuild is false — user-facing history rewrites
|
|
551
|
-
// (/compact, session_tree, /new, fork)
|
|
552
|
-
// sharedSession before the next syncSharedSession call.
|
|
553
|
-
//
|
|
624
|
+
// (/compact, session_tree, /new, fork) usually set needsRebuild or clear
|
|
625
|
+
// sharedSession before the next syncSharedSession call. The remaining case is
|
|
626
|
+
// a pi-side history rewrite that did NOT flag us: a branch summary after an
|
|
627
|
+
// interrupted turn (a retry whose abort never reached the provider), which
|
|
628
|
+
// makes pi's visible history shorter than our cursor.
|
|
554
629
|
if (sharedSession && !sharedSession.needsRebuild && priorMessages.length < sharedSession.cursor) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
630
|
+
// Top-level call with real history left: falling through to a clean start
|
|
631
|
+
// would send only the final user message with no `resume`, so the model
|
|
632
|
+
// answers as if the conversation never happened (issue #10). Rewrite the
|
|
633
|
+
// shared session from the truncated history instead — the turn keeps
|
|
634
|
+
// whatever context pi still has. forceRotate avoids racing a CLI
|
|
635
|
+
// subprocess that may still be writing to the old session file.
|
|
636
|
+
if (priorMessages.length > 0 && !isReentrant) {
|
|
637
|
+
const dropped = sharedSession.cursor - priorMessages.length;
|
|
638
|
+
const sessionId = sharedSession.sessionId;
|
|
639
|
+
debug(`Case 1 synthetic: history shrank by ${dropped} (${priorMessages.length} < cursor ${sharedSession.cursor}) — rebuilding shared session ${sessionId.slice(0, 8)} instead of clean start`);
|
|
640
|
+
piUI?.notify(
|
|
641
|
+
`CodeBuddy SDK: conversation history shrank by ${dropped} message(s) — rebuilding session context instead of starting fresh (see ${ISSUES_URL}#issue-comments if the reply still lacks context)`,
|
|
642
|
+
"warning",
|
|
643
|
+
);
|
|
644
|
+
diagDump("history_shrank_rebuild", { dropped, priorMessages: priorMessages.length, cursor: sharedSession.cursor, sessionId });
|
|
645
|
+
sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
646
|
+
// fall through to REBUILD below
|
|
647
|
+
} else {
|
|
648
|
+
// No prior history (isolated compact-summary prompt) or a reentrant
|
|
649
|
+
// subagent call: nothing to rebuild from, or rewriting would clobber
|
|
650
|
+
// the parent's shared session. Keep the ephemeral clean start.
|
|
651
|
+
debug(`Case 1 synthetic: clean start for shorter context (prior=${priorMessages.length}, reentrant=${Boolean(isReentrant)}), preserving shared session ${sharedSession.sessionId.slice(0, 8)}, cursor=${sharedSession.cursor}`);
|
|
652
|
+
debug(`syncResult: path=clean-start preserve-shared sessionId=${sharedSession.sessionId} cursor=${sharedSession.cursor}`);
|
|
653
|
+
return { sessionId: null, preserveSharedSession: true };
|
|
654
|
+
}
|
|
558
655
|
}
|
|
559
656
|
|
|
560
657
|
// REBUILD path
|
|
@@ -677,7 +774,7 @@ function contextForToolResults(results: McpResult[]): QueryContext | undefined {
|
|
|
677
774
|
return undefined;
|
|
678
775
|
}
|
|
679
776
|
|
|
680
|
-
function resolveMcpTools(
|
|
777
|
+
function resolveMcpTools(declaredTools: Tool[] | undefined, excludeToolName?: string): {
|
|
681
778
|
mcpTools: Tool[];
|
|
682
779
|
customToolNameToSdk: Map<string, string>;
|
|
683
780
|
customToolNameToPi: Map<string, string>;
|
|
@@ -686,9 +783,9 @@ function resolveMcpTools(context: Context, excludeToolName?: string): {
|
|
|
686
783
|
const customToolNameToSdk = new Map<string, string>();
|
|
687
784
|
const customToolNameToPi = new Map<string, string>();
|
|
688
785
|
|
|
689
|
-
if (!
|
|
786
|
+
if (!declaredTools) return { mcpTools, customToolNameToSdk, customToolNameToPi };
|
|
690
787
|
|
|
691
|
-
for (const tool of
|
|
788
|
+
for (const tool of declaredTools) {
|
|
692
789
|
if (tool.name === excludeToolName) continue;
|
|
693
790
|
const sdkName = `${MCP_TOOL_PREFIX}${tool.name}`;
|
|
694
791
|
mcpTools.push(tool);
|
|
@@ -1092,8 +1189,12 @@ async function consumeQuery(
|
|
|
1092
1189
|
function streamCodebuddySdk(model: Model<any>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
1093
1190
|
const stream = newAssistantMessageEventStream();
|
|
1094
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
|
+
|
|
1095
1196
|
// DEBUG: trace followUp message triggering
|
|
1096
|
-
const lastMsgRole =
|
|
1197
|
+
const lastMsgRole = convoMessages[convoMessages.length - 1]?.role;
|
|
1097
1198
|
debug(`provider: streamCodebuddySdk called, activeQuery=${!!ctx().activeQuery}, lastMsgRole=${lastMsgRole}, isReentrant=${ctx().activeQuery !== null}`);
|
|
1098
1199
|
|
|
1099
1200
|
const activeQuery = ctx().activeQuery !== null;
|
|
@@ -1101,7 +1202,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1101
1202
|
const resultCtx = allResults.length > 0 ? contextForToolResults(allResults) : undefined;
|
|
1102
1203
|
const isReentrantUserQuery = activeQuery && lastMsgRole === "user" && allResults.length === 0;
|
|
1103
1204
|
if (isReentrantUserQuery) {
|
|
1104
|
-
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}`);
|
|
1105
1206
|
}
|
|
1106
1207
|
|
|
1107
1208
|
// --- Tool result delivery ---
|
|
@@ -1111,7 +1212,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1111
1212
|
if (resultCtx) {
|
|
1112
1213
|
claimCurrentPiStream(stream, "tool-result", resultCtx);
|
|
1113
1214
|
resultCtx.resetTurnState(model);
|
|
1114
|
-
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}`);
|
|
1115
1216
|
for (const result of allResults) {
|
|
1116
1217
|
const id = result.toolCallId;
|
|
1117
1218
|
if (id && resultCtx.pendingToolCalls.has(id)) {
|
|
@@ -1130,8 +1231,15 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1130
1231
|
}
|
|
1131
1232
|
}
|
|
1132
1233
|
if (resultCtx.pendingToolCalls.size > 0) {
|
|
1133
|
-
|
|
1134
|
-
|
|
1234
|
+
debug(`WARNING: ${resultCtx.pendingToolCalls.size} MCP handlers still waiting after delivering ${allResults.length} results`);
|
|
1235
|
+
// The wedged turn cannot be unwedged in place: retrying tears down the
|
|
1236
|
+
// CLI subprocess while its control pipe is still alive (issue #10). Exit
|
|
1237
|
+
// and resume the session instead — the local transcript is intact.
|
|
1238
|
+
piUI?.notify(
|
|
1239
|
+
`CodeBuddy SDK: ${resultCtx.pendingToolCalls.size} tool handler(s) still waiting — provider may be stuck. ` +
|
|
1240
|
+
`Prefer exiting and resuming this session over retrying in place, or the reply may lose context.`,
|
|
1241
|
+
"warning",
|
|
1242
|
+
);
|
|
1135
1243
|
}
|
|
1136
1244
|
|
|
1137
1245
|
// Detect user messages (steer/followUp) that pi injected into context
|
|
@@ -1143,25 +1251,25 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1143
1251
|
// The bridge can't forward these mid-query (the SDK query is in progress),
|
|
1144
1252
|
// so we save them for replay as continuation queries after consumeQuery ends.
|
|
1145
1253
|
if (lastMsgRole === "user") {
|
|
1146
|
-
const userPrompt = extractUserPrompt(
|
|
1254
|
+
const userPrompt = extractUserPrompt(convoMessages);
|
|
1147
1255
|
if (userPrompt) {
|
|
1148
1256
|
resultCtx.deferredUserMessages.push(userPrompt);
|
|
1149
1257
|
debug(`provider: deferred user message for replay after query (len=${userPrompt.length})`);
|
|
1150
1258
|
}
|
|
1151
1259
|
}
|
|
1152
1260
|
|
|
1153
|
-
if (sharedSession) sharedSession.cursor =
|
|
1154
|
-
resultCtx.latestCursor = Math.max(resultCtx.latestCursor,
|
|
1261
|
+
if (sharedSession) sharedSession.cursor = convoMessages.length;
|
|
1262
|
+
resultCtx.latestCursor = Math.max(resultCtx.latestCursor, convoMessages.length);
|
|
1155
1263
|
return stream;
|
|
1156
1264
|
}
|
|
1157
1265
|
|
|
1158
1266
|
// --- Orphaned tool result (e.g. user aborted a tool call) ---
|
|
1159
1267
|
// The query is gone but pi still delivered the result. Nothing to do — just
|
|
1160
1268
|
// emit end_turn so pi waits for the next real user message.
|
|
1161
|
-
const lastMsg =
|
|
1269
|
+
const lastMsg = convoMessages[convoMessages.length - 1];
|
|
1162
1270
|
if (lastMsg?.role === "toolResult") {
|
|
1163
1271
|
debug(`provider: orphaned tool result after abort, emitting end_turn`);
|
|
1164
|
-
if (sharedSession) sharedSession.cursor =
|
|
1272
|
+
if (sharedSession) sharedSession.cursor = convoMessages.length;
|
|
1165
1273
|
const c = ctx(); // capture current context for the microtask
|
|
1166
1274
|
queueMicrotask(() => {
|
|
1167
1275
|
c.resetTurnState(model);
|
|
@@ -1189,24 +1297,24 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1189
1297
|
queryCtx.resetTurnState(model);
|
|
1190
1298
|
queryCtx.latestCursor = 0;
|
|
1191
1299
|
|
|
1192
|
-
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(
|
|
1300
|
+
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(declaredTools, askCodebuddyToolName);
|
|
1193
1301
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
1194
|
-
const syncResult = syncSharedSession(
|
|
1302
|
+
const syncResult = syncSharedSession(convoMessages, cwd, customToolNameToSdk, model.id, Boolean(isReentrant));
|
|
1195
1303
|
const { sessionId: resumeSessionId } = syncResult;
|
|
1196
|
-
const promptBlocks = extractUserPromptBlocks(
|
|
1197
|
-
let promptText = extractUserPrompt(
|
|
1304
|
+
const promptBlocks = extractUserPromptBlocks(convoMessages);
|
|
1305
|
+
let promptText = extractUserPrompt(convoMessages) ?? "";
|
|
1198
1306
|
|
|
1199
1307
|
// Guard: empty prompt means the last context message isn't a user message.
|
|
1200
1308
|
// This should never happen with per-query state — dump diagnostics if it does.
|
|
1201
1309
|
if (!promptText && !promptBlocks) {
|
|
1202
1310
|
diagDump("empty_prompt", {
|
|
1203
|
-
contextLength:
|
|
1311
|
+
contextLength: convoMessages.length,
|
|
1204
1312
|
lastMsgRole: lastMsg?.role,
|
|
1205
1313
|
isReentrant,
|
|
1206
1314
|
activeQueryContexts: activeQueryContexts.size,
|
|
1207
1315
|
activeQueryExists: queryCtx.activeQuery !== null,
|
|
1208
1316
|
sharedSession: sharedSession ? { sessionId: sharedSession.sessionId.slice(0, 8), cursor: sharedSession.cursor } : null,
|
|
1209
|
-
messageRoles:
|
|
1317
|
+
messageRoles: convoMessages.map((m, i) => `[${i}]${m.role}`).join(" "),
|
|
1210
1318
|
});
|
|
1211
1319
|
// Recover: use a continuation prompt so the SDK doesn't send an empty text block
|
|
1212
1320
|
promptText = "[continue]";
|
|
@@ -1218,7 +1326,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1218
1326
|
const mcpServers = buildMcpServers(mcpTools, queryCtx);
|
|
1219
1327
|
const appendSystemPrompt = providerSettings.appendSystemPrompt !== false;
|
|
1220
1328
|
const systemPrompt = appendSystemPrompt
|
|
1221
|
-
? buildCodebuddySystemPrompt(
|
|
1329
|
+
? buildCodebuddySystemPrompt(baseSystemPrompt)
|
|
1222
1330
|
: undefined;
|
|
1223
1331
|
|
|
1224
1332
|
// MCP auto-loading suppression: CC reads MCP servers from ~/.claude.json (top-level
|
|
@@ -1264,7 +1372,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1264
1372
|
};
|
|
1265
1373
|
|
|
1266
1374
|
debug("provider: fresh query",
|
|
1267
|
-
`model=${cliModel} msgs=${
|
|
1375
|
+
`model=${cliModel} msgs=${convoMessages.length} tools=${mcpTools.length}`,
|
|
1268
1376
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1269
1377
|
`appendSys=${appendSystemPrompt} strictMcp=${strictMcpConfigEnabled}`,
|
|
1270
1378
|
`promptLen=${promptText.length}${promptBlocks ? " [+images]" : ""}`);
|
|
@@ -1292,7 +1400,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1292
1400
|
|
|
1293
1401
|
void (async () => {
|
|
1294
1402
|
await ensureModelsDiscovered();
|
|
1295
|
-
sdkQuery =
|
|
1403
|
+
sdkQuery = startQuery({ prompt, options: queryOptions });
|
|
1296
1404
|
queryCtx.activeQuery = sdkQuery;
|
|
1297
1405
|
activeQueryContexts.add(queryCtx);
|
|
1298
1406
|
|
|
@@ -1324,7 +1432,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1324
1432
|
}
|
|
1325
1433
|
debug(`provider: query done, ignoring captured session ${capturedSessionId?.slice(0, 8) ?? "none"} to preserve shared session`);
|
|
1326
1434
|
} else if (sessionId) {
|
|
1327
|
-
const cursor = Math.max(
|
|
1435
|
+
const cursor = Math.max(convoMessages.length, queryCtx.latestCursor, sharedSession?.cursor ?? 0);
|
|
1328
1436
|
debug(`provider: query done, session=${sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
1329
1437
|
sharedSession = { sessionId, cursor, cwd };
|
|
1330
1438
|
}
|
|
@@ -1341,7 +1449,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1341
1449
|
}
|
|
1342
1450
|
|
|
1343
1451
|
const contOptions = { ...queryOptions, resume: resumeId, ...makeCliDebugOptions("continuation") };
|
|
1344
|
-
const contQuery =
|
|
1452
|
+
const contQuery = startQuery({ prompt: steerPrompt, options: contOptions });
|
|
1345
1453
|
queryCtx.activeQuery = contQuery;
|
|
1346
1454
|
debug(`provider: continuation query, model=${cliModel}, resume=${resumeId.slice(0, 8)}, promptLen=${steerPrompt.length}`);
|
|
1347
1455
|
|
|
@@ -1468,7 +1576,7 @@ async function promptAndWait(
|
|
|
1468
1576
|
`isolated=${options?.isolated ?? false} resume=${resumeSessionId?.slice(0, 8) ?? "none"}`,
|
|
1469
1577
|
`sysPrompt=${Boolean(askSystemPrompt)} promptLen=${prompt.length}`);
|
|
1470
1578
|
|
|
1471
|
-
const sdkQuery =
|
|
1579
|
+
const sdkQuery = startQuery({
|
|
1472
1580
|
prompt,
|
|
1473
1581
|
options: {
|
|
1474
1582
|
cwd,
|
|
@@ -1580,7 +1688,7 @@ let discoverInFlight: Promise<void> | null = null;
|
|
|
1580
1688
|
async function discoverModels(pi: ExtensionAPI): Promise<void> {
|
|
1581
1689
|
await withSdkGate(async () => {
|
|
1582
1690
|
try {
|
|
1583
|
-
const q =
|
|
1691
|
+
const q = startQuery({ prompt: " ", options: { maxTurns: 0, permissionMode: "bypassPermissions", tools: [] } });
|
|
1584
1692
|
const supported = await q.supportedModels();
|
|
1585
1693
|
await q.return().catch(() => {});
|
|
1586
1694
|
if (!supported.length) return;
|
|
@@ -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
|
+
}
|