refactor(schema): rename V2 session events and normalize payloads (#35217)
This commit is contained in:
parent
4790a2772c
commit
394e0b9045
63 changed files with 1888 additions and 1989 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,17 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { DateTime, Effect, Stream } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { AbsolutePath, Agent, Event, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect/index"
|
||||
import {
|
||||
AbsolutePath,
|
||||
Agent,
|
||||
Event,
|
||||
Location,
|
||||
Model,
|
||||
OpenCode,
|
||||
Prompt,
|
||||
Session,
|
||||
SessionMessage,
|
||||
} from "../src/effect/index"
|
||||
|
||||
const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
|
||||
|
||||
|
|
@ -23,7 +33,7 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
|||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
new Response(
|
||||
`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
|
||||
`data: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` +
|
||||
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
),
|
||||
|
|
@ -35,10 +45,10 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
|||
return yield* client.event.subscribe().pipe(Stream.runCollect)
|
||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||
|
||||
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"])
|
||||
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "model.selected"])
|
||||
const durable = events[1]
|
||||
if (durable?.type !== "session.next.model.switched") throw new Error("Expected model event")
|
||||
expect(DateTime.toEpochMillis(durable.data.timestamp)).toBe(1_717_171_717_000)
|
||||
if (durable?.type !== "model.selected") throw new Error("Expected model event")
|
||||
expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000)
|
||||
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
||||
})
|
||||
|
||||
|
|
@ -149,8 +159,8 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||
expect(result.context).toEqual([])
|
||||
expect(logQueries[0]).toEqual({ after: "0" })
|
||||
const logged = Array.from(result.log)
|
||||
expect(logged.map((item) => item.type)).toEqual(["session.next.model.switched", "log.synced"])
|
||||
expect(logged[0]?.type === "session.next.model.switched" && DateTime.toEpochMillis(logged[0].data.timestamp)).toBe(
|
||||
expect(logged.map((item) => item.type)).toEqual(["model.selected", "log.synced"])
|
||||
expect(logged[0]?.type === "model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
|
||||
1_717_171_717_000,
|
||||
)
|
||||
expect(logged.at(-1)).toEqual(synced)
|
||||
|
|
@ -217,12 +227,11 @@ const modelSwitchedMessage = {
|
|||
|
||||
const modelSwitchedEvent = {
|
||||
id: "evt_model",
|
||||
type: "session.next.model.switched",
|
||||
created: 1_717_171_717_000,
|
||||
type: "model.selected",
|
||||
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
|
||||
data: {
|
||||
timestamp: 1_717_171_717_000,
|
||||
sessionID: "ses_test",
|
||||
messageID: "msg_model",
|
||||
model: { id: "claude", providerID: "anthropic" },
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
|
|||
baseUrl: "http://localhost:3000",
|
||||
fetch: async () =>
|
||||
new Response(
|
||||
`: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
|
||||
`: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` +
|
||||
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
),
|
||||
|
|
@ -159,8 +159,8 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
|
|||
const events = []
|
||||
for await (const event of client.event.subscribe()) events.push(event)
|
||||
|
||||
expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
|
||||
expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
|
||||
expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent])
|
||||
expect(events[1]?.type === "model.selected" && events[1].created).toBe(1_717_171_717_000)
|
||||
})
|
||||
|
||||
test("event.subscribe terminates on malformed Promise SSE data", async () => {
|
||||
|
|
@ -328,12 +328,11 @@ const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
|
|||
|
||||
const modelSwitchedEvent = {
|
||||
id: "evt_model",
|
||||
type: "session.next.model.switched",
|
||||
created: 1_717_171_717_000,
|
||||
type: "model.selected",
|
||||
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
|
||||
data: {
|
||||
timestamp: 1_717_171_717_000,
|
||||
sessionID: "ses_test",
|
||||
messageID: "msg_model",
|
||||
model: { id: "claude", providerID: "anthropic" },
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "22e57fed-b9b8-4e94-a3b4-f94bece680a8",
|
||||
"id": "96e9fe64-d810-4102-8f79-3317a88bb6d2",
|
||||
"prevIds": [
|
||||
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
|
||||
"22e57fed-b9b8-4e94-a3b4-f94bece680a8"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
|
|
@ -526,6 +526,16 @@
|
|||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "created",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
|
|
|
|||
|
|
@ -106,8 +106,7 @@ const layer = Layer.effect(
|
|||
yield* events.publish(SessionEvent.Moved, {
|
||||
sessionID: input.sessionID,
|
||||
location: Location.Ref.make({ directory }),
|
||||
subdirectory: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
|
||||
timestamp: yield* DateTime.now,
|
||||
subpath: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
|
||||
})
|
||||
|
||||
if (patch) {
|
||||
|
|
|
|||
2
packages/core/src/database/migration.gen.ts
generated
2
packages/core/src/database/migration.gen.ts
generated
|
|
@ -41,5 +41,7 @@ export const migrations = (
|
|||
import("./migration/20260622170816_reset_v2_session_state"),
|
||||
import("./migration/20260622202450_simplify_session_input"),
|
||||
import("./migration/20260702134641_add_session_context_entry"),
|
||||
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
|
||||
import("./migration/20260703181610_event_created_column"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260703090000_reset_v2_event_rename_sweep",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||
// `created` column is added by the generated 20260703181610_event_created_column
|
||||
// migration, which runs after this wipe (NOT NULL without default is safe on the
|
||||
// emptied table).
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260703181610_event_created_column",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`event\` ADD \`created\` integer NOT NULL;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -81,6 +81,7 @@ export default {
|
|||
\`id\` text PRIMARY KEY,
|
||||
\`aggregate_id\` text NOT NULL,
|
||||
\`seq\` integer NOT NULL,
|
||||
\`created\` integer NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export * as EventV2 from "./event"
|
||||
|
||||
import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
|
||||
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
||||
import type { EventLog } from "@opencode-ai/schema/event-log"
|
||||
|
|
@ -55,6 +55,7 @@ export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
|
|||
export type SerializedEvent = {
|
||||
readonly id: ID
|
||||
readonly type: string
|
||||
readonly created?: DateTime.Utc
|
||||
readonly seq: number
|
||||
readonly aggregateID: string
|
||||
readonly data: Record<string, unknown>
|
||||
|
|
@ -81,6 +82,7 @@ const decodeSerializedEvent = (event: SerializedEvent): Payload => {
|
|||
}
|
||||
return {
|
||||
id: event.id,
|
||||
created: event.created ?? DateTime.makeUnsafe(0),
|
||||
type: definition.type,
|
||||
durable: envelope(event.aggregateID, event.seq, definition.durable.version),
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
|
|
@ -295,6 +297,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
if (
|
||||
stored?.id === event.id &&
|
||||
stored.type === versionedType(definition.type, durable.version) &&
|
||||
stored.created === DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)) &&
|
||||
isDeepStrictEqual(stored.data, encoded)
|
||||
) {
|
||||
if (input.ownerID && row?.ownerID == null) {
|
||||
|
|
@ -366,6 +369,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
id: event.id,
|
||||
aggregate_id: aggregateID,
|
||||
seq,
|
||||
created: DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)),
|
||||
type: versionedType(definition.type, durable.version),
|
||||
data: encoded,
|
||||
},
|
||||
|
|
@ -471,6 +475,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
definition,
|
||||
{
|
||||
id: options?.id ?? ID.create(),
|
||||
created: yield* DateTime.now,
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(location ? { location } : {}),
|
||||
|
|
@ -494,6 +499,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
} else {
|
||||
const payload = {
|
||||
id: event.id,
|
||||
created: event.created ?? DateTime.makeUnsafe(0),
|
||||
type: definition.type,
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
} as Payload
|
||||
|
|
@ -610,6 +616,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
return [
|
||||
decodeSerializedEvent({
|
||||
id: event.id,
|
||||
created: DateTime.makeUnsafe(event.created),
|
||||
aggregateID: event.aggregate_id,
|
||||
seq: event.seq,
|
||||
type: event.type,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const EventTable = sqliteTable(
|
|||
.notNull()
|
||||
.references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),
|
||||
seq: integer().notNull(),
|
||||
created: integer().notNull(),
|
||||
type: text().notNull(),
|
||||
data: text({ mode: "json" }).$type<Record<string, unknown>>().notNull(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -363,8 +363,7 @@ const layer = Layer.effect(
|
|||
yield* events.publish(SessionEvent.Forked, {
|
||||
sessionID,
|
||||
parentID: parent.id,
|
||||
messageID: input.messageID,
|
||||
timestamp: yield* DateTime.now,
|
||||
from: input.messageID,
|
||||
})
|
||||
return yield* result.get(sessionID).pipe(Effect.orDie)
|
||||
}),
|
||||
|
|
@ -551,16 +550,13 @@ const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
activeShells.add(input.sessionID)
|
||||
if ((yield* execution.active).has(input.sessionID)) yield* execution.awaitIdle(input.sessionID)
|
||||
const messageID = SessionMessage.ID.create()
|
||||
const callID = Identifier.ascending()
|
||||
yield* events.publish(
|
||||
SessionEvent.Shell.Started,
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
messageID,
|
||||
callID,
|
||||
command: input.command,
|
||||
timestamp: yield* DateTime.now,
|
||||
},
|
||||
{ id: input.id },
|
||||
)
|
||||
|
|
@ -571,7 +567,6 @@ const layer = Layer.effect(
|
|||
sessionID: input.sessionID,
|
||||
callID,
|
||||
output,
|
||||
timestamp: yield* DateTime.now,
|
||||
})
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
|
|
@ -590,8 +585,6 @@ const layer = Layer.effect(
|
|||
if (!skill) return yield* new SkillNotFoundError({ skill: input.skill })
|
||||
yield* events.publish(SessionEvent.Skill.Activated, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.id ?? SessionMessage.ID.create(),
|
||||
timestamp: yield* DateTime.now,
|
||||
name: skill.name,
|
||||
text: skill.content,
|
||||
})
|
||||
|
|
@ -602,10 +595,8 @@ const layer = Layer.effect(
|
|||
}),
|
||||
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
yield* events.publish(SessionEvent.AgentSwitched, {
|
||||
yield* events.publish(SessionEvent.AgentSelected, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: yield* DateTime.now,
|
||||
agent: input.agent,
|
||||
})
|
||||
}),
|
||||
|
|
@ -617,10 +608,8 @@ const layer = Layer.effect(
|
|||
(session.model.variant ?? "default") === (input.model.variant ?? "default")
|
||||
)
|
||||
return
|
||||
yield* events.publish(SessionEvent.ModelSwitched, {
|
||||
yield* events.publish(SessionEvent.ModelSelected, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: yield* DateTime.now,
|
||||
model: input.model,
|
||||
})
|
||||
}),
|
||||
|
|
@ -628,7 +617,6 @@ const layer = Layer.effect(
|
|||
yield* result.get(input.sessionID)
|
||||
yield* events.publish(SessionEvent.Renamed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
title: input.title,
|
||||
})
|
||||
}),
|
||||
|
|
@ -676,8 +664,6 @@ const layer = Layer.effect(
|
|||
yield* result.get(input.sessionID)
|
||||
yield* events.publish(SessionEvent.Synthetic, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: yield* DateTime.now,
|
||||
text: input.text,
|
||||
description: input.description,
|
||||
metadata: input.metadata,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { EventV2 } from "../event"
|
|||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { llmClient } from "../effect/app-node-platform"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionMessage } from "./message"
|
||||
import type { SessionMessage } from "./message"
|
||||
import { SessionRunnerModel } from "./runner/model"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { Token } from "../util/token"
|
||||
|
|
@ -206,11 +206,8 @@ const make = (dependencies: Dependencies) => {
|
|||
const summaryPrompt = buildPrompt({ previousSummary: input.previousSummary, context: input.context })
|
||||
const summaryOutput = Math.min(output || SUMMARY_OUTPUT_TOKENS, SUMMARY_OUTPUT_TOKENS)
|
||||
if (Token.estimate(summaryPrompt) > context - summaryOutput) return false
|
||||
const messageID = SessionMessage.ID.create()
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Started, {
|
||||
sessionID: input.sessionID,
|
||||
messageID,
|
||||
timestamp: yield* DateTime.now,
|
||||
reason: input.reason,
|
||||
})
|
||||
|
||||
|
|
@ -238,8 +235,6 @@ const make = (dependencies: Dependencies) => {
|
|||
if (!summarized || failed || !summary.trim()) return false
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
messageID,
|
||||
timestamp: yield* DateTime.now,
|
||||
reason: input.reason,
|
||||
text: summary,
|
||||
recent: input.recent,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
export * as SessionContextCheckpoint from "./context-checkpoint"
|
||||
|
||||
import { eq } from "drizzle-orm"
|
||||
import { DateTime, Effect, Option, Schema } from "effect"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import type { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionHistory } from "./history"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionContextCheckpointTable } from "./sql"
|
||||
|
||||
|
|
@ -50,7 +49,7 @@ export const prepare = Effect.fn("SessionContextCheckpoint.prepare")(function* (
|
|||
|
||||
yield* events.publish(
|
||||
SessionEvent.ContextUpdated,
|
||||
{ sessionID, messageID: SessionMessage.ID.create(), timestamp: yield* DateTime.now, text: result.text },
|
||||
{ sessionID, text: result.text },
|
||||
{ commit: () => advance(db, sessionID, result.applied).pipe(Effect.orDie) },
|
||||
)
|
||||
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ const layer = Layer.effect(
|
|||
Exit.isFailure(exit) && !Cause.hasInterrupts(exit.cause) ? Cause.squash(exit.cause) : undefined
|
||||
yield* events.publish(SessionEvent.ExecutionSettled, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
outcome: Exit.isSuccess(exit) ? "success" : Cause.hasInterrupts(exit.cause) ? "interrupted" : "failure",
|
||||
error:
|
||||
failure !== undefined
|
||||
|
|
|
|||
|
|
@ -50,12 +50,10 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||
) {
|
||||
const existing = yield* find(db, input.id)
|
||||
if (existing !== undefined) return existing
|
||||
const timestamp = yield* DateTime.now
|
||||
return yield* events
|
||||
.publish(SessionEvent.PromptAdmitted, {
|
||||
messageID: input.id,
|
||||
inputID: input.id,
|
||||
sessionID: input.sessionID,
|
||||
timestamp,
|
||||
prompt: input.prompt,
|
||||
delivery: input.delivery,
|
||||
})
|
||||
|
|
@ -70,7 +68,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||
sessionID: input.sessionID,
|
||||
prompt: input.prompt,
|
||||
delivery: input.delivery,
|
||||
timeCreated: timestamp,
|
||||
timeCreated: event.created,
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -115,14 +113,11 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
|
|||
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const projectPrompted = Effect.fn("SessionInput.projectPrompted")(function* (
|
||||
export const projectPromptPromoted = Effect.fn("SessionInput.projectPromptPromoted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly prompt: Prompt
|
||||
readonly delivery: Delivery
|
||||
readonly timeCreated: DateTime.Utc
|
||||
readonly promotedSeq: number
|
||||
},
|
||||
) {
|
||||
|
|
@ -141,15 +136,16 @@ export const projectPrompted = Effect.fn("SessionInput.projectPrompted")(functio
|
|||
.pipe(Effect.orDie)
|
||||
if (updated) {
|
||||
const stored = fromRow(updated)
|
||||
if (!matchesProjection(stored, input)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return
|
||||
if (stored.sessionID !== input.sessionID) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return stored
|
||||
}
|
||||
|
||||
// Every Prompted event is published from an admitted inbox row, so a missing or
|
||||
// Every PromptPromoted event is published from an admitted inbox row, so a missing or
|
||||
// divergent row on replay is an invariant violation.
|
||||
const stored = yield* find(db, input.id)
|
||||
if (!stored || !matchesProjection(stored, input) || stored.promotedSeq !== input.promotedSeq)
|
||||
if (!stored || stored.sessionID !== input.sessionID || stored.promotedSeq !== input.promotedSeq)
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return stored
|
||||
})
|
||||
|
||||
export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
||||
|
|
@ -206,12 +202,9 @@ const publish = Effect.fn("SessionInput.publish")(function* (
|
|||
for (const row of rows) {
|
||||
const id = SessionMessage.ID.make(row.id)
|
||||
yield* events
|
||||
.publish(SessionEvent.Prompted, {
|
||||
.publish(SessionEvent.PromptPromoted, {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(row.time_created),
|
||||
messageID: id,
|
||||
prompt: decodePrompt(row.prompt),
|
||||
delivery: row.delivery,
|
||||
inputID: id,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ const layer = Layer.effect(
|
|||
const files = yield* Effect.forEach(
|
||||
toInject,
|
||||
(path) =>
|
||||
fs.readFileStringSafe(path).pipe(
|
||||
Effect.map((content) => (content === undefined ? undefined : { path, content })),
|
||||
),
|
||||
fs
|
||||
.readFileStringSafe(path)
|
||||
.pipe(Effect.map((content) => (content === undefined ? undefined : { path, content }))),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
const readable = files.filter((file): file is { path: string; content: string } => file !== undefined)
|
||||
|
|
@ -74,8 +74,6 @@ const layer = Layer.effect(
|
|||
// metadata so it survives across Location layer restarts.
|
||||
yield* events.publish(SessionEvent.Synthetic, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: yield* DateTime.now,
|
||||
text: readable.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n"),
|
||||
description: `Loaded ${readable.map((file) => describePath(root, file.path)).join(", ")}`,
|
||||
metadata: { instruction: { paths: readable.map((file) => file.path) } },
|
||||
|
|
|
|||
|
|
@ -100,112 +100,100 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
|
||||
return Effect.gen(function* () {
|
||||
yield* SessionEvent.All.match(event, {
|
||||
"session.next.agent.switched": (event) => {
|
||||
"agent.selected": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.AgentSwitched.make({
|
||||
id: event.data.messageID,
|
||||
SessionMessage.AgentSelected.make({
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "agent-switched",
|
||||
metadata: event.metadata,
|
||||
agent: event.data.agent,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
},
|
||||
"session.next.model.switched": (event) => {
|
||||
"model.selected": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.ModelSwitched.make({
|
||||
id: event.data.messageID,
|
||||
SessionMessage.ModelSelected.make({
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "model-switched",
|
||||
metadata: event.metadata,
|
||||
model: event.data.model,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
},
|
||||
"session.next.moved": () => Effect.void,
|
||||
"session.next.renamed": () => Effect.void,
|
||||
"session.next.forked": () => Effect.void,
|
||||
"session.next.prompted": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.User.make({
|
||||
id: event.data.messageID,
|
||||
type: "user",
|
||||
metadata: event.metadata,
|
||||
text: event.data.prompt.text,
|
||||
files: event.data.prompt.files,
|
||||
agents: event.data.prompt.agents,
|
||||
time: { created: event.data.timestamp },
|
||||
}),
|
||||
)
|
||||
},
|
||||
"session.next.prompt.admitted": () => Effect.void,
|
||||
"session.next.execution.settled": () => Effect.void,
|
||||
"session.next.context.updated": (event) =>
|
||||
"session.moved": () => Effect.void,
|
||||
renamed: () => Effect.void,
|
||||
forked: () => Effect.void,
|
||||
"prompt.promoted": () => Effect.void,
|
||||
"prompt.admitted": () => Effect.void,
|
||||
"execution.settled": () => Effect.void,
|
||||
"session.context.updated": (event) =>
|
||||
adapter.appendMessage(
|
||||
SessionMessage.System.make({
|
||||
id: event.data.messageID,
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "system",
|
||||
text: event.data.text,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
),
|
||||
"session.next.synthetic": (event) => {
|
||||
synthetic: (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.Synthetic.make({
|
||||
sessionID: event.data.sessionID,
|
||||
text: event.data.text,
|
||||
description: event.data.description,
|
||||
metadata: event.data.metadata,
|
||||
id: event.data.messageID,
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "synthetic",
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
},
|
||||
"session.next.skill.activated": (event) => {
|
||||
"skill.activated": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.Skill.make({
|
||||
id: event.data.messageID,
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "skill",
|
||||
name: event.data.name,
|
||||
text: event.data.text,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
},
|
||||
"session.next.shell.started": (event) => {
|
||||
"shell.started": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.Shell.make({
|
||||
id: event.data.messageID,
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "shell",
|
||||
metadata: event.metadata,
|
||||
callID: event.data.callID,
|
||||
command: event.data.command,
|
||||
output: "",
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
},
|
||||
"session.next.shell.ended": (event) => {
|
||||
"shell.ended": (event) => {
|
||||
return Effect.gen(function* () {
|
||||
const currentShell = yield* adapter.getCurrentShell(event.data.callID)
|
||||
if (currentShell) {
|
||||
yield* adapter.updateShell(
|
||||
produce(currentShell, (draft) => {
|
||||
draft.output = event.data.output
|
||||
draft.time.completed = event.data.timestamp
|
||||
draft.time.completed = event.created
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
"session.next.step.started": (event) => {
|
||||
"step.started": (event) => {
|
||||
return Effect.gen(function* () {
|
||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||
if (currentAssistant) {
|
||||
yield* adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.time.completed = event.data.timestamp
|
||||
draft.time.completed = event.created
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -215,16 +203,16 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
type: "assistant",
|
||||
agent: event.data.agent,
|
||||
model: event.data.model,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
content: [],
|
||||
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
"session.next.step.ended": (event) => {
|
||||
"step.ended": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
draft.time.completed = event.data.timestamp
|
||||
draft.time.completed = event.created
|
||||
draft.finish = event.data.finish
|
||||
draft.cost = event.data.cost
|
||||
draft.tokens = event.data.tokens
|
||||
|
|
@ -236,33 +224,33 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
}
|
||||
})
|
||||
},
|
||||
"session.next.step.failed": (event) => {
|
||||
"step.failed": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
draft.time.completed = event.data.timestamp
|
||||
draft.time.completed = event.created
|
||||
draft.finish = "error"
|
||||
draft.error = event.data.error
|
||||
})
|
||||
},
|
||||
"session.next.text.started": (event) => {
|
||||
"text.started": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
draft.content.push(
|
||||
castDraft(SessionMessage.AssistantText.make({ type: "text", id: event.data.textID, text: "" })),
|
||||
)
|
||||
})
|
||||
},
|
||||
"session.next.text.delta": (event) => {
|
||||
"text.delta": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestText(draft, event.data.textID)
|
||||
if (match) match.text += event.data.delta
|
||||
})
|
||||
},
|
||||
"session.next.text.ended": (event) => {
|
||||
"text.ended": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestText(draft, event.data.textID)
|
||||
if (match) match.text = event.data.text
|
||||
})
|
||||
},
|
||||
"session.next.tool.input.started": (event) => {
|
||||
"tool.input.started": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
draft.content.push(
|
||||
castDraft(
|
||||
|
|
@ -270,26 +258,26 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
type: "tool",
|
||||
id: event.data.callID,
|
||||
name: event.data.name,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
state: SessionMessage.ToolStatePending.make({ status: "pending", input: "" }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
},
|
||||
"session.next.tool.input.delta": () => Effect.void,
|
||||
"session.next.tool.input.ended": (event) => {
|
||||
"tool.input.delta": () => Effect.void,
|
||||
"tool.input.ended": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && match.state.status === "pending") match.state.input = event.data.text
|
||||
})
|
||||
},
|
||||
"session.next.tool.called": (event) => {
|
||||
"tool.called": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match) {
|
||||
match.provider = event.data.provider
|
||||
match.time.ran = event.data.timestamp
|
||||
match.time.ran = event.created
|
||||
match.state = castDraft(
|
||||
SessionMessage.ToolStateRunning.make({
|
||||
status: "running",
|
||||
|
|
@ -301,7 +289,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
}
|
||||
})
|
||||
},
|
||||
"session.next.tool.progress": (event) => {
|
||||
"tool.progress": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && match.state.status === "running") {
|
||||
|
|
@ -310,7 +298,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
}
|
||||
})
|
||||
},
|
||||
"session.next.tool.success": (event) => {
|
||||
"tool.success": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && match.state.status === "running") {
|
||||
|
|
@ -319,7 +307,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
metadata: match.provider?.metadata,
|
||||
resultMetadata: event.data.provider.metadata,
|
||||
}
|
||||
match.time.completed = event.data.timestamp
|
||||
match.time.completed = event.created
|
||||
match.state = castDraft(
|
||||
SessionMessage.ToolStateCompleted.make({
|
||||
status: "completed",
|
||||
|
|
@ -333,7 +321,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
}
|
||||
})
|
||||
},
|
||||
"session.next.tool.failed": (event) => {
|
||||
"tool.failed": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && (match.state.status === "pending" || match.state.status === "running")) {
|
||||
|
|
@ -342,7 +330,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
metadata: match.provider?.metadata,
|
||||
resultMetadata: event.data.provider.metadata,
|
||||
}
|
||||
match.time.completed = event.data.timestamp
|
||||
match.time.completed = event.created
|
||||
match.state = castDraft(
|
||||
SessionMessage.ToolStateError.make({
|
||||
status: "error",
|
||||
|
|
@ -356,7 +344,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
}
|
||||
})
|
||||
},
|
||||
"session.next.reasoning.started": (event) => {
|
||||
"reasoning.started": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
draft.content.push(
|
||||
castDraft(
|
||||
|
|
@ -365,47 +353,47 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
id: event.data.reasoningID,
|
||||
text: "",
|
||||
providerMetadata: event.data.providerMetadata,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
},
|
||||
"session.next.reasoning.delta": (event) => {
|
||||
"reasoning.delta": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestReasoning(draft, event.data.reasoningID)
|
||||
if (match) match.text += event.data.delta
|
||||
})
|
||||
},
|
||||
"session.next.reasoning.ended": (event) => {
|
||||
"reasoning.ended": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestReasoning(draft, event.data.reasoningID)
|
||||
if (match) {
|
||||
match.text = event.data.text
|
||||
match.time = { created: match.time?.created ?? event.data.timestamp, completed: event.data.timestamp }
|
||||
match.time = { created: match.time?.created ?? event.created, completed: event.created }
|
||||
if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata
|
||||
}
|
||||
})
|
||||
},
|
||||
"session.next.retried": () => Effect.void,
|
||||
"session.next.compaction.started": () => Effect.void,
|
||||
"session.next.compaction.delta": () => Effect.void,
|
||||
"session.next.compaction.ended": (event) => {
|
||||
retried: () => Effect.void,
|
||||
"compaction.started": () => Effect.void,
|
||||
"compaction.delta": () => Effect.void,
|
||||
"compaction.ended": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.Compaction.make({
|
||||
id: event.data.messageID,
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "compaction",
|
||||
metadata: event.metadata,
|
||||
reason: event.data.reason,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
},
|
||||
"session.next.revert.staged": () => Effect.void,
|
||||
"session.next.revert.cleared": () => Effect.void,
|
||||
"session.next.revert.committed": () => Effect.void,
|
||||
"revert.staged": () => Effect.void,
|
||||
"revert.cleared": () => Effect.void,
|
||||
"revert.committed": () => Effect.void,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,21 +158,17 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!parent) return yield* Effect.die(new Error(`Fork parent session not found: ${event.data.parentID}`))
|
||||
const boundary = event.data.messageID
|
||||
const boundary = event.data.from
|
||||
? yield* db
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, event.data.parentID),
|
||||
eq(SessionMessageTable.id, event.data.messageID),
|
||||
),
|
||||
and(eq(SessionMessageTable.session_id, event.data.parentID), eq(SessionMessageTable.id, event.data.from)),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
: undefined
|
||||
if (event.data.messageID && !boundary)
|
||||
return yield* Effect.die(new Error(`Fork boundary message not found: ${event.data.messageID}`))
|
||||
if (event.data.from && !boundary) return yield* Effect.die(new Error(`Fork boundary message not found: ${event.data.from}`))
|
||||
const copied = yield* db
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
|
|
@ -208,8 +204,8 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
tokens_reasoning: 0,
|
||||
tokens_cache_read: 0,
|
||||
tokens_cache_write: 0,
|
||||
time_created: DateTime.toEpochMillis(event.data.timestamp),
|
||||
time_updated: DateTime.toEpochMillis(event.data.timestamp),
|
||||
time_created: DateTime.toEpochMillis(event.created),
|
||||
time_updated: DateTime.toEpochMillis(event.created),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning({ sessionID: SessionTable.id })
|
||||
|
|
@ -474,9 +470,9 @@ const layer = Layer.effectDiscard(
|
|||
.update(SessionTable)
|
||||
.set({
|
||||
directory: event.data.location.directory,
|
||||
path: event.data.subdirectory,
|
||||
path: event.data.subpath,
|
||||
workspace_id: event.data.location.workspaceID ? WorkspaceV2.ID.make(event.data.location.workspaceID) : null,
|
||||
time_updated: DateTime.toEpochMillis(event.data.timestamp),
|
||||
time_updated: DateTime.toEpochMillis(event.created),
|
||||
})
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
|
|
@ -556,19 +552,19 @@ const layer = Layer.effectDiscard(
|
|||
if (next) yield* applyUsage(db, sessionID, next)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.AgentSwitched, (event) =>
|
||||
yield* events.project(SessionEvent.AgentSelected, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||
.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))),
|
||||
)
|
||||
yield* events.project(SessionEvent.ModelSwitched, (event) =>
|
||||
yield* events.project(SessionEvent.ModelSelected, (event) =>
|
||||
Effect.gen(function* () {
|
||||
yield* db
|
||||
.update(SessionTable)
|
||||
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.created) })
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
|
@ -578,25 +574,30 @@ const layer = Layer.effectDiscard(
|
|||
yield* events.project(SessionEvent.Renamed, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||
.set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.created) })
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
|
||||
yield* events.project(SessionEvent.Prompted, (event) =>
|
||||
yield* events.project(SessionEvent.PromptPromoted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInput.projectPrompted(db, {
|
||||
id: event.data.messageID,
|
||||
const input = yield* SessionInput.projectPromptPromoted(db, {
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
prompt: event.data.prompt,
|
||||
delivery: event.data.delivery,
|
||||
timeCreated: event.data.timestamp,
|
||||
promotedSeq: event.durable.seq,
|
||||
})
|
||||
yield* run(db, event)
|
||||
yield* insertMessage(db, event, {
|
||||
id: input.id,
|
||||
type: "user",
|
||||
metadata: event.metadata,
|
||||
text: input.prompt.text,
|
||||
files: input.prompt.files,
|
||||
agents: input.prompt.agents,
|
||||
time: { created: event.created },
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.PromptAdmitted, (event) =>
|
||||
|
|
@ -605,11 +606,11 @@ const layer = Layer.effectDiscard(
|
|||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInput.projectAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.data.messageID,
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
prompt: event.data.prompt,
|
||||
delivery: event.data.delivery,
|
||||
timeCreated: event.data.timestamp,
|
||||
timeCreated: event.created,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
|
@ -617,11 +618,11 @@ const layer = Layer.effectDiscard(
|
|||
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Skill.Activated, (event) =>
|
||||
insertMessage(db, event, {
|
||||
id: event.data.messageID,
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "skill",
|
||||
name: event.data.name,
|
||||
text: event.data.text,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
||||
|
|
@ -646,7 +647,7 @@ const layer = Layer.effectDiscard(
|
|||
.update(SessionTable)
|
||||
.set({
|
||||
revert: { ...event.data.revert, files: event.data.revert.files ? [...event.data.revert.files] : undefined },
|
||||
time_updated: DateTime.toEpochMillis(event.data.timestamp),
|
||||
time_updated: DateTime.toEpochMillis(event.created),
|
||||
})
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
|
|
@ -655,7 +656,7 @@ const layer = Layer.effectDiscard(
|
|||
yield* events.project(SessionEvent.RevertEvent.Cleared, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.created) })
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie, Effect.asVoid),
|
||||
|
|
@ -693,7 +694,7 @@ const layer = Layer.effectDiscard(
|
|||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.update(SessionTable)
|
||||
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.created) })
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: {
|
|||
} satisfies SessionSchema.Info["revert"]
|
||||
yield* events.publish(SessionEvent.RevertEvent.Staged, {
|
||||
sessionID: input.session.id,
|
||||
timestamp: yield* DateTime.now,
|
||||
revert,
|
||||
})
|
||||
return revert
|
||||
|
|
@ -106,7 +105,6 @@ export const clear = Effect.fn("SessionRevert.clear")(function* (session: Sessio
|
|||
const events = yield* EventV2.Service
|
||||
yield* events.publish(SessionEvent.RevertEvent.Cleared, {
|
||||
sessionID: session.id,
|
||||
timestamp: yield* DateTime.now,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -116,6 +114,5 @@ export const commit = Effect.fn("SessionRevert.commit")(function* (session: Sess
|
|||
yield* events.publish(SessionEvent.RevertEvent.Committed, {
|
||||
sessionID: session.id,
|
||||
messageID: session.revert.messageID,
|
||||
timestamp: yield* DateTime.now,
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ const layer = Layer.effect(
|
|||
if (tool.type !== "tool" || (tool.state.status !== "pending" && tool.state.status !== "running")) continue
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: message.id,
|
||||
callID: tool.id,
|
||||
error: { type: "unknown", message: "Tool execution interrupted" },
|
||||
|
|
@ -297,7 +296,6 @@ const layer = Layer.effect(
|
|||
yield* serialized(
|
||||
events.publish(SessionEvent.Step.Ended, {
|
||||
sessionID: session.id,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: yield* publisher.startAssistant(),
|
||||
finish: settlement.finish,
|
||||
cost: 0,
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
...input,
|
||||
assistantMessageID,
|
||||
timestamp: yield* timestamp,
|
||||
snapshot: input.snapshot,
|
||||
})
|
||||
return assistantMessageID
|
||||
|
|
@ -123,7 +122,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* events.publish(SessionEvent.Text.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
timestamp: yield* timestamp,
|
||||
textID,
|
||||
text: value,
|
||||
})
|
||||
|
|
@ -134,7 +132,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* events.publish(SessionEvent.Reasoning.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
timestamp: yield* timestamp,
|
||||
reasoningID,
|
||||
text: value,
|
||||
providerMetadata,
|
||||
|
|
@ -147,7 +144,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${callID}`))
|
||||
yield* events.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
text: value,
|
||||
|
|
@ -176,7 +172,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* toolInput.start(event.id)
|
||||
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID,
|
||||
callID: event.id,
|
||||
name: event.name,
|
||||
|
|
@ -204,7 +199,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
assistantFailed = true
|
||||
yield* events.publish(SessionEvent.Step.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID,
|
||||
error: { type: "unknown", message },
|
||||
})
|
||||
|
|
@ -219,7 +213,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
tool.settled = true
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
error: { type: "unknown", message },
|
||||
|
|
@ -248,7 +241,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* events.publish(SessionEvent.Text.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
timestamp: yield* timestamp,
|
||||
textID: event.id,
|
||||
})
|
||||
return
|
||||
|
|
@ -257,7 +249,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* events.publish(SessionEvent.Text.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
timestamp: yield* timestamp,
|
||||
textID: event.id,
|
||||
delta: event.text,
|
||||
})
|
||||
|
|
@ -270,7 +261,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* events.publish(SessionEvent.Reasoning.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
timestamp: yield* timestamp,
|
||||
reasoningID: event.id,
|
||||
providerMetadata: event.providerMetadata,
|
||||
})
|
||||
|
|
@ -280,7 +270,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* events.publish(SessionEvent.Reasoning.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
timestamp: yield* timestamp,
|
||||
reasoningID: event.id,
|
||||
delta: event.text,
|
||||
})
|
||||
|
|
@ -300,7 +289,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
yield* toolInput.append(event.id, event.text)
|
||||
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
delta: event.text,
|
||||
|
|
@ -322,7 +310,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
tool.providerMetadata = event.providerMetadata
|
||||
yield* events.publish(SessionEvent.Tool.Called, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
tool: event.name,
|
||||
|
|
@ -352,7 +339,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
if ("error" in result) {
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
error: result.error,
|
||||
|
|
@ -363,7 +349,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
}
|
||||
yield* events.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
...result,
|
||||
|
|
@ -382,7 +367,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
tool.settled = true
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
error: { type: "unknown", message: event.message },
|
||||
|
|
|
|||
|
|
@ -42,9 +42,10 @@ const make = (dependencies: Dependencies) => {
|
|||
if (!firstUser) return
|
||||
const agent = yield* dependencies.agents.get(AgentV2.ID.make("title"))
|
||||
if (!agent) return
|
||||
const resolved = yield* (agent.model
|
||||
? dependencies.models.resolve({ ...session, model: agent.model })
|
||||
: dependencies.models.resolve(session)
|
||||
const resolved = yield* (
|
||||
agent.model
|
||||
? dependencies.models.resolve({ ...session, model: agent.model })
|
||||
: dependencies.models.resolve(session)
|
||||
).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!resolved) return
|
||||
const chunks: string[] = []
|
||||
|
|
@ -76,7 +77,6 @@ const make = (dependencies: Dependencies) => {
|
|||
if (!title) return
|
||||
yield* dependencies.events.publish(SessionEvent.Renamed, {
|
||||
sessionID: session.id,
|
||||
timestamp: yield* DateTime.now,
|
||||
title: truncate(title),
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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 } }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } },
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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") },
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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 } },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -156,17 +156,16 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
continue
|
||||
}
|
||||
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
|
||||
const time = "timestamp" in event.data ? toMillis(event.data.timestamp) : Date.now()
|
||||
const time = toMillis(event.created)
|
||||
|
||||
if (event.type === "session.next.prompted") {
|
||||
if (event.data.messageID === messageID) {
|
||||
if (event.type === "prompt.promoted") {
|
||||
if (event.data.inputID === messageID) {
|
||||
promoted = true
|
||||
continue
|
||||
}
|
||||
if (promoted && event.data.delivery === "queue") return
|
||||
}
|
||||
if (
|
||||
event.type === "session.next.execution.settled" &&
|
||||
event.type === "execution.settled" &&
|
||||
event.data.outcome === "interrupted" &&
|
||||
(interrupted || permissionRejected || questionRejected || formCancelled)
|
||||
) {
|
||||
|
|
@ -174,7 +173,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
}
|
||||
if (!promoted) continue
|
||||
|
||||
if (event.type === "session.next.step.started") {
|
||||
if (event.type === "step.started") {
|
||||
const part: StepStartPart = {
|
||||
id: partID(event.id),
|
||||
sessionID: input.sessionID,
|
||||
|
|
@ -190,11 +189,11 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
continue
|
||||
}
|
||||
|
||||
if (event.type === "session.next.text.started") {
|
||||
if (event.type === "text.started") {
|
||||
starts.set(event.data.textID, { id: partID(event.id), timestamp: time })
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.text.ended") {
|
||||
if (event.type === "text.ended") {
|
||||
const started = starts.get(event.data.textID)
|
||||
const part: TextPart = {
|
||||
id: started?.id ?? partID(event.id),
|
||||
|
|
@ -208,11 +207,11 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
continue
|
||||
}
|
||||
|
||||
if (event.type === "session.next.reasoning.started") {
|
||||
if (event.type === "reasoning.started") {
|
||||
starts.set(event.data.reasoningID, { id: partID(event.id), timestamp: time })
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.reasoning.ended" && input.thinking) {
|
||||
if (event.type === "reasoning.ended" && input.thinking) {
|
||||
const started = starts.get(event.data.reasoningID)
|
||||
const part: ReasoningPart = {
|
||||
id: started?.id ?? partID(event.id),
|
||||
|
|
@ -237,7 +236,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
continue
|
||||
}
|
||||
|
||||
if (event.type === "session.next.tool.input.started") {
|
||||
if (event.type === "tool.input.started") {
|
||||
tools.set(event.data.callID, {
|
||||
id: partID(event.id),
|
||||
timestamp: time,
|
||||
|
|
@ -247,12 +246,12 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
})
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.tool.input.ended") {
|
||||
if (event.type === "tool.input.ended") {
|
||||
const current = tools.get(event.data.callID)
|
||||
if (current) current.raw = event.data.text
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.tool.called") {
|
||||
if (event.type === "tool.called") {
|
||||
const current = tools.get(event.data.callID)
|
||||
tools.set(event.data.callID, {
|
||||
id: current?.id ?? partID(event.id),
|
||||
|
|
@ -265,7 +264,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
})
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.tool.success") {
|
||||
if (event.type === "tool.success") {
|
||||
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
||||
const part: ToolPart = {
|
||||
id: current.id,
|
||||
|
|
@ -298,7 +297,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
if (!emit("tool_use", time, { part })) await input.renderTool(part)
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.tool.failed") {
|
||||
if (event.type === "tool.failed") {
|
||||
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
||||
const error = event.data.error.message
|
||||
const part: ToolPart = {
|
||||
|
|
@ -329,7 +328,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
continue
|
||||
}
|
||||
|
||||
if (event.type === "session.next.step.ended") {
|
||||
if (event.type === "step.ended") {
|
||||
const part: StepFinishPart = {
|
||||
id: partID(event.id),
|
||||
sessionID: input.sessionID,
|
||||
|
|
@ -343,19 +342,19 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
emit("step_finish", time, { part })
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.step.failed") {
|
||||
if (event.type === "step.failed") {
|
||||
if (interrupted || permissionRejected || questionRejected || formCancelled) continue
|
||||
emittedError = true
|
||||
process.exitCode = 1
|
||||
if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message)
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.next.execution.settled") {
|
||||
if (event.type === "execution.settled") {
|
||||
if (event.data.outcome === "failure" && !emittedError && !questionRejected && !formCancelled) {
|
||||
emittedError = true
|
||||
process.exitCode = 1
|
||||
const error = event.data.error ?? { type: "unknown", message: "Session execution failed" }
|
||||
if (!emit("error", toMillis(event.data.timestamp), { error })) UI.error(error.message)
|
||||
if (!emit("error", time, { error })) UI.error(error.message)
|
||||
}
|
||||
if (event.data.outcome === "interrupted" && interrupted) process.exitCode = 130
|
||||
return
|
||||
|
|
@ -458,11 +457,12 @@ function partID(eventID: string) {
|
|||
|
||||
function fallbackTool(event: {
|
||||
id: string
|
||||
data: { timestamp: number; assistantMessageID: string; callID: string }
|
||||
created: number
|
||||
data: { assistantMessageID: string; callID: string }
|
||||
}): ToolState {
|
||||
return {
|
||||
id: partID(event.id),
|
||||
timestamp: toMillis(event.data.timestamp),
|
||||
timestamp: toMillis(event.created),
|
||||
assistantMessageID: event.data.assistantMessageID,
|
||||
tool: "tool",
|
||||
input: {},
|
||||
|
|
|
|||
|
|
@ -728,7 +728,7 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput {
|
|||
const data = input.data
|
||||
const event = input.event
|
||||
|
||||
if (event.type === "session.next.shell.started") {
|
||||
if (event.type === "shell.started") {
|
||||
if (event.properties.sessionID !== input.sessionID) {
|
||||
return out(data, commits)
|
||||
}
|
||||
|
|
@ -748,7 +748,7 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput {
|
|||
return out(data, commits, patch({ status: "running shell" }))
|
||||
}
|
||||
|
||||
if (event.type === "session.next.shell.ended") {
|
||||
if (event.type === "shell.ended") {
|
||||
if (event.properties.sessionID !== input.sessionID) {
|
||||
return out(data, commits)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,21 +424,21 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
}
|
||||
|
||||
const reduce = (child: ChildState, event: V2Event) => {
|
||||
if (event.type === "session.next.prompted") {
|
||||
if (userFrame(child, event.data.messageID, event.data.prompt.text)) {
|
||||
touch(child, event.data.timestamp)
|
||||
if (event.type === "prompt.promoted") {
|
||||
if (userFrame(child, event.data.inputID, "")) {
|
||||
touch(child, event.created)
|
||||
notifyDetail(child)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.step.started") {
|
||||
touch(child, event.data.timestamp)
|
||||
if (event.type === "step.started") {
|
||||
touch(child, event.created)
|
||||
if (child.label === FALLBACK_LABEL && event.data.agent) child.label = Locale.titlecase(event.data.agent)
|
||||
if (child.status !== "running") child.status = "running"
|
||||
input.emit()
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.text.delta") {
|
||||
if (event.type === "text.delta") {
|
||||
const projected = child.projectedText.get(event.data.textID)
|
||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||
if (projected && covered >= 0) {
|
||||
|
|
@ -455,11 +455,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
messageID: event.data.assistantMessageID,
|
||||
partID: event.data.textID,
|
||||
})
|
||||
touch(child, event.data.timestamp)
|
||||
touch(child, event.created)
|
||||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.text.ended") {
|
||||
if (event.type === "text.ended") {
|
||||
child.text.set(event.data.textID, event.data.text)
|
||||
child.projectedText.delete(event.data.textID)
|
||||
setFrame(child, `text:${event.data.textID}`, {
|
||||
|
|
@ -470,11 +470,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
messageID: event.data.assistantMessageID,
|
||||
partID: event.data.textID,
|
||||
})
|
||||
touch(child, event.data.timestamp)
|
||||
touch(child, event.created)
|
||||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.reasoning.delta") {
|
||||
if (event.type === "reasoning.delta") {
|
||||
const projected = child.projectedReasoning.get(event.data.reasoningID)
|
||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||
if (projected && covered >= 0) {
|
||||
|
|
@ -495,7 +495,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.reasoning.ended") {
|
||||
if (event.type === "reasoning.ended") {
|
||||
child.reasoning.set(event.data.reasoningID, event.data.text)
|
||||
child.projectedReasoning.delete(event.data.reasoningID)
|
||||
if (!input.thinking) return
|
||||
|
|
@ -510,16 +510,16 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.tool.input.started") {
|
||||
child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.data.timestamp })
|
||||
if (event.type === "tool.input.started") {
|
||||
child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.created })
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.tool.called") {
|
||||
if (event.type === "tool.called") {
|
||||
const current = child.tools.get(event.data.callID)
|
||||
child.tools.set(event.data.callID, {
|
||||
name: event.data.tool,
|
||||
input: event.data.input,
|
||||
started: current?.started ?? event.data.timestamp,
|
||||
started: current?.started ?? event.created,
|
||||
})
|
||||
childTool(
|
||||
child,
|
||||
|
|
@ -529,18 +529,18 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
name: event.data.tool,
|
||||
provider: event.data.provider,
|
||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||
time: { created: current?.started ?? event.data.timestamp, ran: event.data.timestamp },
|
||||
time: { created: current?.started ?? event.created, ran: event.created },
|
||||
},
|
||||
event.data.assistantMessageID,
|
||||
)
|
||||
touch(child, event.data.timestamp)
|
||||
touch(child, event.created)
|
||||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.tool.success" || event.type === "session.next.tool.failed") {
|
||||
if (event.type === "tool.success" || event.type === "tool.failed") {
|
||||
if (child.finishedTools.has(event.data.callID)) return
|
||||
const current = child.tools.get(event.data.callID)
|
||||
const failed = event.type === "session.next.tool.failed"
|
||||
const failed = event.type === "tool.failed"
|
||||
childTool(
|
||||
child,
|
||||
{
|
||||
|
|
@ -566,18 +566,18 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
result: event.data.result,
|
||||
},
|
||||
time: {
|
||||
created: current?.started ?? event.data.timestamp,
|
||||
created: current?.started ?? event.created,
|
||||
ran: current?.started,
|
||||
completed: event.data.timestamp,
|
||||
completed: event.created,
|
||||
},
|
||||
},
|
||||
event.data.assistantMessageID,
|
||||
)
|
||||
touch(child, event.data.timestamp)
|
||||
touch(child, event.created)
|
||||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.step.failed") {
|
||||
if (event.type === "step.failed") {
|
||||
setFrame(child, `error:step:${event.data.assistantMessageID}`, {
|
||||
kind: "error",
|
||||
source: "system",
|
||||
|
|
@ -585,14 +585,14 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
phase: "start",
|
||||
messageID: event.data.assistantMessageID,
|
||||
})
|
||||
touch(child, event.data.timestamp)
|
||||
touch(child, event.created)
|
||||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.execution.settled") {
|
||||
if (event.type === "execution.settled") {
|
||||
child.status =
|
||||
event.data.outcome === "success" ? "completed" : event.data.outcome === "interrupted" ? "cancelled" : "error"
|
||||
touch(child, event.data.timestamp)
|
||||
touch(child, event.created)
|
||||
input.emit()
|
||||
}
|
||||
}
|
||||
|
|
@ -613,15 +613,15 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
|
||||
return {
|
||||
main(event) {
|
||||
if (event.type === "session.next.tool.called") {
|
||||
if (event.type === "tool.called") {
|
||||
if (event.data.tool === "subagent") pendingCalls.set(event.data.callID, event.data.input)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.tool.failed") {
|
||||
if (event.type === "tool.failed") {
|
||||
pendingCalls.delete(event.data.callID)
|
||||
return
|
||||
}
|
||||
if (event.type !== "session.next.tool.success") return
|
||||
if (event.type !== "tool.success") return
|
||||
const pending = pendingCalls.get(event.data.callID)
|
||||
pendingCalls.delete(event.data.callID)
|
||||
const found = childSessionID(record(event.data.structured))
|
||||
|
|
@ -633,7 +633,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
child.status = "running"
|
||||
}
|
||||
if (!found.running && child.status === "running") child.status = "completed"
|
||||
touch(child, event.data.timestamp)
|
||||
touch(child, event.created)
|
||||
input.emit()
|
||||
if (!child.hydrated) void hydrateChild(child)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -294,7 +294,14 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
if (!state.tools.get(item.id)?.running) write([toolCommit(part, "start")])
|
||||
state.finishedTools.add(item.id)
|
||||
state.tools.delete(item.id)
|
||||
write([toolCommit(part, item.state.status === "completed" && part.state.status === "completed" && part.state.output ? "progress" : "final")])
|
||||
write([
|
||||
toolCommit(
|
||||
part,
|
||||
item.state.status === "completed" && part.state.status === "completed" && part.state.output
|
||||
? "progress"
|
||||
: "final",
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
const renderMessage = (message: SessionMessage, render: boolean, reuseVisibleWait: boolean) => {
|
||||
|
|
@ -395,17 +402,17 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
}
|
||||
input.trace?.write("recv.event", event)
|
||||
subagents.main(event)
|
||||
if (event.type === "session.next.prompted") {
|
||||
if (state.wait?.messageID === event.data.messageID) state.wait.promoted = true
|
||||
state.messageIDs.add(event.data.messageID)
|
||||
if (event.type === "prompt.promoted") {
|
||||
if (state.wait?.messageID === event.data.inputID) state.wait.promoted = true
|
||||
state.messageIDs.add(event.data.inputID)
|
||||
write([], { phase: "running", status: "waiting for assistant" })
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.step.started") {
|
||||
if (event.type === "step.started") {
|
||||
write([], { phase: "running", status: "assistant responding" })
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.text.delta") {
|
||||
if (event.type === "text.delta") {
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
|
||||
const projected = state.projectedText.get(key)
|
||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||
|
|
@ -427,7 +434,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
])
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.text.ended") {
|
||||
if (event.type === "text.ended") {
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
|
||||
const previous = state.text.get(key) ?? ""
|
||||
if (event.data.text.length > previous.length)
|
||||
|
|
@ -445,7 +452,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
state.projectedText.delete(key)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.reasoning.delta") {
|
||||
if (event.type === "reasoning.delta") {
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
|
||||
const projected = state.projectedReasoning.get(key)
|
||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||
|
|
@ -468,7 +475,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
])
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.reasoning.ended") {
|
||||
if (event.type === "reasoning.ended") {
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
|
||||
const previous = state.reasoning.get(key) ?? ""
|
||||
if (input.thinking && event.data.text.length > previous.length)
|
||||
|
|
@ -486,17 +493,17 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
state.projectedReasoning.delete(key)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.tool.input.started") {
|
||||
if (event.type === "tool.input.started") {
|
||||
state.tools.set(event.data.callID, {
|
||||
messageID: event.data.assistantMessageID,
|
||||
name: event.data.name,
|
||||
input: {},
|
||||
started: event.data.timestamp,
|
||||
started: event.created,
|
||||
running: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.tool.called") {
|
||||
if (event.type === "tool.called") {
|
||||
if (state.finishedTools.has(event.data.callID)) return
|
||||
const current = state.tools.get(event.data.callID)
|
||||
const item: SessionMessageAssistantTool = {
|
||||
|
|
@ -505,22 +512,29 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
name: event.data.tool,
|
||||
provider: event.data.provider,
|
||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||
time: { created: current?.started ?? event.data.timestamp, ran: event.data.timestamp },
|
||||
time: { created: current?.started ?? event.created, ran: event.created },
|
||||
}
|
||||
renderTool(event.data.assistantMessageID, item)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.tool.progress") return
|
||||
if (event.type === "session.next.tool.success" || event.type === "session.next.tool.failed") {
|
||||
if (event.type === "tool.progress") return
|
||||
if (event.type === "tool.success" || event.type === "tool.failed") {
|
||||
const current = state.tools.get(event.data.callID)
|
||||
const failed = event.type === "session.next.tool.failed"
|
||||
const failed = event.type === "tool.failed"
|
||||
const item: SessionMessageAssistantTool = {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
name: current?.name ?? "tool",
|
||||
provider: event.data.provider,
|
||||
state: failed
|
||||
? { status: "error", input: current?.input ?? {}, structured: {}, content: [], error: event.data.error, result: event.data.result }
|
||||
? {
|
||||
status: "error",
|
||||
input: current?.input ?? {},
|
||||
structured: {},
|
||||
content: [],
|
||||
error: event.data.error,
|
||||
result: event.data.result,
|
||||
}
|
||||
: {
|
||||
status: "completed",
|
||||
input: current?.input ?? {},
|
||||
|
|
@ -529,7 +543,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
outputPaths: event.data.outputPaths,
|
||||
result: event.data.result,
|
||||
},
|
||||
time: { created: current?.started ?? event.data.timestamp, ran: current?.started, completed: event.data.timestamp },
|
||||
time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created },
|
||||
}
|
||||
renderTool(event.data.assistantMessageID, item)
|
||||
return
|
||||
|
|
@ -554,7 +568,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
syncBlockers()
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.step.ended") {
|
||||
if (event.type === "step.ended") {
|
||||
const total =
|
||||
event.data.tokens.input +
|
||||
event.data.tokens.output +
|
||||
|
|
@ -562,16 +576,19 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
event.data.tokens.cache.read +
|
||||
event.data.tokens.cache.write
|
||||
const usage = total > 0 ? total.toLocaleString() : ""
|
||||
write([], { phase: event.data.finish === "tool-calls" ? "running" : "idle", usage: event.data.cost ? `${usage} · ${money.format(event.data.cost)}` : usage })
|
||||
write([], {
|
||||
phase: event.data.finish === "tool-calls" ? "running" : "idle",
|
||||
usage: event.data.cost ? `${usage} · ${money.format(event.data.cost)}` : usage,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.step.failed") {
|
||||
if (event.type === "step.failed") {
|
||||
state.errors.add(event.data.assistantMessageID)
|
||||
if (state.wait) state.wait.failureRendered = true
|
||||
write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }])
|
||||
return
|
||||
}
|
||||
if (event.type === "session.next.execution.settled") {
|
||||
if (event.type === "execution.settled") {
|
||||
write([], { phase: "idle", status: "" })
|
||||
const current = state.wait
|
||||
if (!current || (!current.promoted && !current.interrupted)) return
|
||||
|
|
@ -611,7 +628,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
sseMaxRetryAttempts: 0,
|
||||
throwOnError: true,
|
||||
})
|
||||
const stream = response.stream[Symbol.asyncIterator]() as AsyncGenerator<RunV2Event>
|
||||
const stream = response.stream[Symbol.asyncIterator]() as AsyncGenerator<RunV2Event>
|
||||
try {
|
||||
const first = await stream.next()
|
||||
if (first.done || first.value.type !== "server.connected") throw new Error("Event stream disconnected")
|
||||
|
|
@ -689,10 +706,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
]
|
||||
: [],
|
||||
)
|
||||
const attachments = [
|
||||
...prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])),
|
||||
...promptFiles,
|
||||
]
|
||||
const attachments = [...prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])), ...promptFiles]
|
||||
const agents = next.prompt.parts.flatMap((part) =>
|
||||
part.type === "agent"
|
||||
? [
|
||||
|
|
@ -735,10 +749,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
sessionID: input.sessionID,
|
||||
id: messageID,
|
||||
prompt: {
|
||||
text: [
|
||||
next.prompt.text,
|
||||
...prepared.flatMap((file) => (file.text ? [file.text] : [])),
|
||||
].join("\n\n"),
|
||||
text: [next.prompt.text, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"),
|
||||
files: attachments.length ? attachments : undefined,
|
||||
agents: agents.length ? agents : undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,31 +18,33 @@ function form(id: string, sessionID: string): FormInfo {
|
|||
}
|
||||
|
||||
function formCreated(info: FormInfo): V2Event {
|
||||
return { id: `evt_${info.id}`, type: "form.created", data: { form: info } }
|
||||
return { id: `evt_${info.id}`, created: 0, type: "form.created", data: { form: info } }
|
||||
}
|
||||
|
||||
function prompted(messageID: string): V2Event {
|
||||
function prompted(inputID: string): V2Event {
|
||||
return {
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
|
||||
data: { timestamp: 1, sessionID: "ses_1", messageID, prompt: { text: "hello" }, delivery: "steer" },
|
||||
data: { sessionID: "ses_1", inputID },
|
||||
}
|
||||
}
|
||||
|
||||
function settled(outcome: "success" | "interrupted" = "success"): V2Event {
|
||||
return {
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 2, sessionID: "ses_1", outcome },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome },
|
||||
}
|
||||
}
|
||||
|
||||
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
|
||||
// live events the prompt admission triggers, keyed by the generated message ID.
|
||||
async function run(input: { turn: (messageID: string) => V2Event[]; pendingForms?: FormInfo[]; attached?: boolean }) {
|
||||
async function run(input: { turn: (inputID: string) => V2Event[]; pendingForms?: FormInfo[]; attached?: boolean }) {
|
||||
const sdk = new OpencodeClient()
|
||||
const values: V2Event[] = [{ id: "evt_connected", type: "server.connected", data: {} }]
|
||||
const values: V2Event[] = [{ id: "evt_connected", created: 0, type: "server.connected", data: {} }]
|
||||
let wake: (() => void) | undefined
|
||||
const stream = (async function* (): AsyncGenerator<V2Event, void, unknown> {
|
||||
while (true) {
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ describe("run session data", () => {
|
|||
test("renders direct shell mode from first-class shell events", () => {
|
||||
let data = createSessionData()
|
||||
const started = reduce(data, {
|
||||
type: "session.next.shell.started",
|
||||
type: "shell.started",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 1,
|
||||
|
|
@ -353,7 +353,7 @@ describe("run session data", () => {
|
|||
|
||||
data = started.data
|
||||
const ended = reduce(data, {
|
||||
type: "session.next.shell.ended",
|
||||
type: "shell.ended",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 2,
|
||||
|
|
@ -380,7 +380,7 @@ describe("run session data", () => {
|
|||
|
||||
test("suppresses legacy bash part updates once shell events claim the call", () => {
|
||||
let data = reduce(createSessionData(), {
|
||||
type: "session.next.shell.started",
|
||||
type: "shell.started",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 1,
|
||||
|
|
@ -409,7 +409,7 @@ describe("run session data", () => {
|
|||
).toEqual([])
|
||||
|
||||
data = reduce(data, {
|
||||
type: "session.next.shell.ended",
|
||||
type: "shell.ended",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 2,
|
||||
|
|
@ -463,7 +463,7 @@ describe("run session data", () => {
|
|||
|
||||
expect(
|
||||
reduce(data, {
|
||||
type: "session.next.shell.started",
|
||||
type: "shell.started",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 1,
|
||||
|
|
@ -497,7 +497,7 @@ describe("run session data", () => {
|
|||
|
||||
expect(
|
||||
reduce(data, {
|
||||
type: "session.next.shell.ended",
|
||||
type: "shell.ended",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 2,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ function ok<T>(data: T) {
|
|||
}
|
||||
|
||||
function connected(id = "evt_connected") {
|
||||
return { id, type: "server.connected", data: {} } satisfies RunV2Event
|
||||
return { id, created: 0, type: "server.connected", data: {} } satisfies RunV2Event
|
||||
}
|
||||
|
||||
function durable(sessionID: string, seq = 0, version = 1) {
|
||||
|
|
@ -125,22 +125,20 @@ function sdk(input: {
|
|||
spyOn(client.v2.session, "switchModel").mockImplementation(() => ok(undefined))
|
||||
// The generated methods have conditional return types for throwOnError; the
|
||||
// minimal shapes below are enough for family discovery and model fallback.
|
||||
spyOn(client.v2.session, "list").mockImplementation(
|
||||
(request) => {
|
||||
const parentID = request?.parentID
|
||||
return ok({
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data:
|
||||
input.sessions?.filter((session) =>
|
||||
parentID === undefined
|
||||
? true
|
||||
: parentID === null
|
||||
? session.parentID === undefined
|
||||
: session.parentID === parentID,
|
||||
) ?? [],
|
||||
}) as never
|
||||
},
|
||||
)
|
||||
spyOn(client.v2.session, "list").mockImplementation((request) => {
|
||||
const parentID = request?.parentID
|
||||
return ok({
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
data:
|
||||
input.sessions?.filter((session) =>
|
||||
parentID === undefined
|
||||
? true
|
||||
: parentID === null
|
||||
? session.parentID === undefined
|
||||
: session.parentID === parentID,
|
||||
) ?? [],
|
||||
}) as never
|
||||
})
|
||||
spyOn(client.v2.model, "default").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
|
|
@ -201,21 +199,19 @@ describe("V2 mini transport", () => {
|
|||
while (!admitted) await Bun.sleep(0)
|
||||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: "hello" },
|
||||
delivery: "steer",
|
||||
inputID: "msg_prompt",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_text",
|
||||
type: "session.next.text.delta",
|
||||
created: 0,
|
||||
type: "text.delta",
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
textID: "txt_1",
|
||||
|
|
@ -224,8 +220,9 @@ describe("V2 mini transport", () => {
|
|||
})
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 4, sessionID: "ses_1", outcome: "success" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
await turn
|
||||
|
||||
|
|
@ -261,20 +258,19 @@ describe("V2 mini transport", () => {
|
|||
queueMicrotask(() => {
|
||||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: input.prompt?.text ?? "" },
|
||||
delivery: "steer",
|
||||
inputID: "msg_prompt",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 3, sessionID: "ses_1", outcome: "success" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
})
|
||||
return ok({
|
||||
|
|
@ -356,20 +352,19 @@ describe("V2 mini transport", () => {
|
|||
queueMicrotask(() => {
|
||||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: input.prompt?.text ?? "" },
|
||||
delivery: "steer",
|
||||
inputID: "msg_prompt",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 3, sessionID: "ses_1", outcome: "success" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
})
|
||||
return ok({
|
||||
|
|
@ -454,20 +449,19 @@ describe("V2 mini transport", () => {
|
|||
queueMicrotask(() => {
|
||||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: input.prompt?.text ?? "" },
|
||||
delivery: "steer",
|
||||
inputID: "msg_prompt",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 3, sessionID: "ses_1", outcome: "success" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
})
|
||||
return ok({
|
||||
|
|
@ -528,6 +522,7 @@ describe("V2 mini transport", () => {
|
|||
})
|
||||
events.push({
|
||||
id: "evt_permission",
|
||||
created: 0,
|
||||
type: "permission.v2.asked",
|
||||
data: { id: "per_1", sessionID: "ses_1", action: "read", resources: ["/tmp/file"] },
|
||||
})
|
||||
|
|
@ -728,9 +723,9 @@ describe("V2 mini transport", () => {
|
|||
const replay = transport.replayOnResize({ localRows: () => [], reset: () => resetting })
|
||||
events.push({
|
||||
id: "evt_text",
|
||||
type: "session.next.text.delta",
|
||||
created: 0,
|
||||
type: "text.delta",
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
textID: "txt_1",
|
||||
|
|
@ -813,10 +808,10 @@ describe("V2 mini transport", () => {
|
|||
})
|
||||
events.push({
|
||||
id: "evt_reasoning",
|
||||
type: "session.next.reasoning.ended",
|
||||
created: 0,
|
||||
type: "reasoning.ended",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
reasoningID: "reasoning_1",
|
||||
|
|
@ -869,8 +864,9 @@ describe("V2 mini transport", () => {
|
|||
await transport.interruptActiveTurn()
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 3, sessionID: "ses_1", outcome: "interrupted" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
await turn
|
||||
|
||||
|
|
@ -924,20 +920,19 @@ describe("V2 mini transport", () => {
|
|||
while (!admitted) await Bun.sleep(0)
|
||||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: "hello" },
|
||||
delivery: "steer",
|
||||
inputID: "msg_prompt",
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 3, sessionID: "ses_1", outcome: "success" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
await turn
|
||||
|
||||
|
|
@ -985,22 +980,21 @@ describe("V2 mini transport", () => {
|
|||
while (!admitted) await Bun.sleep(0)
|
||||
events.push({
|
||||
id: "evt_prompted",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_prompt",
|
||||
prompt: { text: "hello" },
|
||||
delivery: "steer",
|
||||
inputID: "msg_prompt",
|
||||
},
|
||||
})
|
||||
await Bun.sleep(0)
|
||||
controller.abort()
|
||||
events.push({
|
||||
id: "evt_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 3, sessionID: "ses_1", outcome: "interrupted" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_1", outcome: "success" },
|
||||
})
|
||||
await turn
|
||||
|
||||
|
|
@ -1054,10 +1048,10 @@ describe("V2 mini transport", () => {
|
|||
|
||||
events.push({
|
||||
id: "evt_child_step",
|
||||
type: "session.next.step.started",
|
||||
created: 0,
|
||||
type: "step.started",
|
||||
durable: durable("ses_child"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_child",
|
||||
assistantMessageID: "msg_child_a",
|
||||
agent: "explore",
|
||||
|
|
@ -1072,9 +1066,9 @@ describe("V2 mini transport", () => {
|
|||
|
||||
events.push({
|
||||
id: "evt_child_text",
|
||||
type: "session.next.text.delta",
|
||||
created: 0,
|
||||
type: "text.delta",
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_child",
|
||||
assistantMessageID: "msg_child_a",
|
||||
textID: "txt_child",
|
||||
|
|
@ -1086,8 +1080,9 @@ describe("V2 mini transport", () => {
|
|||
|
||||
events.push({
|
||||
id: "evt_child_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 4, sessionID: "ses_child", outcome: "success" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_child", outcome: "success" },
|
||||
})
|
||||
while (!states().some((state) => state.tabs.some((tab) => tab.status === "completed"))) await Bun.sleep(0)
|
||||
await transport.close()
|
||||
|
|
@ -1130,10 +1125,10 @@ describe("V2 mini transport", () => {
|
|||
// Both events arrive while session.get is still in flight.
|
||||
events.push({
|
||||
id: "evt_child_step",
|
||||
type: "session.next.step.started",
|
||||
created: 0,
|
||||
type: "step.started",
|
||||
durable: durable("ses_child"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_child",
|
||||
assistantMessageID: "msg_child_a",
|
||||
agent: "explore",
|
||||
|
|
@ -1142,8 +1137,9 @@ describe("V2 mini transport", () => {
|
|||
})
|
||||
events.push({
|
||||
id: "evt_child_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 3, sessionID: "ses_child", outcome: "interrupted" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_child", outcome: "interrupted" },
|
||||
})
|
||||
await Bun.sleep(0)
|
||||
resolveGet?.()
|
||||
|
|
@ -1188,10 +1184,10 @@ describe("V2 mini transport", () => {
|
|||
// Child event arrives first and gets buffered behind the gated session.get.
|
||||
events.push({
|
||||
id: "evt_child_step",
|
||||
type: "session.next.step.started",
|
||||
created: 0,
|
||||
type: "step.started",
|
||||
durable: durable("ses_child"),
|
||||
data: {
|
||||
timestamp: 2,
|
||||
sessionID: "ses_child",
|
||||
assistantMessageID: "msg_child_a",
|
||||
agent: "explore",
|
||||
|
|
@ -1201,10 +1197,10 @@ describe("V2 mini transport", () => {
|
|||
// Parent's background subagent tool.success adopts the child mid-discovery.
|
||||
events.push({
|
||||
id: "evt_parent_call",
|
||||
type: "session.next.tool.called",
|
||||
created: 0,
|
||||
type: "tool.called",
|
||||
durable: durable("ses_1"),
|
||||
data: {
|
||||
timestamp: 3,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_parent_a",
|
||||
callID: "call_sub",
|
||||
|
|
@ -1215,10 +1211,10 @@ describe("V2 mini transport", () => {
|
|||
})
|
||||
events.push({
|
||||
id: "evt_parent_success",
|
||||
type: "session.next.tool.success",
|
||||
created: 0,
|
||||
type: "tool.success",
|
||||
durable: durable("ses_1", 1),
|
||||
data: {
|
||||
timestamp: 4,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_parent_a",
|
||||
callID: "call_sub",
|
||||
|
|
@ -1230,8 +1226,9 @@ describe("V2 mini transport", () => {
|
|||
// The settled event arrives after adoption, so it applies directly.
|
||||
events.push({
|
||||
id: "evt_child_settled",
|
||||
type: "session.next.execution.settled",
|
||||
data: { timestamp: 5, sessionID: "ses_child", outcome: "interrupted" },
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: { sessionID: "ses_child", outcome: "interrupted" },
|
||||
})
|
||||
while (!states().some((state) => state.tabs.some((tab) => tab.status === "cancelled"))) await Bun.sleep(0)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ test.skip("step snapshots carry over to assistant messages", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
agent: "build",
|
||||
model: {
|
||||
id: ModelV2.ID.make("model"),
|
||||
|
|
@ -43,12 +43,12 @@ test.skip("step snapshots carry over to assistant messages", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.ended",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "step.ended",
|
||||
durable: durable(sessionID, 1, 2),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
finish: "stop",
|
||||
cost: 0,
|
||||
tokens: {
|
||||
|
|
@ -76,12 +76,12 @@ test.skip("text ended populates assistant text content", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
agent: "build",
|
||||
model: {
|
||||
id: ModelV2.ID.make("model"),
|
||||
|
|
@ -95,12 +95,12 @@ test.skip("text ended populates assistant text content", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.text.started",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "text.started",
|
||||
durable: durable(sessionID, 1),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
textID: "text-1",
|
||||
},
|
||||
} satisfies SessionEvent.Event),
|
||||
|
|
@ -109,12 +109,12 @@ test.skip("text ended populates assistant text content", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.text.ended",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "text.ended",
|
||||
durable: durable(sessionID, 2),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(3),
|
||||
textID: "text-1",
|
||||
text: "hello assistant",
|
||||
},
|
||||
|
|
@ -135,12 +135,12 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
agent: "build",
|
||||
model: {
|
||||
id: ModelV2.ID.make("model"),
|
||||
|
|
@ -154,12 +154,12 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.tool.input.started",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "tool.input.started",
|
||||
durable: durable(sessionID, 1),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
callID,
|
||||
name: "bash",
|
||||
},
|
||||
|
|
@ -169,12 +169,12 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.tool.called",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "tool.called",
|
||||
durable: durable(sessionID, 2),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(3),
|
||||
callID,
|
||||
tool: "bash",
|
||||
input: { command: "pwd" },
|
||||
|
|
@ -186,12 +186,12 @@ test.skip("tool completion stores completed timestamp", () => {
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.tool.success",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "tool.success",
|
||||
durable: durable(sessionID, 3),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
timestamp: DateTime.makeUnsafe(4),
|
||||
callID,
|
||||
structured: {},
|
||||
content: [{ type: "text", text: "/tmp" }],
|
||||
|
|
@ -212,17 +212,16 @@ test("compaction events reduce to compaction message only when completed", () =>
|
|||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
const id = EventV2.ID.create()
|
||||
const compactionID = SessionMessage.ID.create()
|
||||
const endedID = EventV2.ID.create()
|
||||
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id,
|
||||
type: "session.next.compaction.started",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "compaction.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
messageID: compactionID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
reason: "auto",
|
||||
},
|
||||
} satisfies SessionEvent.Event),
|
||||
|
|
@ -233,11 +232,10 @@ test("compaction events reduce to compaction message only when completed", () =>
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.compaction.delta",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "compaction.delta",
|
||||
data: {
|
||||
sessionID,
|
||||
messageID: compactionID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
text: "hello ",
|
||||
},
|
||||
} satisfies SessionEvent.Event),
|
||||
|
|
@ -246,11 +244,10 @@ test("compaction events reduce to compaction message only when completed", () =>
|
|||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.compaction.delta",
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "compaction.delta",
|
||||
data: {
|
||||
sessionID,
|
||||
messageID: compactionID,
|
||||
timestamp: DateTime.makeUnsafe(3),
|
||||
text: "summary",
|
||||
},
|
||||
} satisfies SessionEvent.Event),
|
||||
|
|
@ -258,13 +255,12 @@ test("compaction events reduce to compaction message only when completed", () =>
|
|||
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.compaction.ended",
|
||||
id: endedID,
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: "compaction.ended",
|
||||
durable: durable(sessionID, 1),
|
||||
data: {
|
||||
sessionID,
|
||||
messageID: compactionID,
|
||||
timestamp: DateTime.makeUnsafe(4),
|
||||
reason: "auto",
|
||||
text: "final summary",
|
||||
recent: "recent context",
|
||||
|
|
@ -274,11 +270,11 @@ test("compaction events reduce to compaction message only when completed", () =>
|
|||
|
||||
expect(state.messages).toHaveLength(1)
|
||||
expect(state.messages[0]).toMatchObject({
|
||||
id: compactionID,
|
||||
id: SessionMessage.ID.fromEvent(endedID),
|
||||
type: "compaction",
|
||||
reason: "auto",
|
||||
summary: "final summary",
|
||||
recent: "recent context",
|
||||
time: { created: DateTime.makeUnsafe(4) },
|
||||
time: { created: DateTime.makeUnsafe(0) },
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Schema } from "effect"
|
|||
import { optional } from "./schema.js"
|
||||
import { ascending } from "./identifier.js"
|
||||
import { Location } from "./location.js"
|
||||
import { statics } from "./schema.js"
|
||||
import { DateTimeUtcFromMillis, statics } from "./schema.js"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
|
||||
Schema.brand("Event.ID"),
|
||||
|
|
@ -60,6 +60,7 @@ export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
|
|||
type PayloadBase<D extends Definition> = {
|
||||
readonly id: ID
|
||||
readonly type: D["type"]
|
||||
readonly created: typeof DateTimeUtcFromMillis.Type
|
||||
readonly data: Data<D>
|
||||
readonly location?: Location.Ref
|
||||
readonly metadata?: Record<string, unknown>
|
||||
|
|
@ -85,6 +86,7 @@ export function durable<
|
|||
const data = Schema.Struct(input.schema)
|
||||
return Schema.Struct({
|
||||
id: ID,
|
||||
created: DateTimeUtcFromMillis,
|
||||
metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
type: Schema.Literal(input.type),
|
||||
durable: DurableEnvelope,
|
||||
|
|
@ -109,6 +111,7 @@ export function ephemeral<
|
|||
const data = Schema.Struct(input.schema)
|
||||
return Schema.Struct({
|
||||
id: ID,
|
||||
created: DateTimeUtcFromMillis,
|
||||
metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
type: Schema.Literal(input.type),
|
||||
location: optional(Location.Ref),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Event } from "./event.js"
|
|||
import { ProviderMetadata, ToolContent } from "./llm.js"
|
||||
import { Delivery } from "./session-delivery.js"
|
||||
import { Model } from "./model.js"
|
||||
import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "./schema.js"
|
||||
import { NonNegativeInt, RelativePath } from "./schema.js"
|
||||
import { FileAttachment, Prompt } from "./prompt.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { Location } from "./location.js"
|
||||
|
|
@ -20,17 +20,16 @@ export const Source = Schema.Struct({
|
|||
end: NonNegativeInt,
|
||||
text: Schema.String,
|
||||
}).annotate({
|
||||
identifier: "session.next.event.source",
|
||||
identifier: "session.event.source",
|
||||
})
|
||||
export interface Source extends Schema.Schema.Type<typeof Source> {}
|
||||
|
||||
const Base = {
|
||||
timestamp: DateTimeUtcFromMillis,
|
||||
sessionID: SessionID,
|
||||
}
|
||||
const PromptFields = {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
inputID: SessionMessage.ID,
|
||||
prompt: Prompt,
|
||||
delivery: Delivery,
|
||||
}
|
||||
|
|
@ -44,48 +43,46 @@ const options = {
|
|||
const stepSettlementOptions = {
|
||||
durable: {
|
||||
aggregate: "sessionID",
|
||||
version: 2,
|
||||
version: 1,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const UnknownError = SessionMessage.UnknownError
|
||||
export type UnknownError = SessionMessage.UnknownError
|
||||
|
||||
export const AgentSwitched = Event.durable({
|
||||
type: "session.next.agent.switched",
|
||||
export const AgentSelected = Event.durable({
|
||||
type: "agent.selected",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
agent: Schema.String,
|
||||
},
|
||||
})
|
||||
export type AgentSwitched = typeof AgentSwitched.Type
|
||||
export type AgentSelected = typeof AgentSelected.Type
|
||||
|
||||
export const ModelSwitched = Event.durable({
|
||||
type: "session.next.model.switched",
|
||||
export const ModelSelected = Event.durable({
|
||||
type: "model.selected",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
model: Model.Ref,
|
||||
},
|
||||
})
|
||||
export type ModelSwitched = typeof ModelSwitched.Type
|
||||
export type ModelSelected = typeof ModelSelected.Type
|
||||
|
||||
export const Moved = Event.durable({
|
||||
type: "session.next.moved",
|
||||
type: "session.moved",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
location: Location.Ref,
|
||||
subdirectory: RelativePath.pipe(optional),
|
||||
subpath: RelativePath.pipe(optional),
|
||||
},
|
||||
})
|
||||
export type Moved = typeof Moved.Type
|
||||
|
||||
export const Renamed = Event.durable({
|
||||
type: "session.next.renamed",
|
||||
type: "renamed",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -95,32 +92,35 @@ export const Renamed = Event.durable({
|
|||
export type Renamed = typeof Renamed.Type
|
||||
|
||||
export const Forked = Event.durable({
|
||||
type: "session.next.forked",
|
||||
type: "forked",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
parentID: SessionID,
|
||||
messageID: SessionMessage.ID.pipe(optional),
|
||||
from: SessionMessage.ID.pipe(optional),
|
||||
},
|
||||
})
|
||||
export type Forked = typeof Forked.Type
|
||||
|
||||
export const Prompted = Event.durable({
|
||||
type: "session.next.prompted",
|
||||
export const PromptPromoted = Event.durable({
|
||||
type: "prompt.promoted",
|
||||
...options,
|
||||
schema: PromptFields,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
inputID: SessionMessage.ID,
|
||||
},
|
||||
})
|
||||
export type Prompted = typeof Prompted.Type
|
||||
export type PromptPromoted = typeof PromptPromoted.Type
|
||||
|
||||
export const PromptAdmitted = Event.durable({
|
||||
type: "session.next.prompt.admitted",
|
||||
type: "prompt.admitted",
|
||||
...options,
|
||||
schema: PromptFields,
|
||||
})
|
||||
export type PromptAdmitted = typeof PromptAdmitted.Type
|
||||
|
||||
export const ExecutionSettled = Event.ephemeral({
|
||||
type: "session.next.execution.settled",
|
||||
type: "execution.settled",
|
||||
schema: {
|
||||
...Base,
|
||||
outcome: Schema.Literals(["success", "failure", "interrupted"]),
|
||||
|
|
@ -130,22 +130,20 @@ export const ExecutionSettled = Event.ephemeral({
|
|||
export type ExecutionSettled = typeof ExecutionSettled.Type
|
||||
|
||||
export const ContextUpdated = Event.durable({
|
||||
type: "session.next.context.updated",
|
||||
type: "session.context.updated",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type ContextUpdated = typeof ContextUpdated.Type
|
||||
|
||||
export const Synthetic = Event.durable({
|
||||
type: "session.next.synthetic",
|
||||
type: "synthetic",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
text: Schema.String,
|
||||
description: Schema.String.pipe(optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
|
||||
|
|
@ -155,11 +153,10 @@ export type Synthetic = typeof Synthetic.Type
|
|||
|
||||
export namespace Skill {
|
||||
export const Activated = Event.durable({
|
||||
type: "session.next.skill.activated",
|
||||
type: "skill.activated",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
name: Schema.String,
|
||||
text: Schema.String,
|
||||
},
|
||||
|
|
@ -169,11 +166,10 @@ export namespace Skill {
|
|||
|
||||
export namespace Shell {
|
||||
export const Started = Event.durable({
|
||||
type: "session.next.shell.started",
|
||||
type: "shell.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
callID: Schema.String,
|
||||
command: Schema.String,
|
||||
},
|
||||
|
|
@ -181,7 +177,7 @@ export namespace Shell {
|
|||
export type Started = typeof Started.Type
|
||||
|
||||
export const Ended = Event.durable({
|
||||
type: "session.next.shell.ended",
|
||||
type: "shell.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -194,7 +190,7 @@ export namespace Shell {
|
|||
|
||||
export namespace Step {
|
||||
export const Started = Event.durable({
|
||||
type: "session.next.step.started",
|
||||
type: "step.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -207,7 +203,7 @@ export namespace Step {
|
|||
export type Started = typeof Started.Type
|
||||
|
||||
export const Ended = Event.durable({
|
||||
type: "session.next.step.ended",
|
||||
type: "step.ended",
|
||||
...stepSettlementOptions,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -230,7 +226,7 @@ export namespace Step {
|
|||
export type Ended = typeof Ended.Type
|
||||
|
||||
export const Failed = Event.durable({
|
||||
type: "session.next.step.failed",
|
||||
type: "step.failed",
|
||||
...stepSettlementOptions,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -243,7 +239,7 @@ export namespace Step {
|
|||
|
||||
export namespace Text {
|
||||
export const Started = Event.durable({
|
||||
type: "session.next.text.started",
|
||||
type: "text.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -255,7 +251,7 @@ export namespace Text {
|
|||
|
||||
// Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
|
||||
export const Delta = Event.ephemeral({
|
||||
type: "session.next.text.delta",
|
||||
type: "text.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
|
|
@ -266,7 +262,7 @@ export namespace Text {
|
|||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.durable({
|
||||
type: "session.next.text.ended",
|
||||
type: "text.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -280,7 +276,7 @@ export namespace Text {
|
|||
|
||||
export namespace Reasoning {
|
||||
export const Started = Event.durable({
|
||||
type: "session.next.reasoning.started",
|
||||
type: "reasoning.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -293,7 +289,7 @@ export namespace Reasoning {
|
|||
|
||||
// Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
|
||||
export const Delta = Event.ephemeral({
|
||||
type: "session.next.reasoning.delta",
|
||||
type: "reasoning.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
|
|
@ -304,7 +300,7 @@ export namespace Reasoning {
|
|||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.durable({
|
||||
type: "session.next.reasoning.ended",
|
||||
type: "reasoning.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -326,7 +322,7 @@ export namespace Tool {
|
|||
|
||||
export namespace Input {
|
||||
export const Started = Event.durable({
|
||||
type: "session.next.tool.input.started",
|
||||
type: "tool.input.started",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
|
|
@ -337,7 +333,7 @@ export namespace Tool {
|
|||
|
||||
// Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
|
||||
export const Delta = Event.ephemeral({
|
||||
type: "session.next.tool.input.delta",
|
||||
type: "tool.input.delta",
|
||||
schema: {
|
||||
...ToolBase,
|
||||
delta: Schema.String,
|
||||
|
|
@ -346,7 +342,7 @@ export namespace Tool {
|
|||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.durable({
|
||||
type: "session.next.tool.input.ended",
|
||||
type: "tool.input.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
|
|
@ -357,7 +353,7 @@ export namespace Tool {
|
|||
}
|
||||
|
||||
export const Called = Event.durable({
|
||||
type: "session.next.tool.called",
|
||||
type: "tool.called",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
|
|
@ -376,7 +372,7 @@ export namespace Tool {
|
|||
* transitions or at a bounded cadence, not persist every stdout/stderr chunk.
|
||||
*/
|
||||
export const Progress = Event.durable({
|
||||
type: "session.next.tool.progress",
|
||||
type: "tool.progress",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
|
|
@ -387,7 +383,7 @@ export namespace Tool {
|
|||
export type Progress = typeof Progress.Type
|
||||
|
||||
export const Success = Event.durable({
|
||||
type: "session.next.tool.success",
|
||||
type: "tool.success",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
|
|
@ -404,7 +400,7 @@ export namespace Tool {
|
|||
export type Success = typeof Success.Type
|
||||
|
||||
export const Failed = Event.durable({
|
||||
type: "session.next.tool.failed",
|
||||
type: "tool.failed",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
|
|
@ -427,12 +423,12 @@ export const RetryError = Schema.Struct({
|
|||
responseBody: Schema.String.pipe(optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.String).pipe(optional),
|
||||
}).annotate({
|
||||
identifier: "session.next.retry_error",
|
||||
identifier: "session.retry.error",
|
||||
})
|
||||
export interface RetryError extends Schema.Schema.Type<typeof RetryError> {}
|
||||
|
||||
export const Retried = Event.durable({
|
||||
type: "session.next.retried",
|
||||
type: "retried",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
|
|
@ -444,32 +440,29 @@ export type Retried = typeof Retried.Type
|
|||
|
||||
export namespace Compaction {
|
||||
export const Started = Event.durable({
|
||||
type: "session.next.compaction.started",
|
||||
type: "compaction.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
export const Delta = Event.ephemeral({
|
||||
type: "session.next.compaction.delta",
|
||||
type: "compaction.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.durable({
|
||||
type: "session.next.compaction.ended",
|
||||
type: "compaction.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessage.ID,
|
||||
reason: Started.data.fields.reason,
|
||||
text: Schema.String,
|
||||
recent: Schema.String,
|
||||
|
|
@ -480,25 +473,25 @@ export namespace Compaction {
|
|||
|
||||
export namespace RevertEvent {
|
||||
export const Staged = Event.durable({
|
||||
type: "session.next.revert.staged",
|
||||
type: "revert.staged",
|
||||
...options,
|
||||
schema: { ...Base, revert: Revert.State },
|
||||
})
|
||||
export const Cleared = Event.durable({ type: "session.next.revert.cleared", ...options, schema: Base })
|
||||
export const Cleared = Event.durable({ type: "revert.cleared", ...options, schema: Base })
|
||||
export const Committed = Event.durable({
|
||||
type: "session.next.revert.committed",
|
||||
type: "revert.committed",
|
||||
...options,
|
||||
schema: { ...Base, messageID: SessionMessage.ID },
|
||||
})
|
||||
}
|
||||
|
||||
export const Definitions = Event.inventory(
|
||||
AgentSwitched,
|
||||
ModelSwitched,
|
||||
AgentSelected,
|
||||
ModelSelected,
|
||||
Moved,
|
||||
Renamed,
|
||||
Forked,
|
||||
Prompted,
|
||||
PromptPromoted,
|
||||
PromptAdmitted,
|
||||
ExecutionSettled,
|
||||
ContextUpdated,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import { FileAttachment, Prompt } from "./prompt.js"
|
|||
import { DateTimeUtcFromMillis, RelativePath, statics } from "./schema.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { ascending } from "./identifier.js"
|
||||
import { Event } from "./event.js"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe(
|
||||
Schema.brand("Session.Message.ID"),
|
||||
statics((schema) => ({ create: () => schema.make("msg_" + ascending()) })),
|
||||
statics((schema) => ({
|
||||
create: () => schema.make("msg_" + ascending()),
|
||||
fromEvent: (eventID: Event.ID) => schema.make(eventID.replace(/^evt_/, "msg_")),
|
||||
})),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
|
|
@ -27,19 +31,19 @@ const Base = {
|
|||
time: Schema.Struct({ created: DateTimeUtcFromMillis }),
|
||||
}
|
||||
|
||||
export interface AgentSwitched extends Schema.Schema.Type<typeof AgentSwitched> {}
|
||||
export const AgentSwitched = Schema.Struct({
|
||||
export interface AgentSelected extends Schema.Schema.Type<typeof AgentSelected> {}
|
||||
export const AgentSelected = Schema.Struct({
|
||||
...Base,
|
||||
type: Schema.Literal("agent-switched"),
|
||||
agent: Schema.String,
|
||||
}).annotate({ identifier: "Session.Message.AgentSwitched" })
|
||||
}).annotate({ identifier: "Session.Message.AgentSelected" })
|
||||
|
||||
export interface ModelSwitched extends Schema.Schema.Type<typeof ModelSwitched> {}
|
||||
export const ModelSwitched = Schema.Struct({
|
||||
export interface ModelSelected extends Schema.Schema.Type<typeof ModelSelected> {}
|
||||
export const ModelSelected = Schema.Struct({
|
||||
...Base,
|
||||
type: Schema.Literal("model-switched"),
|
||||
model: Model.Ref,
|
||||
}).annotate({ identifier: "Session.Message.ModelSwitched" })
|
||||
}).annotate({ identifier: "Session.Message.ModelSelected" })
|
||||
|
||||
export interface User extends Schema.Schema.Type<typeof User> {}
|
||||
export const User = Schema.Struct({
|
||||
|
|
@ -207,8 +211,8 @@ export const Compaction = Schema.Struct({
|
|||
}).annotate({ identifier: "Session.Message.Compaction" })
|
||||
|
||||
export const Message = Schema.Union([
|
||||
AgentSwitched,
|
||||
ModelSwitched,
|
||||
AgentSelected,
|
||||
ModelSelected,
|
||||
User,
|
||||
Synthetic,
|
||||
System,
|
||||
|
|
@ -219,5 +223,5 @@ export const Message = Schema.Union([
|
|||
])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "Session.Message" })
|
||||
export type Message = AgentSwitched | ModelSwitched | User | Synthetic | System | Skill | Shell | Assistant | Compaction
|
||||
export type Message = AgentSelected | ModelSelected | User | Synthetic | System | Skill | Shell | Assistant | Compaction
|
||||
export type Type = Message["type"]
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ describe("public event manifest", () => {
|
|||
expect(Session.Event.Definitions).toBe(SessionEvent.Definitions)
|
||||
expect(Workspace.Event).toBe(WorkspaceEvent)
|
||||
expect(Workspace.Event.Definitions).toBe(WorkspaceEvent.Definitions)
|
||||
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Latest.get("step.ended")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Latest.get("todo.updated")).toBe(SessionTodo.Event.Updated)
|
||||
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
|
||||
expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated)
|
||||
|
|
@ -71,8 +71,8 @@ describe("public event manifest", () => {
|
|||
SessionV1.Event.Diff,
|
||||
SessionV1.Event.Error,
|
||||
])
|
||||
expect(EventManifest.Durable.has("session.next.step.ended.1")).toBe(false)
|
||||
expect(EventManifest.Durable.get("session.next.step.ended.2")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Durable.get("step.ended.1")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Durable.has("step.ended.2")).toBe(false)
|
||||
})
|
||||
|
||||
test("derives durable definitions from explicit definition durability", () => {
|
||||
|
|
@ -85,37 +85,37 @@ describe("public event manifest", () => {
|
|||
"message.removed.1",
|
||||
"message.part.updated.1",
|
||||
"message.part.removed.1",
|
||||
"session.next.agent.switched.1",
|
||||
"session.next.model.switched.1",
|
||||
"session.next.moved.1",
|
||||
"session.next.renamed.1",
|
||||
"session.next.forked.1",
|
||||
"session.next.prompted.1",
|
||||
"session.next.prompt.admitted.1",
|
||||
"session.next.context.updated.1",
|
||||
"session.next.synthetic.1",
|
||||
"session.next.skill.activated.1",
|
||||
"session.next.shell.started.1",
|
||||
"session.next.shell.ended.1",
|
||||
"session.next.step.started.1",
|
||||
"session.next.step.ended.2",
|
||||
"session.next.step.failed.2",
|
||||
"session.next.text.started.1",
|
||||
"session.next.text.ended.1",
|
||||
"session.next.tool.input.started.1",
|
||||
"session.next.tool.input.ended.1",
|
||||
"session.next.tool.called.1",
|
||||
"session.next.tool.progress.1",
|
||||
"session.next.tool.success.1",
|
||||
"session.next.tool.failed.1",
|
||||
"session.next.reasoning.started.1",
|
||||
"session.next.reasoning.ended.1",
|
||||
"session.next.retried.1",
|
||||
"session.next.compaction.started.1",
|
||||
"session.next.compaction.ended.1",
|
||||
"session.next.revert.staged.1",
|
||||
"session.next.revert.cleared.1",
|
||||
"session.next.revert.committed.1",
|
||||
"agent.selected.1",
|
||||
"model.selected.1",
|
||||
"session.moved.1",
|
||||
"renamed.1",
|
||||
"forked.1",
|
||||
"prompt.promoted.1",
|
||||
"prompt.admitted.1",
|
||||
"session.context.updated.1",
|
||||
"synthetic.1",
|
||||
"skill.activated.1",
|
||||
"shell.started.1",
|
||||
"shell.ended.1",
|
||||
"step.started.1",
|
||||
"step.ended.1",
|
||||
"step.failed.1",
|
||||
"text.started.1",
|
||||
"text.ended.1",
|
||||
"tool.input.started.1",
|
||||
"tool.input.ended.1",
|
||||
"tool.called.1",
|
||||
"tool.progress.1",
|
||||
"tool.success.1",
|
||||
"tool.failed.1",
|
||||
"reasoning.started.1",
|
||||
"reasoning.ended.1",
|
||||
"retried.1",
|
||||
"compaction.started.1",
|
||||
"compaction.ended.1",
|
||||
"revert.staged.1",
|
||||
"revert.cleared.1",
|
||||
"revert.committed.1",
|
||||
].toSorted(),
|
||||
)
|
||||
expect(SessionEvent.DurableDefinitions).toEqual(
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ it.live(
|
|||
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
|
||||
})
|
||||
const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe(
|
||||
Stream.filter((event) => event.type === "session.next.prompted" && event.data.messageID === wake.id),
|
||||
Stream.filter((event) => event.type === "prompt.promoted" && event.data.inputID === wake.id),
|
||||
Stream.runHead,
|
||||
Effect.timeout("10 seconds"),
|
||||
Effect.map(Option.getOrThrow),
|
||||
|
|
@ -119,7 +119,7 @@ it.live(
|
|||
expect(page.data.some((session) => session.id === id)).toBe(true)
|
||||
expect(active).toEqual({ data: {}, watermarks: {} })
|
||||
expect(admitted.sessionID).toBe(id)
|
||||
expect(prompted.type).toBe("session.next.prompted")
|
||||
expect(prompted.type).toBe("prompt.promoted")
|
||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
||||
expect(contextEntries).toEqual([
|
||||
{ key: "deploy-target", value: "production" },
|
||||
|
|
@ -127,7 +127,7 @@ it.live(
|
|||
])
|
||||
expect(remainingContextEntries).toEqual([{ key: "deploy-target", value: "production" }])
|
||||
expect(context.some((message) => message.type === "model-switched")).toBe(true)
|
||||
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
|
||||
expect(event).toMatchObject({ type: "model.selected", durable: { seq: 1 } })
|
||||
expect(message).toEqual(modelMessage)
|
||||
expect(missing.map((error) => error._tag)).toEqual([
|
||||
"SessionNotFoundError",
|
||||
|
|
@ -149,13 +149,13 @@ it.live(
|
|||
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||
const id = sessionID(fixture)
|
||||
const connected = yield* Latch.make(false)
|
||||
const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "session.next.prompted" }>>()
|
||||
const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "prompt.promoted" }>>()
|
||||
|
||||
yield* opencode.events.subscribe().pipe(
|
||||
Stream.runForEach((event) =>
|
||||
event.type === "server.connected"
|
||||
? connected.open
|
||||
: event.type === "session.next.prompted" && event.data.sessionID === id
|
||||
: event.type === "prompt.promoted" && event.data.sessionID === id
|
||||
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
||||
: Effect.void,
|
||||
),
|
||||
|
|
@ -191,7 +191,7 @@ it.live(
|
|||
Stream.runForEach((notification: OpenCodeEvent) =>
|
||||
notification.type === "server.connected"
|
||||
? ready.open
|
||||
: notification.type === "session.next.agent.switched" && notification.data.sessionID === id
|
||||
: notification.type === "agent.selected" && notification.data.sessionID === id
|
||||
? event.open
|
||||
: Effect.void,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,13 @@ if (schemas) {
|
|||
}
|
||||
visit({ ...document, components: { ...document.components, schemas: undefined } })
|
||||
for (const name of Object.keys(schemas)) {
|
||||
if (/^SessionNext\w+1$/.test(name) && !reachable.has(name)) delete schemas[name]
|
||||
if (
|
||||
/^(AgentSelected|ModelSelected|SessionMoved|Renamed|Forked|PromptPromoted|PromptAdmitted|ExecutionSettled|ContextUpdated|Synthetic|SkillActivated|ShellStarted|ShellEnded|StepStarted|StepEnded|StepFailed|TextStarted|TextDelta|TextEnded|ReasoningStarted|ReasoningDelta|ReasoningEnded|ToolInputStarted|ToolInputDelta|ToolInputEnded|ToolCalled|ToolProgress|ToolSuccess|ToolFailed|Retried|CompactionStarted|CompactionDelta|CompactionEnded|RevertStaged|RevertCleared|RevertCommitted)1$/.test(
|
||||
name,
|
||||
) &&
|
||||
!reachable.has(name)
|
||||
)
|
||||
delete schemas[name]
|
||||
}
|
||||
await Bun.write("./openapi.json", JSON.stringify(document))
|
||||
}
|
||||
|
|
@ -93,7 +99,11 @@ await createClient({
|
|||
|
||||
const generatedTypesPath = "./src/v2/gen/types.gen.ts"
|
||||
const generatedTypes = await Bun.file(generatedTypesPath).text()
|
||||
if (/export type SessionNext\w+1 =/.test(generatedTypes)) {
|
||||
if (
|
||||
/export type (AgentSelected|ModelSelected|SessionMoved|Renamed|Forked|PromptPromoted|PromptAdmitted|ExecutionSettled|ContextUpdated|Synthetic|SkillActivated|ShellStarted|ShellEnded|StepStarted|StepEnded|StepFailed|TextStarted|TextDelta|TextEnded|ReasoningStarted|ReasoningDelta|ReasoningEnded|ToolInputStarted|ToolInputDelta|ToolInputEnded|ToolCalled|ToolProgress|ToolSuccess|ToolFailed|Retried|CompactionStarted|CompactionDelta|CompactionEnded|RevertStaged|RevertCleared|RevertCommitted)1 =/.test(
|
||||
generatedTypes,
|
||||
)
|
||||
) {
|
||||
throw new Error("Session history generated duplicate Session event variants")
|
||||
}
|
||||
const logTypesPatched = generatedTypes.replace(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -27,6 +27,8 @@ import { createSignal, onCleanup } from "solid-js"
|
|||
|
||||
export type DataSessionStatus = "idle" | "running"
|
||||
|
||||
const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_")
|
||||
|
||||
type LocationData = {
|
||||
agent?: AgentV2Info[]
|
||||
command?: CommandV2Info[]
|
||||
|
|
@ -181,17 +183,21 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
const info = store.session.info[sessionID]
|
||||
if (!info) return
|
||||
const rootID = resolveRoot(sessionID)
|
||||
setStore("session", "family", produce((draft) => {
|
||||
if (sessionID !== rootID && draft[sessionID]) {
|
||||
const members = draft[rootID] ??= []
|
||||
for (const id of draft[sessionID]) {
|
||||
if (!members.includes(id)) members.push(id)
|
||||
setStore(
|
||||
"session",
|
||||
"family",
|
||||
produce((draft) => {
|
||||
if (sessionID !== rootID && draft[sessionID]) {
|
||||
const members = (draft[rootID] ??= [])
|
||||
for (const id of draft[sessionID]) {
|
||||
if (!members.includes(id)) members.push(id)
|
||||
}
|
||||
delete draft[sessionID]
|
||||
}
|
||||
delete draft[sessionID]
|
||||
}
|
||||
const family = draft[rootID] ??= []
|
||||
if (!family.includes(sessionID)) family.push(sessionID)
|
||||
}))
|
||||
const family = (draft[rootID] ??= [])
|
||||
if (!family.includes(sessionID)) family.push(sessionID)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function handleEvent(event: V2Event) {
|
||||
|
|
@ -214,124 +220,113 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
case "skill.updated":
|
||||
void result.location.skill.refresh(event.location)
|
||||
break
|
||||
case "session.next.agent.switched":
|
||||
case "agent.selected":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "agent-switched",
|
||||
agent: event.data.agent,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.model.switched":
|
||||
case "model.selected":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "model", event.data.model)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "model-switched",
|
||||
model: event.data.model,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.renamed":
|
||||
case "renamed":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "title", event.data.title)
|
||||
break
|
||||
case "session.next.prompted": {
|
||||
case "prompt.promoted": {
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const position = index.get(event.data.messageID)
|
||||
const position = index.get(event.data.inputID)
|
||||
const existing = position === undefined ? undefined : draft[position]
|
||||
if (existing?.type === "user") {
|
||||
existing.text = event.data.prompt.text
|
||||
existing.files = event.data.prompt.files
|
||||
existing.agents = event.data.prompt.agents
|
||||
existing.time.created = event.data.timestamp
|
||||
existing.time.created = event.created
|
||||
if (existing.metadata?.queued === true) {
|
||||
delete existing.metadata.queued
|
||||
if (Object.keys(existing.metadata).length === 0) existing.metadata = undefined
|
||||
}
|
||||
return
|
||||
}
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
type: "user",
|
||||
text: event.data.prompt.text,
|
||||
files: event.data.prompt.files,
|
||||
agents: event.data.prompt.agents,
|
||||
time: { created: event.data.timestamp },
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.next.prompt.admitted":
|
||||
case "prompt.admitted":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
id: event.data.inputID,
|
||||
type: "user",
|
||||
text: event.data.prompt.text,
|
||||
files: event.data.prompt.files,
|
||||
agents: event.data.prompt.agents,
|
||||
metadata: { queued: true },
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.context.updated":
|
||||
case "session.context.updated":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "system",
|
||||
text: event.data.text,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.synthetic":
|
||||
case "synthetic":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "synthetic",
|
||||
sessionID: event.data.sessionID,
|
||||
text: event.data.text,
|
||||
description: event.data.description,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.shell.started":
|
||||
case "shell.started":
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "shell",
|
||||
callID: event.data.callID,
|
||||
command: event.data.command,
|
||||
output: "",
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.shell.ended":
|
||||
case "shell.ended":
|
||||
setStore("session", "status", event.data.sessionID, "idle")
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.activeShell(draft, event.data.callID)
|
||||
if (!match) return
|
||||
match.output = event.data.output
|
||||
match.time.completed = event.data.timestamp
|
||||
match.time.completed = event.created
|
||||
})
|
||||
break
|
||||
case "session.next.step.started":
|
||||
case "step.started":
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
if (index.has(event.data.assistantMessageID)) return
|
||||
const currentAssistant = message.activeAssistant(draft)
|
||||
if (currentAssistant) currentAssistant.time.completed = event.data.timestamp
|
||||
if (currentAssistant) currentAssistant.time.completed = event.created
|
||||
message.append(draft, index, {
|
||||
id: event.data.assistantMessageID,
|
||||
type: "assistant",
|
||||
|
|
@ -339,16 +334,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
model: event.data.model,
|
||||
content: [],
|
||||
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.step.ended":
|
||||
case "step.ended":
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
|
||||
if (!currentAssistant) return
|
||||
currentAssistant.time.completed = event.data.timestamp
|
||||
currentAssistant.time.completed = event.created
|
||||
currentAssistant.finish = event.data.finish
|
||||
currentAssistant.cost = event.data.cost
|
||||
currentAssistant.tokens = event.data.tokens
|
||||
|
|
@ -356,16 +351,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.data.snapshot }
|
||||
})
|
||||
break
|
||||
case "session.next.step.failed":
|
||||
case "step.failed":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
|
||||
if (!currentAssistant) return
|
||||
currentAssistant.time.completed = event.data.timestamp
|
||||
currentAssistant.time.completed = event.created
|
||||
currentAssistant.finish = "error"
|
||||
currentAssistant.error = event.data.error
|
||||
})
|
||||
break
|
||||
case "session.next.text.started":
|
||||
case "text.started":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
|
||||
type: "text",
|
||||
|
|
@ -374,7 +369,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
})
|
||||
})
|
||||
break
|
||||
case "session.next.text.delta":
|
||||
case "text.delta":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestText(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -383,7 +378,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
if (match) match.text += event.data.delta
|
||||
})
|
||||
break
|
||||
case "session.next.text.ended":
|
||||
case "text.ended":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestText(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -392,18 +387,18 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
if (match) match.text = event.data.text
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.started":
|
||||
case "tool.input.started":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
name: event.data.name,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
state: { status: "pending", input: "" },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.delta":
|
||||
case "tool.input.delta":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -412,7 +407,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
if (match?.state.status === "pending") match.state.input += event.data.delta
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.ended":
|
||||
case "tool.input.ended":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -421,19 +416,19 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
if (match?.state.status === "pending") match.state.input = event.data.text
|
||||
})
|
||||
break
|
||||
case "session.next.tool.called":
|
||||
case "tool.called":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
)
|
||||
if (!match) return
|
||||
match.time.ran = event.data.timestamp
|
||||
match.time.ran = event.created
|
||||
match.provider = event.data.provider
|
||||
match.state = { status: "running", input: event.data.input, structured: {}, content: [] }
|
||||
})
|
||||
break
|
||||
case "session.next.tool.progress":
|
||||
case "tool.progress":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -444,7 +439,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
match.state.content = [...event.data.content]
|
||||
})
|
||||
break
|
||||
case "session.next.tool.success":
|
||||
case "tool.success":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -463,10 +458,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
metadata: match.provider?.metadata,
|
||||
resultMetadata: event.data.provider.metadata,
|
||||
}
|
||||
match.time.completed = event.data.timestamp
|
||||
match.time.completed = event.created
|
||||
})
|
||||
break
|
||||
case "session.next.tool.failed":
|
||||
case "tool.failed":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -486,21 +481,21 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
metadata: match.provider?.metadata,
|
||||
resultMetadata: event.data.provider.metadata,
|
||||
}
|
||||
match.time.completed = event.data.timestamp
|
||||
match.time.completed = event.created
|
||||
})
|
||||
break
|
||||
case "session.next.reasoning.started":
|
||||
case "reasoning.started":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
|
||||
type: "reasoning",
|
||||
id: event.data.reasoningID,
|
||||
text: "",
|
||||
providerMetadata: event.data.providerMetadata,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.reasoning.delta":
|
||||
case "reasoning.delta":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestReasoning(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -509,7 +504,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
if (match) match.text += event.data.delta
|
||||
})
|
||||
break
|
||||
case "session.next.reasoning.ended":
|
||||
case "reasoning.ended":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestReasoning(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
|
|
@ -517,38 +512,38 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
)
|
||||
if (match) {
|
||||
match.text = event.data.text
|
||||
match.time = { created: match.time?.created ?? event.data.timestamp, completed: event.data.timestamp }
|
||||
match.time = { created: match.time?.created ?? event.created, completed: event.created }
|
||||
if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata
|
||||
}
|
||||
})
|
||||
break
|
||||
case "session.next.retried":
|
||||
case "session.next.compaction.started":
|
||||
case "retried":
|
||||
case "compaction.started":
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
break
|
||||
case "session.next.execution.settled":
|
||||
case "execution.settled":
|
||||
setStore("session", "status", event.data.sessionID, "idle")
|
||||
break
|
||||
case "session.next.revert.staged":
|
||||
case "revert.staged":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "revert", event.data.revert)
|
||||
break
|
||||
case "session.next.revert.cleared":
|
||||
case "session.next.revert.committed":
|
||||
case "revert.cleared":
|
||||
case "revert.committed":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "revert", undefined)
|
||||
break
|
||||
case "session.next.compaction.delta":
|
||||
case "compaction.delta":
|
||||
break
|
||||
case "session.next.compaction.ended":
|
||||
case "compaction.ended":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.messageID,
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "compaction",
|
||||
reason: event.data.reason,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
time: { created: event.data.timestamp },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
|
|
@ -692,8 +687,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return liveByID.get(message.id) ?? message
|
||||
}),
|
||||
...live.filter((message) => !loadedIDs.has(message.id)),
|
||||
]
|
||||
.toSorted((a, b) => a.time.created - b.time.created)
|
||||
].toSorted((a, b) => a.time.created - b.time.created)
|
||||
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])))
|
||||
setStore("session", "message", sessionID, messages)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -74,17 +74,17 @@ const tui: TuiPlugin = async (api) => {
|
|||
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
|
||||
}
|
||||
|
||||
api.event.on("session.next.prompted", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.shell.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.step.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.retried", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.compaction.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.shell.ended", (event) => ended(event.data.sessionID))
|
||||
api.event.on("session.next.step.ended", (event) => {
|
||||
api.event.on("prompt.promoted", (event) => started(event.data.sessionID))
|
||||
api.event.on("shell.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("step.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("retried", (event) => started(event.data.sessionID))
|
||||
api.event.on("compaction.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("shell.ended", (event) => ended(event.data.sessionID))
|
||||
api.event.on("step.ended", (event) => {
|
||||
if (event.data.finish === "tool-calls") return
|
||||
ended(event.data.sessionID)
|
||||
})
|
||||
api.event.on("session.next.step.failed", (event) => {
|
||||
api.event.on("step.failed", (event) => {
|
||||
const sessionID = event.data.sessionID
|
||||
if (!active.has(sessionID)) return
|
||||
errored.add(sessionID)
|
||||
|
|
|
|||
|
|
@ -126,45 +126,49 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
return index === -1 ? rows.length : index
|
||||
}
|
||||
|
||||
const message = (event: { data: { sessionID: string; messageID: string } }) => {
|
||||
if (event.data.sessionID === sessionID()) appendMessage(event.data.messageID)
|
||||
const message = (event: { id: string; data: { sessionID: string } }) => {
|
||||
if (event.data.sessionID === sessionID()) appendMessage(event.id.replace(/^evt_/, "msg_"))
|
||||
}
|
||||
const input = (event: { data: { sessionID: string; inputID: string } }) => {
|
||||
if (event.data.sessionID === sessionID()) appendMessage(event.data.inputID)
|
||||
}
|
||||
const subscriptions = [
|
||||
data.on("session.next.prompt.admitted", message),
|
||||
data.on("session.next.prompted", message),
|
||||
data.on("session.next.context.updated", message),
|
||||
data.on("session.next.synthetic", (event) => {
|
||||
if (event.data.sessionID === sessionID() && event.data.description?.trim()) appendMessage(event.data.messageID)
|
||||
data.on("prompt.admitted", input),
|
||||
data.on("prompt.promoted", input),
|
||||
data.on("session.context.updated", message),
|
||||
data.on("synthetic", (event) => {
|
||||
if (event.data.sessionID === sessionID() && event.data.description?.trim())
|
||||
appendMessage(event.id.replace(/^evt_/, "msg_"))
|
||||
}),
|
||||
data.on("session.next.shell.started", message),
|
||||
data.on("session.next.agent.switched", message),
|
||||
data.on("session.next.model.switched", message),
|
||||
data.on("session.next.compaction.ended", message),
|
||||
data.on("session.next.text.delta", (event) => {
|
||||
data.on("shell.started", message),
|
||||
data.on("agent.selected", message),
|
||||
data.on("model.selected", message),
|
||||
data.on("compaction.ended", message),
|
||||
data.on("text.delta", (event) => {
|
||||
if (event.data.sessionID === sessionID())
|
||||
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.textID })
|
||||
}),
|
||||
data.on("session.next.text.ended", (event) => {
|
||||
data.on("text.ended", (event) => {
|
||||
if (event.data.sessionID === sessionID() && event.data.text.trim())
|
||||
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.textID })
|
||||
}),
|
||||
data.on("session.next.reasoning.delta", (event) => {
|
||||
data.on("reasoning.delta", (event) => {
|
||||
if (event.data.sessionID === sessionID())
|
||||
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.reasoningID })
|
||||
}),
|
||||
data.on("session.next.reasoning.ended", (event) => {
|
||||
data.on("reasoning.ended", (event) => {
|
||||
if (event.data.sessionID === sessionID() && event.data.text.trim())
|
||||
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.reasoningID })
|
||||
}),
|
||||
data.on("session.next.tool.input.started", (event) => {
|
||||
data.on("tool.input.started", (event) => {
|
||||
if (event.data.sessionID === sessionID())
|
||||
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.callID }, event.data.name)
|
||||
}),
|
||||
data.on("session.next.step.ended", (event) => {
|
||||
data.on("step.ended", (event) => {
|
||||
if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return
|
||||
appendFooter(event.data.assistantMessageID)
|
||||
}),
|
||||
data.on("session.next.step.failed", (event) => {
|
||||
data.on("step.failed", (event) => {
|
||||
if (event.data.sessionID === sessionID()) appendFooter(event.data.assistantMessageID)
|
||||
}),
|
||||
]
|
||||
|
|
@ -229,8 +233,6 @@ function hasPart(rows: SessionRow[], ref: PartRef) {
|
|||
return rows.some((row) => {
|
||||
if (row.type === "part") return row.ref.messageID === ref.messageID && row.ref.partID === ref.partID
|
||||
if (row.type !== "group") return false
|
||||
return [...row.refs, ...row.pending].some(
|
||||
(item) => item.messageID === ref.messageID && item.partID === ref.partID,
|
||||
)
|
||||
return [...row.refs, ...row.pending].some((item) => item.messageID === ref.messageID && item.partID === ref.partID)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,12 +90,12 @@ function durable(sessionID: string) {
|
|||
function stepStarted(id: string, sessionID = "session"): V2Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.started",
|
||||
created: 0,
|
||||
type: "step.started",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
},
|
||||
|
|
@ -105,12 +105,12 @@ function stepStarted(id: string, sessionID = "session"): V2Event {
|
|||
function stepEnded(id: string, sessionID = "session", finish = "stop"): V2Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.ended",
|
||||
created: 0,
|
||||
type: "step.ended",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
finish,
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
|
|
@ -121,12 +121,12 @@ function stepEnded(id: string, sessionID = "session", finish = "stop"): V2Event
|
|||
function stepFailed(id: string, sessionID = "session"): V2Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.failed",
|
||||
created: 0,
|
||||
type: "step.failed",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
error: { type: "unknown", message: "boom" },
|
||||
},
|
||||
}
|
||||
|
|
@ -150,8 +150,8 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("notifies for question and permission requests with blurred notifications and always-on sounds", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-1", created: 0, type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", created: 0, type: "permission.asked", data: permission("permission-1") })
|
||||
|
||||
expect(harness.notifications).toEqual([questionNotification, permissionNotification])
|
||||
})
|
||||
|
|
@ -159,23 +159,25 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("dedupes pending questions and permissions until they are resolved", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-1", created: 0, type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", created: 0, type: "question.asked", data: question("question-1") })
|
||||
harness.emit({
|
||||
id: "event-3",
|
||||
created: 0,
|
||||
type: "question.replied",
|
||||
data: { sessionID: "session", requestID: "question-1", answers: [] },
|
||||
})
|
||||
harness.emit({ id: "event-4", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-4", created: 0, type: "question.asked", data: question("question-1") })
|
||||
|
||||
harness.emit({ id: "event-5", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-6", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-5", created: 0, type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-6", created: 0, type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({
|
||||
id: "event-7",
|
||||
created: 0,
|
||||
type: "permission.replied",
|
||||
data: { sessionID: "session", requestID: "permission-1", reply: "once" },
|
||||
})
|
||||
harness.emit({ id: "event-8", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-8", created: 0, type: "permission.asked", data: permission("permission-1") })
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
questionNotification,
|
||||
|
|
@ -205,7 +207,7 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("uses sound-only notifications and subagent_done sound for subagent sessions", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1", "subagent") })
|
||||
harness.emit({ id: "event-1", created: 0, type: "question.asked", data: question("question-1", "subagent") })
|
||||
harness.emit(stepStarted("event-2", "subagent"))
|
||||
harness.emit(stepEnded("event-3", "subagent"))
|
||||
|
||||
|
|
@ -248,12 +250,14 @@ describe("internal notifications TUI plugin", () => {
|
|||
harness.emit(stepStarted("event-1", "abort"))
|
||||
harness.emit({
|
||||
id: "event-2",
|
||||
created: 0,
|
||||
type: "session.error",
|
||||
data: { sessionID: "abort", error: { name: "MessageAbortedError", data: { message: "Aborted" } } },
|
||||
})
|
||||
harness.emit(stepStarted("event-3", "timeout"))
|
||||
harness.emit({
|
||||
id: "event-4",
|
||||
created: 0,
|
||||
type: "session.error",
|
||||
data: { sessionID: "timeout", error: { name: "UnknownError", data: { message: "SSE read timed out" } } },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { onMount } from "solid-js"
|
||||
import { ProjectProvider } from "../../../src/context/project"
|
||||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
|
|
@ -47,8 +49,8 @@ test("refreshes resources into reactive getters", async () => {
|
|||
if (url.pathname === "/api/session/ses_test/message")
|
||||
return json({
|
||||
data: [
|
||||
{ id: "msg_second", type: "user", text: "Second", time: { created: 2 } },
|
||||
{ id: "msg_first", type: "user", text: "First", time: { created: 1 } },
|
||||
{ id: "msg_second", created: 0, type: "user", text: "Second", time: { created: 2 } },
|
||||
{ id: "msg_first", created: 0, type: "user", text: "First", time: { created: 1 } },
|
||||
],
|
||||
cursor: {},
|
||||
})
|
||||
|
|
@ -187,7 +189,9 @@ test("connectedOnce is false until first connect and persists across disconnect"
|
|||
)
|
||||
const connect = () =>
|
||||
stream?.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n`),
|
||||
encoder.encode(
|
||||
`data: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n`,
|
||||
),
|
||||
)
|
||||
const disconnect = () => {
|
||||
stream?.close()
|
||||
|
|
@ -264,12 +268,12 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_step_started",
|
||||
type: "session.next.step.started",
|
||||
created: 0,
|
||||
type: "step.started",
|
||||
durable: durable("session-live"),
|
||||
data: {
|
||||
sessionID: "session-live",
|
||||
assistantMessageID: "message-live",
|
||||
timestamp: 1,
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
},
|
||||
|
|
@ -278,12 +282,12 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_step_ended",
|
||||
type: "session.next.step.ended",
|
||||
created: 0,
|
||||
type: "step.ended",
|
||||
durable: durable("session-live", 1, 2),
|
||||
data: {
|
||||
sessionID: "session-live",
|
||||
assistantMessageID: "message-live",
|
||||
timestamp: 2,
|
||||
finish: "stop",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
|
|
@ -297,10 +301,10 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_execution_settled",
|
||||
type: "session.next.execution.settled",
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: {
|
||||
sessionID: "session-live",
|
||||
timestamp: 3,
|
||||
outcome: "success",
|
||||
},
|
||||
})
|
||||
|
|
@ -308,12 +312,12 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_failed_step_started",
|
||||
type: "session.next.step.started",
|
||||
created: 0,
|
||||
type: "step.started",
|
||||
durable: durable("session-failed"),
|
||||
data: {
|
||||
sessionID: "session-failed",
|
||||
assistantMessageID: "message-failed",
|
||||
timestamp: 3,
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
},
|
||||
|
|
@ -322,12 +326,12 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_step_failed",
|
||||
type: "session.next.step.failed",
|
||||
created: 0,
|
||||
type: "step.failed",
|
||||
durable: durable("session-failed", 1, 2),
|
||||
data: {
|
||||
sessionID: "session-failed",
|
||||
assistantMessageID: "message-failed",
|
||||
timestamp: 4,
|
||||
error: { type: "unknown", message: "Provider unavailable" },
|
||||
},
|
||||
})
|
||||
|
|
@ -339,10 +343,10 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_failed_execution_settled",
|
||||
type: "session.next.execution.settled",
|
||||
created: 0,
|
||||
type: "execution.settled",
|
||||
data: {
|
||||
sessionID: "session-failed",
|
||||
timestamp: 5,
|
||||
outcome: "failure",
|
||||
error: { type: "unknown", message: "Provider unavailable" },
|
||||
},
|
||||
|
|
@ -411,7 +415,7 @@ test("refreshes integrations after integration updates", async () => {
|
|||
expect(data.location.integration.list()).toEqual([])
|
||||
const before = { ...requests }
|
||||
|
||||
emitEvent(events, { id: "evt_integration", type: "integration.updated", data: {} })
|
||||
emitEvent(events, { id: "evt_integration", created: 0, type: "integration.updated", data: {} })
|
||||
await wait(() => data.location.integration.list()?.length === 1)
|
||||
await wait(() => requests.model > before.model && requests.provider > before.provider)
|
||||
expect(data.location.integration.list()?.[0]).toMatchObject({ id: "openai", name: "OpenAI" })
|
||||
|
|
@ -449,7 +453,7 @@ test("refreshes effective catalog data after catalog updates", async () => {
|
|||
try {
|
||||
await wait(() => requests.model > 0 && requests.provider > 0)
|
||||
const before = { ...requests }
|
||||
emitEvent(events, { id: "evt_catalog", type: "catalog.updated", data: {} })
|
||||
emitEvent(events, { id: "evt_catalog", created: 0, type: "catalog.updated", data: {} })
|
||||
await wait(() => requests.model > before.model && requests.provider > before.provider)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
|
|
@ -496,7 +500,7 @@ test("refreshes agents after agent updates", async () => {
|
|||
|
||||
try {
|
||||
await wait(() => data.location.agent.list()?.[0]?.id === "build")
|
||||
emitEvent(events, { id: "evt_agent", type: "agent.updated", data: {} })
|
||||
emitEvent(events, { id: "evt_agent", created: 0, type: "agent.updated", data: {} })
|
||||
await wait(() => data.location.agent.list()?.[0]?.id === "reviewer")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
|
|
@ -541,7 +545,7 @@ test("refreshes references after updates", async () => {
|
|||
try {
|
||||
await mounted
|
||||
await wait(() => requests === 1)
|
||||
emitEvent(events, { id: "evt_reference_1", type: "reference.updated", data: {} })
|
||||
emitEvent(events, { id: "evt_reference_1", created: 0, type: "reference.updated", data: {} })
|
||||
await wait(() => data.location.reference.list()?.length === 1)
|
||||
expect(data.location.reference.list()?.[0]?.name).toBe("docs")
|
||||
} finally {
|
||||
|
|
@ -602,6 +606,7 @@ test("keeps shell state scoped to location", async () => {
|
|||
|
||||
events.emit({
|
||||
id: "evt_shell_created",
|
||||
created: 0,
|
||||
type: "shell.created",
|
||||
location: { directory: other },
|
||||
data: {
|
||||
|
|
@ -650,6 +655,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
await wait(() => data.connection.status() === "connected")
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_asked_1",
|
||||
created: 0,
|
||||
type: "permission.v2.asked",
|
||||
data: {
|
||||
id: "per_1",
|
||||
|
|
@ -660,6 +666,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_asked_2",
|
||||
created: 0,
|
||||
type: "permission.v2.asked",
|
||||
data: {
|
||||
id: "per_2",
|
||||
|
|
@ -672,6 +679,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_replied_1",
|
||||
created: 0,
|
||||
type: "permission.v2.replied",
|
||||
data: { sessionID: "ses_1", requestID: "per_1", reply: "once" },
|
||||
})
|
||||
|
|
@ -680,6 +688,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_replied_2",
|
||||
created: 0,
|
||||
type: "permission.v2.replied",
|
||||
data: { sessionID: "ses_1", requestID: "per_2", reply: "reject" },
|
||||
})
|
||||
|
|
@ -715,6 +724,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
await wait(() => data.connection.status() === "connected")
|
||||
emitEvent(events, {
|
||||
id: "evt_question_asked_1",
|
||||
created: 0,
|
||||
type: "question.v2.asked",
|
||||
data: {
|
||||
id: "que_1",
|
||||
|
|
@ -724,6 +734,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_question_asked_2",
|
||||
created: 0,
|
||||
type: "question.v2.asked",
|
||||
data: {
|
||||
id: "que_2",
|
||||
|
|
@ -735,6 +746,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_question_replied_1",
|
||||
created: 0,
|
||||
type: "question.v2.replied",
|
||||
data: { sessionID: "ses_1", requestID: "que_1", answers: [["First"]] },
|
||||
})
|
||||
|
|
@ -743,6 +755,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_question_rejected_2",
|
||||
created: 0,
|
||||
type: "question.v2.rejected",
|
||||
data: { sessionID: "ses_1", requestID: "que_2" },
|
||||
})
|
||||
|
|
@ -783,52 +796,52 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
await mounted
|
||||
emitEvent(events, {
|
||||
id: "evt_agent_1",
|
||||
type: "session.next.agent.switched",
|
||||
created: 0,
|
||||
type: "agent.selected",
|
||||
durable: durable("session-1"),
|
||||
data: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
|
||||
data: { sessionID: "session-1", agent: "build" },
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_model_1",
|
||||
type: "session.next.model.switched",
|
||||
created: 0,
|
||||
type: "model.selected",
|
||||
durable: durable("session-1", 1),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_model_1",
|
||||
timestamp: 0,
|
||||
model: { id: "model-1", providerID: "provider-1" },
|
||||
},
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_step_started_1",
|
||||
type: "session.next.step.started",
|
||||
created: 0,
|
||||
type: "step.started",
|
||||
durable: durable("session-1", 2),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
timestamp: 1,
|
||||
agent: "build",
|
||||
model: { id: "model-1", providerID: "provider-1" },
|
||||
},
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_input_1",
|
||||
type: "session.next.tool.input.started",
|
||||
created: 0,
|
||||
type: "tool.input.started",
|
||||
durable: durable("session-1", 3),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
timestamp: 2,
|
||||
callID: "call-1",
|
||||
name: "bash",
|
||||
},
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_called_1",
|
||||
type: "session.next.tool.called",
|
||||
created: 0,
|
||||
type: "tool.called",
|
||||
durable: durable("session-1", 4),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 2,
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
|
|
@ -838,11 +851,11 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_failed_1",
|
||||
type: "session.next.tool.failed",
|
||||
created: 0,
|
||||
type: "tool.failed",
|
||||
durable: durable("session-1", 5),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 3,
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
callID: "call-1",
|
||||
error: { type: "unknown", message: "aborted" },
|
||||
|
|
@ -928,12 +941,12 @@ test("renders admitted prompts immediately with queued marker and clears when pr
|
|||
const unsubscribe = sync.listen((event) => received.push(event.name))
|
||||
emitEvent(events, {
|
||||
id: "evt_admitted_1",
|
||||
type: "session.next.prompt.admitted",
|
||||
created: 0,
|
||||
type: "prompt.admitted",
|
||||
durable: durable(sessionID),
|
||||
data: {
|
||||
sessionID,
|
||||
messageID,
|
||||
timestamp: 0,
|
||||
inputID: messageID,
|
||||
prompt: { text: "hello" },
|
||||
delivery: "steer",
|
||||
},
|
||||
|
|
@ -947,19 +960,17 @@ test("renders admitted prompts immediately with queued marker and clears when pr
|
|||
|
||||
emitEvent(events, {
|
||||
id: "evt_prompted_1",
|
||||
type: "session.next.prompted",
|
||||
created: 0,
|
||||
type: "prompt.promoted",
|
||||
durable: durable(sessionID, 1),
|
||||
data: {
|
||||
sessionID,
|
||||
messageID,
|
||||
timestamp: 0,
|
||||
prompt: { text: "hello" },
|
||||
delivery: "steer",
|
||||
inputID: messageID,
|
||||
},
|
||||
})
|
||||
|
||||
await wait(() => received.at(-1) === "session.next.prompted")
|
||||
expect(received.slice(-2)).toEqual(["session.next.prompt.admitted", "session.next.prompted"])
|
||||
await wait(() => received.at(-1) === "prompt.promoted")
|
||||
expect(received.slice(-2)).toEqual(["prompt.admitted", "prompt.promoted"])
|
||||
unsubscribe()
|
||||
const message = sync.session.message.list(sessionID)?.[0]
|
||||
expect(message?.type).toBe("user")
|
||||
|
|
@ -1007,21 +1018,21 @@ test("projects live context updates with their message ID", async () => {
|
|||
await mounted
|
||||
emitEvent(events, {
|
||||
id: "evt_context_1",
|
||||
type: "session.next.context.updated",
|
||||
created: 0,
|
||||
type: "session.context.updated",
|
||||
durable: durable("session-1"),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_context_1",
|
||||
timestamp: 1,
|
||||
text: "Updated context",
|
||||
},
|
||||
})
|
||||
|
||||
await wait(() => sync.session.message.list("session-1")?.length === 1)
|
||||
expect(sync.session.message.list("session-1")?.[0]).toMatchObject({
|
||||
id: "msg_context_1",
|
||||
id: SessionMessage.ID.fromEvent(EventV2.ID.make("evt_context_1")),
|
||||
type: "system",
|
||||
text: "Updated context",
|
||||
time: { created: 0 },
|
||||
})
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function event(payload: V2Event, input: { directory: string; project?: string; w
|
|||
function vcs(branch: string): V2Event {
|
||||
return {
|
||||
id: `evt_vcs_${branch}`,
|
||||
created: 0,
|
||||
type: "vcs.branch.updated",
|
||||
data: {
|
||||
branch,
|
||||
|
|
@ -40,6 +41,7 @@ function vcs(branch: string): V2Event {
|
|||
function update(version: string): V2Event {
|
||||
return {
|
||||
id: `evt_update_${version}`,
|
||||
created: 0,
|
||||
type: "installation.update-available",
|
||||
data: {
|
||||
version,
|
||||
|
|
|
|||
|
|
@ -904,3 +904,17 @@ Compatibility:
|
|||
|
||||
- Existing Context Epoch rows migrate in place by dropping the obsolete selection and pending-replacement columns.
|
||||
- Model and agent switches no longer discard earlier chronological System Context updates by forcing a new baseline.
|
||||
|
||||
## 2026-07-03: Normalize Session Event Names And Envelope Time
|
||||
|
||||
- Drop the experimental `session.next.` prefix from current Session event names.
|
||||
- Rename `agent.switched` to `agent.selected`, `model.switched` to `model.selected`, and `prompted` to `prompt.promoted`; add `prompt.admitted` as the durable prompt admission record.
|
||||
- Add an envelope-level `created` timestamp stored on each event row; payload-level `timestamp` fields are removed.
|
||||
- Remove projection-only `messageID` fields from selected/message-producing events; projected message IDs derive from the event ID. `revert.committed.messageID` remains, and `forked.messageID` is now `forked.from`.
|
||||
- Rename `session.moved.subdirectory` to `subpath` and normalize event-related schema identifiers.
|
||||
|
||||
Compatibility:
|
||||
|
||||
- V2 durable events and projections are experimental and are reset by `20260703090000_reset_v2_event_rename_sweep`; existing V2 event rows, event sequences, projected session messages, and admitted inputs are wiped.
|
||||
- All renamed durable event types restart at version 1 under their normalized names.
|
||||
- Generated Promise, Effect, and legacy JavaScript SDK surfaces were regenerated from the normalized schemas.
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ Before each provider turn, the runner estimates the complete model-visible reque
|
|||
|
||||
Compaction keeps the full transcript durable while replacing its active model representation with one hidden checkpoint containing a structured rolling summary and token-bounded serialized recent context. Provider-native assistant, reasoning, and tool messages never survive across the boundary, avoiding signature and encrypted-reasoning failures when the earlier prefix changes.
|
||||
|
||||
`session.next.compaction.started.1` durably identifies the attempt. Compaction deltas are live-only progress. `session.next.compaction.ended.1` durably stores the final summary and serialized recent context; only this completed event projects a model-visible compaction message. On the next provider attempt, the runner observes that completed compaction and directly renders a fresh Context Epoch baseline. A failed or interrupted attempt therefore leaves the previous history boundary active.
|
||||
`compaction.started.1` durably identifies the attempt. Compaction deltas are live-only progress. `compaction.ended.1` durably stores the final summary and serialized recent context; only this completed event projects a model-visible compaction message. On the next provider attempt, the runner observes that completed compaction and directly renders a fresh Context Epoch baseline. A failed or interrupted attempt therefore leaves the previous history boundary active.
|
||||
|
||||
Repeated compactions update the previous structured summary with newly compacted messages. The runner then reloads projected history and executes the original pending turn.
|
||||
|
||||
|
|
@ -170,9 +170,9 @@ The coordinator's active registry is also the source for `sessions.active()`. It
|
|||
|
||||
Inbox promotion coalesces pending steers in durable admission order. Once continuation would otherwise end, it promotes one queued input at a time in FIFO order. Add explicit inbox backlog and steering-batch limits before exposing broad multi-caller admission or untrusted queue growth.
|
||||
|
||||
Eager local-tool execution is intentionally unbounded in the current local slice. This minimizes tool latency but does not increase SQLite settlement throughput: Session-event publication remains serialized per provider turn. Before broadening exposure, revisit per-turn call limits, output truncation, and operational backpressure using observed workloads. The `session.next.*` event schemas remain experimental and unshipped; databases created by earlier experimental builds are disposable rather than compatibility targets.
|
||||
Eager local-tool execution is intentionally unbounded in the current local slice. This minimizes tool latency but does not increase SQLite settlement throughput: Session-event publication remains serialized per provider turn. Before broadening exposure, revisit per-turn call limits, output truncation, and operational backpressure using observed workloads. The normalized Session event schemas remain experimental and unshipped; databases created by earlier experimental builds are disposable rather than compatibility targets.
|
||||
|
||||
The synchronized `session.next.*` event family and projected Session-message model predate this branch. This slice refines their replay contract: projected Session messages retain their source aggregate sequence so canonical context ordering and `sessions.messages(...)` pagination follow durable event order even when caller-supplied IDs or timestamps do not. Consumers can use `sessions.log({ sessionID, after? })` to replay durable `session.next.*` events after an aggregate sequence cursor, then tail durable events without a race. Live-only text, reasoning, and tool-input fragments remain available through EventV2 subscriptions for connected renderers; they are intentionally absent from the replayable Session stream.
|
||||
The normalized Session event family and projected Session-message model predate this branch. This slice refines their replay contract: projected Session messages retain their source aggregate sequence so canonical context ordering and `sessions.messages(...)` pagination follow durable event order even when caller-supplied IDs differ; event time is carried by the envelope `created` field rather than duplicated in payloads. Consumers can use `sessions.log({ sessionID, after? })` to replay durable Session events after an aggregate sequence cursor, then tail durable events without a race. Live-only text, reasoning, and tool-input fragments remain available through EventV2 subscriptions for connected renderers; they are intentionally absent from the replayable Session stream.
|
||||
|
||||
The first `sessions.log(...)` contract is durable-only during both replay and live tailing. This keeps one cursor equal to one persisted aggregate sequence and is sufficient for reconnect-safe consumers. A later UI-facing API may optionally interleave live-only deltas while connected, but those fragments must remain explicitly ephemeral: they cannot advance the durable cursor, replay after reconnect, or be mistaken for publication boundaries.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue