mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
fix(core): restore plan mode reminder after compaction
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
export * as PlanPlugin from "./plan.js"
|
||||
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { define, type Context } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Agent } from "../agent.js"
|
||||
import { SessionEvent } from "../session/event.js"
|
||||
@@ -40,35 +40,47 @@ export const Plugin = define({
|
||||
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter(
|
||||
(event): event is SessionEvent.Created | SessionEvent.AgentSelected =>
|
||||
event.type === "session.created" || event.type === "session.agent.selected",
|
||||
(event): event is SessionEvent.Created | SessionEvent.AgentSelected | SessionEvent.Compaction.Ended =>
|
||||
event.type === "session.created" ||
|
||||
event.type === "session.agent.selected" ||
|
||||
event.type === "session.compaction.ended",
|
||||
),
|
||||
Stream.runForEach((event) => {
|
||||
const text = reminder(event)
|
||||
if (!text) return Effect.void
|
||||
return ctx.session
|
||||
.synthetic({
|
||||
Stream.runForEach((event) =>
|
||||
Effect.gen(function* () {
|
||||
const text = yield* reminder(ctx.session, event)
|
||||
if (!text) return
|
||||
yield* ctx.session.synthetic({
|
||||
sessionID: event.data.sessionID,
|
||||
text,
|
||||
resume: false,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("failed to inject Plan mode reminder", { sessionID: event.data.sessionID, cause }),
|
||||
),
|
||||
)
|
||||
}),
|
||||
}).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("failed to inject Plan mode reminder", { sessionID: event.data.sessionID, cause }),
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
function reminder(event: SessionEvent.Created | SessionEvent.AgentSelected) {
|
||||
const reminder = Effect.fnUntraced(function* (
|
||||
session: Context["session"],
|
||||
event: SessionEvent.Created | SessionEvent.AgentSelected | SessionEvent.Compaction.Ended,
|
||||
) {
|
||||
if (event.type === "session.created") {
|
||||
if (event.data.agent !== plan) return
|
||||
return enter
|
||||
}
|
||||
if (event.type === "session.compaction.ended") {
|
||||
// Compaction cuts the transcript at the checkpoint, dropping any earlier enter
|
||||
// reminder, so re-assert Plan mode when the session is still on the plan agent.
|
||||
const info = yield* session.get({ sessionID: event.data.sessionID })
|
||||
if (info.agent !== plan) return
|
||||
return enter
|
||||
}
|
||||
if (event.data.agent === event.data.previous) return
|
||||
if (event.data.agent === plan) return enter
|
||||
if (event.data.previous === plan) return leave
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { DateTime, Effect, Stream } from "effect"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { PlanPlugin } from "@opencode-ai/core/plugin/plan"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { it } from "../lib/effect"
|
||||
import { host } from "./host"
|
||||
|
||||
const sessionID = Session.ID.make("ses_plan_test")
|
||||
const plan = Agent.ID.make("plan")
|
||||
const build = Agent.ID.make("build")
|
||||
|
||||
const envelope = () => ({
|
||||
id: Event.ID.create(),
|
||||
created: DateTime.makeUnsafe(0),
|
||||
durable: { aggregateID: sessionID, seq: Event.Seq.make(0), version: Event.Version.make(1) },
|
||||
})
|
||||
|
||||
const agentSelected = (agent: Agent.ID, previous: Agent.ID): SessionEvent.AgentSelected => ({
|
||||
...envelope(),
|
||||
type: "session.agent.selected",
|
||||
data: { sessionID, agent, previous },
|
||||
})
|
||||
|
||||
const compactionEnded = (): SessionEvent.Compaction.Ended => ({
|
||||
...envelope(),
|
||||
type: "session.compaction.ended",
|
||||
data: { sessionID, reason: "manual", text: "summary", recent: "" },
|
||||
})
|
||||
|
||||
const sessionInfo = (agent?: Agent.ID): Session.Info => ({
|
||||
id: sessionID,
|
||||
projectID: Project.ID.make("test"),
|
||||
agent,
|
||||
cost: Money.USD.zero,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
|
||||
location: { directory: AbsolutePath.make("/") },
|
||||
})
|
||||
|
||||
/** Runs the plan plugin against stubbed domains and captures injected reminders. */
|
||||
const run = Effect.fnUntraced(function* (
|
||||
events: ReadonlyArray<SessionEvent.AgentSelected | SessionEvent.Compaction.Ended>,
|
||||
agent?: Agent.ID,
|
||||
) {
|
||||
const captured = new Array<string>()
|
||||
yield* PlanPlugin.Plugin.effect(
|
||||
host({
|
||||
agent: {
|
||||
get: () => Effect.die("unused agent.get"),
|
||||
list: () => Effect.die("unused agent.list"),
|
||||
reload: () => Effect.die("unused agent.reload"),
|
||||
transform: () => Effect.succeed({ dispose: Effect.void }),
|
||||
},
|
||||
tool: {
|
||||
transform: () => Effect.die("unused tool.transform"),
|
||||
hook: () => Effect.succeed({ dispose: Effect.void }),
|
||||
},
|
||||
event: {
|
||||
subscribe: () => Stream.fromIterable(events),
|
||||
},
|
||||
session: {
|
||||
get: () => Effect.succeed(sessionInfo(agent)),
|
||||
synthetic: (input) => {
|
||||
captured.push(input.text)
|
||||
return Effect.succeed(
|
||||
SessionInbox.Synthetic.make({
|
||||
id: SessionMessage.ID.make("msg_plan_test"),
|
||||
sessionID,
|
||||
timeCreated: DateTime.makeUnsafe(0),
|
||||
type: "synthetic",
|
||||
payload: { text: input.text },
|
||||
delivery: "steer",
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
return captured
|
||||
})
|
||||
|
||||
const settle = (captured: ReadonlyArray<string>, expected: number, remaining = 1000): Effect.Effect<void, Error> =>
|
||||
Effect.gen(function* () {
|
||||
if (captured.length >= expected) return
|
||||
if (remaining === 0) {
|
||||
return yield* Effect.fail(new Error(`Timed out waiting for ${expected} reminders, saw ${captured.length}`))
|
||||
}
|
||||
yield* Effect.promise(() => Bun.sleep(1))
|
||||
yield* settle(captured, expected, remaining - 1)
|
||||
})
|
||||
|
||||
describe("plan plugin reminders", () => {
|
||||
it.effect("injects enter and leave reminders on agent switches", () =>
|
||||
Effect.gen(function* () {
|
||||
const captured = yield* run([agentSelected(plan, build), agentSelected(build, plan)])
|
||||
yield* settle(captured, 2)
|
||||
expect(captured[0]).toContain("You are in Plan mode")
|
||||
expect(captured[1]).toContain("NO LONGER in Plan mode")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("re-injects the enter reminder after compaction while on the plan agent", () =>
|
||||
Effect.gen(function* () {
|
||||
const captured = yield* run([compactionEnded()], plan)
|
||||
yield* settle(captured, 1)
|
||||
expect(captured).toHaveLength(1)
|
||||
expect(captured[0]).toContain("You are in Plan mode")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("ignores compaction when the session is not on the plan agent", () =>
|
||||
Effect.gen(function* () {
|
||||
// The trailing switch to plan proves the earlier compaction event was processed.
|
||||
const captured = yield* run([compactionEnded(), agentSelected(plan, build)], build)
|
||||
yield* settle(captured, 1)
|
||||
expect(captured).toHaveLength(1)
|
||||
expect(captured[0]).toContain("You are in Plan mode")
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user