@psnext/slingcli 2.5.20260722-2 → 2.5.20260723-1

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
@@ -2,7 +2,8 @@
2
2
  * Agent loop that works with AgentMessage throughout.
3
3
  * Transforms to Message[] only at the LLM call boundary.
4
4
  */
5
- import { EventStream, streamSimple, validateToolArguments, } from "@earendil-works/pi-ai/compat";
5
+ import { EventStream, validateToolArguments, } from "@earendil-works/pi-ai";
6
+ import { getDefaultStreamFn } from "./stream-fn.js";
6
7
  /**
7
8
  * Start an agent loop with a new prompt message.
8
9
  * The prompt is added to the context and events are emitted for it.
@@ -51,7 +52,7 @@ export async function runAgentLoop(prompts, context, config, emit, signal, strea
51
52
  await emit({ type: "message_start", message: prompt });
52
53
  await emit({ type: "message_end", message: prompt });
53
54
  }
54
- await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
55
+ await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
55
56
  return newMessages;
56
57
  }
57
58
  export async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
@@ -65,7 +66,7 @@ export async function runAgentLoopContinue(context, config, emit, signal, stream
65
66
  const currentContext = { ...context };
66
67
  await emit({ type: "agent_start" });
67
68
  await emit({ type: "turn_start" });
68
- await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
69
+ await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
69
70
  return newMessages;
70
71
  }
71
72
  function createAgentStream() {
@@ -74,7 +75,7 @@ function createAgentStream() {
74
75
  /**
75
76
  * Main loop logic shared by agentLoop and agentLoopContinue.
76
77
  */
77
- async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFn) {
78
+ async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFunction) {
78
79
  let currentContext = initialContext;
79
80
  let config = initialConfig;
80
81
  let firstTurn = true;
@@ -102,7 +103,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
102
103
  pendingMessages = [];
103
104
  }
104
105
  // Stream assistant response
105
- const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
106
+ const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
106
107
  newMessages.push(message);
107
108
  if (message.stopReason === "error" || message.stopReason === "aborted") {
108
109
  await emit({ type: "turn_end", message, toolResults: [] });
@@ -174,7 +175,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
174
175
  * Stream an assistant response from the LLM.
175
176
  * This is where AgentMessage[] gets transformed to Message[] for the LLM.
176
177
  */
177
- async function streamAssistantResponse(context, config, signal, emit, streamFn) {
178
+ async function streamAssistantResponse(context, config, signal, emit, streamFunction) {
178
179
  // Apply context transform if configured (AgentMessage[] → AgentMessage[])
179
180
  let messages = context.messages;
180
181
  if (config.transformContext) {
@@ -188,7 +189,6 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
188
189
  messages: llmMessages,
189
190
  tools: context.tools,
190
191
  };
191
- const streamFunction = streamFn || streamSimple;
192
192
  // Resolve API key (important for expiring tokens)
193
193
  const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
194
194
  const response = await streamFunction(config.model, llmContext, {
@@ -495,6 +495,7 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
495
495
  ...result,
496
496
  content: afterResult.content ?? result.content,
497
497
  details: afterResult.details ?? result.details,
498
+ usage: afterResult.usage ?? result.usage,
498
499
  terminate: afterResult.terminate ?? result.terminate,
499
500
  };
500
501
  isError = afterResult.isError ?? isError;
@@ -535,6 +536,7 @@ function createToolResultMessage(finalized) {
535
536
  // so the null never enters session history or provider payloads.
536
537
  content: finalized.result.content ?? [],
537
538
  details: finalized.result.details,
539
+ usage: finalized.result.usage,
538
540
  ...(finalized.result.addedToolNames?.length ? { addedToolNames: finalized.result.addedToolNames } : {}),
539
541
  isError: finalized.isError,
540
542
  timestamp: Date.now(),
@@ -1,5 +1,5 @@
1
- import { streamSimple, } from "@earendil-works/pi-ai/compat";
2
1
  import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.js";
2
+ import { getDefaultStreamFn } from "./stream-fn.js";
3
3
  function defaultConvertToLlm(messages) {
4
4
  return messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
5
5
  }
@@ -90,7 +90,7 @@ export class Agent {
90
90
  followUpQueue;
91
91
  convertToLlm;
92
92
  transformContext;
93
- streamFn;
93
+ streamFunction;
94
94
  getApiKey;
95
95
  onPayload;
96
96
  onResponse;
@@ -109,25 +109,27 @@ export class Agent {
109
109
  maxRetryDelayMs;
110
110
  /** Tool execution strategy for assistant messages that contain multiple tool calls. */
111
111
  toolExecution;
112
- constructor(options = {}) {
113
- this._state = createMutableAgentState(options.initialState);
114
- this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
115
- this.transformContext = options.transformContext;
116
- this.streamFn = options.streamFn ?? streamSimple;
117
- this.getApiKey = options.getApiKey;
118
- this.onPayload = options.onPayload;
119
- this.onResponse = options.onResponse;
120
- this.beforeToolCall = options.beforeToolCall;
121
- this.afterToolCall = options.afterToolCall;
122
- this.prepareNextTurn = options.prepareNextTurn;
123
- this.prepareNextTurnWithContext = options.prepareNextTurnWithContext;
124
- this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
125
- this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
126
- this.sessionId = options.sessionId;
127
- this.thinkingBudgets = options.thinkingBudgets;
128
- this.transport = options.transport ?? "auto";
129
- this.maxRetryDelayMs = options.maxRetryDelayMs;
130
- this.toolExecution = options.toolExecution ?? "parallel";
112
+ constructor(options) {
113
+ // Older compiled consumers may omit options or streamFn even though the current API requires them.
114
+ const runtimeOptions = options ?? {};
115
+ this._state = createMutableAgentState(runtimeOptions.initialState);
116
+ this.convertToLlm = runtimeOptions.convertToLlm ?? defaultConvertToLlm;
117
+ this.transformContext = runtimeOptions.transformContext;
118
+ this.streamFunction = runtimeOptions.streamFn ?? getDefaultStreamFn();
119
+ this.getApiKey = runtimeOptions.getApiKey;
120
+ this.onPayload = runtimeOptions.onPayload;
121
+ this.onResponse = runtimeOptions.onResponse;
122
+ this.beforeToolCall = runtimeOptions.beforeToolCall;
123
+ this.afterToolCall = runtimeOptions.afterToolCall;
124
+ this.prepareNextTurn = runtimeOptions.prepareNextTurn;
125
+ this.prepareNextTurnWithContext = runtimeOptions.prepareNextTurnWithContext;
126
+ this.steeringQueue = new PendingMessageQueue(runtimeOptions.steeringMode ?? "one-at-a-time");
127
+ this.followUpQueue = new PendingMessageQueue(runtimeOptions.followUpMode ?? "one-at-a-time");
128
+ this.sessionId = runtimeOptions.sessionId;
129
+ this.thinkingBudgets = runtimeOptions.thinkingBudgets;
130
+ this.transport = runtimeOptions.transport ?? "auto";
131
+ this.maxRetryDelayMs = runtimeOptions.maxRetryDelayMs;
132
+ this.toolExecution = runtimeOptions.toolExecution ?? "parallel";
131
133
  }
132
134
  /**
133
135
  * Subscribe to agent lifecycle events.
@@ -262,12 +264,12 @@ export class Agent {
262
264
  }
263
265
  async runPromptMessages(messages, options = {}) {
264
266
  await this.runWithLifecycle(async (signal) => {
265
- await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFn);
267
+ await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFunction);
266
268
  });
267
269
  }
268
270
  async runContinuation() {
269
271
  await this.runWithLifecycle(async (signal) => {
270
- await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFn);
272
+ await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFunction);
271
273
  });
272
274
  }
273
275
  createContextSnapshot() {
@@ -1,3 +1,4 @@
1
+ import { contentText, } from "@earendil-works/pi-ai";
1
2
  import { runAgentLoop } from "../agent-loop.js";
2
3
  import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.js";
3
4
  import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compaction/compaction.js";
@@ -122,6 +123,7 @@ export class AgentHarness {
122
123
  thinkingLevel;
123
124
  systemPrompt;
124
125
  streamOptions;
126
+ retry;
125
127
  resources;
126
128
  tools = new Map();
127
129
  activeToolNames;
@@ -137,6 +139,7 @@ export class AgentHarness {
137
139
  this.models = options.models;
138
140
  this.resources = options.resources ?? {};
139
141
  this.streamOptions = cloneStreamOptions(options.streamOptions);
142
+ this.retry = options.retry;
140
143
  this.systemPrompt = options.systemPrompt;
141
144
  this.validateUniqueNames((options.tools ?? []).map((tool) => tool.name), "Duplicate tool name(s)");
142
145
  for (const tool of options.tools ?? []) {
@@ -193,6 +196,13 @@ export class AgentHarness {
193
196
  }
194
197
  return lastResult;
195
198
  }
199
+ retryCallbacks(operation) {
200
+ return {
201
+ onRetryScheduled: (attempt, maxAttempts, delayMs, errorMessage) => this.emitOwn({ type: "retry_scheduled", operation, attempt, maxAttempts, delayMs, errorMessage }),
202
+ onRetryAttemptStart: () => this.emitOwn({ type: "retry_attempt_start", operation }),
203
+ onRetryFinished: () => this.emitOwn({ type: "retry_finished", operation }),
204
+ };
205
+ }
196
206
  async emitBeforeProviderRequest(model, sessionId, streamOptions) {
197
207
  const handlers = this.getHandlers("before_provider_request");
198
208
  let current = cloneStreamOptions(streamOptions);
@@ -358,9 +368,16 @@ export class AgentHarness {
358
368
  content: result.content,
359
369
  details: result.details,
360
370
  isError,
371
+ usage: result.usage,
361
372
  });
362
373
  return patch
363
- ? { content: patch.content, details: patch.details, isError: patch.isError, terminate: patch.terminate }
374
+ ? {
375
+ content: patch.content,
376
+ details: patch.details,
377
+ isError: patch.isError,
378
+ usage: patch.usage,
379
+ terminate: patch.terminate,
380
+ }
364
381
  : undefined;
365
382
  },
366
383
  prepareNextTurn: async () => {
@@ -634,11 +651,11 @@ export class AgentHarness {
634
651
  const provided = hookResult?.compaction;
635
652
  const compactResult = provided
636
653
  ? { ok: true, value: provided }
637
- : await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel);
654
+ : await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel, this.retry, this.retryCallbacks("compaction"));
638
655
  if (!compactResult.ok)
639
656
  throw compactResult.error;
640
657
  const result = compactResult.value;
641
- const entryId = await this.session.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, provided !== undefined);
658
+ const entryId = await this.session.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, provided !== undefined, result.usage, result.retainedTail);
642
659
  const entry = await this.session.getEntry(entryId);
643
660
  if (entry?.type === "compaction") {
644
661
  await this.emitOwn({ type: "session_compact", compactionEntry: entry, fromHook: provided !== undefined });
@@ -681,6 +698,7 @@ export class AgentHarness {
681
698
  let summaryEntry;
682
699
  let summaryText = hookResult?.summary?.summary;
683
700
  let summaryDetails = hookResult?.summary?.details;
701
+ let summaryUsage = hookResult?.summary?.usage;
684
702
  if (!summaryText && options?.summarize && entries.length > 0) {
685
703
  const model = this.model;
686
704
  if (!model)
@@ -691,6 +709,8 @@ export class AgentHarness {
691
709
  signal: new AbortController().signal,
692
710
  customInstructions: hookResult?.customInstructions ?? options?.customInstructions,
693
711
  replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,
712
+ retry: this.retry,
713
+ callbacks: this.retryCallbacks("branch_summary"),
694
714
  });
695
715
  if (!branchSummary.ok) {
696
716
  if (branchSummary.error.code === "aborted")
@@ -698,6 +718,7 @@ export class AgentHarness {
698
718
  throw new AgentHarnessError("branch_summary", branchSummary.error.message, branchSummary.error);
699
719
  }
700
720
  summaryText = branchSummary.value.summary;
721
+ summaryUsage = branchSummary.value.usage;
701
722
  summaryDetails = {
702
723
  readFiles: branchSummary.value.readFiles,
703
724
  modifiedFiles: branchSummary.value.modifiedFiles,
@@ -707,30 +728,22 @@ export class AgentHarness {
707
728
  let newLeafId;
708
729
  if (targetEntry.type === "message" && targetEntry.message.role === "user") {
709
730
  newLeafId = targetEntry.parentId;
710
- const content = targetEntry.message.content;
711
- editorText =
712
- typeof content === "string"
713
- ? content
714
- : content
715
- .filter((c) => c.type === "text")
716
- .map((c) => c.text)
717
- .join("");
731
+ editorText = contentText(targetEntry.message.content, "");
718
732
  }
719
733
  else if (targetEntry.type === "custom_message") {
720
734
  newLeafId = targetEntry.parentId;
721
- editorText =
722
- typeof targetEntry.content === "string"
723
- ? targetEntry.content
724
- : targetEntry.content
725
- .filter((c) => c.type === "text")
726
- .map((c) => c.text)
727
- .join("");
735
+ editorText = contentText(targetEntry.content, "");
728
736
  }
729
737
  else {
730
738
  newLeafId = targetId;
731
739
  }
732
740
  const summaryId = await this.session.moveTo(newLeafId, summaryText
733
- ? { summary: summaryText, details: summaryDetails, fromHook: hookResult?.summary !== undefined }
741
+ ? {
742
+ summary: summaryText,
743
+ details: summaryDetails,
744
+ usage: summaryUsage,
745
+ fromHook: hookResult?.summary !== undefined,
746
+ }
734
747
  : undefined);
735
748
  if (summaryId) {
736
749
  const entry = await this.session.getEntry(summaryId);
@@ -1,6 +1,7 @@
1
+ import { contentText } from "@earendil-works/pi-ai";
1
2
  import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
2
3
  import { BranchSummaryError, err, ok, SessionError } from "../types.js";
3
- import { estimateTokens, SUMMARIZATION_SYSTEM_PROMPT } from "./compaction.js";
4
+ import { completeSimpleWithRetries, estimateTokens, SUMMARIZATION_SYSTEM_PROMPT } from "./compaction.js";
4
5
  import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, } from "./utils.js";
5
6
  /** Collect entries that should be summarized before navigating to a different session tree entry. */
6
7
  export async function collectEntriesForBranchSummary(session, oldLeafId, targetId) {
@@ -124,7 +125,7 @@ Use this EXACT format:
124
125
  Keep each section concise. Preserve exact file paths, function names, and error messages.`;
125
126
  /** Generate a summary for abandoned branch entries. */
126
127
  export async function generateBranchSummary(entries, options) {
127
- const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
128
+ const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384, retry, callbacks, } = options;
128
129
  const contextWindow = model.contextWindow || 128000;
129
130
  const tokenBudget = contextWindow - reserveTokens;
130
131
  const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget);
@@ -151,22 +152,20 @@ export async function generateBranchSummary(entries, options) {
151
152
  timestamp: Date.now(),
152
153
  },
153
154
  ];
154
- const response = await models.completeSimple(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, { signal, maxTokens: 2048 });
155
+ const response = await completeSimpleWithRetries(models, model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, { signal, maxTokens: 2048 }, retry, callbacks);
155
156
  if (response.stopReason === "aborted") {
156
157
  return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted"));
157
158
  }
158
159
  if (response.stopReason === "error") {
159
160
  return err(new BranchSummaryError("summarization_failed", `Branch summary failed: ${response.errorMessage || "Unknown error"}`));
160
161
  }
161
- let summary = response.content
162
- .filter((c) => c.type === "text")
163
- .map((c) => c.text)
164
- .join("\n");
162
+ let summary = contentText(response.content);
165
163
  summary = BRANCH_SUMMARY_PREAMBLE + summary;
166
164
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
167
165
  summary += formatFileOperations(readFiles, modifiedFiles);
168
166
  return ok({
169
167
  summary: summary || "No summary generated",
168
+ usage: response.usage,
170
169
  readFiles,
171
170
  modifiedFiles,
172
171
  });
@@ -1,3 +1,4 @@
1
+ import { contentText, retryAssistantCall, } from "@earendil-works/pi-ai";
1
2
  import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
2
3
  import { buildSessionContext } from "../session/session.js";
3
4
  import { CompactionError, err, ok } from "../types.js";
@@ -52,6 +53,31 @@ function getMessageFromEntryForCompaction(entry) {
52
53
  }
53
54
  return getMessageFromEntry(entry);
54
55
  }
56
+ export async function completeSimpleWithRetries(models, model, context, options, retry, callbacks) {
57
+ return retryAssistantCall(() => models.completeSimple(model, context, options), retry, options.signal, callbacks);
58
+ }
59
+ function combineUsage(first, second) {
60
+ return {
61
+ input: first.input + second.input,
62
+ output: first.output + second.output,
63
+ cacheRead: first.cacheRead + second.cacheRead,
64
+ cacheWrite: first.cacheWrite + second.cacheWrite,
65
+ ...(first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined
66
+ ? { cacheWrite1h: (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0) }
67
+ : {}),
68
+ ...(first.reasoning !== undefined || second.reasoning !== undefined
69
+ ? { reasoning: (first.reasoning ?? 0) + (second.reasoning ?? 0) }
70
+ : {}),
71
+ totalTokens: first.totalTokens + second.totalTokens,
72
+ cost: {
73
+ input: first.cost.input + second.cost.input,
74
+ output: first.cost.output + second.cost.output,
75
+ cacheRead: first.cost.cacheRead + second.cost.cacheRead,
76
+ cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
77
+ total: first.cost.total + second.cost.total,
78
+ },
79
+ };
80
+ }
55
81
  /** Default compaction settings used by the harness. */
56
82
  export const DEFAULT_COMPACTION_SETTINGS = {
57
83
  enabled: true,
@@ -355,7 +381,12 @@ Use this EXACT format:
355
381
 
356
382
  Keep each section concise. Preserve exact file paths, function names, and error messages.`;
357
383
  /** Generate or update a conversation summary for compaction. */
358
- export async function generateSummary(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel) {
384
+ export async function generateSummary(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks) {
385
+ const result = await generateSummaryWithUsage(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks);
386
+ return result.ok ? ok(result.value.text) : err(result.error);
387
+ }
388
+ /** Generate or update a conversation summary and return its provider usage. */
389
+ export async function generateSummaryWithUsage(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks) {
359
390
  const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
360
391
  let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
361
392
  if (customInstructions) {
@@ -378,18 +409,15 @@ export async function generateSummary(currentMessages, models, model, reserveTok
378
409
  const completionOptions = model.reasoning && thinkingLevel && thinkingLevel !== "off"
379
410
  ? { maxTokens, signal, reasoning: thinkingLevel }
380
411
  : { maxTokens, signal };
381
- const response = await models.completeSimple(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions);
412
+ const response = await completeSimpleWithRetries(models, model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, retry, callbacks);
382
413
  if (response.stopReason === "aborted") {
383
414
  return err(new CompactionError("aborted", response.errorMessage || "Summarization aborted"));
384
415
  }
385
416
  if (response.stopReason === "error") {
386
417
  return err(new CompactionError("summarization_failed", `Summarization failed: ${response.errorMessage || "Unknown error"}`));
387
418
  }
388
- const textContent = response.content
389
- .filter((c) => c.type === "text")
390
- .map((c) => c.text)
391
- .join("\n");
392
- return ok(textContent);
419
+ const textContent = contentText(response.content);
420
+ return ok({ text: textContent, usage: response.usage });
393
421
  }
394
422
  /** Prepare session entries for compaction, or return undefined when compaction is not applicable. */
395
423
  export function prepareCompaction(pathEntries, settings) {
@@ -408,7 +436,9 @@ export function prepareCompaction(pathEntries, settings) {
408
436
  if (prevCompactionIndex >= 0) {
409
437
  const prevCompaction = pathEntries[prevCompactionIndex];
410
438
  previousSummary = prevCompaction.summary;
411
- const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);
439
+ const firstKeptEntryIndex = prevCompaction.firstKeptEntryId
440
+ ? pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId)
441
+ : -1;
412
442
  boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
413
443
  }
414
444
  const boundaryEnd = pathEntries.length;
@@ -434,6 +464,12 @@ export function prepareCompaction(pathEntries, settings) {
434
464
  turnPrefixMessages.push(msg);
435
465
  }
436
466
  }
467
+ const retainedTail = [];
468
+ for (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {
469
+ const msg = getMessageFromEntryForCompaction(pathEntries[i]);
470
+ if (msg)
471
+ retainedTail.push(msg);
472
+ }
437
473
  const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
438
474
  if (cutPoint.isSplitTurn) {
439
475
  for (const msg of turnPrefixMessages) {
@@ -444,6 +480,7 @@ export function prepareCompaction(pathEntries, settings) {
444
480
  firstKeptEntryId,
445
481
  messagesToSummarize,
446
482
  turnPrefixMessages,
483
+ retainedTail,
447
484
  isSplitTurn: cutPoint.isSplitTurn,
448
485
  tokensBefore,
449
486
  previousSummary,
@@ -467,28 +504,37 @@ Summarize the prefix to provide context for the retained suffix:
467
504
  Be concise. Focus on what's needed to understand the kept suffix.`;
468
505
  export { serializeConversation } from "./utils.js";
469
506
  /** Generate compaction summary data from prepared session history. */
470
- export async function compact(preparation, models, model, customInstructions, signal, thinkingLevel) {
471
- const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
507
+ export async function compact(preparation, models, model, customInstructions, signal, thinkingLevel, retry, callbacks) {
508
+ const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, retainedTail, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
472
509
  if (!firstKeptEntryId) {
473
510
  return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
474
511
  }
475
512
  let summary;
513
+ let summaryUsage;
476
514
  if (isSplitTurn && turnPrefixMessages.length > 0) {
477
- const historyResult = messagesToSummarize.length > 0
478
- ? await generateSummary(messagesToSummarize, models, model, settings.reserveTokens, signal, customInstructions, previousSummary, thinkingLevel)
479
- : ok("No prior history.");
480
- if (!historyResult.ok)
481
- return err(historyResult.error);
482
- const turnPrefixResult = await generateTurnPrefixSummary(turnPrefixMessages, models, model, settings.reserveTokens, signal, thinkingLevel);
515
+ let historyText = "No prior history.";
516
+ let historyUsage;
517
+ if (messagesToSummarize.length > 0) {
518
+ const historyResult = await generateSummaryWithUsage(messagesToSummarize, models, model, settings.reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks);
519
+ if (!historyResult.ok)
520
+ return err(historyResult.error);
521
+ historyText = historyResult.value.text;
522
+ historyUsage = historyResult.value.usage;
523
+ }
524
+ const turnPrefixResult = await generateTurnPrefixSummary(turnPrefixMessages, models, model, settings.reserveTokens, signal, thinkingLevel, retry, callbacks);
483
525
  if (!turnPrefixResult.ok)
484
526
  return err(turnPrefixResult.error);
485
- summary = `${historyResult.value}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value}`;
527
+ summary = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value.text}`;
528
+ summaryUsage = historyUsage
529
+ ? combineUsage(historyUsage, turnPrefixResult.value.usage)
530
+ : turnPrefixResult.value.usage;
486
531
  }
487
532
  else {
488
- const summaryResult = await generateSummary(messagesToSummarize, models, model, settings.reserveTokens, signal, customInstructions, previousSummary, thinkingLevel);
533
+ const summaryResult = await generateSummaryWithUsage(messagesToSummarize, models, model, settings.reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks);
489
534
  if (!summaryResult.ok)
490
535
  return err(summaryResult.error);
491
- summary = summaryResult.value;
536
+ summary = summaryResult.value.text;
537
+ summaryUsage = summaryResult.value.usage;
492
538
  }
493
539
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
494
540
  summary += formatFileOperations(readFiles, modifiedFiles);
@@ -496,10 +542,12 @@ export async function compact(preparation, models, model, customInstructions, si
496
542
  summary,
497
543
  firstKeptEntryId,
498
544
  tokensBefore,
545
+ usage: summaryUsage,
546
+ retainedTail,
499
547
  details: { readFiles, modifiedFiles },
500
548
  });
501
549
  }
502
- async function generateTurnPrefixSummary(messages, models, model, reserveTokens, signal, thinkingLevel) {
550
+ async function generateTurnPrefixSummary(messages, models, model, reserveTokens, signal, thinkingLevel, retry, callbacks) {
503
551
  const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
504
552
  const llmMessages = convertToLlm(messages);
505
553
  const conversationText = serializeConversation(llmMessages);
@@ -511,18 +559,19 @@ async function generateTurnPrefixSummary(messages, models, model, reserveTokens,
511
559
  timestamp: Date.now(),
512
560
  },
513
561
  ];
514
- const response = await models.completeSimple(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, model.reasoning && thinkingLevel && thinkingLevel !== "off"
562
+ const completionOptions = model.reasoning && thinkingLevel && thinkingLevel !== "off"
515
563
  ? { maxTokens, signal, reasoning: thinkingLevel }
516
- : { maxTokens, signal });
564
+ : { maxTokens, signal };
565
+ const response = await completeSimpleWithRetries(models, model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, retry, callbacks);
517
566
  if (response.stopReason === "aborted") {
518
567
  return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted"));
519
568
  }
520
569
  if (response.stopReason === "error") {
521
570
  return err(new CompactionError("summarization_failed", `Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`));
522
571
  }
523
- return ok(response.content
524
- .filter((c) => c.type === "text")
525
- .map((c) => c.text)
526
- .join("\n"));
572
+ return ok({
573
+ text: contentText(response.content),
574
+ usage: response.usage,
575
+ });
527
576
  }
528
577
  //# sourceMappingURL=compaction.js.map
@@ -1,3 +1,4 @@
1
+ import { contentText } from "@earendil-works/pi-ai";
1
2
  /** Create an empty file-operation accumulator. */
2
3
  export function createFileOps() {
3
4
  return {
@@ -78,24 +79,15 @@ export function serializeConversation(messages) {
78
79
  const parts = [];
79
80
  for (const msg of messages) {
80
81
  if (msg.role === "user") {
81
- const content = typeof msg.content === "string"
82
- ? msg.content
83
- : msg.content
84
- .filter((c) => c.type === "text")
85
- .map((c) => c.text)
86
- .join("");
82
+ const content = contentText(msg.content, "");
87
83
  if (content)
88
84
  parts.push(`[User]: ${content}`);
89
85
  }
90
86
  else if (msg.role === "assistant") {
91
- const textParts = [];
92
87
  const thinkingParts = [];
93
88
  const toolCalls = [];
94
89
  for (const block of msg.content) {
95
- if (block.type === "text") {
96
- textParts.push(block.text);
97
- }
98
- else if (block.type === "thinking") {
90
+ if (block.type === "thinking") {
99
91
  thinkingParts.push(block.thinking);
100
92
  }
101
93
  else if (block.type === "toolCall") {
@@ -109,18 +101,15 @@ export function serializeConversation(messages) {
109
101
  if (thinkingParts.length > 0) {
110
102
  parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
111
103
  }
112
- if (textParts.length > 0) {
113
- parts.push(`[Assistant]: ${textParts.join("\n")}`);
104
+ if (msg.content.some((block) => block.type === "text")) {
105
+ parts.push(`[Assistant]: ${contentText(msg.content)}`);
114
106
  }
115
107
  if (toolCalls.length > 0) {
116
108
  parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
117
109
  }
118
110
  }
119
111
  else if (msg.role === "toolResult") {
120
- const content = msg.content
121
- .filter((c) => c.type === "text")
122
- .map((c) => c.text)
123
- .join("");
112
+ const content = contentText(msg.content, "");
124
113
  if (content) {
125
114
  parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
126
115
  }