@bastani/atomic 0.8.30-alpha.1 → 0.8.30-alpha.3
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 +4 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +1 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +19 -10
- package/dist/core/anthropic-thinking-guard.d.ts +15 -0
- package/dist/core/anthropic-thinking-guard.d.ts.map +1 -0
- package/dist/core/anthropic-thinking-guard.js +205 -0
- package/dist/core/anthropic-thinking-guard.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +8 -2
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +19 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/context-compaction.d.ts.map +1 -1
- package/dist/core/compaction/context-compaction.js +84 -14
- package/dist/core/compaction/context-compaction.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +15 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +22 -28
- package/dist/core/session-manager.js.map +1 -1
- package/dist/modes/interactive/components/context-compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/context-compaction-summary-message.js +2 -2
- package/dist/modes/interactive/components/context-compaction-summary-message.js.map +1 -1
- package/docs/compaction.md +8 -5
- package/docs/session-format.md +1 -1
- package/docs/workflows.md +2 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
### Fixed
|
|
15
15
|
|
|
16
|
+
- Fixed Anthropic/GitHub Copilot extended-thinking replay by repairing provider payloads so same-model `thinking` and `redacted_thinking` blocks are restored byte-for-byte after provider conversion and extension `before_provider_request` hooks, and by making compaction treat retained thinking-bearing assistant messages as all-or-nothing so sibling tool calls/text cannot be partially removed. This covers signed empty thinking blocks, sanitized thinking text, and compacted sessions that previously triggered 400 `thinking blocks ... cannot be modified` failures.
|
|
17
|
+
- Fixed post-compaction context usage accounting so the footer no longer trusts provider `totalTokens` when normalized usage components are available and avoids double-counting Anthropic-compatible cache buckets that mirror `input` tokens, preventing compacted sessions from displaying roughly doubled context-window percentages such as ~117% when the active prompt is closer to ~58%.
|
|
16
18
|
- Fixed package commands to drain stdout/stderr before the forced post-command exit so piped `atomic list`, help, progress, and self-update fallback output is not truncated under Node while still terminating leaked extension handles.
|
|
17
19
|
- Fixed custom `models.json` providers whose `apiKey` references an unset explicit environment variable so their models are omitted from `/model`, `--list-models`, available-model RPC responses, and automatic fallback candidates until the environment variable is configured.
|
|
18
20
|
- Fixed bash/child-process output draining so late stdout/stderr arriving after process exit continues draining while active, quiet inherited pipes release promptly, endlessly noisy detached descendants are bounded by a longer active-drain cap, and the built-in `bash` tool ignores output after its result accumulator has been finalized.
|
|
@@ -26,6 +28,8 @@
|
|
|
26
28
|
- Fixed bundled workflow and subagent structured-output gates to recover from missing or invalid `structured_output` final answers by issuing up to three corrective retries that echo the actual contract or schema-validation error before failing.
|
|
27
29
|
- Fixed bundled workflow failed-stage metadata so error-stage transcripts remain discoverable and follow-up messaging resumes from the failed conversation instead of resetting to an empty session.
|
|
28
30
|
- Fixed context compaction so older assistant `thinking` and `redacted_thinking` blocks can be removed like other stale blocks, while `thinking` or `redacted_thinking` blocks in the latest assistant message remain rejected by validation and paired tool-result restoration still preserves active context integrity ([#1386](https://github.com/bastani-inc/atomic/issues/1386)).
|
|
31
|
+
- Fixed bundled workflow graph rendering/runtime state for limited-concurrency `ctx.parallel` fan-outs so queued branches now keep sibling parentage after earlier branch failures.
|
|
32
|
+
- Fixed context compaction to universally protect every content block in the latest retained assistant message when that message contains `thinking` or `redacted_thinking`, so `context_delete` and `context_grep_delete` cannot remove visible sibling blocks or make an older partially-filtered thinking-bearing assistant become the latest retained assistant ([#1405](https://github.com/bastani-inc/atomic/issues/1405)).
|
|
29
33
|
|
|
30
34
|
## [0.8.29] - 2026-06-15
|
|
31
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/cursor",
|
|
3
|
-
"version": "0.8.30-alpha.
|
|
3
|
+
"version": "0.8.30-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
|
|
6
6
|
"contributors": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@bastani/atomic-natives": "0.8.30-alpha.
|
|
43
|
+
"@bastani/atomic-natives": "0.8.30-alpha.3",
|
|
44
44
|
"@bufbuild/protobuf": "^2.0.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/intercom",
|
|
3
|
-
"version": "0.8.30-alpha.
|
|
3
|
+
"version": "0.8.30-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
|
|
6
6
|
"contributors": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/mcp",
|
|
3
|
-
"version": "0.8.30-alpha.
|
|
3
|
+
"version": "0.8.30-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
|
|
6
6
|
"contributors": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/subagents",
|
|
3
|
-
"version": "0.8.30-alpha.
|
|
3
|
+
"version": "0.8.30-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
|
|
6
6
|
"contributors": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.8.30-alpha.
|
|
3
|
+
"version": "0.8.30-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
|
|
6
6
|
"contributors": [
|
|
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
16
16
|
- Fixed schema-backed workflow stages to send up to three corrective follow-up prompts when a turn finishes without the required `structured_output` call or with an invalid `structured_output` call, echoing the concrete contract/validation error before failing the stage.
|
|
17
17
|
- Fixed failed workflow stages to retain and persist SDK `sessionId`/`sessionFile` metadata, so post-error transcript inspection and follow-up messaging resume from the failed conversation instead of silently creating a fresh empty session.
|
|
18
18
|
- Fixed schema-backed workflow stages with `noTools: "all"` to keep the restrictive allowlist while still exposing the required `structured_output` final-answer tool.
|
|
19
|
+
- Fixed `ctx.parallel` graph inference so queued branches launched under a limited `concurrency` setting keep the same parent frontier as their sibling branches, even when an earlier sibling fails with `failFast: false`, instead of appearing as downstream children of failed siblings.
|
|
19
20
|
|
|
20
21
|
## [0.8.29] - 2026-06-15
|
|
21
22
|
|
|
@@ -68,7 +68,7 @@ export default defineWorkflow("summarize-pr")
|
|
|
68
68
|
|
|
69
69
|
### Example 2 — Parallel fan-out with `ctx.parallel`
|
|
70
70
|
|
|
71
|
-
Use `ctx.parallel` for independent specialist work. The aggregator receives the specialist outputs through typed task results instead of manual stage/session plumbing.
|
|
71
|
+
Use `ctx.parallel` for independent specialist work. The aggregator receives the specialist outputs through typed task results instead of manual stage/session plumbing. The runtime snapshots the parent graph frontier when the fan-out starts, so every branch shares the same parents even when limited `concurrency` queues later branches or an earlier sibling fails with `failFast: false`.
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
74
|
import { defineWorkflow, Type } from "@bastani/workflows";
|
|
@@ -2575,6 +2575,7 @@ interface ParallelFailFastScope {
|
|
|
2575
2575
|
failed: boolean;
|
|
2576
2576
|
firstFailure?: unknown;
|
|
2577
2577
|
readonly activeStages: Map<string, ParallelFailFastStage>;
|
|
2578
|
+
readonly parentIds?: readonly string[];
|
|
2578
2579
|
}
|
|
2579
2580
|
|
|
2580
2581
|
// ---------------------------------------------------------------------------
|
|
@@ -4041,13 +4042,18 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
4041
4042
|
|
|
4042
4043
|
// b. tracker.onSpawn → provisional parentIds
|
|
4043
4044
|
const provisionalParentIds = tracker.onSpawn(stageId, name);
|
|
4045
|
+
const scopedParentIds = opts.continuation === undefined ? stageFailFastScope?.parentIds : undefined;
|
|
4046
|
+
const initialParentIds = scopedParentIds === undefined ? provisionalParentIds : [...scopedParentIds];
|
|
4047
|
+
if (scopedParentIds !== undefined && !sameStringSet(scopedParentIds, provisionalParentIds)) {
|
|
4048
|
+
tracker.replaceParents(stageId, scopedParentIds);
|
|
4049
|
+
}
|
|
4044
4050
|
|
|
4045
4051
|
// c. Create StageSnapshot as "pending"
|
|
4046
4052
|
const replayKey = `stage:${name}`;
|
|
4047
4053
|
const replayDecision = replayIndex.decide({
|
|
4048
4054
|
displayName: name,
|
|
4049
4055
|
replayKey,
|
|
4050
|
-
parentIds:
|
|
4056
|
+
parentIds: initialParentIds,
|
|
4051
4057
|
stageId,
|
|
4052
4058
|
kind: "stage",
|
|
4053
4059
|
});
|
|
@@ -4599,7 +4605,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
4599
4605
|
throw err;
|
|
4600
4606
|
}
|
|
4601
4607
|
|
|
4602
|
-
if (opts.continuation === undefined && stageSnapshot.startedAt === undefined) {
|
|
4608
|
+
if (opts.continuation === undefined && stageSnapshot.startedAt === undefined && stageFailFastScope?.parentIds === undefined) {
|
|
4603
4609
|
const actualParentIds = tracker.currentParents();
|
|
4604
4610
|
if (!sameStringSet(actualParentIds, stageSnapshot.parentIds)) {
|
|
4605
4611
|
tracker.replaceParents(stageId, actualParentIds);
|
|
@@ -4956,22 +4962,25 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
4956
4962
|
async parallel(steps: readonly WorkflowTaskStep[], options: WorkflowParallelOptions = {}): Promise<WorkflowTaskResult[]> {
|
|
4957
4963
|
throwIfWorkflowExitSelected();
|
|
4958
4964
|
const fallback = parallelFallbackTask(steps, options);
|
|
4959
|
-
const
|
|
4960
|
-
|
|
4961
|
-
|
|
4965
|
+
const failFastEnabled = options.failFast !== false;
|
|
4966
|
+
const parallelScope: ParallelFailFastScope = {
|
|
4967
|
+
failed: false,
|
|
4968
|
+
activeStages: new Map<string, ParallelFailFastStage>(),
|
|
4969
|
+
parentIds: Object.freeze(tracker.currentParents()),
|
|
4970
|
+
};
|
|
4962
4971
|
return mapParallelSteps(steps, options.concurrency, options.failFast, async (step) => {
|
|
4963
4972
|
throwIfWorkflowExitSelected();
|
|
4964
4973
|
const prompt = replaceTaskPlaceholder(step.prompt ?? step.task ?? fallback, options.task ?? fallback);
|
|
4965
4974
|
return await (ctx.task as typeof ctx.task & ((taskName: string, taskOptions: WorkflowTaskOptions, scope?: ParallelFailFastScope) => Promise<WorkflowTaskResult>))(
|
|
4966
4975
|
step.name,
|
|
4967
4976
|
taskWithSharedDefaults(taskOptionsFromStep(step, prompt, taskPrevious(step)), options),
|
|
4968
|
-
|
|
4977
|
+
parallelScope,
|
|
4969
4978
|
);
|
|
4970
4979
|
}, (error) => {
|
|
4971
|
-
if (
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
for (const stage of
|
|
4980
|
+
if (!failFastEnabled) return;
|
|
4981
|
+
parallelScope.failed = true;
|
|
4982
|
+
parallelScope.firstFailure = error;
|
|
4983
|
+
for (const stage of parallelScope.activeStages.values()) {
|
|
4975
4984
|
stage.skip();
|
|
4976
4985
|
}
|
|
4977
4986
|
}, {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Api, Message, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
/**
|
|
3
|
+
* Restore same-model Anthropic replay thinking blocks after provider payload
|
|
4
|
+
* construction and extension payload hooks.
|
|
5
|
+
*
|
|
6
|
+
* Anthropic requires thinking/redacted_thinking blocks from replayed assistant
|
|
7
|
+
* messages to remain byte-for-byte identical to the original response. The
|
|
8
|
+
* upstream pi-ai Anthropic converter currently still sanitizes thinking text,
|
|
9
|
+
* drops signed empty thinking, and does not understand raw redacted_thinking
|
|
10
|
+
* blocks. This guard repairs the already-built Anthropic payload from the
|
|
11
|
+
* pre-provider LLM messages while leaving non-Anthropic and cross-model payloads
|
|
12
|
+
* unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export declare function restoreAnthropicReplayThinkingBlocks(payload: unknown, sourceMessages: readonly Message[], model: Model<Api>): unknown;
|
|
15
|
+
//# sourceMappingURL=anthropic-thinking-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic-thinking-guard.d.ts","sourceRoot":"","sources":["../../src/core/anthropic-thinking-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAoB,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAuLnF;;;;;;;;;;;GAWG;AACH,wBAAgB,oCAAoC,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAyCrI","sourcesContent":["import type { Api, AssistantMessage, Message, Model } from \"@earendil-works/pi-ai\";\n\ntype JsonObject = Record<string, unknown>;\n\ntype AnthropicContentBlock = JsonObject & { type: string };\n\ntype ReplayThinkingBlock =\n\t| { type: \"thinking\"; thinking: string; signature: string }\n\t| { type: \"redacted_thinking\"; data: string };\n\nfunction isObject(value: unknown): value is JsonObject {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isAnthropicContentBlock(value: unknown): value is AnthropicContentBlock {\n\treturn isObject(value) && typeof value.type === \"string\";\n}\n\nfunction isThinkingLikeAnthropicBlock(value: unknown): boolean {\n\tif (!isAnthropicContentBlock(value)) return false;\n\treturn value.type === \"thinking\" || value.type === \"redacted_thinking\";\n}\n\nfunction isSameModelAssistant(message: AssistantMessage, model: Model<Api>): boolean {\n\treturn message.provider === model.provider && message.api === model.api && message.model === model.id;\n}\n\nfunction readReplayThinkingBlock(block: unknown): ReplayThinkingBlock | undefined {\n\tif (!isObject(block) || typeof block.type !== \"string\") return undefined;\n\n\tif (block.type === \"redacted_thinking\") {\n\t\treturn typeof block.data === \"string\" ? { type: \"redacted_thinking\", data: block.data } : undefined;\n\t}\n\n\tif (block.type !== \"thinking\") return undefined;\n\n\tif (block.redacted === true) {\n\t\treturn typeof block.thinkingSignature === \"string\"\n\t\t\t? { type: \"redacted_thinking\", data: block.thinkingSignature }\n\t\t\t: undefined;\n\t}\n\n\treturn typeof block.thinking === \"string\" && typeof block.thinkingSignature === \"string\"\n\t\t? { type: \"thinking\", thinking: block.thinking, signature: block.thinkingSignature }\n\t\t: undefined;\n}\n\nfunction hasReplayThinkingBlock(message: AssistantMessage): boolean {\n\treturn message.content.some((block) => readReplayThinkingBlock(block) !== undefined);\n}\n\nfunction legacyProviderWouldEmitAssistantBlock(block: unknown, allowEmptySignature: boolean): boolean {\n\tif (!isObject(block) || typeof block.type !== \"string\") return false;\n\n\tif (block.type === \"text\") {\n\t\treturn typeof block.text === \"string\" && block.text.trim().length > 0;\n\t}\n\n\tif (block.type === \"toolCall\") {\n\t\treturn true;\n\t}\n\n\tif (block.type !== \"thinking\") {\n\t\treturn false;\n\t}\n\n\tif (block.redacted === true) {\n\t\treturn true;\n\t}\n\n\tif (typeof block.thinking !== \"string\") return false;\n\tif (block.thinking.trim().length === 0) return false;\n\n\tconst signature = typeof block.thinkingSignature === \"string\" ? block.thinkingSignature : undefined;\n\tif (signature !== undefined && signature.trim().length > 0) return true;\n\treturn allowEmptySignature;\n}\n\nfunction legacyProviderWouldEmitAssistant(message: AssistantMessage, model: Model<Api>, allowEmptySignature: boolean): boolean {\n\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") return false;\n\n\t// This mirrors the relevant same-model branch in @earendil-works/pi-ai's\n\t// transformMessages() + Anthropic convertMessages() pipeline closely enough\n\t// to keep assistant ordinal mapping aligned with the provider payload.\n\tif (isSameModelAssistant(message, model)) {\n\t\treturn message.content.some((block) => legacyProviderWouldEmitAssistantBlock(block, allowEmptySignature));\n\t}\n\n\treturn message.content.some((block) => {\n\t\tif (!isObject(block) || typeof block.type !== \"string\") return false;\n\t\tif (block.type === \"text\") return typeof block.text === \"string\" && block.text.trim().length > 0;\n\t\tif (block.type === \"toolCall\") return true;\n\t\tif (block.type !== \"thinking\") return false;\n\t\tif (block.redacted === true) return false;\n\t\treturn typeof block.thinking === \"string\" && block.thinking.trim().length > 0;\n\t});\n}\n\nfunction fallbackTextBlock(block: JsonObject): AnthropicContentBlock | undefined {\n\treturn typeof block.text === \"string\" && block.text.trim().length > 0\n\t\t? { type: \"text\", text: block.text }\n\t\t: undefined;\n}\n\nfunction fallbackToolUseBlock(block: JsonObject): AnthropicContentBlock | undefined {\n\tif (typeof block.id !== \"string\" || typeof block.name !== \"string\") return undefined;\n\treturn { type: \"tool_use\", id: block.id, name: block.name, input: block.arguments ?? {} };\n}\n\nfunction takeProviderBlock(\n\tproviderBlocks: readonly AnthropicContentBlock[],\n\tproviderIndex: number,\n\texpectedType: string,\n): { block: AnthropicContentBlock | undefined; nextProviderIndex: number } {\n\tconst providerBlock = providerBlocks[providerIndex];\n\tif (providerBlock?.type === expectedType) {\n\t\treturn { block: providerBlock, nextProviderIndex: providerIndex + 1 };\n\t}\n\treturn { block: undefined, nextProviderIndex: providerIndex };\n}\n\nfunction repairAssistantContent(\n\toriginalMessage: AssistantMessage,\n\tproviderBlocks: readonly AnthropicContentBlock[],\n): AnthropicContentBlock[] {\n\tconst repaired: AnthropicContentBlock[] = [];\n\tlet providerIndex = 0;\n\n\tfor (const originalBlock of originalMessage.content) {\n\t\tconst replayBlock = readReplayThinkingBlock(originalBlock);\n\t\tif (replayBlock) {\n\t\t\tif (isThinkingLikeAnthropicBlock(providerBlocks[providerIndex])) {\n\t\t\t\tproviderIndex += 1;\n\t\t\t}\n\t\t\trepaired.push({ ...replayBlock });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!isObject(originalBlock) || typeof originalBlock.type !== \"string\") continue;\n\n\t\tif (originalBlock.type === \"text\") {\n\t\t\tconst providerText = takeProviderBlock(providerBlocks, providerIndex, \"text\");\n\t\t\tif (providerText.block) {\n\t\t\t\trepaired.push(providerText.block);\n\t\t\t\tproviderIndex = providerText.nextProviderIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst fallback = fallbackTextBlock(originalBlock);\n\t\t\tif (fallback) repaired.push(fallback);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (originalBlock.type === \"toolCall\") {\n\t\t\tconst providerToolUse = takeProviderBlock(providerBlocks, providerIndex, \"tool_use\");\n\t\t\tif (providerToolUse.block) {\n\t\t\t\trepaired.push(providerToolUse.block);\n\t\t\t\tproviderIndex = providerToolUse.nextProviderIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst fallback = fallbackToolUseBlock(originalBlock);\n\t\t\tif (fallback) repaired.push(fallback);\n\t\t}\n\t}\n\n\tfor (; providerIndex < providerBlocks.length; providerIndex += 1) {\n\t\trepaired.push(providerBlocks[providerIndex]);\n\t}\n\n\treturn repaired;\n}\n\nfunction getAnthropicMessages(payload: unknown): unknown[] | undefined {\n\tif (!isObject(payload)) return undefined;\n\treturn Array.isArray(payload.messages) ? payload.messages : undefined;\n}\n\nfunction getAnthropicAssistantContent(message: unknown): AnthropicContentBlock[] | undefined {\n\tif (!isObject(message) || message.role !== \"assistant\") return undefined;\n\tif (!Array.isArray(message.content)) return undefined;\n\tconst content = message.content.filter(isAnthropicContentBlock);\n\treturn content.length === message.content.length ? content : undefined;\n}\n\n/**\n * Restore same-model Anthropic replay thinking blocks after provider payload\n * construction and extension payload hooks.\n *\n * Anthropic requires thinking/redacted_thinking blocks from replayed assistant\n * messages to remain byte-for-byte identical to the original response. The\n * upstream pi-ai Anthropic converter currently still sanitizes thinking text,\n * drops signed empty thinking, and does not understand raw redacted_thinking\n * blocks. This guard repairs the already-built Anthropic payload from the\n * pre-provider LLM messages while leaving non-Anthropic and cross-model payloads\n * unchanged.\n */\nexport function restoreAnthropicReplayThinkingBlocks(payload: unknown, sourceMessages: readonly Message[], model: Model<Api>): unknown {\n\tif (model.api !== \"anthropic-messages\") return payload;\n\n\tconst payloadMessages = getAnthropicMessages(payload);\n\tif (!payloadMessages) return payload;\n\n\tconst assistantPayloads = payloadMessages\n\t\t.map((message, index) => ({ message, index, content: getAnthropicAssistantContent(message) }))\n\t\t.filter(\n\t\t\t(entry): entry is { message: JsonObject; index: number; content: AnthropicContentBlock[] } =>\n\t\t\t\tentry.content !== undefined && isObject(entry.message),\n\t\t);\n\n\tif (assistantPayloads.length === 0) return payload;\n\n\tconst allowEmptySignature = isObject(model.compat) && model.compat.allowEmptySignature === true;\n\tlet assistantPayloadOrdinal = 0;\n\tlet nextPayloadMessages: unknown[] | undefined;\n\n\tfor (const sourceMessage of sourceMessages) {\n\t\tif (sourceMessage.role !== \"assistant\") continue;\n\t\tif (!legacyProviderWouldEmitAssistant(sourceMessage, model, allowEmptySignature)) continue;\n\n\t\tconst payloadAssistant = assistantPayloads[assistantPayloadOrdinal];\n\t\tassistantPayloadOrdinal += 1;\n\t\tif (!payloadAssistant) break;\n\n\t\tif (!isSameModelAssistant(sourceMessage, model) || !hasReplayThinkingBlock(sourceMessage)) continue;\n\n\t\tconst repairedContent = repairAssistantContent(sourceMessage, payloadAssistant.content);\n\t\tif (JSON.stringify(repairedContent) === JSON.stringify(payloadAssistant.content)) continue;\n\n\t\tnextPayloadMessages ??= [...payloadMessages];\n\t\tnextPayloadMessages[payloadAssistant.index] = {\n\t\t\t...payloadAssistant.message,\n\t\t\tcontent: repairedContent,\n\t\t};\n\t}\n\n\tif (!nextPayloadMessages || !isObject(payload)) return payload;\n\treturn { ...payload, messages: nextPayloadMessages };\n}\n"]}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
function isObject(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function isAnthropicContentBlock(value) {
|
|
5
|
+
return isObject(value) && typeof value.type === "string";
|
|
6
|
+
}
|
|
7
|
+
function isThinkingLikeAnthropicBlock(value) {
|
|
8
|
+
if (!isAnthropicContentBlock(value))
|
|
9
|
+
return false;
|
|
10
|
+
return value.type === "thinking" || value.type === "redacted_thinking";
|
|
11
|
+
}
|
|
12
|
+
function isSameModelAssistant(message, model) {
|
|
13
|
+
return message.provider === model.provider && message.api === model.api && message.model === model.id;
|
|
14
|
+
}
|
|
15
|
+
function readReplayThinkingBlock(block) {
|
|
16
|
+
if (!isObject(block) || typeof block.type !== "string")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (block.type === "redacted_thinking") {
|
|
19
|
+
return typeof block.data === "string" ? { type: "redacted_thinking", data: block.data } : undefined;
|
|
20
|
+
}
|
|
21
|
+
if (block.type !== "thinking")
|
|
22
|
+
return undefined;
|
|
23
|
+
if (block.redacted === true) {
|
|
24
|
+
return typeof block.thinkingSignature === "string"
|
|
25
|
+
? { type: "redacted_thinking", data: block.thinkingSignature }
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
return typeof block.thinking === "string" && typeof block.thinkingSignature === "string"
|
|
29
|
+
? { type: "thinking", thinking: block.thinking, signature: block.thinkingSignature }
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
function hasReplayThinkingBlock(message) {
|
|
33
|
+
return message.content.some((block) => readReplayThinkingBlock(block) !== undefined);
|
|
34
|
+
}
|
|
35
|
+
function legacyProviderWouldEmitAssistantBlock(block, allowEmptySignature) {
|
|
36
|
+
if (!isObject(block) || typeof block.type !== "string")
|
|
37
|
+
return false;
|
|
38
|
+
if (block.type === "text") {
|
|
39
|
+
return typeof block.text === "string" && block.text.trim().length > 0;
|
|
40
|
+
}
|
|
41
|
+
if (block.type === "toolCall") {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (block.type !== "thinking") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (block.redacted === true) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (typeof block.thinking !== "string")
|
|
51
|
+
return false;
|
|
52
|
+
if (block.thinking.trim().length === 0)
|
|
53
|
+
return false;
|
|
54
|
+
const signature = typeof block.thinkingSignature === "string" ? block.thinkingSignature : undefined;
|
|
55
|
+
if (signature !== undefined && signature.trim().length > 0)
|
|
56
|
+
return true;
|
|
57
|
+
return allowEmptySignature;
|
|
58
|
+
}
|
|
59
|
+
function legacyProviderWouldEmitAssistant(message, model, allowEmptySignature) {
|
|
60
|
+
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
61
|
+
return false;
|
|
62
|
+
// This mirrors the relevant same-model branch in @earendil-works/pi-ai's
|
|
63
|
+
// transformMessages() + Anthropic convertMessages() pipeline closely enough
|
|
64
|
+
// to keep assistant ordinal mapping aligned with the provider payload.
|
|
65
|
+
if (isSameModelAssistant(message, model)) {
|
|
66
|
+
return message.content.some((block) => legacyProviderWouldEmitAssistantBlock(block, allowEmptySignature));
|
|
67
|
+
}
|
|
68
|
+
return message.content.some((block) => {
|
|
69
|
+
if (!isObject(block) || typeof block.type !== "string")
|
|
70
|
+
return false;
|
|
71
|
+
if (block.type === "text")
|
|
72
|
+
return typeof block.text === "string" && block.text.trim().length > 0;
|
|
73
|
+
if (block.type === "toolCall")
|
|
74
|
+
return true;
|
|
75
|
+
if (block.type !== "thinking")
|
|
76
|
+
return false;
|
|
77
|
+
if (block.redacted === true)
|
|
78
|
+
return false;
|
|
79
|
+
return typeof block.thinking === "string" && block.thinking.trim().length > 0;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function fallbackTextBlock(block) {
|
|
83
|
+
return typeof block.text === "string" && block.text.trim().length > 0
|
|
84
|
+
? { type: "text", text: block.text }
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
87
|
+
function fallbackToolUseBlock(block) {
|
|
88
|
+
if (typeof block.id !== "string" || typeof block.name !== "string")
|
|
89
|
+
return undefined;
|
|
90
|
+
return { type: "tool_use", id: block.id, name: block.name, input: block.arguments ?? {} };
|
|
91
|
+
}
|
|
92
|
+
function takeProviderBlock(providerBlocks, providerIndex, expectedType) {
|
|
93
|
+
const providerBlock = providerBlocks[providerIndex];
|
|
94
|
+
if (providerBlock?.type === expectedType) {
|
|
95
|
+
return { block: providerBlock, nextProviderIndex: providerIndex + 1 };
|
|
96
|
+
}
|
|
97
|
+
return { block: undefined, nextProviderIndex: providerIndex };
|
|
98
|
+
}
|
|
99
|
+
function repairAssistantContent(originalMessage, providerBlocks) {
|
|
100
|
+
const repaired = [];
|
|
101
|
+
let providerIndex = 0;
|
|
102
|
+
for (const originalBlock of originalMessage.content) {
|
|
103
|
+
const replayBlock = readReplayThinkingBlock(originalBlock);
|
|
104
|
+
if (replayBlock) {
|
|
105
|
+
if (isThinkingLikeAnthropicBlock(providerBlocks[providerIndex])) {
|
|
106
|
+
providerIndex += 1;
|
|
107
|
+
}
|
|
108
|
+
repaired.push({ ...replayBlock });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!isObject(originalBlock) || typeof originalBlock.type !== "string")
|
|
112
|
+
continue;
|
|
113
|
+
if (originalBlock.type === "text") {
|
|
114
|
+
const providerText = takeProviderBlock(providerBlocks, providerIndex, "text");
|
|
115
|
+
if (providerText.block) {
|
|
116
|
+
repaired.push(providerText.block);
|
|
117
|
+
providerIndex = providerText.nextProviderIndex;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const fallback = fallbackTextBlock(originalBlock);
|
|
121
|
+
if (fallback)
|
|
122
|
+
repaired.push(fallback);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (originalBlock.type === "toolCall") {
|
|
126
|
+
const providerToolUse = takeProviderBlock(providerBlocks, providerIndex, "tool_use");
|
|
127
|
+
if (providerToolUse.block) {
|
|
128
|
+
repaired.push(providerToolUse.block);
|
|
129
|
+
providerIndex = providerToolUse.nextProviderIndex;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const fallback = fallbackToolUseBlock(originalBlock);
|
|
133
|
+
if (fallback)
|
|
134
|
+
repaired.push(fallback);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (; providerIndex < providerBlocks.length; providerIndex += 1) {
|
|
138
|
+
repaired.push(providerBlocks[providerIndex]);
|
|
139
|
+
}
|
|
140
|
+
return repaired;
|
|
141
|
+
}
|
|
142
|
+
function getAnthropicMessages(payload) {
|
|
143
|
+
if (!isObject(payload))
|
|
144
|
+
return undefined;
|
|
145
|
+
return Array.isArray(payload.messages) ? payload.messages : undefined;
|
|
146
|
+
}
|
|
147
|
+
function getAnthropicAssistantContent(message) {
|
|
148
|
+
if (!isObject(message) || message.role !== "assistant")
|
|
149
|
+
return undefined;
|
|
150
|
+
if (!Array.isArray(message.content))
|
|
151
|
+
return undefined;
|
|
152
|
+
const content = message.content.filter(isAnthropicContentBlock);
|
|
153
|
+
return content.length === message.content.length ? content : undefined;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Restore same-model Anthropic replay thinking blocks after provider payload
|
|
157
|
+
* construction and extension payload hooks.
|
|
158
|
+
*
|
|
159
|
+
* Anthropic requires thinking/redacted_thinking blocks from replayed assistant
|
|
160
|
+
* messages to remain byte-for-byte identical to the original response. The
|
|
161
|
+
* upstream pi-ai Anthropic converter currently still sanitizes thinking text,
|
|
162
|
+
* drops signed empty thinking, and does not understand raw redacted_thinking
|
|
163
|
+
* blocks. This guard repairs the already-built Anthropic payload from the
|
|
164
|
+
* pre-provider LLM messages while leaving non-Anthropic and cross-model payloads
|
|
165
|
+
* unchanged.
|
|
166
|
+
*/
|
|
167
|
+
export function restoreAnthropicReplayThinkingBlocks(payload, sourceMessages, model) {
|
|
168
|
+
if (model.api !== "anthropic-messages")
|
|
169
|
+
return payload;
|
|
170
|
+
const payloadMessages = getAnthropicMessages(payload);
|
|
171
|
+
if (!payloadMessages)
|
|
172
|
+
return payload;
|
|
173
|
+
const assistantPayloads = payloadMessages
|
|
174
|
+
.map((message, index) => ({ message, index, content: getAnthropicAssistantContent(message) }))
|
|
175
|
+
.filter((entry) => entry.content !== undefined && isObject(entry.message));
|
|
176
|
+
if (assistantPayloads.length === 0)
|
|
177
|
+
return payload;
|
|
178
|
+
const allowEmptySignature = isObject(model.compat) && model.compat.allowEmptySignature === true;
|
|
179
|
+
let assistantPayloadOrdinal = 0;
|
|
180
|
+
let nextPayloadMessages;
|
|
181
|
+
for (const sourceMessage of sourceMessages) {
|
|
182
|
+
if (sourceMessage.role !== "assistant")
|
|
183
|
+
continue;
|
|
184
|
+
if (!legacyProviderWouldEmitAssistant(sourceMessage, model, allowEmptySignature))
|
|
185
|
+
continue;
|
|
186
|
+
const payloadAssistant = assistantPayloads[assistantPayloadOrdinal];
|
|
187
|
+
assistantPayloadOrdinal += 1;
|
|
188
|
+
if (!payloadAssistant)
|
|
189
|
+
break;
|
|
190
|
+
if (!isSameModelAssistant(sourceMessage, model) || !hasReplayThinkingBlock(sourceMessage))
|
|
191
|
+
continue;
|
|
192
|
+
const repairedContent = repairAssistantContent(sourceMessage, payloadAssistant.content);
|
|
193
|
+
if (JSON.stringify(repairedContent) === JSON.stringify(payloadAssistant.content))
|
|
194
|
+
continue;
|
|
195
|
+
nextPayloadMessages ??= [...payloadMessages];
|
|
196
|
+
nextPayloadMessages[payloadAssistant.index] = {
|
|
197
|
+
...payloadAssistant.message,
|
|
198
|
+
content: repairedContent,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (!nextPayloadMessages || !isObject(payload))
|
|
202
|
+
return payload;
|
|
203
|
+
return { ...payload, messages: nextPayloadMessages };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=anthropic-thinking-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic-thinking-guard.js","sourceRoot":"","sources":["../../src/core/anthropic-thinking-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Api, AssistantMessage, Message, Model } from \"@earendil-works/pi-ai\";\n\ntype JsonObject = Record<string, unknown>;\n\ntype AnthropicContentBlock = JsonObject & { type: string };\n\ntype ReplayThinkingBlock =\n\t| { type: \"thinking\"; thinking: string; signature: string }\n\t| { type: \"redacted_thinking\"; data: string };\n\nfunction isObject(value: unknown): value is JsonObject {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isAnthropicContentBlock(value: unknown): value is AnthropicContentBlock {\n\treturn isObject(value) && typeof value.type === \"string\";\n}\n\nfunction isThinkingLikeAnthropicBlock(value: unknown): boolean {\n\tif (!isAnthropicContentBlock(value)) return false;\n\treturn value.type === \"thinking\" || value.type === \"redacted_thinking\";\n}\n\nfunction isSameModelAssistant(message: AssistantMessage, model: Model<Api>): boolean {\n\treturn message.provider === model.provider && message.api === model.api && message.model === model.id;\n}\n\nfunction readReplayThinkingBlock(block: unknown): ReplayThinkingBlock | undefined {\n\tif (!isObject(block) || typeof block.type !== \"string\") return undefined;\n\n\tif (block.type === \"redacted_thinking\") {\n\t\treturn typeof block.data === \"string\" ? { type: \"redacted_thinking\", data: block.data } : undefined;\n\t}\n\n\tif (block.type !== \"thinking\") return undefined;\n\n\tif (block.redacted === true) {\n\t\treturn typeof block.thinkingSignature === \"string\"\n\t\t\t? { type: \"redacted_thinking\", data: block.thinkingSignature }\n\t\t\t: undefined;\n\t}\n\n\treturn typeof block.thinking === \"string\" && typeof block.thinkingSignature === \"string\"\n\t\t? { type: \"thinking\", thinking: block.thinking, signature: block.thinkingSignature }\n\t\t: undefined;\n}\n\nfunction hasReplayThinkingBlock(message: AssistantMessage): boolean {\n\treturn message.content.some((block) => readReplayThinkingBlock(block) !== undefined);\n}\n\nfunction legacyProviderWouldEmitAssistantBlock(block: unknown, allowEmptySignature: boolean): boolean {\n\tif (!isObject(block) || typeof block.type !== \"string\") return false;\n\n\tif (block.type === \"text\") {\n\t\treturn typeof block.text === \"string\" && block.text.trim().length > 0;\n\t}\n\n\tif (block.type === \"toolCall\") {\n\t\treturn true;\n\t}\n\n\tif (block.type !== \"thinking\") {\n\t\treturn false;\n\t}\n\n\tif (block.redacted === true) {\n\t\treturn true;\n\t}\n\n\tif (typeof block.thinking !== \"string\") return false;\n\tif (block.thinking.trim().length === 0) return false;\n\n\tconst signature = typeof block.thinkingSignature === \"string\" ? block.thinkingSignature : undefined;\n\tif (signature !== undefined && signature.trim().length > 0) return true;\n\treturn allowEmptySignature;\n}\n\nfunction legacyProviderWouldEmitAssistant(message: AssistantMessage, model: Model<Api>, allowEmptySignature: boolean): boolean {\n\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") return false;\n\n\t// This mirrors the relevant same-model branch in @earendil-works/pi-ai's\n\t// transformMessages() + Anthropic convertMessages() pipeline closely enough\n\t// to keep assistant ordinal mapping aligned with the provider payload.\n\tif (isSameModelAssistant(message, model)) {\n\t\treturn message.content.some((block) => legacyProviderWouldEmitAssistantBlock(block, allowEmptySignature));\n\t}\n\n\treturn message.content.some((block) => {\n\t\tif (!isObject(block) || typeof block.type !== \"string\") return false;\n\t\tif (block.type === \"text\") return typeof block.text === \"string\" && block.text.trim().length > 0;\n\t\tif (block.type === \"toolCall\") return true;\n\t\tif (block.type !== \"thinking\") return false;\n\t\tif (block.redacted === true) return false;\n\t\treturn typeof block.thinking === \"string\" && block.thinking.trim().length > 0;\n\t});\n}\n\nfunction fallbackTextBlock(block: JsonObject): AnthropicContentBlock | undefined {\n\treturn typeof block.text === \"string\" && block.text.trim().length > 0\n\t\t? { type: \"text\", text: block.text }\n\t\t: undefined;\n}\n\nfunction fallbackToolUseBlock(block: JsonObject): AnthropicContentBlock | undefined {\n\tif (typeof block.id !== \"string\" || typeof block.name !== \"string\") return undefined;\n\treturn { type: \"tool_use\", id: block.id, name: block.name, input: block.arguments ?? {} };\n}\n\nfunction takeProviderBlock(\n\tproviderBlocks: readonly AnthropicContentBlock[],\n\tproviderIndex: number,\n\texpectedType: string,\n): { block: AnthropicContentBlock | undefined; nextProviderIndex: number } {\n\tconst providerBlock = providerBlocks[providerIndex];\n\tif (providerBlock?.type === expectedType) {\n\t\treturn { block: providerBlock, nextProviderIndex: providerIndex + 1 };\n\t}\n\treturn { block: undefined, nextProviderIndex: providerIndex };\n}\n\nfunction repairAssistantContent(\n\toriginalMessage: AssistantMessage,\n\tproviderBlocks: readonly AnthropicContentBlock[],\n): AnthropicContentBlock[] {\n\tconst repaired: AnthropicContentBlock[] = [];\n\tlet providerIndex = 0;\n\n\tfor (const originalBlock of originalMessage.content) {\n\t\tconst replayBlock = readReplayThinkingBlock(originalBlock);\n\t\tif (replayBlock) {\n\t\t\tif (isThinkingLikeAnthropicBlock(providerBlocks[providerIndex])) {\n\t\t\t\tproviderIndex += 1;\n\t\t\t}\n\t\t\trepaired.push({ ...replayBlock });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!isObject(originalBlock) || typeof originalBlock.type !== \"string\") continue;\n\n\t\tif (originalBlock.type === \"text\") {\n\t\t\tconst providerText = takeProviderBlock(providerBlocks, providerIndex, \"text\");\n\t\t\tif (providerText.block) {\n\t\t\t\trepaired.push(providerText.block);\n\t\t\t\tproviderIndex = providerText.nextProviderIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst fallback = fallbackTextBlock(originalBlock);\n\t\t\tif (fallback) repaired.push(fallback);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (originalBlock.type === \"toolCall\") {\n\t\t\tconst providerToolUse = takeProviderBlock(providerBlocks, providerIndex, \"tool_use\");\n\t\t\tif (providerToolUse.block) {\n\t\t\t\trepaired.push(providerToolUse.block);\n\t\t\t\tproviderIndex = providerToolUse.nextProviderIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst fallback = fallbackToolUseBlock(originalBlock);\n\t\t\tif (fallback) repaired.push(fallback);\n\t\t}\n\t}\n\n\tfor (; providerIndex < providerBlocks.length; providerIndex += 1) {\n\t\trepaired.push(providerBlocks[providerIndex]);\n\t}\n\n\treturn repaired;\n}\n\nfunction getAnthropicMessages(payload: unknown): unknown[] | undefined {\n\tif (!isObject(payload)) return undefined;\n\treturn Array.isArray(payload.messages) ? payload.messages : undefined;\n}\n\nfunction getAnthropicAssistantContent(message: unknown): AnthropicContentBlock[] | undefined {\n\tif (!isObject(message) || message.role !== \"assistant\") return undefined;\n\tif (!Array.isArray(message.content)) return undefined;\n\tconst content = message.content.filter(isAnthropicContentBlock);\n\treturn content.length === message.content.length ? content : undefined;\n}\n\n/**\n * Restore same-model Anthropic replay thinking blocks after provider payload\n * construction and extension payload hooks.\n *\n * Anthropic requires thinking/redacted_thinking blocks from replayed assistant\n * messages to remain byte-for-byte identical to the original response. The\n * upstream pi-ai Anthropic converter currently still sanitizes thinking text,\n * drops signed empty thinking, and does not understand raw redacted_thinking\n * blocks. This guard repairs the already-built Anthropic payload from the\n * pre-provider LLM messages while leaving non-Anthropic and cross-model payloads\n * unchanged.\n */\nexport function restoreAnthropicReplayThinkingBlocks(payload: unknown, sourceMessages: readonly Message[], model: Model<Api>): unknown {\n\tif (model.api !== \"anthropic-messages\") return payload;\n\n\tconst payloadMessages = getAnthropicMessages(payload);\n\tif (!payloadMessages) return payload;\n\n\tconst assistantPayloads = payloadMessages\n\t\t.map((message, index) => ({ message, index, content: getAnthropicAssistantContent(message) }))\n\t\t.filter(\n\t\t\t(entry): entry is { message: JsonObject; index: number; content: AnthropicContentBlock[] } =>\n\t\t\t\tentry.content !== undefined && isObject(entry.message),\n\t\t);\n\n\tif (assistantPayloads.length === 0) return payload;\n\n\tconst allowEmptySignature = isObject(model.compat) && model.compat.allowEmptySignature === true;\n\tlet assistantPayloadOrdinal = 0;\n\tlet nextPayloadMessages: unknown[] | undefined;\n\n\tfor (const sourceMessage of sourceMessages) {\n\t\tif (sourceMessage.role !== \"assistant\") continue;\n\t\tif (!legacyProviderWouldEmitAssistant(sourceMessage, model, allowEmptySignature)) continue;\n\n\t\tconst payloadAssistant = assistantPayloads[assistantPayloadOrdinal];\n\t\tassistantPayloadOrdinal += 1;\n\t\tif (!payloadAssistant) break;\n\n\t\tif (!isSameModelAssistant(sourceMessage, model) || !hasReplayThinkingBlock(sourceMessage)) continue;\n\n\t\tconst repairedContent = repairAssistantContent(sourceMessage, payloadAssistant.content);\n\t\tif (JSON.stringify(repairedContent) === JSON.stringify(payloadAssistant.content)) continue;\n\n\t\tnextPayloadMessages ??= [...payloadMessages];\n\t\tnextPayloadMessages[payloadAssistant.index] = {\n\t\t\t...payloadAssistant.message,\n\t\t\tcontent: repairedContent,\n\t\t};\n\t}\n\n\tif (!nextPayloadMessages || !isObject(payload)) return payload;\n\treturn { ...payload, messages: nextPayloadMessages };\n}\n"]}
|
|
@@ -16,8 +16,14 @@ export interface CompactionSettings {
|
|
|
16
16
|
}
|
|
17
17
|
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
18
18
|
/**
|
|
19
|
-
* Calculate
|
|
20
|
-
*
|
|
19
|
+
* Calculate active context-window tokens from provider usage.
|
|
20
|
+
*
|
|
21
|
+
* Prefer normalized component fields over `totalTokens`: some providers expose
|
|
22
|
+
* `totalTokens` as a billing/cumulative total, while the footer needs an active
|
|
23
|
+
* context estimate. Anthropic-compatible endpoints can also mirror cached input
|
|
24
|
+
* in both `input` and `cacheRead`/`cacheWrite`; when cache buckets are nearly the
|
|
25
|
+
* same size as `input`, treat `input` as the full prompt instead of counting the
|
|
26
|
+
* same cached prompt twice.
|
|
21
27
|
*/
|
|
22
28
|
export declare function calculateContextTokens(usage: Usage): number;
|
|
23
29
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/compaction.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAoB,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,2BAA2B,EAAE,kBAKzC,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/compaction.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAoB,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,2BAA2B,EAAE,kBAKzC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAY3D;AAgBD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,KAAK,GAAG,SAAS,CAShF;AAED,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAUD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,oBAAoB,CA4BpF;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAGjH;AAoBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAuC5D","sourcesContent":["/**\n * Neutral context-usage metrics for deciding when a session needs compaction.\n */\n\nimport type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { AssistantMessage, Usage } from \"@earendil-works/pi-ai\";\nimport type { SessionEntry } from \"../session-manager.ts\";\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\t/** Fraction of compactable context to keep. 0.3 is aggressive, 0.7 is light. */\n\tcompression_ratio: number;\n\t/** Number of recent context-eligible messages to preserve in standard mode. */\n\tpreserve_recent: number;\n\t/** Focus query for relevance-based pruning; auto-detected when omitted in settings/options. */\n\tquery?: string;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tcompression_ratio: 0.5,\n\tpreserve_recent: 2,\n};\n\n/**\n * Calculate active context-window tokens from provider usage.\n *\n * Prefer normalized component fields over `totalTokens`: some providers expose\n * `totalTokens` as a billing/cumulative total, while the footer needs an active\n * context estimate. Anthropic-compatible endpoints can also mirror cached input\n * in both `input` and `cacheRead`/`cacheWrite`; when cache buckets are nearly the\n * same size as `input`, treat `input` as the full prompt instead of counting the\n * same cached prompt twice.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\tconst input = Math.max(0, usage.input || 0);\n\tconst output = Math.max(0, usage.output || 0);\n\tconst cacheRead = Math.max(0, usage.cacheRead || 0);\n\tconst cacheWrite = Math.max(0, usage.cacheWrite || 0);\n\tconst cacheTokens = cacheRead + cacheWrite;\n\tconst hasComponents = input > 0 || output > 0 || cacheTokens > 0;\n\tif (!hasComponents) return Math.max(0, usage.totalTokens || 0);\n\n\tconst cacheMirrorsInput = input > 0 && cacheTokens > 0 && cacheTokens >= input * 0.9 && cacheTokens <= input * 1.1;\n\tconst promptTokens = cacheMirrorsInput ? input : input + cacheTokens;\n\treturn promptTokens + output;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface ContextUsageEstimate {\n\ttokens: number;\n\tusageTokens: number;\n\ttrailingTokens: number;\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/**\n * Estimate context tokens from messages, using the last assistant usage when available.\n * If there are messages after the last usage, estimate their tokens with estimateTokens.\n */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AgentMessage): number {\n\tlet chars = 0;\n\n\tswitch (message.role) {\n\t\tcase \"user\": {\n\t\t\tchars = estimateTextAndImageContentChars(\n\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\": {\n\t\t\tchars = estimateTextAndImageContentChars(message.content);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"bashExecution\": {\n\t\t\tchars = message.command.length + message.output.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"branchSummary\": {\n\t\t\tchars = message.summary.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t}\n\n\treturn 0;\n}\n"]}
|
|
@@ -8,11 +8,27 @@ export const DEFAULT_COMPACTION_SETTINGS = {
|
|
|
8
8
|
preserve_recent: 2,
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* Calculate
|
|
12
|
-
*
|
|
11
|
+
* Calculate active context-window tokens from provider usage.
|
|
12
|
+
*
|
|
13
|
+
* Prefer normalized component fields over `totalTokens`: some providers expose
|
|
14
|
+
* `totalTokens` as a billing/cumulative total, while the footer needs an active
|
|
15
|
+
* context estimate. Anthropic-compatible endpoints can also mirror cached input
|
|
16
|
+
* in both `input` and `cacheRead`/`cacheWrite`; when cache buckets are nearly the
|
|
17
|
+
* same size as `input`, treat `input` as the full prompt instead of counting the
|
|
18
|
+
* same cached prompt twice.
|
|
13
19
|
*/
|
|
14
20
|
export function calculateContextTokens(usage) {
|
|
15
|
-
|
|
21
|
+
const input = Math.max(0, usage.input || 0);
|
|
22
|
+
const output = Math.max(0, usage.output || 0);
|
|
23
|
+
const cacheRead = Math.max(0, usage.cacheRead || 0);
|
|
24
|
+
const cacheWrite = Math.max(0, usage.cacheWrite || 0);
|
|
25
|
+
const cacheTokens = cacheRead + cacheWrite;
|
|
26
|
+
const hasComponents = input > 0 || output > 0 || cacheTokens > 0;
|
|
27
|
+
if (!hasComponents)
|
|
28
|
+
return Math.max(0, usage.totalTokens || 0);
|
|
29
|
+
const cacheMirrorsInput = input > 0 && cacheTokens > 0 && cacheTokens >= input * 0.9 && cacheTokens <= input * 1.1;
|
|
30
|
+
const promptTokens = cacheMirrorsInput ? input : input + cacheTokens;
|
|
31
|
+
return promptTokens + output;
|
|
16
32
|
}
|
|
17
33
|
/**
|
|
18
34
|
* Get usage from an assistant message if available.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../../src/core/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Neutral context-usage metrics for deciding when a session needs compaction.\n */\n\nimport type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { AssistantMessage, Usage } from \"@earendil-works/pi-ai\";\nimport type { SessionEntry } from \"../session-manager.ts\";\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\t/** Fraction of compactable context to keep. 0.3 is aggressive, 0.7 is light. */\n\tcompression_ratio: number;\n\t/** Number of recent context-eligible messages to preserve in standard mode. */\n\tpreserve_recent: number;\n\t/** Focus query for relevance-based pruning; auto-detected when omitted in settings/options. */\n\tquery?: string;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tcompression_ratio: 0.5,\n\tpreserve_recent: 2,\n};\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface ContextUsageEstimate {\n\ttokens: number;\n\tusageTokens: number;\n\ttrailingTokens: number;\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/**\n * Estimate context tokens from messages, using the last assistant usage when available.\n * If there are messages after the last usage, estimate their tokens with estimateTokens.\n */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AgentMessage): number {\n\tlet chars = 0;\n\n\tswitch (message.role) {\n\t\tcase \"user\": {\n\t\t\tchars = estimateTextAndImageContentChars(\n\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\": {\n\t\t\tchars = estimateTextAndImageContentChars(message.content);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"bashExecution\": {\n\t\t\tchars = message.command.length + message.output.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"branchSummary\": {\n\t\t\tchars = message.summary.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t}\n\n\treturn 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../../src/core/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Neutral context-usage metrics for deciding when a session needs compaction.\n */\n\nimport type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { AssistantMessage, Usage } from \"@earendil-works/pi-ai\";\nimport type { SessionEntry } from \"../session-manager.ts\";\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\t/** Fraction of compactable context to keep. 0.3 is aggressive, 0.7 is light. */\n\tcompression_ratio: number;\n\t/** Number of recent context-eligible messages to preserve in standard mode. */\n\tpreserve_recent: number;\n\t/** Focus query for relevance-based pruning; auto-detected when omitted in settings/options. */\n\tquery?: string;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tcompression_ratio: 0.5,\n\tpreserve_recent: 2,\n};\n\n/**\n * Calculate active context-window tokens from provider usage.\n *\n * Prefer normalized component fields over `totalTokens`: some providers expose\n * `totalTokens` as a billing/cumulative total, while the footer needs an active\n * context estimate. Anthropic-compatible endpoints can also mirror cached input\n * in both `input` and `cacheRead`/`cacheWrite`; when cache buckets are nearly the\n * same size as `input`, treat `input` as the full prompt instead of counting the\n * same cached prompt twice.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\tconst input = Math.max(0, usage.input || 0);\n\tconst output = Math.max(0, usage.output || 0);\n\tconst cacheRead = Math.max(0, usage.cacheRead || 0);\n\tconst cacheWrite = Math.max(0, usage.cacheWrite || 0);\n\tconst cacheTokens = cacheRead + cacheWrite;\n\tconst hasComponents = input > 0 || output > 0 || cacheTokens > 0;\n\tif (!hasComponents) return Math.max(0, usage.totalTokens || 0);\n\n\tconst cacheMirrorsInput = input > 0 && cacheTokens > 0 && cacheTokens >= input * 0.9 && cacheTokens <= input * 1.1;\n\tconst promptTokens = cacheMirrorsInput ? input : input + cacheTokens;\n\treturn promptTokens + output;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport interface ContextUsageEstimate {\n\ttokens: number;\n\tusageTokens: number;\n\ttrailingTokens: number;\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/**\n * Estimate context tokens from messages, using the last assistant usage when available.\n * If there are messages after the last usage, estimate their tokens with estimateTokens.\n */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AgentMessage): number {\n\tlet chars = 0;\n\n\tswitch (message.role) {\n\t\tcase \"user\": {\n\t\t\tchars = estimateTextAndImageContentChars(\n\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\": {\n\t\t\tchars = estimateTextAndImageContentChars(message.content);\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"bashExecution\": {\n\t\t\tchars = message.command.length + message.output.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"branchSummary\": {\n\t\t\tchars = message.summary.length;\n\t\t\treturn Math.ceil(chars / 4);\n\t\t}\n\t}\n\n\treturn 0;\n}\n"]}
|