export * as SessionCompaction from "./compaction.js" import { AIError, type ContentPart, InvalidProviderOutputError, InvalidRequestError, isContextOverflowFailure, LLMClient, LLMEvent, LLMRequest, Message, type ToolEntry, UnknownProviderError, type Usage, } from "@opencode/ai" import type { StreamOptions } from "@opencode/ai/route" import type { SessionCompactionResult } from "@opencode/plugin/effect/session" import type { SessionError } from "@opencode/schema/session-error" import { makeLocationNode } from "@opencode/util/effect/app-node" import { Context, Effect, Exit, Fiber, Layer, Result, Scope, Stream } from "effect" import { Agent } from "../agent.js" import { Bus } from "../bus.js" import { Database } from "../database/database.js" import { llmClient } from "../effect/app-node-platform.js" import { Model } from "../model.js" import { State } from "../state.js" import { Token } from "../util/token.js" import type { SessionContext } from "./context.js" import { SessionEvent } from "./event.js" import { SessionHistory } from "./history.js" import type { SessionMessage } from "./message.js" import { SessionModelRequest } from "./model-request.js" import { SessionRunnerModel } from "./runner/model.js" import { SessionRunnerRetry } from "./runner/retry.js" import { toLLMMessages } from "./runner/to-llm-message.js" import { SessionRecentText } from "./recent-text.js" import { contentFilterError, toSessionError } from "./to-session-error.js" import { SessionUsage } from "./usage.js" export type Settings = { auto: boolean /** Tokens kept free below the model's limits before compacting. Unset keeps 15%, and at least 16k, free. */ buffer?: number /** Tokens of recent conversation kept verbatim beside the summary. */ keep: number /** Summarize in the background as the context nears the threshold, so automatic compaction can swap it in. */ precompute: boolean } export type Editor = { configure: (settings: Partial) => void } export type Trigger = /** `overflow`: the provider just rejected this context as too long. */ | { readonly reason: "auto" | "overflow"; readonly context: SessionContext.Loaded } /** `inputID` is the `/compact` inbox item, whose message shows the outcome. */ | { readonly reason: "manual"; readonly context: SessionContext.Loaded; readonly inputID: SessionMessage.ID } export type Outcome = /** Only `auto` skips: the context fits, or automatic compaction is off. */ | { readonly status: "skipped" } | { readonly status: "completed" } | { readonly status: "failed"; readonly error: SessionError.Error } export interface Interface extends State.Transformable { readonly compact: (trigger: Trigger) => Effect.Effect /** * Starts a background summary when the context nears the threshold. Never waits for it. Loads `context` only when * background summaries are on. */ readonly precompute: (context: Effect.Effect) => Effect.Effect } export class Service extends Context.Service()("@opencode/SessionCompaction") {} /** * A summary fills `text`, and references the messages it keeps verbatim with `metadata.keepID`; history * rebuilds their text from that. A native compaction fills `native.messages` with its replacement window. */ type Result = { readonly text: string readonly native?: SessionMessage.Native readonly metadata?: Record } type Failure = { readonly error: SessionError.Error } type Prepared = Effect.Success> type Streamed = { readonly text: string } /** A summary run off the critical path: it covers only `messages`, streams no deltas, and bills into `usage`. */ type Background = { readonly messages: ReadonlyArray readonly usage: (usage: SessionUsage.Recorded) => void } /** A background summary of everything before `keepID`, held until automatic compaction is due. */ type Precomputed = { readonly firstID: SessionMessage.ID | undefined readonly keepID: SessionMessage.ID readonly model: SessionContext.Loaded["model"]["ref"] readonly initial: string /** The longest tail since `keepID` that compaction still accepts beside this summary. */ readonly tailLimit: number /** Background summaries that failed since the last compaction, this one included once it has. */ readonly failures: number readonly usage: SessionUsage.Recorded[] readonly fiber: Fiber.Fiber } const NOTHING_TO_COMPACT: Failure = { error: { type: "compaction.unavailable", message: "Nothing to compact yet" } } /** After each "too long" rejection, the next attempt aims at this share of the first rejected request's size. */ const SHRINK_STEPS = [0.85, 0.7, 0.5, 0.35] // The least of the window kept free for the last reply before compaction and for the summary itself. const RESERVE_MIN = 16_000 /** * Share of the window kept free before automatic compaction. Above `RESERVE_MIN`, the margin between it and * `summaryRoom` absorbs a large step past the trigger, so the whole conversation still goes out unchanged and reuses * the cache. Single steps grew by up to ~22k tokens at p99 in real sessions. */ const RESERVE_SHARE = 0.15 /** A common window size, assumed for the compaction request when the model's window is unknown. */ const UNKNOWN_WINDOW = 200_000 /** A summary gets at least this much output, or the room left if less, even when the estimate margin leaves less. */ const SUMMARY_OUTPUT_FLOOR = 32_000 /** * How far one exchange of a summary transcript is shortened, in the order the levels apply: whole; tool outputs cut; * tool outputs left out and tool inputs cut; only the user's text, cut; omitted. */ const OUTPUTS_CLIPPED = 1 const OUTPUTS_REMOVED = 2 const USER_ONLY = 3 const OMITTED = 4 const TRANSCRIPT_LEVELS = [0, OUTPUTS_CLIPPED, OUTPUTS_REMOVED, USER_ONLY, OMITTED] /** Characters kept, from the start and end, of a cut tool input, tool output, or user message. */ const TRANSCRIPT_CLIP = 2_000 const IMAGE_TOKEN_ESTIMATE = 1_500 const PDF_TOKEN_ESTIMATE = 2_000 /** Share of the compaction ceiling at which a background summary starts. */ const PRECOMPUTE_RATIO = 0.85 /** Measured prompt tokens below which a background summary is not worth its cost. */ const PRECOMPUTE_MIN_TOKENS = 64_000 /** * The most text a swapped-in summary keeps verbatim beside it: twice `keep`, at least 30k, at most a quarter of the * budget. A longer tail would leave the context near the threshold, so compaction summarizes from scratch instead. */ const PRECOMPUTED_TAIL_MIN = 30_000 const PRECOMPUTED_TAIL_SHARE = 0.25 /** Failed background summaries after which no new one starts until the next compaction. */ const PRECOMPUTE_MAX_FAILURES = 2 const SUMMARY_TEMPLATE = `You MUST use this format for your response (you may omit sections that aren't applicable). Do not include the