pi-codebuddy-sdk 0.1.9 → 0.1.10
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/context.ts +152 -130
- package/src/dump-context.ts +11 -0
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -1,171 +1,204 @@
|
|
|
1
|
-
import type { Context, ImageContent } from "@earendil-works/pi-ai/compat";
|
|
1
|
+
import type { Context, ImageContent, Message } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { UserMessage as CbUserMessage, ContentBlock } from "@tencent-ai/agent-sdk";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Used on the first turn of a pooled session.
|
|
7
|
-
*
|
|
8
|
-
* Returns either a plain string (text-only) or a structured UserMessage
|
|
9
|
-
* with ContentBlock[] when images are present in the latest user message.
|
|
10
|
-
*/
|
|
5
|
+
// ── public API ──
|
|
6
|
+
|
|
11
7
|
export function buildFullMessage(context: Context): string | CbUserMessage {
|
|
12
8
|
return buildMessage(context, 0);
|
|
13
9
|
}
|
|
14
10
|
|
|
15
|
-
/**
|
|
16
|
-
* Build a CodeBuddy UserMessage from only NEW messages since startIndex.
|
|
17
|
-
* Used on continuation turns of a pooled session (delta mode).
|
|
18
|
-
*
|
|
19
|
-
* Historical messages before startIndex are omitted — the CodeBuddy
|
|
20
|
-
* session already has them in its internal context.
|
|
21
|
-
*/
|
|
22
11
|
export function buildDeltaMessage(context: Context, startIndex: number): string | CbUserMessage {
|
|
23
12
|
return buildMessage(context, startIndex);
|
|
24
13
|
}
|
|
25
14
|
|
|
26
|
-
// ──
|
|
15
|
+
// ── tool boundary (like pi-cursor-sdk's getCursorToolBoundaryText) ──
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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");
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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");
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
const blocks: ContentBlock[] = [];
|
|
32
|
+
// ── system prompt sanitization (like pi-cursor-sdk's sanitizeSystemPromptForCursor) ──
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
34
|
+
function sanitizeSystemPrompt(prompt: string): string {
|
|
35
|
+
let s = prompt;
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} else if (block.type === "image") {
|
|
50
|
-
blocks.push({
|
|
51
|
-
type: "image",
|
|
52
|
-
source: {
|
|
53
|
-
type: "base64",
|
|
54
|
-
media_type: parseImageMediaType(block.mimeType),
|
|
55
|
-
data: stripBase64Prefix(block.data),
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
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
|
+
);
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
67
|
-
}
|
|
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
|
+
);
|
|
68
48
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* The latest user message is always excluded (handled separately).
|
|
72
|
-
*/
|
|
73
|
-
function buildHistoryPrefix(context: Context, startIndex: number): string {
|
|
74
|
-
// ponytail: on first turn, send only previous user messages as context.
|
|
75
|
-
// Assistant messages, tool calls, tool results, system injections
|
|
76
|
-
// (context-mode state, compaction XML, MCP references) are all from
|
|
77
|
-
// another provider and confuse CodeBuddy models.
|
|
78
|
-
if (startIndex === 0) {
|
|
79
|
-
return buildUserOnlyPrefix(context);
|
|
80
|
-
}
|
|
49
|
+
// Strip semantic code intelligence priority section
|
|
50
|
+
s = s.replace(/\n+Semantic code intelligence priority:[\s\S]*$/g, "");
|
|
81
51
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const messages = context.messages;
|
|
52
|
+
return s.trim();
|
|
53
|
+
}
|
|
85
54
|
|
|
86
|
-
|
|
87
|
-
const msg = messages[i];
|
|
88
|
-
if (i === messages.length - 1 && msg.role === "user") continue;
|
|
55
|
+
// ── message formatting ──
|
|
89
56
|
|
|
90
|
-
|
|
57
|
+
function formatMessage(msg: Message): string | undefined {
|
|
58
|
+
switch (msg.role) {
|
|
59
|
+
case "user": {
|
|
91
60
|
const text = extractTextContent(msg.content);
|
|
92
61
|
const hasImages = extractImages(msg).length > 0;
|
|
93
62
|
if (text) {
|
|
94
|
-
|
|
95
|
-
} else if (hasImages) {
|
|
96
|
-
parts.push("User: [image omitted]");
|
|
63
|
+
return `User: ${text}${hasImages ? " [image omitted]" : ""}`;
|
|
97
64
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
106
80
|
}
|
|
107
81
|
}
|
|
108
|
-
|
|
82
|
+
return parts.length > 0 ? `Assistant: ${parts.join("\n")}` : undefined;
|
|
83
|
+
}
|
|
84
|
+
case "toolResult": {
|
|
109
85
|
const text = extractTextContent(msg.content);
|
|
110
|
-
const
|
|
86
|
+
const label = msg.isError ? "Tool error" : "Tool result";
|
|
111
87
|
const snippet = truncateToolResult(text);
|
|
112
|
-
|
|
88
|
+
return `[${label} (${msg.toolName}): ${snippet}]`;
|
|
113
89
|
}
|
|
90
|
+
default:
|
|
91
|
+
return undefined;
|
|
114
92
|
}
|
|
115
|
-
|
|
116
|
-
return parts.length > 0 ? parts.join("\n") + "\n\n" : "";
|
|
117
93
|
}
|
|
118
94
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
* Head 1200 chars (captures output start), tail 600 chars (captures errors/summaries),
|
|
122
|
-
* marker in middle.
|
|
123
|
-
*/
|
|
95
|
+
// ── truncation ──
|
|
96
|
+
|
|
124
97
|
const TOOL_RESULT_HEAD = 1200;
|
|
125
98
|
const TOOL_RESULT_TAIL = 600;
|
|
126
99
|
|
|
127
100
|
function truncateToolResult(text: string): string {
|
|
128
101
|
if (!text) return "(empty)";
|
|
129
102
|
if (text.length <= TOOL_RESULT_HEAD + TOOL_RESULT_TAIL + 50) return text;
|
|
130
|
-
|
|
131
103
|
const head = text.slice(0, TOOL_RESULT_HEAD);
|
|
132
104
|
const tail = text.slice(-TOOL_RESULT_TAIL);
|
|
133
105
|
const skipped = text.length - TOOL_RESULT_HEAD - TOOL_RESULT_TAIL;
|
|
134
106
|
return `${head}\n...[${skipped.toLocaleString()} chars skipped]...\n${tail}`;
|
|
135
107
|
}
|
|
136
108
|
|
|
137
|
-
// ──
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* First-turn prefix: only previous user messages.
|
|
141
|
-
* Skips assistant, tool results, system injections — all provider noise.
|
|
142
|
-
*/
|
|
143
|
-
function buildUserOnlyPrefix(context: Context): string {
|
|
144
|
-
const messages = context.messages;
|
|
145
|
-
if (messages.length <= 1) return "";
|
|
109
|
+
// ── main builder ──
|
|
146
110
|
|
|
111
|
+
function buildMessage(context: Context, startIndex: number): string | CbUserMessage {
|
|
147
112
|
const parts: string[] = [];
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
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}`);
|
|
158
122
|
}
|
|
159
|
-
// skip everything else: assistant, toolResult, system injections
|
|
160
123
|
}
|
|
161
|
-
|
|
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
|
+
};
|
|
162
193
|
}
|
|
163
194
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
200
|
}
|
|
168
|
-
return
|
|
201
|
+
return -1;
|
|
169
202
|
}
|
|
170
203
|
|
|
171
204
|
function extractImages(msg: any): ImageContent[] {
|
|
@@ -175,11 +208,6 @@ function extractImages(msg: any): ImageContent[] {
|
|
|
175
208
|
);
|
|
176
209
|
}
|
|
177
210
|
|
|
178
|
-
function extractTextFromMessage(msg: any): string {
|
|
179
|
-
if (!msg) return "Continue.";
|
|
180
|
-
return extractTextContent(msg.content) || "Continue.";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
211
|
function extractTextContent(content: unknown): string {
|
|
184
212
|
if (typeof content === "string") return content;
|
|
185
213
|
if (!Array.isArray(content)) return "";
|
|
@@ -199,14 +227,8 @@ function extractTextContent(content: unknown): string {
|
|
|
199
227
|
type ImageMediaType = "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
200
228
|
|
|
201
229
|
function parseImageMediaType(mimeType: string): ImageMediaType {
|
|
202
|
-
const valid: ImageMediaType[] = [
|
|
203
|
-
|
|
204
|
-
"image/jpeg",
|
|
205
|
-
"image/gif",
|
|
206
|
-
"image/webp",
|
|
207
|
-
];
|
|
208
|
-
if (valid.includes(mimeType as ImageMediaType))
|
|
209
|
-
return mimeType as ImageMediaType;
|
|
230
|
+
const valid: ImageMediaType[] = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
231
|
+
if (valid.includes(mimeType as ImageMediaType)) return mimeType as ImageMediaType;
|
|
210
232
|
return "image/png";
|
|
211
233
|
}
|
|
212
234
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check what Pi actually injects into Context.messages.
|
|
3
|
+
* Logs all messages with their role and first 100 chars of content.
|
|
4
|
+
*/
|
|
5
|
+
import { createAssistantMessageEventStream } from '@earendil-works/pi-ai';
|
|
6
|
+
|
|
7
|
+
// Register a fake provider to intercept Context
|
|
8
|
+
// We can't easily intercept Context without running Pi, so let's just
|
|
9
|
+
// dump what our extension sees when loaded from Pi.
|
|
10
|
+
console.log("Run this from Pi: pi -e src/dump-context.ts --provider codebuddy --model hy3-preview-agent-ioa -p 'test'");
|
|
11
|
+
console.log("Then check the console output for injected messages.");
|