@oh-my-pi/pi-agent-core 16.1.7 → 16.1.8
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 +15 -0
- package/dist/types/agent-loop.d.ts +2 -0
- package/dist/types/agent.d.ts +19 -1
- package/dist/types/compaction/compaction.d.ts +26 -1
- package/dist/types/types.d.ts +12 -2
- package/package.json +7 -7
- package/src/agent-loop.ts +10 -5
- package/src/agent.ts +55 -3
- package/src/compaction/compaction.ts +35 -5
- package/src/types.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.8] - 2026-06-20
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Changed `transformProviderContext` and `buildSideRequestContext` to return a Promise
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added `buildSideRequestContext` to the `Agent` class to build prompt-cache-friendly provider Contexts for side-channels or ephemeral requests.
|
|
14
|
+
- Added `compactionContextTokens(providerContextTokens, storedConversationEstimate)`: floors the provider-reported context tokens by a local estimate of the stored conversation for the compaction decision, so a `before_provider_request` payload transform (a compression extension, obfuscator, or inline snapcompact) that shrinks the request can no longer deflate provider usage below the true history size and suppress auto-compaction.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Exported helper functions `normalizeMessagesForProvider` and `resolveOwnedDialectFromEnv` from `packages/agent/src/agent-loop.ts`.
|
|
19
|
+
|
|
5
20
|
## [16.1.5] - 2026-06-19
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -8,6 +8,7 @@ import { type AgentRunCoverage, type AgentRunSummary } from "./run-collector";
|
|
|
8
8
|
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types";
|
|
9
9
|
/** Stop-details marker for a provider error after assistant content/tool args already streamed. */
|
|
10
10
|
export declare const STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL = "stream_interrupted_after_content";
|
|
11
|
+
export declare function resolveOwnedDialectFromEnv(value: string | undefined): Dialect | undefined;
|
|
11
12
|
/**
|
|
12
13
|
* Start an agent loop with a new prompt message.
|
|
13
14
|
* The prompt is added to the context and events are emitted for it.
|
|
@@ -54,6 +55,7 @@ export declare function agentLoopContinueDetailed(context: AgentContext, config:
|
|
|
54
55
|
readonly stream: EventStream<AgentEvent, AgentMessage[]>;
|
|
55
56
|
readonly detailed: () => Promise<AgentLoopDetailedResult>;
|
|
56
57
|
};
|
|
58
|
+
export declare function normalizeMessagesForProvider(messages: Context["messages"], model: AgentLoopConfig["model"]): Context["messages"];
|
|
57
59
|
export declare function normalizeTools(tools: AgentContext["tools"], injectIntent: boolean, exampleDialect?: Dialect, pruneDescriptions?: boolean): Context["tools"];
|
|
58
60
|
/** Resolve the human-readable reason an abort carried. A caller that aborts via
|
|
59
61
|
* `AbortController.abort(reason)` with a string or a non-`AbortError` `Error`
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface AgentOptions {
|
|
|
22
22
|
* Optional transform applied after provider context assembly and before
|
|
23
23
|
* telemetry capture/provider send.
|
|
24
24
|
*/
|
|
25
|
-
transformProviderContext?: (context: Context, model: Model) => Context
|
|
25
|
+
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
26
26
|
/**
|
|
27
27
|
* Steering mode: "all" = send all steering messages at once, "one-at-a-time" = one per turn
|
|
28
28
|
*/
|
|
@@ -104,6 +104,13 @@ export interface AgentOptions {
|
|
|
104
104
|
presencePenalty?: number;
|
|
105
105
|
repetitionPenalty?: number;
|
|
106
106
|
serviceTier?: ServiceTier;
|
|
107
|
+
/**
|
|
108
|
+
* Per-call effective service-tier resolver. When set, it authoritatively
|
|
109
|
+
* supplies the request's tier (replacing the static `serviceTier` and its
|
|
110
|
+
* telemetry) per model — used to scope a provider/model into a priority
|
|
111
|
+
* serving path without mutating the shared session `serviceTier`.
|
|
112
|
+
*/
|
|
113
|
+
serviceTierResolver?: (model: Model) => ServiceTier | undefined;
|
|
107
114
|
/**
|
|
108
115
|
* If true, request that the underlying provider omit reasoning/thinking summaries
|
|
109
116
|
* from the response. The model still reasons internally; only the human-readable
|
|
@@ -299,6 +306,8 @@ export declare class Agent {
|
|
|
299
306
|
set repetitionPenalty(value: number | undefined);
|
|
300
307
|
get serviceTier(): ServiceTier | undefined;
|
|
301
308
|
set serviceTier(value: ServiceTier | undefined);
|
|
309
|
+
get serviceTierResolver(): ((model: Model) => ServiceTier | undefined) | undefined;
|
|
310
|
+
set serviceTierResolver(value: ((model: Model) => ServiceTier | undefined) | undefined);
|
|
302
311
|
get hideThinkingSummary(): boolean | undefined;
|
|
303
312
|
set hideThinkingSummary(value: boolean | undefined);
|
|
304
313
|
/**
|
|
@@ -313,6 +322,15 @@ export declare class Agent {
|
|
|
313
322
|
get state(): AgentState;
|
|
314
323
|
get appendOnlyContext(): AppendOnlyContextManager | undefined;
|
|
315
324
|
setAppendOnlyContext(manager?: AppendOnlyContextManager): void;
|
|
325
|
+
/**
|
|
326
|
+
* Assemble the provider Context for a side-channel (no-loop) request, mirroring
|
|
327
|
+
* the main loop's prefix (system + normalized tools) so it shares the prompt
|
|
328
|
+
* cache. Never touches the append-only log or the tool-choice queue. Owned/
|
|
329
|
+
* in-band dialect sessions stay tools-less (matching their no-native-tools wire
|
|
330
|
+
* shape and avoiding tool-markup leakage). `llmMessages` is already converted
|
|
331
|
+
* (and, in production, obfuscated) by the caller.
|
|
332
|
+
*/
|
|
333
|
+
buildSideRequestContext(llmMessages: Message[]): Promise<Context>;
|
|
316
334
|
subscribe(fn: (e: AgentEvent) => void): () => void;
|
|
317
335
|
setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void;
|
|
318
336
|
setRawSseEventInterceptor(fn: SimpleStreamOptions["onSseEvent"] | undefined): void;
|
|
@@ -58,13 +58,38 @@ export declare function effectiveReserveTokens(contextWindow: number, settings:
|
|
|
58
58
|
* Check if compaction should trigger based on context usage.
|
|
59
59
|
*/
|
|
60
60
|
export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Context tokens to feed the compaction decision, floored by a local estimate of
|
|
63
|
+
* the stored conversation.
|
|
64
|
+
*
|
|
65
|
+
* The provider-reported usage is normally ground truth, but a
|
|
66
|
+
* `before_provider_request` payload transform — a compression extension (e.g.
|
|
67
|
+
* Headroom), an obfuscator, or inline snapcompact — can shrink the request below
|
|
68
|
+
* the real stored conversation. The provider then reports deflated prompt
|
|
69
|
+
* tokens, so anchoring compaction purely on that usage lets the real history
|
|
70
|
+
* grow unbounded until it overflows and native compaction can no longer run.
|
|
71
|
+
* Flooring by the agent's own estimate of the stored conversation keeps the
|
|
72
|
+
* compaction trigger honest regardless of on-wire compression. (Display/cost
|
|
73
|
+
* accounting still uses the exact provider usage; only the compaction decision
|
|
74
|
+
* takes the floor.)
|
|
75
|
+
*/
|
|
76
|
+
export declare function compactionContextTokens(providerContextTokens: number, storedConversationEstimate: number): number;
|
|
61
77
|
export declare function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number;
|
|
62
78
|
/**
|
|
63
79
|
* Estimate token count for a message using cl100k_base via the native
|
|
64
80
|
* tokenizer. This is not Claude's first-party tokenizer (Anthropic doesn't
|
|
65
81
|
* publish one) but is within ~5–10% across English/code text.
|
|
82
|
+
*
|
|
83
|
+
* `excludeEncryptedReasoning` drops opaque provider reasoning payloads
|
|
84
|
+
* (`thinkingSignature`, `redactedThinking`) from the estimate. Those are billed
|
|
85
|
+
* by the provider on replay, so the default counts them — but their *local*
|
|
86
|
+
* byte size can diverge wildly from what the provider charges, so the
|
|
87
|
+
* compaction floor (which only needs the reliably-countable, on-wire-compressible
|
|
88
|
+
* content) excludes them to avoid false triggers on thinking-heavy turns.
|
|
66
89
|
*/
|
|
67
|
-
export declare function estimateTokens(message: AgentMessage
|
|
90
|
+
export declare function estimateTokens(message: AgentMessage, options?: {
|
|
91
|
+
excludeEncryptedReasoning?: boolean;
|
|
92
|
+
}): number;
|
|
68
93
|
/**
|
|
69
94
|
* Find the user message (or bashExecution) that starts the turn containing the given entry index.
|
|
70
95
|
* Returns -1 if no turn start found before the index.
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApiKey, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Context, Effort, ImageContent, Message, Model, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolChoice, ToolResultMessage, TSchema } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import type { ApiKey, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Context, Effort, ImageContent, Message, Model, ServiceTier, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolChoice, ToolResultMessage, TSchema } from "@oh-my-pi/pi-ai";
|
|
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";
|
|
@@ -119,7 +119,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
119
119
|
* normalization, and append-only context handling, but before telemetry capture
|
|
120
120
|
* and provider send.
|
|
121
121
|
*/
|
|
122
|
-
transformProviderContext?: (context: Context, model: Model) => Context
|
|
122
|
+
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
123
123
|
/**
|
|
124
124
|
* Resolves the API key or resolver for the current model before each LLM call.
|
|
125
125
|
*
|
|
@@ -266,6 +266,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
266
266
|
* to the next provider call.
|
|
267
267
|
*/
|
|
268
268
|
getDisableReasoning?: () => boolean | undefined;
|
|
269
|
+
/**
|
|
270
|
+
* Per-call effective service-tier resolver. Unlike {@link getReasoning},
|
|
271
|
+
* this is *authoritative*: when set, its return value (including
|
|
272
|
+
* `undefined`) fully replaces the static `serviceTier` for the request and
|
|
273
|
+
* its telemetry. The resolver receives the model being requested so the
|
|
274
|
+
* caller can scope the tier per provider/model without mutating the shared
|
|
275
|
+
* session `serviceTier` (e.g. opting a Fireworks model into the Priority
|
|
276
|
+
* serving path while leaving the OpenAI/Anthropic tier untouched).
|
|
277
|
+
*/
|
|
278
|
+
getServiceTier?: (model: Model) => ServiceTier | undefined;
|
|
269
279
|
/**
|
|
270
280
|
* Called after a tool call has been validated and is about to execute.
|
|
271
281
|
*
|
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": "16.1.
|
|
4
|
+
"version": "16.1.8",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
40
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
43
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
38
|
+
"@oh-my-pi/pi-ai": "16.1.8",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "16.1.8",
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.1.8",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.1.8",
|
|
42
|
+
"@oh-my-pi/pi-wire": "16.1.8",
|
|
43
|
+
"@oh-my-pi/snapcompact": "16.1.8",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -121,7 +121,7 @@ class HarmonyLeakInterruption extends Error {
|
|
|
121
121
|
this.name = "HarmonyLeakInterruption";
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
function resolveOwnedDialectFromEnv(value: string | undefined): Dialect | undefined {
|
|
124
|
+
export function resolveOwnedDialectFromEnv(value: string | undefined): Dialect | undefined {
|
|
125
125
|
switch (value) {
|
|
126
126
|
case "1":
|
|
127
127
|
case "true":
|
|
@@ -502,7 +502,7 @@ function createDetailedCapture(config: AgentLoopConfig): {
|
|
|
502
502
|
};
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
-
function normalizeMessagesForProvider(
|
|
505
|
+
export function normalizeMessagesForProvider(
|
|
506
506
|
messages: Context["messages"],
|
|
507
507
|
model: AgentLoopConfig["model"],
|
|
508
508
|
): Context["messages"] {
|
|
@@ -1161,7 +1161,7 @@ async function streamAssistantResponse(
|
|
|
1161
1161
|
};
|
|
1162
1162
|
}
|
|
1163
1163
|
if (config.transformProviderContext) {
|
|
1164
|
-
llmContext = config.transformProviderContext(llmContext, config.model);
|
|
1164
|
+
llmContext = await config.transformProviderContext(llmContext, config.model);
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
1167
|
// Owned tool calling: take tool calls away from the provider and run them
|
|
@@ -1182,6 +1182,10 @@ async function streamAssistantResponse(
|
|
|
1182
1182
|
|
|
1183
1183
|
const dynamicReasoning = config.getReasoning?.();
|
|
1184
1184
|
const dynamicDisableReasoning = config.getDisableReasoning?.();
|
|
1185
|
+
// `getServiceTier` is authoritative when present (replaces the static tier
|
|
1186
|
+
// for both the wire request and telemetry), so callers can scope priority
|
|
1187
|
+
// per model without touching the shared session `serviceTier`.
|
|
1188
|
+
const effectiveServiceTier = config.getServiceTier ? config.getServiceTier(config.model) : config.serviceTier;
|
|
1185
1189
|
const harmonyMitigationEnabled = isHarmonyLeakMitigationTarget(config.model);
|
|
1186
1190
|
const harmonyAbortController = harmonyMitigationEnabled ? new AbortController() : undefined;
|
|
1187
1191
|
const requestSignal = harmonyAbortController
|
|
@@ -1229,7 +1233,7 @@ async function streamAssistantResponse(
|
|
|
1229
1233
|
topP: config.topP,
|
|
1230
1234
|
topK: config.topK,
|
|
1231
1235
|
presencePenalty: config.presencePenalty,
|
|
1232
|
-
serviceTier:
|
|
1236
|
+
serviceTier: effectiveServiceTier,
|
|
1233
1237
|
reasoningEffort: typeof effectiveReasoning === "string" ? effectiveReasoning : undefined,
|
|
1234
1238
|
toolChoice: effectiveToolChoice,
|
|
1235
1239
|
tools: llmContext.tools,
|
|
@@ -1251,7 +1255,7 @@ async function streamAssistantResponse(
|
|
|
1251
1255
|
const finishChat = async (message: AssistantMessage): Promise<void> => {
|
|
1252
1256
|
await finishChatSpan(telemetry, chatSpan, message, {
|
|
1253
1257
|
stepNumber: chatStepNumber,
|
|
1254
|
-
serviceTier:
|
|
1258
|
+
serviceTier: effectiveServiceTier,
|
|
1255
1259
|
responseHeaders: capturedHeaders,
|
|
1256
1260
|
baseUrl: config.model.baseUrl,
|
|
1257
1261
|
});
|
|
@@ -1267,6 +1271,7 @@ async function streamAssistantResponse(
|
|
|
1267
1271
|
reasoning: effectiveReasoning,
|
|
1268
1272
|
disableReasoning: effectiveDisableReasoning,
|
|
1269
1273
|
temperature: effectiveTemperature,
|
|
1274
|
+
serviceTier: effectiveServiceTier,
|
|
1270
1275
|
signal: finalRequestSignal,
|
|
1271
1276
|
onResponse: captureOnResponse,
|
|
1272
1277
|
});
|
package/src/agent.ts
CHANGED
|
@@ -24,9 +24,17 @@ import {
|
|
|
24
24
|
} from "@oh-my-pi/pi-ai";
|
|
25
25
|
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
26
26
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
27
|
+
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
27
28
|
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
28
29
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
29
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
abortReasonText,
|
|
32
|
+
agentLoop,
|
|
33
|
+
agentLoopContinue,
|
|
34
|
+
normalizeMessagesForProvider,
|
|
35
|
+
normalizeTools,
|
|
36
|
+
resolveOwnedDialectFromEnv,
|
|
37
|
+
} from "./agent-loop";
|
|
30
38
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
31
39
|
import type {
|
|
32
40
|
AgentContext,
|
|
@@ -102,7 +110,7 @@ export interface AgentOptions {
|
|
|
102
110
|
* Optional transform applied after provider context assembly and before
|
|
103
111
|
* telemetry capture/provider send.
|
|
104
112
|
*/
|
|
105
|
-
transformProviderContext?: (context: Context, model: Model) => Context
|
|
113
|
+
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
106
114
|
|
|
107
115
|
/**
|
|
108
116
|
* Steering mode: "all" = send all steering messages at once, "one-at-a-time" = one per turn
|
|
@@ -196,6 +204,13 @@ export interface AgentOptions {
|
|
|
196
204
|
presencePenalty?: number;
|
|
197
205
|
repetitionPenalty?: number;
|
|
198
206
|
serviceTier?: ServiceTier;
|
|
207
|
+
/**
|
|
208
|
+
* Per-call effective service-tier resolver. When set, it authoritatively
|
|
209
|
+
* supplies the request's tier (replacing the static `serviceTier` and its
|
|
210
|
+
* telemetry) per model — used to scope a provider/model into a priority
|
|
211
|
+
* serving path without mutating the shared session `serviceTier`.
|
|
212
|
+
*/
|
|
213
|
+
serviceTierResolver?: (model: Model) => ServiceTier | undefined;
|
|
199
214
|
/**
|
|
200
215
|
* If true, request that the underlying provider omit reasoning/thinking summaries
|
|
201
216
|
* from the response. The model still reasons internally; only the human-readable
|
|
@@ -313,7 +328,7 @@ export class Agent {
|
|
|
313
328
|
#abortController?: AbortController;
|
|
314
329
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
315
330
|
#transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
316
|
-
#transformProviderContext?: (context: Context, model: Model) => Context
|
|
331
|
+
#transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
317
332
|
#steeringQueue: AgentMessage[] = [];
|
|
318
333
|
#followUpQueue: AgentMessage[] = [];
|
|
319
334
|
#steeringMode: "all" | "one-at-a-time";
|
|
@@ -333,6 +348,7 @@ export class Agent {
|
|
|
333
348
|
#presencePenalty?: number;
|
|
334
349
|
#repetitionPenalty?: number;
|
|
335
350
|
#serviceTier?: ServiceTier;
|
|
351
|
+
#serviceTierResolver?: (model: Model) => ServiceTier | undefined;
|
|
336
352
|
#hideThinkingSummary?: boolean;
|
|
337
353
|
#maxRetryDelayMs?: number;
|
|
338
354
|
#getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
|
|
@@ -403,6 +419,7 @@ export class Agent {
|
|
|
403
419
|
this.#presencePenalty = opts.presencePenalty;
|
|
404
420
|
this.#repetitionPenalty = opts.repetitionPenalty;
|
|
405
421
|
this.#serviceTier = opts.serviceTier;
|
|
422
|
+
this.#serviceTierResolver = opts.serviceTierResolver;
|
|
406
423
|
this.#hideThinkingSummary = opts.hideThinkingSummary;
|
|
407
424
|
this.#maxRetryDelayMs = opts.maxRetryDelayMs;
|
|
408
425
|
this.getApiKey = opts.getApiKey;
|
|
@@ -610,6 +627,14 @@ export class Agent {
|
|
|
610
627
|
this.#serviceTier = value;
|
|
611
628
|
}
|
|
612
629
|
|
|
630
|
+
get serviceTierResolver(): ((model: Model) => ServiceTier | undefined) | undefined {
|
|
631
|
+
return this.#serviceTierResolver;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
set serviceTierResolver(value: ((model: Model) => ServiceTier | undefined) | undefined) {
|
|
635
|
+
this.#serviceTierResolver = value;
|
|
636
|
+
}
|
|
637
|
+
|
|
613
638
|
get hideThinkingSummary(): boolean | undefined {
|
|
614
639
|
return this.#hideThinkingSummary;
|
|
615
640
|
}
|
|
@@ -645,6 +670,32 @@ export class Agent {
|
|
|
645
670
|
this.#appendOnlyContext = manager;
|
|
646
671
|
}
|
|
647
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Assemble the provider Context for a side-channel (no-loop) request, mirroring
|
|
675
|
+
* the main loop's prefix (system + normalized tools) so it shares the prompt
|
|
676
|
+
* cache. Never touches the append-only log or the tool-choice queue. Owned/
|
|
677
|
+
* in-band dialect sessions stay tools-less (matching their no-native-tools wire
|
|
678
|
+
* shape and avoiding tool-markup leakage). `llmMessages` is already converted
|
|
679
|
+
* (and, in production, obfuscated) by the caller.
|
|
680
|
+
*/
|
|
681
|
+
async buildSideRequestContext(llmMessages: Message[]): Promise<Context> {
|
|
682
|
+
const model = this.#state.model;
|
|
683
|
+
if (!model) throw new Error("No active model on agent");
|
|
684
|
+
const ownedDialect = this.#dialect ?? resolveOwnedDialectFromEnv(Bun.env.PI_DIALECT);
|
|
685
|
+
const messages = normalizeMessagesForProvider(llmMessages, model);
|
|
686
|
+
const tools = ownedDialect
|
|
687
|
+
? []
|
|
688
|
+
: (normalizeTools(
|
|
689
|
+
this.#state.tools,
|
|
690
|
+
this.#intentTracing,
|
|
691
|
+
preferredDialect(model.id),
|
|
692
|
+
this.#pruneToolDescriptions,
|
|
693
|
+
) ?? []);
|
|
694
|
+
let context: Context = { systemPrompt: this.#state.systemPrompt, messages, tools };
|
|
695
|
+
if (this.#transformProviderContext) context = await this.#transformProviderContext(context, model);
|
|
696
|
+
return context;
|
|
697
|
+
}
|
|
698
|
+
|
|
648
699
|
subscribe(fn: (e: AgentEvent) => void): () => void {
|
|
649
700
|
this.#listeners.add(fn);
|
|
650
701
|
return () => this.#listeners.delete(fn);
|
|
@@ -1087,6 +1138,7 @@ export class Agent {
|
|
|
1087
1138
|
getToolChoice,
|
|
1088
1139
|
getReasoning: () => this.#state.thinkingLevel,
|
|
1089
1140
|
getDisableReasoning: () => this.#state.disableReasoning,
|
|
1141
|
+
getServiceTier: this.#serviceTierResolver,
|
|
1090
1142
|
getSteeringMessages: async () => {
|
|
1091
1143
|
if (skipInitialSteeringPoll) {
|
|
1092
1144
|
skipInitialSteeringPoll = false;
|
|
@@ -228,6 +228,25 @@ export function shouldCompact(contextTokens: number, contextWindow: number, sett
|
|
|
228
228
|
return contextTokens > thresholdTokens;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Context tokens to feed the compaction decision, floored by a local estimate of
|
|
233
|
+
* the stored conversation.
|
|
234
|
+
*
|
|
235
|
+
* The provider-reported usage is normally ground truth, but a
|
|
236
|
+
* `before_provider_request` payload transform — a compression extension (e.g.
|
|
237
|
+
* Headroom), an obfuscator, or inline snapcompact — can shrink the request below
|
|
238
|
+
* the real stored conversation. The provider then reports deflated prompt
|
|
239
|
+
* tokens, so anchoring compaction purely on that usage lets the real history
|
|
240
|
+
* grow unbounded until it overflows and native compaction can no longer run.
|
|
241
|
+
* Flooring by the agent's own estimate of the stored conversation keeps the
|
|
242
|
+
* compaction trigger honest regardless of on-wire compression. (Display/cost
|
|
243
|
+
* accounting still uses the exact provider usage; only the compaction decision
|
|
244
|
+
* takes the floor.)
|
|
245
|
+
*/
|
|
246
|
+
export function compactionContextTokens(providerContextTokens: number, storedConversationEstimate: number): number {
|
|
247
|
+
return Math.max(Math.max(0, providerContextTokens), Math.max(0, storedConversationEstimate));
|
|
248
|
+
}
|
|
249
|
+
|
|
231
250
|
export function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
232
251
|
// Fixed token limit takes priority over percentage
|
|
233
252
|
const thresholdTokens = settings.thresholdTokens;
|
|
@@ -259,8 +278,15 @@ const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
|
259
278
|
* Estimate token count for a message using cl100k_base via the native
|
|
260
279
|
* tokenizer. This is not Claude's first-party tokenizer (Anthropic doesn't
|
|
261
280
|
* publish one) but is within ~5–10% across English/code text.
|
|
281
|
+
*
|
|
282
|
+
* `excludeEncryptedReasoning` drops opaque provider reasoning payloads
|
|
283
|
+
* (`thinkingSignature`, `redactedThinking`) from the estimate. Those are billed
|
|
284
|
+
* by the provider on replay, so the default counts them — but their *local*
|
|
285
|
+
* byte size can diverge wildly from what the provider charges, so the
|
|
286
|
+
* compaction floor (which only needs the reliably-countable, on-wire-compressible
|
|
287
|
+
* content) excludes them to avoid false triggers on thinking-heavy turns.
|
|
262
288
|
*/
|
|
263
|
-
export function estimateTokens(message: AgentMessage): number {
|
|
289
|
+
export function estimateTokens(message: AgentMessage, options?: { excludeEncryptedReasoning?: boolean }): number {
|
|
264
290
|
const fragments: string[] = [];
|
|
265
291
|
let extra = 0;
|
|
266
292
|
if ((message as { role?: string }).role === "bashExecution") {
|
|
@@ -296,14 +322,18 @@ export function estimateTokens(message: AgentMessage): number {
|
|
|
296
322
|
// reasoning items, Anthropic signed thinking blocks, etc.). Without
|
|
297
323
|
// counting it, this estimator can read ~half of the provider-reported
|
|
298
324
|
// usage on thinking-heavy turns — see #2275 for the resulting
|
|
299
|
-
// compaction-trigger / post-check metric divergence.
|
|
300
|
-
|
|
325
|
+
// compaction-trigger / post-check metric divergence. The compaction
|
|
326
|
+
// floor excludes it (its local byte size diverges from provider billing).
|
|
327
|
+
if (block.thinkingSignature && !options?.excludeEncryptedReasoning) {
|
|
328
|
+
fragments.push(block.thinkingSignature);
|
|
329
|
+
}
|
|
301
330
|
} else if (block.type === "toolCall") {
|
|
302
331
|
fragments.push(block.name);
|
|
303
332
|
fragments.push(JSON.stringify(block.arguments));
|
|
304
333
|
} else if (block.type === "redactedThinking") {
|
|
305
|
-
// Encrypted reasoning blob the provider still bills for on replay
|
|
306
|
-
|
|
334
|
+
// Encrypted reasoning blob the provider still bills for on replay;
|
|
335
|
+
// excluded from the compaction floor for the same reason as above.
|
|
336
|
+
if (!options?.excludeEncryptedReasoning) fragments.push(block.data);
|
|
307
337
|
}
|
|
308
338
|
}
|
|
309
339
|
break;
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ImageContent,
|
|
9
9
|
Message,
|
|
10
10
|
Model,
|
|
11
|
+
ServiceTier,
|
|
11
12
|
SimpleStreamOptions,
|
|
12
13
|
Static,
|
|
13
14
|
streamSimple,
|
|
@@ -154,7 +155,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
154
155
|
* normalization, and append-only context handling, but before telemetry capture
|
|
155
156
|
* and provider send.
|
|
156
157
|
*/
|
|
157
|
-
transformProviderContext?: (context: Context, model: Model) => Context
|
|
158
|
+
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
161
|
* Resolves the API key or resolver for the current model before each LLM call.
|
|
@@ -315,6 +316,17 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
315
316
|
*/
|
|
316
317
|
getDisableReasoning?: () => boolean | undefined;
|
|
317
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Per-call effective service-tier resolver. Unlike {@link getReasoning},
|
|
321
|
+
* this is *authoritative*: when set, its return value (including
|
|
322
|
+
* `undefined`) fully replaces the static `serviceTier` for the request and
|
|
323
|
+
* its telemetry. The resolver receives the model being requested so the
|
|
324
|
+
* caller can scope the tier per provider/model without mutating the shared
|
|
325
|
+
* session `serviceTier` (e.g. opting a Fireworks model into the Priority
|
|
326
|
+
* serving path while leaving the OpenAI/Anthropic tier untouched).
|
|
327
|
+
*/
|
|
328
|
+
getServiceTier?: (model: Model) => ServiceTier | undefined;
|
|
329
|
+
|
|
318
330
|
/**
|
|
319
331
|
* Called after a tool call has been validated and is about to execute.
|
|
320
332
|
*
|