import { isAbsolute, relative, resolve } from "node:path"; import { isDeepStrictEqual } from "node:util"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { type BeforeAgentStartEvent, 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 { expandHomePath } from "./path-expansion"; 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 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; } const runtimeProjectedReplacementsByScope = new Map< string, Map >(); interface PendingProjectionSavingsBatch { readonly branchLeafId: string; readonly entryIds: readonly [string, ...string[]]; readonly savedTokens: number; } interface PendingProjectionSavingsState { readonly branchSavedTokens: number; readonly liveBatches: readonly PendingProjectionSavingsBatch[]; } export interface PendingProjectionSavingsEstimate { readonly savedTokens: number; readonly entryIds: readonly string[]; } export interface LivePendingProjectionSavings { readonly branchLeafId: string; readonly entryIds: readonly [string, ...string[]]; } const pendingProjectionSavingsByScope = new Map< string, PendingProjectionSavingsState >(); export interface ContextProjectionUsage { readonly tokens: number | null; readonly contextWindow: number; readonly percent: number | null; } /** Records token savings that have not been confirmed by a later successful provider usage. */ export function addPendingProjectionSavings( sessionId: string, savedTokens: number, liveSavings: LivePendingProjectionSavings, ): void { if (savedTokens <= 0) { return; } const scope = getRuntimePendingProjectionScope(sessionId); const state = getPendingProjectionSavingsState(scope); pendingProjectionSavingsByScope.set(scope, { branchSavedTokens: state.branchSavedTokens, liveBatches: [ ...state.liveBatches, { branchLeafId: liveSavings.branchLeafId, entryIds: [...new Set(liveSavings.entryIds)] as [string, ...string[]], savedTokens, }, ], }); } /** Replaces branch-backed pending savings while preserving live projections that are not branch-visible yet. */ export function setPendingProjectionSavings( sessionId: string, savedTokens: number, entryIds: readonly string[], activeBranchEntryIds: ReadonlySet, ): void { const scope = getRuntimePendingProjectionScope(sessionId); const state = getPendingProjectionSavingsState(scope); const branchEntryIds = new Set(entryIds); const liveBatches = state.liveBatches.filter((batch) => { if (!activeBranchEntryIds.has(batch.branchLeafId)) { return false; } return !batch.entryIds.every((entryId) => branchEntryIds.has(entryId)); }); const nextState = { branchSavedTokens: Math.max(0, savedTokens), liveBatches, }; if (getPendingProjectionSavingsTotal(nextState) <= 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 projection savings not yet reflected by provider usage. */ export function getProjectionAwareContextUsage( sessionId: string, usage: ContextProjectionUsage | undefined, ): ContextProjectionUsage | undefined { if (usage === undefined || usage.tokens === null) { return usage; } const pendingSavings = getPendingProjectionSavingsTotal( pendingProjectionSavingsByScope.get( getRuntimePendingProjectionScope(sessionId), ), ); 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) ?? { branchSavedTokens: 0, liveBatches: [], } ); } /** Sums branch-backed and live savings that provider usage has not confirmed yet. */ function getPendingProjectionSavingsTotal( state: PendingProjectionSavingsState | undefined, ): number { if (state === undefined) { return 0; } return ( state.branchSavedTokens + state.liveBatches.reduce((total, batch) => total + batch.savedTokens, 0) ); } /** Estimates projected savings that are newer than the latest successful provider usage. */ export function estimatePendingProjectionSavings({ branchEntries, cwd, config, loadedSkillRoots = [], }: PendingProjectionSavingsEstimateOptions): PendingProjectionSavingsEstimate { const pendingReplacements = collectPendingProjectedReplacements(branchEntries); if (pendingReplacements.size === 0) { return { savedTokens: 0, entryIds: [] }; } return { savedTokens: estimateProjectedSavedTokens({ branchEntries, cwd, projectedReplacementsByEntryId: pendingReplacements, config, loadedSkillRoots, }), entryIds: [...pendingReplacements.keys()], }; } /** 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 = mergeProjectedReplacements( collectProjectedReplacements(branchEntries), getRuntimeProjectedReplacements(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 = mergeProjectedReplacements( collectProjectedReplacements(branchEntries), getRuntimeProjectedReplacements(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 collectProjectedReplacementsFromEntries(branchEntries); } /** 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; } /** Collects projection state appended after the latest valid provider usage. */ function collectPendingProjectedReplacements( branchEntries: readonly SessionEntry[], ): Map { const latestValidUsageIndex = findLastEntryIndex( branchEntries, (entry) => entry.type === "message" && hasValidAssistantContextUsage(entry.message), ); return collectProjectedReplacementsFromEntries( branchEntries.slice(latestValidUsageIndex + 1), ); } /** Collects projection replacement text from extension-owned custom state entries. */ function collectProjectedReplacementsFromEntries( entries: readonly SessionEntry[], ): Map { const projectedReplacementsByEntryId = new Map(); for (const entry of entries) { if ( entry.type !== "custom" || entry.customType !== CONTEXT_PROJECTION_CUSTOM_TYPE || !isProjectionStateEntryData(entry.data) ) { continue; } for (const projectedEntry of entry.data.projectedEntries) { projectedReplacementsByEntryId.set( projectedEntry.entryId, projectedEntry.replacementText, ); } } return projectedReplacementsByEntryId; } /** Returns true when an assistant message contains provider usage that reflects its request. */ export 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, ): void { runtimeProjectedReplacementsByScope.set( getRuntimeProjectionScope(cwd), new Map(projectedReplacementsByEntryId), ); } /** Estimates current projected token savings from branch-local projection state. */ export function estimateProjectedSavedTokens({ branchEntries, cwd, projectedReplacementsByEntryId, config, loadedSkillRoots = [], }: ProjectionSavingsEstimateOptions): number { if (projectedReplacementsByEntryId.size === 0) { return 0; } const decision = projectContextMessages({ mappedContext: buildContextEntryMapping(branchEntries), projectedReplacementsByEntryId, config, loadedSkillRoots, cwd, activeProjectionLevel: undefined, }); return estimateSavedTokens(decision.savedTokens); } /** 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); 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 the same branch message sequence that pi uses, but keeps the source entry beside each message. */ export function buildContextEntryMapping( branchEntries: readonly SessionEntry[], ): MappedContextEntry[] { const mappedEntries: MappedContextEntry[] = []; const appendContextEntry = (entry: SessionEntry): void => { const message = createContextMessageForEntry(entry); if (message !== undefined) { mappedEntries.push({ entry, message }); } }; const compactionIndex = findLastEntryIndex( branchEntries, (entry) => entry.type === "compaction", ); if (compactionIndex === -1) { for (const entry of branchEntries) { appendContextEntry(entry); } return mappedEntries; } const compactionEntry = branchEntries[compactionIndex]; if (compactionEntry?.type !== "compaction") { return mappedEntries; } mappedEntries.push({ entry: compactionEntry, message: createCompactionSummaryMessage(compactionEntry), }); let foundFirstKeptEntry = false; for (let index = 0; index < compactionIndex; index += 1) { const entry = branchEntries[index]; if (entry === undefined) { continue; } if (entry.id === compactionEntry.firstKeptEntryId) { foundFirstKeptEntry = true; } if (foundFirstKeptEntry) { appendContextEntry(entry); } } for ( let index = compactionIndex + 1; index < branchEntries.length; index += 1 ) { const entry = branchEntries[index]; if (entry !== undefined) { appendContextEntry(entry); } } return mappedEntries; } /** 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[] = []; 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; changed = true; if (result.projectedEntry !== undefined) { newSavedTokens += result.savedTokens; newProjectedEntries.push(result.projectedEntry); } return result.message; }); return { messages, newProjectedEntries, savedTokens, newSavedTokens, 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; } /** Creates the model-visible message that corresponds to a session entry. */ function createContextMessageForEntry( entry: SessionEntry, ): AgentMessage | undefined { if (entry.type === "message") { return entry.message; } if (entry.type === "custom_message") { return { role: "custom", customType: entry.customType, content: entry.content, display: entry.display, details: entry.details, timestamp: new Date(entry.timestamp).getTime(), } as AgentMessage; } if (entry.type === "branch_summary" && entry.summary) { return { role: "branchSummary", summary: entry.summary, fromId: entry.fromId, timestamp: new Date(entry.timestamp).getTime(), } as AgentMessage; } return undefined; } /** Creates the model-visible compaction summary message that pi emits for the latest compaction. */ function createCompactionSummaryMessage( entry: Extract, ): AgentMessage { return { role: "compactionSummary", summary: entry.summary, tokensBefore: entry.tokensBefore, timestamp: new Date(entry.timestamp).getTime(), } as AgentMessage; } /** 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 the path argument accepted by pi's read tool. */ function getReadToolCallPath(args: unknown): string | undefined { if (!isRecord(args)) { return undefined; } const path = args["path"]; if (typeof path === "string") { return path; } const filePath = args["file_path"]; return typeof filePath === "string" ? filePath : undefined; } /** Resolves read input paths with the same cwd, tilde, and @-prefix semantics used by pi's read tool. */ function resolveReadInputPath(inputPath: string, cwd: string): string { const withoutAtPrefix = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath; const expandedPath = expandHomePath(withoutAtPrefix); return isAbsolute(expandedPath) ? resolve(expandedPath) : resolve(cwd, expandedPath); } /** Expands the home directory shorthand accepted by pi path tools. */ /** 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 the target path is the root path or a descendant of it. */ function isPathInsideOrEqual(targetPath: string, rootPath: string): boolean { const relativePath = relative(resolve(rootPath), resolve(targetPath)); return ( relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath)) ); } /** 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 getRuntimeProjectedReplacements( cwd: string, ): ReadonlyMap { return ( runtimeProjectedReplacementsByScope.get(getRuntimeProjectionScope(cwd)) ?? new Map() ); } function getRuntimeProjectionScope(cwd: string): string { return `${getAgentDir()}\0${cwd}`; } function getRuntimePendingProjectionScope(sessionId: string): string { return `${getAgentDir()}\0${sessionId}`; } function mergeProjectedReplacements( persistedReplacements: ReadonlyMap, runtimeReplacements: ReadonlyMap, ): Map { return new Map([...persistedReplacements, ...runtimeReplacements]); } /** 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), ); }