pi-codebuddy-sdk 0.1.11 → 0.1.12
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 +51 -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,33 @@ 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
|
+
const content = (messages[i] as any).content;
|
|
87
|
+
if (typeof content === "string") return content;
|
|
88
|
+
if (Array.isArray(content)) {
|
|
89
|
+
const texts = content
|
|
90
|
+
.filter((c: any) => c?.type === "text")
|
|
91
|
+
.map((c: any) => c.text)
|
|
92
|
+
.join("\n");
|
|
93
|
+
if (texts) return texts;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return "Continue.";
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
// ── thinking config mapping ──
|
|
74
101
|
|
|
75
102
|
const DEFAULT_THINKING_BUDGETS: Record<string, number> = {
|
|
76
103
|
minimal: 1600,
|
|
77
104
|
low: 4000,
|
|
78
|
-
medium: 0,
|
|
79
|
-
high: 0,
|
|
105
|
+
medium: 0,
|
|
106
|
+
high: 0,
|
|
80
107
|
xhigh: 32000,
|
|
81
108
|
};
|
|
82
109
|
|
|
@@ -114,53 +141,35 @@ export function streamCodebuddy(
|
|
|
114
141
|
const partial = makePartial(model);
|
|
115
142
|
|
|
116
143
|
queueMicrotask(async () => {
|
|
117
|
-
let session: Awaited<ReturnType<CodebuddySdk["unstable_v2_createSession"]>> | undefined;
|
|
118
|
-
let isFirst = false;
|
|
119
|
-
|
|
120
144
|
try {
|
|
121
145
|
stream.push({ type: "start", partial });
|
|
122
146
|
|
|
123
147
|
const sdk = await loadCodebuddySdk();
|
|
148
|
+
const userText = extractLatestUserText(context);
|
|
124
149
|
|
|
125
150
|
const { thinking, effort } = buildThinkingConfig(
|
|
126
151
|
options?.reasoning,
|
|
127
152
|
options?.thinkingBudgets,
|
|
128
153
|
);
|
|
129
154
|
|
|
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, {
|
|
155
|
+
const q = sdk.query({
|
|
156
|
+
prompt: userText,
|
|
157
|
+
options: {
|
|
138
158
|
model: model.id,
|
|
139
159
|
permissionMode: "bypassPermissions",
|
|
140
160
|
maxTurns: 100,
|
|
141
161
|
thinking,
|
|
142
162
|
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 }>();
|
|
163
|
+
includePartialMessages: true,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
157
166
|
|
|
158
167
|
// ── abort handling ──
|
|
159
168
|
let aborted = false;
|
|
160
169
|
const onAbort = async () => {
|
|
161
170
|
if (aborted) return;
|
|
162
171
|
aborted = true;
|
|
163
|
-
try { await
|
|
172
|
+
try { await q.interrupt(); } catch { /* best-effort */ }
|
|
164
173
|
partial.stopReason = "aborted";
|
|
165
174
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
166
175
|
};
|
|
@@ -172,10 +181,14 @@ export function streamCodebuddy(
|
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
// ──
|
|
176
|
-
|
|
184
|
+
// ── per-index block tracking ──
|
|
185
|
+
type BlockKind = 'text' | 'thinking' | 'tool_use' | 'redacted_thinking';
|
|
186
|
+
const blockKind = new Map<number, BlockKind>();
|
|
187
|
+
const blockAccum = new Map<number, string>();
|
|
188
|
+
const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
|
|
189
|
+
|
|
190
|
+
let hasStreamEvents = false;
|
|
177
191
|
|
|
178
|
-
// ── helpers for stream_event block tracking ──
|
|
179
192
|
function onBlockStart(idx: number, kind: BlockKind, initText: string, meta?: { id: string; name: string; signature?: string }) {
|
|
180
193
|
blockKind.set(idx, kind);
|
|
181
194
|
blockAccum.set(idx, initText);
|
|
@@ -186,22 +199,18 @@ export function streamCodebuddy(
|
|
|
186
199
|
partial.content = [...partial.content, tc];
|
|
187
200
|
stream.push({ type: 'thinking_start', contentIndex: idx, partial });
|
|
188
201
|
stream.push({ type: 'thinking_end', contentIndex: idx, content: '[redacted]', partial });
|
|
189
|
-
msgCount++;
|
|
190
202
|
} else if (kind === 'thinking') {
|
|
191
203
|
const tc: ThinkingContent = { type: 'thinking', thinking: initText, thinkingSignature: meta?.signature };
|
|
192
204
|
partial.content = [...partial.content, tc];
|
|
193
205
|
stream.push({ type: 'thinking_start', contentIndex: idx, partial });
|
|
194
|
-
msgCount++;
|
|
195
206
|
} else if (kind === 'text') {
|
|
196
207
|
const tb: TextContent = { type: 'text', text: initText };
|
|
197
208
|
partial.content = [...partial.content, tb];
|
|
198
209
|
stream.push({ type: 'text_start', contentIndex: idx, partial });
|
|
199
|
-
msgCount++;
|
|
200
210
|
} else if (kind === 'tool_use' && meta) {
|
|
201
211
|
const toolCall: ToolCall = { type: 'toolCall', id: meta.id, name: meta.name, arguments: {} };
|
|
202
212
|
partial.content = [...partial.content, toolCall];
|
|
203
213
|
stream.push({ type: 'toolcall_start', contentIndex: idx, partial });
|
|
204
|
-
msgCount++;
|
|
205
214
|
}
|
|
206
215
|
}
|
|
207
216
|
|
|
@@ -229,7 +238,7 @@ export function streamCodebuddy(
|
|
|
229
238
|
const blocks = [...partial.content];
|
|
230
239
|
try {
|
|
231
240
|
blocks[idx] = { ...blocks[idx], arguments: JSON.parse(newJson) };
|
|
232
|
-
} catch { /* partial JSON
|
|
241
|
+
} catch { /* partial JSON */ }
|
|
233
242
|
partial.content = blocks;
|
|
234
243
|
stream.push({ type: 'toolcall_delta', contentIndex: idx, delta: deltaValue, partial } as any);
|
|
235
244
|
}
|
|
@@ -248,8 +257,8 @@ export function streamCodebuddy(
|
|
|
248
257
|
}
|
|
249
258
|
}
|
|
250
259
|
|
|
251
|
-
// ── iterate
|
|
252
|
-
for await (const msg of
|
|
260
|
+
// ── iterate ──
|
|
261
|
+
for await (const msg of q) {
|
|
253
262
|
if (aborted) return;
|
|
254
263
|
|
|
255
264
|
if (msg.type === "system") {
|
|
@@ -257,7 +266,6 @@ export function streamCodebuddy(
|
|
|
257
266
|
continue;
|
|
258
267
|
}
|
|
259
268
|
|
|
260
|
-
// Primary path: token-level streaming via stream_event
|
|
261
269
|
if (msg.type === "stream_event") {
|
|
262
270
|
hasStreamEvents = true;
|
|
263
271
|
const evt = msg.event;
|
|
@@ -279,21 +287,19 @@ export function streamCodebuddy(
|
|
|
279
287
|
if (d.type === 'text_delta') onBlockDelta(evt.index, 'text_delta', d.text);
|
|
280
288
|
else if (d.type === 'thinking_delta') onBlockDelta(evt.index, 'thinking_delta', d.thinking);
|
|
281
289
|
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
290
|
} else if (evt.type === "content_block_stop") {
|
|
284
291
|
onBlockStop(evt.index);
|
|
285
292
|
}
|
|
286
293
|
continue;
|
|
287
294
|
}
|
|
288
295
|
|
|
289
|
-
// Fallback: full assistant messages
|
|
296
|
+
// Fallback: full assistant messages
|
|
290
297
|
if (msg.type === "assistant" && !hasStreamEvents) {
|
|
291
|
-
msgCount++;
|
|
292
298
|
let ci = 0;
|
|
293
299
|
for (const block of msg.message.content) {
|
|
294
300
|
const idx = ci++;
|
|
295
301
|
if (block.type === "thinking") {
|
|
296
|
-
onBlockStart(idx, 'thinking', block.thinking
|
|
302
|
+
onBlockStart(idx, 'thinking', block.thinking);
|
|
297
303
|
onBlockStop(idx);
|
|
298
304
|
} else if (block.type === "redacted_thinking") {
|
|
299
305
|
onBlockStart(idx, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
|
|
@@ -319,24 +325,13 @@ export function streamCodebuddy(
|
|
|
319
325
|
partial.stopReason = "error";
|
|
320
326
|
partial.errorMessage = msg.errors?.join("; ") ?? `CodeBuddy error: ${msg.subtype}`;
|
|
321
327
|
stream.push({ type: "error", reason: "error", error: { ...partial } });
|
|
322
|
-
evictSession(cwd);
|
|
323
328
|
}
|
|
324
329
|
break;
|
|
325
330
|
}
|
|
326
331
|
}
|
|
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
332
|
} catch (error) {
|
|
333
|
-
evictSession(process.cwd());
|
|
334
333
|
const errPartial = makeErrorPartial(model, error);
|
|
335
334
|
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
335
|
}
|
|
341
336
|
});
|
|
342
337
|
|
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
|
-
}
|