@linxiraos/pi-agent-core 1.0.9 → 1.1.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 +2 -1283
- package/LICENSE +23 -0
- package/package.json +77 -78
- package/src/agent-loop.ts +17 -2
- package/src/agent.ts +25 -4
- package/src/append-only-context.ts +28 -2
- package/src/compaction/branch-summarization.ts +15 -8
- package/src/compaction/compaction-v2-streaming.ts +2 -0
- package/src/compaction/compaction.ts +234 -162
- package/src/compaction/entries.ts +11 -0
- package/src/compaction/errors.ts +2 -2
- package/src/compaction/index.ts +1 -0
- package/src/compaction/message-cache.ts +33 -34
- package/src/compaction/messages.ts +37 -6
- package/src/compaction/openai.ts +54 -20
- package/src/compaction/prompts/handoff-document.md +2 -0
- package/src/compaction/prompts/handoff-summary-context.md +9 -0
- package/src/compaction/prompts/summarization-system.md +2 -0
- package/src/compaction/pruning.ts +25 -13
- package/src/compaction/shake.ts +18 -16
- package/src/compaction/transcript-tokens.ts +111 -0
- package/src/compaction/utils.ts +9 -2
- package/src/tokenizer.ts +273 -18
- package/src/types.ts +1 -1
- package/dist/types/agent-loop.d.ts +0 -137
- package/dist/types/agent.d.ts +0 -465
- package/dist/types/append-only-context.d.ts +0 -131
- package/dist/types/compaction/branch-summarization.d.ts +0 -101
- package/dist/types/compaction/compaction-v2-streaming.d.ts +0 -85
- package/dist/types/compaction/compaction.d.ts +0 -321
- package/dist/types/compaction/entries.d.ts +0 -111
- package/dist/types/compaction/errors.d.ts +0 -38
- package/dist/types/compaction/index.d.ts +0 -13
- package/dist/types/compaction/message-cache.d.ts +0 -23
- package/dist/types/compaction/messages.d.ts +0 -79
- package/dist/types/compaction/openai.d.ts +0 -117
- package/dist/types/compaction/pruning.d.ts +0 -105
- package/dist/types/compaction/shake.d.ts +0 -99
- package/dist/types/compaction/tool-protection.d.ts +0 -19
- package/dist/types/compaction/utils.d.ts +0 -61
- package/dist/types/compaction.d.ts +0 -1
- package/dist/types/index.d.ts +0 -13
- package/dist/types/pause.d.ts +0 -46
- package/dist/types/proxy.d.ts +0 -91
- package/dist/types/replay-policy.d.ts +0 -5
- package/dist/types/run-collector.d.ts +0 -196
- package/dist/types/telemetry.d.ts +0 -625
- package/dist/types/thinking.d.ts +0 -18
- package/dist/types/tokenizer.d.ts +0 -2
- package/dist/types/types.d.ts +0 -786
- package/dist/types/utils/yield.d.ts +0 -71
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner
|
|
4
|
+
Copyright (c) 2025-2026 Can Bölük
|
|
5
|
+
Copyright (c) 2026 Stencil Labs, Inc.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,79 +1,78 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@linxiraos/pi-agent-core",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
|
+
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
|
+
"author": "Can Boluk",
|
|
8
|
+
"contributors": [
|
|
9
|
+
"Mario Zechner"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
|
|
15
|
+
"directory": "packages/agent"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/Linxira-OS/linxira-zeta/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"llm",
|
|
24
|
+
"transport",
|
|
25
|
+
"state-management"
|
|
26
|
+
],
|
|
27
|
+
"main": "./src/index.ts",
|
|
28
|
+
"types": "./src/index.ts",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"check": "biome check . && bun run check:types",
|
|
31
|
+
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
32
|
+
"lint": "biome lint .",
|
|
33
|
+
"test": "bun test --parallel",
|
|
34
|
+
"fix": "biome check --write --unsafe .",
|
|
35
|
+
"fmt": "biome format --write ."
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@linxiraos/pi-ai": "catalog:",
|
|
39
|
+
"@linxiraos/pi-catalog": "catalog:",
|
|
40
|
+
"@linxiraos/pi-natives": "catalog:",
|
|
41
|
+
"@linxiraos/pi-utils": "catalog:",
|
|
42
|
+
"@linxiraos/pi-wire": "catalog:",
|
|
43
|
+
"@linxiraos/pi-snapcompact": "catalog:",
|
|
44
|
+
"@opentelemetry/api": "catalog:"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@linxiraos/pi-omptype": "catalog:",
|
|
48
|
+
"@opentelemetry/context-async-hooks": "catalog:",
|
|
49
|
+
"@opentelemetry/sdk-trace-base": "catalog:",
|
|
50
|
+
"@types/bun": "catalog:"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"bun": ">=1.3.14"
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"src",
|
|
57
|
+
"README.md",
|
|
58
|
+
"CHANGELOG.md"
|
|
59
|
+
],
|
|
60
|
+
"exports": {
|
|
61
|
+
".": {
|
|
62
|
+
"types": "./src/index.ts",
|
|
63
|
+
"import": "./src/index.ts"
|
|
64
|
+
},
|
|
65
|
+
"./compaction": {
|
|
66
|
+
"types": "./src/compaction.ts",
|
|
67
|
+
"import": "./src/compaction.ts"
|
|
68
|
+
},
|
|
69
|
+
"./compaction/*": {
|
|
70
|
+
"types": "./src/compaction/*.ts",
|
|
71
|
+
"import": "./src/compaction/*.ts"
|
|
72
|
+
},
|
|
73
|
+
"./*": {
|
|
74
|
+
"types": "./src/*.ts",
|
|
75
|
+
"import": "./src/*.ts"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/agent-loop.ts
CHANGED
|
@@ -623,6 +623,15 @@ function buildAgentEndEvent(
|
|
|
623
623
|
* Push a `turn_end` event and run the awaited per-turn hook when the run is
|
|
624
624
|
* still healthy. The hook is skipped for externally aborted or errored turns so
|
|
625
625
|
* a user interrupt does not hang on a background backlog wait.
|
|
626
|
+
*
|
|
627
|
+
* A {@link TERMINAL_TOOL_RESULT_ABORT_REASON} abort is the exception: it is a
|
|
628
|
+
* graceful yield (e.g. a subagent's final `yield` tool), not a user interrupt.
|
|
629
|
+
* The completed tool batch is persisted and the turn must still reach
|
|
630
|
+
* `onTurnEnd` so per-turn bookkeeping — notably advisor review of the yield
|
|
631
|
+
* delta (#9505) — runs exactly as it does for a plain end-of-turn message. The
|
|
632
|
+
* hook receives no signal in that case so downstream waits (advisor catch-up)
|
|
633
|
+
* behave identically to a normal final turn instead of short-circuiting on the
|
|
634
|
+
* spent abort.
|
|
626
635
|
*/
|
|
627
636
|
async function emitTurnEnd(
|
|
628
637
|
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
@@ -635,10 +644,16 @@ async function emitTurnEnd(
|
|
|
635
644
|
runHookOnAbortedMessage = false,
|
|
636
645
|
): Promise<void> {
|
|
637
646
|
stream.push({ type: "turn_end", message, toolResults });
|
|
647
|
+
const terminalYield = signal?.reason === TERMINAL_TOOL_RESULT_ABORT_REASON;
|
|
638
648
|
const isAbortedOrError =
|
|
639
649
|
message.role === "assistant" && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
640
|
-
if (signal?.aborted || (isAbortedOrError && !runHookOnAbortedMessage)) return;
|
|
641
|
-
await config.onTurnEnd?.(currentContext.messages,
|
|
650
|
+
if ((signal?.aborted && !terminalYield) || (isAbortedOrError && !runHookOnAbortedMessage)) return;
|
|
651
|
+
await config.onTurnEnd?.(currentContext.messages, terminalYield ? undefined : signal, {
|
|
652
|
+
message,
|
|
653
|
+
toolResults,
|
|
654
|
+
willContinue: false,
|
|
655
|
+
...context,
|
|
656
|
+
});
|
|
642
657
|
}
|
|
643
658
|
|
|
644
659
|
function createGateStopMessage(model: Model, reason: string | undefined): AssistantMessage {
|
package/src/agent.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "./agent-loop";
|
|
38
38
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
39
39
|
import { isProviderRefusalMessage } from "./replay-policy";
|
|
40
|
+
import { Tokenizer, tokenizerEncodingForModel } from "./tokenizer";
|
|
40
41
|
import type {
|
|
41
42
|
AgentBeforeModelCall,
|
|
42
43
|
AgentContext,
|
|
@@ -363,7 +364,7 @@ export class Agent {
|
|
|
363
364
|
pendingToolCalls: new Set<string>(),
|
|
364
365
|
error: undefined,
|
|
365
366
|
};
|
|
366
|
-
|
|
367
|
+
#tokenizer = new Tokenizer(this.#state.model);
|
|
367
368
|
#listeners = new Set<(e: AgentEvent) => void>();
|
|
368
369
|
#abortController?: AbortController;
|
|
369
370
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
@@ -459,6 +460,7 @@ export class Agent {
|
|
|
459
460
|
if (opts.initialState?.messages) this.#state.messages = opts.initialState.messages.slice();
|
|
460
461
|
if (opts.initialState?.pendingToolCalls)
|
|
461
462
|
this.#state.pendingToolCalls = new Set(opts.initialState.pendingToolCalls);
|
|
463
|
+
this.#syncTokenizer(this.#state.model);
|
|
462
464
|
this.#convertToLlm = opts.convertToLlm || defaultConvertToLlm;
|
|
463
465
|
this.#transformContext = opts.transformContext;
|
|
464
466
|
this.#steeringMode = opts.steeringMode || "one-at-a-time";
|
|
@@ -720,11 +722,29 @@ export class Agent {
|
|
|
720
722
|
set maxRetryDelayMs(value: number | undefined) {
|
|
721
723
|
this.#maxRetryDelayMs = value;
|
|
722
724
|
}
|
|
723
|
-
|
|
724
725
|
get state(): AgentState {
|
|
725
726
|
return this.#state;
|
|
726
727
|
}
|
|
727
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Tokenizer for the active model. The instance is replaced whenever the
|
|
731
|
+
* active model's encoding changes (see {@link setModel}), so callers must
|
|
732
|
+
* not cache it across model switches.
|
|
733
|
+
*/
|
|
734
|
+
get tokenizer(): Tokenizer {
|
|
735
|
+
return this.#tokenizer;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Swap the tokenizer only when the encoding actually changes, so the warm
|
|
740
|
+
* per-message memo survives same-encoding model switches.
|
|
741
|
+
*/
|
|
742
|
+
#syncTokenizer(model: Model | null | undefined): void {
|
|
743
|
+
if (tokenizerEncodingForModel(model) !== this.#tokenizer.encoding) {
|
|
744
|
+
this.#tokenizer = new Tokenizer(model);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
728
748
|
get appendOnlyContext(): AppendOnlyContextManager | undefined {
|
|
729
749
|
return this.#appendOnlyContext;
|
|
730
750
|
}
|
|
@@ -902,8 +922,9 @@ export class Agent {
|
|
|
902
922
|
this.#state.systemPrompt = typeof v === "string" ? [v] : v;
|
|
903
923
|
}
|
|
904
924
|
|
|
905
|
-
setModel(
|
|
906
|
-
this.#state.model =
|
|
925
|
+
setModel(model: Model) {
|
|
926
|
+
this.#state.model = model;
|
|
927
|
+
this.#syncTokenizer(model);
|
|
907
928
|
}
|
|
908
929
|
|
|
909
930
|
setThinkingLevel(l: Effort | undefined) {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Context, Message, Tool } from "@linxiraos/pi-ai";
|
|
18
18
|
import { normalizeTools } from "./agent-loop";
|
|
19
|
-
import
|
|
19
|
+
import { messageEstimateVersion } from "./compaction/message-cache";
|
|
20
|
+
import type { AgentContext, AgentMessage } from "./types";
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// StablePrefix (formerly ImmutablePrefix)
|
|
@@ -177,6 +178,26 @@ export class AppendOnlyContextManager {
|
|
|
177
178
|
* point on every subsequent turn.
|
|
178
179
|
*/
|
|
179
180
|
#messageDigests: number[] = [];
|
|
181
|
+
/**
|
|
182
|
+
* Digests memoized by message object identity, validated by the message's
|
|
183
|
+
* estimate version ({@link messageEstimateVersion}). Synced message objects
|
|
184
|
+
* are stable between calls: converted fragments are cached per session
|
|
185
|
+
* message identity and handed back unchanged on every call, so an unchanged
|
|
186
|
+
* history re-hits this memo instead of re-serializing every previously-
|
|
187
|
+
* synced message on each LLM call.
|
|
188
|
+
*
|
|
189
|
+
* Owner-side rewrites (prune/shake/strip-images) mutate messages IN PLACE
|
|
190
|
+
* under stable identity — for assistant pass-through fragments the log
|
|
191
|
+
* aliases the very object being mutated — so a bare identity memo would
|
|
192
|
+
* serve pre-mutation bytes forever. Those owners MUST call
|
|
193
|
+
* `invalidateMessageCache`, which bumps the symbol-keyed version tag
|
|
194
|
+
* this memo validates before every hit; a version mismatch recomputes from
|
|
195
|
+
* actual bytes and the sync diverges exactly as if a fresh object had
|
|
196
|
+
* arrived. Mutating a synced message without that bump violates the
|
|
197
|
+
* cache-coherence contract shared with the tokenizer and convert caches
|
|
198
|
+
* (see `compaction/message-cache.ts`) and is unsupported.
|
|
199
|
+
*/
|
|
200
|
+
#digestMemo = new WeakMap<object, { version: number; digest: number }>();
|
|
180
201
|
|
|
181
202
|
build(context: AgentContext, options: BuildOptions): Context {
|
|
182
203
|
this.prefix.build(context, options);
|
|
@@ -290,6 +311,9 @@ export class AppendOnlyContextManager {
|
|
|
290
311
|
#messageDigest(msg: unknown): number {
|
|
291
312
|
if (!msg || typeof msg !== "object") return 0;
|
|
292
313
|
const m = msg as Record<string, unknown>;
|
|
314
|
+
const version = messageEstimateVersion(msg as AgentMessage);
|
|
315
|
+
const cached = this.#digestMemo.get(m);
|
|
316
|
+
if (cached !== undefined && cached.version === version) return cached.digest;
|
|
293
317
|
const payload = JSON.stringify({
|
|
294
318
|
r: m.role ?? null,
|
|
295
319
|
c: m.content ?? null,
|
|
@@ -304,7 +328,9 @@ export class AppendOnlyContextManager {
|
|
|
304
328
|
for (let j = 0; j < payload.length; j++) {
|
|
305
329
|
hash = ((hash << 5) - hash + payload.charCodeAt(j)) | 0;
|
|
306
330
|
}
|
|
307
|
-
|
|
331
|
+
const hash32 = hash >>> 0;
|
|
332
|
+
this.#digestMemo.set(m, { version, digest: hash32 });
|
|
333
|
+
return hash32;
|
|
308
334
|
}
|
|
309
335
|
}
|
|
310
336
|
|
|
@@ -9,8 +9,8 @@ import type { Api, ApiKey, AssistantMessage, Context, Model, SimpleStreamOptions
|
|
|
9
9
|
import { preferredDialect } from "@linxiraos/pi-catalog/identity";
|
|
10
10
|
import { prompt } from "@linxiraos/pi-utils";
|
|
11
11
|
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
12
|
+
import { Tokenizer } from "../tokenizer";
|
|
12
13
|
import type { AgentMessage } from "../types";
|
|
13
|
-
import { estimateTokens } from "./compaction";
|
|
14
14
|
import type { ReadonlySessionManager, SessionEntry } from "./entries";
|
|
15
15
|
import {
|
|
16
16
|
type ConvertToLlm,
|
|
@@ -191,7 +191,9 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
|
191
191
|
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
|
192
192
|
|
|
193
193
|
case "compaction":
|
|
194
|
-
return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp,
|
|
194
|
+
return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp, {
|
|
195
|
+
shortSummary: entry.shortSummary,
|
|
196
|
+
});
|
|
195
197
|
|
|
196
198
|
// These don't contribute to conversation content
|
|
197
199
|
case "thinking_level_change":
|
|
@@ -206,14 +208,14 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
|
|
209
|
-
function estimateBranchSummaryTokens(message: AgentMessage): number {
|
|
210
|
-
if (message.role !== "toolResult") return
|
|
211
|
+
function estimateBranchSummaryTokens(message: AgentMessage, tokenizer: Tokenizer): number {
|
|
212
|
+
if (message.role !== "toolResult") return tokenizer.countMessage(message);
|
|
211
213
|
const text = message.content
|
|
212
214
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
213
215
|
.map(c => c.text)
|
|
214
216
|
.join("");
|
|
215
217
|
if (!text) return 0;
|
|
216
|
-
return
|
|
218
|
+
return tokenizer.countMessage({
|
|
217
219
|
...message,
|
|
218
220
|
content: [{ type: "text", text: truncateToolResultForSummary(text) }],
|
|
219
221
|
});
|
|
@@ -232,7 +234,11 @@ function estimateBranchSummaryTokens(message: AgentMessage): number {
|
|
|
232
234
|
* @param entries - Entries in chronological order
|
|
233
235
|
* @param tokenBudget - Maximum tokens to include (0 = no limit)
|
|
234
236
|
*/
|
|
235
|
-
export function prepareBranchEntries(
|
|
237
|
+
export function prepareBranchEntries(
|
|
238
|
+
entries: SessionEntry[],
|
|
239
|
+
tokenizer: Tokenizer,
|
|
240
|
+
tokenBudget: number = 0,
|
|
241
|
+
): BranchPreparation {
|
|
236
242
|
const messages: AgentMessage[] = [];
|
|
237
243
|
const fileOps = createFileOps();
|
|
238
244
|
let totalTokens = 0;
|
|
@@ -264,7 +270,7 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
|
|
|
264
270
|
// Extract file ops from assistant messages (tool calls)
|
|
265
271
|
extractFileOpsFromMessage(message, fileOps);
|
|
266
272
|
|
|
267
|
-
const tokens = estimateBranchSummaryTokens(message);
|
|
273
|
+
const tokens = estimateBranchSummaryTokens(message, tokenizer);
|
|
268
274
|
|
|
269
275
|
// Check budget before adding
|
|
270
276
|
if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
|
|
@@ -309,8 +315,9 @@ export async function generateBranchSummary(
|
|
|
309
315
|
// Token budget = context window minus reserved space for prompt + response
|
|
310
316
|
const contextWindow = model.contextWindow || 128000;
|
|
311
317
|
const tokenBudget = contextWindow - reserveTokens;
|
|
318
|
+
const tokenizer = new Tokenizer(model);
|
|
312
319
|
|
|
313
|
-
const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget);
|
|
320
|
+
const { messages, fileOps } = prepareBranchEntries(entries, tokenizer, tokenBudget);
|
|
314
321
|
|
|
315
322
|
if (messages.length === 0) {
|
|
316
323
|
return { summary: "No content to summarize" };
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "@linxiraos/pi-ai/providers/openai-shared";
|
|
26
26
|
import { captureOpenAIHttpError } from "@linxiraos/pi-ai/utils/openai-http";
|
|
27
27
|
import {
|
|
28
|
+
applyCodexResidencyHeader,
|
|
28
29
|
CODEX_BASE_URL,
|
|
29
30
|
getCodexAccountId,
|
|
30
31
|
OPENAI_HEADER_VALUES,
|
|
@@ -416,6 +417,7 @@ function buildCompactionV2Headers(
|
|
|
416
417
|
if (accountId) {
|
|
417
418
|
headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
|
|
418
419
|
}
|
|
420
|
+
applyCodexResidencyHeader(headers, apiKey);
|
|
419
421
|
if (routingSessionId) {
|
|
420
422
|
headers[OPENAI_HEADERS.CONVERSATION_ID] = routingSessionId;
|
|
421
423
|
headers[OPENAI_HEADERS.SESSION_ID] = routingSessionId;
|