diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 747f0492e0c3..2aeb01694fbb 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -41,6 +41,7 @@ export type SessionMessageAgentSelected = { time: { created: number } type: "agent-switched" agent: string + previous?: string } export type PromptBase64 = string @@ -2535,6 +2536,7 @@ export type SessionImportInput = { readonly time: { readonly created: number } readonly type: "agent-switched" readonly agent: string + readonly previous?: string } | { readonly id: string @@ -2786,6 +2788,7 @@ export type SessionImportInput = { readonly time: { readonly created: number } readonly type: "agent-switched" readonly agent: string + readonly previous?: string } | { readonly id: string @@ -3037,6 +3040,7 @@ export type SessionImportInput = { readonly time: { readonly created: number } readonly type: "agent-switched" readonly agent: string + readonly previous?: string } | { readonly id: string diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index d4fbe6ab3eb1..c9e35f54170d 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -4,6 +4,7 @@ import { SessionEvent } from "./event" import { SessionMessage } from "./message" export interface Adapter { + readonly getAgent: () => Effect.Effect readonly getModel: () => Effect.Effect readonly getCurrentAssistant: () => Effect.Effect readonly getAssistant: ( @@ -59,15 +60,19 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) { "session.created": () => Effect.void, "session.usage.recorded": () => Effect.void, "session.agent.selected": (event) => { - return adapter.appendMessage( - SessionMessage.AgentSelected.make({ - id: SessionMessage.ID.fromEvent(event.id), - type: "agent-switched", - metadata: event.metadata, - agent: event.data.agent, - time: { created: event.created }, - }), - ) + return Effect.gen(function* () { + const previous = yield* adapter.getAgent() + yield* adapter.appendMessage( + SessionMessage.AgentSelected.make({ + id: SessionMessage.ID.fromEvent(event.id), + type: "agent-switched", + metadata: event.metadata, + agent: event.data.agent, + previous, + time: { created: event.created }, + }), + ) + }) }, "session.model.selected": (event) => { return Effect.gen(function* () { diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index a72b91375753..3588dde6ef3f 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -5,6 +5,7 @@ import { DateTime, Effect, Layer, Schema, Stream } from "effect" import { Database } from "../database/database" import { Bus } from "../bus" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" +import { Agent } from "../agent" import { Model } from "../model" import { SessionEvent } from "./event" import { SessionMessage } from "./message" @@ -230,6 +231,17 @@ function run(db: DatabaseService, event: MessageEvent) { } const appendMessage = (message: SessionMessage.Info) => insertMessage(db, event, message) const adapter: SessionMessageUpdater.Adapter = { + getAgent() { + return db + .select({ agent: SessionTable.agent }) + .from(SessionTable) + .where(eq(SessionTable.id, event.data.sessionID)) + .get() + .pipe( + Effect.orDie, + Effect.map((row) => (row?.agent ? Agent.ID.make(row.agent) : undefined)), + ) + }, getModel() { return db .select({ model: SessionTable.model }) @@ -398,12 +410,15 @@ const layer = Layer.effectDiscard( db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie), ) yield* bus.project(SessionEvent.AgentSelected, (event) => - db - .update(SessionTable) - .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) }) - .where(eq(SessionTable.id, event.data.sessionID)) - .run() - .pipe(Effect.orDie, Effect.andThen(run(db, event))), + Effect.gen(function* () { + yield* run(db, event) + yield* db + .update(SessionTable) + .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) }) + .where(eq(SessionTable.id, event.data.sessionID)) + .run() + .pipe(Effect.orDie) + }), ) yield* bus.project(SessionEvent.ModelSelected, (event) => Effect.gen(function* () { diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 56db03a2c00b..750a33adf690 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -648,7 +648,7 @@ describe("Session.create", () => { it.effect("switches the selected agent through the durable Session event", () => Effect.gen(function* () { const session = yield* Session.Service - const created = yield* session.create({ location }) + const created = yield* session.create({ location, agent: Agent.ID.make("build") }) yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("plan") }) @@ -656,6 +656,9 @@ describe("Session.create", () => { 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" } }]) + expect(yield* session.messages({ sessionID: created.id, order: "asc" })).toMatchObject([ + { type: "agent-switched", agent: "plan", previous: "build" }, + ]) }), ) diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index 2a2cedf89e0b..f45f769f2886 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -358,6 +358,7 @@ describe("SessionProjector", () => { directory: "/project", title: "test", version: "test", + agent: "plan", model: previousModel, }) .run() @@ -459,6 +460,10 @@ describe("SessionProjector", () => { text: "synthetic context", metadata: { source: "projector-test" }, }) + expect(messages.find((message) => message.type === "agent-switched")).toMatchObject({ + agent: build, + previous: "plan", + }) expect(messages.find((message) => message.type === "model-switched")).toMatchObject({ previous: previousModel }) expect(messages.find((message) => message.type === "shell")).toMatchObject({ command: "pwd", diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 6309514687bb..186566dfc6b8 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -12733,6 +12733,9 @@ }, "agent": { "type": "string" + }, + "previous": { + "type": "string" } }, "required": ["id", "time", "type", "agent"], diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index b7f775a59b0a..bafb95e8dc7e 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -42,6 +42,7 @@ export const AgentSelected = Schema.Struct({ ...Base, type: Schema.tag("agent-switched"), agent: Agent.ID, + previous: Agent.ID.pipe(optional), }).annotate({ identifier: "Session.Message.AgentSelected" }) export interface ModelSelected extends Schema.Schema.Type {} diff --git a/packages/www/openapi.json b/packages/www/openapi.json index 6309514687bb..186566dfc6b8 100644 --- a/packages/www/openapi.json +++ b/packages/www/openapi.json @@ -12733,6 +12733,9 @@ }, "agent": { "type": "string" + }, + "previous": { + "type": "string" } }, "required": ["id", "time", "type", "agent"], diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 6309514687bb..186566dfc6b8 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -12733,6 +12733,9 @@ }, "agent": { "type": "string" + }, + "previous": { + "type": "string" } }, "required": ["id", "time", "type", "agent"],