pi-claude-agent-sdk 0.8.5 → 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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +59 -120
package/README.md CHANGED
@@ -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 `compact-summary`. Useful when a resume fails or CC misbehaves internally — shows the CLI's own view of session loading, API requests, and tool calls.
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.5",
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": [
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 { compact, generateBranchSummary, type BranchSummaryResult, type CompactionEntry, type ExtensionAPI, type ExtensionContext, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
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";
@@ -366,18 +366,33 @@ function newAssistantOutput(model: Model<any>, text: string, stopReason: Assista
366
366
  };
367
367
  }
368
368
 
369
- function extractIsolatedSummaryPrompt(messages: Context["messages"]): string {
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;
370
374
  if (messages.length !== 1 || messages[0].role !== "user") {
371
375
  throw new Error(
372
- `isolatedStreamFn: expected exactly 1 user message, got ${messages.length} ` +
376
+ `standalone provider request expected exactly 1 user message, got ${messages.length} ` +
373
377
  `(${messages.map((m) => m.role).join(",")})`,
374
378
  );
375
379
  }
376
380
  const promptText = extractUserPrompt(messages);
377
- if (!promptText) throw new Error("isolatedStreamFn: summarization prompt is empty");
381
+ if (!promptText) throw new Error("standalone provider request prompt is empty");
378
382
  return promptText;
379
383
  }
380
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
+
381
396
  /** Failure text for an SDK result, or undefined when it succeeded. CC reports API failures
382
397
  * (429 capacity, overload, prompt-too-long) with `is_error` on an otherwise success-shaped
383
398
  * result; the dedicated error subtypes carry `errors` instead. */
@@ -407,13 +422,13 @@ function describeRateLimitFailure(rejection: { rateLimitType?: string; resetsAt?
407
422
  return `Claude rate limit${kind}${resets}: ${failure}`;
408
423
  }
409
424
 
410
- function isolatedStreamFn(model: Model<any>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
425
+ function standaloneStreamFn(model: Model<any>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
411
426
  const stream = newAssistantMessageEventStream();
412
- void runIsolatedSummary(model, context, options, stream);
427
+ void runStandaloneRequest(model, context, options, stream);
413
428
  return stream;
414
429
  }
415
430
 
416
- async function runIsolatedSummary(
431
+ async function runStandaloneRequest(
417
432
  model: Model<any>,
418
433
  context: Context,
419
434
  options: SimpleStreamOptions | undefined,
@@ -428,14 +443,20 @@ async function runIsolatedSummary(
428
443
  };
429
444
 
430
445
  try {
431
- const promptText = extractIsolatedSummaryPrompt(context.messages);
446
+ const promptText = extractStandalonePrompt(context);
432
447
  const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
433
- const compactProviderSettings = loadConfig(cwd).provider;
434
- const claudeExecutableResolution = resolveClaudeCodeExecutable(model.id, compactProviderSettings?.pathToClaudeCodeExecutable);
448
+ const standaloneProviderSettings = loadConfig(cwd).provider;
449
+ const claudeExecutableResolution = resolveClaudeCodeExecutable(model.id, standaloneProviderSettings?.pathToClaudeCodeExecutable);
435
450
  if (claudeExecutableResolution.error) throw new Error(claudeExecutableResolution.error);
436
451
  const claudeExecutable = claudeExecutableResolution.path;
437
452
  const cliModel = claudeCodeModelId(model, longContextSettings);
438
- debug(`compact summary: spawn model=${cliModel} registeredModel=${model.id} promptLen=${promptText.length}`);
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"}`);
439
460
  const childEnv = await resolveClaudeChildEnv(piModelRegistry);
440
461
 
441
462
  sdkQuery = query({
@@ -452,8 +473,10 @@ async function runIsolatedSummary(
452
473
  systemPrompt: context.systemPrompt,
453
474
  model: cliModel,
454
475
  maxTurns: 1,
476
+ extraArgs,
477
+ ...(effort ? { effort } : {}),
455
478
  ...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
456
- ...makeCliDebugOptions("compact-summary"),
479
+ ...makeCliDebugOptions("standalone"),
457
480
  },
458
481
  });
459
482
 
@@ -465,11 +488,12 @@ async function runIsolatedSummary(
465
488
  let assistantText = "";
466
489
  let finalText = "";
467
490
  let errorText: string | undefined;
491
+ let resultUsage: Record<string, number | undefined> | undefined;
468
492
  let firstEventLogged = false;
469
493
 
470
494
  for await (const message of sdkQuery) {
471
495
  if (!firstEventLogged) {
472
- debug(`compact summary: first event type=${message.type}`);
496
+ debug(`standalone: first event type=${message.type}`);
473
497
  firstEventLogged = true;
474
498
  }
475
499
  if (wasAborted) break;
@@ -479,15 +503,16 @@ async function runIsolatedSummary(
479
503
  if (block.type === "text" && typeof block.text === "string") assistantText += block.text;
480
504
  }
481
505
  } else if (message.type === "result") {
482
- logServedContextWindow("compact summary", message, model);
506
+ logServedContextWindow("standalone", message, model);
483
507
  errorText = resultErrorText(message);
508
+ resultUsage = (message as SDKMessage & { usage?: Record<string, number | undefined> }).usage;
484
509
  if (!errorText && message.subtype === "success") finalText = message.result || assistantText;
485
510
  }
486
511
  }
487
512
 
488
513
  if (wasAborted) {
489
514
  const output = newAssistantOutput(model, "", "aborted", "Operation aborted");
490
- debug("compact summary: aborted");
515
+ debug("standalone: aborted");
491
516
  stream.push({ type: "error", reason: "aborted", error: output });
492
517
  stream.end();
493
518
  return;
@@ -495,19 +520,21 @@ async function runIsolatedSummary(
495
520
 
496
521
  const text = finalText || assistantText;
497
522
  if (errorText || !text.trim()) {
498
- const msg = errorText ?? "Claude Code summary returned empty text";
499
- debug(`compact summary: error ${msg}`);
523
+ const msg = errorText ?? "Claude Code standalone request returned empty text";
524
+ debug(`standalone: error ${msg}`);
500
525
  stream.push({ type: "error", reason: "error", error: newAssistantOutput(model, "", "error", msg) });
501
526
  stream.end();
502
527
  return;
503
528
  }
504
529
 
505
- debug(`compact summary: done textLen=${text.length}`);
506
- stream.push({ type: "done", reason: "stop", message: newAssistantOutput(model, text, "stop") });
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 });
507
534
  stream.end();
508
535
  } catch (err) {
509
536
  const msg = errorMessage(err);
510
- debug("runIsolatedSummary threw; pushing terminal error", err);
537
+ debug("runStandaloneRequest threw; pushing terminal error", err);
511
538
  stream.push({ type: "error", reason: "error", error: newAssistantOutput(model, "", "error", msg) });
512
539
  stream.end();
513
540
  } finally {
@@ -516,17 +543,6 @@ async function runIsolatedSummary(
516
543
  }
517
544
  }
518
545
 
519
- function reinjectPriorCompactionFileOps(branchEntries: Array<{ type: string; details?: unknown }>, preparation: { fileOps: { read: Set<string>; edited: Set<string> } }): void {
520
- const prior = [...branchEntries]
521
- .reverse()
522
- .find((entry): entry is CompactionEntry => entry.type === "compaction");
523
- const details = prior?.details as { readFiles?: unknown; modifiedFiles?: unknown } | undefined;
524
- if (!Array.isArray(details?.readFiles) || !Array.isArray(details?.modifiedFiles)) return;
525
- for (const file of details.readFiles) preparation.fileOps.read.add(String(file));
526
- for (const file of details.modifiedFiles) preparation.fileOps.edited.add(String(file));
527
- debug(`compact takeover: re-injected prior file ops read=${details.readFiles.length} modified=${details.modifiedFiles.length}`);
528
- }
529
-
530
546
  interface SyncResult {
531
547
  sessionId: string | null;
532
548
  preserveSharedSession?: boolean;
@@ -643,8 +659,8 @@ function syncSharedSession(
643
659
  // the completion handler). Remove this branch and a subagent resumes — then
644
660
  // overwrites — the parent's session.
645
661
  //
646
- // It is NOT, despite an earlier comment here, the isolated compact-summary
647
- // path: runIsolatedSummary never calls syncSharedSession at all.
662
+ // It is not the standalone no-cache path: runStandaloneRequest never calls
663
+ // syncSharedSession at all.
648
664
  //
649
665
  // Only reachable when needsRebuild is false — user-facing history rewrites
650
666
  // (/compact, session_tree, /new, fork) always set needsRebuild or clear
@@ -722,7 +738,8 @@ export const __test = {
722
738
  drainForAbort,
723
739
  CC_CHILD_ENV,
724
740
  buildMcpServers,
725
- branchSummaryOutcome,
741
+ isStandaloneRequest,
742
+ extractStandalonePrompt,
726
743
  };
727
744
 
728
745
  // --- Provider helpers: tool name mapping ---
@@ -833,25 +850,6 @@ function reportLeaks(label: string): void {
833
850
  );
834
851
  }
835
852
 
836
- /** What pi's branch summary means for the navigation it was asked for.
837
- *
838
- * Cancelling on failure matches pi's own path, which rethrows a summary error out
839
- * of the navigation rather than moving without one. Separated from the event
840
- * handler so this decision is testable without a Claude Code subprocess — driving
841
- * `generateBranchSummary` itself would only be testing pi. */
842
- function branchSummaryOutcome(result: BranchSummaryResult): { cancel: true } | { summary: { summary: string; details: unknown; usage?: BranchSummaryResult["usage"] } } {
843
- if (result.aborted) return { cancel: true };
844
- if (result.error) throw new Error(result.error);
845
- debug(`session_before_tree: takeover complete summaryLen=${result.summary?.length ?? 0}`);
846
- return {
847
- summary: {
848
- summary: result.summary ?? "",
849
- details: { readFiles: result.readFiles ?? [], modifiedFiles: result.modifiedFiles ?? [] },
850
- usage: result.usage,
851
- },
852
- };
853
- }
854
-
855
853
  function contextForToolResults(results: McpResult[]): QueryContext | undefined {
856
854
  for (const result of results) {
857
855
  const id = result.toolCallId;
@@ -1424,9 +1422,15 @@ function drainForAbort(c: QueryContext, promptStream: PromptStream): void {
1424
1422
  c.releasePendingToolCalls("Operation aborted");
1425
1423
  }
1426
1424
 
1427
- /** Provider entry point. Pi calls this for each new prompt and each tool result.
1428
- * Two cases: tool result delivery (active query) or fresh query. */
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. */
1429
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
+
1430
1434
  showStartupNoticeOnce();
1431
1435
  const stream = newAssistantMessageEventStream();
1432
1436
 
@@ -1828,39 +1832,6 @@ export default function (pi: ExtensionAPI) {
1828
1832
  clearSession("session_shutdown");
1829
1833
  });
1830
1834
 
1831
- pi.on("session_before_compact", async (event, ctx) => {
1832
- if (ctx.model?.baseUrl !== "claude-bridge") return undefined;
1833
- debug(
1834
- `session_before_compact: takeover reason=${event.reason} willRetry=${event.willRetry} ` +
1835
- `isSplitTurn=${event.preparation.isSplitTurn} messages=${event.preparation.messagesToSummarize.length} ` +
1836
- `turnPrefix=${event.preparation.turnPrefixMessages.length}`,
1837
- );
1838
- try {
1839
- reinjectPriorCompactionFileOps(event.branchEntries, event.preparation);
1840
- const compaction = await compact(
1841
- event.preparation,
1842
- ctx.model,
1843
- undefined,
1844
- undefined,
1845
- event.customInstructions,
1846
- event.signal,
1847
- undefined,
1848
- isolatedStreamFn,
1849
- undefined,
1850
- );
1851
- debug(`session_before_compact: takeover complete summaryLen=${compaction.summary.length}`);
1852
- return { compaction };
1853
- } catch (err) {
1854
- const msg = errorMessage(err);
1855
- debug("session_before_compact: takeover failed; cancelling to avoid native compact fallback", err);
1856
- ctx.ui?.notify?.(
1857
- `pi-claude-agent-sdk compact failed (${msg}); cancelled to avoid known hang. Retry, switch model, or reduce context.`,
1858
- "error",
1859
- );
1860
- return { cancel: true };
1861
- }
1862
- });
1863
-
1864
1835
  // pi /compact and session-tree navigation (rewind / fork-at-point /
1865
1836
  // branch switch) both mutate pi's messages array out from under the
1866
1837
  // bridge. syncSharedSession's REUSE check would otherwise see
@@ -1877,38 +1848,6 @@ export default function (pi: ExtensionAPI) {
1877
1848
  pi.on("session_compact", (event) => markRebuild(`session_compact:${event.reason}:willRetry=${event.willRetry}`));
1878
1849
  pi.on("session_tree", () => markRebuild("session_tree"));
1879
1850
 
1880
- // Branch summarization — rewind or fork-at-point with "summarize" — is the other
1881
- // place pi asks the model for a summary, and unlike compaction it runs through
1882
- // the *agent's* stream function (agent-session passes `streamFn:
1883
- // this.agent.streamFunction`). On a bridge model that reaches this provider
1884
- // carrying pi's internal summarization prompt, which no `before_agent_start`
1885
- // ever recorded, so the prompt-capture resolver has nothing to resolve it to.
1886
- // Take it over the way compaction is taken over: the summary runs as its own
1887
- // Claude Code subprocess, never touching the live session or the resolver.
1888
- pi.on("session_before_tree", async (event, ctx) => {
1889
- if (ctx.model?.baseUrl !== "claude-bridge") return undefined;
1890
- const { entriesToSummarize, userWantsSummary, customInstructions, replaceInstructions } = event.preparation;
1891
- if (!userWantsSummary || entriesToSummarize.length === 0) return undefined;
1892
- debug(`session_before_tree: takeover entries=${entriesToSummarize.length} target=${event.preparation.targetId.slice(0, 8)}`);
1893
- try {
1894
- const result = await generateBranchSummary(entriesToSummarize, {
1895
- model: ctx.model,
1896
- signal: event.signal,
1897
- customInstructions,
1898
- replaceInstructions,
1899
- streamFn: isolatedStreamFn,
1900
- });
1901
- return branchSummaryOutcome(result);
1902
- } catch (err) {
1903
- debug("session_before_tree: takeover failed; cancelling navigation", err);
1904
- ctx.ui?.notify?.(
1905
- `pi-claude-agent-sdk branch summary failed (${errorMessage(err)}); navigation cancelled.`,
1906
- "error",
1907
- );
1908
- return { cancel: true };
1909
- }
1910
- });
1911
-
1912
1851
  // --- Provider ---
1913
1852
  //
1914
1853
  // Guard against re-registration when the module is loaded multiple times