From b295fb2f5f5a0de2cafb5aa595acac1f4b4f09be Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:24:30 -0500 Subject: [PATCH] fix(core): derive previous agent in projection (#41685) --- packages/client/src/effect/api/api.ts | 6 +----- .../client/src/promise/generated/types.ts | 2 +- packages/core/src/plugin/host.ts | 1 + packages/core/src/plugin/plan.ts | 21 ++++++++++--------- packages/core/src/plugin/runtime.ts | 2 ++ packages/core/src/session.ts | 3 +-- packages/core/src/session/message-updater.ts | 2 +- packages/core/test/plugin/host.ts | 1 + packages/core/test/session-create.test.ts | 2 +- packages/plugin/src/effect/session.ts | 7 ++++++- packages/protocol/openapi.json | 3 --- packages/schema/src/session-event.ts | 1 - packages/www/openapi.json | 3 --- packages/www/public/openapi.json | 3 --- 14 files changed, 26 insertions(+), 31 deletions(-) diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index d601be89a3..52b848b2ea 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -339,11 +339,7 @@ export type Endpoint5_31Output = readonly type: "session.agent.selected" readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } readonly location?: Location.Ref | undefined - readonly data: { - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly previous?: Agent.ID | undefined - } + readonly data: { readonly sessionID: Session.ID; readonly agent: Agent.ID } } | { readonly id: Event.ID diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 1c8bc73aad..2aeb01694f 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -436,7 +436,7 @@ export type SessionAgentSelected = { type: "session.agent.selected" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; agent: string; previous?: string } + data: { sessionID: string; agent: string } } export type SessionModelSelected = { diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index faee955c66..6df99210e0 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -349,6 +349,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../p input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }), }), get: (input) => runtime.session.get(input.sessionID), + message: runtime.session.message, prompt: runtime.session.prompt, generate: (input) => runtime.session.generate(input).pipe(Effect.map((text) => ({ text }))), command: runtime.session.command, diff --git a/packages/core/src/plugin/plan.ts b/packages/core/src/plugin/plan.ts index 649fc9ec3f..ecb6a7694a 100644 --- a/packages/core/src/plugin/plan.ts +++ b/packages/core/src/plugin/plan.ts @@ -2,6 +2,7 @@ export * as PlanPlugin from "./plan" import { ToolFailure } from "@opencode-ai/ai" import { define } from "@opencode-ai/plugin/effect/plugin" +import { SessionMessage } from "@opencode-ai/schema/session-message" import { Effect, Stream } from "effect" import { Agent } from "../agent" @@ -37,18 +38,18 @@ export const Plugin = define({ yield* ctx.event.subscribe().pipe( Stream.filter((event) => event.type === "session.agent.selected"), - Stream.runForEach((event) => { - if (event.data.agent === event.data.previous) return Effect.void - const text = event.data.agent === plan ? enter : event.data.previous === plan ? leave : undefined - if (!text) return Effect.void - return ctx.session - .synthetic({ + Stream.runForEach((event) => + Effect.gen(function* () { + const message = yield* ctx.session.message({ sessionID: event.data.sessionID, - text, - resume: false, + messageID: SessionMessage.ID.fromEvent(event.id), }) - .pipe(Effect.catch(() => Effect.void)) - }), + if (message?.type !== "agent-switched" || message.agent === message.previous) return + const text = message.agent === plan ? enter : message.previous === plan ? leave : undefined + if (!text) return + yield* ctx.session.synthetic({ sessionID: event.data.sessionID, text, resume: false }) + }).pipe(Effect.catch(() => Effect.void)), + ), Effect.forkScoped({ startImmediately: true }), ) }), diff --git a/packages/core/src/plugin/runtime.ts b/packages/core/src/plugin/runtime.ts index a7815ac810..5316a01a2a 100644 --- a/packages/core/src/plugin/runtime.ts +++ b/packages/core/src/plugin/runtime.ts @@ -14,6 +14,7 @@ export interface Interface { | "get" | "create" | "messages" + | "message" | "prompt" | "generate" | "command" @@ -59,6 +60,7 @@ export const layerWithCell = (cell: Cell) => get: (sessionID) => require(cell, (runtime) => runtime.session.get(sessionID)), create: (input) => require(cell, (runtime) => runtime.session.create(input)), messages: (input) => require(cell, (runtime) => runtime.session.messages(input)), + message: (input) => require(cell, (runtime) => runtime.session.message(input)), prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)), generate: (input) => require(cell, (runtime) => runtime.session.generate(input)), command: (input) => require(cell, (runtime) => runtime.session.command(input)), diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index bbc0ee9c8e..fb49b19cb8 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -716,11 +716,10 @@ const layer = Layer.effect( .pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) }), switchAgent: Effect.fn("Session.switchAgent")(function* (input) { - const session = yield* result.get(input.sessionID) + yield* result.get(input.sessionID) yield* bus.publish(SessionEvent.AgentSelected, { sessionID: input.sessionID, agent: input.agent, - previous: session.agent, }) }), switchModel: Effect.fn("Session.switchModel")(function* (input) { diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 9bb86b813b..c9e35f5417 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -61,7 +61,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) { "session.usage.recorded": () => Effect.void, "session.agent.selected": (event) => { return Effect.gen(function* () { - const previous = event.data.previous ?? (yield* adapter.getAgent()) + const previous = yield* adapter.getAgent() yield* adapter.appendMessage( SessionMessage.AgentSelected.make({ id: SessionMessage.ID.fromEvent(event.id), diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 18ea39d3c3..1ef1e355fa 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -102,6 +102,7 @@ export function host(overrides: Overrides = {}): Plugin.Context { hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")), create: overrides.session?.create ?? (() => Effect.die("unused session.create")), get: overrides.session?.get ?? (() => Effect.die("unused session.get")), + message: overrides.session?.message ?? (() => Effect.die("unused session.message")), prompt: overrides.session?.prompt ?? (() => Effect.die("unused session.prompt")), generate: overrides.session?.generate ?? (() => Effect.die("unused session.generate")), command: overrides.session?.command ?? (() => Effect.die("unused session.command")), diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 02b5eb1650..b8a7160b3d 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -654,7 +654,7 @@ describe("Session.create", () => { expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" }) expect( Array.from(yield* logEvents(session, created.id, true).pipe(Stream.drop(1), Stream.take(1), Stream.runCollect)), - ).toMatchObject([{ type: "session.agent.selected", data: { agent: "plan", previous: "build" } }]) + ).toMatchObject([{ type: "session.agent.selected", data: { agent: "plan" } }]) expect(yield* session.messages({ sessionID: created.id, order: "asc" })).toMatchObject([ { type: "agent-switched", agent: "plan", previous: "build" }, ]) diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index c4564c2cd6..4cc18403e1 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -3,7 +3,8 @@ import type { Message, SystemPart } from "@opencode-ai/ai" import type { Agent } from "@opencode-ai/schema/agent" import type { Model } from "@opencode-ai/schema/model" import type { Session } from "@opencode-ai/schema/session" -import type { JsonSchema } from "effect" +import type { SessionMessage } from "@opencode-ai/schema/session-message" +import type { Effect, JsonSchema } from "effect" import type { Hooks } from "./registration.js" export interface SessionContext { @@ -40,5 +41,9 @@ export type SessionDomain = Pick< SessionApi, "create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait" > & { + readonly message: (input: { + readonly sessionID: Session.ID + readonly messageID: SessionMessage.ID + }) => Effect.Effect readonly hook: Hooks } diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 0de6df569e..186566dfc6 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -14373,9 +14373,6 @@ }, "agent": { "type": "string" - }, - "previous": { - "type": "string" } }, "required": ["sessionID", "agent"], diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 1513e6fc37..2ae3f7c405 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -69,7 +69,6 @@ export const AgentSelected = Event.durable({ schema: { ...Base, agent: Agent.ID, - previous: Agent.ID.pipe(optional), }, }) export type AgentSelected = typeof AgentSelected.Type diff --git a/packages/www/openapi.json b/packages/www/openapi.json index 0de6df569e..186566dfc6 100644 --- a/packages/www/openapi.json +++ b/packages/www/openapi.json @@ -14373,9 +14373,6 @@ }, "agent": { "type": "string" - }, - "previous": { - "type": "string" } }, "required": ["sessionID", "agent"], diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 0de6df569e..186566dfc6 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -14373,9 +14373,6 @@ }, "agent": { "type": "string" - }, - "previous": { - "type": "string" } }, "required": ["sessionID", "agent"],