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
15 changes: 12 additions & 3 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ function providerMeta(metadata: Record<string, any> | undefined) {
export const toModelMessagesEffect = Effect.fnUntraced(function* (
input: WithParts[],
model: Provider.Model,
options?: { stripMedia?: boolean; toolOutputMaxChars?: number },
options?: { stripMedia?: boolean; stripPlanModeReminders?: boolean; toolOutputMaxChars?: number },
) {
const result: UIMessage[] = []
const toolNames = new Set<string>()
Expand Down Expand Up @@ -771,6 +771,15 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
return { type: "json", value: output as never }
}

const skipText = (part: TextPart) => {
if (part.ignored) return true
if (!options?.stripPlanModeReminders) return false
if (!part.synthetic) return false
if (part.metadata?.["kind"] === "plan_reminder") return true
// Fallback for sessions persisted before reminders were tagged with metadata.
return part.text.includes("Plan mode is active") || part.text.includes("Plan mode ACTIVE")
}

for (const msg of input) {
if (msg.parts.length === 0) continue

Expand All @@ -782,7 +791,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
}
result.push(userMessage)
for (const part of msg.parts) {
if (part.type === "text" && !part.ignored)
if (part.type === "text" && !skipText(part))
userMessage.parts.push({
type: "text",
text: part.text,
Expand Down Expand Up @@ -969,7 +978,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
export function toModelMessages(
input: WithParts[],
model: Provider.Model,
options?: { stripMedia?: boolean; toolOutputMaxChars?: number },
options?: { stripMedia?: boolean; stripPlanModeReminders?: boolean; toolOutputMaxChars?: number },
): Promise<ModelMessage[]> {
return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer)))
}
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export const layer = Layer.effect(
type: "text",
text: PROMPT_PLAN,
synthetic: true,
metadata: { kind: "plan_reminder" },
})
}
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
Expand Down Expand Up @@ -349,6 +350,7 @@ This is critical - your turn should only end with either asking the user a quest
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
</system-reminder>`,
synthetic: true,
metadata: { kind: "plan_reminder" },
})
userMessage.parts.push(part)
return input.messages
Expand Down Expand Up @@ -1479,7 +1481,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
sys.skills(agent),
Effect.sync(() => sys.environment(model)),
instruction.system().pipe(Effect.orDie),
MessageV2.toModelMessagesEffect(msgs, model),
MessageV2.toModelMessagesEffect(msgs, model, { stripPlanModeReminders: agent.name !== "plan" }),
])
const system = [...env, ...(skills ? [skills] : []), ...instructions]
const format = lastUser.format ?? { type: "text" as const }
Expand Down
94 changes: 94 additions & 0 deletions packages/opencode/test/session/message-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,100 @@ describe("session.message-v2.toModelMessage", () => {
])
})

test("can strip stale synthetic plan mode reminders", async () => {
const messageID = "m-user"
const planTurn: MessageV2.WithParts = {
info: userInfo("m-plan"),
parts: [
{
...basePart("m-plan", "p0"),
type: "text",
text: "<system-reminder>\nPlan mode ACTIVE - older session without metadata.\n</system-reminder>",
synthetic: true,
},
{
...basePart("m-plan", "p0b"),
type: "text",
text: "draft a plan",
},
] as MessageV2.Part[],
}
const buildTurn: MessageV2.WithParts = {
info: userInfo(messageID),
parts: [
{
...basePart(messageID, "p1"),
type: "text",
text: "<system-reminder>\nPlan mode is active. Do not edit files.\n</system-reminder>",
synthetic: true,
metadata: { kind: "plan_reminder" },
},
{
...basePart(messageID, "p2"),
type: "text",
text: "<system-reminder>\nYour operational mode has changed from plan to build.\n</system-reminder>",
synthetic: true,
},
{
...basePart(messageID, "p3"),
type: "text",
text: "Please implement the plan.",
},
] as MessageV2.Part[],
}
const input: MessageV2.WithParts[] = [planTurn, buildTurn]

expect(await MessageV2.toModelMessages(input, model, { stripPlanModeReminders: true })).toStrictEqual([
{
role: "user",
content: [{ type: "text", text: "draft a plan" }],
},
{
role: "user",
content: [
{
type: "text",
text: "<system-reminder>\nYour operational mode has changed from plan to build.\n</system-reminder>",
},
{ type: "text", text: "Please implement the plan." },
],
},
])
})

test("keeps plan reminders when not stripping", async () => {
const messageID = "m-user"
const input: MessageV2.WithParts[] = [
{
info: userInfo(messageID),
parts: [
{
...basePart(messageID, "p1"),
type: "text",
text: "<system-reminder>\nPlan mode is active.\n</system-reminder>",
synthetic: true,
metadata: { kind: "plan_reminder" },
},
{
...basePart(messageID, "p2"),
type: "text",
text: "draft a plan",
},
] as MessageV2.Part[],
},
]

expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
{
role: "user",
content: [
{ type: "text", text: "<system-reminder>\nPlan mode is active.\n</system-reminder>" },
{ type: "text", text: "draft a plan" },
],
},
])
})

test("converts user text/file parts and injects compaction/subtask prompts", async () => {
const messageID = "m-user"

Expand Down
Loading