@raoxxxwq/pi-codebuddy-sdk 0.4.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 -1
- package/src/index.ts +39 -17
- package/src/query-state.ts +5 -0
- package/src/skills.ts +8 -3
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,14 +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 extraArgs: Record<string, string | null> = {};
|
|
704
|
-
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
705
705
|
return {
|
|
706
706
|
appendSystemPrompt,
|
|
707
|
-
strictMcpConfigEnabled,
|
|
708
707
|
tools: [] as string[],
|
|
709
|
-
extraArgs,
|
|
708
|
+
extraArgs: { "strict-mcp-config": null } as Record<string, string | null>,
|
|
710
709
|
settingSources: appendSystemPrompt
|
|
711
710
|
? undefined
|
|
712
711
|
: settings.settingSources ?? ["user", "project"] as SettingSource[],
|
|
@@ -781,6 +780,18 @@ function hasRequiredParams(toolName: string, tools: Tool[]): boolean {
|
|
|
781
780
|
return Array.isArray(required) && required.length > 0;
|
|
782
781
|
}
|
|
783
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
|
+
|
|
784
795
|
// Renames for CodeBuddy SDK param names that differ from pi's native names.
|
|
785
796
|
// Keys not listed here pass through unchanged, so new pi params work automatically.
|
|
786
797
|
const SDK_KEY_RENAMES: Record<string, Record<string, string>> = {
|
|
@@ -1135,7 +1146,6 @@ function processStreamEvent(
|
|
|
1135
1146
|
c.turnBlocks.push({ type: "thinking", thinking: "", thinkingSignature: "", index: event.index });
|
|
1136
1147
|
c.currentPiStream!.push({ type: "thinking_start", contentIndex: c.turnBlocks.length - 1, partial: c.turnOutput });
|
|
1137
1148
|
} else if (event.content_block?.type === "tool_use") {
|
|
1138
|
-
c.turnToolCallIds.push(event.content_block.id);
|
|
1139
1149
|
c.turnToolCallIds.push(event.content_block.id);
|
|
1140
1150
|
c.turnBlocks.push({
|
|
1141
1151
|
type: "toolCall", id: event.content_block.id,
|
|
@@ -1637,22 +1647,31 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1637
1647
|
toolUseID: opts.toolUseID,
|
|
1638
1648
|
};
|
|
1639
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
|
+
}
|
|
1640
1658
|
return { behavior: "allow" as const, toolUseID: opts.toolUseID };
|
|
1641
1659
|
};
|
|
1642
1660
|
|
|
1643
1661
|
debug("provider: fresh query",
|
|
1644
1662
|
`model=${cliModel} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
1645
1663
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1646
|
-
`appendSys=${appendSystemPrompt} strictMcp
|
|
1664
|
+
`appendSys=${appendSystemPrompt} strictMcp=true`,
|
|
1647
1665
|
`promptLen=${promptText.length}${promptBlocks ? " [+images]" : ""}`);
|
|
1648
1666
|
|
|
1649
1667
|
// 3. Start SDK query (wait for model discovery + serialize SDK subprocess access)
|
|
1650
1668
|
let wasAborted = false;
|
|
1651
1669
|
let sdkQuery: ReturnType<typeof query> | undefined;
|
|
1670
|
+
const liveQueryRef: { current?: ReturnType<typeof query> } = {};
|
|
1652
1671
|
const abortCtx = queryCtx;
|
|
1653
1672
|
|
|
1654
1673
|
const requestAbort = () => {
|
|
1655
|
-
|
|
1674
|
+
interruptLiveQuery(liveQueryRef);
|
|
1656
1675
|
};
|
|
1657
1676
|
const onAbort = () => {
|
|
1658
1677
|
wasAborted = true;
|
|
@@ -1667,11 +1686,12 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1667
1686
|
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1668
1687
|
}
|
|
1669
1688
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1689
|
+
void (async () => {
|
|
1690
|
+
await ensureModelsDiscovered();
|
|
1691
|
+
sdkQuery = query({ prompt, options: queryOptions });
|
|
1692
|
+
liveQueryRef.current = sdkQuery;
|
|
1693
|
+
queryCtx.activeQuery = sdkQuery;
|
|
1694
|
+
activeQueryContexts.add(queryCtx);
|
|
1675
1695
|
|
|
1676
1696
|
try {
|
|
1677
1697
|
const { capturedSessionId } = await consumeQuery(sdkQuery, customToolNameToPi, model, () => wasAborted, queryCtx);
|
|
@@ -1717,10 +1737,11 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1717
1737
|
break;
|
|
1718
1738
|
}
|
|
1719
1739
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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}`);
|
|
1724
1745
|
|
|
1725
1746
|
try {
|
|
1726
1747
|
const { capturedSessionId: contSid } = await consumeQuery(contQuery, customToolNameToPi, model, () => wasAborted, queryCtx);
|
|
@@ -1766,6 +1787,7 @@ function streamCodebuddySdk(model: Model<any>, context: Context, options?: Simpl
|
|
|
1766
1787
|
queryCtx.currentPiStream = null;
|
|
1767
1788
|
} finally {
|
|
1768
1789
|
if (options?.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1790
|
+
liveQueryRef.current = undefined;
|
|
1769
1791
|
if (queryCtx.activeQuery === sdkQuery) {
|
|
1770
1792
|
for (const pending of queryCtx.pendingToolCalls.values()) { pending.resolve({ content: [{ type: "text", text: "Query ended" }] }); }
|
|
1771
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
|
@@ -71,9 +71,14 @@ export function buildPiToolBridgeInstruction(options: ToolBridgeInstructionOptio
|
|
|
71
71
|
if (has("bash")) {
|
|
72
72
|
lines.push(`- Use \`${mcpName("bash")}\` only when file tools are insufficient, for search/test/build/git information, or when command execution is requested.`);
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.");
|
|
77
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.");
|
|
78
83
|
|
|
79
84
|
return lines.join("\n");
|