pi-codebuddy-sdk 0.1.8 → 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 +156 -111
- package/src/dump-context.ts +11 -0
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -1,148 +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[] = [];
|
|
39
|
-
|
|
40
|
-
if (prefix) {
|
|
41
|
-
blocks.push({ type: "text", text: prefix });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
for (const block of lastUserMsg!.content) {
|
|
45
|
-
if (typeof block === "string") {
|
|
46
|
-
blocks.push({ type: "text", text: block });
|
|
47
|
-
} else if (block.type === "text") {
|
|
48
|
-
blocks.push({ type: "text", text: block.text });
|
|
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
|
-
}
|
|
32
|
+
// ── system prompt sanitization (like pi-cursor-sdk's sanitizeSystemPromptForCursor) ──
|
|
60
33
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
session_id: "",
|
|
64
|
-
message: { role: "user", content: blocks },
|
|
65
|
-
parent_tool_use_id: null,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
34
|
+
function sanitizeSystemPrompt(prompt: string): string {
|
|
35
|
+
let s = prompt;
|
|
68
36
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const parts: string[] = [];
|
|
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
|
+
);
|
|
75
42
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
);
|
|
79
48
|
|
|
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 msg = messages[i];
|
|
85
|
-
if (i === messages.length - 1 && msg.role === "user") continue;
|
|
52
|
+
return s.trim();
|
|
53
|
+
}
|
|
86
54
|
|
|
87
|
-
|
|
88
|
-
// provider and contain provider-specific instructions (MCP tools,
|
|
89
|
-
// context-mode, boot protocol) that confuse CodeBuddy models.
|
|
90
|
-
if (msg.role === "assistant" && startIndex === 0) continue;
|
|
55
|
+
// ── message formatting ──
|
|
91
56
|
|
|
92
|
-
|
|
57
|
+
function formatMessage(msg: Message): string | undefined {
|
|
58
|
+
switch (msg.role) {
|
|
59
|
+
case "user": {
|
|
93
60
|
const text = extractTextContent(msg.content);
|
|
94
61
|
const hasImages = extractImages(msg).length > 0;
|
|
95
62
|
if (text) {
|
|
96
|
-
|
|
97
|
-
} else if (hasImages) {
|
|
98
|
-
parts.push("User: [image omitted]");
|
|
63
|
+
return `User: ${text}${hasImages ? " [image omitted]" : ""}`;
|
|
99
64
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
108
80
|
}
|
|
109
81
|
}
|
|
110
|
-
|
|
82
|
+
return parts.length > 0 ? `Assistant: ${parts.join("\n")}` : undefined;
|
|
83
|
+
}
|
|
84
|
+
case "toolResult": {
|
|
111
85
|
const text = extractTextContent(msg.content);
|
|
112
|
-
const
|
|
86
|
+
const label = msg.isError ? "Tool error" : "Tool result";
|
|
113
87
|
const snippet = truncateToolResult(text);
|
|
114
|
-
|
|
88
|
+
return `[${label} (${msg.toolName}): ${snippet}]`;
|
|
115
89
|
}
|
|
90
|
+
default:
|
|
91
|
+
return undefined;
|
|
116
92
|
}
|
|
117
|
-
|
|
118
|
-
return parts.length > 0 ? parts.join("\n") + "\n\n" : "";
|
|
119
93
|
}
|
|
120
94
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* Head 1200 chars (captures output start), tail 600 chars (captures errors/summaries),
|
|
124
|
-
* marker in middle.
|
|
125
|
-
*/
|
|
95
|
+
// ── truncation ──
|
|
96
|
+
|
|
126
97
|
const TOOL_RESULT_HEAD = 1200;
|
|
127
98
|
const TOOL_RESULT_TAIL = 600;
|
|
128
99
|
|
|
129
100
|
function truncateToolResult(text: string): string {
|
|
130
101
|
if (!text) return "(empty)";
|
|
131
102
|
if (text.length <= TOOL_RESULT_HEAD + TOOL_RESULT_TAIL + 50) return text;
|
|
132
|
-
|
|
133
103
|
const head = text.slice(0, TOOL_RESULT_HEAD);
|
|
134
104
|
const tail = text.slice(-TOOL_RESULT_TAIL);
|
|
135
105
|
const skipped = text.length - TOOL_RESULT_HEAD - TOOL_RESULT_TAIL;
|
|
136
106
|
return `${head}\n...[${skipped.toLocaleString()} chars skipped]...\n${tail}`;
|
|
137
107
|
}
|
|
138
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
|
+
|
|
139
195
|
// ── helpers ──
|
|
140
196
|
|
|
141
|
-
function
|
|
142
|
-
for (let i =
|
|
143
|
-
if (
|
|
197
|
+
function findLatestUserIndex(messages: Message[]): number {
|
|
198
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
199
|
+
if (messages[i].role === "user") return i;
|
|
144
200
|
}
|
|
145
|
-
return
|
|
201
|
+
return -1;
|
|
146
202
|
}
|
|
147
203
|
|
|
148
204
|
function extractImages(msg: any): ImageContent[] {
|
|
@@ -152,11 +208,6 @@ function extractImages(msg: any): ImageContent[] {
|
|
|
152
208
|
);
|
|
153
209
|
}
|
|
154
210
|
|
|
155
|
-
function extractTextFromMessage(msg: any): string {
|
|
156
|
-
if (!msg) return "Continue.";
|
|
157
|
-
return extractTextContent(msg.content) || "Continue.";
|
|
158
|
-
}
|
|
159
|
-
|
|
160
211
|
function extractTextContent(content: unknown): string {
|
|
161
212
|
if (typeof content === "string") return content;
|
|
162
213
|
if (!Array.isArray(content)) return "";
|
|
@@ -176,14 +227,8 @@ function extractTextContent(content: unknown): string {
|
|
|
176
227
|
type ImageMediaType = "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
177
228
|
|
|
178
229
|
function parseImageMediaType(mimeType: string): ImageMediaType {
|
|
179
|
-
const valid: ImageMediaType[] = [
|
|
180
|
-
|
|
181
|
-
"image/jpeg",
|
|
182
|
-
"image/gif",
|
|
183
|
-
"image/webp",
|
|
184
|
-
];
|
|
185
|
-
if (valid.includes(mimeType as ImageMediaType))
|
|
186
|
-
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;
|
|
187
232
|
return "image/png";
|
|
188
233
|
}
|
|
189
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.");
|