Compare commits

...

1 Commits

Author SHA1 Message Date
Aiden Cline a759399c89 feat(core): track previous agent in messages 2026-08-10 04:26:25 +00:00
5 changed files with 52 additions and 17 deletions
@@ -41,6 +41,7 @@ export type SessionMessageAgentSelected = {
time: { created: number } time: { created: number }
type: "agent-switched" type: "agent-switched"
agent: string agent: string
previous?: string
} }
export type PromptBase64 = string export type PromptBase64 = string
@@ -2535,6 +2536,7 @@ export type SessionImportInput = {
readonly time: { readonly created: number } readonly time: { readonly created: number }
readonly type: "agent-switched" readonly type: "agent-switched"
readonly agent: string readonly agent: string
readonly previous?: string
} }
| { | {
readonly id: string readonly id: string
@@ -2786,6 +2788,7 @@ export type SessionImportInput = {
readonly time: { readonly created: number } readonly time: { readonly created: number }
readonly type: "agent-switched" readonly type: "agent-switched"
readonly agent: string readonly agent: string
readonly previous?: string
} }
| { | {
readonly id: string readonly id: string
@@ -3037,6 +3040,7 @@ export type SessionImportInput = {
readonly time: { readonly created: number } readonly time: { readonly created: number }
readonly type: "agent-switched" readonly type: "agent-switched"
readonly agent: string readonly agent: string
readonly previous?: string
} }
| { | {
readonly id: string readonly id: string
+14 -9
View File
@@ -4,6 +4,7 @@ import { SessionEvent } from "./event"
import { SessionMessage } from "./message" import { SessionMessage } from "./message"
export interface Adapter { export interface Adapter {
readonly getAgent: () => Effect.Effect<SessionMessage.AgentSelected["agent"] | undefined, never, never>
readonly getModel: () => Effect.Effect<SessionMessage.ModelSelected["model"] | undefined, never, never> readonly getModel: () => Effect.Effect<SessionMessage.ModelSelected["model"] | undefined, never, never>
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never> readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
readonly getAssistant: ( readonly getAssistant: (
@@ -59,15 +60,19 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
"session.created": () => Effect.void, "session.created": () => Effect.void,
"session.usage.recorded": () => Effect.void, "session.usage.recorded": () => Effect.void,
"session.agent.selected": (event) => { "session.agent.selected": (event) => {
return adapter.appendMessage( return Effect.gen(function* () {
SessionMessage.AgentSelected.make({ const previous = yield* adapter.getAgent()
id: SessionMessage.ID.fromEvent(event.id), yield* adapter.appendMessage(
type: "agent-switched", SessionMessage.AgentSelected.make({
metadata: event.metadata, id: SessionMessage.ID.fromEvent(event.id),
agent: event.data.agent, type: "agent-switched",
time: { created: event.created }, metadata: event.metadata,
}), agent: event.data.agent,
) previous,
time: { created: event.created },
}),
)
})
}, },
"session.model.selected": (event) => { "session.model.selected": (event) => {
return Effect.gen(function* () { return Effect.gen(function* () {
+21 -6
View File
@@ -6,6 +6,7 @@ import { Database } from "../database/database"
import { Bus } from "../bus" import { Bus } from "../bus"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Model } from "../model" import { Model } from "../model"
import { Agent } from "../agent"
import { SessionEvent } from "./event" import { SessionEvent } from "./event"
import { SessionMessage } from "./message" import { SessionMessage } from "./message"
import { SessionMessageUpdater } from "./message-updater" import { SessionMessageUpdater } from "./message-updater"
@@ -230,6 +231,17 @@ function run(db: DatabaseService, event: MessageEvent) {
} }
const appendMessage = (message: SessionMessage.Info) => insertMessage(db, event, message) const appendMessage = (message: SessionMessage.Info) => insertMessage(db, event, message)
const adapter: SessionMessageUpdater.Adapter = { 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() { getModel() {
return db return db
.select({ model: SessionTable.model }) .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), db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie),
) )
yield* bus.project(SessionEvent.AgentSelected, (event) => yield* bus.project(SessionEvent.AgentSelected, (event) =>
db Effect.gen(function* () {
.update(SessionTable) yield* run(db, event)
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) }) yield* db
.where(eq(SessionTable.id, event.data.sessionID)) .update(SessionTable)
.run() .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) })
.pipe(Effect.orDie, Effect.andThen(run(db, event))), .where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
}),
) )
yield* bus.project(SessionEvent.ModelSelected, (event) => yield* bus.project(SessionEvent.ModelSelected, (event) =>
Effect.gen(function* () { Effect.gen(function* () {
+12 -2
View File
@@ -358,6 +358,7 @@ describe("SessionProjector", () => {
directory: "/project", directory: "/project",
title: "test", title: "test",
version: "test", version: "test",
agent: build,
model: previousModel, model: previousModel,
}) })
.run() .run()
@@ -366,7 +367,11 @@ describe("SessionProjector", () => {
yield* bus.publish(SessionEvent.AgentSelected, { yield* bus.publish(SessionEvent.AgentSelected, {
sessionID, sessionID,
agent: build, agent: Agent.ID.make("plan"),
})
yield* bus.publish(SessionEvent.AgentSelected, {
sessionID,
agent: Agent.ID.make("general"),
}) })
yield* bus.publish(SessionEvent.ModelSelected, { yield* bus.publish(SessionEvent.ModelSelected, {
sessionID, sessionID,
@@ -449,6 +454,7 @@ describe("SessionProjector", () => {
) )
expect(messages.map((message) => message.type)).toEqual([ expect(messages.map((message) => message.type)).toEqual([
"agent-switched",
"agent-switched", "agent-switched",
"model-switched", "model-switched",
"synthetic", "synthetic",
@@ -459,6 +465,10 @@ describe("SessionProjector", () => {
text: "synthetic context", text: "synthetic context",
metadata: { source: "projector-test" }, metadata: { source: "projector-test" },
}) })
expect(messages.filter((message) => message.type === "agent-switched")).toMatchObject([
{ agent: "plan", previous: "build" },
{ agent: "general", previous: "plan" },
])
expect(messages.find((message) => message.type === "model-switched")).toMatchObject({ previous: previousModel }) expect(messages.find((message) => message.type === "model-switched")).toMatchObject({ previous: previousModel })
expect(messages.find((message) => message.type === "shell")).toMatchObject({ expect(messages.find((message) => message.type === "shell")).toMatchObject({
command: "pwd", command: "pwd",
@@ -474,7 +484,7 @@ describe("SessionProjector", () => {
expect( expect(
yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie), yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie),
).toMatchObject({ ).toMatchObject({
agent: "build", agent: "general",
model, model,
time_updated: DateTime.toEpochMillis(created), time_updated: DateTime.toEpochMillis(created),
}) })
+1
View File
@@ -42,6 +42,7 @@ export const AgentSelected = Schema.Struct({
...Base, ...Base,
type: Schema.tag("agent-switched"), type: Schema.tag("agent-switched"),
agent: Agent.ID, agent: Agent.ID,
previous: Agent.ID.pipe(optional),
}).annotate({ identifier: "Session.Message.AgentSelected" }) }).annotate({ identifier: "Session.Message.AgentSelected" })
export interface ModelSelected extends Schema.Schema.Type<typeof ModelSelected> {} export interface ModelSelected extends Schema.Schema.Type<typeof ModelSelected> {}