chore(tui): merge v2 into form branch
This commit is contained in:
commit
be0cd93c8f
235 changed files with 5364 additions and 3885 deletions
|
|
@ -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)`,
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ const locationLayer = Layer.succeed(
|
|||
location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }),
|
||||
),
|
||||
)
|
||||
const Message = EventV2.define({
|
||||
const Message = EventV2.ephemeral({
|
||||
type: "test.message",
|
||||
schema: {
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
const SyncMessage = EventV2.define({
|
||||
const SyncMessage = EventV2.durable({
|
||||
type: "test.sync",
|
||||
durable: {
|
||||
version: 1,
|
||||
|
|
@ -41,7 +41,7 @@ const SyncMessage = EventV2.define({
|
|||
},
|
||||
})
|
||||
|
||||
const SyncSent = EventV2.define({
|
||||
const SyncSent = EventV2.durable({
|
||||
type: "test.sent",
|
||||
durable: {
|
||||
version: 1,
|
||||
|
|
@ -53,14 +53,14 @@ const SyncSent = EventV2.define({
|
|||
},
|
||||
})
|
||||
|
||||
const GlobalMessage = EventV2.define({
|
||||
const GlobalMessage = EventV2.ephemeral({
|
||||
type: "test.global",
|
||||
schema: {
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
const VersionedMessage = EventV2.define({
|
||||
const VersionedMessage = EventV2.durable({
|
||||
type: "test.versioned",
|
||||
durable: {
|
||||
version: 2,
|
||||
|
|
@ -129,12 +129,12 @@ describe("EventV2", () => {
|
|||
|
||||
it.effect("selects the latest durable definition independent of declaration order", () =>
|
||||
Effect.sync(() => {
|
||||
const latest = EventV2.define({
|
||||
const latest = EventV2.durable({
|
||||
type: "test.out-of-order",
|
||||
durable: { version: 2, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const historical = EventV2.define({
|
||||
const historical = EventV2.durable({
|
||||
type: "test.out-of-order",
|
||||
durable: { version: 1, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,79 @@ describe("Form", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("requires every when condition to match and treats empty when as active", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Form.Service
|
||||
const created = yield* service.create({
|
||||
sessionID: "global",
|
||||
mode: "form",
|
||||
fields: [
|
||||
{ key: "a", type: "boolean" },
|
||||
{ key: "b", type: "boolean" },
|
||||
{
|
||||
key: "x",
|
||||
type: "string",
|
||||
required: true,
|
||||
when: [
|
||||
{ key: "a", op: "eq", value: true },
|
||||
{ key: "b", op: "eq", value: true },
|
||||
],
|
||||
},
|
||||
{ key: "z", type: "string", required: true, when: [] },
|
||||
],
|
||||
})
|
||||
|
||||
const missingX = yield* service.reply({ id: created.id, answer: { a: true, b: true, z: "ok" } }).pipe(Effect.flip)
|
||||
expect(missingX).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: x" }))
|
||||
|
||||
const inactiveX = yield* service
|
||||
.reply({ id: created.id, answer: { a: true, b: false, x: "nope", z: "ok" } })
|
||||
.pipe(Effect.flip)
|
||||
expect(inactiveX).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Form field is not active: x" }))
|
||||
|
||||
const missingZ = yield* service.reply({ id: created.id, answer: { a: true, b: false } }).pipe(Effect.flip)
|
||||
expect(missingZ).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: z" }))
|
||||
|
||||
yield* service.reply({ id: created.id, answer: { a: true, b: false, z: "ok" } })
|
||||
expect(yield* service.state(created.id)).toEqual({ status: "answered", answer: { a: true, b: false, z: "ok" } })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("evaluates neq against multiselect answers as non-inclusion", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Form.Service
|
||||
const options = [
|
||||
{ value: "go", label: "Go" },
|
||||
{ value: "ts", label: "TypeScript" },
|
||||
]
|
||||
const created = yield* service.create({
|
||||
sessionID: "global",
|
||||
mode: "form",
|
||||
fields: [
|
||||
{ key: "langs", type: "multiselect", options },
|
||||
{ key: "note", type: "string", required: true, when: [{ key: "langs", op: "neq", value: "go" }] },
|
||||
],
|
||||
})
|
||||
|
||||
const missing = yield* service.reply({ id: created.id, answer: { langs: ["ts"] } }).pipe(Effect.flip)
|
||||
expect(missing).toEqual(
|
||||
new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: note" }),
|
||||
)
|
||||
|
||||
// an answered-but-empty multiselect also satisfies neq
|
||||
const missingEmpty = yield* service.reply({ id: created.id, answer: { langs: [] } }).pipe(Effect.flip)
|
||||
expect(missingEmpty).toEqual(
|
||||
new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: note" }),
|
||||
)
|
||||
|
||||
const inactive = yield* service.reply({ id: created.id, answer: { langs: ["go"], note: "x" } }).pipe(Effect.flip)
|
||||
expect(inactive).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Form field is not active: note" }))
|
||||
|
||||
yield* service.reply({ id: created.id, answer: { langs: ["go"] } })
|
||||
expect(yield* service.state(created.id)).toEqual({ status: "answered", answer: { langs: ["go"] } })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("treats unanswered when references as false and cascades inactivity", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Form.Service
|
||||
|
|
|
|||
14
packages/core/test/mcp.test.ts
Normal file
14
packages/core/test/mcp.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { MCP } from "@opencode-ai/core/mcp/index"
|
||||
import { MCPClient } from "@opencode-ai/core/mcp/client"
|
||||
|
||||
describe("MCP errors", () => {
|
||||
test("expose useful messages", () => {
|
||||
expect(new MCP.NotFoundError({ server: MCP.ServerName.make("demo") }).message).toBe("MCP server not found: demo")
|
||||
expect(new MCP.ToolCallError({ server: MCP.ServerName.make("demo"), tool: "search", message: "failed" }).message).toBe(
|
||||
"failed",
|
||||
)
|
||||
expect(new MCPClient.NeedsAuthError({ server: "demo" }).message).toBe("MCP server requires authentication: demo")
|
||||
expect(new MCPClient.ConnectError({ server: "demo", message: "offline" }).message).toBe("offline")
|
||||
})
|
||||
})
|
||||
|
|
@ -149,6 +149,22 @@ describe("ModelsDev Service", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("allows models.dev entries without temperature metadata", () =>
|
||||
Effect.sync(() => {
|
||||
const result = Schema.decodeUnknownSync(ModelsDev.Model)({
|
||||
id: "no-temperature-model",
|
||||
name: "No Temperature Model",
|
||||
release_date: "2026-01-01",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: true,
|
||||
limit: { context: 128000, output: 8192 },
|
||||
})
|
||||
|
||||
expect(result.temperature).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("get() returns providers from disk when cache file exists", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* writeCache(fixture)
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
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 { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
|
|
@ -62,6 +62,13 @@ const assertCreateInputTypes = (session: SessionV2.Interface) => {
|
|||
}
|
||||
void assertCreateInputTypes
|
||||
|
||||
function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
||||
}
|
||||
|
||||
describe("SessionV2.create", () => {
|
||||
it.effect("creates a fresh projected session when the ID is omitted", () =>
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -190,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",
|
||||
})
|
||||
|
||||
|
|
@ -208,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 },
|
||||
})
|
||||
|
|
@ -260,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 } })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -373,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" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
|
@ -399,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,
|
||||
|
|
@ -459,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)))
|
||||
}),
|
||||
|
|
@ -476,20 +482,41 @@ describe("SessionV2.create", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("reports unfinished Session operations as unavailable", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const created = yield* session.create({ location })
|
||||
const unavailable = (
|
||||
effect: Effect.Effect<void, SessionV2.NotFoundError | SessionV2.OperationUnavailableError>,
|
||||
) =>
|
||||
effect.pipe(
|
||||
Effect.flip,
|
||||
Effect.map((error) => (error instanceof SessionV2.OperationUnavailableError ? error.operation : "not-found")),
|
||||
)
|
||||
it.live("runs a shell command and projects the started/ended shell message", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const created = yield* session.create({
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
|
||||
expect(yield* unavailable(session.shell({ sessionID: created.id, command: "pwd" }))).toBe("shell")
|
||||
}),
|
||||
yield* session.shell({ sessionID: created.id, command: "echo hello" })
|
||||
|
||||
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?.time.completed).toBeDefined()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("still emits shell ended for a failing command", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const created = yield* session.create({
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
|
||||
yield* session.shell({ sessionID: created.id, command: "false" })
|
||||
|
||||
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?.time.completed).toBeDefined()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("switches the selected agent through the durable Session event", () =>
|
||||
|
|
@ -502,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" } }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -535,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"])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ describe("SessionV2.log", () => {
|
|||
|
||||
it.effect("reads across undecodable gaps in aggregate order and marks the true log position", () =>
|
||||
Effect.gen(function* () {
|
||||
const GapEvent = EventV2.define({
|
||||
const GapEvent = EventV2.durable({
|
||||
type: "test.session.log.gap",
|
||||
durable: { aggregate: "sessionID", version: 1 },
|
||||
schema: { sessionID: SessionV2.ID, value: Schema.String },
|
||||
|
|
|
|||
|
|
@ -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") },
|
||||
|
|
@ -354,7 +354,7 @@ Recent work
|
|||
state: SessionMessage.ToolStateError.make({
|
||||
status: "error",
|
||||
input: { query: "Effect" },
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
error: { type: "unknown", message: "Step interrupted" },
|
||||
content: [],
|
||||
structured: {},
|
||||
}),
|
||||
|
|
@ -362,7 +362,7 @@ Recent work
|
|||
}),
|
||||
],
|
||||
finish: "error",
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
error: { type: "unknown", message: "Step interrupted" },
|
||||
time: { created, completed: created },
|
||||
}),
|
||||
],
|
||||
|
|
@ -386,7 +386,7 @@ Recent work
|
|||
result: {
|
||||
type: "error",
|
||||
value: {
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
error: { type: "unknown", message: "Step interrupted" },
|
||||
content: [],
|
||||
structured: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const execution = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const sessionRunner = yield* SessionRunner.Service
|
||||
const coordinator = yield* SessionRunCoordinator.make<SessionV2.ID, SessionRunner.RunError>({
|
||||
drain: (sessionID, force) => sessionRunner.run({ sessionID, force }),
|
||||
drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }),
|
||||
})
|
||||
return SessionExecution.Service.of({
|
||||
active: coordinator.active,
|
||||
|
|
@ -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" })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ const execution = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const sessionRunner = yield* SessionRunner.Service
|
||||
const coordinator = yield* SessionRunCoordinator.make<SessionV2.ID, SessionRunner.RunError>({
|
||||
drain: (sessionID, force) => sessionRunner.run({ sessionID, force }),
|
||||
drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }),
|
||||
})
|
||||
return SessionExecution.Service.of({
|
||||
active: coordinator.active,
|
||||
|
|
@ -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,
|
||||
|
|
@ -574,7 +575,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
|||
)
|
||||
|
||||
const runner = yield* SessionRunner.Service
|
||||
const fiber = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild)
|
||||
const fiber = yield* runner.drain({ sessionID, force: true }).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(streamed)
|
||||
yield* Fiber.interrupt(fiber)
|
||||
expect(yield* session.context(sessionID)).toMatchObject([
|
||||
|
|
@ -582,7 +583,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
|||
{
|
||||
type: "assistant",
|
||||
finish: "error",
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
error: { type: "unknown", message: "Step interrupted" },
|
||||
content: [
|
||||
kind === "tool input"
|
||||
? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } }
|
||||
|
|
@ -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,
|
||||
|
|
@ -3011,9 +2983,9 @@ describe("SessionRunnerLLM", () => {
|
|||
expect(requests).toHaveLength(1)
|
||||
expect(yield* session.context(sessionID)).toMatchObject([
|
||||
{ type: "user", text: "Interrupt provider" },
|
||||
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Provider turn interrupted" } },
|
||||
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Step interrupted" } },
|
||||
])
|
||||
expect(yield* recordedEventTypes(sessionID)).toContain("session.next.step.failed.2")
|
||||
expect(yield* recordedEventTypes(sessionID)).toContain("step.failed.1")
|
||||
yield* session.interrupt(sessionID)
|
||||
}),
|
||||
)
|
||||
|
|
@ -3035,7 +3007,7 @@ describe("SessionRunnerLLM", () => {
|
|||
]
|
||||
|
||||
const runner = yield* SessionRunner.Service
|
||||
const run = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild)
|
||||
const run = yield* runner.drain({ sessionID, force: true }).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(toolExecutionsStarted)
|
||||
yield* Fiber.interrupt(run)
|
||||
toolExecutionGate = undefined
|
||||
|
|
@ -3046,7 +3018,7 @@ describe("SessionRunnerLLM", () => {
|
|||
{
|
||||
type: "assistant",
|
||||
finish: "error",
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
error: { type: "unknown", message: "Step interrupted" },
|
||||
content: [
|
||||
{
|
||||
type: "tool",
|
||||
|
|
@ -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")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -3424,9 +3396,10 @@ describe("SessionRunnerLLM", () => {
|
|||
streamStarted = undefined
|
||||
response = [LLMEvent.textStart({ id: "text-1" }), LLMEvent.textStart({ id: "text-1" })]
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(
|
||||
"Duplicate text start: text-1",
|
||||
)
|
||||
const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))
|
||||
expect(defect).toBeInstanceOf(Error)
|
||||
if (!(defect instanceof Error)) return
|
||||
expect(defect.message).toBe("Duplicate text start: text-1")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -3468,9 +3441,10 @@ describe("SessionRunnerLLM", () => {
|
|||
streamStarted = undefined
|
||||
response = [LLMEvent.toolInputDelta({ id: "call-1", name: "read", text: "{}" })]
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(
|
||||
"Tool input delta before start: call-1",
|
||||
)
|
||||
const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))
|
||||
expect(defect).toBeInstanceOf(Error)
|
||||
if (!(defect instanceof Error)) return
|
||||
expect(defect.message).toBe("Tool input delta before start: call-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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue