Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/opencode/src/agent/prompt/compaction.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ Summarize only the conversation history you are given. The newest turns may be k

If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.

Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.
Always follow the exact output structure requested by the user prompt. Keep every section.

CRITICAL — Preserve detail that matters for continuing work:
- Exact error messages, stack traces, and failure modes
- Exact file paths, line numbers, function and variable names
- Exact command outputs, return values, and data structures
- User preferences, specifications, style guidance, and constraints
- Architectural decisions and why alternatives were rejected
- Tool configurations, API endpoints, environment details
- Active todos, progress state, and workflow phase

Prefer substantive detail over brevity. A few specific, accurate sentences are better than a terse bullet that loses meaning. When preserving a file path, error message, or identifier, include the exact text so the next agent can use it without guessing.

Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.
16 changes: 16 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,25 @@ export const Info = Schema.Struct({
preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
}),
preserve_recent_tokens_max: Schema.optional(NonNegativeInt).annotate({
description:
"Upper bound on tokens preserved verbatim from recent turns (default: 32000). Only used when preserve_recent_tokens is not set.",
}),
reserved: Schema.optional(NonNegativeInt).annotate({
description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
}),
tool_output_max_chars: Schema.optional(PositiveInt).annotate({
description:
"Max characters of tool output text to include when building compaction context (default: 2000). Increase for more detailed summaries.",
}),
summary_template: Schema.optional(Schema.String).annotate({
description:
"Custom Markdown template for compaction summaries. Replaces the built-in template. Should include sections like Goal, Progress, Key Decisions, etc.",
}),
model: Schema.optional(Schema.String).annotate({
description:
"Model ID override for compaction, e.g. 'anthropic/claude-opus-4'. Defaults to the session model.",
}),
}),
),
experimental: Schema.optional(
Expand Down
40 changes: 29 additions & 11 deletions packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export const Event = {

export const PRUNE_MINIMUM = 20_000
export const PRUNE_PROTECT = 40_000
const TOOL_OUTPUT_MAX_CHARS = 2_000
const DEFAULT_TOOL_OUTPUT_MAX_CHARS = 2_000
const PRUNE_PROTECTED_TOOLS = ["skill"]
const DEFAULT_TAIL_TURNS = 2
const MIN_PRESERVE_RECENT_TOKENS = 2_000
const MAX_PRESERVE_RECENT_TOKENS = 8_000
const DEFAULT_MAX_PRESERVE_RECENT_TOKENS = 32_000
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
<template>
## Goal
Expand Down Expand Up @@ -68,6 +68,12 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te

## Relevant Files
- [file or directory path: why it matters, or "(none)"]

## Active Skills & Workflow
- [skill name: why loaded, current workflow phase and mode, or "(none)"]

## Active Todos
- [status] content (priority) — e.g. "[in_progress] Create API endpoints (high)"
</template>

Rules:
Expand Down Expand Up @@ -120,7 +126,7 @@ function completedCompactions(messages: MessageV2.WithParts[]) {
})
}

function buildPrompt(input: { previousSummary?: string; context: string[] }) {
function buildPrompt(input: { previousSummary?: string; context: string[]; template?: string }) {
const anchor = input.previousSummary
? [
"Update the anchored summary below using the conversation history above.",
Expand All @@ -130,13 +136,16 @@ function buildPrompt(input: { previousSummary?: string; context: string[] }) {
"</previous-summary>",
].join("\n")
: "Create a new anchored summary from the conversation history above."
return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
return [anchor, input.template ?? SUMMARY_TEMPLATE, ...input.context].join("\n\n")
}

function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) {
return (
input.cfg.compaction?.preserve_recent_tokens ??
Math.min(MAX_PRESERVE_RECENT_TOKENS, Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25)))
Math.min(
input.cfg.compaction?.preserve_recent_tokens_max ?? DEFAULT_MAX_PRESERVE_RECENT_TOKENS,
Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25)),
)
)
}

Expand Down Expand Up @@ -381,10 +390,16 @@ export const layer = Layer.effect(
}

const agent = yield* agents.get("compaction")
const model = agent.model
? yield* provider.getModel(agent.model.providerID, agent.model.modelID).pipe(Effect.orDie)
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID).pipe(Effect.orDie)
const cfg = yield* config.get()
const model =
cfg.compaction?.model
? yield* (() => {
const p = Provider.parseModel(cfg.compaction.model!)
return provider.getModel(p.providerID, p.modelID).pipe(Effect.orDie)
})()
: agent.model
? yield* provider.getModel(agent.model.providerID, agent.model.modelID).pipe(Effect.orDie)
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID).pipe(Effect.orDie)
const history = compactionPart && messages.at(-1)?.info.id === input.parentID ? messages.slice(0, -1) : messages
const prior = completedCompactions(history)
const hidden = new Set(prior.flatMap((item) => [item.userIndex, item.assistantIndex]))
Expand All @@ -400,12 +415,15 @@ export const layer = Layer.effect(
{ sessionID: input.sessionID },
{ context: [], prompt: undefined },
)
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
const nextPrompt =
compacting.prompt ??
buildPrompt({ previousSummary, context: compacting.context, template: cfg.compaction?.summary_template })
const msgs = structuredClone(selected.head)
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
const toolOutputMaxChars = cfg.compaction?.tool_output_max_chars ?? DEFAULT_TOOL_OUTPUT_MAX_CHARS
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
stripMedia: true,
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
toolOutputMaxChars,
})
const ctx = yield* InstanceState.context
const msg: MessageV2.Assistant = {
Expand Down Expand Up @@ -537,7 +555,7 @@ export const layer = Layer.effect(
(input.overflow
? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n"
: "") +
"Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed."
"Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.\n\nIf the Active Skills & Workflow section above lists any loaded skills or workflow modes, re-load them with the skill tool before continuing."
yield* session.updatePart({
id: PartID.ascending(),
messageID: continueMsg.id,
Expand Down
Loading