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
59 changes: 55 additions & 4 deletions packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export namespace SessionCompaction {
// Allow plugins to inject context or replace compaction prompt.
const compacting = yield* plugin.trigger(
"experimental.session.compacting",
{ sessionID: input.sessionID },
{ sessionID: input.sessionID, agent: userMessage.agent },
{ context: [], prompt: undefined },
)
const defaultPrompt = `Provide a detailed prompt for continuing our conversation above.
Expand All @@ -194,6 +194,10 @@ Respond in the same language as the user's messages in the conversation.

When constructing the summary, try to stick to this template:
---
## Agent Role & Constraints

[If the system prompt indicates a specialized agent role (e.g. evaluator, reviewer, judge, explorer, planner), state the agent name, its role, and any behavioral constraints (read-only, no implementation, output format requirements). If the agent is a general-purpose implementor, write "Default agent — no special constraints." Frame next steps in terms appropriate to the agent's role: evaluators should evaluate, reviewers should review, planners should plan — do NOT frame all agents as implementors.]

## Goal

[What goal(s) is the user trying to accomplish?]
Expand All @@ -217,9 +221,39 @@ When constructing the summary, try to stick to this template:
---`

const prompt = compacting.prompt ?? [defaultPrompt, ...compacting.context].join("\n\n")
// Resolve the source agent to preserve its identity during compaction
const source = yield* agents.get(userMessage.agent)
const system: string[] = []
if (source?.prompt) {
const max = 4000
const truncated =
source.prompt.length > max ? source.prompt.slice(0, max) + "\n[...truncated]" : source.prompt
system.push(truncated)
}
const msgs = structuredClone(messages)
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, { stripMedia: true })

// Convert structured tool-call/tool-result parts into plain text so the
// compaction model never sees tool markup and can't hallucinate tool calls.
for (const msg of modelMessages) {
if (!Array.isArray(msg.content)) continue
msg.content = msg.content.map((part: any) => {
if (part.type === "tool-call") {
const inputStr = typeof part.input === "string" ? part.input : JSON.stringify(part.input)
const truncatedInput = inputStr.length > 300 ? inputStr.slice(0, 300) + "... [truncated]" : inputStr
return { type: "text" as const, text: `[Called tool: ${part.toolName}]\n[Input: ${truncatedInput}]` }
}
if (part.type === "tool-result") {
const outputStr = typeof part.output === "string" ? part.output : JSON.stringify(part.output)
const truncatedOutput = outputStr.length > 500 ? outputStr.slice(0, 500) + "... [truncated]" : outputStr
return { type: "text" as const, text: `[Tool result: ${part.toolName}]\n${truncatedOutput}` }
}
return part
})
}
// Remove tool-role messages that are now empty or redundant after transformation
const compactionMessages = modelMessages.filter((msg) => msg.role !== "tool")
const ctx = yield* InstanceState.context
const msg: MessageV2.Assistant = {
id: MessageID.ascending(),
Expand Down Expand Up @@ -259,9 +293,10 @@ When constructing the summary, try to stick to this template:
agent,
sessionID: input.sessionID,
tools: {},
system: [],
toolChoice: "none",
system,
messages: [
...modelMessages,
...compactionMessages,
{
role: "user",
content: [{ type: "text", text: prompt }],
Expand All @@ -283,6 +318,11 @@ When constructing the summary, try to stick to this template:
}

if (result === "continue" && input.auto) {
// Inject post-compaction agent identity reminder for specialized agents
const reminder =
source?.prompt && !source?.native
? `<system-reminder>You are the "${userMessage.agent}" agent. Your role and constraints from your system prompt still apply after this compaction. Do not deviate from your assigned role.</system-reminder>`
: undefined
if (replay) {
const original = replay.info
const replayMsg = yield* session.updateMessage({
Expand Down Expand Up @@ -310,6 +350,17 @@ When constructing the summary, try to stick to this template:
sessionID: input.sessionID,
})
}
if (reminder) {
yield* session.updatePart({
id: PartID.ascending(),
messageID: replayMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: reminder,
time: { start: Date.now(), end: Date.now() },
})
}
}

if (!replay) {
Expand All @@ -332,7 +383,7 @@ When constructing the summary, try to stick to this template:
sessionID: input.sessionID,
type: "text",
synthetic: true,
text,
text: reminder ? text + "\n\n" + reminder : text,
time: {
start: Date.now(),
end: Date.now(),
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ export namespace SessionProcessor {

case "tool-input-start":
if (ctx.assistantMessage.summary) {
throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
log.warn("ignoring tool call during summary generation", { tool: value.toolName })
return
}
ctx.toolcalls[value.id] = yield* session.updatePart({
id: ctx.toolcalls[value.id]?.id ?? PartID.ascending(),
Expand All @@ -173,7 +174,8 @@ export namespace SessionProcessor {

case "tool-call": {
if (ctx.assistantMessage.summary) {
throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
log.warn("ignoring tool call during summary generation", { tool: value.toolName })
return
}
const match = ctx.toolcalls[value.toolCallId]
if (!match) return
Expand Down
Loading
Loading