@linxiraos/pi-agent-core 1.2.0 → 1.3.0

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
@@ -1,8 +1,42 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [1.2.0] - 2026-10-12
3
+ ## [1.3.0] - 2026-10-18
4
4
 
5
- - 版本线推进至 1.2.0;本版无独立用户可见变化。
5
+ ### Breaking Changes
6
+
7
+ - Removed `estimateImageContentTokens()` from `@linxiraos/pi-agent-core/image-tokens`; use `Tokenizer.imageTokens(size, detail)`, which prices with the reading model's catalog image rule ([#15446](https://github.com/can1357/oh-my-pi/pull/15446) by [@will-bogusz](https://github.com/will-bogusz))
8
+ - `applyShakeRegion()` and `applyShakeRegions()` take the replacement's token count (`replacementTokens`); a block rewrite of an assistant turn adds the tokens it removes to `elidedOutputTokens`, so a replayed Claude turn's count stays lowered after a reload ([#15446](https://github.com/can1357/oh-my-pi/pull/15446) by [@will-bogusz](https://github.com/will-bogusz))
9
+ - Removed `ApprovalDetailsOptions`; `formatApprovalDetails` takes only `args` again, since the compact approval prompt no longer shows tool details ([#15642](https://github.com/can1357/oh-my-pi/pull/15642) by [@H4vC](https://github.com/H4vC))
10
+
11
+ ### Added
12
+
13
+ - Added `AgentLoopConfig.onDetachedToolUpdate`: a tool's `onUpdate` frames that arrive after its run's event stream ended (a returned call whose background job keeps reporting) reach it instead of being dropped; `Agent` forwards them to its subscribers as `tool_execution_update` events
14
+ - `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))
15
+ - `Tokenizer.countLatestTurn`: counts a message as the transcript's latest assistant turn, which an Anthropic Messages replay keeps byte-for-byte ([#15791](https://github.com/can1357/oh-my-pi/pull/15791) by [@will-bogusz](https://github.com/will-bogusz))
16
+ - `Agent.prioritizeSteering`: moves given pending steering messages ahead of older ones so one-at-a-time delivery takes them first ([#15642](https://github.com/can1357/oh-my-pi/pull/15642))
17
+ - `AgentTool.delegationGuidance`: instructions a same-name extension replacement inherits in its description when it delegates to the native tool ([#15477](https://github.com/can1357/oh-my-pi/pull/15477) by [@iliaal](https://github.com/iliaal))
18
+ - `CompactionPreparation.lastSummarizedEntryId` and `CompactionResult.providerReplayThroughEntryId`: a remote compaction that could only fit the history before the kept tail reports the last entry its native history covers; later entries replay normally ([#15712](https://github.com/can1357/oh-my-pi/pull/15712) by [@H4vC](https://github.com/H4vC))
19
+ - `agent_start` carries `resumedToolCallIds` when a continued run first executes the calls of an unpaired assistant tail
20
+ - `prepareQueuedMessages` receives which queue (`steering` or `followUp`) a batch came from ([#16026](https://github.com/can1357/oh-my-pi/pull/16026))
21
+
22
+ ### Changed
23
+
24
+ - 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))
25
+
26
+ ### Fixed
27
+
28
+ - Fixed Claude context counts reading Opus 5.5 requests about 55% over their billed input: a replayed thinking turn now counts at the output size Claude billed for it, not its signature's bytes ([#15446](https://github.com/can1357/oh-my-pi/pull/15446) by [@will-bogusz](https://github.com/will-bogusz))
29
+ - Fixed screenshots and other non-frame images being counted at the OpenAI rate for every model instead of the reading model's own; Claude billed them about 16% higher ([#15446](https://github.com/can1357/oh-my-pi/pull/15446) by [@will-bogusz](https://github.com/will-bogusz))
30
+ - Fixed OpenAI Responses and Codex context estimates leaving out replayed reasoning: each turn whose encrypted reasoning actually replays now counts its reported reasoning tokens (its ciphertext when the backend reports none), and none while a cold session rebuilds it out of the request, so compaction and snapcompact sizing match billing; branch summaries no longer budget for encrypted reasoning they never send ([#15447](https://github.com/can1357/oh-my-pi/pull/15447) by [@will-bogusz](https://github.com/will-bogusz))
31
+ - 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))
32
+ - Fixed a Cursor tool call re-sent by the server persisting two results under one id; the re-run's result replaces the first ([#15749](https://github.com/can1357/oh-my-pi/issues/15749))
33
+ - Fixed compaction keeping more recent history than `keepRecentTokens` when the latest Claude turn on the Anthropic Messages API ended its tool calls on `stop` or `length`: the replay keeps that turn's signed reasoning, but the cut point counted only its visible blocks ([#15791](https://github.com/can1357/oh-my-pi/pull/15791) by [@will-bogusz](https://github.com/will-bogusz))
34
+ - Fixed native (OpenAI/Codex) remote compaction refusing or truncating sessions that fit the window: its preflight counted the JSON-escaped request, inflating quote-, backslash- and newline-heavy tool output such as disassembly by ~10–20%; it now counts text as the model reads it ([#15712](https://github.com/can1357/oh-my-pi/pull/15712) by [@H4vC](https://github.com/H4vC))
35
+ - Fixed OpenAI V1/V2 remote compaction failing outright when the history, kept tail included, exceeds the context window (a large final response or tool output after the last check); it retries on the history before the kept tail ([#15712](https://github.com/can1357/oh-my-pi/pull/15712) by [@H4vC](https://github.com/H4vC))
36
+ - Fixed fitted output caps overflowing the context window on strict hosts (llama.cpp-family) after turns with many parallel tool results; chat-template framing is now charged per locally counted message ([#15699](https://github.com/can1357/oh-my-pi/issues/15699))
37
+ - Fixed leaked DeepSeek DSML markup reaching saved history from reasoning blocks and from turns that also made a valid tool call ([#15485](https://github.com/can1357/oh-my-pi/issues/15485))
38
+ - Cancelling an `ask` after a background job finishes now reports the user cancellation instead of telling the model to retry a skipped tool ([#15985](https://github.com/can1357/oh-my-pi/issues/15985))
39
+ - Fixed Cursor turns failing with `PROVIDER_ERROR` when a follow-up (a finished background job, a late diagnostic, a reminder) arrived while the previous turn was still streaming ([#15821](https://github.com/can1357/oh-my-pi/pull/15821) by [@robfeldmann](https://github.com/robfeldmann))
6
40
 
7
41
  ## [9.2.2] - 2026-01-31
8
42
 
@@ -134,6 +168,10 @@
134
168
 
135
169
  Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mono](https://github.com/badlogic/pi-mono).
136
170
 
171
+ ## [1.2.0] - 2026-10-12
172
+
173
+ - 版本线推进至 1.2.0;本版无独立用户可见变化。
174
+
137
175
  ## [1.1.27] - 2026-10-07
138
176
 
139
177
  - 版本线推进至 1.1.27;本版无独立用户可见变化。
@@ -8,8 +8,8 @@ no rights in third-party code, fonts, data, or marks identified below.
8
8
  PYTHON DEPENDENCY DISTRIBUTION ROLE
9
9
  ===================================
10
10
 
11
- omp-rpc has no third-party runtime dependencies. robomp declares FastAPI,
12
- Uvicorn, HTTPX, Pydantic, pydantic-settings, python-dotenv, Click, and omp-rpc
11
+ zeta-rpc has no third-party runtime dependencies. robomp declares FastAPI,
12
+ Uvicorn, HTTPX, Pydantic, pydantic-settings, python-dotenv, Click, and zeta-rpc
13
13
  as ordinary installer-resolved dependencies. The Python wheels do not vendor
14
14
  those projects; each dependency remains a separately installed distribution
15
15
  carrying its own license metadata and files. Container/system assemblers must
@@ -79,9 +79,9 @@ uutils coreutils (https://github.com/uutils/coreutils), MIT
79
79
  --------------------------------------------------------------------------------
80
80
  Covers: base32, base64, basename, cat, cksum (shared checksum machinery),
81
81
  b2sum, md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, comm, cp,
82
- cut, date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nproc, paste,
83
- printenv, readlink, realpath, rm, seq, sort, stat, tac, tail, tee, touch, tr,
84
- truncate, uname, uniq, wc, whoami, yes.
82
+ cut, date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nl, nproc,
83
+ paste, printenv, readlink, realpath, rm, seq, sort, stat, tac, tail, tee,
84
+ touch, tr, truncate, uname, uniq, wc, whoami, yes.
85
85
 
86
86
  Copyright (c) uutils developers
87
87
 
@@ -824,6 +824,56 @@ public licenses.
824
824
 
825
825
  Creative Commons may be contacted at creativecommons.org.
826
826
 
827
+ -------------------------------------------------------------------------------
828
+ scripts/brand/fonts/ANSI Shadow.flf, scripts/brand/fonts/mini.flf
829
+
830
+ Two FIGlet font files from the standard FIGlet font distribution — the
831
+ figlet.org font set shipped by figlet.js
832
+ (https://github.com/patorjk/figlet.js/tree/master/fonts), fetched from
833
+ figlet.js 1.8.0 — vendored so the welcome/splash brand art
834
+ (`packages/tui/src/prompt/welcome.ts`) is generated reproducibly and offline
835
+ by `bun scripts/brand/generate-logo-art.ts`. Both files are unmodified and
836
+ retain their original FIGlet headers. They are build-time inputs only: no font
837
+ file or font data is compiled into, or shipped with, a Zeta release artifact.
838
+ The fonts' own headers carry their terms:
839
+
840
+ - `ANSI Shadow.flf` — "ANSI Shadow". Author unknown ("Font Author: ?" in the
841
+ header); a TheDraw-archive font converted with the FIGlet font editor
842
+ (http://patorjk.com/figfont-editor). Distributed under the FIGlet font
843
+ license as carried by the standard FIGlet font set: free to use and
844
+ redistribute with the header intact.
845
+ - `mini.flf` — "mini" by Glenn Chappell (4/93), from figlet release 2.1
846
+ (12 Aug 1994), "Includes ISO Latin-1"; modified by Paul Burton
847
+ <solution@earthlink.net> 12/96. Header grant: "Permission is hereby given to
848
+ modify this font, as long as the modifier's name is placed on a comment
849
+ line."
850
+
851
+ FIGlet renderer — `figlet` 1.8.0 (https://github.com/patorjk/figlet.js), a
852
+ root devDependency used only to generate the art at build time; it is not
853
+ imported by any runtime bundle or shipped artifact.
854
+
855
+ MIT License
856
+
857
+ Copyright (C) 2014-present Patrick Gillespie and other contributors
858
+
859
+ Permission is hereby granted, free of charge, to any person obtaining a copy
860
+ of this software and associated documentation files (the "Software"), to deal
861
+ in the Software without restriction, including without limitation the rights
862
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
863
+ copies of the Software, and to permit persons to whom the Software is
864
+ furnished to do so, subject to the following conditions:
865
+
866
+ The above copyright notice and this permission notice shall be included in
867
+ all copies or substantial portions of the Software.
868
+
869
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
870
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
871
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
872
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
873
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
874
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
875
+ SOFTWARE.
876
+
827
877
  BUN / NPM DEPENDENCY INVENTORY
828
878
  ==============================
829
879
  This inventory is traversed from bun.lock, not from the host node_modules tree.
@@ -3,7 +3,7 @@ import type { Dialect } from "@linxiraos/pi-ai/dialect";
3
3
  import type { HarmonyAuditEvent } from "@linxiraos/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
  }
@@ -395,8 +395,10 @@ export declare class Agent {
395
395
  get state(): AgentState;
396
396
  /**
397
397
  * Tokenizer for the active model. The instance is replaced whenever the
398
- * active model's encoding or snapcompact frame pricing changes (see
399
- * {@link setModel}), so callers must not cache it across model switches.
398
+ * active model's encoding, image or thinking-replay pricing, or
399
+ * replayed-reasoning reader changes (see {@link setModel}), or its Responses
400
+ * session turns cold or warm, so callers must not cache it across model
401
+ * switches or requests.
400
402
  */
401
403
  get tokenizer(): Tokenizer;
402
404
  get appendOnlyContext(): AppendOnlyContextManager | undefined;
@@ -425,8 +427,9 @@ export declare class Agent {
425
427
  onQueueChange(listener: () => void): () => void;
426
428
  /** Register an independently removable hook that runs before queued messages are consumed. */
427
429
  addBeforeQueuedMessageDequeueHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void;
428
- /** Register an independently removable hook that runs immediately before each model call. */
429
- 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;
430
433
  /**
431
434
  * Take back dequeued input the transcript has not recorded yet, ahead of an abort (Esc restores
432
435
  * it to the editor): steering live steering took for the in-flight response, then each batch
@@ -511,6 +514,13 @@ export declare class Agent {
511
514
  * Delivered after current tool execution, skips remaining tools.
512
515
  */
513
516
  steer(m: AgentMessage): void;
517
+ /**
518
+ * Move the given pending steering messages (by identity) ahead of the rest,
519
+ * keeping both groups' relative order, so one-at-a-time delivery takes them
520
+ * first. Messages no longer pending, including a claimed batch already
521
+ * being delivered, are ignored.
522
+ */
523
+ prioritizeSteering(messages: readonly AgentMessage[]): void;
514
524
  /**
515
525
  * Queue a follow-up message to be processed after the agent finishes.
516
526
  * Delivered only when agent has no more tool calls or steering messages.
@@ -28,6 +28,12 @@ export interface CompactionResult<T = unknown> {
28
28
  details?: T;
29
29
  /** Hook-provided data to persist alongside compaction entry. */
30
30
  preserveData?: Record<string, unknown>;
31
+ /**
32
+ * Last entry the provider-native replay history covers. Set when a remote
33
+ * compaction could only fit the history before the kept tail; later entries
34
+ * replay normally after the native history.
35
+ */
36
+ providerReplayThroughEntryId?: string;
31
37
  }
32
38
  export interface CompactionSettings {
33
39
  enabled: boolean;
@@ -131,6 +137,14 @@ export declare function shouldCompact(contextTokens: number, contextWindow: numb
131
137
  * takes the floor.)
132
138
  */
133
139
  export declare function compactionContextTokens(providerContextTokens: number, storedConversationEstimate: number): number;
140
+ /**
141
+ * The prompt budget below `contextWindow`: the window less
142
+ * {@link resolveBudgetReserveTokens}, capped at `contextWindow - 1` so it never
143
+ * reaches the whole window even when the reserve resolves to 0. Independent of
144
+ * any configured trigger; the default {@link resolveThresholdTokens} and
145
+ * length-stop recovery's "did the window run out" check both use it.
146
+ */
147
+ export declare function promptBudgetTokens(contextWindow: number, settings: CompactionSettings): number;
134
148
  export declare function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number;
135
149
  /**
136
150
  * True for entries that begin a conversational turn (a user request, a
@@ -187,11 +201,11 @@ export interface SummaryOptions {
187
201
  convertToLlm?: ConvertToLlm;
188
202
  /**
189
203
  * 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`.
204
+ * `summarized`, as read by the summarizer `model`. Anthropic on-demand
205
+ * compaction sends it so retained signed thinking keeps its prefix; without
206
+ * it the request uses `remoteSystemPrompt`, `tools` and `convertToLlm`.
193
207
  */
194
- buildProviderContext?: (summarized: AgentMessage[], retained: AgentMessage[], signal?: AbortSignal) => Promise<Context>;
208
+ buildProviderContext?: (summarized: AgentMessage[], retained: AgentMessage[], signal: AbortSignal | undefined, model: Model) => Promise<Context>;
195
209
  /**
196
210
  * Whether a message is a turn the user wrote. Remote Compaction V2 keeps these
197
211
  * next to the compaction item. Defaults to `role === "user"`; hosts whose
@@ -321,6 +335,8 @@ export interface CompactionPreparation {
321
335
  recentMessages: AgentMessage[];
322
336
  /** Entry IDs parallel to recentMessages, for an Anthropic-safe keep-tail boundary. */
323
337
  recentEntryIds?: string[];
338
+ /** Last entry before the kept tail; a remote compaction that cannot fit the tail covers history through it. */
339
+ lastSummarizedEntryId?: string;
324
340
  /** Whether this is a split turn (cut point in middle of turn) */
325
341
  isSplitTurn: boolean;
326
342
  tokensBefore: number;
@@ -31,7 +31,7 @@ export declare const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: string;
31
31
  /**
32
32
  * Tokens of stored Responses replacement history. Retained `input_image` parts
33
33
  * count at their image estimate rather than as base64 text; everything else,
34
- * opaque compaction state included, counts as serialized.
34
+ * opaque compaction state included, counts as decoded text.
35
35
  */
36
36
  export declare function countResponsesHistoryTokens(items: unknown[], tokenizer: Tokenizer): number;
37
37
  export interface TrimRemoteCompactionInputResult {
@@ -46,6 +46,15 @@ export interface TrimRemoteCompactionInputResult {
46
46
  */
47
47
  fits: boolean;
48
48
  }
49
+ /** Options for {@link trimRemoteCompactionInputToContextWindow}. */
50
+ export interface TrimRemoteCompactionOptions {
51
+ /**
52
+ * Whether oversized trailing tool outputs may be rewritten (default true).
53
+ * False when a caller can compact a shorter prefix instead, so a kept tool
54
+ * result replays intact rather than being replaced.
55
+ */
56
+ rewriteOutputs?: boolean;
57
+ }
49
58
  /**
50
59
  * Preserve the full native transcript unless trailing tool outputs alone push a
51
60
  * remote compaction request beyond the model window. Replacing only those
@@ -54,7 +63,7 @@ export interface TrimRemoteCompactionInputResult {
54
63
  * fits but whose image estimate does not is still sendable: refusing it on an
55
64
  * image guess would skip native compaction the provider would have accepted.
56
65
  */
57
- export declare function trimRemoteCompactionInputToContextWindow(input: Array<Record<string, unknown>>, tokenizer: Tokenizer, contextWindow: number | null | undefined, instructions: string, tools?: unknown[]): TrimRemoteCompactionInputResult;
66
+ export declare function trimRemoteCompactionInputToContextWindow(input: Array<Record<string, unknown>>, tokenizer: Tokenizer, contextWindow: number | null | undefined, instructions: string, tools?: unknown[], options?: TrimRemoteCompactionOptions): TrimRemoteCompactionInputResult;
58
67
  /**
59
68
  * Refuse a native compaction request whose prepared input cannot fit the model
60
69
  * window, before any network I/O. Re-expanded history behind an unreadable
@@ -117,6 +126,8 @@ export declare function requestOpenAiRemoteCompaction(model: Model, apiKey: stri
117
126
  sessionId?: string;
118
127
  providerSessionState?: Map<string, ProviderSessionState>;
119
128
  codexCompaction?: CodexCompactionContext;
129
+ /** Forwarded to {@link trimRemoteCompactionInputToContextWindow}. */
130
+ rewriteOutputs?: boolean;
120
131
  }): Promise<OpenAiRemoteCompactionResponse>;
121
132
  /**
122
133
  * Generic remote-compaction POST. Two wire shapes are auto-selected by
@@ -87,12 +87,13 @@ export declare function collectShakeRegions(entries: SessionEntry[], tokenizer:
87
87
  *
88
88
  * Tool-result: replaces its first non-empty text block with the placeholder,
89
89
  * removes its other text blocks, preserves every non-text block, and stamps
90
- * `prunedAt`. Block: splices `replacement` over `[start, end)` of the
91
- * target text block. When several block regions share one text block they MUST
92
- * be applied highest-start-first so earlier offsets stay valid — use
93
- * {@link applyShakeRegions}, which orders them correctly.
90
+ * `prunedAt`. Block: splices `replacement` (`replacementTokens` long) over
91
+ * `[start, end)` of the target text block, and on an assistant turn adds the
92
+ * tokens removed to `elidedOutputTokens`. When several block regions share one
93
+ * text block they MUST be applied highest-start-first so earlier offsets stay
94
+ * valid — use {@link applyShakeRegions}, which orders them correctly.
94
95
  */
95
- export declare function applyShakeRegion(region: ShakeRegion, replacement: string): void;
96
+ export declare function applyShakeRegion(region: ShakeRegion, replacement: string, replacementTokens: number): void;
96
97
  /**
97
98
  * Apply many regions at once. Block regions are applied highest-start-first so
98
99
  * that splicing one region never shifts the offsets of another in the same text
@@ -101,4 +102,5 @@ export declare function applyShakeRegion(region: ShakeRegion, replacement: strin
101
102
  export declare function applyShakeRegions(items: Array<{
102
103
  region: ShakeRegion;
103
104
  replacement: string;
105
+ replacementTokens: number;
104
106
  }>): void;
@@ -58,11 +58,16 @@ export declare function truncateToolResultForSummary(text: string): string;
58
58
  export declare function escapeSummaryBoundaryTags(text: string): string;
59
59
  /**
60
60
  * Serialize LLM messages as plain summary input without provider control tokens.
61
+ * `dropThinking` is the summarizer's resolved `classifier-cot-extraction` policy.
61
62
  */
62
- export declare function serializeConversationForSummary(messages: Message[], dialect?: Dialect): string;
63
+ export declare function serializeConversationForSummary(messages: Message[], dialect?: Dialect, dropThinking?: boolean): string;
63
64
  /**
64
65
  * Serialize LLM messages to transcript text.
65
66
  * Call convertToLlm() first to handle custom message types.
67
+ * `dropThinking` omits assistant reasoning from dialect transcripts: set it from
68
+ * the reader's resolved `classifier-cot-extraction` catalog policy (Claude's
69
+ * classifier refuses inputs that reproduce the model's own reasoning as text,
70
+ * "reasoning_extraction"). Reasoning is ephemeral and low-signal for a summary.
66
71
  */
67
- export declare function serializeConversation(messages: Message[], dialect?: Dialect): string;
72
+ export declare function serializeConversation(messages: Message[], dialect?: Dialect, dropThinking?: boolean): string;
68
73
  export declare const SUMMARIZATION_SYSTEM_PROMPT: string;
@@ -1,29 +1,26 @@
1
1
  import type { ImageContent } from "@linxiraos/pi-ai";
2
- import { type ImageSize } from "@linxiraos/pi-catalog/compat/image-tokenization";
2
+ import { type ImageSize, type ImageTokenization } from "@linxiraos/pi-catalog/compat/image-tokenization";
3
3
  /**
4
4
  * Dimension-based image token estimates shared by the local context counter
5
5
  * ({@link Tokenizer.countMessage}) and the native remote-compaction fit probe.
6
6
  * Both sides must price the same image the same way: when they disagreed
7
7
  * (1.2k flat locally vs a 12k flat worst case remotely) an image-heavy session
8
8
  * stayed under the local compaction trigger yet was refused as over-window by
9
- * the remote probe.
10
- *
11
- * Follows the catalog's image rule for the OpenAI Responses wire (GPT-5.5's
12
- * 32px patches, a per-detail pixel limit and patch budget, x1.2 multiplier),
13
- * which is also a close upper estimate for other providers once omp has
14
- * downscaled the image (≤1568px by default).
9
+ * the remote probe. Both price with the reading model's catalog image rule
10
+ * ({@link Tokenizer.imageTokens}); a reader without one falls back to the
11
+ * OpenAI Responses wire's rule (GPT-5.5's 32px patches, a per-detail pixel
12
+ * limit and patch budget, x1.2 multiplier).
15
13
  */
16
14
  /** OpenAI image `detail` values; `undefined` means the provider default (`auto`). */
17
15
  export type ImageDetail = ImageContent["detail"];
18
16
  export type { ImageSize };
19
17
  /**
20
- * Estimated input tokens for one image. Unknown dimensions (undecodable data,
21
- * remote URLs, provider file ids) charge the detail level's full patch budget.
18
+ * Estimated input tokens for one image under `rule`, the OpenAI Responses
19
+ * wire's by default. Unknown dimensions (undecodable data, remote URLs,
20
+ * provider file ids) charge the rule's most expensive size.
22
21
  */
23
- export declare function estimateImageTokens(size: ImageSize | null | undefined, detail?: ImageDetail): number;
22
+ export declare function estimateImageTokens(size: ImageSize | null | undefined, detail?: ImageDetail, rule?: ImageTokenization): number;
24
23
  /** Pixel size read from the header of base64-encoded image data, or null. */
25
24
  export declare function base64ImageSize(base64: string): ImageSize | null;
26
25
  /** Pixel size of a `data:<mime>;base64,` URL, or null for any other URL. */
27
26
  export declare function dataUrlImageSize(url: string): ImageSize | null;
28
- /** {@link estimateImageTokens} for an in-memory image block. */
29
- export declare function estimateImageContentTokens(image: Pick<ImageContent, "data" | "detail">): number;
@@ -2,14 +2,22 @@ import type { Context, Model } from "@linxiraos/pi-ai";
2
2
  import type { Tokenizer } from "./tokenizer.js";
3
3
  /** Smallest output cap {@link fitOutputTokensToContextWindow} will request. */
4
4
  export declare const MIN_FITTED_OUTPUT_TOKENS = 1024;
5
+ /**
6
+ * Chat-template framing charged per locally counted message. `Tokenizer.countMessage`
7
+ * counts content only, but every template wraps each message in role markers and
8
+ * separators, and tool results in response wrappers: about 5 tokens per merged
9
+ * Qwen3 `<tool_response>` or Llama 3 turn, 10–15 for harmony or Mistral tool results.
10
+ * The proportional margin scales with content, so many short messages (a turn's
11
+ * parallel tool results) leave this uncovered. Provider-anchored messages need no
12
+ * charge: the host's own report already includes their framing.
13
+ */
14
+ export declare const MESSAGE_FRAMING_TOKENS = 16;
5
15
  /**
6
16
  * Absolute headway subtracted from the remaining room (unconditionally, on anchored and
7
- * fully-local counts alike). Proportional padding covers
8
- * tokenizer drift that scales with the prompt, but a host can still count a few tokens more
9
- * than any local estimate can see (chat-template framing, reasoning wrappers). Measured
10
- * against a 262,144-token host: fitted caps landed +11 to +42 tokens over and were rejected
11
- * with 400s. 64 tokens covers the observed drift with margin to spare; the
12
- * {@link MIN_FITTED_OUTPUT_TOKENS} floor still applies.
17
+ * fully-local counts alike). Covers per-request framing no message count sees: the
18
+ * generation prompt, the anchoring turn's end marker, reasoning wrappers. Measured
19
+ * against a 262,144-token host: fitted caps landed +11 to +42 tokens over and were
20
+ * rejected with 400s. The {@link MIN_FITTED_OUTPUT_TOKENS} floor still applies.
13
21
  */
14
22
  export declare const OUTPUT_FIT_HEADWAY_TOKENS = 64;
15
23
  /**
@@ -27,7 +35,9 @@ export declare const OUTPUT_FIT_HEADWAY_TOKENS = 64;
27
35
  * The prompt size is the provider's own report from the newest trustworthy
28
36
  * assistant turn (see {@link findRequestUsageAnchor}) plus a local count of
29
37
  * only the messages appended after it; the whole context is counted locally
30
- * only when no turn can anchor (fresh or freshly rewritten context).
38
+ * only when no turn can anchor (fresh or freshly rewritten context). Local
39
+ * counts carry a proportional tokenizer margin plus
40
+ * {@link MESSAGE_FRAMING_TOKENS} per message.
31
41
  *
32
42
  * Returns `maxTokens` unchanged when the requested cap already fits, the
33
43
  * model declares no window, the host ends generation at the window itself
@@ -1,9 +1,27 @@
1
1
  import type { Model } from "@linxiraos/pi-ai";
2
+ import { type ThinkingReplayTokens } from "@linxiraos/pi-catalog/compat/thinking-replay";
2
3
  import * as natives from "@linxiraos/pi-natives";
3
4
  import * as snapcompact from "@linxiraos/pi-snapcompact";
5
+ import { type ImageDetail, type ImageSize } from "./image-tokens.js";
4
6
  import type { AgentMessage } from "./types.js";
5
7
  /** Maps the catalog-resolved tokenizer family to its native implementation. */
6
8
  export declare function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null | undefined): natives.Encoding | null;
9
+ type TokenizerModel = Pick<Model, "tokenizer"> & snapcompact.ShapeTarget & {
10
+ compat?: Model["compat"];
11
+ thinking?: Model["thinking"];
12
+ };
13
+ /** Identity of the reader whose replayed reasoning a {@link Tokenizer} counts; equal keys count it alike. */
14
+ export declare function reasoningReplayKey(model: TokenizerModel | null | undefined): string;
15
+ /** Request state a {@link Tokenizer} counts for, beyond its model. */
16
+ export interface TokenizerOptions {
17
+ /**
18
+ * Whether the request replays native Responses history (pi-ai's
19
+ * `openAIResponsesReplaysNativeHistory`). A cold session rebuilds a turn
20
+ * holding encrypted reasoning from its text and tool calls, without the
21
+ * reasoning or its signature. Default: true.
22
+ */
23
+ nativeHistoryReplay?: boolean;
24
+ }
7
25
  /**
8
26
  * `strict` always tries an exact native count (the catalog-resolved tokenizer
9
27
  * when known, o200k_base otherwise), falling back to the byte upper bound when
@@ -19,11 +37,15 @@ export interface MessageCountOptions {
19
37
  /**
20
38
  * Drop opaque provider reasoning payloads (`thinkingSignature`,
21
39
  * `redactedThinking`, native server-tool blocks) from the estimate. Those
22
- * are billed by the provider on replay, so the default counts them — but
23
- * their *local* byte size can diverge wildly from what the provider
24
- * charges, so the compaction floor (which only needs the reliably-countable,
25
- * on-wire-compressible content) excludes them to avoid false triggers on
26
- * thinking-heavy turns.
40
+ * are billed by the provider on replay, so the default counts them (a reader
41
+ * billing replayed thinking at `output` size: the reasoning, at its turn's
42
+ * output size) — but their *local* byte size can diverge wildly from what
43
+ * the provider charges, so the compaction floor (which only needs the
44
+ * reliably-countable, on-wire-compressible content) excludes them to avoid
45
+ * false triggers on thinking-heavy turns.
46
+ * Native Responses reasoning that actually replays is not an opaque byte
47
+ * guess: it is charged at the turn's provider-reported reasoning tokens in
48
+ * both arms.
27
49
  */
28
50
  excludeEncryptedReasoning?: boolean;
29
51
  }
@@ -43,15 +65,28 @@ export interface TokenBudgetCheck {
43
65
  * Model-aware local token counter. Immutable: the catalog-resolved encoding
44
66
  * is fixed at construction, so a cached count can never straddle two
45
67
  * encodings. An `Agent` owns one for its active model (swapping the instance
46
- * when the model's encoding or snapcompact frame pricing changes); one-shot
47
- * flows construct their own for the model that will be billed. Known
48
- * tokenizer families use exact native counts; unknown models keep the fast
49
- * byte estimate (or o200k when `PI_TOKENIZER_ACCURATE=1`).
68
+ * when the model's encoding, image or thinking-replay pricing, replay
69
+ * identity or native-history replay state changes); one-shot flows construct
70
+ * their own for the model that will be billed. Known tokenizer families use
71
+ * exact native counts; unknown models keep the fast byte estimate (or o200k
72
+ * when `PI_TOKENIZER_ACCURATE=1`).
50
73
  */
51
74
  export declare class Tokenizer {
52
75
  #private;
53
76
  readonly frameBillingKey: string;
54
- constructor(model?: (Pick<Model, "tokenizer"> & snapcompact.ShapeTarget) | null);
77
+ readonly reasoningReplayKey: string;
78
+ /** How the reader bills replayed signed thinking (catalog `thinking-replay-tokens`). */
79
+ readonly thinkingReplayTokens: ThinkingReplayTokens;
80
+ /** Whether the counted request replays native Responses history; see {@link TokenizerOptions}. */
81
+ readonly nativeHistoryReplay: boolean;
82
+ /**
83
+ * Counts what `streamSimple` sends `model`. Its catalog compat decides
84
+ * whether Responses reasoning history is filtered: `streamOpenAIResponses`'
85
+ * per-request `filterReasoningHistory` override is a debugging hook
86
+ * `streamSimple` never forwards, so a caller using it passes `model` with
87
+ * that compat set.
88
+ */
89
+ constructor(model?: TokenizerModel | null, options?: TokenizerOptions);
55
90
  get encoding(): natives.Encoding | null;
56
91
  countTokens(text: string | string[], mode?: TokenCountMode): number;
57
92
  /**
@@ -72,10 +107,35 @@ export declare class Tokenizer {
72
107
  * Settled historical messages are counted once and reused until an owner
73
108
  * (prune/shake/strip-images) calls `invalidateMessageCache`; streaming
74
109
  * assistants bypass the memo entirely (see the message-cache settle-gate
75
- * invariant). Image blocks charge {@link estimateImageContentTokens}, the
76
- * same dimension-based estimate the native remote-compaction probe applies.
110
+ * invariant). Image blocks charge {@link imageTokens}, the same estimate
111
+ * the native remote-compaction probe applies.
77
112
  */
78
113
  countMessage(message: AgentMessage, options?: MessageCountOptions): number;
114
+ /**
115
+ * {@link countMessage} for the transcript's latest surviving assistant turn
116
+ * (`isSurvivingAssistant`), which an Anthropic Messages replay keeps
117
+ * byte-for-byte (`transformMessages`): a turn whose tool calls ended without
118
+ * a tool-use stop keeps its signatures then, so its reasoning is charged. A
119
+ * count of one message cannot see its position; a caller walking back from
120
+ * the end of the transcript counts that turn here. Not memoized: it differs
121
+ * from {@link countMessage} only for that one turn.
122
+ */
123
+ countLatestTurn(message: AgentMessage, options?: MessageCountOptions): number;
79
124
  /** Sum of {@link countMessage} over `messages`. */
80
125
  countMessages(messages: readonly AgentMessage[], options?: MessageCountOptions): number;
126
+ /**
127
+ * Input tokens for one image (not a snapcompact frame) under the reader's
128
+ * catalog image rule, or the generic estimate when it has none.
129
+ */
130
+ imageTokens(size: ImageSize | null | undefined, detail?: ImageDetail): number;
131
+ /**
132
+ * The replayed native Responses reasoning {@link countMessage} charges
133
+ * across `messages`: the part of a count that changes with the reading
134
+ * model. A replay whose backend reported no reasoning tokens counts at the
135
+ * signature price `countMessage` gives it.
136
+ */
137
+ replayedReasoningTokens(messages: readonly AgentMessage[]): number;
138
+ /** Whether a Tokenizer built for `model` with `options` would count every message as this one does. */
139
+ countsAlike(model: TokenizerModel | null | undefined, options?: TokenizerOptions): boolean;
81
140
  }
141
+ export {};
@@ -12,8 +12,10 @@ export interface QueuedMessagePreparation {
12
12
  /** Append context after the originals; undefined stops this attempt, retaining originals unless explicitly removed. */
13
13
  commit(): readonly AgentMessage[] | undefined;
14
14
  }
15
+ /** Which agent queue a batch came from: steering amends the running task, follow-ups start the next one. */
16
+ export type QueuedMessageQueue = "steering" | "followUp";
15
17
  /** Prepare an exclusively claimed batch. Undefined delivers unchanged; the signal also aborts when the claim is cancelled. */
16
- export type PrepareQueuedMessages = (messages: readonly AgentMessage[], signal: AbortSignal) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
18
+ export type PrepareQueuedMessages = (messages: readonly AgentMessage[], signal: AbortSignal, queue: QueuedMessageQueue) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
17
19
  /** Called once an aside has been inserted into the agent's live context. */
18
20
  export declare const ASIDE_MESSAGE_COMMIT: unique symbol;
19
21
  /** Symbol-keyed handoff for one finalized, tool-owned stream speculation session. */
@@ -279,6 +281,15 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
279
281
  * tools finish first.
280
282
  */
281
283
  hasQueuedAsides?: () => boolean | Promise<boolean>;
284
+ /**
285
+ * Receives a tool's `onUpdate` frames that arrive after this run's event
286
+ * stream ended: a call that returned while its background job keeps
287
+ * reporting (`details.async`), whose run yielded before the job settled.
288
+ * Without it those frames are dropped.
289
+ */
290
+ onDetachedToolUpdate?: (event: Extract<AgentEvent, {
291
+ type: "tool_execution_update";
292
+ }>) => void;
282
293
  /**
283
294
  * Returns follow-up messages to process after the agent would otherwise stop.
284
295
  *
@@ -1000,6 +1011,11 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
1000
1011
  loadMode?: ToolLoadMode;
1001
1012
  /** Short one-line summary used for tool discovery indexes. */
1002
1013
  summary?: string;
1014
+ /**
1015
+ * Instructions a same-name extension replacement inherits in its description, because results it
1016
+ * obtains by delegating to this native tool through `ctx.invokeTool` follow this tool's contract.
1017
+ */
1018
+ readonly delegationGuidance?: string;
1003
1019
  /**
1004
1020
  * On-demand documentation topics (`topic → markdown`), readable as
1005
1021
  * `xd://<tool>/<topic>`. Lets a tool keep large sub-surfaces out of its
@@ -1092,6 +1108,11 @@ export interface AgentContext {
1092
1108
  */
1093
1109
  export type AgentEvent = {
1094
1110
  type: "agent_start";
1111
+ /**
1112
+ * Calls of the unpaired assistant tail a continued run executes before its
1113
+ * first model request. They run without a new assistant `message_end`.
1114
+ */
1115
+ resumedToolCallIds?: string[];
1095
1116
  } | {
1096
1117
  type: "agent_end";
1097
1118
  messages: AgentMessage[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-agent-core",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Linxira-OS",
@@ -28,26 +28,26 @@
28
28
  "types": "./dist/types/index.d.ts",
29
29
  "scripts": {
30
30
  "check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
31
- "check:types": "tsgo -p tsconfig.json --noEmit",
31
+ "check:types": "tsc -p tsconfig.json --noEmit",
32
32
  "lint": "oxlint .",
33
33
  "test": "bun test --parallel",
34
34
  "fix": "oxlint --fix --fix-suggestions . && bun run fmt",
35
35
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
36
36
  },
37
37
  "dependencies": {
38
- "@linxiraos/pi-ai": "1.2.0",
39
- "@linxiraos/pi-catalog": "1.2.0",
40
- "@linxiraos/pi-natives": "1.2.0",
41
- "@linxiraos/pi-utils": "1.2.0",
42
- "@linxiraos/pi-wire": "1.2.0",
43
- "@linxiraos/pi-snapcompact": "1.2.0",
38
+ "@linxiraos/pi-ai": "1.3.0",
39
+ "@linxiraos/pi-catalog": "1.3.0",
40
+ "@linxiraos/pi-natives": "1.3.0",
41
+ "@linxiraos/pi-utils": "1.3.0",
42
+ "@linxiraos/pi-wire": "1.3.0",
43
+ "@linxiraos/pi-snapcompact": "1.3.0",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@linxiraos/pi-omptype": "1.2.0",
47
+ "@linxiraos/pi-omptype": "1.3.0",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
- "@types/bun": "^1.3.14"
50
+ "@types/bun": "1.4.3"
51
51
  },
52
52
  "engines": {
53
53
  "bun": ">=1.3.14"