refactor(schema): session shell payloads and event prefix restore (#35229)
This commit is contained in:
parent
64e4f6f91b
commit
650d774372
49 changed files with 1521 additions and 1200 deletions
|
|
@ -213,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: "forked",
|
||||
type: "session.forked",
|
||||
durable: { seq: 0 },
|
||||
data: { sessionID: forked.id, parentID: parent.id },
|
||||
})
|
||||
|
|
@ -378,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: "prompt.admitted", data: { prompt: { text: "Hello" } } },
|
||||
{ durable: { seq: 2 }, type: "prompt.promoted" },
|
||||
{ durable: { seq: 1 }, type: "session.prompt.admitted", data: { prompt: { text: "Hello" } } },
|
||||
{ durable: { seq: 2 }, type: "session.prompt.promoted" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
|
@ -494,8 +494,9 @@ describe("SessionV2.create", () => {
|
|||
|
||||
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
|
||||
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
|
||||
expect(shell).toMatchObject({ type: "shell", command: "echo hello" })
|
||||
expect(shell?.output).toContain("hello")
|
||||
expect(shell).toMatchObject({ type: "shell", shell: { command: "echo hello", status: "exited", exit: 0 } })
|
||||
expect(shell?.output?.output).toContain("hello")
|
||||
expect(shell?.output?.truncated).toBe(false)
|
||||
expect(shell?.time.completed).toBeDefined()
|
||||
}),
|
||||
),
|
||||
|
|
@ -513,7 +514,8 @@ describe("SessionV2.create", () => {
|
|||
|
||||
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
|
||||
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
|
||||
expect(shell).toMatchObject({ type: "shell", command: "false" })
|
||||
expect(shell).toMatchObject({ type: "shell", shell: { command: "false", status: "exited" } })
|
||||
expect(shell?.shell.exit).not.toBe(0)
|
||||
expect(shell?.time.completed).toBeDefined()
|
||||
}),
|
||||
),
|
||||
|
|
@ -529,7 +531,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: "agent.selected", data: { agent: "plan" } }])
|
||||
).toMatchObject([{ type: "session.agent.selected", data: { agent: "plan" } }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -562,7 +564,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: "model.selected", data: { model } }])
|
||||
).toMatchObject([{ type: "session.model.selected", data: { model } }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,14 +40,14 @@ describe("SessionV2.log", () => {
|
|||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const created = yield* session.create({ location })
|
||||
yield* session.rename({ sessionID: created.id, title: "renamed" })
|
||||
yield* session.rename({ sessionID: created.id, title: "session.renamed" })
|
||||
|
||||
const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id })))
|
||||
const watermark = (yield* events.sequences([created.id])).get(created.id)
|
||||
|
||||
// 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(["renamed", "log.synced"])
|
||||
expect(items.map((item) => item.type)).toEqual(["session.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", "renamed"])
|
||||
expect(items.map((item) => item.type)).toEqual(["log.synced", "session.renamed"])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ describe("SessionV2 watermarks", () => {
|
|||
const events = yield* EventV2.Service
|
||||
const first = yield* session.create({ location })
|
||||
const second = yield* session.create({ location })
|
||||
yield* session.rename({ sessionID: first.id, title: "renamed" })
|
||||
yield* session.rename({ sessionID: first.id, title: "session.renamed" })
|
||||
|
||||
const page = yield* session.list()
|
||||
const sequences = yield* events.sequences([first.id, second.id])
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater
|
|||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import {
|
||||
SessionContextCheckpointTable,
|
||||
SessionInputTable,
|
||||
|
|
@ -257,15 +258,32 @@ describe("SessionProjector", () => {
|
|||
})
|
||||
yield* events.publish(SessionEvent.Shell.Started, {
|
||||
sessionID,
|
||||
callID: "shell-1",
|
||||
command: "pwd",
|
||||
shell: Shell.Info.make({
|
||||
id: Shell.ID.make("sh_projector"),
|
||||
status: "running",
|
||||
command: "pwd",
|
||||
cwd: "/project",
|
||||
shell: "/bin/sh",
|
||||
file: "/tmp/sh_projector.out",
|
||||
metadata: {},
|
||||
time: { started: 0 },
|
||||
}),
|
||||
})
|
||||
yield* events.publish(SessionEvent.Shell.Ended, {
|
||||
sessionID,
|
||||
callID: "shell-1",
|
||||
output: "/project",
|
||||
shell: Shell.Info.make({
|
||||
id: Shell.ID.make("sh_projector"),
|
||||
status: "exited",
|
||||
command: "pwd",
|
||||
cwd: "/project",
|
||||
shell: "/bin/sh",
|
||||
file: "/tmp/sh_projector.out",
|
||||
exit: 0,
|
||||
metadata: {},
|
||||
time: { started: 0, completed: 1 },
|
||||
}),
|
||||
output: { output: "/project", cursor: 8, size: 8, truncated: false },
|
||||
})
|
||||
const compactionID = SessionMessage.ID.create()
|
||||
yield* events.publish(SessionEvent.Compaction.Started, {
|
||||
sessionID,
|
||||
reason: "manual",
|
||||
|
|
@ -320,7 +338,8 @@ describe("SessionProjector", () => {
|
|||
metadata: { source: "projector-test" },
|
||||
})
|
||||
expect(messages.find((message) => message.type === "shell")).toMatchObject({
|
||||
output: "/project",
|
||||
shell: { command: "pwd", status: "exited", exit: 0 },
|
||||
output: { output: "/project", truncated: false },
|
||||
time: { completed: DateTime.makeUnsafe(0) },
|
||||
})
|
||||
expect(messages.find((message) => message.type === "compaction")).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -256,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, "prompt.admitted"],
|
||||
[1, "prompt.admitted"],
|
||||
[2, "prompt.promoted"],
|
||||
[3, "prompt.promoted"],
|
||||
[0, "session.prompt.admitted"],
|
||||
[1, "session.prompt.admitted"],
|
||||
[2, "session.prompt.promoted"],
|
||||
[3, "session.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, "prompt.admitted"]])
|
||||
).toEqual([[1, "session.prompt.admitted"]])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
|
|||
import { AgentAttachment, FileAttachment } from "@opencode-ai/core/session/prompt"
|
||||
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import { DateTime } from "effect"
|
||||
|
||||
const created = DateTime.makeUnsafe(0)
|
||||
|
|
@ -87,9 +88,18 @@ describe("toLLMMessages", () => {
|
|||
SessionMessage.Shell.make({
|
||||
id: id("shell"),
|
||||
type: "shell",
|
||||
callID: "shell-1",
|
||||
command: "pwd",
|
||||
output: "/project",
|
||||
shell: Shell.Info.make({
|
||||
id: Shell.ID.make("sh_test"),
|
||||
status: "exited",
|
||||
command: "pwd",
|
||||
cwd: "/project",
|
||||
shell: "/bin/sh",
|
||||
file: "/tmp/sh_test.out",
|
||||
exit: 0,
|
||||
metadata: {},
|
||||
time: { started: 0, completed: 0 },
|
||||
}),
|
||||
output: { output: "/project", cursor: 8, size: 8, truncated: false },
|
||||
time: { created, completed: created },
|
||||
}),
|
||||
SessionMessage.Compaction.make({
|
||||
|
|
|
|||
|
|
@ -193,12 +193,12 @@ describe("SessionRunnerLLM recorded", () => {
|
|||
.orderBy(EventTable.seq)
|
||||
.all()).map((event) => event.type),
|
||||
).toEqual([
|
||||
"prompt.admitted.1",
|
||||
"prompt.promoted.1",
|
||||
"step.started.1",
|
||||
"text.started.1",
|
||||
"text.ended.1",
|
||||
"step.ended.1",
|
||||
"session.prompt.admitted.1",
|
||||
"session.prompt.promoted.1",
|
||||
"session.step.started.1",
|
||||
"session.text.started.1",
|
||||
"session.text.ended.1",
|
||||
"session.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 === "tool.success.1")
|
||||
const success = published.find((event) => event.type === "session.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 === "tool.success.1")
|
||||
const success = published.find((event) => event.type === "session.tool.success.1")
|
||||
expect(success?.data).toHaveProperty("result")
|
||||
})
|
||||
|
||||
|
|
@ -110,8 +110,8 @@ test("binary failure emits no success event", async () => {
|
|||
}),
|
||||
),
|
||||
)
|
||||
expect(published.some((event) => event.type === "tool.success.1")).toBe(false)
|
||||
expect(published.some((event) => event.type === "tool.failed.1")).toBe(true)
|
||||
expect(published.some((event) => event.type === "session.tool.success.1")).toBe(false)
|
||||
expect(published.some((event) => event.type === "session.tool.failed.1")).toBe(true)
|
||||
})
|
||||
|
||||
test("old success event data containing result still decodes", () => {
|
||||
|
|
|
|||
|
|
@ -2985,7 +2985,7 @@ describe("SessionRunnerLLM", () => {
|
|||
{ type: "user", text: "Interrupt provider" },
|
||||
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Step interrupted" } },
|
||||
])
|
||||
expect(yield* recordedEventTypes(sessionID)).toContain("step.failed.1")
|
||||
expect(yield* recordedEventTypes(sessionID)).toContain("session.step.failed.1")
|
||||
yield* session.interrupt(sessionID)
|
||||
}),
|
||||
)
|
||||
|
|
@ -3029,8 +3029,8 @@ describe("SessionRunnerLLM", () => {
|
|||
},
|
||||
])
|
||||
const eventTypes = yield* recordedEventTypes(sessionID)
|
||||
expect(eventTypes).toContain("step.failed.1")
|
||||
expect(eventTypes).not.toContain("step.ended.1")
|
||||
expect(eventTypes).toContain("session.step.failed.1")
|
||||
expect(eventTypes).not.toContain("session.step.ended.1")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue