import { resolve } from "node:path"; import { isDeepStrictEqual } from "node:util"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { type BeforeAgentStartEvent, buildSessionProjection, getAgentDir, type SessionEntry, } from "@earendil-works/pi-coding-agent"; import { readExtensionConfigFile } from "./agent-suite-storage"; import { countProjectionTextTokens } from "./context-size"; import { readCustomCompactionConfig } from "./custom-compaction-config"; import { getReadToolCallPath, isPathInsideOrEqual, resolveReadInputPath, } from "./skill-paths"; import { parseToolResultSummaryConfig, type ToolResultSummaryConfig, } from "./tool-result-summary"; /** Suite directory owned by context-projection. */ const CONTEXT_PROJECTION_EXTENSION_DIR = "context-projection"; /** Legacy config file name supported for existing installations. */ const CONTEXT_PROJECTION_LEGACY_CONFIG_FILE = "context-projection.json"; /** Extension-owned custom entry type used for branch-local projection state. */ export const CONTEXT_PROJECTION_CUSTOM_TYPE = "context-projection"; /** Config key that disables or enables provider-context projection. */ const ENABLED_CONFIG_KEY = "enabled"; /** Config key for the first remaining-token threshold that enables projection. */ const PROJECTION_REMAINING_TOKENS_L1_CONFIG_KEY = "projectionRemainingTokensL1"; /** Config key for the second remaining-token threshold that enables projection. */ const PROJECTION_REMAINING_TOKENS_L2_CONFIG_KEY = "projectionRemainingTokensL2"; /** Config key for the third remaining-token threshold that enables projection. */ const PROJECTION_REMAINING_TOKENS_L3_CONFIG_KEY = "projectionRemainingTokensL3"; /** Config key that enables best-effort projection of the complete compaction source. */ const PROJECT_COMPACTION_SOURCE_CONFIG_KEY = "projectCompactionSource"; /** Config key for the minimum number of newest tool-use turns kept unprojected. */ const KEEP_RECENT_TURNS_CONFIG_KEY = "keepRecentTurns"; /** Config key for the newest tool-use turn ratio kept unprojected in long sessions. */ const KEEP_RECENT_TURNS_PERCENT_CONFIG_KEY = "keepRecentTurnsPercent"; /** Config key for the first-level minimum combined token count eligible for projection. */ const MIN_TOOL_RESULT_TOKENS_L1_CONFIG_KEY = "minToolResultTokensL1"; /** Config key for the second-level minimum combined token count eligible for projection. */ const MIN_TOOL_RESULT_TOKENS_L2_CONFIG_KEY = "minToolResultTokensL2"; /** Config key for the third-level minimum combined token count eligible for projection. */ const MIN_TOOL_RESULT_TOKENS_L3_CONFIG_KEY = "minToolResultTokensL3"; /** Config key for tool names whose successful text results must stay visible. */ const PROJECTION_IGNORED_TOOLS_CONFIG_KEY = "projectionIgnoredTools"; /** Removed config key that now fails fast with migration guidance. */ const REMOVED_PLACEHOLDER_CONFIG_KEY = "placeholder"; /** Config key for the text used when a projected tool result has no summary. */ const OMITTED_NOTICE_CONFIG_KEY = "omittedNotice"; /** Config key for the notice used when a projected tool result includes a summary. */ const SUMMARY_NOTICE_CONFIG_KEY = "summaryNotice"; /** Config key for optional summaries generated before projecting tool results. */ const SUMMARY_CONFIG_KEY = "summary"; /** Advisor and council outputs must stay visible because they carry decision-critical guidance. */ const CONSULT_ADVISOR_TOOL_NAME = "consult_advisor"; const CONVENE_COUNCIL_TOOL_NAME = "convene_council"; /** Workflow tool results are durable model instructions outside compaction. */ const WORKFLOW_TOOL_NAME_PREFIX = "workflow_"; /** Built-in tool names whose results are excluded from projection. */ const BUILT_IN_PROJECTION_IGNORED_TOOLS = [ CONSULT_ADVISOR_TOOL_NAME, CONVENE_COUNCIL_TOOL_NAME, ] as const; /** Default first remaining-token threshold for explicit projection enablement. */ const DEFAULT_PROJECTION_REMAINING_TOKENS_L1 = 70_000; /** Default second remaining-token threshold for explicit projection enablement. */ const DEFAULT_PROJECTION_REMAINING_TOKENS_L2 = 50_000; /** Default third remaining-token threshold for explicit projection enablement. */ const DEFAULT_PROJECTION_REMAINING_TOKENS_L3 = 30_000; /** Compaction projects every eligible discarded result by default. */ const DEFAULT_PROJECT_COMPACTION_SOURCE = true; /** Default newest tool-use turns kept visible before projection. */ const DEFAULT_KEEP_RECENT_TURNS = 10; /** Default newest tool-use turn ratio kept visible in long sessions. */ const DEFAULT_KEEP_RECENT_TURNS_PERCENT = 0.2; /** Default first-level minimum token count for projecting a tool result. */ const DEFAULT_MIN_TOOL_RESULT_TOKENS_L1 = 4_000; /** Default second-level minimum token count for projecting a tool result. */ const DEFAULT_MIN_TOOL_RESULT_TOKENS_L2 = 2_000; /** Default third-level minimum token count for projecting a tool result. */ const DEFAULT_MIN_TOOL_RESULT_TOKENS_L3 = 1_000; /** Default notice for projected tool results that have no summary. */ const DEFAULT_OMITTED_NOTICE = "Result omitted. Run tool again for full result."; /** Default notice for projected tool results that include a summary. */ const DEFAULT_SUMMARY_NOTICE = "Full result omitted. Summary below. Run tool again for full result. MUST NOT rely on summary when making critical decisions."; /** Factor used to render token usage as a percentage of the context window. */ const PERCENT_FACTOR = 100; /** Fatal issue reported when projection level remaining-token thresholds are not descending. */ const PROJECTION_LEVEL_ORDER_ERROR = "projectionRemainingTokensL1 must be greater than or equal to projectionRemainingTokensL2, and projectionRemainingTokensL2 must be greater than or equal to projectionRemainingTokensL3"; /** Fatal issue reported when the removed placeholder config key is still present. */ const REMOVED_PLACEHOLDER_CONFIG_ERROR = "unsupported config key: placeholder. Use omittedNotice and summaryNotice."; /** Config keys accepted by the context projection config object. */ const CONTEXT_PROJECTION_CONFIG_KEYS = [ ENABLED_CONFIG_KEY, PROJECTION_REMAINING_TOKENS_L1_CONFIG_KEY, MIN_TOOL_RESULT_TOKENS_L1_CONFIG_KEY, PROJECTION_REMAINING_TOKENS_L2_CONFIG_KEY, MIN_TOOL_RESULT_TOKENS_L2_CONFIG_KEY, PROJECTION_REMAINING_TOKENS_L3_CONFIG_KEY, MIN_TOOL_RESULT_TOKENS_L3_CONFIG_KEY, PROJECT_COMPACTION_SOURCE_CONFIG_KEY, KEEP_RECENT_TURNS_CONFIG_KEY, KEEP_RECENT_TURNS_PERCENT_CONFIG_KEY, PROJECTION_IGNORED_TOOLS_CONFIG_KEY, OMITTED_NOTICE_CONFIG_KEY, SUMMARY_NOTICE_CONFIG_KEY, SUMMARY_CONFIG_KEY, ] as const; export type ContextProjectionConfigResult = | { readonly kind: "valid"; readonly config: ContextProjectionConfig; } | { readonly kind: "disabled" } | { readonly kind: "invalid"; readonly issue?: string; readonly fatal?: boolean; }; export type ContextProjectionSummaryConfig = ToolResultSummaryConfig; export type AppliedProjectionLevel = "L1" | "L2" | "L3"; export interface ProjectionLevel { readonly label: AppliedProjectionLevel; readonly remainingTokens: number; readonly minToolResultTokens: number; } type ProjectionLevelTuple = readonly [ ProjectionLevel, ProjectionLevel, ProjectionLevel, ]; export interface ContextProjectionConfig { readonly enabled: true; readonly projectCompactionSource: boolean; readonly projectionLevels: ProjectionLevelTuple; readonly keepRecentTurns: number; readonly keepRecentTurnsPercent: number; readonly projectionIgnoredTools: readonly string[]; readonly omittedNotice: string; readonly summaryNotice: string; readonly summary: ContextProjectionSummaryConfig; } export interface ProjectedEntryState { readonly entryId: string; readonly replacementText: string; } interface ContextProjectionStateEntryData { readonly projectedEntries: readonly ProjectedEntryState[]; readonly appliedLevel?: AppliedProjectionLevel; } export interface MappedContextEntry { readonly entry: SessionEntry; readonly message: AgentMessage; } export interface ProjectionDecision { readonly messages: AgentMessage[]; readonly newProjectedEntries: ProjectedEntryState[]; readonly savedTokens: number; readonly newSavedTokens: number; readonly savedTokensByEntryId: ReadonlyMap; readonly newSavedTokensByEntryId: ReadonlyMap; readonly changed: boolean; } interface ProjectContextMessagesOptions { readonly mappedContext: readonly MappedContextEntry[]; readonly projectedReplacementsByEntryId: ReadonlyMap; readonly replacementTextByEntryId?: ReadonlyMap; readonly config: ContextProjectionConfig; readonly loadedSkillRoots: readonly string[]; readonly cwd: string; readonly activeProjectionLevel: ProjectionLevel | undefined; } interface ProjectionSavingsEstimateOptions { readonly branchEntries: readonly SessionEntry[]; readonly cwd: string; readonly projectedReplacementsByEntryId: ReadonlyMap; readonly config: ContextProjectionConfig; readonly loadedSkillRoots?: readonly string[]; } interface PendingProjectionSavingsEstimateOptions { readonly branchEntries: readonly SessionEntry[]; readonly cwd: string; readonly config: ContextProjectionConfig; readonly loadedSkillRoots?: readonly string[]; } interface ProjectMappedContextEntryOptions { readonly entry: SessionEntry; readonly message: AgentMessage; readonly protectedEntryIds: ReadonlySet; readonly readPathsByToolCallId: ReadonlyMap; readonly loadedSkillRoots: readonly string[]; readonly ignoredTools: ReadonlySet; readonly projectedReplacementsByEntryId: ReadonlyMap; readonly replacementTextByEntryId: ReadonlyMap | undefined; readonly config: ContextProjectionConfig; readonly activeProjectionLevel: ProjectionLevel | undefined; } type ProjectMappedContextEntryResult = | { readonly kind: "unchanged"; readonly message: AgentMessage } | { readonly kind: "projected"; readonly message: AgentMessage; readonly projectedEntry: ProjectedEntryState | undefined; readonly savedTokens: number; }; /** Input needed to reconstruct advisor-visible context with recorded projection applied. */ export interface ContextProjectionReplayOptions { readonly branchEntries: readonly SessionEntry[]; readonly cwd: string; readonly loadedSkillRoots?: readonly string[]; } /** Input needed to replay recorded projection for Pi's fixed retained suffix. */ export interface RetainedContextProjectionReplayOptions extends ContextProjectionReplayOptions { readonly firstKeptEntryId: string; } interface RuntimeProjectedReplacementState { readonly branchLeafId: string | null; readonly replacements: ReadonlyMap; } const runtimeProjectedReplacementsByScope = new Map< string, RuntimeProjectedReplacementState >(); export interface ProjectedEntrySavings { readonly entryId: string; readonly replacementText: string; readonly savedTokens: number; } export interface LivePendingProjectionSavings { readonly branchLeafId: string; readonly entries: readonly [ ProjectedEntrySavings, ...ProjectedEntrySavings[], ]; } interface PendingProjectionSavingsState { readonly branchEntries: readonly ProjectedEntrySavings[]; readonly liveBatches: readonly LivePendingProjectionSavings[]; } export interface PendingProjectionSavingsEstimate { readonly savedTokens: number; readonly entries: readonly ProjectedEntrySavings[]; } const pendingProjectionSavingsByScope = new Map< string, PendingProjectionSavingsState >(); export interface ContextProjectionUsage { readonly tokens: number | null; readonly contextWindow: number; readonly percent: number | null; } /** Records entry-level token savings before the appended projection state becomes branch-visible. */ export function addPendingProjectionSavings( sessionId: string, liveSavings: LivePendingProjectionSavings, ): void { const entries = deduplicateProjectedEntrySavings(liveSavings.entries); if (entries.length === 0) { return; } const scope = getRuntimePendingProjectionScope(sessionId); const state = getPendingProjectionSavingsState(scope); pendingProjectionSavingsByScope.set(scope, { branchEntries: state.branchEntries, liveBatches: [ ...state.liveBatches, { branchLeafId: liveSavings.branchLeafId, entries: entries as [ProjectedEntrySavings, ...ProjectedEntrySavings[]], }, ], }); } interface PendingProjectionSavingsSync { readonly entries: readonly ProjectedEntrySavings[]; readonly branchEntries: readonly SessionEntry[]; } /** Replaces branch-backed savings and retains only live entries not yet controlled by later branch state. */ export function setPendingProjectionSavings( sessionId: string, { entries, branchEntries }: PendingProjectionSavingsSync, ): void { const scope = getRuntimePendingProjectionScope(sessionId); const state = getPendingProjectionSavingsState(scope); const liveBatches = state.liveBatches.flatMap((batch) => { const anchorIndex = branchEntries.findIndex( (entry) => entry.id === batch.branchLeafId, ); if (anchorIndex < 0) { return []; } const laterActions = foldProjectedReplacements( branchEntries.slice(anchorIndex + 1), ).latestActions; const remainingEntries = batch.entries.filter( (entry) => !laterActions.has(entry.entryId), ); return remainingEntries.length === 0 ? [] : [ { branchLeafId: batch.branchLeafId, entries: remainingEntries as [ ProjectedEntrySavings, ...ProjectedEntrySavings[], ], }, ]; }); const nextState = { branchEntries: deduplicateProjectedEntrySavings(entries), liveBatches, }; if ( nextState.branchEntries.length === 0 && nextState.liveBatches.length === 0 ) { pendingProjectionSavingsByScope.delete(scope); return; } pendingProjectionSavingsByScope.set(scope, nextState); } /** Clears pending token savings after provider usage catches up with the projected context. */ export function resetPendingProjectionSavings(sessionId: string): void { pendingProjectionSavingsByScope.delete( getRuntimePendingProjectionScope(sessionId), ); } /** Returns context usage adjusted by savings selected from the current active branch. */ export function getProjectionAwareContextUsage( sessionId: string, branchEntries: readonly SessionEntry[], usage: ContextProjectionUsage | undefined, ): ContextProjectionUsage | undefined { if (usage === undefined || usage.tokens === null) { return usage; } const pendingSavings = getPendingProjectionSavingsTotal( pendingProjectionSavingsByScope.get( getRuntimePendingProjectionScope(sessionId), ), branchEntries, ); if (pendingSavings <= 0) { return usage; } const tokens = Math.max(0, usage.tokens - pendingSavings); return { ...usage, tokens, percent: usage.contextWindow > 0 ? (tokens / usage.contextWindow) * PERCENT_FACTOR : null, }; } /** Returns existing pending savings state or the empty state for one runtime scope. */ function getPendingProjectionSavingsState( scope: string, ): PendingProjectionSavingsState { return ( pendingProjectionSavingsByScope.get(scope) ?? { branchEntries: [], liveBatches: [], } ); } interface LiveSavingsVisibility { readonly effective: boolean; readonly projectionIndex: number | undefined; } /** Sums savings selected by canonical visibility and raw append-order boundaries. */ function getPendingProjectionSavingsTotal( state: PendingProjectionSavingsState | undefined, branchEntries: readonly SessionEntry[], ): number { if (state === undefined) { return 0; } const selection = selectUsageProjectionReplacements(branchEntries); const selectedSavings = collectSelectedLiveSavings( state.liveBatches, branchEntries, selection.responseIndex, selection.visibleEntryIds, ); for (const entry of state.branchEntries) { if ( entry.savedTokens > 0 && selection.visibleEntryIds.has(entry.entryId) && selection.replacements.get(entry.entryId) === entry.replacementText ) { selectedSavings.set(entry.entryId, entry.savedTokens); } } return [...selectedSavings.values()].reduce( (total, savedTokens) => total + savedTokens, 0, ); } /** Collects effective entry-level live savings that the selected native estimate does not include. */ function collectSelectedLiveSavings( batches: readonly LivePendingProjectionSavings[], branchEntries: readonly SessionEntry[], responseIndex: number | undefined, visibleEntryIds: ReadonlySet, ): Map { const selectedSavings = new Map(); for (const batch of batches) { const anchorIndex = branchEntries.findIndex( (entry) => entry.id === batch.branchLeafId, ); if (anchorIndex < 0) { continue; } const laterState = foldProjectedReplacements( branchEntries.slice(anchorIndex + 1), ); for (const entry of batch.entries) { if (!visibleEntryIds.has(entry.entryId)) { continue; } const visibility = resolveLiveSavingsVisibility( entry, laterState, anchorIndex, ); if ( visibility.effective && entry.savedTokens > 0 && isLiveSavingsAfterResponse(visibility, anchorIndex, responseIndex) ) { selectedSavings.set(entry.entryId, entry.savedTokens); } } } return selectedSavings; } /** Resolves whether later branch state preserves, persists, replaces, or invalidates one live entry. */ function resolveLiveSavingsVisibility( entry: ProjectedEntrySavings, laterState: ReturnType, anchorIndex: number, ): LiveSavingsVisibility { const laterAction = laterState.latestActions.get(entry.entryId); if ( laterAction?.kind === "invalidated" || (laterAction?.kind === "projected" && laterAction.replacementText !== entry.replacementText) ) { return { effective: false, projectionIndex: undefined }; } return { effective: true, projectionIndex: laterAction?.kind === "projected" ? anchorIndex + 1 + laterAction.entryIndex : undefined, }; } /** Returns whether one effective live projection was created after the selected response. */ function isLiveSavingsAfterResponse( visibility: LiveSavingsVisibility, anchorIndex: number, responseIndex: number | undefined, ): boolean { if (responseIndex === undefined) { return true; } return visibility.projectionIndex === undefined ? anchorIndex >= responseIndex : visibility.projectionIndex > responseIndex; } /** Keeps only the latest positive savings value for each projected target. */ function deduplicateProjectedEntrySavings( entries: readonly ProjectedEntrySavings[], ): ProjectedEntrySavings[] { const byEntryId = new Map(); for (const entry of entries) { if (entry.savedTokens > 0) { byEntryId.set(entry.entryId, entry); } } return [...byEntryId.values()]; } /** Estimates all effective branch-backed projection savings for later branch-aware selection. */ export function estimateEffectiveProjectionSavings({ branchEntries, cwd, config, loadedSkillRoots = [], }: PendingProjectionSavingsEstimateOptions): PendingProjectionSavingsEstimate { return estimateProjectionSavings({ branchEntries, cwd, config, loadedSkillRoots, projectedReplacementsByEntryId: collectProjectedReplacements(branchEntries), }); } /** Reads and validates context-projection config while absent config keeps projection disabled. */ export async function readContextProjectionConfig(): Promise { const configFile = await readExtensionConfigFile({ extensionDir: CONTEXT_PROJECTION_EXTENSION_DIR, legacyConfigFileName: CONTEXT_PROJECTION_LEGACY_CONFIG_FILE, }); if (configFile.kind === "missing") { return { kind: "disabled" }; } if (configFile.kind === "read-error") { return { kind: "invalid" }; } try { const config: unknown = JSON.parse(configFile.file.content); const projectionConfig = parseContextProjectionConfig(config); if (projectionConfig.kind !== "valid") { return projectionConfig; } const customCompactionConfig = await readCustomCompactionConfig(); if (customCompactionConfig.kind === "disabled") { return { kind: "invalid", issue: "custom-compaction must be enabled when context-projection is enabled", fatal: true, }; } if (customCompactionConfig.kind === "invalid") { return { kind: "invalid", issue: `custom-compaction configuration is invalid: ${customCompactionConfig.issue}`, fatal: true, }; } return projectionConfig; } catch (error) { if ( error instanceof Error && (error.message.startsWith("summary.") || error.message === PROJECTION_LEVEL_ORDER_ERROR) ) { return { kind: "invalid", issue: error.message, fatal: true }; } return { kind: "invalid" }; } } /** Parses the config file into the complete projection settings contract. */ function parseContextProjectionConfig( config: unknown, ): ContextProjectionConfigResult { if (!isRecord(config)) { return { kind: "invalid" }; } const unsupportedKeyResult = validateContextProjectionConfigKeys(config); if (unsupportedKeyResult !== undefined) { return unsupportedKeyResult; } const enabled = config[ENABLED_CONFIG_KEY]; if (enabled !== undefined && typeof enabled !== "boolean") { return { kind: "invalid" }; } const projectionLevels = parseProjectionLevels(config); if (projectionLevels === undefined) { return { kind: "invalid" }; } if (enabled !== true) { return { kind: "disabled" }; } const projectCompactionSource = config[PROJECT_COMPACTION_SOURCE_CONFIG_KEY] ?? DEFAULT_PROJECT_COMPACTION_SOURCE; const keepRecentTurns = config[KEEP_RECENT_TURNS_CONFIG_KEY] ?? DEFAULT_KEEP_RECENT_TURNS; const keepRecentTurnsPercent = config[KEEP_RECENT_TURNS_PERCENT_CONFIG_KEY] ?? DEFAULT_KEEP_RECENT_TURNS_PERCENT; const projectionIgnoredTools = config[PROJECTION_IGNORED_TOOLS_CONFIG_KEY] ?? []; const omittedNotice = config[OMITTED_NOTICE_CONFIG_KEY] ?? DEFAULT_OMITTED_NOTICE; const summaryNotice = config[SUMMARY_NOTICE_CONFIG_KEY] ?? DEFAULT_SUMMARY_NOTICE; const summary = parseContextProjectionSummaryConfig( config[SUMMARY_CONFIG_KEY], ); if ( typeof projectCompactionSource !== "boolean" || !isNonNegativeInteger(keepRecentTurns) || !isPercentNumber(keepRecentTurnsPercent) || !isUniqueNonEmptyStringArray(projectionIgnoredTools) || !isNonEmptyString(omittedNotice) || !isNonEmptyString(summaryNotice) || summary === undefined ) { return { kind: "invalid" }; } return { kind: "valid", config: { enabled: true, projectCompactionSource, projectionLevels, keepRecentTurns, keepRecentTurnsPercent, projectionIgnoredTools, omittedNotice, summaryNotice, summary, }, }; } /** Rejects unknown keys and preserves migration guidance for removed placeholders. */ function validateContextProjectionConfigKeys( config: Record, ): ContextProjectionConfigResult | undefined { const unsupportedKey = Object.keys(config).find( (key) => !CONTEXT_PROJECTION_CONFIG_KEYS.includes( key as (typeof CONTEXT_PROJECTION_CONFIG_KEYS)[number], ), ); if (unsupportedKey === REMOVED_PLACEHOLDER_CONFIG_KEY) { return { kind: "invalid", issue: REMOVED_PLACEHOLDER_CONFIG_ERROR, fatal: true, }; } return unsupportedKey === undefined ? undefined : { kind: "invalid" }; } /** Parses and normalizes the three projection trigger levels. */ function parseProjectionLevels( config: Record, ): ProjectionLevelTuple | undefined { const remainingTokensL1 = parseNonNegativeIntegerConfigValue( config[PROJECTION_REMAINING_TOKENS_L1_CONFIG_KEY], DEFAULT_PROJECTION_REMAINING_TOKENS_L1, ); const minToolResultTokensL1 = parseNonNegativeIntegerConfigValue( config[MIN_TOOL_RESULT_TOKENS_L1_CONFIG_KEY], DEFAULT_MIN_TOOL_RESULT_TOKENS_L1, ); const remainingTokensL2 = parseNonNegativeIntegerConfigValue( config[PROJECTION_REMAINING_TOKENS_L2_CONFIG_KEY], DEFAULT_PROJECTION_REMAINING_TOKENS_L2, ); const minToolResultTokensL2 = parseNonNegativeIntegerConfigValue( config[MIN_TOOL_RESULT_TOKENS_L2_CONFIG_KEY], DEFAULT_MIN_TOOL_RESULT_TOKENS_L2, ); const remainingTokensL3 = parseNonNegativeIntegerConfigValue( config[PROJECTION_REMAINING_TOKENS_L3_CONFIG_KEY], DEFAULT_PROJECTION_REMAINING_TOKENS_L3, ); const minToolResultTokensL3 = parseNonNegativeIntegerConfigValue( config[MIN_TOOL_RESULT_TOKENS_L3_CONFIG_KEY], DEFAULT_MIN_TOOL_RESULT_TOKENS_L3, ); if ( remainingTokensL1 === undefined || minToolResultTokensL1 === undefined || remainingTokensL2 === undefined || minToolResultTokensL2 === undefined || remainingTokensL3 === undefined || minToolResultTokensL3 === undefined ) { return undefined; } if ( remainingTokensL1 < remainingTokensL2 || remainingTokensL2 < remainingTokensL3 ) { throw new Error(PROJECTION_LEVEL_ORDER_ERROR); } return normalizeEqualProjectionLevelThresholds([ { label: "L1", remainingTokens: remainingTokensL1, minToolResultTokens: minToolResultTokensL1, }, { label: "L2", remainingTokens: remainingTokensL2, minToolResultTokens: minToolResultTokensL2, }, { label: "L3", remainingTokens: remainingTokensL3, minToolResultTokens: minToolResultTokensL3, }, ]); } /** Parses an optional non-negative integer config value after applying its default. */ function parseNonNegativeIntegerConfigValue( value: unknown, defaultValue: number, ): number | undefined { const resolvedValue = value ?? defaultValue; return isNonNegativeInteger(resolvedValue) ? resolvedValue : undefined; } /** Applies the lowest tool-result threshold to every level with the same remaining-token threshold. */ function normalizeEqualProjectionLevelThresholds( projectionLevels: ProjectionLevelTuple, ): ProjectionLevelTuple { const [level1, level2, level3] = projectionLevels; return [ normalizeProjectionLevelThreshold(level1, projectionLevels), normalizeProjectionLevelThreshold(level2, projectionLevels), normalizeProjectionLevelThreshold(level3, projectionLevels), ]; } /** Applies the lowest matching tool-result threshold to one projection level. */ function normalizeProjectionLevelThreshold( level: ProjectionLevel, projectionLevels: ProjectionLevelTuple, ): ProjectionLevel { const minToolResultTokens = Math.min( ...projectionLevels .filter( (candidate) => candidate.remainingTokens === level.remainingTokens, ) .map((candidate) => candidate.minToolResultTokens), ); return { ...level, minToolResultTokens }; } /** Parses optional summary config while keeping summary disabled by default. */ function parseContextProjectionSummaryConfig( config: unknown, ): ContextProjectionSummaryConfig | undefined { return parseToolResultSummaryConfig(config, { defaultEnabled: false }); } /** Replays recorded projection only for entries in Pi's fixed retained suffix. */ export async function replayRetainedContextProjection({ branchEntries, firstKeptEntryId, cwd, loadedSkillRoots = [], }: RetainedContextProjectionReplayOptions): Promise { const firstKeptIndex = branchEntries.findIndex( (entry) => entry.id === firstKeptEntryId, ); if (firstKeptIndex < 0) { return []; } const mappedContext = buildContextEntryMapping( branchEntries.slice(firstKeptIndex), ); const originalMessages = mappedContext.map(({ message }) => message); const config = await readContextProjectionConfig(); if (config.kind !== "valid") { return originalMessages; } const projectedReplacementsByEntryId = collectEffectiveProjectedReplacements( branchEntries, cwd, ); if (projectedReplacementsByEntryId.size === 0) { return originalMessages; } const decision = projectContextMessages({ mappedContext, projectedReplacementsByEntryId, config: config.config, loadedSkillRoots, cwd, activeProjectionLevel: undefined, }); return decision.changed ? decision.messages : originalMessages; } /** Maps one saved branch using only projection replacements persisted in that branch. */ export function replayPersistedContextProjection( branchEntries: readonly SessionEntry[], ): AgentMessage[] { const projectedReplacementsByEntryId = collectProjectedReplacements(branchEntries); return buildContextEntryMapping(branchEntries).map(({ entry, message }) => { const replacementText = projectedReplacementsByEntryId.get(entry.id); return replacementText === undefined || !isSuccessfulTextToolResult(message) ? message : createProjectedToolResult(message, replacementText); }); } /** Returns branch context with persisted projection state applied when projection is active. */ export async function replayContextProjection({ branchEntries, cwd, loadedSkillRoots = [], }: ContextProjectionReplayOptions): Promise { const mappedContext = buildContextEntryMapping(branchEntries); const originalMessages = mappedContext.map(({ message }) => message); const config = await readContextProjectionConfig(); if (config.kind !== "valid") { return originalMessages; } const projectedReplacementsByEntryId = collectEffectiveProjectedReplacements( branchEntries, cwd, ); if (projectedReplacementsByEntryId.size === 0) { return originalMessages; } const decision = projectContextMessages({ mappedContext, projectedReplacementsByEntryId, config: config.config, loadedSkillRoots, cwd, activeProjectionLevel: undefined, }); return decision.changed ? decision.messages : originalMessages; } /** Collects projected entries from extension-owned custom entries on the active branch only. */ export function collectProjectedReplacements( branchEntries: readonly SessionEntry[], ): Map { return foldProjectedReplacements(branchEntries).replacements; } /** Reconciles active-branch order with replacements not visible in the branch yet. */ export function collectEffectiveProjectedReplacements( branchEntries: readonly SessionEntry[], cwd: string, ): Map { const branchState = foldProjectedReplacements(branchEntries); const runtimeState = runtimeProjectedReplacementsByScope.get( getRuntimeProjectionScope(cwd), ); if (runtimeState === undefined || runtimeState.branchLeafId === null) { return branchState.replacements; } const anchorIndex = branchEntries.findIndex( (entry) => entry.id === runtimeState.branchLeafId, ); if (anchorIndex < 0) { return branchState.replacements; } const laterState = foldProjectedReplacements( branchEntries.slice(anchorIndex + 1), ); for (const [entryId, replacementText] of runtimeState.replacements) { if (laterState.latestActions.has(entryId)) { continue; } branchState.replacements.set(entryId, replacementText); } return branchState.replacements; } /** Restores the deepest projection threshold recorded after the latest compaction. */ export function collectAppliedProjectionLevel( branchEntries: readonly SessionEntry[], ): AppliedProjectionLevel | undefined { let appliedLevel: AppliedProjectionLevel | undefined; for (const entry of branchEntries) { if (entry.type === "compaction") { appliedLevel = undefined; continue; } if ( entry.type !== "custom" || entry.customType !== CONTEXT_PROJECTION_CUSTOM_TYPE || !isProjectionStateEntryData(entry.data) || entry.data.appliedLevel === undefined ) { continue; } if ( appliedLevel === undefined || isProjectionLevelDeeper(entry.data.appliedLevel, appliedLevel) ) { appliedLevel = entry.data.appliedLevel; } } return appliedLevel; } interface UsageProjectionSelection { readonly replacements: Map; readonly responseIndex: number | undefined; readonly visibleEntryIds: ReadonlySet; } /** Selects all active savings or only post-response savings from canonical visibility and raw append order. */ function selectUsageProjectionReplacements( branchEntries: readonly SessionEntry[], ): UsageProjectionSelection { const branchIndexById = new Map( branchEntries.map((entry, index) => [entry.id, index] as const), ); const mappedContext = buildContextEntryMapping(branchEntries); const visibleEntryIds = new Set(mappedContext.map(({ entry }) => entry.id)); let responseIndex: number | undefined; for (const { entry, message } of mappedContext) { if (!hasValidAssistantContextUsage(message)) { continue; } const index = branchIndexById.get(entry.id); if ( index !== undefined && (responseIndex === undefined || index > responseIndex) ) { responseIndex = index; } } const latestBoundaryIndex = findLastEntryIndex( branchEntries, (entry) => entry.type === "context_edit" || entry.type === "compaction", ); if (responseIndex === undefined || responseIndex <= latestBoundaryIndex) { return { replacements: collectProjectedReplacements(branchEntries), responseIndex: undefined, visibleEntryIds, }; } return { replacements: collectProjectedReplacementsFromEntries( branchEntries.slice(responseIndex + 1), ), responseIndex, visibleEntryIds, }; } /** Collects projection replacement text from extension-owned custom state entries. */ function collectProjectedReplacementsFromEntries( entries: readonly SessionEntry[], ): Map { return foldProjectedReplacements(entries).replacements; } type ProjectedReplacementAction = | { readonly kind: "invalidated" } | { readonly kind: "projected"; readonly entryIndex: number; readonly replacementText: string; }; /** Folds repository replacements and Pi edits in raw active-branch append order. */ function foldProjectedReplacements(entries: readonly SessionEntry[]): { readonly replacements: Map; readonly latestActions: Map; } { const replacements = new Map(); const latestActions = new Map(); for (const [entryIndex, entry] of entries.entries()) { if (entry.type === "context_edit") { replacements.delete(entry.targetId); latestActions.set(entry.targetId, { kind: "invalidated" }); continue; } if ( entry.type !== "custom" || entry.customType !== CONTEXT_PROJECTION_CUSTOM_TYPE || !isProjectionStateEntryData(entry.data) ) { continue; } for (const projectedEntry of entry.data.projectedEntries) { replacements.set(projectedEntry.entryId, projectedEntry.replacementText); latestActions.set(projectedEntry.entryId, { kind: "projected", entryIndex, replacementText: projectedEntry.replacementText, }); } } return { replacements, latestActions }; } /** Returns true when an assistant message contains provider usage that reflects its request. */ function hasValidAssistantContextUsage(message: AgentMessage): boolean { if (message.role !== "assistant") { return false; } return ( message.stopReason !== "aborted" && message.stopReason !== "error" && estimateAssistantUsageTokens(message.usage) > 0 ); } /** Returns the provider-reported context size for assistant usage objects. */ function estimateAssistantUsageTokens( usage: Extract["usage"], ): number { return ( usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite ); } /** Publishes active in-memory projection state for other extension entry points in the same process. */ export function publishRuntimeProjectedReplacements( cwd: string, projectedReplacementsByEntryId: ReadonlyMap, branchLeafId: string | null, ): void { runtimeProjectedReplacementsByScope.set(getRuntimeProjectionScope(cwd), { branchLeafId, replacements: new Map(projectedReplacementsByEntryId), }); } /** Estimates current projected token savings from branch-local projection state. */ export function estimateProjectedSavedTokens( options: ProjectionSavingsEstimateOptions, ): number { return estimateProjectionSavings(options).savedTokens; } /** Estimates entry-level values without duplicating the projection token calculation. */ function estimateProjectionSavings({ branchEntries, cwd, projectedReplacementsByEntryId, config, loadedSkillRoots = [], }: ProjectionSavingsEstimateOptions): PendingProjectionSavingsEstimate { if (projectedReplacementsByEntryId.size === 0) { return { savedTokens: 0, entries: [] }; } const decision = projectContextMessages({ mappedContext: buildContextEntryMapping(branchEntries), projectedReplacementsByEntryId, config, loadedSkillRoots, cwd, activeProjectionLevel: undefined, }); const entries = [...decision.savedTokensByEntryId].flatMap( ([entryId, savedTokens]) => { const replacementText = projectedReplacementsByEntryId.get(entryId); return replacementText === undefined || savedTokens <= 0 ? [] : [{ entryId, replacementText, savedTokens }]; }, ); return { savedTokens: entries.reduce((total, entry) => total + entry.savedTokens, 0), entries, }; } /** Maps provider-context messages back to active branch entries without treating persisted retry errors as provider context. */ export function mapEventMessagesToBranchEntries( eventMessages: readonly AgentMessage[], branchEntries: readonly SessionEntry[], ): MappedContextEntry[] | undefined { const mappedEntries = buildContextEntryMapping(branchEntries).filter( ({ message }) => message.role !== "system", ); const eventMappedEntries: MappedContextEntry[] = []; let eventIndex = 0; for (const mappedEntry of mappedEntries) { const eventMessage = eventMessages[eventIndex]; if ( eventMessage !== undefined && isMatchingContextMessage(mappedEntry, eventMessage) ) { eventMappedEntries.push({ entry: mappedEntry.entry, message: eventMessage, }); eventIndex += 1; continue; } if (isPersistedProviderError(mappedEntry)) { continue; } return undefined; } return eventIndex === eventMessages.length ? eventMappedEntries : undefined; } /** Matches live custom messages without the timestamp that Pi regenerates during persistence. */ function isMatchingContextMessage( mappedEntry: MappedContextEntry, eventMessage: AgentMessage, ): boolean { if (isDeepStrictEqual(mappedEntry.message, eventMessage)) { return true; } if ( mappedEntry.entry.type !== "custom_message" || mappedEntry.message.role !== "custom" || eventMessage.role !== "custom" ) { return false; } const { timestamp: _persistedTimestamp, ...persistedMessage } = mappedEntry.message; const { timestamp: _liveTimestamp, ...liveMessage } = eventMessage; return isDeepStrictEqual(persistedMessage, liveMessage); } /** Identifies provider errors that Pi keeps in session history but removes from agent state before an automatic retry. */ function isPersistedProviderError(entry: MappedContextEntry): boolean { return ( entry.entry.type === "message" && entry.message.role === "assistant" && entry.message.stopReason === "error" ); } /** Builds Pi's canonical branch message sequence while keeping each source entry beside its messages. */ export function buildContextEntryMapping( branchEntries: readonly SessionEntry[], ): MappedContextEntry[] { return buildSessionProjection([...branchEntries]).entries.flatMap( ({ sourceEntry, messages }) => messages.map((message) => ({ entry: sourceEntry, message })), ); } /** Returns projected provider-context messages and newly persisted projection state. */ export function projectContextMessages({ mappedContext, projectedReplacementsByEntryId, replacementTextByEntryId, config, loadedSkillRoots, cwd, activeProjectionLevel, }: ProjectContextMessagesOptions): ProjectionDecision { const protectedEntryIds = collectProtectedEntryIds(mappedContext, config); const readPathsByToolCallId = collectReadPathsByToolCallId( mappedContext, cwd, ); const ignoredTools = getProjectionIgnoredTools(config); const newProjectedEntries: ProjectedEntryState[] = []; const savedTokensByEntryId = new Map(); const newSavedTokensByEntryId = new Map(); let savedTokens = 0; let newSavedTokens = 0; let changed = false; const messages = mappedContext.map(({ entry, message }) => { const result = projectMappedContextEntry({ entry, message, protectedEntryIds, readPathsByToolCallId, loadedSkillRoots, ignoredTools, projectedReplacementsByEntryId, replacementTextByEntryId, config, activeProjectionLevel, }); if (result.kind === "unchanged") { return result.message; } savedTokens += result.savedTokens; savedTokensByEntryId.set( entry.id, (savedTokensByEntryId.get(entry.id) ?? 0) + result.savedTokens, ); changed = true; if (result.projectedEntry !== undefined) { newSavedTokens += result.savedTokens; newSavedTokensByEntryId.set( entry.id, (newSavedTokensByEntryId.get(entry.id) ?? 0) + result.savedTokens, ); newProjectedEntries.push(result.projectedEntry); } return result.message; }); return { messages, newProjectedEntries, savedTokens, newSavedTokens, savedTokensByEntryId, newSavedTokensByEntryId, changed, }; } /** Projects one mapped context entry or returns it unchanged when projection rules reject it. */ function projectMappedContextEntry({ entry, message, protectedEntryIds, readPathsByToolCallId, loadedSkillRoots, ignoredTools, projectedReplacementsByEntryId, replacementTextByEntryId, config, activeProjectionLevel, }: ProjectMappedContextEntryOptions): ProjectMappedContextEntryResult { if (entry.type !== "message" || !isSuccessfulTextToolResult(message)) { return { kind: "unchanged", message }; } if ( shouldKeepToolResultVisible( message, readPathsByToolCallId, loadedSkillRoots, ignoredTools, ) ) { return { kind: "unchanged", message }; } const alreadyProjected = projectedReplacementsByEntryId.has(entry.id); const newlyEligible = activeProjectionLevel !== undefined && !protectedEntryIds.has(entry.id) && countProjectionTextTokens(getTextToolResultText(message)) >= activeProjectionLevel.minToolResultTokens; if (!alreadyProjected && !newlyEligible) { return { kind: "unchanged", message }; } const replacementText = alreadyProjected ? (projectedReplacementsByEntryId.get(entry.id) ?? config.omittedNotice) : (replacementTextByEntryId?.get(entry.id) ?? config.omittedNotice); return { kind: "projected", message: createProjectedToolResult(message, replacementText), projectedEntry: alreadyProjected ? undefined : { entryId: entry.id, replacementText }, savedTokens: calculateProjectedTokenSavings( getTextToolResultText(message), replacementText, ), }; } /** Replaces one successful tool result while preserving its correlation fields. */ function createProjectedToolResult( message: Extract, replacementText: string, ): Extract { return { ...message, content: [{ type: "text" as const, text: replacementText }], }; } /** Collects loaded skill root directories from the prompt options available before an agent turn. */ export function collectLoadedSkillRoots( event: BeforeAgentStartEvent, ): readonly string[] { return ( event.systemPromptOptions?.skills?.map((skill) => resolve(skill.baseDir)) ?? [] ); } /** Returns the approximate token count removed from provider context. */ export function estimateSavedTokens(savedTokens: number): number { return savedTokens; } /** Collects resolved read paths by tool call ID so matching tool results can be classified. */ function collectReadPathsByToolCallId( mappedContext: readonly MappedContextEntry[], cwd: string, ): ReadonlyMap { const readPathsByToolCallId = new Map(); for (const { message } of mappedContext) { if (message.role !== "assistant" || !Array.isArray(message.content)) { continue; } for (const contentBlock of message.content) { if (contentBlock.type !== "toolCall" || contentBlock.name !== "read") { continue; } const readPath = getReadToolCallPath(contentBlock.arguments); if (readPath !== undefined) { readPathsByToolCallId.set( contentBlock.id, resolveReadInputPath(readPath, cwd), ); } } } return readPathsByToolCallId; } /** Returns true when a read result belongs to a loaded skill root and must stay visible. */ function isLoadedSkillReadResult( message: Extract, readPathsByToolCallId: ReadonlyMap, loadedSkillRoots: readonly string[], ): boolean { if (message.toolName !== "read") { return false; } const readPath = readPathsByToolCallId.get(message.toolCallId); return ( readPath !== undefined && loadedSkillRoots.some((skillRoot) => isPathInsideOrEqual(readPath, skillRoot), ) ); } /** Returns true when projection must not hide this successful text tool result. */ function shouldKeepToolResultVisible( message: Extract, readPathsByToolCallId: ReadonlyMap, loadedSkillRoots: readonly string[], ignoredTools: ReadonlySet, ): boolean { return ( message.toolName.startsWith(WORKFLOW_TOOL_NAME_PREFIX) || ignoredTools.has(message.toolName) || isLoadedSkillReadResult(message, readPathsByToolCallId, loadedSkillRoots) ); } /** Returns tool result entry IDs from the newest assistant tool-use turns protected from first-time projection. */ function collectProtectedEntryIds( mappedContext: readonly MappedContextEntry[], config: ContextProjectionConfig, ): Set { let currentToolUseTurn: | { readonly ordinal: number; readonly toolCallIds: ReadonlySet } | undefined; let toolUseTurnCount = 0; const toolResultTurns = new Map(); for (const { entry, message } of mappedContext) { const toolCallIds = collectAssistantToolCallIds(message); if (toolCallIds.size > 0) { currentToolUseTurn = { ordinal: toolUseTurnCount, toolCallIds, }; toolUseTurnCount += 1; continue; } if (message.role !== "toolResult") { currentToolUseTurn = undefined; continue; } if ( entry.type === "message" && currentToolUseTurn !== undefined && currentToolUseTurn.toolCallIds.has(message.toolCallId) ) { toolResultTurns.set(entry.id, currentToolUseTurn.ordinal); } } const effectiveKeepRecentTurns = getEffectiveKeepRecentTurns( toolUseTurnCount, config, ); if (effectiveKeepRecentTurns === 0) { return new Set(); } const firstProtectedTurn = Math.max( 0, toolUseTurnCount - effectiveKeepRecentTurns, ); const protectedEntryIds = new Set(); for (const [entryId, turnOrdinal] of toolResultTurns) { if (turnOrdinal >= firstProtectedTurn) { protectedEntryIds.add(entryId); } } return protectedEntryIds; } /** Returns the recent-turn protection window from fixed minimum and session-relative ratio. */ function getEffectiveKeepRecentTurns( toolUseTurnCount: number, config: ContextProjectionConfig, ): number { return Math.max( config.keepRecentTurns, Math.ceil(toolUseTurnCount * config.keepRecentTurnsPercent), ); } /** Collects tool-call IDs when an assistant message starts a tool-use turn. */ function collectAssistantToolCallIds(message: AgentMessage): Set { const toolCallIds = new Set(); if (message.role !== "assistant" || !Array.isArray(message.content)) { return toolCallIds; } for (const contentBlock of message.content) { if (contentBlock.type === "toolCall") { toolCallIds.add(contentBlock.id); } } return toolCallIds; } /** Returns true when the message is a successful tool result that contains only text blocks. */ function isSuccessfulTextToolResult( message: AgentMessage, ): message is Extract { return ( message.role === "toolResult" && message.isError !== true && Array.isArray(message.content) && message.content.every((contentBlock) => contentBlock.type === "text") ); } /** Returns the combined text content of a text-only tool result. */ function getTextToolResultText( message: Extract, ): string { return message.content .map((contentBlock) => contentBlock.type === "text" ? contentBlock.text : "", ) .join(""); } /** Returns true when custom entry data matches the projection state contract. */ function isProjectionStateEntryData( data: unknown, ): data is ContextProjectionStateEntryData { if (!isRecord(data)) { return false; } const projectedEntries = data["projectedEntries"]; const appliedLevel = data["appliedLevel"]; return ( Array.isArray(projectedEntries) && projectedEntries.every(isProjectedEntryState) && (appliedLevel === undefined || isAppliedProjectionLevel(appliedLevel)) ); } /** Returns true for the closed projection threshold labels stored in session state. */ function isAppliedProjectionLevel( value: unknown, ): value is AppliedProjectionLevel { return value === "L1" || value === "L2" || value === "L3"; } const PROJECTION_LEVEL_ORDER: readonly AppliedProjectionLevel[] = [ "L1", "L2", "L3", ]; /** Returns whether one threshold is deeper than a previously applied threshold. */ export function isProjectionLevelDeeper( candidate: AppliedProjectionLevel, applied: AppliedProjectionLevel, ): boolean { return ( PROJECTION_LEVEL_ORDER.indexOf(candidate) > PROJECTION_LEVEL_ORDER.indexOf(applied) ); } /** Returns true when a custom-entry item identifies one projected entry and its stable replacement text. */ function isProjectedEntryState(value: unknown): value is ProjectedEntryState { return ( isRecord(value) && isNonEmptyString(value["entryId"]) && isNonEmptyString(value["replacementText"]) ); } /** Finds the last index that satisfies a predicate without relying on newer runtime APIs. */ function findLastEntryIndex( values: readonly T[], predicate: (value: T) => boolean, ): number { for (let index = values.length - 1; index >= 0; index -= 1) { const value = values[index]; if (value !== undefined && predicate(value)) { return index; } } return -1; } /** Returns true when the value is a non-negative integer. */ function isNonNegativeInteger(value: unknown): value is number { return typeof value === "number" && Number.isInteger(value) && value >= 0; } /** Returns true when the value is a ratio from zero to one. */ function isPercentNumber(value: unknown): value is number { return typeof value === "number" && value >= 0 && value <= 1; } /** Returns true when a value is a non-empty string after whitespace is ignored. */ function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim() !== ""; } /** Returns true when a value is a duplicate-free list of non-empty tool names. */ function isUniqueNonEmptyStringArray( value: unknown, ): value is readonly string[] { if (!Array.isArray(value)) { return false; } const seenValues = new Set(); for (const item of value) { if (typeof item !== "string" || item.trim() === "") { return false; } if (seenValues.has(item)) { return false; } seenValues.add(item); } return true; } /** Returns configured and built-in tool names whose results must stay visible. */ function getProjectionIgnoredTools( config: ContextProjectionConfig, ): Set { return new Set([ ...BUILT_IN_PROJECTION_IGNORED_TOOLS, ...config.projectionIgnoredTools, ]); } function getRuntimeProjectionScope(cwd: string): string { return `${getAgentDir()}\0${cwd}`; } function getRuntimePendingProjectionScope(sessionId: string): string { return `${getAgentDir()}\0${sessionId}`; } /** Returns true when a runtime value is a non-array object. */ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** Returns the token count removed after the original content is replaced by projection text. */ function calculateProjectedTokenSavings( originalText: string, replacementText: string, ): number { return Math.max( 0, countProjectionTextTokens(originalText) - countProjectionTextTokens(replacementText), ); }