pi-codebuddy-sdk 0.1.11 → 0.1.13
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 +1 -1
- package/src/provider.ts +101 -56
- package/src/context.ts +0 -238
- package/src/session-pool.ts +0 -99
package/package.json
CHANGED
package/src/provider.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeBuddy Provider — stateless, lightweight.
|
|
3
|
+
*
|
|
4
|
+
* Uses sdk.query() (one-shot) — no context building, no session pool,
|
|
5
|
+
* no tool boundary. Pi manages conversation, CodeBuddy is just a model backend.
|
|
6
|
+
*
|
|
7
|
+
* Architecture follows pi-openai-compatible / pi-anthropic pattern:
|
|
8
|
+
* extract latest user message → send to SDK → stream events back to Pi.
|
|
9
|
+
*/
|
|
1
10
|
import {
|
|
2
11
|
createAssistantMessageEventStream,
|
|
3
12
|
type Api,
|
|
@@ -16,8 +25,6 @@ import type {
|
|
|
16
25
|
Effort,
|
|
17
26
|
RawMessageStreamEvent,
|
|
18
27
|
} from "@tencent-ai/agent-sdk";
|
|
19
|
-
import { buildFullMessage, buildDeltaMessage } from "./context.js";
|
|
20
|
-
import { acquireSession, evictSession } from "./session-pool.js";
|
|
21
28
|
|
|
22
29
|
type CodebuddySdk = typeof import("@tencent-ai/agent-sdk");
|
|
23
30
|
|
|
@@ -70,13 +77,83 @@ function extractUsageUpdate(msg: CbMessage): Partial<AssistantMessage> | null {
|
|
|
70
77
|
return null;
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
// ── extract latest user message ──
|
|
81
|
+
|
|
82
|
+
function extractLatestUserText(context: Context): string {
|
|
83
|
+
const messages = context.messages;
|
|
84
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
85
|
+
if (messages[i].role === "user") {
|
|
86
|
+
let raw = extractRawContent((messages[i] as any).content);
|
|
87
|
+
if (!raw) continue;
|
|
88
|
+
// Strip context-mode session state injection prepended to user messages.
|
|
89
|
+
// Pi's context-mode adds its hierarchy/state before the actual user input.
|
|
90
|
+
raw = stripContextModeInjection(raw);
|
|
91
|
+
if (raw) return raw;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return "Continue.";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractRawContent(content: unknown): string {
|
|
98
|
+
if (typeof content === "string") return content;
|
|
99
|
+
if (!Array.isArray(content)) return "";
|
|
100
|
+
return content
|
|
101
|
+
.filter((c: any) => c?.type === "text")
|
|
102
|
+
.map((c: any) => c.text)
|
|
103
|
+
.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Pi context-mode prepends its session state to user messages.
|
|
108
|
+
* Strip it to keep CodeBuddy from treating it as conversation content.
|
|
109
|
+
*
|
|
110
|
+
* Injected format:
|
|
111
|
+
* context-mode active. Hierarchy: ...
|
|
112
|
+
* <session_state source="compaction">
|
|
113
|
+
* <session_mode>investigate</session_mode>
|
|
114
|
+
* </session_state>
|
|
115
|
+
* [actual user message]
|
|
116
|
+
*/
|
|
117
|
+
function stripContextModeInjection(text: string): string {
|
|
118
|
+
// Find the end of context-mode injection
|
|
119
|
+
// Pattern: </session_state> followed by newlines then actual message
|
|
120
|
+
const sessionEnd = text.lastIndexOf("</session_state>");
|
|
121
|
+
if (sessionEnd >= 0) {
|
|
122
|
+
const after = text.slice(sessionEnd + "</session_state>".length).trimStart();
|
|
123
|
+
if (after) return after;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fallback: look for "context-mode active" header and take everything after it + session_state
|
|
127
|
+
const cmStart = text.indexOf("context-mode active.");
|
|
128
|
+
if (cmStart >= 0) {
|
|
129
|
+
// Find first blank line after </session_state> or end of XML
|
|
130
|
+
const afterSession = afterSessionState(text);
|
|
131
|
+
if (afterSession) return afterSession;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function afterSessionState(text: string): string | null {
|
|
138
|
+
// Try to find the actual user message after context-mode XML injection
|
|
139
|
+
const patterns = [
|
|
140
|
+
/context-mode active\.[\s\S]*?<\/session_state>\s*\n+([\s\S]+)$/,
|
|
141
|
+
/<session_state[\s\S]*?<\/session_state>\s*\n+([\s\S]+)$/,
|
|
142
|
+
];
|
|
143
|
+
for (const re of patterns) {
|
|
144
|
+
const m = text.match(re);
|
|
145
|
+
if (m?.[1]?.trim()) return m[1].trim();
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
73
150
|
// ── thinking config mapping ──
|
|
74
151
|
|
|
75
152
|
const DEFAULT_THINKING_BUDGETS: Record<string, number> = {
|
|
76
153
|
minimal: 1600,
|
|
77
154
|
low: 4000,
|
|
78
|
-
medium: 0,
|
|
79
|
-
high: 0,
|
|
155
|
+
medium: 0,
|
|
156
|
+
high: 0,
|
|
80
157
|
xhigh: 32000,
|
|
81
158
|
};
|
|
82
159
|
|
|
@@ -114,53 +191,35 @@ export function streamCodebuddy(
|
|
|
114
191
|
const partial = makePartial(model);
|
|
115
192
|
|
|
116
193
|
queueMicrotask(async () => {
|
|
117
|
-
let session: Awaited<ReturnType<CodebuddySdk["unstable_v2_createSession"]>> | undefined;
|
|
118
|
-
let isFirst = false;
|
|
119
|
-
|
|
120
194
|
try {
|
|
121
195
|
stream.push({ type: "start", partial });
|
|
122
196
|
|
|
123
197
|
const sdk = await loadCodebuddySdk();
|
|
198
|
+
const userText = extractLatestUserText(context);
|
|
124
199
|
|
|
125
200
|
const { thinking, effort } = buildThinkingConfig(
|
|
126
201
|
options?.reasoning,
|
|
127
202
|
options?.thinkingBudgets,
|
|
128
203
|
);
|
|
129
204
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const cwd = process.cwd();
|
|
135
|
-
|
|
136
|
-
const { session: pooled, message: userMessage, isFirst: first } =
|
|
137
|
-
await acquireSession(cwd, sdk, context, buildFullMessage, buildDeltaMessage, {
|
|
205
|
+
const q = sdk.query({
|
|
206
|
+
prompt: userText,
|
|
207
|
+
options: {
|
|
138
208
|
model: model.id,
|
|
139
209
|
permissionMode: "bypassPermissions",
|
|
140
210
|
maxTurns: 100,
|
|
141
211
|
thinking,
|
|
142
212
|
effort,
|
|
143
|
-
includePartialMessages:
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
session = pooled;
|
|
147
|
-
isFirst = first;
|
|
148
|
-
|
|
149
|
-
let msgCount = 0; // track content blocks for empty-response detection
|
|
150
|
-
let hasStreamEvents = false; // true once we see first stream_event
|
|
151
|
-
|
|
152
|
-
// Per-index tracking for stream_event delta accumulation
|
|
153
|
-
type BlockKind = 'text' | 'thinking' | 'tool_use' | 'redacted_thinking';
|
|
154
|
-
const blockKind = new Map<number, BlockKind>();
|
|
155
|
-
const blockAccum = new Map<number, string>();
|
|
156
|
-
const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
|
|
213
|
+
includePartialMessages: true,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
157
216
|
|
|
158
217
|
// ── abort handling ──
|
|
159
218
|
let aborted = false;
|
|
160
219
|
const onAbort = async () => {
|
|
161
220
|
if (aborted) return;
|
|
162
221
|
aborted = true;
|
|
163
|
-
try { await
|
|
222
|
+
try { await q.interrupt(); } catch { /* best-effort */ }
|
|
164
223
|
partial.stopReason = "aborted";
|
|
165
224
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
166
225
|
};
|
|
@@ -172,10 +231,14 @@ export function streamCodebuddy(
|
|
|
172
231
|
}
|
|
173
232
|
}
|
|
174
233
|
|
|
175
|
-
// ──
|
|
176
|
-
|
|
234
|
+
// ── per-index block tracking ──
|
|
235
|
+
type BlockKind = 'text' | 'thinking' | 'tool_use' | 'redacted_thinking';
|
|
236
|
+
const blockKind = new Map<number, BlockKind>();
|
|
237
|
+
const blockAccum = new Map<number, string>();
|
|
238
|
+
const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
|
|
239
|
+
|
|
240
|
+
let hasStreamEvents = false;
|
|
177
241
|
|
|
178
|
-
// ── helpers for stream_event block tracking ──
|
|
179
242
|
function onBlockStart(idx: number, kind: BlockKind, initText: string, meta?: { id: string; name: string; signature?: string }) {
|
|
180
243
|
blockKind.set(idx, kind);
|
|
181
244
|
blockAccum.set(idx, initText);
|
|
@@ -186,22 +249,18 @@ export function streamCodebuddy(
|
|
|
186
249
|
partial.content = [...partial.content, tc];
|
|
187
250
|
stream.push({ type: 'thinking_start', contentIndex: idx, partial });
|
|
188
251
|
stream.push({ type: 'thinking_end', contentIndex: idx, content: '[redacted]', partial });
|
|
189
|
-
msgCount++;
|
|
190
252
|
} else if (kind === 'thinking') {
|
|
191
253
|
const tc: ThinkingContent = { type: 'thinking', thinking: initText, thinkingSignature: meta?.signature };
|
|
192
254
|
partial.content = [...partial.content, tc];
|
|
193
255
|
stream.push({ type: 'thinking_start', contentIndex: idx, partial });
|
|
194
|
-
msgCount++;
|
|
195
256
|
} else if (kind === 'text') {
|
|
196
257
|
const tb: TextContent = { type: 'text', text: initText };
|
|
197
258
|
partial.content = [...partial.content, tb];
|
|
198
259
|
stream.push({ type: 'text_start', contentIndex: idx, partial });
|
|
199
|
-
msgCount++;
|
|
200
260
|
} else if (kind === 'tool_use' && meta) {
|
|
201
261
|
const toolCall: ToolCall = { type: 'toolCall', id: meta.id, name: meta.name, arguments: {} };
|
|
202
262
|
partial.content = [...partial.content, toolCall];
|
|
203
263
|
stream.push({ type: 'toolcall_start', contentIndex: idx, partial });
|
|
204
|
-
msgCount++;
|
|
205
264
|
}
|
|
206
265
|
}
|
|
207
266
|
|
|
@@ -229,7 +288,7 @@ export function streamCodebuddy(
|
|
|
229
288
|
const blocks = [...partial.content];
|
|
230
289
|
try {
|
|
231
290
|
blocks[idx] = { ...blocks[idx], arguments: JSON.parse(newJson) };
|
|
232
|
-
} catch { /* partial JSON
|
|
291
|
+
} catch { /* partial JSON */ }
|
|
233
292
|
partial.content = blocks;
|
|
234
293
|
stream.push({ type: 'toolcall_delta', contentIndex: idx, delta: deltaValue, partial } as any);
|
|
235
294
|
}
|
|
@@ -248,8 +307,8 @@ export function streamCodebuddy(
|
|
|
248
307
|
}
|
|
249
308
|
}
|
|
250
309
|
|
|
251
|
-
// ── iterate
|
|
252
|
-
for await (const msg of
|
|
310
|
+
// ── iterate ──
|
|
311
|
+
for await (const msg of q) {
|
|
253
312
|
if (aborted) return;
|
|
254
313
|
|
|
255
314
|
if (msg.type === "system") {
|
|
@@ -257,7 +316,6 @@ export function streamCodebuddy(
|
|
|
257
316
|
continue;
|
|
258
317
|
}
|
|
259
318
|
|
|
260
|
-
// Primary path: token-level streaming via stream_event
|
|
261
319
|
if (msg.type === "stream_event") {
|
|
262
320
|
hasStreamEvents = true;
|
|
263
321
|
const evt = msg.event;
|
|
@@ -279,21 +337,19 @@ export function streamCodebuddy(
|
|
|
279
337
|
if (d.type === 'text_delta') onBlockDelta(evt.index, 'text_delta', d.text);
|
|
280
338
|
else if (d.type === 'thinking_delta') onBlockDelta(evt.index, 'thinking_delta', d.thinking);
|
|
281
339
|
else if (d.type === 'input_json_delta') onBlockDelta(evt.index, 'input_json_delta', d.partial_json);
|
|
282
|
-
// ponytail: signature_delta not surfaced to Pi — signature only sent at block start
|
|
283
340
|
} else if (evt.type === "content_block_stop") {
|
|
284
341
|
onBlockStop(evt.index);
|
|
285
342
|
}
|
|
286
343
|
continue;
|
|
287
344
|
}
|
|
288
345
|
|
|
289
|
-
// Fallback: full assistant messages
|
|
346
|
+
// Fallback: full assistant messages
|
|
290
347
|
if (msg.type === "assistant" && !hasStreamEvents) {
|
|
291
|
-
msgCount++;
|
|
292
348
|
let ci = 0;
|
|
293
349
|
for (const block of msg.message.content) {
|
|
294
350
|
const idx = ci++;
|
|
295
351
|
if (block.type === "thinking") {
|
|
296
|
-
onBlockStart(idx, 'thinking', block.thinking
|
|
352
|
+
onBlockStart(idx, 'thinking', block.thinking);
|
|
297
353
|
onBlockStop(idx);
|
|
298
354
|
} else if (block.type === "redacted_thinking") {
|
|
299
355
|
onBlockStart(idx, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
@@ -319,24 +375,13 @@ export function streamCodebuddy(
|
|
|
319
375
|
partial.stopReason = "error";
|
|
320
376
|
partial.errorMessage = msg.errors?.join("; ") ?? `CodeBuddy error: ${msg.subtype}`;
|
|
321
377
|
stream.push({ type: "error", reason: "error", error: { ...partial } });
|
|
322
|
-
evictSession(cwd);
|
|
323
378
|
}
|
|
324
379
|
break;
|
|
325
380
|
}
|
|
326
381
|
}
|
|
327
|
-
|
|
328
|
-
// Some models (Hunyuan) return empty on session reuse → evict for fresh session next turn
|
|
329
|
-
if (msgCount === 0) {
|
|
330
|
-
evictSession(cwd);
|
|
331
|
-
}
|
|
332
382
|
} catch (error) {
|
|
333
|
-
evictSession(process.cwd());
|
|
334
383
|
const errPartial = makeErrorPartial(model, error);
|
|
335
384
|
stream.push({ type: "error", reason: "error", error: errPartial });
|
|
336
|
-
} finally {
|
|
337
|
-
// ponytail: do NOT close pooled sessions — they are reused across turns.
|
|
338
|
-
// Only close when evicted (via evictSession).
|
|
339
|
-
// session?.close() intentionally omitted here.
|
|
340
385
|
}
|
|
341
386
|
});
|
|
342
387
|
|
package/src/context.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import type { Context, ImageContent, Message } from "@earendil-works/pi-ai/compat";
|
|
2
|
-
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { UserMessage as CbUserMessage, ContentBlock } from "@tencent-ai/agent-sdk";
|
|
4
|
-
|
|
5
|
-
// ── public API ──
|
|
6
|
-
|
|
7
|
-
export function buildFullMessage(context: Context): string | CbUserMessage {
|
|
8
|
-
return buildMessage(context, 0);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function buildDeltaMessage(context: Context, startIndex: number): string | CbUserMessage {
|
|
12
|
-
return buildMessage(context, startIndex);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// ── tool boundary (like pi-cursor-sdk's getCursorToolBoundaryText) ──
|
|
16
|
-
|
|
17
|
-
const CODEBUDDY_TOOL_BOUNDARY = [
|
|
18
|
-
"CodeBuddy SDK tool boundary:",
|
|
19
|
-
"Call only CodeBuddy SDK native tools (Read, Write, Bash, Grep, Edit, Glob, etc.).",
|
|
20
|
-
"Do not call any Pi-ecosystem tools — they are not registered in this session.",
|
|
21
|
-
"Tool names from conversation history refer to a prior provider; they are unavailable now.",
|
|
22
|
-
"CodeBuddy executes tools locally on the filesystem.",
|
|
23
|
-
].join("\n");
|
|
24
|
-
|
|
25
|
-
const CODEBUDDY_TOOL_TAIL = [
|
|
26
|
-
"Answer the latest user request above using CodeBuddy SDK tools and capabilities.",
|
|
27
|
-
"Shell: cd to repo path for project commands; session cwd may differ from tool args.",
|
|
28
|
-
"Exact-output requests: output exactly the requested text; no preamble or checks unless asked.",
|
|
29
|
-
"Tools: call available CodeBuddy SDK tools; never print tool cards or names as assistant text.",
|
|
30
|
-
].join("\n");
|
|
31
|
-
|
|
32
|
-
// ── system prompt sanitization (like pi-cursor-sdk's sanitizeSystemPromptForCursor) ──
|
|
33
|
-
|
|
34
|
-
function sanitizeSystemPrompt(prompt: string): string {
|
|
35
|
-
let s = prompt;
|
|
36
|
-
|
|
37
|
-
// Strip Pi tool catalog section
|
|
38
|
-
s = s.replace(
|
|
39
|
-
/Available tools:\n[\s\S]*?\n\nIn addition to the tools above[\s\S]*?\n\n/g,
|
|
40
|
-
"Pi tool catalog omitted: CodeBuddy can call only CodeBuddy SDK tools.\n\n",
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// Strip Guidelines section (Pi-specific guidelines)
|
|
44
|
-
s = s.replace(
|
|
45
|
-
/Guidelines:\n[\s\S]*?\n\nPi documentation /g,
|
|
46
|
-
"Guidelines:\n- Be concise in your responses.\n- Show file paths clearly when working with files.\n\nPi documentation ",
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// Strip semantic code intelligence priority section
|
|
50
|
-
s = s.replace(/\n+Semantic code intelligence priority:[\s\S]*$/g, "");
|
|
51
|
-
|
|
52
|
-
return s.trim();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── message formatting ──
|
|
56
|
-
|
|
57
|
-
function formatMessage(msg: Message): string | undefined {
|
|
58
|
-
switch (msg.role) {
|
|
59
|
-
case "user": {
|
|
60
|
-
const text = extractTextContent(msg.content);
|
|
61
|
-
const hasImages = extractImages(msg).length > 0;
|
|
62
|
-
if (text) {
|
|
63
|
-
return `User: ${text}${hasImages ? " [image omitted]" : ""}`;
|
|
64
|
-
}
|
|
65
|
-
if (hasImages) return "User: [image omitted]";
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
case "assistant": {
|
|
69
|
-
const blocks = Array.isArray(msg.content) ? msg.content : [];
|
|
70
|
-
const parts: string[] = [];
|
|
71
|
-
for (const block of blocks) {
|
|
72
|
-
if (typeof block === "object" && block !== null && "type" in block) {
|
|
73
|
-
if (block.type === "text") {
|
|
74
|
-
parts.push((block as { text: string }).text);
|
|
75
|
-
} else if (block.type === "toolCall") {
|
|
76
|
-
const tc = block as { name: string; arguments: Record<string, unknown> };
|
|
77
|
-
parts.push(`[Tool used: ${tc.name}(${JSON.stringify(tc.arguments)})]`);
|
|
78
|
-
}
|
|
79
|
-
// Skip thinking content blocks
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return parts.length > 0 ? `Assistant: ${parts.join("\n")}` : undefined;
|
|
83
|
-
}
|
|
84
|
-
case "toolResult": {
|
|
85
|
-
const text = extractTextContent(msg.content);
|
|
86
|
-
const label = msg.isError ? "Tool error" : "Tool result";
|
|
87
|
-
const snippet = truncateToolResult(text);
|
|
88
|
-
return `[${label} (${msg.toolName}): ${snippet}]`;
|
|
89
|
-
}
|
|
90
|
-
default:
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ── truncation ──
|
|
96
|
-
|
|
97
|
-
const TOOL_RESULT_HEAD = 1200;
|
|
98
|
-
const TOOL_RESULT_TAIL = 600;
|
|
99
|
-
|
|
100
|
-
function truncateToolResult(text: string): string {
|
|
101
|
-
if (!text) return "(empty)";
|
|
102
|
-
if (text.length <= TOOL_RESULT_HEAD + TOOL_RESULT_TAIL + 50) return text;
|
|
103
|
-
const head = text.slice(0, TOOL_RESULT_HEAD);
|
|
104
|
-
const tail = text.slice(-TOOL_RESULT_TAIL);
|
|
105
|
-
const skipped = text.length - TOOL_RESULT_HEAD - TOOL_RESULT_TAIL;
|
|
106
|
-
return `${head}\n...[${skipped.toLocaleString()} chars skipped]...\n${tail}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── main builder ──
|
|
110
|
-
|
|
111
|
-
function buildMessage(context: Context, startIndex: number): string | CbUserMessage {
|
|
112
|
-
const parts: string[] = [];
|
|
113
|
-
|
|
114
|
-
// 1. Tool boundary (always first)
|
|
115
|
-
parts.push(CODEBUDDY_TOOL_BOUNDARY);
|
|
116
|
-
|
|
117
|
-
// 2. Sanitized system prompt (first turn only — like pi-cursor-sdk)
|
|
118
|
-
if (startIndex === 0 && context.systemPrompt) {
|
|
119
|
-
const sanitized = sanitizeSystemPrompt(context.systemPrompt);
|
|
120
|
-
if (sanitized) {
|
|
121
|
-
parts.push(`System instructions from pi:\n${sanitized}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 3. Normalize and format messages (filter out non-LLM roles via convertToLlm)
|
|
126
|
-
const rawMessages = context.messages as Parameters<typeof convertToLlm>[0];
|
|
127
|
-
let llmMessages = convertToLlm(rawMessages);
|
|
128
|
-
|
|
129
|
-
// ponytail: filter compaction/branch summary user messages on first turn.
|
|
130
|
-
// convertToLlm wraps them as user messages with <summary> tags; Hunyuan
|
|
131
|
-
// parrots context-mode state from them. Smarter models follow boundary text.
|
|
132
|
-
if (startIndex === 0) {
|
|
133
|
-
llmMessages = llmMessages.filter((m) => {
|
|
134
|
-
if (m.role !== "user") return true;
|
|
135
|
-
const text = extractTextContent(m.content);
|
|
136
|
-
// Compaction summaries are large blocks with context-mode state
|
|
137
|
-
if (text.includes("<summary>") || text.includes("compacted into")) return false;
|
|
138
|
-
// context-mode session state injection
|
|
139
|
-
if (text.includes("context-mode active") || text.includes("<session_state")) return false;
|
|
140
|
-
return true;
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
const latestUserIdx = findLatestUserIndex(llmMessages);
|
|
144
|
-
const msgParts: string[] = [];
|
|
145
|
-
for (let i = startIndex; i < llmMessages.length; i++) {
|
|
146
|
-
if (i === latestUserIdx) continue; // handled separately (images etc.)
|
|
147
|
-
const formatted = formatMessage(llmMessages[i]);
|
|
148
|
-
if (formatted) msgParts.push(formatted);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// 4. Latest user message
|
|
152
|
-
const lastUser = latestUserIdx >= 0 ? llmMessages[latestUserIdx] : null;
|
|
153
|
-
const lastUserText = lastUser ? extractTextContent(lastUser.content) : "Continue.";
|
|
154
|
-
const images = lastUser ? extractImages(lastUser) : [];
|
|
155
|
-
|
|
156
|
-
// 5. Tool tail guard
|
|
157
|
-
parts.push(
|
|
158
|
-
msgParts.length > 0 ? msgParts.join("\n") : "(new conversation)",
|
|
159
|
-
);
|
|
160
|
-
parts.push(CODEBUDDY_TOOL_TAIL);
|
|
161
|
-
|
|
162
|
-
const prefix = parts.join("\n\n");
|
|
163
|
-
|
|
164
|
-
// Plain text (no images)
|
|
165
|
-
if (images.length === 0) {
|
|
166
|
-
return prefix + "\n\n" + lastUserText;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Structured message with images
|
|
170
|
-
const blocks: ContentBlock[] = [];
|
|
171
|
-
blocks.push({ type: "text", text: prefix });
|
|
172
|
-
blocks.push({ type: "text", text: lastUserText });
|
|
173
|
-
for (const block of lastUser!.content) {
|
|
174
|
-
if (typeof block === "object" && block !== null && "type" in block && (block as { type: string }).type === "image") {
|
|
175
|
-
const img = block as ImageContent;
|
|
176
|
-
blocks.push({
|
|
177
|
-
type: "image",
|
|
178
|
-
source: {
|
|
179
|
-
type: "base64",
|
|
180
|
-
media_type: parseImageMediaType(img.mimeType),
|
|
181
|
-
data: stripBase64Prefix(img.data),
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
type: "user",
|
|
189
|
-
session_id: "",
|
|
190
|
-
message: { role: "user", content: blocks },
|
|
191
|
-
parent_tool_use_id: null,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── helpers ──
|
|
196
|
-
|
|
197
|
-
function findLatestUserIndex(messages: Message[]): number {
|
|
198
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
199
|
-
if (messages[i].role === "user") return i;
|
|
200
|
-
}
|
|
201
|
-
return -1;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function extractImages(msg: any): ImageContent[] {
|
|
205
|
-
if (!msg || !Array.isArray(msg.content)) return [];
|
|
206
|
-
return msg.content.filter(
|
|
207
|
-
(c: any) => typeof c === "object" && c !== null && c.type === "image",
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function extractTextContent(content: unknown): string {
|
|
212
|
-
if (typeof content === "string") return content;
|
|
213
|
-
if (!Array.isArray(content)) return "";
|
|
214
|
-
return content
|
|
215
|
-
.filter(
|
|
216
|
-
(c): c is { type: "text"; text: string } =>
|
|
217
|
-
typeof c === "object" &&
|
|
218
|
-
c !== null &&
|
|
219
|
-
"type" in c &&
|
|
220
|
-
c.type === "text" &&
|
|
221
|
-
"text" in c,
|
|
222
|
-
)
|
|
223
|
-
.map((c) => c.text)
|
|
224
|
-
.join("\n");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
type ImageMediaType = "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
228
|
-
|
|
229
|
-
function parseImageMediaType(mimeType: string): ImageMediaType {
|
|
230
|
-
const valid: ImageMediaType[] = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
231
|
-
if (valid.includes(mimeType as ImageMediaType)) return mimeType as ImageMediaType;
|
|
232
|
-
return "image/png";
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function stripBase64Prefix(data: string): string {
|
|
236
|
-
const comma = data.indexOf(",");
|
|
237
|
-
return comma >= 0 ? data.slice(comma + 1) : data;
|
|
238
|
-
}
|
package/src/session-pool.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Pool — CodeBuddy CLI subprocess reuse across Pi turns.
|
|
3
|
-
*
|
|
4
|
-
* ~3s CLI cold-start saved per turn after the first.
|
|
5
|
-
*
|
|
6
|
-
* Key: cwd + fingerprint of conversation's first user message.
|
|
7
|
-
* Session change detected when the first message differs.
|
|
8
|
-
*
|
|
9
|
-
* Anti-double-history: on continuation, only send new messages
|
|
10
|
-
* (the delta since last send). CodeBuddy session keeps internal context.
|
|
11
|
-
*/
|
|
12
|
-
import type { Message as CbMessage, Session } from "@tencent-ai/agent-sdk";
|
|
13
|
-
import type { Context } from "@earendil-works/pi-ai/compat";
|
|
14
|
-
|
|
15
|
-
type CodebuddySdk = typeof import("@tencent-ai/agent-sdk");
|
|
16
|
-
|
|
17
|
-
interface PoolEntry {
|
|
18
|
-
session: Session;
|
|
19
|
-
sentCount: number;
|
|
20
|
-
firstFingerprint: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const pool = new Map<string, PoolEntry>();
|
|
24
|
-
|
|
25
|
-
function fingerprintFirstMessage(context: Context): string {
|
|
26
|
-
const first = context.messages[0];
|
|
27
|
-
if (!first || first.role !== "user") return "";
|
|
28
|
-
if (typeof first.content === "string") return first.content.slice(0, 80);
|
|
29
|
-
if (Array.isArray(first.content)) {
|
|
30
|
-
const texts = first.content
|
|
31
|
-
.filter((c: any) => c?.type === "text")
|
|
32
|
-
.map((c: any) => c.text)
|
|
33
|
-
.join(" ");
|
|
34
|
-
return texts.slice(0, 80);
|
|
35
|
-
}
|
|
36
|
-
return "";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function acquireSession(
|
|
40
|
-
cwd: string,
|
|
41
|
-
sdk: CodebuddySdk,
|
|
42
|
-
context: Context,
|
|
43
|
-
buildFull: (ctx: Context) => string | CbMessage,
|
|
44
|
-
buildDelta: (ctx: Context, startIndex: number) => string | CbMessage,
|
|
45
|
-
sessionOptions: Parameters<CodebuddySdk["unstable_v2_createSession"]>[0],
|
|
46
|
-
): Promise<{
|
|
47
|
-
session: Session;
|
|
48
|
-
message: string | CbMessage;
|
|
49
|
-
isFirst: boolean;
|
|
50
|
-
}> {
|
|
51
|
-
const key = cwd || process.cwd();
|
|
52
|
-
const fp = fingerprintFirstMessage(context);
|
|
53
|
-
const existing = pool.get(key);
|
|
54
|
-
|
|
55
|
-
const isNew = !existing || existing.firstFingerprint !== fp;
|
|
56
|
-
|
|
57
|
-
if (isNew) {
|
|
58
|
-
if (existing) {
|
|
59
|
-
try { existing.session.close(); } catch { /* best-effort */ }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const session = sdk.unstable_v2_createSession(sessionOptions);
|
|
63
|
-
try {
|
|
64
|
-
await session.connect();
|
|
65
|
-
} catch (e) {
|
|
66
|
-
try { session.close(); } catch { /* best-effort, already broken */ }
|
|
67
|
-
throw e;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const message = buildFull(context);
|
|
71
|
-
pool.set(key, { session, sentCount: context.messages.length, firstFingerprint: fp });
|
|
72
|
-
return { session, message, isFirst: true };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const deltaStart = existing.sentCount;
|
|
76
|
-
const message =
|
|
77
|
-
deltaStart < context.messages.length
|
|
78
|
-
? buildDelta(context, deltaStart)
|
|
79
|
-
: "Continue.";
|
|
80
|
-
|
|
81
|
-
existing.sentCount = context.messages.length;
|
|
82
|
-
return { session: existing.session, message, isFirst: false };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function evictSession(cwd: string): void {
|
|
86
|
-
const key = cwd || process.cwd();
|
|
87
|
-
const entry = pool.get(key);
|
|
88
|
-
if (entry) {
|
|
89
|
-
try { entry.session.close(); } catch { /* best-effort */ }
|
|
90
|
-
pool.delete(key);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function closeAll(): void {
|
|
95
|
-
for (const [key, entry] of pool) {
|
|
96
|
-
try { entry.session.close(); } catch { /* best-effort */ }
|
|
97
|
-
pool.delete(key);
|
|
98
|
-
}
|
|
99
|
-
}
|