@oh-my-pi/pi-agent-core 18.15.0 → 18.17.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 +22 -0
- package/dist/types/agent-loop.d.ts +2 -0
- package/dist/types/agent.d.ts +11 -1
- package/dist/types/append-only-context.d.ts +9 -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/telemetry.d.ts +3 -1
- package/dist/types/tokenizer.d.ts +9 -9
- package/dist/types/types.d.ts +19 -0
- package/package.json +11 -11
- package/src/agent-loop.ts +75 -23
- package/src/agent.ts +83 -10
- package/src/append-only-context.ts +49 -18
- package/src/compaction/branch-summarization.ts +4 -10
- package/src/compaction/compaction-v2-streaming.ts +2 -3
- package/src/compaction/compaction.ts +6 -10
- package/src/compaction/openai.ts +0 -1
- package/src/compaction/utils.ts +19 -98
- package/src/telemetry.ts +1 -1
- package/src/tokenizer.ts +53 -24
- package/src/types.ts +21 -0
- package/src/compaction/prompts/file-operations.md +0 -5
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
|
@@ -48,6 +48,22 @@ export type PrepareQueuedMessages = (
|
|
|
48
48
|
queue: QueuedMessageQueue,
|
|
49
49
|
) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Synchronous arbiter over queued messages leaving a queue for delivery, for a
|
|
53
|
+
* queue whose messages someone else may withdraw concurrently (a session homed
|
|
54
|
+
* on another thread whose view pops them).
|
|
55
|
+
*/
|
|
56
|
+
export interface QueuedMessageGate {
|
|
57
|
+
/**
|
|
58
|
+
* Called with the batch about to be removed from `queue` for delivery; returns
|
|
59
|
+
* the candidates that must be dropped from the queue instead. The others are
|
|
60
|
+
* claimed for delivery.
|
|
61
|
+
*/
|
|
62
|
+
claim(queue: QueuedMessageQueue, candidates: readonly AgentMessage[]): readonly AgentMessage[];
|
|
63
|
+
/** Called with claimed messages that left delivery undelivered (requeued, withdrawn, or cancelled). */
|
|
64
|
+
release(messages: readonly AgentMessage[]): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
/** Called once an aside has been inserted into the agent's live context. */
|
|
52
68
|
export const ASIDE_MESSAGE_COMMIT = Symbol("aside-message-commit");
|
|
53
69
|
/** Symbol-keyed handoff for one finalized, tool-owned stream speculation session. */
|
|
@@ -278,6 +294,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
278
294
|
*/
|
|
279
295
|
getSteeringMessages?: (signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
280
296
|
|
|
297
|
+
/** Whether input is still owned by a queue and must be restored if a model call is aborted before it starts. */
|
|
298
|
+
isUndeliveredQueuedMessage?: (message: AgentMessage) => boolean;
|
|
299
|
+
|
|
281
300
|
/**
|
|
282
301
|
* Peeks whether steering messages are queued, without consuming them.
|
|
283
302
|
*
|
|
@@ -457,6 +476,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
457
476
|
* then strips from arguments before executing tools.
|
|
458
477
|
*/
|
|
459
478
|
intentTracing?: boolean;
|
|
479
|
+
/** Schema description of the injected intent field; `PI_INTENT_DESC` overrides it. Default `concise intent`. */
|
|
480
|
+
intentDescription?: string;
|
|
460
481
|
/**
|
|
461
482
|
* Strip tool descriptions (top-level + nested schema annotations) from the
|
|
462
483
|
* provider-bound tool specs. Use when the full catalog is rendered into the
|