@oh-my-pi/pi-agent-core 18.14.2 → 18.16.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 +21 -0
- package/dist/types/agent.d.ts +11 -0
- package/dist/types/compaction/branch-summarization.d.ts +2 -3
- package/dist/types/compaction/compaction.d.ts +2 -2
- package/dist/types/compaction/utils.d.ts +6 -22
- package/dist/types/sent-tool-definitions.d.ts +5 -0
- package/dist/types/tokenizer.d.ts +9 -9
- package/dist/types/types.d.ts +1 -1
- package/package.json +8 -8
- package/src/agent-loop.ts +2 -2
- package/src/agent.ts +14 -0
- package/src/compaction/branch-summarization.ts +4 -10
- package/src/compaction/compaction-v2-streaming.ts +2 -2
- package/src/compaction/compaction.ts +6 -10
- package/src/compaction/openai.ts +4 -0
- package/src/compaction/utils.ts +19 -98
- package/src/sent-tool-definitions.ts +10 -0
- package/src/tokenizer.ts +53 -24
- package/src/types.ts +1 -1
- package/src/compaction/prompts/file-operations.md +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.16.0] - 2026-11-02
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Deprecated `createFileOps`, `computeFileLists`, `formatFileOperations`, `upsertFileOperations`, `isUrlSchemePath` and `FileOperations` in `@oh-my-pi/pi-agent-core/compaction`; they now re-export `@oh-my-pi/snapcompact`, which callers should import directly
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Context estimates for a model without a known tokenizer family (Gemini, Grok and the like), and token counts made without a model (compress metrics, the legacy `estimateTokens` export), count text in o200k_base instead of guessing 4 bytes per token, which undercounted what those providers bill by 11–48% on code-heavy sessions and let compaction fire late after a switch to them ([#17607](https://github.com/can1357/oh-my-pi/pull/17607) by [@will-bogusz](https://github.com/will-bogusz))
|
|
14
|
+
- Context estimates no longer charge a reader another model's thinking signatures or redacted thinking, which the request strips: a Claude session switched to Grok or Gemini, including Gemini through Factory, counted them as text ([#17607](https://github.com/can1357/oh-my-pi/pull/17607) by [@will-bogusz](https://github.com/will-bogusz))
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- Removed `PI_TOKENIZER_ACCURATE`: every model is now counted with a native tokenizer ([#17607](https://github.com/can1357/oh-my-pi/pull/17607) by [@will-bogusz](https://github.com/will-bogusz))
|
|
19
|
+
|
|
20
|
+
## [18.15.0] - 2026-11-01
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed OpenAI remote compaction (`/responses/compact`) replaying backend-only `internal_chat_message_metadata_passthrough` metadata that Codex rejects as an unknown parameter ([#17590](https://github.com/can1357/oh-my-pi/pull/17590) by [@briankeefe](https://github.com/briankeefe))
|
|
25
|
+
|
|
5
26
|
## [18.14.2] - 2026-10-31
|
|
6
27
|
|
|
7
28
|
### Added
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ApiKey, type AssistantMessage, type AssistantMessageEvent, type Co
|
|
|
2
2
|
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
|
+
import { SentToolDefinitions } from "./sent-tool-definitions.js";
|
|
5
6
|
import { Tokenizer } from "./tokenizer.js";
|
|
6
7
|
import type { AgentBeforeModelCall, AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideBoundary, AsideMessage, PrepareQueuedMessages, SpeculativeToolExecutionConfig, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
|
|
7
8
|
export declare class AgentBusyError extends Error {
|
|
@@ -64,6 +65,11 @@ export interface AgentOptions {
|
|
|
64
65
|
* Shared provider state map for session-scoped transport/session caches.
|
|
65
66
|
*/
|
|
66
67
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
68
|
+
/**
|
|
69
|
+
* Tool definitions already sent, for an Agent that replays another Agent's request history
|
|
70
|
+
* (see {@link Agent.copySentToolDefinitions}). Defaults to an empty store.
|
|
71
|
+
*/
|
|
72
|
+
sentToolDefinitions?: SentToolDefinitions;
|
|
67
73
|
/**
|
|
68
74
|
* Resolves an API key or resolver dynamically for each LLM call.
|
|
69
75
|
* Useful for expiring tokens and model-scoped credential routing.
|
|
@@ -295,6 +301,11 @@ export declare class Agent {
|
|
|
295
301
|
* Set the prompt cache key forwarded to providers.
|
|
296
302
|
*/
|
|
297
303
|
set promptCacheKey(value: string | undefined);
|
|
304
|
+
/**
|
|
305
|
+
* A detached copy of the tool definitions this agent has sent (`AgentOptions.sentToolDefinitions`), without the
|
|
306
|
+
* names `keep` rejects.
|
|
307
|
+
*/
|
|
308
|
+
copySentToolDefinitions(keep?: (name: string) => boolean): SentToolDefinitions;
|
|
298
309
|
/**
|
|
299
310
|
* Fields added to every log record written while this agent runs (the
|
|
300
311
|
* owning session's `sessionId`), so in-process agents' records stay apart.
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* a summary of the branch being left so context isn't lost.
|
|
6
6
|
*/
|
|
7
7
|
import type { Api, ApiKey, AssistantMessage, Context, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
8
9
|
import { type AgentTelemetry } from "../telemetry.js";
|
|
9
10
|
import { Tokenizer } from "../tokenizer.js";
|
|
10
11
|
import type { AgentMessage } from "../types.js";
|
|
11
12
|
import type { ReadonlySessionManager, SessionEntry } from "./entries.js";
|
|
12
13
|
import { type ConvertToLlm } from "./messages.js";
|
|
13
|
-
import { type FileOperations } from "./utils.js";
|
|
14
14
|
export interface BranchSummaryResult {
|
|
15
15
|
summary?: string;
|
|
16
16
|
readFiles?: string[];
|
|
@@ -23,12 +23,11 @@ export interface BranchSummaryDetails {
|
|
|
23
23
|
readFiles: string[];
|
|
24
24
|
modifiedFiles: string[];
|
|
25
25
|
}
|
|
26
|
-
export type { FileOperations } from "./utils.js";
|
|
27
26
|
export interface BranchPreparation {
|
|
28
27
|
/** Messages extracted for summarization, in chronological order */
|
|
29
28
|
messages: AgentMessage[];
|
|
30
29
|
/** File operations extracted from tool calls */
|
|
31
|
-
fileOps: FileOperations;
|
|
30
|
+
fileOps: snapcompact.FileOperations;
|
|
32
31
|
/** Total estimated tokens in messages */
|
|
33
32
|
totalTokens: number;
|
|
34
33
|
}
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* and after compaction the session is reloaded.
|
|
6
6
|
*/
|
|
7
7
|
import { type Api, type ApiKey, type AssistantMessage, type CodexCompactionContext, type Context, type FetchImpl, type MessageAttribution, type Model, type OneshotRetryOptions, type ProviderSessionState, type SimpleStreamOptions, type Tool, type Usage } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
8
9
|
import { type AgentTelemetry } from "../telemetry.js";
|
|
9
10
|
import { ThinkingLevel } from "../thinking.js";
|
|
10
11
|
import { Tokenizer } from "../tokenizer.js";
|
|
11
12
|
import type { AgentMessage } from "../types.js";
|
|
12
13
|
import type { SessionEntry } from "./entries.js";
|
|
13
14
|
import { type ConvertToLlm } from "./messages.js";
|
|
14
|
-
import { type FileOperations } from "./utils.js";
|
|
15
15
|
/** Details stored in CompactionEntry.details for file tracking */
|
|
16
16
|
export interface CompactionDetails {
|
|
17
17
|
readFiles: string[];
|
|
@@ -368,7 +368,7 @@ export interface CompactionPreparation {
|
|
|
368
368
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
369
369
|
previousPreserveData?: Record<string, unknown>;
|
|
370
370
|
/** File operations extracted from messagesToSummarize */
|
|
371
|
-
fileOps: FileOperations;
|
|
371
|
+
fileOps: snapcompact.FileOperations;
|
|
372
372
|
/** Compaction settions from settings.jsonl */
|
|
373
373
|
settings: CompactionSettings;
|
|
374
374
|
}
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { Message } from "@oh-my-pi/pi-ai";
|
|
5
5
|
import { type Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
6
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
6
7
|
import type { AgentMessage } from "../types.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
export declare function createFileOps(): FileOperations;
|
|
8
|
+
/** @deprecated Import these from `@oh-my-pi/snapcompact`. */
|
|
9
|
+
export { computeFileLists, createFileOps, type FileOperations, isUrlSchemePath, upsertFileOperations, } from "@oh-my-pi/snapcompact";
|
|
10
|
+
/** @deprecated Use `upsertFileOperations("", readFiles, modifiedFiles, readSet)` from `@oh-my-pi/snapcompact`. */
|
|
11
|
+
export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
|
|
13
12
|
/** Error text for a summarization call that hit its output limit (`stopReason: "length"`): partial text is never used. */
|
|
14
13
|
export declare function incompleteOutputError(prefix: string): string;
|
|
15
14
|
/**
|
|
@@ -33,25 +32,10 @@ export declare function stripReadSelector(path: string): string;
|
|
|
33
32
|
* than its plain lines: verbatim `raw` bytes or the merge `conflicts` view.
|
|
34
33
|
*/
|
|
35
34
|
export declare function isAlternateFormReadSelector(sel: string): boolean;
|
|
36
|
-
/**
|
|
37
|
-
* Whether `path` references a `scheme://` URL (internal URI or web URL) rather
|
|
38
|
-
* than a filesystem path that belongs in the compaction `<files>` summary.
|
|
39
|
-
*/
|
|
40
|
-
export declare function isUrlSchemePath(path: string): boolean;
|
|
41
35
|
/**
|
|
42
36
|
* Extract file operations from tool calls in an assistant message.
|
|
43
37
|
*/
|
|
44
|
-
export declare function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void;
|
|
45
|
-
/**
|
|
46
|
-
* Compute final file lists from file operations.
|
|
47
|
-
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
|
48
|
-
*/
|
|
49
|
-
export declare function computeFileLists(fileOps: FileOperations): {
|
|
50
|
-
readFiles: string[];
|
|
51
|
-
modifiedFiles: string[];
|
|
52
|
-
};
|
|
53
|
-
export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
|
|
54
|
-
export declare function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
|
|
38
|
+
export declare function extractFileOpsFromMessage(message: AgentMessage, fileOps: snapcompact.FileOperations): void;
|
|
55
39
|
/**
|
|
56
40
|
* Truncate tool results to the same representation used in summarization prompts.
|
|
57
41
|
*/
|
|
@@ -8,6 +8,11 @@ export declare class SentToolDefinitions {
|
|
|
8
8
|
#private;
|
|
9
9
|
/** Remember the definitions a request is about to send. */
|
|
10
10
|
record(tools: readonly Tool[]): void;
|
|
11
|
+
/**
|
|
12
|
+
* A detached copy: an Agent that replays another's request history declares the same withdrawn tools. `keep`
|
|
13
|
+
* leaves out a definition the copy must never declare.
|
|
14
|
+
*/
|
|
15
|
+
copy(keep?: (name: string) => boolean): SentToolDefinitions;
|
|
11
16
|
/**
|
|
12
17
|
* `context` with {@link Context.inactiveTools} set to the definitions for names the latest
|
|
13
18
|
* `requestControls.tools.declared` in its messages holds that are not in its `tools`; cleared when
|
|
@@ -10,6 +10,7 @@ type TokenizerModel = Pick<Model, "tokenizer"> & snapcompact.ShapeTarget & {
|
|
|
10
10
|
compat?: Model["compat"];
|
|
11
11
|
thinking?: Model["thinking"];
|
|
12
12
|
reasoning?: boolean;
|
|
13
|
+
requestModelId?: string;
|
|
13
14
|
};
|
|
14
15
|
/** Identity of the reader whose replayed reasoning a {@link Tokenizer} counts; equal keys count it alike. */
|
|
15
16
|
export declare function reasoningReplayKey(model: TokenizerModel | null | undefined): string;
|
|
@@ -24,13 +25,12 @@ export interface TokenizerOptions {
|
|
|
24
25
|
nativeHistoryReplay?: boolean;
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
* when known, o200k_base otherwise
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* undercounts).
|
|
28
|
+
* Every mode counts with the native tokenizer: the catalog-resolved family
|
|
29
|
+
* when known, o200k_base otherwise. They differ only in the fallback when the
|
|
30
|
+
* loaded addon does not recognize that encoding: `strict` and `upperbound`
|
|
31
|
+
* the raw byte length (never undercounts), `approximate` a bytes/4 guess.
|
|
32
|
+
* Tests (`NODE_ENV=test`) skip the native count outside `strict` and use that
|
|
33
|
+
* fallback.
|
|
34
34
|
*/
|
|
35
35
|
export type TokenCountMode = "strict" | "approximate" | "upperbound";
|
|
36
36
|
/** Options for {@link Tokenizer.countMessage} / {@link Tokenizer.countMessages}. */
|
|
@@ -69,8 +69,8 @@ export interface TokenBudgetCheck {
|
|
|
69
69
|
* when the model's encoding, image or thinking-replay pricing, replay
|
|
70
70
|
* identity or native-history replay state changes); one-shot flows construct
|
|
71
71
|
* their own for the model that will be billed. Known tokenizer families use
|
|
72
|
-
* exact native counts;
|
|
73
|
-
*
|
|
72
|
+
* exact native counts; other models are counted in o200k_base, which bills
|
|
73
|
+
* unknown readers closer than a bytes/4 guess.
|
|
74
74
|
*/
|
|
75
75
|
export declare class Tokenizer {
|
|
76
76
|
#private;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -968,7 +968,7 @@ export type ToolApprovalDecision = ToolTier | {
|
|
|
968
968
|
* prompt policies to the tool it dispatches into. */
|
|
969
969
|
policyKey?: string;
|
|
970
970
|
};
|
|
971
|
-
export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
|
|
971
|
+
export type ToolApproval = ToolApprovalDecision | ((args: unknown, context?: AgentToolContext) => ToolApprovalDecision);
|
|
972
972
|
/**
|
|
973
973
|
* Context passed to tool execution.
|
|
974
974
|
* Apps can extend via declaration merging.
|
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.
|
|
4
|
+
"version": "18.16.0",
|
|
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.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.16.0",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.16.0",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.16.0",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.16.0",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.16.0",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.16.0",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.
|
|
50
|
+
"@oh-my-pi/omptype": "18.16.0",
|
|
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-loop.ts
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
signalListLabel,
|
|
54
54
|
} from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
55
55
|
import { isDsmlLeakRecoveryTarget, removeDsmlToolMarkupLeak } from "@oh-my-pi/pi-ai/utils/dsml-leak";
|
|
56
|
-
import { cloneJsonTree, logger, sanitizeText, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
56
|
+
import { cloneJsonTree, logger, sanitizeText, structuredCloneJSON, toError } from "@oh-my-pi/pi-utils";
|
|
57
57
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
58
58
|
import { LiveSteeringChannel } from "./live-steering";
|
|
59
59
|
import { agentPauseGate } from "./pause";
|
|
@@ -1188,7 +1188,7 @@ function resolveAsides(entries: AsideMessage[] | undefined): AgentMessage[] {
|
|
|
1188
1188
|
if (message) out.push(message);
|
|
1189
1189
|
}
|
|
1190
1190
|
} catch (error) {
|
|
1191
|
-
discardAsides(out,
|
|
1191
|
+
discardAsides(out, toError(error));
|
|
1192
1192
|
throw error;
|
|
1193
1193
|
}
|
|
1194
1194
|
return out;
|
package/src/agent.ts
CHANGED
|
@@ -176,6 +176,11 @@ export interface AgentOptions {
|
|
|
176
176
|
* Shared provider state map for session-scoped transport/session caches.
|
|
177
177
|
*/
|
|
178
178
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
179
|
+
/**
|
|
180
|
+
* Tool definitions already sent, for an Agent that replays another Agent's request history
|
|
181
|
+
* (see {@link Agent.copySentToolDefinitions}). Defaults to an empty store.
|
|
182
|
+
*/
|
|
183
|
+
sentToolDefinitions?: SentToolDefinitions;
|
|
179
184
|
|
|
180
185
|
/**
|
|
181
186
|
* Resolves an API key or resolver dynamically for each LLM call.
|
|
@@ -568,6 +573,7 @@ export class Agent {
|
|
|
568
573
|
this.#deadline = opts.deadline;
|
|
569
574
|
this.#promptCacheKey = opts.promptCacheKey;
|
|
570
575
|
this.#providerSessionState = opts.providerSessionState;
|
|
576
|
+
if (opts.sentToolDefinitions) this.#sentToolDefinitions = opts.sentToolDefinitions;
|
|
571
577
|
this.#thinkingBudgets = opts.thinkingBudgets;
|
|
572
578
|
this.#temperature = opts.temperature;
|
|
573
579
|
this.#topP = opts.topP;
|
|
@@ -642,6 +648,14 @@ export class Agent {
|
|
|
642
648
|
this.#promptCacheKey = value;
|
|
643
649
|
}
|
|
644
650
|
|
|
651
|
+
/**
|
|
652
|
+
* A detached copy of the tool definitions this agent has sent (`AgentOptions.sentToolDefinitions`), without the
|
|
653
|
+
* names `keep` rejects.
|
|
654
|
+
*/
|
|
655
|
+
copySentToolDefinitions(keep?: (name: string) => boolean): SentToolDefinitions {
|
|
656
|
+
return this.#sentToolDefinitions.copy(keep);
|
|
657
|
+
}
|
|
658
|
+
|
|
645
659
|
/**
|
|
646
660
|
* Fields added to every log record written while this agent runs (the
|
|
647
661
|
* owning session's `sessionId`), so in-process agents' records stay apart.
|
|
@@ -24,16 +24,12 @@ import {
|
|
|
24
24
|
import branchSummaryPrompt from "./prompts/branch-summary.md" with { type: "text" };
|
|
25
25
|
import branchSummaryPreamble from "./prompts/branch-summary-preamble.md" with { type: "text" };
|
|
26
26
|
import {
|
|
27
|
-
computeFileLists,
|
|
28
|
-
createFileOps,
|
|
29
27
|
extractFileOpsFromMessage,
|
|
30
|
-
type FileOperations,
|
|
31
28
|
incompleteOutputError,
|
|
32
29
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
33
30
|
serializeConversationForSummary,
|
|
34
31
|
stripReadSelector,
|
|
35
32
|
truncateToolResultForSummary,
|
|
36
|
-
upsertFileOperations,
|
|
37
33
|
} from "./utils";
|
|
38
34
|
|
|
39
35
|
// ============================================================================
|
|
@@ -54,13 +50,11 @@ export interface BranchSummaryDetails {
|
|
|
54
50
|
modifiedFiles: string[];
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
export type { FileOperations } from "./utils";
|
|
58
|
-
|
|
59
53
|
export interface BranchPreparation {
|
|
60
54
|
/** Messages extracted for summarization, in chronological order */
|
|
61
55
|
messages: AgentMessage[];
|
|
62
56
|
/** File operations extracted from tool calls */
|
|
63
|
-
fileOps: FileOperations;
|
|
57
|
+
fileOps: snapcompact.FileOperations;
|
|
64
58
|
/** Total estimated tokens in messages */
|
|
65
59
|
totalTokens: number;
|
|
66
60
|
}
|
|
@@ -252,7 +246,7 @@ export function prepareBranchEntries(
|
|
|
252
246
|
tokenBudget: number = 0,
|
|
253
247
|
): BranchPreparation {
|
|
254
248
|
const messages: AgentMessage[] = [];
|
|
255
|
-
const fileOps = createFileOps();
|
|
249
|
+
const fileOps = snapcompact.createFileOps();
|
|
256
250
|
let totalTokens = 0;
|
|
257
251
|
|
|
258
252
|
// First pass: collect file ops from ALL entries (even if they don't fit in token budget)
|
|
@@ -400,8 +394,8 @@ export async function generateBranchSummary(
|
|
|
400
394
|
summary = BRANCH_SUMMARY_PREAMBLE + summary;
|
|
401
395
|
|
|
402
396
|
// Compute file lists and append to summary
|
|
403
|
-
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
404
|
-
summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
397
|
+
const { readFiles, modifiedFiles } = snapcompact.computeFileLists(fileOps);
|
|
398
|
+
summary = snapcompact.upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
405
399
|
|
|
406
400
|
return {
|
|
407
401
|
summary: summary || "No summary generated",
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
OPENAI_HEADER_VALUES,
|
|
33
33
|
OPENAI_HEADERS,
|
|
34
34
|
} from "@oh-my-pi/pi-catalog/wire/codex";
|
|
35
|
-
import { $env, isUnexpectedSocketCloseMessage, logger, ptree, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
35
|
+
import { $env, isUnexpectedSocketCloseMessage, logger, ptree, stringifyJson, toError } from "@oh-my-pi/pi-utils";
|
|
36
36
|
import { appendAzureApiVersion, resolveAzureOpenAiBaseUrl } from "./azure-openai-endpoint";
|
|
37
37
|
import { prepareBedrockCompactionRequest } from "./bedrock";
|
|
38
38
|
|
|
@@ -320,7 +320,7 @@ export async function requestCompactionV2Streaming(
|
|
|
320
320
|
codexAccessPrograms: options?.codexAccessPrograms,
|
|
321
321
|
});
|
|
322
322
|
} catch (err) {
|
|
323
|
-
const error =
|
|
323
|
+
const error = toError(err);
|
|
324
324
|
if (signal?.aborted) throw error;
|
|
325
325
|
|
|
326
326
|
if (isRetryableCompactionError(error) && attempt < V2_COMPACTION_MAX_RETRIES) {
|
|
@@ -86,16 +86,12 @@ import handoffDocumentPrompt from "./prompts/handoff-document.md" with { type: "
|
|
|
86
86
|
import snapcompactArchiveContextPrompt from "./prompts/snapcompact-archive-context.md" with { type: "text" };
|
|
87
87
|
|
|
88
88
|
import {
|
|
89
|
-
computeFileLists,
|
|
90
|
-
createFileOps,
|
|
91
89
|
escapeSummaryBoundaryTags,
|
|
92
90
|
extractFileOpsFromMessage,
|
|
93
|
-
type FileOperations,
|
|
94
91
|
incompleteOutputError,
|
|
95
92
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
96
93
|
serializeConversationForSummary,
|
|
97
94
|
stripReadSelector,
|
|
98
|
-
upsertFileOperations,
|
|
99
95
|
} from "./utils";
|
|
100
96
|
|
|
101
97
|
// ============================================================================
|
|
@@ -115,8 +111,8 @@ function extractFileOperations(
|
|
|
115
111
|
messages: AgentMessage[],
|
|
116
112
|
entries: SessionEntry[],
|
|
117
113
|
prevCompactionIndex: number,
|
|
118
|
-
): FileOperations {
|
|
119
|
-
const fileOps = createFileOps();
|
|
114
|
+
): snapcompact.FileOperations {
|
|
115
|
+
const fileOps = snapcompact.createFileOps();
|
|
120
116
|
|
|
121
117
|
// Collect from previous compaction's details (if pi-generated)
|
|
122
118
|
if (prevCompactionIndex >= 0) {
|
|
@@ -1384,7 +1380,7 @@ export interface CompactionPreparation {
|
|
|
1384
1380
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
1385
1381
|
previousPreserveData?: Record<string, unknown>;
|
|
1386
1382
|
/** File operations extracted from messagesToSummarize */
|
|
1387
|
-
fileOps: FileOperations;
|
|
1383
|
+
fileOps: snapcompact.FileOperations;
|
|
1388
1384
|
/** Compaction settions from settings.jsonl */
|
|
1389
1385
|
settings: CompactionSettings;
|
|
1390
1386
|
}
|
|
@@ -2343,8 +2339,8 @@ export async function compact(
|
|
|
2343
2339
|
});
|
|
2344
2340
|
|
|
2345
2341
|
// Compute file lists and append to summary
|
|
2346
|
-
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
2347
|
-
summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
2342
|
+
const { readFiles, modifiedFiles } = snapcompact.computeFileLists(fileOps);
|
|
2343
|
+
summary = snapcompact.upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
2348
2344
|
if (nativeSummary !== undefined) {
|
|
2349
2345
|
// The replayed block (summary and any `tool_changes`) stays
|
|
2350
2346
|
// byte-identical to the API's output so it matches its signature. The
|
|
@@ -2352,7 +2348,7 @@ export async function compact(
|
|
|
2352
2348
|
// summary message with the block and skips its text, so they would
|
|
2353
2349
|
// otherwise be invisible to this provider. Every other provider keeps
|
|
2354
2350
|
// reading the entry text.
|
|
2355
|
-
const filesText = upsertFileOperations("", readFiles, modifiedFiles, fileOps.read) || undefined;
|
|
2351
|
+
const filesText = snapcompact.upsertFileOperations("", readFiles, modifiedFiles, fileOps.read) || undefined;
|
|
2356
2352
|
preserveData = withAnthropicCompactionPreserveData(preserveData, {
|
|
2357
2353
|
provider: model.provider,
|
|
2358
2354
|
content: nativeSummary,
|
package/src/compaction/openai.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
getOpenAIResponsesHistoryItems,
|
|
46
46
|
getOpenAIResponsesHistoryPayload,
|
|
47
47
|
normalizeResponsesToolCallId,
|
|
48
|
+
stripOpenAIResponsesInternalMetadataForReplay,
|
|
48
49
|
stripOpenAIResponsesOutputOnlyStatusesForReplay,
|
|
49
50
|
truncateResponseItemId,
|
|
50
51
|
} from "@oh-my-pi/pi-ai/utils";
|
|
@@ -846,6 +847,9 @@ export function buildOpenAiNativeHistory(
|
|
|
846
847
|
msgIndex++;
|
|
847
848
|
}
|
|
848
849
|
|
|
850
|
+
for (let index = 0; index < input.length; index++) {
|
|
851
|
+
input[index] = stripOpenAIResponsesInternalMetadataForReplay(input[index]!);
|
|
852
|
+
}
|
|
849
853
|
return stripOpenAIResponsesOutputOnlyStatusesForReplay(hoistInterleavedResponsesToolBatchMessages(input));
|
|
850
854
|
}
|
|
851
855
|
|
package/src/compaction/utils.ts
CHANGED
|
@@ -5,27 +5,27 @@
|
|
|
5
5
|
import type { Message, ToolCall } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import { type Dialect, getDialectDefinition } from "@oh-my-pi/pi-ai/dialect";
|
|
7
7
|
import { escapeHarmonyControlTokens } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
8
|
-
import {
|
|
8
|
+
import { prompt, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
9
10
|
import type { AgentMessage } from "../types";
|
|
10
|
-
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
11
11
|
import summarizationSystemPrompt from "./prompts/summarization-system.md" with { type: "text" };
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/** @deprecated Import these from `@oh-my-pi/snapcompact`. */
|
|
14
|
+
export {
|
|
15
|
+
computeFileLists,
|
|
16
|
+
createFileOps,
|
|
17
|
+
type FileOperations,
|
|
18
|
+
isUrlSchemePath,
|
|
19
|
+
upsertFileOperations,
|
|
20
|
+
} from "@oh-my-pi/snapcompact";
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
read: new Set(),
|
|
26
|
-
written: new Set(),
|
|
27
|
-
edited: new Set(),
|
|
28
|
-
};
|
|
22
|
+
/** @deprecated Use `upsertFileOperations("", readFiles, modifiedFiles, readSet)` from `@oh-my-pi/snapcompact`. */
|
|
23
|
+
export function formatFileOperations(
|
|
24
|
+
readFiles: string[],
|
|
25
|
+
modifiedFiles: string[],
|
|
26
|
+
readSet?: ReadonlySet<string>,
|
|
27
|
+
): string {
|
|
28
|
+
return snapcompact.upsertFileOperations("", readFiles, modifiedFiles, readSet);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Error text for a summarization call that hit its output limit (`stopReason: "length"`): partial text is never used. */
|
|
@@ -91,27 +91,10 @@ export function isAlternateFormReadSelector(sel: string): boolean {
|
|
|
91
91
|
return sel.split(":").some(part => READ_ALTERNATE_FORM_RE.test(part));
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
* A real filesystem path never contains a `scheme://` URL. Tool-call paths that
|
|
96
|
-
* do — `conflict://1`, `artifact://3`, `local://ctx.md`, `history://…`,
|
|
97
|
-
* `issue://12`, `https://…`, and the tolerated `file.ts:conflict://1` prefix
|
|
98
|
-
* form — are session-scoped or remote resources, not files the post-compaction
|
|
99
|
-
* agent can re-ground on. Keep them out of the `<files>` summary.
|
|
100
|
-
*/
|
|
101
|
-
const URL_SCHEME_RE = /[a-z][a-z0-9+.-]*:\/\//i;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Whether `path` references a `scheme://` URL (internal URI or web URL) rather
|
|
105
|
-
* than a filesystem path that belongs in the compaction `<files>` summary.
|
|
106
|
-
*/
|
|
107
|
-
export function isUrlSchemePath(path: string): boolean {
|
|
108
|
-
return URL_SCHEME_RE.test(path);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
94
|
/**
|
|
112
95
|
* Extract file operations from tool calls in an assistant message.
|
|
113
96
|
*/
|
|
114
|
-
export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {
|
|
97
|
+
export function extractFileOpsFromMessage(message: AgentMessage, fileOps: snapcompact.FileOperations): void {
|
|
115
98
|
if (message.role !== "assistant") return;
|
|
116
99
|
if (!("content" in message) || !Array.isArray(message.content)) return;
|
|
117
100
|
|
|
@@ -128,7 +111,7 @@ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOp
|
|
|
128
111
|
|
|
129
112
|
// Internal URIs (conflict://, artifact://, local://, history://, …) and
|
|
130
113
|
// web URLs are not re-groundable files — keep them out of `<files>`.
|
|
131
|
-
if (isUrlSchemePath(path)) continue;
|
|
114
|
+
if (snapcompact.isUrlSchemePath(path)) continue;
|
|
132
115
|
|
|
133
116
|
switch (block.name) {
|
|
134
117
|
case "read":
|
|
@@ -144,68 +127,6 @@ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOp
|
|
|
144
127
|
}
|
|
145
128
|
}
|
|
146
129
|
|
|
147
|
-
/**
|
|
148
|
-
* Compute final file lists from file operations.
|
|
149
|
-
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
|
150
|
-
*/
|
|
151
|
-
export function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {
|
|
152
|
-
// Drop any `scheme://` URLs (e.g. legacy `conflict://`/`artifact://` entries
|
|
153
|
-
// rehydrated straight into `fileOps` from a pre-fix compaction summary) — only
|
|
154
|
-
// real files belong in `<files>`. New tool-call scans are already filtered.
|
|
155
|
-
const modified = new Set([...fileOps.edited, ...fileOps.written].filter(f => !isUrlSchemePath(f)));
|
|
156
|
-
const readOnly = [...fileOps.read].filter(f => !isUrlSchemePath(f) && !modified.has(f)).sort();
|
|
157
|
-
const modifiedFiles = [...modified].sort();
|
|
158
|
-
return { readFiles: readOnly, modifiedFiles };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Format file operations as one `<files>` tag: a grouped, prefix-folded
|
|
163
|
-
* directory tree (find-tool shape — `# dir/` headers, bare basenames) with a
|
|
164
|
-
* ` (Read)` / ` (Write)` / ` (RW)` marker per file instead of separate
|
|
165
|
-
* read/modified lists. `readSet` is the cumulative read set (`fileOps.read`),
|
|
166
|
-
* used to tell modified files that were also read (RW) from blind writes.
|
|
167
|
-
*/
|
|
168
|
-
const FILE_OPERATION_SUMMARY_LIMIT = 20;
|
|
169
|
-
|
|
170
|
-
function stripFileOperationTags(summary: string): string {
|
|
171
|
-
// Legacy <read-files>/<modified-files> tags are still stripped so summaries
|
|
172
|
-
// written before the combined <files> tag self-heal on the next compaction.
|
|
173
|
-
return summary
|
|
174
|
-
.replace(/<files>[\s\S]*?<\/files>\s*/g, "")
|
|
175
|
-
.replace(/<read-files>[\s\S]*?<\/read-files>\s*/g, "")
|
|
176
|
-
.replace(/<modified-files>[\s\S]*?<\/modified-files>\s*/g, "")
|
|
177
|
-
.trimEnd();
|
|
178
|
-
}
|
|
179
|
-
export function formatFileOperations(
|
|
180
|
-
readFiles: string[],
|
|
181
|
-
modifiedFiles: string[],
|
|
182
|
-
readSet?: ReadonlySet<string>,
|
|
183
|
-
): string {
|
|
184
|
-
if (readFiles.length === 0 && modifiedFiles.length === 0) return "";
|
|
185
|
-
const mode = new Map<string, "Read" | "Write" | "RW">();
|
|
186
|
-
for (const file of readFiles) mode.set(file, "Read");
|
|
187
|
-
for (const file of modifiedFiles) mode.set(file, readSet?.has(file) ? "RW" : "Write");
|
|
188
|
-
const all = [...mode.keys()].sort();
|
|
189
|
-
let files = formatGroupedPaths(all.slice(0, FILE_OPERATION_SUMMARY_LIMIT), path => ` (${mode.get(path)})`);
|
|
190
|
-
if (all.length > FILE_OPERATION_SUMMARY_LIMIT) {
|
|
191
|
-
files += `\n[…${all.length - FILE_OPERATION_SUMMARY_LIMIT} files elided…]`;
|
|
192
|
-
}
|
|
193
|
-
return prompt.render(fileOperationsTemplate, { files });
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function upsertFileOperations(
|
|
197
|
-
summary: string,
|
|
198
|
-
readFiles: string[],
|
|
199
|
-
modifiedFiles: string[],
|
|
200
|
-
readSet?: ReadonlySet<string>,
|
|
201
|
-
): string {
|
|
202
|
-
const baseSummary = stripFileOperationTags(summary);
|
|
203
|
-
const fileOperations = formatFileOperations(readFiles, modifiedFiles, readSet);
|
|
204
|
-
if (!fileOperations) return baseSummary;
|
|
205
|
-
if (!baseSummary) return fileOperations;
|
|
206
|
-
return `${baseSummary}\n\n${fileOperations}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
130
|
// ============================================================================
|
|
210
131
|
// Message Serialization
|
|
211
132
|
// ============================================================================
|
|
@@ -13,6 +13,16 @@ export class SentToolDefinitions {
|
|
|
13
13
|
for (const tool of tools) this.#byName.set(tool.name, tool);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A detached copy: an Agent that replays another's request history declares the same withdrawn tools. `keep`
|
|
18
|
+
* leaves out a definition the copy must never declare.
|
|
19
|
+
*/
|
|
20
|
+
copy(keep: (name: string) => boolean = () => true): SentToolDefinitions {
|
|
21
|
+
const copy = new SentToolDefinitions();
|
|
22
|
+
copy.record([...this.#byName.values()].filter(tool => keep(tool.name)));
|
|
23
|
+
return copy;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
/**
|
|
17
27
|
* `context` with {@link Context.inactiveTools} set to the definitions for names the latest
|
|
18
28
|
* `requestControls.tools.declared` in its messages holds that are not in its `tools`; cleared when
|
package/src/tokenizer.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { renderDemotedThinking } from "@oh-my-pi/pi-ai/dialect/demotion";
|
|
3
|
+
import { isFactoryDroidGeminiWire, keepsFactoryGeminiReasoning } from "@oh-my-pi/pi-ai/providers/factory-droid/gemini";
|
|
3
4
|
import { ollamaChatDropsOwnThinking } from "@oh-my-pi/pi-ai/providers/ollama";
|
|
4
5
|
import { completionsDropsOwnThinking, isLiteLLMThinkingSignature } from "@oh-my-pi/pi-ai/providers/openai-completions";
|
|
5
6
|
import { isColdReplayableResponsesTurn } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
6
7
|
import {
|
|
8
|
+
dropsForeignReasoningPayload,
|
|
7
9
|
isSameModelTurn,
|
|
8
10
|
shouldDropTruncatedThinkingOnlyAssistant,
|
|
9
11
|
} from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
@@ -22,7 +24,6 @@ import { base64ImageSize, estimateImageTokens, type ImageDetail, type ImageSize
|
|
|
22
24
|
import type { AgentMessage } from "./types";
|
|
23
25
|
|
|
24
26
|
const testEnv = Bun.env.NODE_ENV === "test";
|
|
25
|
-
const accurate = process.env.PI_TOKENIZER_ACCURATE === "1" && !testEnv;
|
|
26
27
|
|
|
27
28
|
const NATIVE_ENCODING: Record<ModelTokenizer, natives.Encoding> = {
|
|
28
29
|
"claude-v3": natives.Encoding.ClaudeV3,
|
|
@@ -33,6 +34,7 @@ const NATIVE_ENCODING: Record<ModelTokenizer, natives.Encoding> = {
|
|
|
33
34
|
"deepseek-v3": natives.Encoding.DeepSeekV3,
|
|
34
35
|
"kimi-k2": natives.Encoding.KimiK2,
|
|
35
36
|
glm5: natives.Encoding.Glm5,
|
|
37
|
+
cl100k: natives.Encoding.Cl100kBase,
|
|
36
38
|
o200k: natives.Encoding.O200kBase,
|
|
37
39
|
};
|
|
38
40
|
|
|
@@ -42,7 +44,12 @@ export function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
type TokenizerModel = Pick<Model, "tokenizer"> &
|
|
45
|
-
snapcompact.ShapeTarget & {
|
|
47
|
+
snapcompact.ShapeTarget & {
|
|
48
|
+
compat?: Model["compat"];
|
|
49
|
+
thinking?: Model["thinking"];
|
|
50
|
+
reasoning?: boolean;
|
|
51
|
+
requestModelId?: string;
|
|
52
|
+
};
|
|
46
53
|
|
|
47
54
|
/** The model an assistant turn's native reasoning (signed thinking, Responses history) is replayed to. */
|
|
48
55
|
interface ReasoningReplayReader {
|
|
@@ -77,13 +84,12 @@ export interface TokenizerOptions {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/**
|
|
80
|
-
*
|
|
81
|
-
* when known, o200k_base otherwise
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* undercounts).
|
|
87
|
+
* Every mode counts with the native tokenizer: the catalog-resolved family
|
|
88
|
+
* when known, o200k_base otherwise. They differ only in the fallback when the
|
|
89
|
+
* loaded addon does not recognize that encoding: `strict` and `upperbound`
|
|
90
|
+
* the raw byte length (never undercounts), `approximate` a bytes/4 guess.
|
|
91
|
+
* Tests (`NODE_ENV=test`) skip the native count outside `strict` and use that
|
|
92
|
+
* fallback.
|
|
87
93
|
*/
|
|
88
94
|
export type TokenCountMode = "strict" | "approximate" | "upperbound";
|
|
89
95
|
|
|
@@ -205,8 +211,8 @@ interface MessageEstimate {
|
|
|
205
211
|
* when the model's encoding, image or thinking-replay pricing, replay
|
|
206
212
|
* identity or native-history replay state changes); one-shot flows construct
|
|
207
213
|
* their own for the model that will be billed. Known tokenizer families use
|
|
208
|
-
* exact native counts;
|
|
209
|
-
*
|
|
214
|
+
* exact native counts; other models are counted in o200k_base, which bills
|
|
215
|
+
* unknown readers closer than a bytes/4 guess.
|
|
210
216
|
*/
|
|
211
217
|
export class Tokenizer {
|
|
212
218
|
readonly #encoding: natives.Encoding | null;
|
|
@@ -217,6 +223,8 @@ export class Tokenizer {
|
|
|
217
223
|
readonly #dropsCompletionsThinking: boolean;
|
|
218
224
|
/** Claude's own API and prefix-bound hosts keep another model's signature from their deployment. */
|
|
219
225
|
readonly #readsDeploymentSignatures: boolean;
|
|
226
|
+
/** Factory's Gemini generate wire, whose replay pass keeps only Google-origin reasoning. */
|
|
227
|
+
readonly #factoryGeminiWire: boolean;
|
|
220
228
|
readonly reasoningReplayKey: string;
|
|
221
229
|
/** How the reader bills replayed signed thinking (catalog `thinking-replay-tokens`). */
|
|
222
230
|
readonly thinkingReplayTokens: ThinkingReplayTokens;
|
|
@@ -259,6 +267,10 @@ export class Tokenizer {
|
|
|
259
267
|
model?.api === "anthropic-messages" &&
|
|
260
268
|
((compat !== undefined && "officialEndpoint" in compat && compat.officialEndpoint) ||
|
|
261
269
|
model.thinking?.prefixBinding === true);
|
|
270
|
+
this.#factoryGeminiWire =
|
|
271
|
+
model?.api === "factory-droid-agent" &&
|
|
272
|
+
model.id !== undefined &&
|
|
273
|
+
isFactoryDroidGeminiWire({ id: model.id, requestModelId: model.requestModelId });
|
|
262
274
|
this.reasoningReplayKey = reasoningReplayKey(model);
|
|
263
275
|
this.thinkingReplayTokens = resolveThinkingReplayTokens(model ?? {});
|
|
264
276
|
this.nativeHistoryReplay = options?.nativeHistoryReplay ?? true;
|
|
@@ -269,11 +281,8 @@ export class Tokenizer {
|
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
countTokens(text: string | string[], mode: TokenCountMode = "approximate"): number {
|
|
272
|
-
if (mode
|
|
273
|
-
|
|
274
|
-
return countTokensNat(text, this.#encoding, mode, this.#nativeCounts).tokens;
|
|
275
|
-
if (accurate) return countTokensNat(text, undefined, mode, this.#nativeCounts).tokens;
|
|
276
|
-
return sumFragments(text, mode === "upperbound" ? byteLength : byteEstimate);
|
|
284
|
+
if (testEnv && mode !== "strict") return sumFragments(text, mode === "upperbound" ? byteLength : byteEstimate);
|
|
285
|
+
return countTokensNat(text, this.#encoding, mode, this.#nativeCounts).tokens;
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
/**
|
|
@@ -365,6 +374,21 @@ export class Tokenizer {
|
|
|
365
374
|
);
|
|
366
375
|
}
|
|
367
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Whether `message`'s opaque reasoning payload (thinking signatures, redacted
|
|
379
|
+
* thinking) may reach this reader: everything except what `transformMessages`
|
|
380
|
+
* certainly strips from another model's turn, or what Factory's Gemini wire
|
|
381
|
+
* drops from a turn not of Google origin. Without a replay reader (no
|
|
382
|
+
* model, or a `filterReasoningHistory` target) every payload is charged,
|
|
383
|
+
* which overcounts a filtering reader's foreign turns.
|
|
384
|
+
*/
|
|
385
|
+
#receivesReasoningPayload(message: AssistantMessage): boolean {
|
|
386
|
+
const reader = this.#reasoningReader;
|
|
387
|
+
if (reader === undefined) return true;
|
|
388
|
+
if (this.#factoryGeminiWire) return keepsFactoryGeminiReasoning(message);
|
|
389
|
+
return !dropsForeignReasoningPayload(message, reader);
|
|
390
|
+
}
|
|
391
|
+
|
|
368
392
|
/**
|
|
369
393
|
* The replayed native Responses reasoning {@link countMessage} charges
|
|
370
394
|
* across `messages`: the part of a count that changes with the reading
|
|
@@ -468,15 +492,17 @@ export class Tokenizer {
|
|
|
468
492
|
fragments.push(block.thinking);
|
|
469
493
|
// Providers charge for the opaque signature/reasoning payload that
|
|
470
494
|
// rides alongside the thinking text (OpenAI Responses encrypted
|
|
471
|
-
// reasoning items, Gemini thought signatures)
|
|
472
|
-
// `
|
|
473
|
-
//
|
|
474
|
-
//
|
|
495
|
+
// reasoning items, Gemini thought signatures) wherever it survives
|
|
496
|
+
// replay; `transformMessages` strips another model's on most wires.
|
|
497
|
+
// A reader billing at `output` size (Claude) charges the reasoning,
|
|
498
|
+
// priced below, not these bytes. The compaction floor excludes it
|
|
499
|
+
// (its local byte size diverges from provider billing).
|
|
475
500
|
if (
|
|
476
501
|
block.thinkingSignature &&
|
|
477
502
|
!excludeEncryptedReasoning &&
|
|
478
503
|
signaturesPriced &&
|
|
479
|
-
typeof replayedReasoning !== "number"
|
|
504
|
+
typeof replayedReasoning !== "number" &&
|
|
505
|
+
this.#receivesReasoningPayload(message)
|
|
480
506
|
) {
|
|
481
507
|
fragments.push(block.thinkingSignature);
|
|
482
508
|
}
|
|
@@ -485,10 +511,13 @@ export class Tokenizer {
|
|
|
485
511
|
fragments.push(block.name);
|
|
486
512
|
fragments.push(stringifyJson(block.arguments) ?? "null");
|
|
487
513
|
} else if (block.type === "redactedThinking") {
|
|
488
|
-
// Encrypted reasoning blob the provider still bills for
|
|
489
|
-
// excluded from the compaction floor for
|
|
514
|
+
// Encrypted reasoning blob the provider still bills for wherever it
|
|
515
|
+
// survives replay, as above; excluded from the compaction floor for
|
|
516
|
+
// the same reason.
|
|
490
517
|
signedReasoning = true;
|
|
491
|
-
if (!excludeEncryptedReasoning && signaturesPriced
|
|
518
|
+
if (!excludeEncryptedReasoning && signaturesPriced && this.#receivesReasoningPayload(message)) {
|
|
519
|
+
fragments.push(block.data);
|
|
520
|
+
}
|
|
492
521
|
} else if (block.type === "anthropicServerTool") {
|
|
493
522
|
// Native Anthropic server-tool call/result replayed verbatim on the
|
|
494
523
|
// wire (server_tool_use input and opaque result content). The provider
|
package/src/types.ts
CHANGED
|
@@ -1087,7 +1087,7 @@ export type ToolApprovalDecision =
|
|
|
1087
1087
|
* prompt policies to the tool it dispatches into. */
|
|
1088
1088
|
policyKey?: string;
|
|
1089
1089
|
};
|
|
1090
|
-
export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
|
|
1090
|
+
export type ToolApproval = ToolApprovalDecision | ((args: unknown, context?: AgentToolContext) => ToolApprovalDecision);
|
|
1091
1091
|
|
|
1092
1092
|
/**
|
|
1093
1093
|
* Context passed to tool execution.
|