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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codebuddy-sdk",
3
- "version": "0.1.9",
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,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
- * 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[] = [];
32
+ // ── system prompt sanitization (like pi-cursor-sdk's sanitizeSystemPromptForCursor) ──
39
33
 
40
- if (prefix) {
41
- blocks.push({ type: "text", text: prefix });
42
- }
34
+ function sanitizeSystemPrompt(prompt: string): string {
35
+ let s = prompt;
43
36
 
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
- }
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
- return {
62
- type: "user",
63
- session_id: "",
64
- message: { role: "user", content: blocks },
65
- parent_tool_use_id: null,
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
- * 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
- // 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
- // Continuation turns: include all new messages (from this CodeBuddy session).
83
- const parts: string[] = [];
84
- const messages = context.messages;
52
+ return s.trim();
53
+ }
85
54
 
86
- for (let i = startIndex; i < messages.length; i++) {
87
- const msg = messages[i];
88
- if (i === messages.length - 1 && msg.role === "user") continue;
55
+ // ── message formatting ──
89
56
 
90
- if (msg.role === "user") {
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
- parts.push(`User: ${text}${hasImages ? " [image omitted]" : ""}`);
95
- } else if (hasImages) {
96
- parts.push("User: [image omitted]");
63
+ return `User: ${text}${hasImages ? " [image omitted]" : ""}`;
97
64
  }
98
- } else if (msg.role === "assistant") {
99
- const text = extractTextContent(msg.content);
100
- if (text) parts.push(`Assistant: ${text}`);
101
- for (const block of msg.content) {
102
- if (block.type === "toolCall") {
103
- parts.push(
104
- `[Tool used: ${block.name}(${JSON.stringify(block.arguments)})]`,
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
- } else if (msg.role === "toolResult") {
82
+ return parts.length > 0 ? `Assistant: ${parts.join("\n")}` : undefined;
83
+ }
84
+ case "toolResult": {
109
85
  const text = extractTextContent(msg.content);
110
- const status = msg.isError ? "Error" : "Result";
86
+ const label = msg.isError ? "Tool error" : "Tool result";
111
87
  const snippet = truncateToolResult(text);
112
- parts.push(`[Tool ${status} (${msg.toolName}): ${snippet}]`);
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
- * Truncate tool result with head + tail strategy (industry best practice).
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
- // ── helpers ──
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
- for (let i = 0; i < messages.length - 1; i++) {
149
- const msg = messages[i];
150
- if (msg.role === "user") {
151
- const text = extractTextContent(msg.content);
152
- const hasImages = extractImages(msg).length > 0;
153
- if (text) {
154
- parts.push(`User: ${text}${hasImages ? " [image omitted]" : ""}`);
155
- } else if (hasImages) {
156
- parts.push("User: [image omitted]");
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
- return parts.length > 0 ? parts.join("\n") + "\n\n" : "";
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
- function findLastUserMessage(context: Context, startIndex: number) {
165
- for (let i = context.messages.length - 1; i >= startIndex; i--) {
166
- if (context.messages[i].role === "user") return context.messages[i];
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 null;
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
- "image/png",
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.");