@linxiraos/pi-agent-core 1.1.26 → 1.1.27
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 +23 -2
- package/dist/types/agent.d.ts +12 -5
- package/dist/types/compaction/anthropic.d.ts +14 -8
- package/dist/types/compaction/compaction-v2-streaming.d.ts +5 -1
- package/dist/types/compaction/compaction.d.ts +14 -0
- package/dist/types/compaction/entries.d.ts +2 -2
- package/dist/types/compaction/openai.d.ts +14 -2
- package/dist/types/compaction/pruning.d.ts +33 -9
- package/dist/types/compaction/utils.d.ts +5 -0
- package/dist/types/image-tokens.d.ts +29 -0
- package/dist/types/telemetry.d.ts +10 -0
- package/dist/types/tokenizer.d.ts +9 -6
- package/package.json +8 -8
- package/src/agent-loop.ts +176 -19
- package/src/agent.ts +77 -17
- package/src/compaction/anthropic.ts +30 -12
- package/src/compaction/compaction-v2-streaming.ts +13 -15
- package/src/compaction/compaction.ts +127 -31
- package/src/compaction/entries.ts +2 -2
- package/src/compaction/openai.ts +63 -28
- package/src/compaction/pruning.ts +81 -21
- package/src/compaction/utils.ts +9 -0
- package/src/image-tokens.ts +77 -0
- package/src/prompts/dsml-tool-call-leak.md +19 -0
- package/src/telemetry.ts +19 -1
- package/src/tokenizer.ts +24 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [1.1.
|
|
5
|
+
## [1.1.27] - 2026-10-07
|
|
6
6
|
|
|
7
|
-
- 版本线推进至 1.1.
|
|
7
|
+
- 版本线推进至 1.1.27;本版无独立用户可见变化。
|
|
8
|
+
|
|
9
|
+
### Breaking Changes
|
|
10
|
+
|
|
11
|
+
- `Agent.withdrawUndeliveredQueuedMessages()` replaces `withdrawLiveSteering()` and returns `{ steering, followUp }`: it also takes back queued input already dequeued for the next model call, which the aborted run then neither records nor reports in `agent_end` ([#14179](https://github.com/can1357/oh-my-pi/pull/14179) by [@andrebrait](https://github.com/andrebrait))
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `Agent.setOnModelCallSystemPrompt`, called with the exact system prompt each model call is built from ([#14338](https://github.com/can1357/oh-my-pi/pull/14338) by [@will-bogusz](https://github.com/will-bogusz))
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `SessionInitEntry.systemPrompt` holds the system prompt blocks as sent; session files written earlier keep one joined string ([#14338](https://github.com/can1357/oh-my-pi/pull/14338) by [@will-bogusz](https://github.com/will-bogusz))
|
|
20
|
+
- Image token estimates now use the catalog's `imageTokens()` formula with its OpenAI Responses wire rule; values are unchanged ([#14286](https://github.com/can1357/oh-my-pi/pull/14286) by [@will-bogusz](https://github.com/will-bogusz)).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed the context count and auto-compaction trigger pricing every snapcompact archive frame at Opus's high-res rate, which overcounted Codex and Gemini sessions and could re-trigger compaction right after compacting ([#14291](https://github.com/can1357/oh-my-pi/pull/14291) by [@will-bogusz](https://github.com/will-bogusz))
|
|
25
|
+
- Fixed native (OpenAI) compaction being refused as over the context window in sessions with many screenshots. Images were counted as about 1,200 tokens when deciding to compact but 12,000 when checking whether the compaction request fits; both checks now estimate images from their actual dimensions, and a request is no longer refused when only the image estimate pushes it over the window ([#14260](https://github.com/can1357/oh-my-pi/pull/14260) by [@H4vC](https://github.com/H4vC)).
|
|
26
|
+
- Fixed OpenAI and Codex Remote Compaction V2 dropping your recent messages instead of keeping them next to the compaction summary ([#14247](https://github.com/can1357/oh-my-pi/pull/14247) by [@will-bogusz](https://github.com/will-bogusz))
|
|
27
|
+
- Fixed the failed V2 remote compaction warning claiming a V1 fallback on Codex, where V1 does not run ([#14245](https://github.com/can1357/oh-my-pi/pull/14245) by [@will-bogusz](https://github.com/will-bogusz))
|
|
28
|
+
- Fixed Anthropic native compaction being rejected with `Invalid signature in thinking block` (or silently dropping the summarized thinking) on models with preserved thinking ([#14251](https://github.com/can1357/oh-my-pi/pull/14251) by [@will-bogusz](https://github.com/will-bogusz))
|
|
8
29
|
|
|
9
30
|
## [14.9.5] - 2026-05-12
|
|
10
31
|
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -390,8 +390,8 @@ export declare class Agent {
|
|
|
390
390
|
get state(): AgentState;
|
|
391
391
|
/**
|
|
392
392
|
* Tokenizer for the active model. The instance is replaced whenever the
|
|
393
|
-
* active model's encoding
|
|
394
|
-
* not cache it across model switches.
|
|
393
|
+
* active model's encoding or snapcompact frame pricing changes (see
|
|
394
|
+
* {@link setModel}), so callers must not cache it across model switches.
|
|
395
395
|
*/
|
|
396
396
|
get tokenizer(): Tokenizer;
|
|
397
397
|
get appendOnlyContext(): AppendOnlyContextManager | undefined;
|
|
@@ -422,10 +422,15 @@ export declare class Agent {
|
|
|
422
422
|
/** Register an independently removable hook that runs immediately before each model call. */
|
|
423
423
|
addBeforeModelCallHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void;
|
|
424
424
|
/**
|
|
425
|
-
* Take back
|
|
426
|
-
* the
|
|
425
|
+
* Take back dequeued input the transcript has not recorded yet, ahead of an abort (Esc restores
|
|
426
|
+
* it to the editor): steering live steering took for the in-flight response, then each batch
|
|
427
|
+
* dequeued for the next model call, oldest first. The aborted run then neither records it (or
|
|
428
|
+
* the context prepared for it) nor requeues it.
|
|
427
429
|
*/
|
|
428
|
-
|
|
430
|
+
withdrawUndeliveredQueuedMessages(): {
|
|
431
|
+
steering: AgentMessage[];
|
|
432
|
+
followUp: AgentMessage[];
|
|
433
|
+
};
|
|
429
434
|
/** Steering live steering took for the streaming response; the transcript records it once
|
|
430
435
|
* that response (or its tool batch) ends, which is when the model switches to it. */
|
|
431
436
|
peekLiveSteeredMessages(): AgentMessage[];
|
|
@@ -434,6 +439,8 @@ export declare class Agent {
|
|
|
434
439
|
setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
|
|
435
440
|
setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
|
|
436
441
|
setOnTurnEnd(fn: ((messages: AgentMessage[], signal?: AbortSignal, context?: AgentTurnEndContext) => Promise<void> | void) | undefined): void;
|
|
442
|
+
/** Called with the exact system prompt each model call is built from, after before-model-call hooks. */
|
|
443
|
+
setOnModelCallSystemPrompt(fn: ((systemPrompt: string[]) => void) | undefined): void;
|
|
437
444
|
/**
|
|
438
445
|
* Install or replace the host pre-model-call gate; pass `undefined` to
|
|
439
446
|
* remove it. Gates are sampled when a run starts: installing the first
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Anthropic on-demand compaction (`compact-2026-09-04` beta).
|
|
3
3
|
*
|
|
4
|
-
* The request sends only the prefix to summarize,
|
|
5
|
-
* system prompt, tools and thinking settings.
|
|
6
|
-
* replaces that prefix; the retained tail is
|
|
4
|
+
* The request sends only the prefix to summarize, as the live conversation
|
|
5
|
+
* sent it: same system prompt, tools, history bytes and thinking settings.
|
|
6
|
+
* The returned signed block replaces that prefix; the retained tail is
|
|
7
|
+
* replayed from session entries.
|
|
7
8
|
*/
|
|
8
|
-
import type { AnthropicCompactionPayload, ApiKey,
|
|
9
|
+
import type { AnthropicCompactionFiles, AnthropicCompactionPayload, ApiKey, Context, Effort, Model, SimpleStreamOptions, Usage } from "@linxiraos/pi-ai";
|
|
9
10
|
import { type InstrumentedChatSpanOptions } from "../telemetry.js";
|
|
10
11
|
import type { AgentMessage } from "../types.js";
|
|
11
12
|
export declare const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
|
|
@@ -17,8 +18,12 @@ export interface AnthropicCompactionPreserveData {
|
|
|
17
18
|
signature?: string;
|
|
18
19
|
/** Legacy threshold block state; replay-only. */
|
|
19
20
|
encryptedContent?: string;
|
|
20
|
-
/** Harness file metadata (`<files>` section) replayed after the
|
|
21
|
+
/** Harness file metadata (`<files>` section) replayed after the retained tail. */
|
|
21
22
|
filesText?: string;
|
|
23
|
+
/** Earlier summaries' file metadata replayed inside the retained tail. */
|
|
24
|
+
retainedFiles?: AnthropicCompactionFiles[];
|
|
25
|
+
/** The retained tail replays unchanged (see `AnthropicCompactionPayload.exactTail`). */
|
|
26
|
+
exactTail?: true;
|
|
22
27
|
/** Model that wrote the summary. */
|
|
23
28
|
model?: string;
|
|
24
29
|
/** Prompt tokens the compaction request processed, for display. */
|
|
@@ -51,10 +56,11 @@ export declare function findAnthropicCompactionCut(messages: readonly (AgentMess
|
|
|
51
56
|
/** Instructions replace the API default; the request contains only summarized messages. */
|
|
52
57
|
export declare function buildAnthropicCompactionInstructions(basePrompt: string, customInstructions: string | undefined, extraContext: string | undefined): string;
|
|
53
58
|
export interface AnthropicNativeCompactionRequest {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
tools?: Tool[];
|
|
59
|
+
/** The summarized prefix as the live turn would send it. */
|
|
60
|
+
context: Context;
|
|
57
61
|
instructions: string;
|
|
62
|
+
/** Replayed file metadata due before this time ends the request (see `AnthropicCompactionRequest.filesDueBefore`). */
|
|
63
|
+
filesDueBefore?: number;
|
|
58
64
|
maxTokens: number;
|
|
59
65
|
reasoning?: Effort;
|
|
60
66
|
}
|
|
@@ -26,6 +26,8 @@ export interface CompactionV2Usage {
|
|
|
26
26
|
export interface CompactionV2Request {
|
|
27
27
|
body: OpenAICodexCompactionBody;
|
|
28
28
|
input: unknown[];
|
|
29
|
+
/** Serialized user-written turns to keep next to the compaction item; defaults to `input`. */
|
|
30
|
+
retainedUserItems: unknown[];
|
|
29
31
|
retainedMessageBudget: number;
|
|
30
32
|
sessionId?: string;
|
|
31
33
|
promptCacheKey?: string;
|
|
@@ -56,12 +58,14 @@ export declare function buildCompactionV2Request(model: Model, input: unknown[],
|
|
|
56
58
|
sessionId?: string;
|
|
57
59
|
promptCacheKey?: string;
|
|
58
60
|
retainedMessageBudget?: number;
|
|
61
|
+
retainedUserItems?: unknown[];
|
|
59
62
|
}): CompactionV2Request;
|
|
60
63
|
/** Wrap a body built by the normal Codex serializer for V2 compaction transport. */
|
|
61
64
|
export declare function buildCompactionV2RequestFromBody(model: Model, body: OpenAICodexCompactionBody, options?: {
|
|
62
65
|
sessionId?: string;
|
|
63
66
|
promptCacheKey?: string;
|
|
64
67
|
retainedMessageBudget?: number;
|
|
68
|
+
retainedUserItems?: unknown[];
|
|
65
69
|
}): CompactionV2Request;
|
|
66
70
|
/** Request V2 compaction over the normal OpenAI Responses streaming endpoint. */
|
|
67
71
|
export declare function requestCompactionV2Streaming(model: Model, apiKey: string, request: CompactionV2Request, signal?: AbortSignal, options?: {
|
|
@@ -72,7 +76,7 @@ export declare function requestCompactionV2Streaming(model: Model, apiKey: strin
|
|
|
72
76
|
codexCompaction?: CodexCompactionContext;
|
|
73
77
|
preferWebsockets?: boolean;
|
|
74
78
|
}): Promise<CompactionV2Response>;
|
|
75
|
-
/** Build Codex-style V2 replacement history from
|
|
79
|
+
/** Build Codex-style V2 replacement history from retention candidates plus compaction output. */
|
|
76
80
|
export declare function buildCompactionV2ReplacementHistory(input: unknown[], compactionItem: Record<string, unknown>, retainedMessageBudget?: number): {
|
|
77
81
|
replacementHistory: Array<Record<string, unknown>>;
|
|
78
82
|
retainedImageCount: number;
|
|
@@ -177,6 +177,20 @@ export interface SummaryOptions {
|
|
|
177
177
|
initiatorOverride?: MessageAttribution;
|
|
178
178
|
metadata?: Record<string, unknown>;
|
|
179
179
|
convertToLlm?: ConvertToLlm;
|
|
180
|
+
/**
|
|
181
|
+
* The provider context a live turn sends for `summarized` + `retained`, cut to
|
|
182
|
+
* `summarized`. Anthropic on-demand compaction sends it so retained signed
|
|
183
|
+
* thinking keeps its prefix; without it the request uses `remoteSystemPrompt`,
|
|
184
|
+
* `tools` and `convertToLlm`.
|
|
185
|
+
*/
|
|
186
|
+
buildProviderContext?: (summarized: AgentMessage[], retained: AgentMessage[], signal?: AbortSignal) => Promise<Context>;
|
|
187
|
+
/**
|
|
188
|
+
* Whether a message is a turn the user wrote. Remote Compaction V2 keeps these
|
|
189
|
+
* next to the compaction item. Defaults to `role === "user"`; hosts whose
|
|
190
|
+
* user-initiated turns also arrive as custom messages (e.g. skill invocations)
|
|
191
|
+
* widen it.
|
|
192
|
+
*/
|
|
193
|
+
isUserAuthored?: (message: AgentMessage) => boolean;
|
|
180
194
|
/**
|
|
181
195
|
* Optional telemetry handle. When provided, every LLM call emitted during
|
|
182
196
|
* compaction is wrapped in an OTEL chat span tagged with
|
|
@@ -88,8 +88,8 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
|
88
88
|
}
|
|
89
89
|
export interface SessionInitEntry extends SessionEntryBase {
|
|
90
90
|
type: "session_init";
|
|
91
|
-
/**
|
|
92
|
-
systemPrompt: string;
|
|
91
|
+
/** System prompt blocks exactly as sent to the model; files written before blocks were kept store one joined string. */
|
|
92
|
+
systemPrompt: string[] | string;
|
|
93
93
|
/** Initial task/user message */
|
|
94
94
|
task: string;
|
|
95
95
|
/** Tools available to the agent */
|
|
@@ -28,19 +28,31 @@ export declare const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompac
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const REMOTE_COMPACTION_TIMEOUT_MS = 300000;
|
|
30
30
|
export declare const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: string;
|
|
31
|
+
/**
|
|
32
|
+
* Tokens of stored Responses replacement history. Retained `input_image` parts
|
|
33
|
+
* count at their image estimate rather than as base64 text; everything else,
|
|
34
|
+
* opaque compaction state included, counts as serialized.
|
|
35
|
+
*/
|
|
36
|
+
export declare function countResponsesHistoryTokens(items: unknown[], tokenizer: Tokenizer): number;
|
|
31
37
|
export interface TrimRemoteCompactionInputResult {
|
|
32
38
|
input: Array<Record<string, unknown>>;
|
|
33
39
|
rewrittenOutputs: number;
|
|
34
40
|
estimatedTokensBefore: number;
|
|
35
41
|
estimatedTokensAfter: number;
|
|
36
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Whether `input` may be sent; false means it must not be. True when the
|
|
44
|
+
* full estimate fits, or when only the image estimate pushes it over (image
|
|
45
|
+
* pricing is a guess; the provider's own accounting decides).
|
|
46
|
+
*/
|
|
37
47
|
fits: boolean;
|
|
38
48
|
}
|
|
39
49
|
/**
|
|
40
50
|
* Preserve the full native transcript unless trailing tool outputs alone push a
|
|
41
51
|
* remote compaction request beyond the model window. Replacing only those
|
|
42
52
|
* outputs keeps call/result pairing and all earlier assistant/reasoning history,
|
|
43
|
-
* matching Codex's recovery path for oversized tool turns.
|
|
53
|
+
* matching Codex's recovery path for oversized tool turns. A request whose text
|
|
54
|
+
* fits but whose image estimate does not is still sendable: refusing it on an
|
|
55
|
+
* image guess would skip native compaction the provider would have accepted.
|
|
44
56
|
*/
|
|
45
57
|
export declare function trimRemoteCompactionInputToContextWindow(input: Array<Record<string, unknown>>, tokenizer: Tokenizer, contextWindow: number | null | undefined, instructions: string, tools?: unknown[]): TrimRemoteCompactionInputResult;
|
|
46
58
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool output pruning utilities for compaction.
|
|
3
3
|
*/
|
|
4
|
+
import type { ToolResultMessage } from "@linxiraos/pi-ai";
|
|
4
5
|
import type { Tokenizer } from "../tokenizer.js";
|
|
5
6
|
import type { SessionEntry } from "./entries.js";
|
|
6
7
|
import { type ProtectedToolMatcher } from "./tool-protection.js";
|
|
@@ -18,6 +19,8 @@ export interface PruneConfig {
|
|
|
18
19
|
* unchanged.
|
|
19
20
|
*/
|
|
20
21
|
supersedeKey?: SupersedeKeyFn;
|
|
22
|
+
/** Whether a keyed result shows its whole target (see {@link SupersedePruneConfig.supersedeComplete}). */
|
|
23
|
+
supersedeComplete?: SupersedeCompleteFn;
|
|
21
24
|
/** Useless-flagged results bypass the protect window (see {@link USELESS_NOTICE}). Default true. */
|
|
22
25
|
pruneUseless?: boolean;
|
|
23
26
|
/**
|
|
@@ -56,15 +59,35 @@ export declare const SUPERSEDED_NOTICE = "[Superseded by a newer read of this fi
|
|
|
56
59
|
export declare const USELESS_NOTICE = "[Uneventful result elided]";
|
|
57
60
|
/**
|
|
58
61
|
* Maps a tool call to a supersede key. Results sharing a key form a group in
|
|
59
|
-
* which
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* `undefined` to exempt a call from
|
|
62
|
+
* which a newer successful result supersedes every older one. A key `K` also
|
|
63
|
+
* covers keys with prefix `K + "\u0000"`: they are superseded only when the
|
|
64
|
+
* newest successful result for `K` is complete (see
|
|
65
|
+
* {@link SupersedeCompleteFn}). Return `undefined` to exempt a call from
|
|
66
|
+
* supersede grouping.
|
|
63
67
|
*/
|
|
64
68
|
export type SupersedeKeyFn = (toolName: string, args: Record<string, unknown>) => string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a successful keyed result shows its whole target, so a result for a
|
|
71
|
+
* parent key (e.g. a selector-free read) may supersede child-key results (e.g.
|
|
72
|
+
* range reads). A summary, truncated page, or notice is not complete.
|
|
73
|
+
*/
|
|
74
|
+
export type SupersedeCompleteFn = (message: ToolResultMessage) => boolean;
|
|
65
75
|
export interface SupersedePruneConfig {
|
|
66
|
-
/** Supersede key function;
|
|
76
|
+
/** Supersede key function; a newer successful result with the same key supersedes older ones (see {@link SupersedeKeyFn}). */
|
|
67
77
|
supersedeKey?: SupersedeKeyFn;
|
|
78
|
+
/**
|
|
79
|
+
* Whether a successful keyed result is complete. Absent, every successful
|
|
80
|
+
* result is complete. Child-key results are superseded only when the parent
|
|
81
|
+
* key's newest successful result is complete; same-key results follow
|
|
82
|
+
* newest-wins, since a same-key re-read that shows a different view means
|
|
83
|
+
* the target changed. A failed result supersedes only older failed results.
|
|
84
|
+
*
|
|
85
|
+
* Trade-off, by design: after `read foo.ts:50-200` → edit → a bare
|
|
86
|
+
* `read foo.ts` that returns a summary, the pre-edit range stays in context
|
|
87
|
+
* until a complete bare read or a same-range re-read. Keeping possibly stale
|
|
88
|
+
* detail is preferred over losing detail the summary does not show.
|
|
89
|
+
*/
|
|
90
|
+
supersedeComplete?: SupersedeCompleteFn;
|
|
68
91
|
/** Also prune results flagged useless by their tool. Default false. */
|
|
69
92
|
pruneUseless?: boolean;
|
|
70
93
|
/** Prune a candidate now when all messages after it total at most this many estimated tokens. Default 8 000. */
|
|
@@ -113,9 +136,10 @@ export declare function pruneToolOutputs(entries: SessionEntry[], tokenizer: Tok
|
|
|
113
136
|
* selector stripped (the read tool's own splitter grammar via
|
|
114
137
|
* {@link splitReadSelector}, e.g. `src/foo.ts:50-200`, `:2-4:raw`).
|
|
115
138
|
* Internal/URL-scheme paths (`skill://…`, `https://…`) are exempt.
|
|
116
|
-
* Selector-free reads key on the bare path;
|
|
117
|
-
* `path + "\u0000" + selector`, so
|
|
118
|
-
*
|
|
119
|
-
*
|
|
139
|
+
* Selector-free reads key on the bare path; line-range reads key on
|
|
140
|
+
* `path + "\u0000" + selector`, so a complete bare-path read can supersede them.
|
|
141
|
+
* `raw` and `conflicts` reads show a different form than a plain read (e.g. raw
|
|
142
|
+
* notebook JSON vs converted notebook text), so they key on
|
|
143
|
+
* `path + "\u0001" + selector` and are superseded only by the same selector.
|
|
120
144
|
*/
|
|
121
145
|
export declare function readToolSupersedeKey(toolName: string, args: Record<string, unknown>): string | undefined;
|
|
@@ -26,6 +26,11 @@ export declare function splitReadSelector(path: string): {
|
|
|
26
26
|
* and matches its write/edit path when computing Read/Write/RW markers.
|
|
27
27
|
*/
|
|
28
28
|
export declare function stripReadSelector(path: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a selector from {@link splitReadSelector} shows the file in a form other
|
|
31
|
+
* than its plain lines: verbatim `raw` bytes or the merge `conflicts` view.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isAlternateFormReadSelector(sel: string): boolean;
|
|
29
34
|
/**
|
|
30
35
|
* Whether `path` references a `scheme://` URL (internal URI or web URL) rather
|
|
31
36
|
* than a filesystem path that belongs in the compaction `<files>` summary.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ImageContent } from "@linxiraos/pi-ai";
|
|
2
|
+
import { type ImageSize } from "@linxiraos/pi-catalog/compat/image-tokenization";
|
|
3
|
+
/**
|
|
4
|
+
* Dimension-based image token estimates shared by the local context counter
|
|
5
|
+
* ({@link Tokenizer.countMessage}) and the native remote-compaction fit probe.
|
|
6
|
+
* Both sides must price the same image the same way: when they disagreed
|
|
7
|
+
* (1.2k flat locally vs a 12k flat worst case remotely) an image-heavy session
|
|
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).
|
|
15
|
+
*/
|
|
16
|
+
/** OpenAI image `detail` values; `undefined` means the provider default (`auto`). */
|
|
17
|
+
export type ImageDetail = ImageContent["detail"];
|
|
18
|
+
export type { ImageSize };
|
|
19
|
+
/**
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
export declare function estimateImageTokens(size: ImageSize | null | undefined, detail?: ImageDetail): number;
|
|
24
|
+
/** Pixel size read from the header of base64-encoded image data, or null. */
|
|
25
|
+
export declare function base64ImageSize(base64: string): ImageSize | null;
|
|
26
|
+
/** Pixel size of a `data:<mime>;base64,` URL, or null for any other URL. */
|
|
27
|
+
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;
|
|
@@ -145,10 +145,18 @@ export interface ChatUsageSnapshot {
|
|
|
145
145
|
}
|
|
146
146
|
/** Context passed to the cost estimator. */
|
|
147
147
|
export interface CostEstimatorContext {
|
|
148
|
+
/** OTel provider label (`gen_ai.provider.name`). */
|
|
148
149
|
readonly provider: string;
|
|
150
|
+
/** Provider id before OTel normalization (e.g. `openai-codex` for the `openai` label). */
|
|
151
|
+
readonly providerId: string | undefined;
|
|
152
|
+
/** Model id reported on the response; a provider may report a served model instead of the requested one. */
|
|
149
153
|
readonly model: string;
|
|
154
|
+
/** Requested model id, when the caller knows it. */
|
|
155
|
+
readonly modelId: string | undefined;
|
|
150
156
|
readonly serviceTier: ServiceTier | undefined;
|
|
151
157
|
readonly usage: ChatUsageSnapshot;
|
|
158
|
+
/** Cost the provider already computed for this request; the value persisted with the message. */
|
|
159
|
+
readonly usageCost: Usage["cost"];
|
|
152
160
|
}
|
|
153
161
|
/**
|
|
154
162
|
* Cost estimator result.
|
|
@@ -390,6 +398,8 @@ export interface ChatRequestSnapshot {
|
|
|
390
398
|
*/
|
|
391
399
|
export declare function finishChatSpan(telemetry: AgentTelemetry | undefined, span: Span | undefined, message: AssistantMessage, options: {
|
|
392
400
|
readonly stepNumber: number;
|
|
401
|
+
/** Requested model id; `message.model` may name a served fallback model instead. */
|
|
402
|
+
readonly modelId?: string;
|
|
393
403
|
readonly serviceTier?: ServiceTier;
|
|
394
404
|
readonly responseHeaders?: Readonly<Record<string, string>>;
|
|
395
405
|
readonly baseUrl?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Model } from "@linxiraos/pi-ai";
|
|
2
2
|
import * as natives from "@linxiraos/pi-natives";
|
|
3
|
+
import * as snapcompact from "@linxiraos/pi-snapcompact";
|
|
3
4
|
import type { AgentMessage } from "./types.js";
|
|
4
5
|
/** Maps the catalog-resolved tokenizer family to its native implementation. */
|
|
5
6
|
export declare function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null | undefined): natives.Encoding | null;
|
|
@@ -42,14 +43,15 @@ export interface TokenBudgetCheck {
|
|
|
42
43
|
* Model-aware local token counter. Immutable: the catalog-resolved encoding
|
|
43
44
|
* is fixed at construction, so a cached count can never straddle two
|
|
44
45
|
* encodings. An `Agent` owns one for its active model (swapping the instance
|
|
45
|
-
* when the model's encoding changes); one-shot
|
|
46
|
-
* the model that will be billed. Known
|
|
47
|
-
* counts; unknown models keep the fast
|
|
48
|
-
* `PI_TOKENIZER_ACCURATE=1`).
|
|
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`).
|
|
49
50
|
*/
|
|
50
51
|
export declare class Tokenizer {
|
|
51
52
|
#private;
|
|
52
|
-
|
|
53
|
+
readonly frameBillingKey: string;
|
|
54
|
+
constructor(model?: (Pick<Model, "tokenizer"> & snapcompact.ShapeTarget) | null);
|
|
53
55
|
get encoding(): natives.Encoding | null;
|
|
54
56
|
countTokens(text: string | string[], mode?: TokenCountMode): number;
|
|
55
57
|
/**
|
|
@@ -70,7 +72,8 @@ export declare class Tokenizer {
|
|
|
70
72
|
* Settled historical messages are counted once and reused until an owner
|
|
71
73
|
* (prune/shake/strip-images) calls `invalidateMessageCache`; streaming
|
|
72
74
|
* assistants bypass the memo entirely (see the message-cache settle-gate
|
|
73
|
-
* invariant). Image blocks charge
|
|
75
|
+
* invariant). Image blocks charge {@link estimateImageContentTokens}, the
|
|
76
|
+
* same dimension-based estimate the native remote-compaction probe applies.
|
|
74
77
|
*/
|
|
75
78
|
countMessage(message: AgentMessage, options?: MessageCountOptions): number;
|
|
76
79
|
/** Sum of {@link countMessage} over `messages`. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-agent-core",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.27",
|
|
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",
|
|
@@ -35,16 +35,16 @@
|
|
|
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.1.
|
|
39
|
-
"@linxiraos/pi-catalog": "1.1.
|
|
40
|
-
"@linxiraos/pi-natives": "1.1.
|
|
41
|
-
"@linxiraos/pi-utils": "1.1.
|
|
42
|
-
"@linxiraos/pi-wire": "1.1.
|
|
43
|
-
"@linxiraos/pi-snapcompact": "1.1.
|
|
38
|
+
"@linxiraos/pi-ai": "1.1.27",
|
|
39
|
+
"@linxiraos/pi-catalog": "1.1.27",
|
|
40
|
+
"@linxiraos/pi-natives": "1.1.27",
|
|
41
|
+
"@linxiraos/pi-utils": "1.1.27",
|
|
42
|
+
"@linxiraos/pi-wire": "1.1.27",
|
|
43
|
+
"@linxiraos/pi-snapcompact": "1.1.27",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@linxiraos/pi-omptype": "1.1.
|
|
47
|
+
"@linxiraos/pi-omptype": "1.1.27",
|
|
48
48
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
49
49
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
50
50
|
"@types/bun": "^1.3.14"
|