fix(core): derive previous agent in projection (#41685)

This commit is contained in:
Aiden Cline
2026-08-11 01:24:30 -05:00
committed by GitHub
parent cc792f79b7
commit b295fb2f5f
14 changed files with 26 additions and 31 deletions
+1 -5
View File
@@ -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
@@ -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 = {
+1
View File
@@ -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,
+11 -10
View File
@@ -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 }),
)
}),
+2
View File
@@ -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)),
+1 -2
View File
@@ -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) {
+1 -1
View File
@@ -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),
+1
View File
@@ -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")),
+1 -1
View File
@@ -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" },
])
+6 -1
View File
@@ -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<unknown>,
"create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait"
> & {
readonly message: (input: {
readonly sessionID: Session.ID
readonly messageID: SessionMessage.ID
}) => Effect.Effect<SessionMessage.Info | undefined, unknown>
readonly hook: Hooks<SessionHooks>
}
-3
View File
@@ -14373,9 +14373,6 @@
},
"agent": {
"type": "string"
},
"previous": {
"type": "string"
}
},
"required": ["sessionID", "agent"],
-1
View File
@@ -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
-3
View File
@@ -14373,9 +14373,6 @@
},
"agent": {
"type": "string"
},
"previous": {
"type": "string"
}
},
"required": ["sessionID", "agent"],
-3
View File
@@ -14373,9 +14373,6 @@
},
"agent": {
"type": "string"
},
"previous": {
"type": "string"
}
},
"required": ["sessionID", "agent"],