@psnext/slingcli 2.5.20260722-2 → 2.5.20260722-3

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 (100) hide show
  1. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +9 -7
  2. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +25 -23
  3. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +32 -19
  4. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +6 -7
  5. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +75 -26
  6. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/utils.js +6 -17
  7. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.js +56 -4
  8. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/memory-storage.js +56 -4
  9. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo-utils.js +2 -2
  10. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +30 -14
  11. package/node_modules/@earendil-works/pi-agent-core/dist/index.js +4 -2
  12. package/node_modules/@earendil-works/pi-agent-core/dist/stream-fn.js +17 -0
  13. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  14. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +2 -7
  15. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +14 -2
  16. package/node_modules/@earendil-works/pi-ai/dist/api/pi-messages.js +2 -4
  17. package/node_modules/@earendil-works/pi-ai/dist/auth/helpers.js +3 -2
  18. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.js +249 -0
  19. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/load.js +5 -0
  20. package/node_modules/@earendil-works/pi-ai/dist/bun-oauth.js +2 -0
  21. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +2 -0
  22. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +60 -0
  23. package/node_modules/@earendil-works/pi-ai/dist/index.js +2 -0
  24. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +4 -0
  25. package/node_modules/@earendil-works/pi-ai/dist/providers/all.js +8 -0
  26. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +3 -2
  27. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -0
  28. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cerebras.json +1 -1
  29. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  30. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  31. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  32. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  33. package/node_modules/@earendil-works/pi-ai/dist/providers/data/groq.json +1 -1
  34. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  35. package/node_modules/@earendil-works/pi-ai/dist/providers/data/kimi-coding.json +1 -1
  36. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
  37. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
  38. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
  39. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
  40. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  41. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  42. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  43. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-cn.json +1 -0
  44. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -0
  45. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  46. package/node_modules/@earendil-works/pi-ai/dist/providers/data/xai.json +1 -1
  47. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.js +10 -2
  48. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.js +2 -0
  49. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-cn.js +15 -0
  50. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-cn.models.js +5 -0
  51. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan.js +15 -0
  52. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan.models.js +5 -0
  53. package/node_modules/@earendil-works/pi-ai/dist/providers/radius-config.js +18 -14
  54. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +3 -0
  55. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +79 -0
  56. package/node_modules/@earendil-works/pi-ai/dist/utils/text.js +10 -0
  57. package/node_modules/@earendil-works/{pi-agent-core/dist/harness/session → pi-ai/dist/utils}/uuid.js +4 -8
  58. package/node_modules/@earendil-works/pi-ai/package.json +8 -5
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +2 -0
  60. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +72 -57
  61. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/branch-summarization.js +8 -11
  62. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +63 -26
  63. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/utils.js +6 -17
  64. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +5 -0
  65. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +2 -0
  66. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.js +18 -15
  67. package/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js +1 -1
  68. package/node_modules/@earendil-works/pi-coding-agent/dist/core/remote-catalog-provider.js +26 -7
  69. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +6 -2
  70. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +100 -24
  71. package/node_modules/@earendil-works/pi-coding-agent/dist/core/usage-totals.js +52 -0
  72. package/node_modules/@earendil-works/pi-coding-agent/dist/extensions/llama/client.js +3 -1
  73. package/node_modules/@earendil-works/pi-coding-agent/dist/extensions/llama/index.js +3 -3
  74. package/node_modules/@earendil-works/pi-coding-agent/dist/index.js +1 -1
  75. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +5 -1
  76. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js +20 -21
  77. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +69 -25
  78. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +7 -0
  79. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +4 -0
  80. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +16 -31
  81. package/node_modules/@earendil-works/pi-coding-agent/package.json +4 -4
  82. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/cli.js +5 -5
  83. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/config.js +9 -9
  84. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/client.js +1 -1
  85. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/server.js +1 -1
  86. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/radius.js +8 -8
  87. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/rpc-process.js +1 -1
  88. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/serve.js +3 -3
  89. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/storage.js +7 -7
  90. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/supervisor.js +2 -2
  91. package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/package.json +9 -6
  92. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +16 -10
  93. package/node_modules/@earendil-works/pi-tui/dist/tui.js +4 -2
  94. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  95. package/package.json +7 -7
  96. package/slingshot/index.js +253 -250
  97. /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/handler.js +0 -0
  98. /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/index.js +0 -0
  99. /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/protocol.js +0 -0
  100. /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/types.js +0 -0
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname } from "node:path";
17
+ import { contentText } from "@earendil-works/pi-ai";
17
18
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
18
19
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
19
20
  import { stripFrontmatter } from "../utils/frontmatter.js";
@@ -35,6 +36,7 @@ import { buildSystemPrompt } from "./system-prompt.js";
35
36
  import { createLocalBashOperations } from "./tools/bash.js";
36
37
  import { createAllToolDefinitions } from "./tools/index.js";
37
38
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
39
+ import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
38
40
  /**
39
41
  * Parse a skill block from message text.
40
42
  * Returns null if the text doesn't contain a skill block.
@@ -188,7 +190,7 @@ export class AgentSession {
188
190
  throw new Error(formatNoApiKeyFoundMessage(model.provider));
189
191
  }
190
192
  async _getSummarizationRequestAuth(model) {
191
- if (this.agent.streamFn === streamSimple) {
193
+ if (this.agent.streamFunction === streamSimple) {
192
194
  return this._getRequiredRequestAuth(model);
193
195
  }
194
196
  try {
@@ -243,6 +245,7 @@ export class AgentSession {
243
245
  content: result.content,
244
246
  details: result.details,
245
247
  isError,
248
+ usage: result.usage,
246
249
  });
247
250
  if (!hookResult) {
248
251
  return undefined;
@@ -251,6 +254,7 @@ export class AgentSession {
251
254
  content: hookResult.content,
252
255
  details: hookResult.details,
253
256
  isError: hookResult.isError ?? isError,
257
+ usage: hookResult.usage,
254
258
  };
255
259
  };
256
260
  }
@@ -325,7 +329,7 @@ export class AgentSession {
325
329
  // This ensures the UI sees the updated queue state
326
330
  if (event.type === "message_start" && event.message.role === "user") {
327
331
  this._overflowRecoveryAttempted = false;
328
- const messageText = this._getUserMessageText(event.message);
332
+ const messageText = contentText(event.message.content, "");
329
333
  if (messageText) {
330
334
  // Check steering queue first
331
335
  const steeringIndex = this._steeringMessages.indexOf(messageText);
@@ -394,16 +398,6 @@ export class AgentSession {
394
398
  }
395
399
  return false;
396
400
  }
397
- /** Extract text content from a message */
398
- _getUserMessageText(message) {
399
- if (message.role !== "user")
400
- return "";
401
- const content = message.content;
402
- if (typeof content === "string")
403
- return content;
404
- const textBlocks = content.filter((c) => c.type === "text");
405
- return textBlocks.map((c) => c.text).join("");
406
- }
407
401
  /** Find the last assistant message in agent state (including aborted ones) */
408
402
  _findLastAssistantMessage() {
409
403
  const messages = this.agent.state.messages;
@@ -570,7 +564,7 @@ export class AgentSession {
570
564
  catch {
571
565
  // Dispose must succeed even if an abort hook throws.
572
566
  }
573
- this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured sling or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
567
+ this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
574
568
  this._disconnectFromAgent();
575
569
  this._eventListeners = [];
576
570
  cleanupSessionResources(this.sessionId);
@@ -1414,26 +1408,29 @@ export class AgentSession {
1414
1408
  let summary;
1415
1409
  let firstKeptEntryId;
1416
1410
  let tokensBefore;
1411
+ let usage;
1417
1412
  let details;
1418
1413
  if (extensionCompaction) {
1419
1414
  // Extension provided compaction content
1420
1415
  summary = extensionCompaction.summary;
1421
1416
  firstKeptEntryId = extensionCompaction.firstKeptEntryId;
1422
1417
  tokensBefore = extensionCompaction.tokensBefore;
1418
+ usage = extensionCompaction.usage;
1423
1419
  details = extensionCompaction.details;
1424
1420
  }
1425
1421
  else {
1426
1422
  // Generate compaction result
1427
- const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, env);
1423
+ const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
1428
1424
  summary = result.summary;
1429
1425
  firstKeptEntryId = result.firstKeptEntryId;
1430
1426
  tokensBefore = result.tokensBefore;
1427
+ usage = result.usage;
1431
1428
  details = result.details;
1432
1429
  }
1433
1430
  if (this._compactionAbortController.signal.aborted) {
1434
1431
  throw new Error("Compaction cancelled");
1435
1432
  }
1436
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1433
+ this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
1437
1434
  const newEntries = this.sessionManager.getEntries();
1438
1435
  const sessionContext = this.sessionManager.buildSessionContext();
1439
1436
  this.agent.state.messages = sessionContext.messages;
@@ -1454,6 +1451,7 @@ export class AgentSession {
1454
1451
  firstKeptEntryId,
1455
1452
  tokensBefore,
1456
1453
  estimatedTokensAfter,
1454
+ usage,
1457
1455
  details,
1458
1456
  };
1459
1457
  this._emit({
@@ -1600,7 +1598,7 @@ export class AgentSession {
1600
1598
  let apiKey;
1601
1599
  let headers;
1602
1600
  let env;
1603
- if (this.agent.streamFn === streamSimple) {
1601
+ if (this.agent.streamFunction === streamSimple) {
1604
1602
  const authResult = await this._modelRuntime.getAuth(this.model);
1605
1603
  if (!authResult?.auth.apiKey)
1606
1604
  return false;
@@ -1649,20 +1647,23 @@ export class AgentSession {
1649
1647
  let summary;
1650
1648
  let firstKeptEntryId;
1651
1649
  let tokensBefore;
1650
+ let usage;
1652
1651
  let details;
1653
1652
  if (extensionCompaction) {
1654
1653
  // Extension provided compaction content
1655
1654
  summary = extensionCompaction.summary;
1656
1655
  firstKeptEntryId = extensionCompaction.firstKeptEntryId;
1657
1656
  tokensBefore = extensionCompaction.tokensBefore;
1657
+ usage = extensionCompaction.usage;
1658
1658
  details = extensionCompaction.details;
1659
1659
  }
1660
1660
  else {
1661
1661
  // Generate compaction result
1662
- const compactResult = await compact(preparation, this.model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, env);
1662
+ const compactResult = await compact(preparation, this.model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
1663
1663
  summary = compactResult.summary;
1664
1664
  firstKeptEntryId = compactResult.firstKeptEntryId;
1665
1665
  tokensBefore = compactResult.tokensBefore;
1666
+ usage = compactResult.usage;
1666
1667
  details = compactResult.details;
1667
1668
  }
1668
1669
  if (this._autoCompactionAbortController.signal.aborted) {
@@ -1675,7 +1676,7 @@ export class AgentSession {
1675
1676
  });
1676
1677
  return false;
1677
1678
  }
1678
- this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1679
+ this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
1679
1680
  const newEntries = this.sessionManager.getEntries();
1680
1681
  const sessionContext = this.sessionManager.buildSessionContext();
1681
1682
  this.agent.state.messages = sessionContext.messages;
@@ -1696,6 +1697,7 @@ export class AgentSession {
1696
1697
  firstKeptEntryId,
1697
1698
  tokensBefore,
1698
1699
  estimatedTokensAfter,
1700
+ usage,
1699
1701
  details,
1700
1702
  };
1701
1703
  this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
@@ -2086,6 +2088,34 @@ export class AgentSession {
2086
2088
  return false;
2087
2089
  return isRetryableAssistantError(message);
2088
2090
  }
2091
+ /**
2092
+ * Retry policy + callbacks shared by compaction and branch-summary summarization calls.
2093
+ * Uses the same `settings.retry` budget/backoff as agent-turn retries so a single transient
2094
+ * stream drop no longer fails the whole operation. `source` carries the context
2095
+ * the TUI needs to render the retry and recreate the underlying indicator.
2096
+ */
2097
+ _summarizationRetryCallbacks(source) {
2098
+ return {
2099
+ onRetryScheduled: (attempt, maxAttempts, delayMs, errorMessage) => {
2100
+ this._emit({
2101
+ type: "summarization_retry_scheduled",
2102
+ attempt,
2103
+ maxAttempts,
2104
+ delayMs,
2105
+ errorMessage,
2106
+ });
2107
+ },
2108
+ onRetryAttemptStart: () => {
2109
+ this._emit({
2110
+ type: "summarization_retry_attempt_start",
2111
+ ...source,
2112
+ });
2113
+ },
2114
+ onRetryFinished: () => {
2115
+ this._emit({ type: "summarization_retry_finished" });
2116
+ },
2117
+ };
2118
+ }
2089
2119
  /**
2090
2120
  * Prepare a retryable error for continuation with exponential backoff.
2091
2121
  * @returns true if the caller should continue the agent, false otherwise
@@ -2331,6 +2361,7 @@ export class AgentSession {
2331
2361
  // Run default summarizer if needed
2332
2362
  let summaryText;
2333
2363
  let summaryDetails;
2364
+ let summaryUsage;
2334
2365
  if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
2335
2366
  const model = this.model;
2336
2367
  const { apiKey, headers, env } = await this._getSummarizationRequestAuth(model);
@@ -2344,7 +2375,9 @@ export class AgentSession {
2344
2375
  customInstructions,
2345
2376
  replaceInstructions,
2346
2377
  reserveTokens: branchSummarySettings.reserveTokens,
2347
- streamFn: this.agent.streamFn,
2378
+ streamFn: this.agent.streamFunction,
2379
+ retry: this.settingsManager.getRetrySettings(),
2380
+ callbacks: this._summarizationRetryCallbacks({ source: "branchSummary" }),
2348
2381
  });
2349
2382
  if (result.aborted) {
2350
2383
  return { cancelled: true, aborted: true };
@@ -2353,6 +2386,7 @@ export class AgentSession {
2353
2386
  throw new Error(result.error);
2354
2387
  }
2355
2388
  summaryText = result.summary;
2389
+ summaryUsage = result.usage;
2356
2390
  summaryDetails = {
2357
2391
  readFiles: result.readFiles || [],
2358
2392
  modifiedFiles: result.modifiedFiles || [],
@@ -2361,6 +2395,7 @@ export class AgentSession {
2361
2395
  else if (extensionSummary) {
2362
2396
  summaryText = extensionSummary.summary;
2363
2397
  summaryDetails = extensionSummary.details;
2398
+ summaryUsage = extensionSummary.usage;
2364
2399
  }
2365
2400
  // Determine the new leaf position based on target type
2366
2401
  let newLeafId;
@@ -2368,18 +2403,12 @@ export class AgentSession {
2368
2403
  if (targetEntry.type === "message" && targetEntry.message.role === "user") {
2369
2404
  // User message: leaf = parent (null if root), text goes to editor
2370
2405
  newLeafId = targetEntry.parentId;
2371
- editorText = this._extractUserMessageText(targetEntry.message.content);
2406
+ editorText = contentText(targetEntry.message.content, "");
2372
2407
  }
2373
2408
  else if (targetEntry.type === "custom_message") {
2374
2409
  // Custom message: leaf = parent (null if root), text goes to editor
2375
2410
  newLeafId = targetEntry.parentId;
2376
- editorText =
2377
- typeof targetEntry.content === "string"
2378
- ? targetEntry.content
2379
- : targetEntry.content
2380
- .filter((c) => c.type === "text")
2381
- .map((c) => c.text)
2382
- .join("");
2411
+ editorText = contentText(targetEntry.content, "");
2383
2412
  }
2384
2413
  else {
2385
2414
  // Non-user message: leaf = selected node
@@ -2390,7 +2419,7 @@ export class AgentSession {
2390
2419
  let summaryEntry;
2391
2420
  if (summaryText) {
2392
2421
  // Create summary at target position (can be null for root)
2393
- const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);
2422
+ const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension, summaryUsage);
2394
2423
  summaryEntry = this.sessionManager.getEntry(summaryId);
2395
2424
  // Attach label to the summary entry
2396
2425
  if (label) {
@@ -2438,24 +2467,13 @@ export class AgentSession {
2438
2467
  continue;
2439
2468
  if (entry.message.role !== "user")
2440
2469
  continue;
2441
- const text = this._extractUserMessageText(entry.message.content);
2470
+ const text = contentText(entry.message.content, "");
2442
2471
  if (text) {
2443
2472
  result.push({ entryId: entry.id, text });
2444
2473
  }
2445
2474
  }
2446
2475
  return result;
2447
2476
  }
2448
- _extractUserMessageText(content) {
2449
- if (typeof content === "string")
2450
- return content;
2451
- if (Array.isArray(content)) {
2452
- return content
2453
- .filter((c) => c.type === "text")
2454
- .map((c) => c.text)
2455
- .join("");
2456
- }
2457
- return "";
2458
- }
2459
2477
  /**
2460
2478
  * Get session statistics. Aggregates over ALL session entries (including
2461
2479
  * history that was compacted away), so token/cost totals reflect what was
@@ -2467,12 +2485,11 @@ export class AgentSession {
2467
2485
  let toolResults = 0;
2468
2486
  let totalMessages = 0;
2469
2487
  let toolCalls = 0;
2470
- let totalInput = 0;
2471
- let totalOutput = 0;
2472
- let totalCacheRead = 0;
2473
- let totalCacheWrite = 0;
2474
- let totalCost = 0;
2488
+ const usageTotals = createUsageTotals();
2475
2489
  for (const entry of this.sessionManager.getEntries()) {
2490
+ if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
2491
+ addUsageToTotals(usageTotals, entry.usage);
2492
+ }
2476
2493
  if (entry.type !== "message")
2477
2494
  continue;
2478
2495
  totalMessages++;
@@ -2482,6 +2499,9 @@ export class AgentSession {
2482
2499
  }
2483
2500
  else if (message.role === "toolResult") {
2484
2501
  toolResults++;
2502
+ if (message.usage) {
2503
+ addUsageToTotals(usageTotals, message.usage);
2504
+ }
2485
2505
  }
2486
2506
  else if (message.role === "assistant") {
2487
2507
  assistantMessages++;
@@ -2489,12 +2509,7 @@ export class AgentSession {
2489
2509
  if (Array.isArray(assistantMsg.content)) {
2490
2510
  toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
2491
2511
  }
2492
- const usage = assistantMsg.usage;
2493
- totalInput += usage.input;
2494
- totalOutput += usage.output;
2495
- totalCacheRead += usage.cacheRead;
2496
- totalCacheWrite += usage.cacheWrite;
2497
- totalCost += usage.cost.total;
2512
+ addUsageToTotals(usageTotals, assistantMsg.usage);
2498
2513
  }
2499
2514
  }
2500
2515
  return {
@@ -2506,13 +2521,13 @@ export class AgentSession {
2506
2521
  toolResults,
2507
2522
  totalMessages,
2508
2523
  tokens: {
2509
- input: totalInput,
2510
- output: totalOutput,
2511
- cacheRead: totalCacheRead,
2512
- cacheWrite: totalCacheWrite,
2513
- total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
2524
+ input: usageTotals.input,
2525
+ output: usageTotals.output,
2526
+ cacheRead: usageTotals.cacheRead,
2527
+ cacheWrite: usageTotals.cacheWrite,
2528
+ total: usageTotals.input + usageTotals.output + usageTotals.cacheRead + usageTotals.cacheWrite,
2514
2529
  },
2515
- cost: totalCost,
2530
+ cost: usageTotals.cost,
2516
2531
  contextUsage: this.getContextUsage(),
2517
2532
  };
2518
2533
  }
@@ -4,9 +4,9 @@
4
4
  * When navigating to a different point in the session tree, this generates
5
5
  * a summary of the branch being left so context isn't lost.
6
6
  */
7
- import { completeSimple } from "@earendil-works/pi-ai/compat";
7
+ import { contentText } from "@earendil-works/pi-ai";
8
8
  import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
9
- import { estimateTokens } from "./compaction.js";
9
+ import { completeSummarization, estimateTokens } from "./compaction.js";
10
10
  import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
11
11
  // ============================================================================
12
12
  // Entry Collection
@@ -185,7 +185,7 @@ Keep each section concise. Preserve exact file paths, function names, and error
185
185
  * @param options - Generation options
186
186
  */
187
187
  export async function generateBranchSummary(entries, options) {
188
- const { model, apiKey, headers, env, signal, customInstructions, replaceInstructions, reserveTokens = 16384, streamFn, } = options;
188
+ const { model, apiKey, headers, env, signal, customInstructions, replaceInstructions, reserveTokens = 16384, streamFn, retry, callbacks, } = options;
189
189
  // Token budget = context window minus reserved space for prompt + response
190
190
  const contextWindow = model.contextWindow || 128000;
191
191
  const tokenBudget = contextWindow - reserveTokens;
@@ -218,12 +218,11 @@ export async function generateBranchSummary(entries, options) {
218
218
  ];
219
219
  // Call LLM for summarization. Prefer the session stream function so SDK
220
220
  // request behavior (timeouts, retries, attribution headers) stays consistent
221
- // without running through agent state/events.
221
+ // without running through agent state/events. Retried via completeSummarization
222
+ // so transient stream drops reuse the configured retry policy.
222
223
  const context = { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages };
223
224
  const requestOptions = { apiKey, headers, env, signal, maxTokens: 2048 };
224
- const response = streamFn
225
- ? await (await streamFn(model, context, requestOptions)).result()
226
- : await completeSimple(model, context, requestOptions);
225
+ const response = await completeSummarization(model, context, requestOptions, streamFn, retry, callbacks);
227
226
  // Check if aborted or errored
228
227
  if (response.stopReason === "aborted") {
229
228
  return { aborted: true };
@@ -231,10 +230,7 @@ export async function generateBranchSummary(entries, options) {
231
230
  if (response.stopReason === "error") {
232
231
  return { error: response.errorMessage || "Summarization failed" };
233
232
  }
234
- let summary = response.content
235
- .filter((c) => c.type === "text")
236
- .map((c) => c.text)
237
- .join("\n");
233
+ let summary = contentText(response.content);
238
234
  // Prepend preamble to provide context about the branch summary
239
235
  summary = BRANCH_SUMMARY_PREAMBLE + summary;
240
236
  // Compute file lists and append to summary
@@ -242,6 +238,7 @@ export async function generateBranchSummary(entries, options) {
242
238
  summary += formatFileOperations(readFiles, modifiedFiles);
243
239
  return {
244
240
  summary: summary || "No summary generated",
241
+ usage: response.usage,
245
242
  readFiles,
246
243
  modifiedFiles,
247
244
  };
@@ -4,6 +4,7 @@
4
4
  * Pure functions for compaction logic. The session manager handles I/O,
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
+ import { contentText, retryAssistantCall } from "@earendil-works/pi-ai";
7
8
  import { completeSimple } from "@earendil-works/pi-ai/compat";
8
9
  import { convertToLlm } from "../messages.js";
9
10
  import { buildSessionContext, sessionEntryToContextMessages, } from "../session-manager.js";
@@ -48,6 +49,28 @@ function getMessageFromEntryForCompaction(entry) {
48
49
  }
49
50
  return sessionEntryToContextMessages(entry)[0];
50
51
  }
52
+ function combineUsage(first, second) {
53
+ return {
54
+ input: first.input + second.input,
55
+ output: first.output + second.output,
56
+ cacheRead: first.cacheRead + second.cacheRead,
57
+ cacheWrite: first.cacheWrite + second.cacheWrite,
58
+ ...(first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined
59
+ ? { cacheWrite1h: (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0) }
60
+ : {}),
61
+ ...(first.reasoning !== undefined || second.reasoning !== undefined
62
+ ? { reasoning: (first.reasoning ?? 0) + (second.reasoning ?? 0) }
63
+ : {}),
64
+ totalTokens: first.totalTokens + second.totalTokens,
65
+ cost: {
66
+ input: first.cost.input + second.cost.input,
67
+ output: first.cost.output + second.cost.output,
68
+ cacheRead: first.cost.cacheRead + second.cost.cacheRead,
69
+ cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
70
+ total: first.cost.total + second.cost.total,
71
+ },
72
+ };
73
+ }
51
74
  export const DEFAULT_COMPACTION_SETTINGS = {
52
75
  enabled: true,
53
76
  reserveTokens: 16384,
@@ -407,18 +430,26 @@ function createSummarizationOptions(model, maxTokens, apiKey, headers, env, sign
407
430
  }
408
431
  return options;
409
432
  }
410
- async function completeSummarization(model, context, options, streamFn) {
411
- if (!streamFn) {
412
- return completeSimple(model, context, options);
413
- }
414
- const stream = await streamFn(model, context, options);
415
- return stream.result();
433
+ /**
434
+ * Shared choke point for every compaction/branch-summary summarization call. Wraps the
435
+ * single LLM call in {@link retryAssistantCall} so transient stream drops (e.g.
436
+ * `terminated`, socket close) honor the configured retry policy instead of failing
437
+ * the whole compaction on the first attempt. Deterministic errors and aborts return
438
+ * immediately (see {@link retryAssistantCall}).
439
+ */
440
+ export async function completeSummarization(model, context, options, streamFn, retry, callbacks) {
441
+ const produce = async () => streamFn ? (await streamFn(model, context, options)).result() : completeSimple(model, context, options);
442
+ return retryAssistantCall(produce, retry, options.signal, callbacks);
416
443
  }
417
444
  /**
418
445
  * Generate a summary of the conversation using the LLM.
419
446
  * If previousSummary is provided, uses the update prompt to merge.
420
447
  */
421
- export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env) {
448
+ export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks) {
449
+ return (await generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks)).text;
450
+ }
451
+ /** Generate or update a conversation summary and return its provider usage. */
452
+ export async function generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks) {
422
453
  const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
423
454
  // Use update prompt if we have a previous summary, otherwise initial prompt
424
455
  let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
@@ -443,15 +474,12 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
443
474
  },
444
475
  ];
445
476
  const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel);
446
- const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, streamFn);
477
+ const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, streamFn, retry, callbacks);
447
478
  if (response.stopReason === "error") {
448
479
  throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
449
480
  }
450
- const textContent = response.content
451
- .filter((c) => c.type === "text")
452
- .map((c) => c.text)
453
- .join("\n");
454
- return textContent;
481
+ const textContent = contentText(response.content);
482
+ return { text: textContent, usage: response.usage };
455
483
  }
456
484
  export function prepareCompaction(pathEntries, settings) {
457
485
  if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
@@ -544,21 +572,29 @@ Be concise. Focus on what's needed to understand the kept suffix.`;
544
572
  * @param preparation - Pre-calculated preparation from prepareCompaction()
545
573
  * @param customInstructions - Optional custom focus for the summary
546
574
  */
547
- export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, env) {
575
+ export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, env, retry, callbacks) {
548
576
  const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
549
577
  // Generate summaries and merge into one
550
578
  let summary;
579
+ let summaryUsage;
551
580
  if (isSplitTurn && turnPrefixMessages.length > 0) {
552
- const historyResult = messagesToSummarize.length > 0
553
- ? await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env)
554
- : "No prior history.";
555
- const turnPrefixResult = await generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn);
581
+ let historyText = "No prior history.";
582
+ let historyUsage;
583
+ if (messagesToSummarize.length > 0) {
584
+ const historyResult = await generateSummaryWithUsage(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks);
585
+ historyText = historyResult.text;
586
+ historyUsage = historyResult.usage;
587
+ }
588
+ const turnPrefixResult = await generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn, retry, callbacks);
556
589
  // Merge into single summary
557
- summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
590
+ summary = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.text}`;
591
+ summaryUsage = historyUsage ? combineUsage(historyUsage, turnPrefixResult.usage) : turnPrefixResult.usage;
558
592
  }
559
593
  else {
560
594
  // Just generate history summary
561
- summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env);
595
+ const result = await generateSummaryWithUsage(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks);
596
+ summary = result.text;
597
+ summaryUsage = result.usage;
562
598
  }
563
599
  // Compute file lists and append to summary
564
600
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
@@ -570,13 +606,14 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
570
606
  summary,
571
607
  firstKeptEntryId,
572
608
  tokensBefore,
609
+ usage: summaryUsage,
573
610
  details: { readFiles, modifiedFiles },
574
611
  };
575
612
  }
576
613
  /**
577
614
  * Generate a summary for a turn prefix (when splitting a turn).
578
615
  */
579
- async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn) {
616
+ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn, retry, callbacks) {
580
617
  const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); // Smaller budget for turn prefix
581
618
  const llmMessages = convertToLlm(messages);
582
619
  const conversationText = serializeConversation(llmMessages);
@@ -588,13 +625,13 @@ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey,
588
625
  timestamp: Date.now(),
589
626
  },
590
627
  ];
591
- const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel), streamFn);
628
+ const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel), streamFn, retry, callbacks);
592
629
  if (response.stopReason === "error") {
593
630
  throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
594
631
  }
595
- return response.content
596
- .filter((c) => c.type === "text")
597
- .map((c) => c.text)
598
- .join("\n");
632
+ return {
633
+ text: contentText(response.content),
634
+ usage: response.usage,
635
+ };
599
636
  }
600
637
  //# sourceMappingURL=compaction.js.map
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Shared utilities for compaction and branch summarization.
3
3
  */
4
+ import { contentText } from "@earendil-works/pi-ai";
4
5
  export function createFileOps() {
5
6
  return {
6
7
  read: new Set(),
@@ -94,24 +95,15 @@ export function serializeConversation(messages) {
94
95
  const parts = [];
95
96
  for (const msg of messages) {
96
97
  if (msg.role === "user") {
97
- const content = typeof msg.content === "string"
98
- ? msg.content
99
- : msg.content
100
- .filter((c) => c.type === "text")
101
- .map((c) => c.text)
102
- .join("");
98
+ const content = contentText(msg.content, "");
103
99
  if (content)
104
100
  parts.push(`[User]: ${content}`);
105
101
  }
106
102
  else if (msg.role === "assistant") {
107
- const textParts = [];
108
103
  const thinkingParts = [];
109
104
  const toolCalls = [];
110
105
  for (const block of msg.content) {
111
- if (block.type === "text") {
112
- textParts.push(block.text);
113
- }
114
- else if (block.type === "thinking") {
106
+ if (block.type === "thinking") {
115
107
  thinkingParts.push(block.thinking);
116
108
  }
117
109
  else if (block.type === "toolCall") {
@@ -125,18 +117,15 @@ export function serializeConversation(messages) {
125
117
  if (thinkingParts.length > 0) {
126
118
  parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
127
119
  }
128
- if (textParts.length > 0) {
129
- parts.push(`[Assistant]: ${textParts.join("\n")}`);
120
+ if (msg.content.some((block) => block.type === "text")) {
121
+ parts.push(`[Assistant]: ${contentText(msg.content)}`);
130
122
  }
131
123
  if (toolCalls.length > 0) {
132
124
  parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
133
125
  }
134
126
  }
135
127
  else if (msg.role === "toolResult") {
136
- const content = msg.content
137
- .filter((c) => c.type === "text")
138
- .map((c) => c.text)
139
- .join("");
128
+ const content = contentText(msg.content, "");
140
129
  if (content) {
141
130
  parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
142
131
  }
@@ -657,6 +657,10 @@ export class ExtensionRunner {
657
657
  currentEvent.isError = handlerResult.isError;
658
658
  modified = true;
659
659
  }
660
+ if (handlerResult.usage !== undefined) {
661
+ currentEvent.usage = handlerResult.usage;
662
+ modified = true;
663
+ }
660
664
  }
661
665
  catch (err) {
662
666
  const message = err instanceof Error ? err.message : String(err);
@@ -677,6 +681,7 @@ export class ExtensionRunner {
677
681
  content: currentEvent.content,
678
682
  details: currentEvent.details,
679
683
  isError: currentEvent.isError,
684
+ usage: currentEvent.usage,
680
685
  };
681
686
  }
682
687
  async emitToolCall(event) {
@@ -40,6 +40,8 @@ export const defaultModelPerProvider = {
40
40
  "kimi-coding": "kimi-for-coding",
41
41
  "cloudflare-workers-ai": "@cf/moonshotai/kimi-k2.6",
42
42
  "cloudflare-ai-gateway": "workers-ai/@cf/moonshotai/kimi-k2.6",
43
+ "qwen-token-plan": "qwen3.7-max",
44
+ "qwen-token-plan-cn": "qwen3.7-max",
43
45
  xiaomi: "mimo-v2.5-pro",
44
46
  "xiaomi-token-plan-cn": "mimo-v2.5-pro",
45
47
  "xiaomi-token-plan-ams": "mimo-v2.5-pro",