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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codebuddy-sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "CodeBuddy Agent SDK provider for Pi — native tool execution, 29+ models, thinking controls, session pool",
5
5
  "author": "slahser",
6
6
  "license": "MIT",
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
- * Build a CodeBuddy UserMessage from the FULL Pi Context.
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
- // ── internal ──
15
+ // ── tool boundary (like pi-cursor-sdk's getCursorToolBoundaryText) ──
27
16
 
28
- function buildMessage(context: Context, startIndex: number): string | CbUserMessage {
29
- const prefix = buildHistoryPrefix(context, startIndex);
30
- const lastUserMsg = findLastUserMessage(context, startIndex);
31
- const images = extractImages(lastUserMsg);
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
- if (images.length === 0) {
34
- return prefix + extractTextFromMessage(lastUserMsg);
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
- // Image support: build ContentBlock[]
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
- return {
62
- type: "user",
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
- * Build conversation history prefix for messages in [startIndex, end).
71
- * The latest user message is always excluded (handled separately).
72
- */
73
- function buildHistoryPrefix(context: Context, startIndex: number): string {
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
- // ponytail: skip Pi systemPrompt — CodeBuddy uses its own.
77
- // Pi's prompt references MCP/tool ecosystem not present in CodeBuddy,
78
- // which confuses the model into calling non-existent tools.
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
- const messages = context.messages;
49
+ // Strip semantic code intelligence priority section
50
+ s = s.replace(/\n+Semantic code intelligence priority:[\s\S]*$/g, "");
81
51
 
82
- // All messages in range except the last user message
83
- for (let i = startIndex; i < messages.length; i++) {
84
- const msg = messages[i];
85
- if (i === messages.length - 1 && msg.role === "user") continue;
52
+ return s.trim();
53
+ }
86
54
 
87
- // Skip assistant messages on first turn — they're from the previous
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
- if (msg.role === "user") {
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
- parts.push(`User: ${text}${hasImages ? " [image omitted]" : ""}`);
97
- } else if (hasImages) {
98
- parts.push("User: [image omitted]");
63
+ return `User: ${text}${hasImages ? " [image omitted]" : ""}`;
99
64
  }
100
- } else if (msg.role === "assistant") {
101
- const text = extractTextContent(msg.content);
102
- if (text) parts.push(`Assistant: ${text}`);
103
- for (const block of msg.content) {
104
- if (block.type === "toolCall") {
105
- parts.push(
106
- `[Tool used: ${block.name}(${JSON.stringify(block.arguments)})]`,
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
- } else if (msg.role === "toolResult") {
82
+ return parts.length > 0 ? `Assistant: ${parts.join("\n")}` : undefined;
83
+ }
84
+ case "toolResult": {
111
85
  const text = extractTextContent(msg.content);
112
- const status = msg.isError ? "Error" : "Result";
86
+ const label = msg.isError ? "Tool error" : "Tool result";
113
87
  const snippet = truncateToolResult(text);
114
- parts.push(`[Tool ${status} (${msg.toolName}): ${snippet}]`);
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
- * Truncate tool result with head + tail strategy (industry best practice).
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 findLastUserMessage(context: Context, startIndex: number) {
142
- for (let i = context.messages.length - 1; i >= startIndex; i--) {
143
- if (context.messages[i].role === "user") return context.messages[i];
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 null;
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
- "image/png",
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.");