pi-claude-agent-sdk 0.8.4 → 0.8.6
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 +3 -3
- package/package.json +3 -3
- package/src/claude-executable.ts +149 -0
- package/src/index.ts +76 -122
- package/src/models.ts +8 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ pi install npm:pi-claude-agent-sdk
|
|
|
21
21
|
|
|
22
22
|
## Provider
|
|
23
23
|
|
|
24
|
-
Use `/model` to select `claude-bridge/claude-fable-5-1`, `claude-bridge/claude-fable-5`, `claude-bridge/claude-opus-5`, `claude-bridge/claude-opus-4-8`, `claude-bridge/claude-opus-4-7`, `claude-bridge/claude-opus-4-6`, `claude-bridge/claude-sonnet-5`, `claude-bridge/claude-sonnet-4-6`, or `claude-bridge/claude-haiku-4-5`. The `fable` shortcut resolves to Fable 5.1. Fable 5.1 needs Claude Code **2.1.251 or newer
|
|
24
|
+
Use `/model` to select `claude-bridge/claude-fable-5-1`, `claude-bridge/claude-fable-5`, `claude-bridge/claude-opus-5`, `claude-bridge/claude-opus-4-8`, `claude-bridge/claude-opus-4-7`, `claude-bridge/claude-opus-4-6`, `claude-bridge/claude-sonnet-5`, `claude-bridge/claude-sonnet-4-6`, or `claude-bridge/claude-haiku-4-5`. The `fable` shortcut resolves to Fable 5.1. Fable 5.1 needs Claude Code **2.1.251 or newer** (the SDK's bundled CLI is 2.1.257). If you point `provider.pathToClaudeCodeExecutable` at an older binary, or a later model outruns the bundle, the bridge uses a current `claude` on PATH when it finds one.
|
|
25
25
|
|
|
26
26
|
Behind the scenes, pi's tools are bridged to Claude Code but it should all work like normal in pi. Bash commands get a 120-second default timeout (matching Claude Code's default) since pi's bash has no timeout by default. Skills in pi are copied over to Claude Code's system prompt so should work as they would with any other pi provider. Steering works mid-turn: a message sent while Claude is running a tool reaches it at that tool boundary, not after the whole turn finishes.
|
|
27
27
|
|
|
@@ -49,7 +49,7 @@ Config: `~/.pi/agent/claude-bridge.json` (global) or the project Pi config direc
|
|
|
49
49
|
- `longContextExtraUsage` — set to `true` to enable 1M models that cost money through Extra Usage. It enables Sonnet 4.6 with 1M on every plan and Opus 4.6 with 1M on Pro. Not needed for Opus 4.7 or 4.8.
|
|
50
50
|
- `strictMcpConfig` — block MCP servers from `~/.claude.json` / `.mcp.json` (default `true`). Cloud MCP (Gmail/Drive via claude.ai OAuth) is always blocked.
|
|
51
51
|
- `autoMemoryEnabled` — enable Claude Code's auto-memory system (default `false`)
|
|
52
|
-
- `pathToClaudeCodeExecutable` — path to the `claude` binary. Useful if your OS/filesystem has the SDK's bundled musl/glibc binaries in a place where they can't run. For example, with Nix you can set the binary to e.g. `"/home/you/.nix-profile/bin/claude"`.
|
|
52
|
+
- `pathToClaudeCodeExecutable` — path to the `claude` binary. Useful if your OS/filesystem has the SDK's bundled musl/glibc binaries in a place where they can't run, or to pin a specific CLI. For example, with Nix you can set the binary to e.g. `"/home/you/.nix-profile/bin/claude"`.
|
|
53
53
|
|
|
54
54
|
**Extension providers and models.json:** pi's `modelOverrides` in `~/.pi/agent/models.json` do not currently apply to extension-registered providers (like claude-bridge). Overriding `contextWindow` or other fields requires editing `src/models.ts` directly.
|
|
55
55
|
|
|
@@ -66,7 +66,7 @@ Integration tests spawn real `pi` and Claude Code subprocesses, so they need wri
|
|
|
66
66
|
Set `CLAUDE_BRIDGE_DEBUG=1` to enable debug output:
|
|
67
67
|
|
|
68
68
|
- **Bridge log** at `~/.pi/agent/claude-bridge.log` — every provider call, session sync decision, tool result delivery, and CC's stderr. Override location with `CLAUDE_BRIDGE_DEBUG_PATH`.
|
|
69
|
-
- **Per-query Claude Code CLI logs** at `~/.pi/agent/cc-cli-logs/<timestamp>-<tag>-<seq>.log` — the CC subprocess's own debug stream, one file per `query()` call. Tags are `provider` (main turn) or `
|
|
69
|
+
- **Per-query Claude Code CLI logs** at `~/.pi/agent/cc-cli-logs/<timestamp>-<tag>-<seq>.log` — the CC subprocess's own debug stream, one file per `query()` call. Tags are `provider` (resumable main turn) or `standalone` (compaction, branch summary, and tool-free extension-owned no-cache completion). Useful when a resume fails or CC misbehaves internally — shows the CLI's own view of session loading, API requests, and tool calls.
|
|
70
70
|
|
|
71
71
|
When filing a bug about a session-resume failure (e.g. "No conversation found"), the most useful attachments are the `syncResult:` lines from the bridge log plus the matching `cc-cli-logs/` file for the failing query.
|
|
72
72
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-claude-agent-sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider.",
|
|
6
6
|
"keywords": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"type": "module",
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@anthropic-ai/claude-agent-sdk": "^0.
|
|
43
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.257",
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
45
|
"cc-session-io": "^0.4.0",
|
|
46
46
|
"change-case": "^5.4.4"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@earendil-works/pi-coding-agent": ">=0.82.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@anthropic-ai/sdk": "^0.
|
|
53
|
+
"@anthropic-ai/sdk": "^0.93.0",
|
|
54
54
|
"@earendil-works/pi-ai": "^0.83.0",
|
|
55
55
|
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
56
56
|
"@types/node": "^24.13.2",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Pick a Claude Code CLI that can actually serve the selected model.
|
|
2
|
+
// The Agent SDK bundles its own binary (`claudeCodeVersion` in its package.json).
|
|
3
|
+
// Some models (Fable 5.1) 400 against an older CLI; when the bundle is below
|
|
4
|
+
// the model's minimum we use a current `claude` on PATH, matching what the
|
|
5
|
+
// README already told people to set via pathToClaudeCodeExecutable.
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { delimiter, dirname, join } from "node:path";
|
|
11
|
+
import { minClaudeCodeVersionForModel } from "./models.js";
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
export type ClaudeVersion = { major: number; minor: number; patch: number };
|
|
16
|
+
|
|
17
|
+
export type ClaudeExecutableSource = "configured" | "bundled" | "path";
|
|
18
|
+
|
|
19
|
+
export type ClaudeExecutableResolution = {
|
|
20
|
+
/** Absolute path for the SDK, or undefined to use the bundled CLI. */
|
|
21
|
+
path?: string;
|
|
22
|
+
source: ClaudeExecutableSource;
|
|
23
|
+
/** Set when no available CLI meets the model's minimum version. */
|
|
24
|
+
error?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ClaudeExecutableDeps = {
|
|
28
|
+
bundledVersion?: () => ClaudeVersion | undefined;
|
|
29
|
+
findOnPath?: () => string | undefined;
|
|
30
|
+
readVersion?: (executable: string) => ClaudeVersion | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const versionCache = new Map<string, ClaudeVersion | undefined>();
|
|
34
|
+
|
|
35
|
+
export function parseClaudeVersion(text: string): ClaudeVersion | undefined {
|
|
36
|
+
const match = text.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
37
|
+
if (!match) return undefined;
|
|
38
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatClaudeVersion(version: ClaudeVersion): string {
|
|
42
|
+
return `${version.major}.${version.minor}.${version.patch}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function compareClaudeVersion(a: ClaudeVersion, b: ClaudeVersion): number {
|
|
46
|
+
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readBundledClaudeCodeVersion(): ClaudeVersion | undefined {
|
|
50
|
+
try {
|
|
51
|
+
// The SDK does not export ./package.json; read the file next to its entry.
|
|
52
|
+
const pkgPath = join(dirname(require.resolve("@anthropic-ai/claude-agent-sdk")), "package.json");
|
|
53
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { claudeCodeVersion?: string };
|
|
54
|
+
return parseClaudeVersion(pkg.claudeCodeVersion ?? "");
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function findExecutableOnPath(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
61
|
+
const pathEnv = env.PATH ?? env.Path;
|
|
62
|
+
if (!pathEnv) return undefined;
|
|
63
|
+
const exts = process.platform === "win32"
|
|
64
|
+
? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
65
|
+
: [""];
|
|
66
|
+
const names = process.platform === "win32" && !exts.some((ext) => name.toUpperCase().endsWith(ext.toUpperCase()))
|
|
67
|
+
? [name, ...exts.map((ext) => name + ext)]
|
|
68
|
+
: [name];
|
|
69
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
70
|
+
if (!dir) continue;
|
|
71
|
+
for (const candidateName of names) {
|
|
72
|
+
const candidate = join(dir, candidateName);
|
|
73
|
+
try {
|
|
74
|
+
accessSync(candidate, constants.X_OK);
|
|
75
|
+
return candidate;
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readClaudeCliVersion(executable: string): ClaudeVersion | undefined {
|
|
83
|
+
if (versionCache.has(executable)) return versionCache.get(executable);
|
|
84
|
+
try {
|
|
85
|
+
const result = spawnSync(executable, ["--version"], {
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
timeout: 8000,
|
|
88
|
+
env: process.env,
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
});
|
|
91
|
+
const version = parseClaudeVersion(`${result.stdout ?? ""}\n${result.stderr ?? ""}`);
|
|
92
|
+
versionCache.set(executable, version);
|
|
93
|
+
return version;
|
|
94
|
+
} catch {
|
|
95
|
+
versionCache.set(executable, undefined);
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function tooOldError(modelId: string, min: ClaudeVersion, found: string): string {
|
|
101
|
+
return `${modelId} requires Claude Code ${formatClaudeVersion(min)} or newer (${found}). Install a current claude on PATH, or set provider.pathToClaudeCodeExecutable in ~/.pi/agent/claude-bridge.json.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveClaudeCodeExecutable(
|
|
105
|
+
modelId: string,
|
|
106
|
+
configured?: string,
|
|
107
|
+
deps: ClaudeExecutableDeps = {},
|
|
108
|
+
): ClaudeExecutableResolution {
|
|
109
|
+
const bundledVersion = deps.bundledVersion ?? readBundledClaudeCodeVersion;
|
|
110
|
+
const findOnPath = deps.findOnPath ?? (() => findExecutableOnPath("claude"));
|
|
111
|
+
const readVersion = deps.readVersion ?? readClaudeCliVersion;
|
|
112
|
+
const minText = minClaudeCodeVersionForModel(modelId);
|
|
113
|
+
const min = minText ? parseClaudeVersion(minText) : undefined;
|
|
114
|
+
|
|
115
|
+
if (configured) {
|
|
116
|
+
if (min) {
|
|
117
|
+
const version = readVersion(configured);
|
|
118
|
+
if (version && compareClaudeVersion(version, min) < 0) {
|
|
119
|
+
return {
|
|
120
|
+
source: "configured",
|
|
121
|
+
error: tooOldError(modelId, min, `provider.pathToClaudeCodeExecutable is ${formatClaudeVersion(version)} at ${configured}`),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { path: configured, source: "configured" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!min) return { source: "bundled" };
|
|
129
|
+
|
|
130
|
+
const bundled = bundledVersion();
|
|
131
|
+
if (bundled && compareClaudeVersion(bundled, min) >= 0) {
|
|
132
|
+
return { source: "bundled" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pathClaude = findOnPath();
|
|
136
|
+
if (pathClaude) {
|
|
137
|
+
const version = readVersion(pathClaude);
|
|
138
|
+
if (version && compareClaudeVersion(version, min) >= 0) {
|
|
139
|
+
return { path: pathClaude, source: "path" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (bundled) {
|
|
144
|
+
return { source: "bundled", error: tooOldError(modelId, min, `bundled CLI is ${formatClaudeVersion(bundled)}`) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Bundle version unknown and PATH missing/unreadable: let the SDK try.
|
|
148
|
+
return { source: "bundled" };
|
|
149
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { calculateCost, type AssistantMessage, type AssistantMessageEventStream, type Context, type ImageContent, type Model, type SimpleStreamOptions, type TextContent, type Tool, type UserMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import * as piAi from "@earendil-works/pi-ai";
|
|
3
3
|
import { getModels } from "@earendil-works/pi-ai/compat";
|
|
4
|
-
import {
|
|
4
|
+
import { type ExtensionAPI, type ExtensionContext, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { query, type EffortLevel, type SDKMessage, type SettingSource } from "@anthropic-ai/claude-agent-sdk";
|
|
6
6
|
import type { Base64ImageSource, ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
|
7
7
|
import { createSession, deleteSession, openSession, repairToolPairing } from "cc-session-io";
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { collectCarriedAttachments, placeCarriedAttachments, type CarriedAttachment } from "./attachments.js";
|
|
25
25
|
import { createToolServer } from "./mcp-server.js";
|
|
26
26
|
import { CC_CHILD_ENV, resolveClaudeChildEnv, type AnthropicAuthRegistry } from "./child-env.js";
|
|
27
|
+
import { resolveClaudeCodeExecutable } from "./claude-executable.js";
|
|
27
28
|
|
|
28
29
|
// Compat (#2): use factory if available (pi-ai ≥0.66), else fall back to constructor (gsd-pi etc.)
|
|
29
30
|
const _piAi = piAi as any;
|
|
@@ -87,7 +88,7 @@ function debug(...args: unknown[]) {
|
|
|
87
88
|
// Per-query CLI debug capture. When CLAUDE_BRIDGE_DEBUG=1, ask the Claude Code
|
|
88
89
|
// CLI subprocess to write its own debug log to a file we choose, and also
|
|
89
90
|
// forward its stderr into our debug stream. Drops straight into the real SDK's
|
|
90
|
-
// Options — see @anthropic-ai/claude-agent-sdk sdk.d.ts:
|
|
91
|
+
// Options — see @anthropic-ai/claude-agent-sdk sdk.d.ts:2091 (debug, debugFile,
|
|
91
92
|
// stderr). Without this, CC's internal view of the world is invisible to us
|
|
92
93
|
// and "No conversation found" / empty-error reports are unactionable.
|
|
93
94
|
let nextCliDebugSeq = 1;
|
|
@@ -365,18 +366,33 @@ function newAssistantOutput(model: Model<any>, text: string, stopReason: Assista
|
|
|
365
366
|
};
|
|
366
367
|
}
|
|
367
368
|
|
|
368
|
-
function
|
|
369
|
+
function extractStandalonePrompt(context: Context): string {
|
|
370
|
+
if (context.tools?.length) {
|
|
371
|
+
throw new Error(`standalone provider requests do not support tools (got ${context.tools.length})`);
|
|
372
|
+
}
|
|
373
|
+
const messages = context.messages;
|
|
369
374
|
if (messages.length !== 1 || messages[0].role !== "user") {
|
|
370
375
|
throw new Error(
|
|
371
|
-
`
|
|
376
|
+
`standalone provider request expected exactly 1 user message, got ${messages.length} ` +
|
|
372
377
|
`(${messages.map((m) => m.role).join(",")})`,
|
|
373
378
|
);
|
|
374
379
|
}
|
|
375
380
|
const promptText = extractUserPrompt(messages);
|
|
376
|
-
if (!promptText) throw new Error("
|
|
381
|
+
if (!promptText) throw new Error("standalone provider request prompt is empty");
|
|
377
382
|
return promptText;
|
|
378
383
|
}
|
|
379
384
|
|
|
385
|
+
/** Pi marks summaries and extension-owned nested completions as no-cache,
|
|
386
|
+
* standalone requests. They must never enter the resumable provider path: its
|
|
387
|
+
* prompt capture and shared Claude Code session belong to the interactive
|
|
388
|
+
* agent, not to an unrelated planner or summarizer. */
|
|
389
|
+
function isStandaloneRequest(context: Context, options?: SimpleStreamOptions): boolean {
|
|
390
|
+
return options?.cacheRetention === "none"
|
|
391
|
+
&& context.tools === undefined
|
|
392
|
+
&& context.messages.length === 1
|
|
393
|
+
&& context.messages[0]?.role === "user";
|
|
394
|
+
}
|
|
395
|
+
|
|
380
396
|
/** Failure text for an SDK result, or undefined when it succeeded. CC reports API failures
|
|
381
397
|
* (429 capacity, overload, prompt-too-long) with `is_error` on an otherwise success-shaped
|
|
382
398
|
* result; the dedicated error subtypes carry `errors` instead. */
|
|
@@ -406,13 +422,13 @@ function describeRateLimitFailure(rejection: { rateLimitType?: string; resetsAt?
|
|
|
406
422
|
return `Claude rate limit${kind}${resets}: ${failure}`;
|
|
407
423
|
}
|
|
408
424
|
|
|
409
|
-
function
|
|
425
|
+
function standaloneStreamFn(model: Model<any>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
410
426
|
const stream = newAssistantMessageEventStream();
|
|
411
|
-
void
|
|
427
|
+
void runStandaloneRequest(model, context, options, stream);
|
|
412
428
|
return stream;
|
|
413
429
|
}
|
|
414
430
|
|
|
415
|
-
async function
|
|
431
|
+
async function runStandaloneRequest(
|
|
416
432
|
model: Model<any>,
|
|
417
433
|
context: Context,
|
|
418
434
|
options: SimpleStreamOptions | undefined,
|
|
@@ -427,12 +443,20 @@ async function runIsolatedSummary(
|
|
|
427
443
|
};
|
|
428
444
|
|
|
429
445
|
try {
|
|
430
|
-
const promptText =
|
|
446
|
+
const promptText = extractStandalonePrompt(context);
|
|
431
447
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
432
|
-
const
|
|
433
|
-
const
|
|
448
|
+
const standaloneProviderSettings = loadConfig(cwd).provider;
|
|
449
|
+
const claudeExecutableResolution = resolveClaudeCodeExecutable(model.id, standaloneProviderSettings?.pathToClaudeCodeExecutable);
|
|
450
|
+
if (claudeExecutableResolution.error) throw new Error(claudeExecutableResolution.error);
|
|
451
|
+
const claudeExecutable = claudeExecutableResolution.path;
|
|
434
452
|
const cliModel = claudeCodeModelId(model, longContextSettings);
|
|
435
|
-
|
|
453
|
+
const effort = options?.reasoning
|
|
454
|
+
? ((model as any).thinkingLevelMap?.[options.reasoning] as EffortLevel | undefined)
|
|
455
|
+
?? REASONING_TO_EFFORT[options.reasoning]
|
|
456
|
+
: undefined;
|
|
457
|
+
const extraArgs: Record<string, string | null> = {};
|
|
458
|
+
if (effort || adaptiveThinkingAlwaysOn(model.id)) extraArgs["thinking-display"] = "summarized";
|
|
459
|
+
debug(`standalone: spawn model=${cliModel} registeredModel=${model.id} promptLen=${promptText.length} effort=${effort ?? "default"}`);
|
|
436
460
|
const childEnv = await resolveClaudeChildEnv(piModelRegistry);
|
|
437
461
|
|
|
438
462
|
sdkQuery = query({
|
|
@@ -449,8 +473,10 @@ async function runIsolatedSummary(
|
|
|
449
473
|
systemPrompt: context.systemPrompt,
|
|
450
474
|
model: cliModel,
|
|
451
475
|
maxTurns: 1,
|
|
476
|
+
extraArgs,
|
|
477
|
+
...(effort ? { effort } : {}),
|
|
452
478
|
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
453
|
-
...makeCliDebugOptions("
|
|
479
|
+
...makeCliDebugOptions("standalone"),
|
|
454
480
|
},
|
|
455
481
|
});
|
|
456
482
|
|
|
@@ -462,11 +488,12 @@ async function runIsolatedSummary(
|
|
|
462
488
|
let assistantText = "";
|
|
463
489
|
let finalText = "";
|
|
464
490
|
let errorText: string | undefined;
|
|
491
|
+
let resultUsage: Record<string, number | undefined> | undefined;
|
|
465
492
|
let firstEventLogged = false;
|
|
466
493
|
|
|
467
494
|
for await (const message of sdkQuery) {
|
|
468
495
|
if (!firstEventLogged) {
|
|
469
|
-
debug(`
|
|
496
|
+
debug(`standalone: first event type=${message.type}`);
|
|
470
497
|
firstEventLogged = true;
|
|
471
498
|
}
|
|
472
499
|
if (wasAborted) break;
|
|
@@ -476,15 +503,16 @@ async function runIsolatedSummary(
|
|
|
476
503
|
if (block.type === "text" && typeof block.text === "string") assistantText += block.text;
|
|
477
504
|
}
|
|
478
505
|
} else if (message.type === "result") {
|
|
479
|
-
logServedContextWindow("
|
|
506
|
+
logServedContextWindow("standalone", message, model);
|
|
480
507
|
errorText = resultErrorText(message);
|
|
508
|
+
resultUsage = (message as SDKMessage & { usage?: Record<string, number | undefined> }).usage;
|
|
481
509
|
if (!errorText && message.subtype === "success") finalText = message.result || assistantText;
|
|
482
510
|
}
|
|
483
511
|
}
|
|
484
512
|
|
|
485
513
|
if (wasAborted) {
|
|
486
514
|
const output = newAssistantOutput(model, "", "aborted", "Operation aborted");
|
|
487
|
-
debug("
|
|
515
|
+
debug("standalone: aborted");
|
|
488
516
|
stream.push({ type: "error", reason: "aborted", error: output });
|
|
489
517
|
stream.end();
|
|
490
518
|
return;
|
|
@@ -492,19 +520,21 @@ async function runIsolatedSummary(
|
|
|
492
520
|
|
|
493
521
|
const text = finalText || assistantText;
|
|
494
522
|
if (errorText || !text.trim()) {
|
|
495
|
-
const msg = errorText ?? "Claude Code
|
|
496
|
-
debug(`
|
|
523
|
+
const msg = errorText ?? "Claude Code standalone request returned empty text";
|
|
524
|
+
debug(`standalone: error ${msg}`);
|
|
497
525
|
stream.push({ type: "error", reason: "error", error: newAssistantOutput(model, "", "error", msg) });
|
|
498
526
|
stream.end();
|
|
499
527
|
return;
|
|
500
528
|
}
|
|
501
529
|
|
|
502
|
-
|
|
503
|
-
|
|
530
|
+
const output = newAssistantOutput(model, text, "stop");
|
|
531
|
+
if (resultUsage) updateUsage(output, resultUsage, model);
|
|
532
|
+
debug(`standalone: done textLen=${text.length}`);
|
|
533
|
+
stream.push({ type: "done", reason: "stop", message: output });
|
|
504
534
|
stream.end();
|
|
505
535
|
} catch (err) {
|
|
506
536
|
const msg = errorMessage(err);
|
|
507
|
-
debug("
|
|
537
|
+
debug("runStandaloneRequest threw; pushing terminal error", err);
|
|
508
538
|
stream.push({ type: "error", reason: "error", error: newAssistantOutput(model, "", "error", msg) });
|
|
509
539
|
stream.end();
|
|
510
540
|
} finally {
|
|
@@ -513,17 +543,6 @@ async function runIsolatedSummary(
|
|
|
513
543
|
}
|
|
514
544
|
}
|
|
515
545
|
|
|
516
|
-
function reinjectPriorCompactionFileOps(branchEntries: Array<{ type: string; details?: unknown }>, preparation: { fileOps: { read: Set<string>; edited: Set<string> } }): void {
|
|
517
|
-
const prior = [...branchEntries]
|
|
518
|
-
.reverse()
|
|
519
|
-
.find((entry): entry is CompactionEntry => entry.type === "compaction");
|
|
520
|
-
const details = prior?.details as { readFiles?: unknown; modifiedFiles?: unknown } | undefined;
|
|
521
|
-
if (!Array.isArray(details?.readFiles) || !Array.isArray(details?.modifiedFiles)) return;
|
|
522
|
-
for (const file of details.readFiles) preparation.fileOps.read.add(String(file));
|
|
523
|
-
for (const file of details.modifiedFiles) preparation.fileOps.edited.add(String(file));
|
|
524
|
-
debug(`compact takeover: re-injected prior file ops read=${details.readFiles.length} modified=${details.modifiedFiles.length}`);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
546
|
interface SyncResult {
|
|
528
547
|
sessionId: string | null;
|
|
529
548
|
preserveSharedSession?: boolean;
|
|
@@ -640,8 +659,8 @@ function syncSharedSession(
|
|
|
640
659
|
// the completion handler). Remove this branch and a subagent resumes — then
|
|
641
660
|
// overwrites — the parent's session.
|
|
642
661
|
//
|
|
643
|
-
// It is
|
|
644
|
-
//
|
|
662
|
+
// It is not the standalone no-cache path: runStandaloneRequest never calls
|
|
663
|
+
// syncSharedSession at all.
|
|
645
664
|
//
|
|
646
665
|
// Only reachable when needsRebuild is false — user-facing history rewrites
|
|
647
666
|
// (/compact, session_tree, /new, fork) always set needsRebuild or clear
|
|
@@ -719,7 +738,8 @@ export const __test = {
|
|
|
719
738
|
drainForAbort,
|
|
720
739
|
CC_CHILD_ENV,
|
|
721
740
|
buildMcpServers,
|
|
722
|
-
|
|
741
|
+
isStandaloneRequest,
|
|
742
|
+
extractStandalonePrompt,
|
|
723
743
|
};
|
|
724
744
|
|
|
725
745
|
// --- Provider helpers: tool name mapping ---
|
|
@@ -830,25 +850,6 @@ function reportLeaks(label: string): void {
|
|
|
830
850
|
);
|
|
831
851
|
}
|
|
832
852
|
|
|
833
|
-
/** What pi's branch summary means for the navigation it was asked for.
|
|
834
|
-
*
|
|
835
|
-
* Cancelling on failure matches pi's own path, which rethrows a summary error out
|
|
836
|
-
* of the navigation rather than moving without one. Separated from the event
|
|
837
|
-
* handler so this decision is testable without a Claude Code subprocess — driving
|
|
838
|
-
* `generateBranchSummary` itself would only be testing pi. */
|
|
839
|
-
function branchSummaryOutcome(result: BranchSummaryResult): { cancel: true } | { summary: { summary: string; details: unknown; usage?: BranchSummaryResult["usage"] } } {
|
|
840
|
-
if (result.aborted) return { cancel: true };
|
|
841
|
-
if (result.error) throw new Error(result.error);
|
|
842
|
-
debug(`session_before_tree: takeover complete summaryLen=${result.summary?.length ?? 0}`);
|
|
843
|
-
return {
|
|
844
|
-
summary: {
|
|
845
|
-
summary: result.summary ?? "",
|
|
846
|
-
details: { readFiles: result.readFiles ?? [], modifiedFiles: result.modifiedFiles ?? [] },
|
|
847
|
-
usage: result.usage,
|
|
848
|
-
},
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
|
|
852
853
|
function contextForToolResults(results: McpResult[]): QueryContext | undefined {
|
|
853
854
|
for (const result of results) {
|
|
854
855
|
const id = result.toolCallId;
|
|
@@ -1421,9 +1422,15 @@ function drainForAbort(c: QueryContext, promptStream: PromptStream): void {
|
|
|
1421
1422
|
c.releasePendingToolCalls("Operation aborted");
|
|
1422
1423
|
}
|
|
1423
1424
|
|
|
1424
|
-
/** Provider entry point. Pi calls this for
|
|
1425
|
-
*
|
|
1425
|
+
/** Provider entry point. Pi calls this for normal agent turns, but also for
|
|
1426
|
+
* standalone no-cache completions made by compaction and extensions. Route the
|
|
1427
|
+
* latter before touching prompt captures or resumable-session state. */
|
|
1426
1428
|
function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
1429
|
+
if (isStandaloneRequest(context, options)) {
|
|
1430
|
+
debug(`provider: routing standalone cacheRetention=none request to isolated subprocess`);
|
|
1431
|
+
return standaloneStreamFn(model, context, options);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1427
1434
|
showStartupNoticeOnce();
|
|
1428
1435
|
const stream = newAssistantMessageEventStream();
|
|
1429
1436
|
|
|
@@ -1492,6 +1499,19 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1492
1499
|
const queryCtx = isReentrant ? new QueryContext() : ctx();
|
|
1493
1500
|
debug(`provider: fresh query setup, isReentrant=${isReentrant}, activeContexts=${activeQueryContexts.size}`);
|
|
1494
1501
|
|
|
1502
|
+
// Fail before claiming a stream if this model needs a newer CLI than we have.
|
|
1503
|
+
const claudeExecutableResolution = resolveClaudeCodeExecutable(model.id, providerSettings.pathToClaudeCodeExecutable);
|
|
1504
|
+
if (claudeExecutableResolution.error) {
|
|
1505
|
+
debug(`provider: ${claudeExecutableResolution.error}`);
|
|
1506
|
+
stream.push({ type: "error", reason: "error", error: newAssistantOutput(model, "", "error", claudeExecutableResolution.error) });
|
|
1507
|
+
stream.end();
|
|
1508
|
+
return stream;
|
|
1509
|
+
}
|
|
1510
|
+
const claudeExecutable = claudeExecutableResolution.path;
|
|
1511
|
+
if (claudeExecutableResolution.source === "path") {
|
|
1512
|
+
debug(`provider: using PATH claude ${claudeExecutable} (bundled CLI is too old for ${model.id})`);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1495
1515
|
// Resolved first: an unaccountable system prompt throws, and doing that before
|
|
1496
1516
|
// anything is claimed or reset leaves no half-built query behind — in particular
|
|
1497
1517
|
// no stream claimed on the shared context that nobody will ever end.
|
|
@@ -1561,7 +1581,6 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1561
1581
|
// programmatically and ignore filesystem MCP entries — applied unconditionally because
|
|
1562
1582
|
// settingSources is left at CC's default, which loads all sources.
|
|
1563
1583
|
const strictMcpConfigEnabled = providerSettings.strictMcpConfig !== false;
|
|
1564
|
-
const claudeExecutable = providerSettings.pathToClaudeCodeExecutable;
|
|
1565
1584
|
|
|
1566
1585
|
// Prefer the model's own thinkingLevelMap when present (pi-ai 0.72+ ships
|
|
1567
1586
|
// per-model overrides — e.g. opus-4-7 wants xhigh→xhigh, not xhigh→max).
|
|
@@ -1813,39 +1832,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1813
1832
|
clearSession("session_shutdown");
|
|
1814
1833
|
});
|
|
1815
1834
|
|
|
1816
|
-
pi.on("session_before_compact", async (event, ctx) => {
|
|
1817
|
-
if (ctx.model?.baseUrl !== "claude-bridge") return undefined;
|
|
1818
|
-
debug(
|
|
1819
|
-
`session_before_compact: takeover reason=${event.reason} willRetry=${event.willRetry} ` +
|
|
1820
|
-
`isSplitTurn=${event.preparation.isSplitTurn} messages=${event.preparation.messagesToSummarize.length} ` +
|
|
1821
|
-
`turnPrefix=${event.preparation.turnPrefixMessages.length}`,
|
|
1822
|
-
);
|
|
1823
|
-
try {
|
|
1824
|
-
reinjectPriorCompactionFileOps(event.branchEntries, event.preparation);
|
|
1825
|
-
const compaction = await compact(
|
|
1826
|
-
event.preparation,
|
|
1827
|
-
ctx.model,
|
|
1828
|
-
undefined,
|
|
1829
|
-
undefined,
|
|
1830
|
-
event.customInstructions,
|
|
1831
|
-
event.signal,
|
|
1832
|
-
undefined,
|
|
1833
|
-
isolatedStreamFn,
|
|
1834
|
-
undefined,
|
|
1835
|
-
);
|
|
1836
|
-
debug(`session_before_compact: takeover complete summaryLen=${compaction.summary.length}`);
|
|
1837
|
-
return { compaction };
|
|
1838
|
-
} catch (err) {
|
|
1839
|
-
const msg = errorMessage(err);
|
|
1840
|
-
debug("session_before_compact: takeover failed; cancelling to avoid native compact fallback", err);
|
|
1841
|
-
ctx.ui?.notify?.(
|
|
1842
|
-
`pi-claude-agent-sdk compact failed (${msg}); cancelled to avoid known hang. Retry, switch model, or reduce context.`,
|
|
1843
|
-
"error",
|
|
1844
|
-
);
|
|
1845
|
-
return { cancel: true };
|
|
1846
|
-
}
|
|
1847
|
-
});
|
|
1848
|
-
|
|
1849
1835
|
// pi /compact and session-tree navigation (rewind / fork-at-point /
|
|
1850
1836
|
// branch switch) both mutate pi's messages array out from under the
|
|
1851
1837
|
// bridge. syncSharedSession's REUSE check would otherwise see
|
|
@@ -1862,38 +1848,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1862
1848
|
pi.on("session_compact", (event) => markRebuild(`session_compact:${event.reason}:willRetry=${event.willRetry}`));
|
|
1863
1849
|
pi.on("session_tree", () => markRebuild("session_tree"));
|
|
1864
1850
|
|
|
1865
|
-
// Branch summarization — rewind or fork-at-point with "summarize" — is the other
|
|
1866
|
-
// place pi asks the model for a summary, and unlike compaction it runs through
|
|
1867
|
-
// the *agent's* stream function (agent-session passes `streamFn:
|
|
1868
|
-
// this.agent.streamFunction`). On a bridge model that reaches this provider
|
|
1869
|
-
// carrying pi's internal summarization prompt, which no `before_agent_start`
|
|
1870
|
-
// ever recorded, so the prompt-capture resolver has nothing to resolve it to.
|
|
1871
|
-
// Take it over the way compaction is taken over: the summary runs as its own
|
|
1872
|
-
// Claude Code subprocess, never touching the live session or the resolver.
|
|
1873
|
-
pi.on("session_before_tree", async (event, ctx) => {
|
|
1874
|
-
if (ctx.model?.baseUrl !== "claude-bridge") return undefined;
|
|
1875
|
-
const { entriesToSummarize, userWantsSummary, customInstructions, replaceInstructions } = event.preparation;
|
|
1876
|
-
if (!userWantsSummary || entriesToSummarize.length === 0) return undefined;
|
|
1877
|
-
debug(`session_before_tree: takeover entries=${entriesToSummarize.length} target=${event.preparation.targetId.slice(0, 8)}`);
|
|
1878
|
-
try {
|
|
1879
|
-
const result = await generateBranchSummary(entriesToSummarize, {
|
|
1880
|
-
model: ctx.model,
|
|
1881
|
-
signal: event.signal,
|
|
1882
|
-
customInstructions,
|
|
1883
|
-
replaceInstructions,
|
|
1884
|
-
streamFn: isolatedStreamFn,
|
|
1885
|
-
});
|
|
1886
|
-
return branchSummaryOutcome(result);
|
|
1887
|
-
} catch (err) {
|
|
1888
|
-
debug("session_before_tree: takeover failed; cancelling navigation", err);
|
|
1889
|
-
ctx.ui?.notify?.(
|
|
1890
|
-
`pi-claude-agent-sdk branch summary failed (${errorMessage(err)}); navigation cancelled.`,
|
|
1891
|
-
"error",
|
|
1892
|
-
);
|
|
1893
|
-
return { cancel: true };
|
|
1894
|
-
}
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
1851
|
// --- Provider ---
|
|
1898
1852
|
//
|
|
1899
1853
|
// Guard against re-registration when the module is loaded multiple times
|
package/src/models.ts
CHANGED
|
@@ -120,6 +120,14 @@ export function thinkingBoundToPrefix(modelId: string): boolean {
|
|
|
120
120
|
return modelId === "claude-fable-5-1" || modelId.startsWith("claude-fable-5-1[");
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** Minimum Claude Code CLI version that will accept this model. Undefined
|
|
124
|
+
* means the SDK's bundled CLI is fine. Fable 5.1 400s on 2.1.141 with
|
|
125
|
+
* "version 2.1.251 or newer is required". */
|
|
126
|
+
export function minClaudeCodeVersionForModel(modelId: string): string | undefined {
|
|
127
|
+
if (modelId === "claude-fable-5-1" || modelId.startsWith("claude-fable-5-1[")) return "2.1.251";
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
export function resolveModel<T extends { id: string }>(models: T[], input: string): T | undefined {
|
|
124
132
|
const lower = input.toLowerCase();
|
|
125
133
|
// Exact match first: otherwise `claude-fable-5` would hit `claude-fable-5-1`
|