@raoxxxwq/pi-codebuddy-sdk 0.5.0 → 0.6.0
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/README.md +34 -6
- package/package.json +1 -1
- package/src/config.ts +0 -2
- package/src/index.ts +40 -20
- package/src/query-state.ts +5 -0
- package/src/skills.ts +10 -15
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Pi extension that registers **CodeBuddy** as a model provider. You keep using Pi
|
|
|
9
9
|
|
|
10
10
|
- Exposes CodeBuddy models in Pi (`codebuddy/...` in `/model`)
|
|
11
11
|
- Bridges Pi tools to the SDK (Pi still executes tools; CodeBuddy only plans and calls them)
|
|
12
|
+
- Enforces one bridged tool call per assistant turn on the main Provider Path for stability
|
|
12
13
|
- Forwards Pi's system prompt, skills, and project `AGENTS.md` so the model acts as Pi—not as standalone CodeBuddy Code
|
|
13
14
|
- Supports session resume, compaction, streaming, thinking levels, and images
|
|
14
15
|
- Learns runtime-served context windows over time and keeps Pi's registered model metadata conservatively aligned with observed reality
|
|
@@ -79,34 +80,61 @@ Pick any entry prefixed with `codebuddy/` (for example `codebuddy/hy3-preview-ag
|
|
|
79
80
|
|
|
80
81
|
Tools, skills, extensions, `/compact`, and steer behave the same as with other Pi providers.
|
|
81
82
|
|
|
83
|
+
### AskCodebuddy
|
|
84
|
+
|
|
85
|
+
When the AskCodebuddy tool is enabled (default), any provider can delegate a focused sub-task to a separate CodeBuddy call. This is the **Delegation Path** — unlike the main Provider Path, CodeBuddy runs its own native tools directly (not Pi-bridged MCP tools).
|
|
86
|
+
|
|
87
|
+
- `"read"` mode (default): codebase questions — review, analysis, explain.
|
|
88
|
+
- `"full"` mode: allows writing and bash execution (runs without feedback to Pi — use with care).
|
|
89
|
+
- `"none"` mode: general knowledge only (no file access).
|
|
90
|
+
|
|
91
|
+
AskCodebuddy is blocked automatically when the active provider is already `codebuddy/...` (prevents circular delegation).
|
|
92
|
+
|
|
82
93
|
## Configuration
|
|
83
94
|
|
|
84
95
|
**Optional.** Defaults work for most users.
|
|
85
96
|
|
|
86
|
-
File: `~/.pi/agent/codebuddy-sdk.json` or `.pi/codebuddy-sdk.json`
|
|
97
|
+
File: `~/.pi/agent/codebuddy-sdk.json` (global) or `.pi/codebuddy-sdk.json` (project). Project config overrides global.
|
|
87
98
|
|
|
88
99
|
```json
|
|
89
100
|
{
|
|
90
101
|
"askCodebuddy": {
|
|
91
102
|
"enabled": true,
|
|
92
|
-
"allowFullMode": true
|
|
103
|
+
"allowFullMode": true,
|
|
104
|
+
"defaultMode": "read"
|
|
93
105
|
},
|
|
94
106
|
"provider": {
|
|
95
107
|
"appendSystemPrompt": true,
|
|
96
|
-
"strictMcpConfig": true,
|
|
97
108
|
"pathToCodebuddyCode": "/path/to/codebuddy"
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
```
|
|
101
112
|
|
|
113
|
+
### AskCodebuddy options
|
|
114
|
+
|
|
102
115
|
| Option | Default | Meaning |
|
|
103
116
|
|--------|---------|---------|
|
|
104
117
|
| `askCodebuddy.enabled` | `true` | Register the AskCodebuddy delegation tool |
|
|
105
|
-
| `askCodebuddy.allowFullMode` | `true` | Allow write-capable delegation mode |
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
118
|
+
| `askCodebuddy.allowFullMode` | `true` | Allow write-capable (`"full"`) delegation mode |
|
|
119
|
+
| `askCodebuddy.defaultMode` | `"read"` | Default delegation mode: `"read"` (file access), `"full"` (write + bash), or `"none"` (general knowledge) |
|
|
120
|
+
| `askCodebuddy.defaultIsolated` | `false` | Default whether the delegation runs in a clean session (no conversation history) |
|
|
121
|
+
| `askCodebuddy.appendSkills` | `true` | Include Pi skills block in the delegation system prompt |
|
|
122
|
+
| `askCodebuddy.name` | `"AskCodebuddy"` | Tool name |
|
|
123
|
+
| `askCodebuddy.label` | `"Ask CodeBuddy"` | Tool label shown in the Pi TUI |
|
|
124
|
+
| `askCodebuddy.description` | auto | Tool description shown in Pi |
|
|
125
|
+
|
|
126
|
+
### Provider options
|
|
127
|
+
|
|
128
|
+
These control the main Provider Path (when you pick `codebuddy/...` in `/model`). Options marked **escape hatch** are not for everyday tuning — disable only for debugging or compatibility.
|
|
129
|
+
|
|
130
|
+
| Option | Default | Meaning |
|
|
131
|
+
|--------|---------|---------|
|
|
132
|
+
| `provider.appendSystemPrompt` | `true` | Use Pi's system prompt and Pi Tool Bridge guidance instead of CodeBuddy's default identity (**escape hatch** — disabling re-enables CodeBuddy filesystem settings) |
|
|
133
|
+
| `provider.settingSources` | `["user","project"]` | CodeBuddy filesystem settings to load; only used when `appendSystemPrompt=false` (**escape hatch**) |
|
|
108
134
|
| `provider.pathToCodebuddyCode` | auto | Path to `codebuddy` when it is **not** on `PATH` |
|
|
109
135
|
|
|
136
|
+
The main Provider Path always runs with strict MCP enabled, so only the Pi-bridged MCP server is visible to CodeBuddy in provider mode.
|
|
137
|
+
|
|
110
138
|
## Privacy
|
|
111
139
|
|
|
112
140
|
- The extension does **not** send conversation data to this repository or any third-party telemetry endpoint.
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -666,6 +666,9 @@ export const __test = {
|
|
|
666
666
|
syncSharedSession,
|
|
667
667
|
isEmptyArgs,
|
|
668
668
|
hasRequiredParams,
|
|
669
|
+
claimSerialToolUse,
|
|
670
|
+
interruptLiveQuery,
|
|
671
|
+
processStreamEvent,
|
|
669
672
|
};
|
|
670
673
|
|
|
671
674
|
function createDelegationSessionFromContext(
|
|
@@ -699,16 +702,10 @@ function stripToolHistoryForDelegation(messages: Context["messages"]): Context["
|
|
|
699
702
|
|
|
700
703
|
function buildProviderBoundaryOptions(settings: NonNullable<Config["provider"]>) {
|
|
701
704
|
const appendSystemPrompt = settings.appendSystemPrompt !== false;
|
|
702
|
-
const strictMcpConfigEnabled = settings.strictMcpConfig !== false;
|
|
703
|
-
const serialToolCalls = settings.serialToolCalls !== false;
|
|
704
|
-
const extraArgs: Record<string, string | null> = {};
|
|
705
|
-
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
706
705
|
return {
|
|
707
706
|
appendSystemPrompt,
|
|
708
|
-
strictMcpConfigEnabled,
|
|
709
|
-
serialToolCalls,
|
|
710
707
|
tools: [] as string[],
|
|
711
|
-
extraArgs,
|
|
708
|
+
extraArgs: { "strict-mcp-config": null } as Record<string, string | null>,
|
|
712
709
|
settingSources: appendSystemPrompt
|
|
713
710
|
? undefined
|
|
714
711
|
: settings.settingSources ?? ["user", "project"] as SettingSource[],
|
|
@@ -783,6 +780,18 @@ function hasRequiredParams(toolName: string, tools: Tool[]): boolean {
|
|
|
783
780
|
return Array.isArray(required) && required.length > 0;
|
|
784
781
|
}
|
|
785
782
|
|
|
783
|
+
function claimSerialToolUse(queryCtx: QueryContext, toolUseId: string): boolean {
|
|
784
|
+
if (!queryCtx.claimedToolUseId) {
|
|
785
|
+
queryCtx.claimedToolUseId = toolUseId;
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
return queryCtx.claimedToolUseId === toolUseId;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function interruptLiveQuery(ref: { current?: { interrupt?: () => Promise<void> | void } }): void {
|
|
792
|
+
void ref.current?.interrupt?.();
|
|
793
|
+
}
|
|
794
|
+
|
|
786
795
|
// Renames for CodeBuddy SDK param names that differ from pi's native names.
|
|
787
796
|
// Keys not listed here pass through unchanged, so new pi params work automatically.
|
|
788
797
|
const SDK_KEY_RENAMES: Record<string, Record<string, string>> = {
|
|
@@ -1137,7 +1146,6 @@ function processStreamEvent(
|
|
|
1137
1146
|
c.turnBlocks.push({ type: "thinking", thinking: "", thinkingSignature: "", index: event.index });
|
|
1138
1147
|
c.currentPiStream!.push({ type: "thinking_start", contentIndex: c.turnBlocks.length - 1, partial: c.turnOutput });
|
|
1139
1148
|
} else if (event.content_block?.type === "tool_use") {
|
|
1140
|
-
c.turnToolCallIds.push(event.content_block.id);
|
|
1141
1149
|
c.turnToolCallIds.push(event.content_block.id);
|
|
1142
1150
|
c.turnBlocks.push({
|
|
1143
1151
|
type: "toolCall", id: event.content_block.id,
|
|
@@ -1583,7 +1591,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1583
1591
|
const boundaryOptions = buildProviderBoundaryOptions(providerSettings);
|
|
1584
1592
|
const appendSystemPrompt = boundaryOptions.appendSystemPrompt;
|
|
1585
1593
|
const systemPrompt = appendSystemPrompt
|
|
1586
|
-
? buildCodebuddySystemPrompt(context.systemPrompt, { availableToolNames: mcpTools.map((tool) => tool.name)
|
|
1594
|
+
? buildCodebuddySystemPrompt(context.systemPrompt, { availableToolNames: mcpTools.map((tool) => tool.name) })
|
|
1587
1595
|
: undefined;
|
|
1588
1596
|
|
|
1589
1597
|
// Provider Path keeps CodeBuddy inside Pi's tool boundary: no built-in SDK
|
|
@@ -1639,22 +1647,31 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1639
1647
|
toolUseID: opts.toolUseID,
|
|
1640
1648
|
};
|
|
1641
1649
|
}
|
|
1650
|
+
if (!claimSerialToolUse(queryCtx, opts.toolUseID)) {
|
|
1651
|
+
debug(`canUseTool: rejecting ${toolName}→${piName} — serial mode already claimed by ${queryCtx.claimedToolUseId} (toolUseId=${opts.toolUseID})`);
|
|
1652
|
+
return {
|
|
1653
|
+
behavior: "deny" as const,
|
|
1654
|
+
message: `Only one tool call is allowed per assistant turn. Wait for the current tool result before calling another tool in a later turn.`,
|
|
1655
|
+
toolUseID: opts.toolUseID,
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1642
1658
|
return { behavior: "allow" as const, toolUseID: opts.toolUseID };
|
|
1643
1659
|
};
|
|
1644
1660
|
|
|
1645
1661
|
debug("provider: fresh query",
|
|
1646
1662
|
`model=${cliModel} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
1647
1663
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1648
|
-
`appendSys=${appendSystemPrompt} strictMcp
|
|
1664
|
+
`appendSys=${appendSystemPrompt} strictMcp=true`,
|
|
1649
1665
|
`promptLen=${promptText.length}${promptBlocks ? " [+images]" : ""}`);
|
|
1650
1666
|
|
|
1651
1667
|
// 3. Start SDK query (wait for model discovery + serialize SDK subprocess access)
|
|
1652
1668
|
let wasAborted = false;
|
|
1653
1669
|
let sdkQuery: ReturnType<typeof query> | undefined;
|
|
1670
|
+
const liveQueryRef: { current?: ReturnType<typeof query> } = {};
|
|
1654
1671
|
const abortCtx = queryCtx;
|
|
1655
1672
|
|
|
1656
1673
|
const requestAbort = () => {
|
|
1657
|
-
|
|
1674
|
+
interruptLiveQuery(liveQueryRef);
|
|
1658
1675
|
};
|
|
1659
1676
|
const onAbort = () => {
|
|
1660
1677
|
wasAborted = true;
|
|
@@ -1669,11 +1686,12 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1669
1686
|
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1670
1687
|
}
|
|
1671
1688
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1689
|
+
void (async () => {
|
|
1690
|
+
await ensureModelsDiscovered();
|
|
1691
|
+
sdkQuery = query({ prompt, options: queryOptions });
|
|
1692
|
+
liveQueryRef.current = sdkQuery;
|
|
1693
|
+
queryCtx.activeQuery = sdkQuery;
|
|
1694
|
+
activeQueryContexts.add(queryCtx);
|
|
1677
1695
|
|
|
1678
1696
|
try {
|
|
1679
1697
|
const { capturedSessionId } = await consumeQuery(sdkQuery, customToolNameToPi, model, () => wasAborted, queryCtx);
|
|
@@ -1719,10 +1737,11 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1719
1737
|
break;
|
|
1720
1738
|
}
|
|
1721
1739
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1740
|
+
const contOptions = { ...queryOptions, resume: resumeId, ...makeCliDebugOptions("continuation") };
|
|
1741
|
+
const contQuery = query({ prompt: steerPrompt, options: contOptions });
|
|
1742
|
+
liveQueryRef.current = contQuery;
|
|
1743
|
+
queryCtx.activeQuery = contQuery;
|
|
1744
|
+
debug(`provider: continuation query, model=${cliModel}, resume=${resumeId.slice(0, 8)}, promptLen=${steerPrompt.length}`);
|
|
1726
1745
|
|
|
1727
1746
|
try {
|
|
1728
1747
|
const { capturedSessionId: contSid } = await consumeQuery(contQuery, customToolNameToPi, model, () => wasAborted, queryCtx);
|
|
@@ -1768,6 +1787,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1768
1787
|
queryCtx.currentPiStream = null;
|
|
1769
1788
|
} finally {
|
|
1770
1789
|
if (options?.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1790
|
+
liveQueryRef.current = undefined;
|
|
1771
1791
|
if (queryCtx.activeQuery === sdkQuery) {
|
|
1772
1792
|
for (const pending of queryCtx.pendingToolCalls.values()) { pending.resolve({ content: [{ type: "text", text: "Query ended" }] }); }
|
|
1773
1793
|
queryCtx.pendingToolCalls.clear();
|
package/src/query-state.ts
CHANGED
|
@@ -41,6 +41,10 @@ export class QueryContext {
|
|
|
41
41
|
// Prevents double-deferral and lets the assistant-message path know it
|
|
42
42
|
// must emit the done event after backfilling.
|
|
43
43
|
doneDeferredForArgs = false;
|
|
44
|
+
// First toolUseID observed by canUseTool in this assistant turn.
|
|
45
|
+
// Any different toolUseID in the same turn is denied so provider-path
|
|
46
|
+
// tools always execute serially.
|
|
47
|
+
claimedToolUseId: string | null = null;
|
|
44
48
|
|
|
45
49
|
// Per-turn (reset together)
|
|
46
50
|
turnOutput: AssistantMessage | null = null;
|
|
@@ -66,6 +70,7 @@ export class QueryContext {
|
|
|
66
70
|
this.turnSawToolCall = false;
|
|
67
71
|
this.argsPendingBlocks = [];
|
|
68
72
|
this.doneDeferredForArgs = false;
|
|
73
|
+
this.claimedToolUseId = null;
|
|
69
74
|
this.matchedToolCallIds = new Set();
|
|
70
75
|
// turnToolCallIds and nextHandlerIdx are NOT reset — they persist across
|
|
71
76
|
// tool-result delivery callbacks within the same assistant message.
|
package/src/skills.ts
CHANGED
|
@@ -20,7 +20,6 @@ const BUILTIN_TOOL_GUIDANCE: Record<string, string> = {
|
|
|
20
20
|
|
|
21
21
|
export interface ToolBridgeInstructionOptions {
|
|
22
22
|
availableToolNames?: Iterable<string>;
|
|
23
|
-
serialToolCalls?: boolean;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
function normalizeToolNames(names: Iterable<string> | undefined): Set<string> {
|
|
@@ -72,18 +71,14 @@ export function buildPiToolBridgeInstruction(options: ToolBridgeInstructionOptio
|
|
|
72
71
|
if (has("bash")) {
|
|
73
72
|
lines.push(`- Use \`${mcpName("bash")}\` only when file tools are insufficient, for search/test/build/git information, or when command execution is requested.`);
|
|
74
73
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
lines.push("Call AT MOST ONE tool per turn. Never emit multiple tool_call blocks in a single response. Issue one tool call, wait for its result, then decide the next call in the following turn. Parallel tool calls are unsupported and will fail.");
|
|
84
|
-
} else if (toolNames.size > 1) {
|
|
85
|
-
lines.push("When calling multiple tools in a single response, ensure each tool_call has complete arguments — do not leave required fields empty or rely on defaults. Incomplete parallel tool calls will be rejected.");
|
|
86
|
-
}
|
|
74
|
+
// Force serial tool calls. CodeBuddy's MCP client drops arguments for
|
|
75
|
+
// 2nd+ parallel tool_call blocks in a single response: the
|
|
76
|
+
// content_block_stop for those blocks never arrives, so pi finalizes a
|
|
77
|
+
// dangling toolcall_start with empty {} args and the call fails
|
|
78
|
+
// validation. Instructing one-tool-per-turn avoids the failure at the
|
|
79
|
+
// source. The stream-side backfill defense cannot catch this because it
|
|
80
|
+
// keys off content_block_stop, which never fires for the dropped call.
|
|
81
|
+
lines.push("Call AT MOST ONE tool per turn. Never emit multiple tool_call blocks in a single response. A second tool call in the same turn will be denied. Issue one tool call, wait for its result, then decide the next call in the following turn. Parallel tool calls are unsupported and will fail.");
|
|
87
82
|
lines.push("Tool arguments must match the Pi tool schema exactly. After a tool result, base the next step on that result; if it is an error, correct the call instead of assuming success.");
|
|
88
83
|
|
|
89
84
|
return lines.join("\n");
|
|
@@ -140,13 +135,13 @@ function applySkillsRewrite(systemPrompt: string): string {
|
|
|
140
135
|
/** Pi system prompt as CodeBuddy override (replaces default "CodeBuddy Code" identity). */
|
|
141
136
|
export function buildCodebuddySystemPrompt(
|
|
142
137
|
piSystemPrompt: string | undefined,
|
|
143
|
-
options?: { includeAgents?: boolean; includeSkills?: boolean; includeToolBridge?: boolean; availableToolNames?: Iterable<string
|
|
138
|
+
options?: { includeAgents?: boolean; includeSkills?: boolean; includeToolBridge?: boolean; availableToolNames?: Iterable<string> },
|
|
144
139
|
): string | undefined {
|
|
145
140
|
const parts: string[] = [];
|
|
146
141
|
const includeToolBridge = options?.includeToolBridge !== false;
|
|
147
142
|
|
|
148
143
|
if (includeToolBridge) {
|
|
149
|
-
parts.push(buildPiToolBridgeInstruction({ availableToolNames: options?.availableToolNames
|
|
144
|
+
parts.push(buildPiToolBridgeInstruction({ availableToolNames: options?.availableToolNames }));
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
if (piSystemPrompt) {
|