@oh-my-pi/pi-agent-core 18.1.17 → 18.1.18
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 +10 -0
- package/dist/types/compaction/anthropic.d.ts +108 -0
- package/dist/types/compaction/compaction.d.ts +7 -1
- package/dist/types/compaction/index.d.ts +1 -0
- package/package.json +8 -8
- package/src/compaction/anthropic.ts +310 -0
- package/src/compaction/compaction.ts +160 -5
- package/src/compaction/index.ts +1 -0
- package/src/compaction/prompts/anthropic-compaction-instructions.md +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.18] - 2026-09-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Anthropic server-side compaction as a `remote` compaction backend: model lines the beta supports (`compat.supportsServerCompaction`, rule-owned in the catalog: Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5) on the official endpoint, resolved the way the provider routes requests, plus Anthropic-compatible routes with `remoteCompaction.enabled`, compact by re-issuing the live turn's own request — same system prompt, tools, and history, so it reads the prompt cache the last turn wrote — with the `compact_20260112` edit paused after the summary and the harness summary prompt as `instructions`. The instructions name where the retained tail begins so the summary covers only the history the rebuilt context drops. The API's summary is stored as the entry text and as `preserveData.anthropicCompaction`, replayed natively on later Anthropic requests and read as plain text by every other provider; the retained tail comes from session entries as with a local summary. Contexts below 55k tokens (the API trigger floor plus margin) keep summarizing locally, and a response without a summary is a native failure, like the OpenAI lanes. An aborted compaction response is the abort (a cancellation, never a native failure) and an error response keeps its HTTP status, so auth and timeout classification match the OpenAI lanes; the block's opaque `encrypted_content` is persisted as `preserveData.anthropicCompaction.encryptedContent` and replayed verbatim.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `compact()` now forwards the caller's `oneshotRetry` opt-out to every summarization oneshot; auto-compaction's outer retry loop no longer multiplies with the inner transient-failure retries.
|
|
14
|
+
|
|
5
15
|
## [18.1.17] - 2026-09-10
|
|
6
16
|
|
|
7
17
|
### Changed
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic server-side compaction (`compact-2026-01-12` beta).
|
|
3
|
+
*
|
|
4
|
+
* The compaction request is the live turn's own request shape — same system
|
|
5
|
+
* prompt, tools, and message history — plus the `compact_20260112` edit with
|
|
6
|
+
* `pause_after_compaction`. The API summarizes the prompt from the already
|
|
7
|
+
* cached prefix and stops; the summary arrives as a `compaction` block that
|
|
8
|
+
* the provider surfaces as an `anthropicCompaction` payload. The summary is
|
|
9
|
+
* plain text, so it doubles as the compaction entry's readable summary for
|
|
10
|
+
* every other provider, while the Anthropic provider replays it as a native
|
|
11
|
+
* block (the API drops everything that precedes it). The retained tail after
|
|
12
|
+
* the cut point is replayed from session entries exactly like a local summary.
|
|
13
|
+
*/
|
|
14
|
+
import type { AnthropicCompactionPayload, ApiKey, Effort, Message, Model, SimpleStreamOptions, Tool, Usage } from "@oh-my-pi/pi-ai";
|
|
15
|
+
import { type InstrumentedChatSpanOptions } from "../telemetry.js";
|
|
16
|
+
export declare const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
|
|
17
|
+
/** The API rejects a `compact_20260112` trigger below this many input tokens. */
|
|
18
|
+
export declare const ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS = 50000;
|
|
19
|
+
/**
|
|
20
|
+
* Smallest context the native lane accepts. The trigger sits at the API
|
|
21
|
+
* floor, so a prompt that lands below it is answered instead of compacted;
|
|
22
|
+
* the margin over the floor absorbs the difference between the last reported
|
|
23
|
+
* context size and the compaction request's own input.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS = 55000;
|
|
26
|
+
/** Summary persisted under {@link ANTHROPIC_COMPACTION_PRESERVE_KEY}. */
|
|
27
|
+
export interface AnthropicCompactionPreserveData {
|
|
28
|
+
provider: string;
|
|
29
|
+
content: string;
|
|
30
|
+
/** Opaque provider state the API attached to the block; replayed verbatim. */
|
|
31
|
+
encryptedContent?: string;
|
|
32
|
+
/** Harness file metadata (`<files>` section) replayed after the native block. */
|
|
33
|
+
filesText?: string;
|
|
34
|
+
/** Model that wrote the summary. */
|
|
35
|
+
model?: string;
|
|
36
|
+
/** Prompt tokens the compaction request processed, for display. */
|
|
37
|
+
usedTokens?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether a model compacts through the Anthropic compaction beta. Model
|
|
41
|
+
* eligibility is catalog policy (`compat.supportsServerCompaction`, the
|
|
42
|
+
* lineage the beta documents); endpoint eligibility is resolved the way the
|
|
43
|
+
* provider routes requests, so a Foundry or `ANTHROPIC_BASE_URL` reroute of a
|
|
44
|
+
* first-party model is excluded unless the route opted in with
|
|
45
|
+
* `remoteCompaction.enabled`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function shouldUseAnthropicNativeCompaction(model: Model): model is Model<"anthropic-messages">;
|
|
48
|
+
export declare function getPreservedAnthropicCompactionData(preserveData: Record<string, unknown> | undefined): AnthropicCompactionPreserveData | undefined;
|
|
49
|
+
/** Set or strip the Anthropic compaction slot; a new compaction never inherits a stale summary. */
|
|
50
|
+
export declare function withAnthropicCompactionPreserveData(preserveData: Record<string, unknown> | undefined, compaction: AnthropicCompactionPreserveData | undefined): Record<string, unknown> | undefined;
|
|
51
|
+
/** Replay payload for a compaction summary the active model produced natively. */
|
|
52
|
+
export declare function getAnthropicCompactionPayload(preserveData: Record<string, unknown> | undefined): AnthropicCompactionPayload | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The retained tail as the model will see it, for the summarization
|
|
55
|
+
* instructions: how many of the conversation's final wire messages stay in
|
|
56
|
+
* context verbatim, and the role of the first. The compaction request carries
|
|
57
|
+
* the whole conversation so the prompt cache the live turn wrote is read, but
|
|
58
|
+
* the summary must cover only the history before that tail — the local
|
|
59
|
+
* summarizer never sees the tail, and the rebuilt context replays it after the
|
|
60
|
+
* summary. Counting mirrors the provider's message conversion (consecutive
|
|
61
|
+
* tool results collapse into one user message; developer messages are user
|
|
62
|
+
* messages). Structured for the prompt template, which renders the
|
|
63
|
+
* singular/plural wording; the description quotes no content: quoting the
|
|
64
|
+
* tail would hand the summarizer the very facts it must leave to the tail.
|
|
65
|
+
*/
|
|
66
|
+
export interface RetainedTailScope {
|
|
67
|
+
count: number;
|
|
68
|
+
role: "assistant" | "user";
|
|
69
|
+
}
|
|
70
|
+
export declare function describeRetainedTail(messages: readonly Message[]): RetainedTailScope | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Summarization prompt sent as the edit's `instructions`, which replace the
|
|
73
|
+
* API default entirely. The template lays out the retained-tail boundary
|
|
74
|
+
* first, so the summary covers only the history the rebuilt context drops,
|
|
75
|
+
* then the caller's extra context, the same structure prompt as the local
|
|
76
|
+
* summarizer, the caller's focus, and the tool-abstention clause the API
|
|
77
|
+
* recommends when tools are defined (a summarization pass that calls a tool
|
|
78
|
+
* yields no summary).
|
|
79
|
+
*/
|
|
80
|
+
export declare function buildAnthropicCompactionInstructions(basePrompt: string, customInstructions: string | undefined, extraContext: string | undefined, retainedTail: RetainedTailScope | undefined): string;
|
|
81
|
+
export interface AnthropicNativeCompactionRequest {
|
|
82
|
+
systemPrompt: string[];
|
|
83
|
+
messages: Message[];
|
|
84
|
+
tools?: Tool[];
|
|
85
|
+
instructions: string;
|
|
86
|
+
maxTokens: number;
|
|
87
|
+
reasoning?: Effort;
|
|
88
|
+
}
|
|
89
|
+
export interface AnthropicNativeCompactionResponse {
|
|
90
|
+
content: string;
|
|
91
|
+
encryptedContent?: string;
|
|
92
|
+
usage: Usage;
|
|
93
|
+
model: string;
|
|
94
|
+
}
|
|
95
|
+
export interface AnthropicNativeCompactionOptions extends Pick<SimpleStreamOptions, "initiatorOverride" | "metadata" | "fetch" | "sessionId" | "promptCacheKey" | "providerSessionState" | "maxInFlightRequests">, Pick<InstrumentedChatSpanOptions, "completeImpl" | "telemetry" | "retry"> {
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Run one compaction request and return the summary the API wrote, with the
|
|
99
|
+
* opaque `encrypted_content` the API attached for the replay. `completeSimple`
|
|
100
|
+
* resolves terminal failures as messages, so their classification is restored
|
|
101
|
+
* here: an aborted response is an `AbortError` (a cancellation, never a native
|
|
102
|
+
* failure) and an error response keeps its HTTP status, so auth and timeout
|
|
103
|
+
* handling downstream classify it the same way as the OpenAI lanes. A response
|
|
104
|
+
* without a summary is a native failure — the API answers the prompt instead
|
|
105
|
+
* when its input never reached the trigger, and returns an empty block when
|
|
106
|
+
* the model called a tool during summarization.
|
|
107
|
+
*/
|
|
108
|
+
export declare function requestAnthropicNativeCompaction(model: Model<"anthropic-messages">, apiKey: ApiKey, request: AnthropicNativeCompactionRequest, signal: AbortSignal | undefined, options: AnthropicNativeCompactionOptions): Promise<AnthropicNativeCompactionResponse>;
|
|
@@ -65,7 +65,11 @@ export declare const DEFAULT_RESERVE_TOKENS = 16384;
|
|
|
65
65
|
*/
|
|
66
66
|
export declare const MAX_SUMMARY_TOKENS = 16384;
|
|
67
67
|
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Whether a compaction candidate preserves provider-native transport under the
|
|
70
|
+
* effective settings: an OpenAI Responses compact route (V1 or streamed V2) or
|
|
71
|
+
* the Anthropic compaction beta.
|
|
72
|
+
*/
|
|
69
73
|
export declare function shouldUseProviderNativeCompaction(model: Model, settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">): boolean;
|
|
70
74
|
/**
|
|
71
75
|
* Calculate total context tokens from usage.
|
|
@@ -290,6 +294,8 @@ export interface CompactionPreparation {
|
|
|
290
294
|
tokensBefore: number;
|
|
291
295
|
/** Summary from previous compaction, for iterative update */
|
|
292
296
|
previousSummary?: string;
|
|
297
|
+
/** ISO timestamp of the previous compaction entry, for iterative update */
|
|
298
|
+
previousSummaryTimestamp?: string;
|
|
293
299
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
294
300
|
previousPreserveData?: Record<string, unknown>;
|
|
295
301
|
/** File operations extracted from messagesToSummarize */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "18.1.
|
|
4
|
+
"version": "18.1.18",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "18.1.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "18.1.
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
42
|
-
"@oh-my-pi/pi-wire": "18.1.
|
|
43
|
-
"@oh-my-pi/snapcompact": "18.1.
|
|
38
|
+
"@oh-my-pi/pi-ai": "18.1.18",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "18.1.18",
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.1.18",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.1.18",
|
|
42
|
+
"@oh-my-pi/pi-wire": "18.1.18",
|
|
43
|
+
"@oh-my-pi/snapcompact": "18.1.18",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@oh-my-pi/omptype": "18.1.
|
|
47
|
+
"@oh-my-pi/omptype": "18.1.18",
|
|
48
48
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
49
49
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
50
50
|
"@types/bun": "^1.3.14"
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic server-side compaction (`compact-2026-01-12` beta).
|
|
3
|
+
*
|
|
4
|
+
* The compaction request is the live turn's own request shape — same system
|
|
5
|
+
* prompt, tools, and message history — plus the `compact_20260112` edit with
|
|
6
|
+
* `pause_after_compaction`. The API summarizes the prompt from the already
|
|
7
|
+
* cached prefix and stops; the summary arrives as a `compaction` block that
|
|
8
|
+
* the provider surfaces as an `anthropicCompaction` payload. The summary is
|
|
9
|
+
* plain text, so it doubles as the compaction entry's readable summary for
|
|
10
|
+
* every other provider, while the Anthropic provider replays it as a native
|
|
11
|
+
* block (the API drops everything that precedes it). The retained tail after
|
|
12
|
+
* the cut point is replayed from session entries exactly like a local summary.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
AnthropicCompactionPayload,
|
|
17
|
+
ApiKey,
|
|
18
|
+
AssistantMessage,
|
|
19
|
+
Effort,
|
|
20
|
+
Message,
|
|
21
|
+
Model,
|
|
22
|
+
SimpleStreamOptions,
|
|
23
|
+
Tool,
|
|
24
|
+
Usage,
|
|
25
|
+
} from "@oh-my-pi/pi-ai";
|
|
26
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
27
|
+
import { supportsAnthropicCompaction } from "@oh-my-pi/pi-ai/providers/anthropic";
|
|
28
|
+
import { isRecord, prompt } from "@oh-my-pi/pi-utils";
|
|
29
|
+
import { type InstrumentedChatSpanOptions, instrumentedCompleteSimple } from "../telemetry";
|
|
30
|
+
import anthropicCompactionInstructionsPrompt from "./prompts/anthropic-compaction-instructions.md" with { type: "text" };
|
|
31
|
+
|
|
32
|
+
export const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
|
|
33
|
+
|
|
34
|
+
/** The API rejects a `compact_20260112` trigger below this many input tokens. */
|
|
35
|
+
export const ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS = 50_000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Smallest context the native lane accepts. The trigger sits at the API
|
|
39
|
+
* floor, so a prompt that lands below it is answered instead of compacted;
|
|
40
|
+
* the margin over the floor absorbs the difference between the last reported
|
|
41
|
+
* context size and the compaction request's own input.
|
|
42
|
+
*/
|
|
43
|
+
export const ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS = 55_000;
|
|
44
|
+
|
|
45
|
+
/** Summary persisted under {@link ANTHROPIC_COMPACTION_PRESERVE_KEY}. */
|
|
46
|
+
export interface AnthropicCompactionPreserveData {
|
|
47
|
+
provider: string;
|
|
48
|
+
content: string;
|
|
49
|
+
/** Opaque provider state the API attached to the block; replayed verbatim. */
|
|
50
|
+
encryptedContent?: string;
|
|
51
|
+
/** Harness file metadata (`<files>` section) replayed after the native block. */
|
|
52
|
+
filesText?: string;
|
|
53
|
+
/** Model that wrote the summary. */
|
|
54
|
+
model?: string;
|
|
55
|
+
/** Prompt tokens the compaction request processed, for display. */
|
|
56
|
+
usedTokens?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages"> {
|
|
60
|
+
return model.api === "anthropic-messages";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a model compacts through the Anthropic compaction beta. Model
|
|
65
|
+
* eligibility is catalog policy (`compat.supportsServerCompaction`, the
|
|
66
|
+
* lineage the beta documents); endpoint eligibility is resolved the way the
|
|
67
|
+
* provider routes requests, so a Foundry or `ANTHROPIC_BASE_URL` reroute of a
|
|
68
|
+
* first-party model is excluded unless the route opted in with
|
|
69
|
+
* `remoteCompaction.enabled`.
|
|
70
|
+
*/
|
|
71
|
+
export function shouldUseAnthropicNativeCompaction(model: Model): model is Model<"anthropic-messages"> {
|
|
72
|
+
return isAnthropicMessagesModel(model) && supportsAnthropicCompaction(model);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getPreservedAnthropicCompactionData(
|
|
76
|
+
preserveData: Record<string, unknown> | undefined,
|
|
77
|
+
): AnthropicCompactionPreserveData | undefined {
|
|
78
|
+
const candidate = preserveData?.[ANTHROPIC_COMPACTION_PRESERVE_KEY];
|
|
79
|
+
if (!isRecord(candidate)) return undefined;
|
|
80
|
+
if (typeof candidate.provider !== "string" || candidate.provider.length === 0) return undefined;
|
|
81
|
+
if (typeof candidate.content !== "string" || candidate.content.length === 0) return undefined;
|
|
82
|
+
return {
|
|
83
|
+
provider: candidate.provider,
|
|
84
|
+
content: candidate.content,
|
|
85
|
+
...(typeof candidate.encryptedContent === "string" && candidate.encryptedContent.length > 0
|
|
86
|
+
? { encryptedContent: candidate.encryptedContent }
|
|
87
|
+
: {}),
|
|
88
|
+
...(typeof candidate.filesText === "string" && candidate.filesText.length > 0
|
|
89
|
+
? { filesText: candidate.filesText }
|
|
90
|
+
: {}),
|
|
91
|
+
...(typeof candidate.model === "string" ? { model: candidate.model } : {}),
|
|
92
|
+
...(typeof candidate.usedTokens === "number" ? { usedTokens: candidate.usedTokens } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Set or strip the Anthropic compaction slot; a new compaction never inherits a stale summary. */
|
|
97
|
+
export function withAnthropicCompactionPreserveData(
|
|
98
|
+
preserveData: Record<string, unknown> | undefined,
|
|
99
|
+
compaction: AnthropicCompactionPreserveData | undefined,
|
|
100
|
+
): Record<string, unknown> | undefined {
|
|
101
|
+
if (compaction) {
|
|
102
|
+
return { ...preserveData, [ANTHROPIC_COMPACTION_PRESERVE_KEY]: compaction };
|
|
103
|
+
}
|
|
104
|
+
if (!preserveData || !(ANTHROPIC_COMPACTION_PRESERVE_KEY in preserveData)) {
|
|
105
|
+
return preserveData;
|
|
106
|
+
}
|
|
107
|
+
const { [ANTHROPIC_COMPACTION_PRESERVE_KEY]: _removed, ...rest } = preserveData;
|
|
108
|
+
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Replay payload for a compaction summary the active model produced natively. */
|
|
112
|
+
export function getAnthropicCompactionPayload(
|
|
113
|
+
preserveData: Record<string, unknown> | undefined,
|
|
114
|
+
): AnthropicCompactionPayload | undefined {
|
|
115
|
+
const preserved = getPreservedAnthropicCompactionData(preserveData);
|
|
116
|
+
if (!preserved) return undefined;
|
|
117
|
+
return {
|
|
118
|
+
type: "anthropicCompaction",
|
|
119
|
+
provider: preserved.provider,
|
|
120
|
+
content: preserved.content,
|
|
121
|
+
...(preserved.encryptedContent ? { encryptedContent: preserved.encryptedContent } : {}),
|
|
122
|
+
...(preserved.filesText ? { filesText: preserved.filesText } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The retained tail as the model will see it, for the summarization
|
|
128
|
+
* instructions: how many of the conversation's final wire messages stay in
|
|
129
|
+
* context verbatim, and the role of the first. The compaction request carries
|
|
130
|
+
* the whole conversation so the prompt cache the live turn wrote is read, but
|
|
131
|
+
* the summary must cover only the history before that tail — the local
|
|
132
|
+
* summarizer never sees the tail, and the rebuilt context replays it after the
|
|
133
|
+
* summary. Counting mirrors the provider's message conversion (consecutive
|
|
134
|
+
* tool results collapse into one user message; developer messages are user
|
|
135
|
+
* messages). Structured for the prompt template, which renders the
|
|
136
|
+
* singular/plural wording; the description quotes no content: quoting the
|
|
137
|
+
* tail would hand the summarizer the very facts it must leave to the tail.
|
|
138
|
+
*/
|
|
139
|
+
export interface RetainedTailScope {
|
|
140
|
+
count: number;
|
|
141
|
+
role: "assistant" | "user";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function describeRetainedTail(messages: readonly Message[]): RetainedTailScope | undefined {
|
|
145
|
+
const first = messages[0];
|
|
146
|
+
if (!first) return undefined;
|
|
147
|
+
let count = 0;
|
|
148
|
+
let previousWasToolResult = false;
|
|
149
|
+
for (const message of messages) {
|
|
150
|
+
const isToolResult = message.role === "toolResult";
|
|
151
|
+
if (!(isToolResult && previousWasToolResult)) count += 1;
|
|
152
|
+
previousWasToolResult = isToolResult;
|
|
153
|
+
}
|
|
154
|
+
// Mirror the provider's trailing-assistant prefill: a tail ending in a
|
|
155
|
+
// live assistant turn gains a synthetic trailing user message on the
|
|
156
|
+
// wire, which stays verbatim too. Only blocks the converter emits count —
|
|
157
|
+
// blank text never serializes, and images, redacted thinking, and fallback
|
|
158
|
+
// markers need target context this scope lacks, so a turn of only those
|
|
159
|
+
// emits nothing and draws no pad. Without the pad in
|
|
160
|
+
// the scope, the summary could duplicate the tail head.
|
|
161
|
+
const last = messages[messages.length - 1];
|
|
162
|
+
if (last?.role === "assistant" && last.content.some(emitsWireBlock)) {
|
|
163
|
+
count += 1;
|
|
164
|
+
}
|
|
165
|
+
return { count, role: first.role === "assistant" ? "assistant" : "user" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether an assistant content block reaches the wire. Server-tool blocks
|
|
170
|
+
* serialize unconditionally; blank text never does. Redacted thinking and
|
|
171
|
+
* fallback markers replay only for specific deployments, which the scope
|
|
172
|
+
* cannot see, so a turn of only those conservatively draws no pad.
|
|
173
|
+
*/
|
|
174
|
+
function emitsWireBlock(block: AssistantMessage["content"][number]): boolean {
|
|
175
|
+
switch (block.type) {
|
|
176
|
+
case "text":
|
|
177
|
+
return block.text.trim().length > 0;
|
|
178
|
+
case "toolCall":
|
|
179
|
+
case "anthropicServerTool":
|
|
180
|
+
return true;
|
|
181
|
+
case "thinking":
|
|
182
|
+
return block.thinking.trim().length > 0 || (block.thinkingSignature ?? "").trim().length > 0;
|
|
183
|
+
default:
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Summarization prompt sent as the edit's `instructions`, which replace the
|
|
190
|
+
* API default entirely. The template lays out the retained-tail boundary
|
|
191
|
+
* first, so the summary covers only the history the rebuilt context drops,
|
|
192
|
+
* then the caller's extra context, the same structure prompt as the local
|
|
193
|
+
* summarizer, the caller's focus, and the tool-abstention clause the API
|
|
194
|
+
* recommends when tools are defined (a summarization pass that calls a tool
|
|
195
|
+
* yields no summary).
|
|
196
|
+
*/
|
|
197
|
+
export function buildAnthropicCompactionInstructions(
|
|
198
|
+
basePrompt: string,
|
|
199
|
+
customInstructions: string | undefined,
|
|
200
|
+
extraContext: string | undefined,
|
|
201
|
+
retainedTail: RetainedTailScope | undefined,
|
|
202
|
+
): string {
|
|
203
|
+
return prompt.render(anthropicCompactionInstructionsPrompt, {
|
|
204
|
+
basePrompt,
|
|
205
|
+
customInstructions,
|
|
206
|
+
extraContext,
|
|
207
|
+
retainedTail,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface AnthropicNativeCompactionRequest {
|
|
212
|
+
systemPrompt: string[];
|
|
213
|
+
messages: Message[];
|
|
214
|
+
tools?: Tool[];
|
|
215
|
+
instructions: string;
|
|
216
|
+
maxTokens: number;
|
|
217
|
+
reasoning?: Effort;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface AnthropicNativeCompactionResponse {
|
|
221
|
+
content: string;
|
|
222
|
+
encryptedContent?: string;
|
|
223
|
+
usage: Usage;
|
|
224
|
+
model: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface AnthropicNativeCompactionOptions
|
|
228
|
+
extends
|
|
229
|
+
Pick<
|
|
230
|
+
SimpleStreamOptions,
|
|
231
|
+
| "initiatorOverride"
|
|
232
|
+
| "metadata"
|
|
233
|
+
| "fetch"
|
|
234
|
+
| "sessionId"
|
|
235
|
+
| "promptCacheKey"
|
|
236
|
+
| "providerSessionState"
|
|
237
|
+
| "maxInFlightRequests"
|
|
238
|
+
>,
|
|
239
|
+
Pick<InstrumentedChatSpanOptions, "completeImpl" | "telemetry" | "retry"> {}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Run one compaction request and return the summary the API wrote, with the
|
|
243
|
+
* opaque `encrypted_content` the API attached for the replay. `completeSimple`
|
|
244
|
+
* resolves terminal failures as messages, so their classification is restored
|
|
245
|
+
* here: an aborted response is an `AbortError` (a cancellation, never a native
|
|
246
|
+
* failure) and an error response keeps its HTTP status, so auth and timeout
|
|
247
|
+
* handling downstream classify it the same way as the OpenAI lanes. A response
|
|
248
|
+
* without a summary is a native failure — the API answers the prompt instead
|
|
249
|
+
* when its input never reached the trigger, and returns an empty block when
|
|
250
|
+
* the model called a tool during summarization.
|
|
251
|
+
*/
|
|
252
|
+
export async function requestAnthropicNativeCompaction(
|
|
253
|
+
model: Model<"anthropic-messages">,
|
|
254
|
+
apiKey: ApiKey,
|
|
255
|
+
request: AnthropicNativeCompactionRequest,
|
|
256
|
+
signal: AbortSignal | undefined,
|
|
257
|
+
options: AnthropicNativeCompactionOptions,
|
|
258
|
+
): Promise<AnthropicNativeCompactionResponse> {
|
|
259
|
+
const response = await instrumentedCompleteSimple(
|
|
260
|
+
model,
|
|
261
|
+
{ systemPrompt: request.systemPrompt, messages: request.messages, tools: request.tools },
|
|
262
|
+
{
|
|
263
|
+
apiKey,
|
|
264
|
+
signal,
|
|
265
|
+
maxTokens: request.maxTokens,
|
|
266
|
+
reasoning: request.reasoning,
|
|
267
|
+
initiatorOverride: options.initiatorOverride,
|
|
268
|
+
metadata: options.metadata,
|
|
269
|
+
fetch: options.fetch,
|
|
270
|
+
sessionId: options.sessionId,
|
|
271
|
+
promptCacheKey: options.promptCacheKey,
|
|
272
|
+
providerSessionState: options.providerSessionState,
|
|
273
|
+
maxInFlightRequests: options.maxInFlightRequests,
|
|
274
|
+
anthropicCompaction: {
|
|
275
|
+
triggerInputTokens: ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS,
|
|
276
|
+
pauseAfterCompaction: true,
|
|
277
|
+
instructions: request.instructions,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
telemetry: options.telemetry,
|
|
282
|
+
oneshotKind: "compaction_native",
|
|
283
|
+
completeImpl: options.completeImpl,
|
|
284
|
+
retry: options.retry,
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
if (response.stopReason === "aborted") {
|
|
288
|
+
throw new AIError.AbortError("Anthropic compaction aborted", { cause: signal?.reason });
|
|
289
|
+
}
|
|
290
|
+
if (response.stopReason === "error") {
|
|
291
|
+
const message = `Anthropic compaction failed: ${response.errorMessage ?? "unknown error"}`;
|
|
292
|
+
throw response.errorStatus === undefined
|
|
293
|
+
? new Error(message)
|
|
294
|
+
: new AIError.ProviderHttpError(message, response.errorStatus);
|
|
295
|
+
}
|
|
296
|
+
const payload = response.providerPayload;
|
|
297
|
+
if (payload?.type !== "anthropicCompaction" || payload.content.length === 0) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
response.stopDetails?.type === "compaction"
|
|
300
|
+
? "Anthropic compaction returned no summary"
|
|
301
|
+
: "Anthropic compaction response carried no compaction block",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
content: payload.content,
|
|
306
|
+
encryptedContent: payload.encryptedContent,
|
|
307
|
+
usage: response.usage,
|
|
308
|
+
model: response.model,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -42,6 +42,15 @@ import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
|
42
42
|
import { ThinkingLevel } from "../thinking";
|
|
43
43
|
import { Tokenizer } from "../tokenizer";
|
|
44
44
|
import type { AgentMessage } from "../types";
|
|
45
|
+
import {
|
|
46
|
+
ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS,
|
|
47
|
+
buildAnthropicCompactionInstructions,
|
|
48
|
+
describeRetainedTail,
|
|
49
|
+
getPreservedAnthropicCompactionData,
|
|
50
|
+
requestAnthropicNativeCompaction,
|
|
51
|
+
shouldUseAnthropicNativeCompaction,
|
|
52
|
+
withAnthropicCompactionPreserveData,
|
|
53
|
+
} from "./anthropic";
|
|
45
54
|
import {
|
|
46
55
|
buildCompactionV2Request,
|
|
47
56
|
buildCompactionV2RequestFromBody,
|
|
@@ -53,7 +62,13 @@ import {
|
|
|
53
62
|
} from "./compaction-v2-streaming";
|
|
54
63
|
import type { CompactionEntry, SessionEntry } from "./entries";
|
|
55
64
|
import { NativeCompactionError } from "./errors";
|
|
56
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
type ConvertToLlm,
|
|
67
|
+
createBranchSummaryMessage,
|
|
68
|
+
createCompactionSummaryMessage,
|
|
69
|
+
createCustomMessage,
|
|
70
|
+
defaultConvertToLlm,
|
|
71
|
+
} from "./messages";
|
|
57
72
|
import {
|
|
58
73
|
buildOpenAiNativeHistory,
|
|
59
74
|
getPreservedOpenAiRemoteCompactionData,
|
|
@@ -224,7 +239,11 @@ export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
|
|
|
224
239
|
v2RetainedMessageBudget: V2_RETAINED_MESSAGE_TOKEN_BUDGET,
|
|
225
240
|
};
|
|
226
241
|
|
|
227
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* Whether a compaction candidate preserves provider-native transport under the
|
|
244
|
+
* effective settings: an OpenAI Responses compact route (V1 or streamed V2) or
|
|
245
|
+
* the Anthropic compaction beta.
|
|
246
|
+
*/
|
|
228
247
|
export function shouldUseProviderNativeCompaction(
|
|
229
248
|
model: Model,
|
|
230
249
|
settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">,
|
|
@@ -232,7 +251,8 @@ export function shouldUseProviderNativeCompaction(
|
|
|
232
251
|
if (settings.remoteEnabled === false) return false;
|
|
233
252
|
return (
|
|
234
253
|
shouldUseOpenAiRemoteCompaction(model) ||
|
|
235
|
-
(settings.remoteStreamingV2Enabled !== false && shouldUseCompactionV2Streaming(model))
|
|
254
|
+
(settings.remoteStreamingV2Enabled !== false && shouldUseCompactionV2Streaming(model)) ||
|
|
255
|
+
shouldUseAnthropicNativeCompaction(model)
|
|
236
256
|
);
|
|
237
257
|
}
|
|
238
258
|
|
|
@@ -1236,6 +1256,8 @@ export interface CompactionPreparation {
|
|
|
1236
1256
|
tokensBefore: number;
|
|
1237
1257
|
/** Summary from previous compaction, for iterative update */
|
|
1238
1258
|
previousSummary?: string;
|
|
1259
|
+
/** ISO timestamp of the previous compaction entry, for iterative update */
|
|
1260
|
+
previousSummaryTimestamp?: string;
|
|
1239
1261
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
1240
1262
|
previousPreserveData?: Record<string, unknown>;
|
|
1241
1263
|
/** File operations extracted from messagesToSummarize */
|
|
@@ -1410,6 +1432,7 @@ export function prepareCompaction(
|
|
|
1410
1432
|
isSplitTurn: cutPoint.isSplitTurn,
|
|
1411
1433
|
tokensBefore,
|
|
1412
1434
|
previousSummary: previousCompaction?.summary,
|
|
1435
|
+
previousSummaryTimestamp: previousCompaction?.timestamp,
|
|
1413
1436
|
previousPreserveData: previousCompaction?.preserveData,
|
|
1414
1437
|
fileOps,
|
|
1415
1438
|
settings,
|
|
@@ -1566,6 +1589,9 @@ export async function compact(
|
|
|
1566
1589
|
tools: options?.tools,
|
|
1567
1590
|
fetch: options?.fetch,
|
|
1568
1591
|
completeImpl: options?.completeImpl,
|
|
1592
|
+
// The caller's opt-out must reach every summarization oneshot, otherwise
|
|
1593
|
+
// an outer retry loop multiplies with the inner one (see SummaryOptions).
|
|
1594
|
+
oneshotRetry: options?.oneshotRetry,
|
|
1569
1595
|
};
|
|
1570
1596
|
|
|
1571
1597
|
const previousSnapcompactArchive = snapcompact.getPreservedArchive(previousPreserveData);
|
|
@@ -1580,7 +1606,10 @@ export async function compact(
|
|
|
1580
1606
|
? createSnapcompactArchiveMigrationMessage(previousSnapcompactArchiveText)
|
|
1581
1607
|
: undefined;
|
|
1582
1608
|
|
|
1583
|
-
let preserveData =
|
|
1609
|
+
let preserveData = withAnthropicCompactionPreserveData(
|
|
1610
|
+
withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined),
|
|
1611
|
+
undefined,
|
|
1612
|
+
);
|
|
1584
1613
|
const remoteMessages: AgentMessage[] = [
|
|
1585
1614
|
...(snapcompactArchiveMigrationMessage ? [snapcompactArchiveMigrationMessage] : []),
|
|
1586
1615
|
...messagesToSummarize,
|
|
@@ -1765,6 +1794,112 @@ export async function compact(
|
|
|
1765
1794
|
}
|
|
1766
1795
|
}
|
|
1767
1796
|
|
|
1797
|
+
// Anthropic server-side compaction: the live turn's request shape plus the
|
|
1798
|
+
// compact edit, so the API summarizes from its cached prefix. The summary is
|
|
1799
|
+
// real text, persisted both as the entry summary and as the native replay
|
|
1800
|
+
// payload. A context below the API's trigger floor cannot compact remotely
|
|
1801
|
+
// and takes the local summarizer instead — an eligibility boundary, not a
|
|
1802
|
+
// failure.
|
|
1803
|
+
let nativeSummary: string | undefined;
|
|
1804
|
+
let nativeEncryptedContent: string | undefined;
|
|
1805
|
+
let nativeUsedTokens: number | undefined;
|
|
1806
|
+
if (
|
|
1807
|
+
!usedRemoteCompaction &&
|
|
1808
|
+
settings.remoteEnabled !== false &&
|
|
1809
|
+
shouldUseAnthropicNativeCompaction(model) &&
|
|
1810
|
+
tokensBefore >= ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS
|
|
1811
|
+
) {
|
|
1812
|
+
const previousNative = getPreservedAnthropicCompactionData(previousPreserveData);
|
|
1813
|
+
// Lead with the previous summary exactly as the live context renders it:
|
|
1814
|
+
// natively when this provider wrote it, as text otherwise. The request
|
|
1815
|
+
// then shares the live turn's prefix byte-for-byte. A prior snapcompact
|
|
1816
|
+
// archive is already merged into that summary text, so the archive
|
|
1817
|
+
// migration message the OpenAI lanes carry is omitted here.
|
|
1818
|
+
// The rewrite marker must precede every message this request replays —
|
|
1819
|
+
// summarized history and retained tail alike — exactly like the live
|
|
1820
|
+
// context rebuild predates its tail. A previous compaction's commit
|
|
1821
|
+
// timestamp is newer than re-retained or re-summarized turns, so
|
|
1822
|
+
// reusing it would strip their bound thinking only in this request,
|
|
1823
|
+
// diverging from the cached live prefix (and possibly dropping below
|
|
1824
|
+
// the trigger). Manually built preparations with no input at all fall
|
|
1825
|
+
// back to the current time.
|
|
1826
|
+
const firstReplayed = messagesToSummarize[0] ?? turnPrefixMessages[0] ?? recentMessages[0];
|
|
1827
|
+
const previousSummaryAt =
|
|
1828
|
+
firstReplayed !== undefined ? new Date(firstReplayed.timestamp - 1).toISOString() : new Date().toISOString();
|
|
1829
|
+
const previousSummaryMessage = previousSummaryForCompaction
|
|
1830
|
+
? createCompactionSummaryMessage(previousSummaryForCompaction, tokensBefore, previousSummaryAt, {
|
|
1831
|
+
providerPayload:
|
|
1832
|
+
previousNative?.provider === model.provider
|
|
1833
|
+
? {
|
|
1834
|
+
type: "anthropicCompaction",
|
|
1835
|
+
provider: previousNative.provider,
|
|
1836
|
+
content: previousNative.content,
|
|
1837
|
+
...(previousNative.encryptedContent
|
|
1838
|
+
? { encryptedContent: previousNative.encryptedContent }
|
|
1839
|
+
: {}),
|
|
1840
|
+
...(previousNative.filesText ? { filesText: previousNative.filesText } : {}),
|
|
1841
|
+
}
|
|
1842
|
+
: undefined,
|
|
1843
|
+
})
|
|
1844
|
+
: undefined;
|
|
1845
|
+
const convertToLlm = summaryOptions.convertToLlm ?? defaultConvertToLlm;
|
|
1846
|
+
const retainedTail = convertToLlm(recentMessages);
|
|
1847
|
+
const messages = [
|
|
1848
|
+
...convertToLlm([
|
|
1849
|
+
...(previousSummaryMessage ? [previousSummaryMessage] : []),
|
|
1850
|
+
...messagesToSummarize,
|
|
1851
|
+
...turnPrefixMessages,
|
|
1852
|
+
]),
|
|
1853
|
+
...retainedTail,
|
|
1854
|
+
];
|
|
1855
|
+
try {
|
|
1856
|
+
const remote = await requestAnthropicNativeCompaction(
|
|
1857
|
+
model,
|
|
1858
|
+
apiKey,
|
|
1859
|
+
{
|
|
1860
|
+
systemPrompt: summaryOptions.remoteSystemPrompt ?? [SUMMARIZATION_SYSTEM_PROMPT],
|
|
1861
|
+
messages,
|
|
1862
|
+
tools: summaryOptions.tools,
|
|
1863
|
+
instructions: buildAnthropicCompactionInstructions(
|
|
1864
|
+
summaryOptions.promptOverride ?? SUMMARIZATION_PROMPT,
|
|
1865
|
+
customInstructions,
|
|
1866
|
+
formatAdditionalContext(summaryOptions.extraContext).trim() || undefined,
|
|
1867
|
+
describeRetainedTail(retainedTail),
|
|
1868
|
+
),
|
|
1869
|
+
maxTokens: Math.min(Math.floor(0.8 * reserveTokens), MAX_SUMMARY_TOKENS),
|
|
1870
|
+
reasoning: resolveCompactionEffort(model, summaryOptions.thinkingLevel),
|
|
1871
|
+
},
|
|
1872
|
+
signal,
|
|
1873
|
+
{
|
|
1874
|
+
initiatorOverride: summaryOptions.initiatorOverride,
|
|
1875
|
+
metadata: summaryOptions.metadata,
|
|
1876
|
+
fetch: summaryOptions.fetch,
|
|
1877
|
+
sessionId: summaryOptions.sessionId,
|
|
1878
|
+
promptCacheKey: summaryOptions.promptCacheKey,
|
|
1879
|
+
providerSessionState: summaryOptions.providerSessionState,
|
|
1880
|
+
completeImpl: summaryOptions.completeImpl,
|
|
1881
|
+
telemetry: summaryOptions.telemetry,
|
|
1882
|
+
retry: summaryOneshotRetry(summaryOptions),
|
|
1883
|
+
},
|
|
1884
|
+
);
|
|
1885
|
+
nativeSummary = remote.content;
|
|
1886
|
+
nativeEncryptedContent = remote.encryptedContent;
|
|
1887
|
+
nativeUsedTokens = calculatePromptTokens(remote.usage);
|
|
1888
|
+
usedRemoteCompaction = true;
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
// A user/session abort is a cancellation, not a remote failure —
|
|
1891
|
+
// swallowing it here would downgrade Esc into "fall back to local
|
|
1892
|
+
// summarization" and keep compaction running on an aborted signal.
|
|
1893
|
+
if (signal?.aborted) throw err;
|
|
1894
|
+
nativeCompactionError = selectNativeCompactionError(nativeCompactionError, err);
|
|
1895
|
+
logger.warn("Anthropic server-side compaction failed", {
|
|
1896
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1897
|
+
model: model.id,
|
|
1898
|
+
provider: model.provider,
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1768
1903
|
if (!usedRemoteCompaction && nativeCompactionError !== undefined && !summaryOptions.remoteEndpoint) {
|
|
1769
1904
|
throw new NativeCompactionError(nativeCompactionError);
|
|
1770
1905
|
}
|
|
@@ -1772,7 +1907,11 @@ export async function compact(
|
|
|
1772
1907
|
// Generate summaries (can be parallel if both needed) and merge into one
|
|
1773
1908
|
let summary: string;
|
|
1774
1909
|
|
|
1775
|
-
if (
|
|
1910
|
+
if (nativeSummary !== undefined) {
|
|
1911
|
+
// The API wrote a real summary; it is the entry text. The replayed
|
|
1912
|
+
// block below stays verbatim so it matches the opaque state.
|
|
1913
|
+
summary = nativeSummary;
|
|
1914
|
+
} else if (usedRemoteCompaction) {
|
|
1776
1915
|
// Remote compaction (V2 or V1) already compacted remotely; the durable
|
|
1777
1916
|
// history lives in the provider replay payload (preserveData). Skip local
|
|
1778
1917
|
// summarization so a successful remote compaction never pays for a second,
|
|
@@ -1831,6 +1970,22 @@ export async function compact(
|
|
|
1831
1970
|
// Compute file lists and append to summary
|
|
1832
1971
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
1833
1972
|
summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
1973
|
+
if (nativeSummary !== undefined) {
|
|
1974
|
+
// The replayed block stays byte-identical to the API's summary so it
|
|
1975
|
+
// matches `encryptedContent`. The harness file lists above travel
|
|
1976
|
+
// separately: the converter replaces the summary message with the
|
|
1977
|
+
// block and skips its text, so they would otherwise be invisible to
|
|
1978
|
+
// this provider. Every other provider keeps reading the entry text.
|
|
1979
|
+
const filesText = upsertFileOperations("", readFiles, modifiedFiles, fileOps.read) || undefined;
|
|
1980
|
+
preserveData = withAnthropicCompactionPreserveData(preserveData, {
|
|
1981
|
+
provider: model.provider,
|
|
1982
|
+
content: nativeSummary,
|
|
1983
|
+
...(nativeEncryptedContent ? { encryptedContent: nativeEncryptedContent } : {}),
|
|
1984
|
+
...(filesText ? { filesText } : {}),
|
|
1985
|
+
model: model.id,
|
|
1986
|
+
usedTokens: nativeUsedTokens,
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1834
1989
|
|
|
1835
1990
|
if (!firstKeptEntryId) {
|
|
1836
1991
|
throw new Error("First kept entry has no ID - session may need migration");
|
package/src/compaction/index.ts
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{{#if retainedTail}}
|
|
2
|
+
SCOPE: The conversation's final {{#when retainedTail.count "==" 1}}{{retainedTail.role}} message stays{{else}}{{retainedTail.count}} messages, starting with a {{retainedTail.role}} message, stay{{/when}} in context verbatim after your summary. Summarize ONLY the history before those messages. You MUST NOT restate anything from those final messages — the reader sees them right after the summary — and you MUST treat them as the most recent state when describing progress and next steps.
|
|
3
|
+
{{else}}
|
|
4
|
+
SCOPE: The conversation above is the transcript to summarize. The API replaces everything before your summary with it, so nothing you leave out survives into the next context window.
|
|
5
|
+
{{/if}}
|
|
6
|
+
|
|
7
|
+
{{#if extraContext}}
|
|
8
|
+
{{extraContext}}
|
|
9
|
+
|
|
10
|
+
{{/if}}
|
|
11
|
+
{{basePrompt}}
|
|
12
|
+
{{#if customInstructions}}
|
|
13
|
+
|
|
14
|
+
Additional focus: {{customInstructions}}
|
|
15
|
+
{{/if}}
|
|
16
|
+
|
|
17
|
+
You MUST NOT call any tools while writing the summary; respond with the summary text only.
|