@oh-my-pi/pi-agent-core 18.10.3 → 18.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.10.4] - 2026-10-13
6
+
7
+ ### Added
8
+
9
+ - `AgentTool.formatApprovalDetails` receives `ApprovalDetailsOptions`; `compact: true` asks for untruncated bodies so a compact approval prompt can count their lines ([#15550](https://github.com/can1357/oh-my-pi/pull/15550) by [@H4vC](https://github.com/H4vC))
10
+
11
+ ### Changed
12
+
13
+ - Before-model-call hooks now receive the loop context as a second argument, and `buildProviderContext` receives the summarizer model ([#15436](https://github.com/can1357/oh-my-pi/pull/15436) by [@will-bogusz](https://github.com/will-bogusz))
14
+
15
+ ### Fixed
16
+
17
+ - Fixed a Claude compaction or branch summarizer receiving assistant reasoning from a snapcompact archive rendered under another model, which Claude refuses (`reasoning_extraction`) ([#15436](https://github.com/can1357/oh-my-pi/pull/15436) by [@will-bogusz](https://github.com/will-bogusz))
18
+
5
19
  ## [18.10.3] - 2026-10-12
6
20
 
7
21
  ### Breaking Changes
@@ -3,7 +3,7 @@ import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
3
3
  import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
4
4
  import type { AppendOnlyContextManager } from "./append-only-context.js";
5
5
  import { Tokenizer } from "./tokenizer.js";
6
- import type { AgentBeforeModelCall, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideMessage, PrepareQueuedMessages, SpeculativeToolExecutionConfig, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
6
+ import type { AgentBeforeModelCall, AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideMessage, PrepareQueuedMessages, SpeculativeToolExecutionConfig, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
7
7
  export declare class AgentBusyError extends Error {
8
8
  constructor(message?: string);
9
9
  }
@@ -427,8 +427,9 @@ export declare class Agent {
427
427
  onQueueChange(listener: () => void): () => void;
428
428
  /** Register an independently removable hook that runs before queued messages are consumed. */
429
429
  addBeforeQueuedMessageDequeueHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void;
430
- /** Register an independently removable hook that runs immediately before each model call. */
431
- addBeforeModelCallHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void;
430
+ /** Register an independently removable hook that runs immediately before each model call, with the
431
+ * loop's live context (mutating `context.messages` changes what this and later calls send). */
432
+ addBeforeModelCallHook(hook: (signal: AbortSignal | undefined, context: AgentContext) => Promise<void> | void): () => void;
432
433
  /**
433
434
  * Take back dequeued input the transcript has not recorded yet, ahead of an abort (Esc restores
434
435
  * it to the editor): steering live steering took for the in-flight response, then each batch
@@ -187,11 +187,11 @@ export interface SummaryOptions {
187
187
  convertToLlm?: ConvertToLlm;
188
188
  /**
189
189
  * The provider context a live turn sends for `summarized` + `retained`, cut to
190
- * `summarized`. Anthropic on-demand compaction sends it so retained signed
191
- * thinking keeps its prefix; without it the request uses `remoteSystemPrompt`,
192
- * `tools` and `convertToLlm`.
190
+ * `summarized`, as read by the summarizer `model`. Anthropic on-demand
191
+ * compaction sends it so retained signed thinking keeps its prefix; without
192
+ * it the request uses `remoteSystemPrompt`, `tools` and `convertToLlm`.
193
193
  */
194
- buildProviderContext?: (summarized: AgentMessage[], retained: AgentMessage[], signal?: AbortSignal) => Promise<Context>;
194
+ buildProviderContext?: (summarized: AgentMessage[], retained: AgentMessage[], signal: AbortSignal | undefined, model: Model) => Promise<Context>;
195
195
  /**
196
196
  * Whether a message is a turn the user wrote. Remote Compaction V2 keeps these
197
197
  * next to the compaction item. Defaults to `role === "user"`; hosts whose
@@ -637,10 +637,19 @@ export interface SpeculativePhysicalOutcome {
637
637
  /** Opaque evidence binding the result to the local bytes actually consumed. */
638
638
  evidence?: unknown;
639
639
  }
640
+ /** How an approval prompt will show what `formatApprovalDetails` returns. */
641
+ export interface ApprovalDetailsOptions {
642
+ /**
643
+ * The prompt keeps only each detail's first line plus a count of the rest (the transcript
644
+ * shows the call in full): return bodies untruncated so the count is the whole value's.
645
+ * The host truncates the rows it keeps. Otherwise truncate long values as usual.
646
+ */
647
+ compact?: boolean;
648
+ }
640
649
  export interface SpeculativeToolReference {
641
650
  name: string;
642
651
  approval?: ToolApproval;
643
- formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
652
+ formatApprovalDetails?: (args: unknown, options?: ApprovalDetailsOptions) => string | string[] | undefined;
644
653
  }
645
654
  export interface SpeculativeOperationContext extends ToolSpeculationExecutionContext {
646
655
  tool: SpeculativeToolReference;
@@ -1082,7 +1091,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
1082
1091
  /** Capability tier declaration used by approval gates. Omitted means "exec". */
1083
1092
  approval?: ToolApproval;
1084
1093
  /** Lines appended after the standard approval prompt header. */
1085
- formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
1094
+ formatApprovalDetails?: (args: unknown, options?: ApprovalDetailsOptions) => string | string[] | undefined;
1086
1095
  /** The main execution callback for this tool. */
1087
1096
  execute: AgentToolExecFn<TParameters, TDetails, TTheme>;
1088
1097
  /** Optional custom rendering for tool call display (returns UI component) */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-agent-core",
4
- "version": "18.10.3",
4
+ "version": "18.10.4",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -38,16 +38,16 @@
38
38
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
39
39
  },
40
40
  "dependencies": {
41
- "@oh-my-pi/pi-ai": "18.10.3",
42
- "@oh-my-pi/pi-catalog": "18.10.3",
43
- "@oh-my-pi/pi-natives": "18.10.3",
44
- "@oh-my-pi/pi-utils": "18.10.3",
45
- "@oh-my-pi/pi-wire": "18.10.3",
46
- "@oh-my-pi/snapcompact": "18.10.3",
41
+ "@oh-my-pi/pi-ai": "18.10.4",
42
+ "@oh-my-pi/pi-catalog": "18.10.4",
43
+ "@oh-my-pi/pi-natives": "18.10.4",
44
+ "@oh-my-pi/pi-utils": "18.10.4",
45
+ "@oh-my-pi/pi-wire": "18.10.4",
46
+ "@oh-my-pi/snapcompact": "18.10.4",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.10.3",
50
+ "@oh-my-pi/omptype": "18.10.4",
51
51
  "@opentelemetry/context-async-hooks": "^2.9.0",
52
52
  "@opentelemetry/sdk-trace-base": "^2.9.0",
53
53
  "@types/bun": "^1.3.14"
package/src/agent.ts CHANGED
@@ -504,7 +504,7 @@ export class Agent {
504
504
  #telemetry?: AgentLoopConfig["telemetry"];
505
505
  #appendOnlyContext?: AppendOnlyContextManager;
506
506
  #beforeQueuedMessageDequeueHooks = new Set<(signal?: AbortSignal) => Promise<void> | void>();
507
- #beforeModelCallHooks = new Set<(signal?: AbortSignal) => Promise<void> | void>();
507
+ #beforeModelCallHooks = new Set<(signal: AbortSignal | undefined, context: AgentContext) => Promise<void> | void>();
508
508
 
509
509
  /** Buffered Cursor tool results with text length at time of call (for correct ordering) */
510
510
  #cursorToolResultBuffer: CursorToolResultEntry[] = [];
@@ -964,15 +964,18 @@ export class Agent {
964
964
  return () => this.#beforeQueuedMessageDequeueHooks.delete(registration);
965
965
  }
966
966
 
967
- /** Register an independently removable hook that runs immediately before each model call. */
968
- addBeforeModelCallHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void {
969
- const registration = (signal?: AbortSignal) => hook(signal);
967
+ /** Register an independently removable hook that runs immediately before each model call, with the
968
+ * loop's live context (mutating `context.messages` changes what this and later calls send). */
969
+ addBeforeModelCallHook(
970
+ hook: (signal: AbortSignal | undefined, context: AgentContext) => Promise<void> | void,
971
+ ): () => void {
972
+ const registration = (signal: AbortSignal | undefined, context: AgentContext) => hook(signal, context);
970
973
  this.#beforeModelCallHooks.add(registration);
971
974
  return () => this.#beforeModelCallHooks.delete(registration);
972
975
  }
973
976
 
974
- async #runBeforeModelCallHooks(signal?: AbortSignal): Promise<void> {
975
- for (const hook of this.#beforeModelCallHooks) await hook(signal);
977
+ async #runBeforeModelCallHooks(signal: AbortSignal | undefined, context: AgentContext): Promise<void> {
978
+ for (const hook of this.#beforeModelCallHooks) await hook(signal, context);
976
979
  }
977
980
 
978
981
  async #runBeforeQueuedMessageDequeueHooks(signal?: AbortSignal): Promise<void> {
@@ -1884,7 +1887,7 @@ export class Agent {
1884
1887
  getApiKey: this.getApiKey,
1885
1888
  getToolContext: this.#getToolContext,
1886
1889
  syncContextBeforeModelCall: async (context, signal) => {
1887
- await this.#runBeforeModelCallHooks(signal);
1890
+ await this.#runBeforeModelCallHooks(signal, context);
1888
1891
  if (this.#listeners.size > 0) {
1889
1892
  await Bun.sleep(0);
1890
1893
  }
@@ -8,6 +8,7 @@
8
8
  import type { Api, ApiKey, AssistantMessage, Context, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
9
9
  import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
10
10
  import { prompt } from "@oh-my-pi/pi-utils";
11
+ import * as snapcompact from "@oh-my-pi/snapcompact";
11
12
  import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
12
13
  import { Tokenizer } from "../tokenizer";
13
14
  import type { AgentMessage } from "../types";
@@ -326,12 +327,22 @@ export async function generateBranchSummary(
326
327
  const tokenBudget = contextWindow - reserveTokens;
327
328
  const tokenizer = new Tokenizer(model);
328
329
 
329
- const { messages, fileOps } = prepareBranchEntries(entries, tokenizer, tokenBudget);
330
+ const { messages: branchMessages, fileOps } = prepareBranchEntries(entries, tokenizer, tokenBudget);
330
331
 
331
- if (messages.length === 0) {
332
+ if (branchMessages.length === 0) {
332
333
  return { summary: "No content to summarize" };
333
334
  }
334
335
 
336
+ // A snapcompact summary rendered for another model may carry reasoning
337
+ // sections and their legend, which Claude refuses (issue #6093).
338
+ const messages = snapcompact.readerIncludesThinking(model)
339
+ ? branchMessages
340
+ : branchMessages.map(message =>
341
+ message.role === "compactionSummary"
342
+ ? { ...message, summary: snapcompact.textWithoutThinking(message.summary) }
343
+ : message,
344
+ );
345
+
335
346
  // Transform to LLM-compatible messages, then serialize to text
336
347
  // Serialization prevents the model from treating it as a conversation to continue
337
348
  const llmMessages = (options.convertToLlm ?? defaultConvertToLlm)(messages);
@@ -673,14 +673,15 @@ export interface SummaryOptions {
673
673
  convertToLlm?: ConvertToLlm;
674
674
  /**
675
675
  * The provider context a live turn sends for `summarized` + `retained`, cut to
676
- * `summarized`. Anthropic on-demand compaction sends it so retained signed
677
- * thinking keeps its prefix; without it the request uses `remoteSystemPrompt`,
678
- * `tools` and `convertToLlm`.
676
+ * `summarized`, as read by the summarizer `model`. Anthropic on-demand
677
+ * compaction sends it so retained signed thinking keeps its prefix; without
678
+ * it the request uses `remoteSystemPrompt`, `tools` and `convertToLlm`.
679
679
  */
680
680
  buildProviderContext?: (
681
681
  summarized: AgentMessage[],
682
682
  retained: AgentMessage[],
683
- signal?: AbortSignal,
683
+ signal: AbortSignal | undefined,
684
+ model: Model,
684
685
  ) => Promise<Context>;
685
686
  /**
686
687
  * Whether a message is a turn the user wrote. Remote Compaction V2 keeps these
@@ -1647,12 +1648,28 @@ export async function compact(
1647
1648
  oneshotRetry: options?.oneshotRetry,
1648
1649
  };
1649
1650
 
1650
- const previousSnapcompactArchive = snapcompact.getPreservedArchive(previousPreserveData);
1651
+ // An archive keeps the reasoning policy of the model it was rendered for;
1652
+ // Claude refuses inputs that reproduce reasoning (issue #6093), so a
1653
+ // summarizer whose classifier refuses quoted reasoning (the
1654
+ // `classifier-cot-extraction` catalog axis) reads the archive's no-reasoning view (scrubbed
1655
+ // while its tool-output spans are still marked), and a summary with no
1656
+ // archive behind it is scrubbed of `¶think:` sections.
1657
+ const withoutThinking = !snapcompact.readerIncludesThinking(model);
1658
+ const storedSnapcompactArchive = snapcompact.getPreservedArchive(previousPreserveData);
1659
+ const snapcompactView =
1660
+ storedSnapcompactArchive && withoutThinking
1661
+ ? snapcompact.viewWithoutThinking(previousSummary ?? "", storedSnapcompactArchive)
1662
+ : undefined;
1663
+ const previousSnapcompactArchive = snapcompactView?.archive ?? storedSnapcompactArchive;
1664
+ const previousSummaryForReader =
1665
+ withoutThinking && previousSummary !== undefined
1666
+ ? (snapcompactView?.summary ?? snapcompact.textWithoutThinking(previousSummary))
1667
+ : previousSummary;
1651
1668
  const previousSnapcompactArchiveText = previousSnapcompactArchive
1652
1669
  ? snapcompact.archiveSourceText(previousSnapcompactArchive)
1653
1670
  : undefined;
1654
1671
  const previousSummaryForCompaction = mergePreviousSummaryWithSnapcompactArchive(
1655
- previousSummary,
1672
+ previousSummaryForReader,
1656
1673
  previousSnapcompactArchiveText,
1657
1674
  );
1658
1675
  const snapcompactArchiveMigrationMessage = previousSnapcompactArchiveText
@@ -1663,8 +1680,8 @@ export async function compact(
1663
1680
  // A local summary has no native payload to carry it into the first remote
1664
1681
  // request. Encode it as history; do not resend opaque native placeholders.
1665
1682
  const previousSummaryMigrationMessage =
1666
- settings.remoteEnabled !== false && previousSummary && !previousNativeHistory
1667
- ? createCompactionSummaryMessage(previousSummary, tokensBefore, new Date().toISOString())
1683
+ settings.remoteEnabled !== false && previousSummaryForReader && !previousNativeHistory
1684
+ ? createCompactionSummaryMessage(previousSummaryForReader, tokensBefore, new Date().toISOString())
1668
1685
  : undefined;
1669
1686
 
1670
1687
  let preserveData = withAnthropicCompactionPreserveData(
@@ -1981,7 +1998,7 @@ export async function compact(
1981
1998
  // history when the host can build them: kept thinking remains valid
1982
1999
  // only when the summarized prefix matches what was sent byte for byte.
1983
2000
  const context = summaryOptions.buildProviderContext
1984
- ? await summaryOptions.buildProviderContext(summarizedMessages, allMessages.slice(safeCut), signal)
2001
+ ? await summaryOptions.buildProviderContext(summarizedMessages, allMessages.slice(safeCut), signal, model)
1985
2002
  : {
1986
2003
  systemPrompt: summaryOptions.remoteSystemPrompt ?? [],
1987
2004
  messages: (summaryOptions.convertToLlm ?? defaultConvertToLlm)(summarizedMessages),
package/src/types.ts CHANGED
@@ -718,10 +718,20 @@ export interface SpeculativePhysicalOutcome {
718
718
  evidence?: unknown;
719
719
  }
720
720
 
721
+ /** How an approval prompt will show what `formatApprovalDetails` returns. */
722
+ export interface ApprovalDetailsOptions {
723
+ /**
724
+ * The prompt keeps only each detail's first line plus a count of the rest (the transcript
725
+ * shows the call in full): return bodies untruncated so the count is the whole value's.
726
+ * The host truncates the rows it keeps. Otherwise truncate long values as usual.
727
+ */
728
+ compact?: boolean;
729
+ }
730
+
721
731
  export interface SpeculativeToolReference {
722
732
  name: string;
723
733
  approval?: ToolApproval;
724
- formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
734
+ formatApprovalDetails?: (args: unknown, options?: ApprovalDetailsOptions) => string | string[] | undefined;
725
735
  }
726
736
 
727
737
  export interface SpeculativeOperationContext extends ToolSpeculationExecutionContext {
@@ -1218,7 +1228,7 @@ export interface AgentTool<
1218
1228
  approval?: ToolApproval;
1219
1229
 
1220
1230
  /** Lines appended after the standard approval prompt header. */
1221
- formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
1231
+ formatApprovalDetails?: (args: unknown, options?: ApprovalDetailsOptions) => string | string[] | undefined;
1222
1232
 
1223
1233
  /** The main execution callback for this tool. */
1224
1234
  execute: AgentToolExecFn<TParameters, TDetails, TTheme>;