refactor(core): make v2 session inputs event sourced (#30785)
This commit is contained in:
parent
057958c933
commit
76ecf2e58c
43 changed files with 4671 additions and 757 deletions
|
|
@ -21,6 +21,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
|
|
@ -29,7 +30,7 @@ import { SessionRunner } from "@opencode-ai/core/session/runner"
|
|||
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
||||
import { SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
|
@ -240,6 +241,7 @@ const replaySessionProjection = (id: SessionV2.ID) =>
|
|||
.pipe(Effect.orDie)
|
||||
|
||||
yield* events.remove(id)
|
||||
yield* db.delete(SessionInputTable).where(eq(SessionInputTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* events.replayAll(
|
||||
recorded.map((event) => ({
|
||||
|
|
@ -425,7 +427,9 @@ describe("SessionRunnerLLM", () => {
|
|||
const message = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run automatically" }) })
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* session.messages({ sessionID })).toEqual([message])
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{ id: message.id, type: "user", text: "Run automatically" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -1217,9 +1221,11 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover interrupted tool" }), resume: false })
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
const assistant = yield* events.publish(SessionEvent.Step.Started, {
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
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") },
|
||||
|
|
@ -1227,21 +1233,21 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: assistant.id,
|
||||
assistantMessageID,
|
||||
callID: "call-interrupted",
|
||||
name: "echo",
|
||||
})
|
||||
yield* events.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: assistant.id,
|
||||
assistantMessageID,
|
||||
callID: "call-interrupted",
|
||||
text: '{"text":"stale"}',
|
||||
})
|
||||
yield* events.publish(SessionEvent.Tool.Called, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: assistant.id,
|
||||
assistantMessageID,
|
||||
callID: "call-interrupted",
|
||||
tool: "echo",
|
||||
input: { text: "stale" },
|
||||
|
|
@ -1279,9 +1285,11 @@ describe("SessionRunnerLLM", () => {
|
|||
prompt: new Prompt({ text: "Recover interrupted hosted tool" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
const assistant = yield* events.publish(SessionEvent.Step.Started, {
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
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") },
|
||||
|
|
@ -1289,21 +1297,21 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: assistant.id,
|
||||
assistantMessageID,
|
||||
callID: "call-hosted-interrupted",
|
||||
name: "web_search",
|
||||
})
|
||||
yield* events.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: assistant.id,
|
||||
assistantMessageID,
|
||||
callID: "call-hosted-interrupted",
|
||||
text: '{"query":"stale"}',
|
||||
})
|
||||
yield* events.publish(SessionEvent.Tool.Called, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: assistant.id,
|
||||
assistantMessageID,
|
||||
callID: "call-hosted-interrupted",
|
||||
tool: "web_search",
|
||||
input: { query: "stale" },
|
||||
|
|
@ -1337,9 +1345,11 @@ describe("SessionRunnerLLM", () => {
|
|||
prompt: new Prompt({ text: "Recover interrupted tool input" }),
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
const assistant = yield* events.publish(SessionEvent.Step.Started, {
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
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") },
|
||||
|
|
@ -1347,7 +1357,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: assistant.id,
|
||||
assistantMessageID,
|
||||
callID: "call-pending-interrupted",
|
||||
name: "echo",
|
||||
})
|
||||
|
|
@ -1414,7 +1424,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.PromptLifecycle.Promoted, () => (fail ? Effect.die(defect) : Effect.void))
|
||||
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover promoted input" }), resume: false })
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
|
||||
|
|
@ -1433,6 +1443,30 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("does not strand a committed promotion when a post-commit listener defects", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* events.listen((event) =>
|
||||
event.type === SessionEvent.PromptLifecycle.Promoted.type
|
||||
? Effect.die("fail after prompt promotion commits")
|
||||
: Effect.void,
|
||||
)
|
||||
yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: new Prompt({ text: "Run committed promotion" }),
|
||||
resume: false,
|
||||
})
|
||||
|
||||
requests.length = 0
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(userTexts(requests[0]!)).toEqual(["Run committed promotion"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("runs different sessions concurrently", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue