refactor(schema): rename V2 session events and normalize payloads (#35217)

This commit is contained in:
Kit Langton 2026-07-03 14:25:59 -04:00 committed by GitHub
commit 394e0b9045
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 1888 additions and 1989 deletions

View file

@ -255,7 +255,7 @@ describe("DatabaseMigration", () => {
)
yield* db.run(sql`INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 9)`)
yield* db.run(
sql`INSERT INTO event (id, aggregate_id, seq, type, data) VALUES ('event', 'session', 9, 'session.updated.1', '{}')`,
sql`INSERT INTO event (id, aggregate_id, seq, type, data, created) VALUES ('event', 'session', 9, 'session.updated.1', '{}', 1)`,
)
yield* db.run(
sql`INSERT INTO session_input (id, session_id, prompt, delivery, admitted_seq, time_created) VALUES ('input', 'session', '{}', 'steer', 9, 1)`,

View file

@ -554,6 +554,7 @@ describe("EventV2", () => {
yield* events.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -573,6 +574,7 @@ describe("EventV2", () => {
yield* events.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -609,6 +611,7 @@ describe("EventV2", () => {
const exit = yield* events
.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID: envelopeAggregateID,
@ -642,6 +645,7 @@ describe("EventV2", () => {
yield* events.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -650,6 +654,7 @@ describe("EventV2", () => {
const exit = yield* events
.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 5,
aggregateID,
@ -674,13 +679,14 @@ describe("EventV2", () => {
yield* events.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1),
seq: 0,
aggregateID,
data: { sessionID: aggregateID, messageID: "msg_context", timestamp: 0, text: "context" },
data: { sessionID: aggregateID, text: "context" },
})
expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0))
expect(received[0]?.created).toEqual(DateTime.makeUnsafe(0))
}),
)
@ -690,6 +696,7 @@ describe("EventV2", () => {
const exit = yield* events
.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "unknown.event.1",
seq: 0,
aggregateID: EventV2.ID.create(),
@ -708,6 +715,7 @@ describe("EventV2", () => {
const source = yield* events.replayAll([
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -715,6 +723,7 @@ describe("EventV2", () => {
},
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@ -735,6 +744,7 @@ describe("EventV2", () => {
const one = yield* events.replayAll([
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -742,6 +752,7 @@ describe("EventV2", () => {
},
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@ -751,6 +762,7 @@ describe("EventV2", () => {
const two = yield* events.replayAll([
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 2,
aggregateID,
@ -758,6 +770,7 @@ describe("EventV2", () => {
},
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 3,
aggregateID,
@ -793,6 +806,7 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@ -812,6 +826,7 @@ describe("EventV2", () => {
const id = EventV2.ID.create()
const replayed = {
id,
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -833,6 +848,7 @@ describe("EventV2", () => {
const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned"))
const replayed = {
id: published.id,
created: published.created,
type: EventV2.versionedType(DurableMessage.type, 1),
seq: published.durable!.seq,
aggregateID,
@ -867,6 +883,7 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -895,6 +912,7 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@ -905,6 +923,7 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 2,
aggregateID,
@ -937,6 +956,7 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -949,6 +969,7 @@ describe("EventV2", () => {
.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@ -970,6 +991,7 @@ describe("EventV2", () => {
yield* events.listen((event) => Effect.sync(() => received.push(event)))
const replayed = {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -990,6 +1012,7 @@ describe("EventV2", () => {
const aggregateID = Session.ID.create()
const replayed = {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -1014,6 +1037,7 @@ describe("EventV2", () => {
const id = EventV2.ID.create()
yield* events.replay({
id,
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -1023,6 +1047,7 @@ describe("EventV2", () => {
const exit = yield* events
.replay({
id,
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@ -1045,6 +1070,7 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@ -1055,6 +1081,7 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@ -1116,6 +1143,7 @@ describe("EventV2", () => {
yield* events.replay({
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,

View file

@ -85,17 +85,13 @@ describe("SessionV2.compact", () => {
const prompt = Prompt.make({ text: "Please compact this session history." })
yield* events.publish(SessionEvent.PromptAdmitted, {
sessionID: created.id,
messageID,
timestamp: DateTime.makeUnsafe(0),
inputID: messageID,
prompt,
delivery: "steer",
})
yield* events.publish(SessionEvent.Prompted, {
yield* events.publish(SessionEvent.PromptPromoted, {
sessionID: created.id,
messageID,
timestamp: DateTime.makeUnsafe(0),
prompt,
delivery: "steer",
inputID: messageID,
})
yield* session.compact({ sessionID: created.id })

View file

@ -197,8 +197,6 @@ describe("SessionV2.create", () => {
yield* SessionInput.promoteSteers(db, events, parent.id)
yield* events.publish(SessionEvent.Synthetic, {
sessionID: parent.id,
messageID: SessionMessage.ID.create(),
timestamp: yield* DateTime.now,
text: "parent note",
})
@ -215,7 +213,7 @@ describe("SessionV2.create", () => {
expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id))
expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({
type: "session.next.forked",
type: "forked",
durable: { seq: 0 },
data: { sessionID: forked.id, parentID: parent.id },
})
@ -267,7 +265,7 @@ describe("SessionV2.create", () => {
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
expect(context).toMatchObject([{ text: "First" }])
expect(context[0]?.id).not.toBe(first.id)
expect(history[0]).toMatchObject({ data: { messageID: second.id } })
expect(history[0]).toMatchObject({ data: { from: second.id } })
}),
)
@ -380,8 +378,8 @@ describe("SessionV2.create", () => {
expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)),
).toMatchObject([
{ durable: { seq: 1 }, type: "session.next.prompt.admitted", data: { prompt: { text: "Hello" } } },
{ durable: { seq: 2 }, type: "session.next.prompted" },
{ durable: { seq: 1 }, type: "prompt.admitted", data: { prompt: { text: "Hello" } } },
{ durable: { seq: 2 }, type: "prompt.promoted" },
])
}),
)
@ -406,6 +404,7 @@ describe("SessionV2.create", () => {
.all()
.pipe(Effect.orDie)).map((event) => ({
id: event.id,
created: DateTime.makeUnsafe(event.created),
aggregateID: event.aggregate_id,
seq: event.seq,
type: event.type,
@ -466,7 +465,7 @@ describe("SessionV2.create", () => {
).toEqual([
[0, EventV2.versionedType(SessionV1.Event.Created.type, 1)],
[1, EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1)],
[2, EventV2.versionedType(SessionEvent.Prompted.type, 1)],
[2, EventV2.versionedType(SessionEvent.PromptPromoted.type, 1)],
])
}).pipe(Effect.provide(Layer.fresh(targetLayer)))
}),
@ -530,7 +529,7 @@ describe("SessionV2.create", () => {
expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" })
expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)),
).toMatchObject([{ type: "session.next.agent.switched", data: { agent: "plan" } }])
).toMatchObject([{ type: "agent.selected", data: { agent: "plan" } }])
}),
)
@ -563,7 +562,7 @@ describe("SessionV2.create", () => {
expect(yield* session.get(created.id)).toMatchObject({ model })
expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)),
).toMatchObject([{ type: "session.next.model.switched", data: { model } }])
).toMatchObject([{ type: "model.selected", data: { model } }])
}),
)

View file

@ -133,8 +133,6 @@ const seedSynthetic = (sessionID: SessionV2.ID, paths: string[]) =>
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.Synthetic, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: yield* DateTime.now,
text: `Instructions from: ${paths[0]}\nprior`,
description: `Loaded ${paths[0]}`,
metadata: { instruction: { paths } },

View file

@ -47,7 +47,7 @@ describe("SessionV2.log", () => {
// Session creation commits a non-public durable event, so the marker's
// seq covers more of the aggregate than the public events emitted.
expect(items.map((item) => item.type)).toEqual(["session.next.renamed", "log.synced"])
expect(items.map((item) => item.type)).toEqual(["renamed", "log.synced"])
expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: watermark })
}),
)
@ -64,7 +64,7 @@ describe("SessionV2.log", () => {
yield* session.rename({ sessionID: created.id, title: "renamed live" })
const items = Array.from(yield* Fiber.join(fiber))
expect(items.map((item) => item.type)).toEqual(["log.synced", "session.next.renamed"])
expect(items.map((item) => item.type)).toEqual(["log.synced", "renamed"])
}),
)

View file

@ -79,7 +79,6 @@ describe("SessionProjector", () => {
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.RevertEvent.Staged, {
sessionID,
timestamp: DateTime.makeUnsafe(1),
revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), diff: "patch", files: [] },
})
expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toMatchObject({
@ -87,17 +86,15 @@ describe("SessionProjector", () => {
snapshot: "tree",
files: [],
})
yield* events.publish(SessionEvent.RevertEvent.Cleared, { sessionID, timestamp: DateTime.makeUnsafe(2) })
yield* events.publish(SessionEvent.RevertEvent.Cleared, { sessionID })
expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toBeNull()
yield* events.publish(SessionEvent.RevertEvent.Staged, {
sessionID,
timestamp: DateTime.makeUnsafe(3),
revert: { messageID: boundary, files: [] },
})
yield* events.publish(SessionEvent.RevertEvent.Committed, {
sessionID,
messageID: boundary,
timestamp: DateTime.makeUnsafe(4),
})
expect(
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
@ -131,37 +128,29 @@ describe("SessionProjector", () => {
yield* events.publish(SessionEvent.PromptAdmitted, {
sessionID,
messageID: SessionMessage.ID.make("msg_first"),
timestamp: created,
inputID: SessionMessage.ID.make("msg_first"),
prompt: Prompt.make({ text: "first" }),
delivery: "steer",
})
yield* events.publish(
SessionEvent.Prompted,
SessionEvent.PromptPromoted,
{
sessionID,
messageID: SessionMessage.ID.make("msg_first"),
timestamp: created,
prompt: Prompt.make({ text: "first" }),
delivery: "steer",
inputID: SessionMessage.ID.make("msg_first"),
},
{ id: EventV2.ID.make("evt_z") },
)
yield* events.publish(SessionEvent.PromptAdmitted, {
sessionID,
messageID: SessionMessage.ID.make("msg_second"),
timestamp: created,
inputID: SessionMessage.ID.make("msg_second"),
prompt: Prompt.make({ text: "second" }),
delivery: "steer",
})
yield* events.publish(
SessionEvent.Prompted,
SessionEvent.PromptPromoted,
{
sessionID,
messageID: SessionMessage.ID.make("msg_second"),
timestamp: created,
prompt: Prompt.make({ text: "second" }),
delivery: "steer",
inputID: SessionMessage.ID.make("msg_second"),
},
{ id: EventV2.ID.make("evt_a") },
)
@ -190,7 +179,7 @@ describe("SessionProjector", () => {
}).pipe(Effect.provide(sessionsLayer)),
)
it.effect("marks an inbox row promoted with the Prompted event sequence", () =>
it.effect("marks an inbox row promoted with the PromptPromoted event sequence", () =>
Effect.gen(function* () {
const { db } = yield* Database.Service
yield* db
@ -220,12 +209,9 @@ describe("SessionProjector", () => {
})
if (!admitted) return yield* Effect.die("Prompt admission failed")
const event = yield* events.publish(SessionEvent.Prompted, {
const event = yield* events.publish(SessionEvent.PromptPromoted, {
sessionID,
timestamp: admitted.timeCreated,
messageID: id,
prompt: Prompt.make({ text: "promote me" }),
delivery: "steer",
inputID: id,
})
expect(
@ -256,49 +242,36 @@ describe("SessionProjector", () => {
.pipe(Effect.orDie)
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.AgentSwitched, {
yield* events.publish(SessionEvent.AgentSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: created,
agent: "build",
})
yield* events.publish(SessionEvent.ModelSwitched, {
yield* events.publish(SessionEvent.ModelSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: created,
model,
})
yield* events.publish(SessionEvent.Synthetic, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: created,
text: "synthetic context",
metadata: { source: "projector-test" },
})
yield* events.publish(SessionEvent.Shell.Started, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: created,
callID: "shell-1",
command: "pwd",
})
yield* events.publish(SessionEvent.Shell.Ended, {
sessionID,
timestamp: DateTime.makeUnsafe(1),
callID: "shell-1",
output: "/project",
})
const compactionID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID,
messageID: compactionID,
timestamp: created,
reason: "manual",
})
yield* events.publish(SessionEvent.Compaction.Delta, {
sessionID,
messageID: compactionID,
timestamp: created,
text: "partial",
})
expect(
@ -319,8 +292,6 @@ describe("SessionProjector", () => {
).toEqual([])
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID,
messageID: compactionID,
timestamp: DateTime.makeUnsafe(1),
reason: "manual",
text: "summary",
recent: "recent context",
@ -350,7 +321,7 @@ describe("SessionProjector", () => {
})
expect(messages.find((message) => message.type === "shell")).toMatchObject({
output: "/project",
time: { completed: DateTime.makeUnsafe(1) },
time: { completed: DateTime.makeUnsafe(0) },
})
expect(messages.find((message) => message.type === "compaction")).toMatchObject({
summary: "summary",
@ -388,13 +359,20 @@ describe("SessionProjector", () => {
.pipe(Effect.orDie)
const events = yield* EventV2.Service
const id = SessionMessage.ID.make("msg_creator_collision")
const {
id: _,
type,
...data
} = encodeMessage({ id, sessionID, type: "synthetic", text: "existing", time: { created } })
yield* db
.insert(SessionMessageTable)
.values({ id, session_id: sessionID, type, seq: 0, time_created: 0, data })
.run()
yield* events.publish(SessionEvent.Synthetic, { sessionID, messageID: id, timestamp: created, text: "keep me" })
const exit = yield* events
.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID: id,
timestamp: created,
agent: "build",
model,
})
@ -464,7 +442,6 @@ describe("SessionProjector", () => {
const service = yield* EventV2.Service
yield* service.publish(SessionEvent.Step.Ended, {
sessionID,
timestamp: DateTime.makeUnsafe(1),
assistantMessageID: SessionMessage.ID.make("msg_assistant_2"),
finish: "stop",
cost: 0,
@ -485,7 +462,7 @@ describe("SessionProjector", () => {
expect(messages[1]).toMatchObject({
type: "assistant",
finish: "stop",
time: { completed: DateTime.makeUnsafe(1) },
time: { completed: DateTime.makeUnsafe(0) },
})
}),
)
@ -526,7 +503,6 @@ describe("SessionProjector", () => {
yield* service.publish(SessionEvent.Text.Started, {
sessionID,
assistantMessageID: SessionMessage.ID.make("msg_assistant_completed"),
timestamp: DateTime.makeUnsafe(3),
textID: "text-stale",
})

View file

@ -201,7 +201,6 @@ describe("SessionV2.prompt", () => {
yield* db.insert(SessionMessageTable).values(assistantRow(stale, 100)).run().pipe(Effect.orDie)
yield* events.publish(SessionEvent.RevertEvent.Staged, {
sessionID,
timestamp: yield* DateTime.now,
revert: { messageID: boundary.id, files: [] },
})
expect((yield* session.get(sessionID)).revert?.messageID).toBe(boundary.id)
@ -257,16 +256,16 @@ describe("SessionV2.prompt", () => {
const streamed = Array.from(yield* Fiber.join(fiber))
expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([
[0, "session.next.prompt.admitted"],
[1, "session.next.prompt.admitted"],
[2, "session.next.prompted"],
[3, "session.next.prompted"],
[0, "prompt.admitted"],
[1, "prompt.admitted"],
[2, "prompt.promoted"],
[3, "prompt.promoted"],
])
expect(
Array.from(
yield* publicEvents({ sessionID, after: streamed[0].durable?.seq }).pipe(Stream.take(1), Stream.runCollect),
).map((event): [number | undefined, string] => [event.durable?.seq, event.type]),
).toEqual([[1, "session.next.prompt.admitted"]])
).toEqual([[1, "prompt.admitted"]])
}),
)
@ -429,7 +428,7 @@ describe("SessionV2.prompt", () => {
{ concurrency: "unbounded" },
)
expect(yield* eventCount(EventV2.versionedType(SessionEvent.Prompted.type, 1))).toBe(1)
expect(yield* eventCount(EventV2.versionedType(SessionEvent.PromptPromoted.type, 1))).toBe(1)
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 1 })
expect(yield* session.messages({ sessionID })).toMatchObject([
{ id: messageID, type: "user", text: "Promote once" },
@ -467,6 +466,7 @@ describe("SessionV2.prompt", () => {
yield* events.replayAll(
recorded.map((event) => ({
id: event.id,
created: DateTime.makeUnsafe(event.created),
aggregateID: event.aggregate_id,
seq: event.seq,
type: event.type,
@ -514,13 +514,23 @@ describe("SessionV2.prompt", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.Synthetic, {
const { db } = yield* Database.Service
const {
id: _,
type,
...data
} = encodeMessage({
id: messageID,
sessionID,
messageID,
timestamp: yield* DateTime.now,
type: "synthetic",
text: "Existing history",
time: { created: DateTime.makeUnsafe(0) },
})
yield* db
.insert(SessionMessageTable)
.values({ id: messageID, session_id: sessionID, type, seq: 0, time_created: 0, data })
.run()
.pipe(Effect.orDie)
const failure = yield* session
.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "Conflicting prompt" }), resume: false })

View file

@ -51,13 +51,13 @@ describe("toLLMMessages", () => {
const file = FileAttachment.make({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
const messages = toLLMMessages(
[
SessionMessage.AgentSwitched.make({
SessionMessage.AgentSelected.make({
id: id("agent"),
type: "agent-switched",
agent: "build",
time: { created },
}),
SessionMessage.ModelSwitched.make({
SessionMessage.ModelSelected.make({
id: id("model"),
type: "model-switched",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },

View file

@ -193,12 +193,12 @@ describe("SessionRunnerLLM recorded", () => {
.orderBy(EventTable.seq)
.all()).map((event) => event.type),
).toEqual([
"session.next.prompt.admitted.1",
"session.next.prompted.1",
"session.next.step.started.1",
"session.next.text.started.1",
"session.next.text.ended.1",
"session.next.step.ended.2",
"prompt.admitted.1",
"prompt.promoted.1",
"step.started.1",
"text.started.1",
"text.ended.1",
"step.ended.1",
])
}),
)

View file

@ -76,7 +76,7 @@ test("local tool success serializes media base64 once and reconstructs from stru
await Effect.runPromise(publisher.publish(call))
await Effect.runPromise(publisher.publish(result))
const success = published.find((event) => event.type === "session.next.tool.success.1")
const success = published.find((event) => event.type === "tool.success.1")
expect(success).toBeDefined()
const serialized = JSON.stringify(success)
expect(serialized.split(base64)).toHaveLength(2)
@ -94,7 +94,7 @@ test("provider-executed success retains its compatibility result", async () => {
const { published, publisher } = capture()
await Effect.runPromise(publisher.publish(LLMEvent.toolCall({ ...call, providerExecuted: true })))
await Effect.runPromise(publisher.publish(LLMEvent.toolResult({ ...result, providerExecuted: true })))
const success = published.find((event) => event.type === "session.next.tool.success.1")
const success = published.find((event) => event.type === "tool.success.1")
expect(success?.data).toHaveProperty("result")
})
@ -110,14 +110,13 @@ test("binary failure emits no success event", async () => {
}),
),
)
expect(published.some((event) => event.type === "session.next.tool.success.1")).toBe(false)
expect(published.some((event) => event.type === "session.next.tool.failed.1")).toBe(true)
expect(published.some((event) => event.type === "tool.success.1")).toBe(false)
expect(published.some((event) => event.type === "tool.failed.1")).toBe(true)
})
test("old success event data containing result still decodes", () => {
const decoded = Schema.decodeUnknownSync(SessionEvent.Tool.Success.data)({
sessionID,
timestamp: Date.now(),
assistantMessageID: SessionMessage.ID.create(),
callID: "call-old",
structured: { type: "media", mime: "image/png" },
@ -133,6 +132,6 @@ test("step finish records settlement without publishing step ended", async () =>
await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 })))
await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: "stop" })))
expect(published.some((event) => event.type === "session.next.step.ended.2")).toBe(false)
expect(published.some((event) => event.type === "step.ended.2")).toBe(false)
expect(publisher.stepSettlement()).toMatchObject({ finish: "stop" })
})

View file

@ -423,6 +423,7 @@ const replaySessionProjection = (id: SessionV2.ID) =>
yield* events.replayAll(
recorded.map((event) => ({
id: event.id,
created: DateTime.makeUnsafe(event.created),
aggregateID: event.aggregate_id,
seq: event.seq,
type: event.type,
@ -740,7 +741,6 @@ describe("SessionRunnerLLM", () => {
yield* events.publish(SessionEvent.Moved, {
sessionID,
timestamp: DateTime.makeUnsafe(1),
location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }),
})
expect(
@ -848,7 +848,7 @@ describe("SessionRunnerLLM", () => {
yield* db
.select({ id: EventTable.id })
.from(EventTable)
.where(eq(EventTable.type, "session.next.context.updated.1"))
.where(eq(EventTable.type, "session.context.updated.1"))
.all()
.pipe(Effect.orDie),
).toHaveLength(1)
@ -1010,10 +1010,8 @@ describe("SessionRunnerLLM", () => {
response = []
yield* session.resume(sessionID)
skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills")
yield* events.publish(SessionEvent.AgentSwitched, {
yield* events.publish(SessionEvent.AgentSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(1),
agent: "reviewer",
})
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
@ -1039,10 +1037,8 @@ describe("SessionRunnerLLM", () => {
if (switched) return Effect.void
switched = true
return events
.publish(SessionEvent.AgentSwitched, {
.publish(SessionEvent.AgentSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(1),
agent: "reviewer",
})
.pipe(Effect.asVoid)
@ -1069,10 +1065,8 @@ describe("SessionRunnerLLM", () => {
if (switched) return Effect.void
switched = true
return events
.publish(SessionEvent.ModelSwitched, {
.publish(SessionEvent.ModelSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(1),
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
.pipe(Effect.asVoid)
@ -1175,10 +1169,8 @@ describe("SessionRunnerLLM", () => {
systemBaseline = "Changed context"
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.ModelSwitched, {
yield* events.publish(SessionEvent.ModelSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(1),
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
systemBaseline = "Replacement context"
@ -1217,10 +1209,8 @@ describe("SessionRunnerLLM", () => {
requests.length = 0
response = []
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.ModelSwitched, {
yield* events.publish(SessionEvent.ModelSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(1),
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
systemUnavailable = true
@ -1252,14 +1242,10 @@ describe("SessionRunnerLLM", () => {
const compactionID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID,
messageID: compactionID,
timestamp: DateTime.makeUnsafe(1),
reason: "manual",
})
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID,
messageID: compactionID,
timestamp: DateTime.makeUnsafe(2),
reason: "manual",
text: "summary",
recent: "",
@ -1482,14 +1468,10 @@ describe("SessionRunnerLLM", () => {
const compactionID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID,
messageID: compactionID,
timestamp: DateTime.makeUnsafe(1),
reason: "manual",
})
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID,
messageID: compactionID,
timestamp: DateTime.makeUnsafe(2),
reason: "manual",
text: "summary",
recent: "",
@ -1686,10 +1668,8 @@ describe("SessionRunnerLLM", () => {
toolExecutionsReady = 1
const run = yield* Effect.forkChild(session.resume(sessionID))
yield* Deferred.await(toolExecutionsStarted)
yield* events.publish(SessionEvent.ModelSwitched, {
yield* events.publish(SessionEvent.ModelSelected, {
sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(1),
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
systemBaseline = "Replacement context"
@ -2403,27 +2383,23 @@ describe("SessionRunnerLLM", () => {
yield* events.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
timestamp: yield* DateTime.now,
agent: "build",
model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") },
})
yield* events.publish(SessionEvent.Tool.Input.Started, {
sessionID,
timestamp: yield* DateTime.now,
assistantMessageID,
callID: "call-interrupted",
name: "echo",
})
yield* events.publish(SessionEvent.Tool.Input.Ended, {
sessionID,
timestamp: yield* DateTime.now,
assistantMessageID,
callID: "call-interrupted",
text: '{"text":"stale"}',
})
yield* events.publish(SessionEvent.Tool.Called, {
sessionID,
timestamp: yield* DateTime.now,
assistantMessageID,
callID: "call-interrupted",
tool: "echo",
@ -2467,27 +2443,23 @@ describe("SessionRunnerLLM", () => {
yield* events.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
timestamp: yield* DateTime.now,
agent: "build",
model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") },
})
yield* events.publish(SessionEvent.Tool.Input.Started, {
sessionID,
timestamp: yield* DateTime.now,
assistantMessageID,
callID: "call-hosted-interrupted",
name: "web_search",
})
yield* events.publish(SessionEvent.Tool.Input.Ended, {
sessionID,
timestamp: yield* DateTime.now,
assistantMessageID,
callID: "call-hosted-interrupted",
text: '{"query":"stale"}',
})
yield* events.publish(SessionEvent.Tool.Called, {
sessionID,
timestamp: yield* DateTime.now,
assistantMessageID,
callID: "call-hosted-interrupted",
tool: "web_search",
@ -2527,13 +2499,11 @@ describe("SessionRunnerLLM", () => {
yield* events.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
timestamp: yield* DateTime.now,
agent: "build",
model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") },
})
yield* events.publish(SessionEvent.Tool.Input.Started, {
sessionID,
timestamp: yield* DateTime.now,
assistantMessageID,
callID: "call-pending-interrupted",
name: "echo",
@ -2578,7 +2548,7 @@ describe("SessionRunnerLLM", () => {
const events = yield* EventV2.Service
const defect = new Error("fail after prompt promotion")
let fail = true
yield* events.project(SessionEvent.Prompted, () => (fail ? Effect.die(defect) : Effect.void))
yield* events.project(SessionEvent.PromptPromoted, () => (fail ? Effect.die(defect) : Effect.void))
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover promoted input" }), resume: false })
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
@ -2603,7 +2573,9 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* events.listen((event) =>
event.type === SessionEvent.Prompted.type ? Effect.die("fail after prompt promotion commits") : Effect.void,
event.type === SessionEvent.PromptPromoted.type
? Effect.die("fail after prompt promotion commits")
: Effect.void,
)
yield* session.prompt({
sessionID,
@ -3013,7 +2985,7 @@ describe("SessionRunnerLLM", () => {
{ type: "user", text: "Interrupt provider" },
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Provider turn interrupted" } },
])
expect(yield* recordedEventTypes(sessionID)).toContain("session.next.step.failed.2")
expect(yield* recordedEventTypes(sessionID)).toContain("step.failed.1")
yield* session.interrupt(sessionID)
}),
)
@ -3057,8 +3029,8 @@ describe("SessionRunnerLLM", () => {
},
])
const eventTypes = yield* recordedEventTypes(sessionID)
expect(eventTypes).toContain("session.next.step.failed.2")
expect(eventTypes).not.toContain("session.next.step.ended.2")
expect(eventTypes).toContain("step.failed.1")
expect(eventTypes).not.toContain("step.ended.1")
}),
)

View file

@ -86,17 +86,13 @@ const prompt = (sessionID: SessionV2.ID, text: string) =>
const messageID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.PromptAdmitted, {
sessionID,
messageID,
timestamp: DateTime.makeUnsafe(0),
inputID: messageID,
prompt: Prompt.make({ text }),
delivery: "steer",
})
yield* events.publish(SessionEvent.Prompted, {
yield* events.publish(SessionEvent.PromptPromoted, {
sessionID,
messageID,
timestamp: DateTime.makeUnsafe(0),
prompt: Prompt.make({ text }),
delivery: "steer",
inputID: messageID,
})
})

View file

@ -51,7 +51,6 @@ describe("Tool.Progress", () => {
yield* service.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
timestamp,
agent: "build",
model,
})
@ -69,14 +68,12 @@ describe("Tool.Progress", () => {
Effect.gen(function* () {
yield* service.publish(SessionEvent.Tool.Input.Started, {
sessionID,
timestamp,
assistantMessageID,
callID,
name: "bash",
})
yield* service.publish(SessionEvent.Tool.Called, {
sessionID,
timestamp,
assistantMessageID,
callID,
tool: "bash",
@ -92,7 +89,6 @@ describe("Tool.Progress", () => {
yield* service.publish(SessionEvent.Tool.Progress, {
sessionID,
timestamp,
assistantMessageID,
callID: "call-success",
structured: { phase: "checkpoint" },
@ -104,7 +100,6 @@ describe("Tool.Progress", () => {
const success = yield* service.publish(SessionEvent.Tool.Success, {
sessionID,
timestamp,
assistantMessageID,
callID: "call-success",
structured: { phase: "done" },
@ -118,7 +113,6 @@ describe("Tool.Progress", () => {
yield* start("call-failed")
yield* service.publish(SessionEvent.Tool.Progress, {
sessionID,
timestamp,
assistantMessageID,
callID: "call-failed",
structured: { phase: "checkpoint" },
@ -126,7 +120,6 @@ describe("Tool.Progress", () => {
})
const failed = yield* service.publish(SessionEvent.Tool.Failed, {
sessionID,
timestamp,
assistantMessageID,
callID: "call-failed",
error: { type: "unknown", message: "boom" },

View file

@ -148,8 +148,8 @@ test("Core reuses the canonical shared schemas", async () => {
[coreSessionInput.Admitted, SessionInput.Admitted],
[coreSessionMessage.ID, SessionMessage.ID],
[coreSessionMessage.UnknownError, SessionMessage.UnknownError],
[coreSessionMessage.AgentSwitched, SessionMessage.AgentSwitched],
[coreSessionMessage.ModelSwitched, SessionMessage.ModelSwitched],
[coreSessionMessage.AgentSelected, SessionMessage.AgentSelected],
[coreSessionMessage.ModelSelected, SessionMessage.ModelSelected],
[coreSessionMessage.User, SessionMessage.User],
[coreSessionMessage.Synthetic, SessionMessage.Synthetic],
[coreSessionMessage.System, SessionMessage.System],

View file

@ -79,27 +79,23 @@ const executionNode = makeGlobalNode({
yield* events.publish(SessionEvent.Step.Started, {
sessionID: id,
assistantMessageID,
timestamp: yield* DateTime.now,
agent: session.agent ?? AgentV2.ID.make("code"),
model: sessionModel,
})
yield* events.publish(SessionEvent.Text.Started, {
sessionID: id,
assistantMessageID,
timestamp: yield* DateTime.now,
textID,
})
yield* events.publish(SessionEvent.Text.Ended, {
sessionID: id,
assistantMessageID,
timestamp: yield* DateTime.now,
textID,
text: "ok",
})
yield* events.publish(SessionEvent.Step.Ended, {
sessionID: id,
assistantMessageID,
timestamp: yield* DateTime.now,
finish: "stop",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },

View file

@ -53,27 +53,23 @@ const executionNode = makeGlobalNode({
yield* events.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
timestamp: yield* DateTime.now,
agent: AgentV2.ID.make("reviewer"),
model: childModel,
})
yield* events.publish(SessionEvent.Text.Started, {
sessionID,
assistantMessageID,
timestamp: yield* DateTime.now,
textID,
})
yield* events.publish(SessionEvent.Text.Ended, {
sessionID,
assistantMessageID,
timestamp: yield* DateTime.now,
textID,
text: childText,
})
yield* events.publish(SessionEvent.Step.Ended, {
sessionID,
assistantMessageID,
timestamp: yield* DateTime.now,
finish: "stop",
cost: 0,
tokens,