refactor(session): remove async facade exports (#22471)
This commit is contained in:
parent
4f967d5bc0
commit
68384613be
24 changed files with 1163 additions and 971 deletions
|
|
@ -3,6 +3,7 @@ import { APICallError } from "ai"
|
|||
import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
|
|
@ -14,7 +15,7 @@ import { Log } from "../../src/util/log"
|
|||
import { Permission } from "../../src/permission"
|
||||
import { Plugin } from "../../src/plugin"
|
||||
import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
|
||||
import { Session } from "../../src/session"
|
||||
import { Session as SessionNs } from "../../src/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
|
|
@ -29,6 +30,26 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
|||
|
||||
Log.init({ print: false })
|
||||
|
||||
function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
|
||||
return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
|
||||
}
|
||||
|
||||
const svc = {
|
||||
...SessionNs,
|
||||
create(input?: SessionNs.CreateInput) {
|
||||
return run(SessionNs.Service.use((svc) => svc.create(input)))
|
||||
},
|
||||
messages(input: z.output<typeof SessionNs.MessagesInput>) {
|
||||
return run(SessionNs.Service.use((svc) => svc.messages(input)))
|
||||
},
|
||||
updateMessage<T extends MessageV2.Info>(msg: T) {
|
||||
return run(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
|
||||
},
|
||||
updatePart<T extends MessageV2.Part>(part: T) {
|
||||
return run(SessionNs.Service.use((svc) => svc.updatePart(part)))
|
||||
},
|
||||
}
|
||||
|
||||
const summary = Layer.succeed(
|
||||
SessionSummary.Service,
|
||||
SessionSummary.Service.of({
|
||||
|
|
@ -80,7 +101,7 @@ function createModel(opts: {
|
|||
const wide = () => ProviderTest.fake({ model: createModel({ context: 100_000, output: 32_000 }) })
|
||||
|
||||
async function user(sessionID: SessionID, text: string) {
|
||||
const msg = await Session.updateMessage({
|
||||
const msg = await svc.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID,
|
||||
|
|
@ -88,7 +109,7 @@ async function user(sessionID: SessionID, text: string) {
|
|||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: msg.id,
|
||||
sessionID,
|
||||
|
|
@ -119,12 +140,12 @@ async function assistant(sessionID: SessionID, parentID: MessageID, root: string
|
|||
time: { created: Date.now() },
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(msg)
|
||||
await svc.updateMessage(msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
async function tool(sessionID: SessionID, messageID: MessageID, tool: string, output: string) {
|
||||
return Session.updatePart({
|
||||
return svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID,
|
||||
sessionID,
|
||||
|
|
@ -171,7 +192,7 @@ function runtime(result: "continue" | "compact", plugin = Plugin.defaultLayer, p
|
|||
return ManagedRuntime.make(
|
||||
Layer.mergeAll(SessionCompaction.layer, bus).pipe(
|
||||
Layer.provide(provider.layer),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(SessionNs.defaultLayer),
|
||||
Layer.provide(layer(result)),
|
||||
Layer.provide(Agent.defaultLayer),
|
||||
Layer.provide(plugin),
|
||||
|
|
@ -191,9 +212,9 @@ const deps = Layer.mergeAll(
|
|||
)
|
||||
|
||||
const env = Layer.mergeAll(
|
||||
Session.defaultLayer,
|
||||
SessionNs.defaultLayer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
SessionCompaction.layer.pipe(Layer.provide(Session.defaultLayer), Layer.provideMerge(deps)),
|
||||
SessionCompaction.layer.pipe(Layer.provide(SessionNs.defaultLayer), Layer.provideMerge(deps)),
|
||||
)
|
||||
|
||||
const it = testEffect(env)
|
||||
|
|
@ -227,7 +248,7 @@ function liveRuntime(layer: Layer.Layer<LLM.Service>, provider = ProviderTest.fa
|
|||
return ManagedRuntime.make(
|
||||
Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus, status).pipe(
|
||||
Layer.provide(provider.layer),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(SessionNs.defaultLayer),
|
||||
Layer.provide(Snapshot.defaultLayer),
|
||||
Layer.provide(layer),
|
||||
Layer.provide(Permission.defaultLayer),
|
||||
|
|
@ -467,9 +488,9 @@ describe("session.compaction.create", () => {
|
|||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const compact = yield* SessionCompaction.Service
|
||||
const session = yield* Session.Service
|
||||
const ssn = yield* SessionNs.Service
|
||||
|
||||
const info = yield* session.create({})
|
||||
const info = yield* ssn.create({})
|
||||
|
||||
yield* compact.create({
|
||||
sessionID: info.id,
|
||||
|
|
@ -479,7 +500,7 @@ describe("session.compaction.create", () => {
|
|||
overflow: true,
|
||||
})
|
||||
|
||||
const msgs = yield* session.messages({ sessionID: info.id })
|
||||
const msgs = yield* ssn.messages({ sessionID: info.id })
|
||||
expect(msgs).toHaveLength(1)
|
||||
expect(msgs[0].info.role).toBe("user")
|
||||
expect(msgs[0].parts).toHaveLength(1)
|
||||
|
|
@ -499,9 +520,9 @@ describe("session.compaction.prune", () => {
|
|||
provideTmpdirInstance((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const compact = yield* SessionCompaction.Service
|
||||
const session = yield* Session.Service
|
||||
const info = yield* session.create({})
|
||||
const a = yield* session.updateMessage({
|
||||
const ssn = yield* SessionNs.Service
|
||||
const info = yield* ssn.create({})
|
||||
const a = yield* ssn.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: info.id,
|
||||
|
|
@ -509,7 +530,7 @@ describe("session.compaction.prune", () => {
|
|||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* session.updatePart({
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: a.id,
|
||||
sessionID: info.id,
|
||||
|
|
@ -536,8 +557,8 @@ describe("session.compaction.prune", () => {
|
|||
time: { created: Date.now() },
|
||||
finish: "end_turn",
|
||||
}
|
||||
yield* session.updateMessage(b)
|
||||
yield* session.updatePart({
|
||||
yield* ssn.updateMessage(b)
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: b.id,
|
||||
sessionID: info.id,
|
||||
|
|
@ -554,7 +575,7 @@ describe("session.compaction.prune", () => {
|
|||
},
|
||||
})
|
||||
for (const text of ["second", "third"]) {
|
||||
const msg = yield* session.updateMessage({
|
||||
const msg = yield* ssn.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: info.id,
|
||||
|
|
@ -562,7 +583,7 @@ describe("session.compaction.prune", () => {
|
|||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* session.updatePart({
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: msg.id,
|
||||
sessionID: info.id,
|
||||
|
|
@ -573,7 +594,7 @@ describe("session.compaction.prune", () => {
|
|||
|
||||
yield* compact.prune({ sessionID: info.id })
|
||||
|
||||
const msgs = yield* session.messages({ sessionID: info.id })
|
||||
const msgs = yield* ssn.messages({ sessionID: info.id })
|
||||
const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool")
|
||||
expect(part?.type).toBe("tool")
|
||||
expect(part?.state.status).toBe("completed")
|
||||
|
|
@ -589,9 +610,9 @@ describe("session.compaction.prune", () => {
|
|||
provideTmpdirInstance((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const compact = yield* SessionCompaction.Service
|
||||
const session = yield* Session.Service
|
||||
const info = yield* session.create({})
|
||||
const a = yield* session.updateMessage({
|
||||
const ssn = yield* SessionNs.Service
|
||||
const info = yield* ssn.create({})
|
||||
const a = yield* ssn.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: info.id,
|
||||
|
|
@ -599,7 +620,7 @@ describe("session.compaction.prune", () => {
|
|||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* session.updatePart({
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: a.id,
|
||||
sessionID: info.id,
|
||||
|
|
@ -626,8 +647,8 @@ describe("session.compaction.prune", () => {
|
|||
time: { created: Date.now() },
|
||||
finish: "end_turn",
|
||||
}
|
||||
yield* session.updateMessage(b)
|
||||
yield* session.updatePart({
|
||||
yield* ssn.updateMessage(b)
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: b.id,
|
||||
sessionID: info.id,
|
||||
|
|
@ -644,7 +665,7 @@ describe("session.compaction.prune", () => {
|
|||
},
|
||||
})
|
||||
for (const text of ["second", "third"]) {
|
||||
const msg = yield* session.updateMessage({
|
||||
const msg = yield* ssn.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: info.id,
|
||||
|
|
@ -652,7 +673,7 @@ describe("session.compaction.prune", () => {
|
|||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* session.updatePart({
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: msg.id,
|
||||
sessionID: info.id,
|
||||
|
|
@ -663,7 +684,7 @@ describe("session.compaction.prune", () => {
|
|||
|
||||
yield* compact.prune({ sessionID: info.id })
|
||||
|
||||
const msgs = yield* session.messages({ sessionID: info.id })
|
||||
const msgs = yield* ssn.messages({ sessionID: info.id })
|
||||
const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool")
|
||||
expect(part?.type).toBe("tool")
|
||||
if (part?.type === "tool" && part.state.status === "completed") {
|
||||
|
|
@ -680,12 +701,12 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const reply = await assistant(session.id, msg.id, tmp.path)
|
||||
const rt = runtime("continue")
|
||||
try {
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
await expect(
|
||||
rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
|
|
@ -710,9 +731,9 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const done = defer()
|
||||
let seen = false
|
||||
const rt = runtime("continue", Plugin.defaultLayer, wide())
|
||||
|
|
@ -760,11 +781,11 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const rt = runtime("compact", Plugin.defaultLayer, wide())
|
||||
try {
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
|
|
@ -776,7 +797,7 @@ describe("session.compaction.process", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const summary = (await Session.messages({ sessionID: session.id })).find(
|
||||
const summary = (await svc.messages({ sessionID: session.id })).find(
|
||||
(msg) => msg.info.role === "assistant" && msg.info.summary,
|
||||
)
|
||||
|
||||
|
|
@ -798,11 +819,11 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const rt = runtime("continue", Plugin.defaultLayer, wide())
|
||||
try {
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
|
|
@ -814,7 +835,7 @@ describe("session.compaction.process", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const all = await Session.messages({ sessionID: session.id })
|
||||
const all = await svc.messages({ sessionID: session.id })
|
||||
const last = all.at(-1)
|
||||
|
||||
expect(result).toBe("continue")
|
||||
|
|
@ -838,11 +859,11 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const rt = runtime("continue", autocontinue(false), wide())
|
||||
try {
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
|
|
@ -854,7 +875,7 @@ describe("session.compaction.process", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const all = await Session.messages({ sessionID: session.id })
|
||||
const all = await svc.messages({ sessionID: session.id })
|
||||
const last = all.at(-1)
|
||||
|
||||
expect(result).toBe("continue")
|
||||
|
|
@ -881,10 +902,10 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
await user(session.id, "root")
|
||||
const replay = await user(session.id, "image")
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: replay.id,
|
||||
sessionID: session.id,
|
||||
|
|
@ -896,7 +917,7 @@ describe("session.compaction.process", () => {
|
|||
const msg = await user(session.id, "current")
|
||||
const rt = runtime("continue", Plugin.defaultLayer, wide())
|
||||
try {
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
|
|
@ -909,7 +930,7 @@ describe("session.compaction.process", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const last = (await Session.messages({ sessionID: session.id })).at(-1)
|
||||
const last = (await svc.messages({ sessionID: session.id })).at(-1)
|
||||
|
||||
expect(result).toBe("continue")
|
||||
expect(last?.info.role).toBe("user")
|
||||
|
|
@ -929,13 +950,13 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
await user(session.id, "earlier")
|
||||
const msg = await user(session.id, "current")
|
||||
|
||||
const rt = runtime("continue", Plugin.defaultLayer, wide())
|
||||
try {
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const result = await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
|
|
@ -948,7 +969,7 @@ describe("session.compaction.process", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const last = (await Session.messages({ sessionID: session.id })).at(-1)
|
||||
const last = (await svc.messages({ sessionID: session.id })).at(-1)
|
||||
|
||||
expect(result).toBe("continue")
|
||||
expect(last?.info.role).toBe("user")
|
||||
|
|
@ -989,9 +1010,9 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const abort = new AbortController()
|
||||
const rt = liveRuntime(stub.layer, wide())
|
||||
let off: (() => void) | undefined
|
||||
|
|
@ -1063,9 +1084,9 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
const abort = new AbortController()
|
||||
const rt = runtime("continue", plugin(ready), wide())
|
||||
let run: Promise<"continue" | "stop"> | undefined
|
||||
|
|
@ -1100,7 +1121,7 @@ describe("session.compaction.process", () => {
|
|||
abort.abort()
|
||||
expect(await run).toBe("stop")
|
||||
|
||||
const all = await Session.messages({ sessionID: session.id })
|
||||
const all = await svc.messages({ sessionID: session.id })
|
||||
expect(all.some((msg) => msg.info.role === "assistant" && msg.info.summary)).toBe(false)
|
||||
} finally {
|
||||
abort.abort()
|
||||
|
|
@ -1165,11 +1186,11 @@ describe("session.compaction.process", () => {
|
|||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const msg = await user(session.id, "hello")
|
||||
const rt = liveRuntime(stub.layer, wide())
|
||||
try {
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await svc.messages({ sessionID: session.id })
|
||||
await rt.runPromise(
|
||||
SessionCompaction.Service.use((svc) =>
|
||||
svc.process({
|
||||
|
|
@ -1181,7 +1202,7 @@ describe("session.compaction.process", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const summary = (await Session.messages({ sessionID: session.id })).find(
|
||||
const summary = (await svc.messages({ sessionID: session.id })).find(
|
||||
(item) => item.info.role === "assistant" && item.info.summary,
|
||||
)
|
||||
|
||||
|
|
@ -1211,10 +1232,10 @@ describe("util.token.estimate", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("session.getUsage", () => {
|
||||
describe("SessionNs.getUsage", () => {
|
||||
test("normalizes standard usage to token format", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
|
|
@ -1241,7 +1262,7 @@ describe("session.getUsage", () => {
|
|||
|
||||
test("extracts cached tokens to cache.read", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
|
|
@ -1265,7 +1286,7 @@ describe("session.getUsage", () => {
|
|||
|
||||
test("handles anthropic cache write metadata", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
|
|
@ -1294,7 +1315,7 @@ describe("session.getUsage", () => {
|
|||
test("subtracts cached tokens for anthropic provider", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
// AI SDK v6 normalizes inputTokens to include cached tokens for all providers
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
|
|
@ -1321,7 +1342,7 @@ describe("session.getUsage", () => {
|
|||
|
||||
test("separates reasoning tokens from output tokens", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
|
|
@ -1355,7 +1376,7 @@ describe("session.getUsage", () => {
|
|||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
})
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
|
|
@ -1380,7 +1401,7 @@ describe("session.getUsage", () => {
|
|||
|
||||
test("handles undefined optional values gracefully", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
|
|
@ -1416,7 +1437,7 @@ describe("session.getUsage", () => {
|
|||
cache: { read: 0.3, write: 3.75 },
|
||||
},
|
||||
})
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1_000_000,
|
||||
|
|
@ -1457,7 +1478,7 @@ describe("session.getUsage", () => {
|
|||
},
|
||||
}
|
||||
if (npm === "@ai-sdk/amazon-bedrock") {
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage,
|
||||
metadata: {
|
||||
|
|
@ -1478,7 +1499,7 @@ describe("session.getUsage", () => {
|
|||
return
|
||||
}
|
||||
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage,
|
||||
metadata: {
|
||||
|
|
@ -1499,7 +1520,7 @@ describe("session.getUsage", () => {
|
|||
|
||||
test("extracts cache write tokens from vertex metadata key", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000, npm: "@ai-sdk/google-vertex/anthropic" })
|
||||
const result = Session.getUsage({
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import path from "path"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Session } from "../../src/session"
|
||||
import { Session as SessionNs } from "../../src/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
|
|
@ -10,12 +11,32 @@ import { Log } from "../../src/util/log"
|
|||
const root = path.join(__dirname, "../..")
|
||||
Log.init({ print: false })
|
||||
|
||||
function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
|
||||
return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
|
||||
}
|
||||
|
||||
const svc = {
|
||||
...SessionNs,
|
||||
create(input?: SessionNs.CreateInput) {
|
||||
return run(SessionNs.Service.use((svc) => svc.create(input)))
|
||||
},
|
||||
remove(id: SessionID) {
|
||||
return run(SessionNs.Service.use((svc) => svc.remove(id)))
|
||||
},
|
||||
updateMessage<T extends MessageV2.Info>(msg: T) {
|
||||
return run(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
|
||||
},
|
||||
updatePart<T extends MessageV2.Part>(part: T) {
|
||||
return run(SessionNs.Service.use((svc) => svc.updatePart(part)))
|
||||
},
|
||||
}
|
||||
|
||||
async function fill(sessionID: SessionID, count: number, time = (i: number) => Date.now() + i) {
|
||||
const ids = [] as MessageID[]
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = MessageID.ascending()
|
||||
ids.push(id)
|
||||
await Session.updateMessage({
|
||||
await svc.updateMessage({
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
|
|
@ -25,7 +46,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D
|
|||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
messageID: id,
|
||||
|
|
@ -38,7 +59,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D
|
|||
|
||||
async function addUser(sessionID: SessionID, text?: string) {
|
||||
const id = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
await svc.updateMessage({
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
|
|
@ -49,7 +70,7 @@ async function addUser(sessionID: SessionID, text?: string) {
|
|||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
if (text) {
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
messageID: id,
|
||||
|
|
@ -66,7 +87,7 @@ async function addAssistant(
|
|||
opts?: { summary?: boolean; finish?: string; error?: MessageV2.Assistant["error"] },
|
||||
) {
|
||||
const id = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
await svc.updateMessage({
|
||||
id,
|
||||
sessionID,
|
||||
role: "assistant",
|
||||
|
|
@ -87,7 +108,7 @@ async function addAssistant(
|
|||
}
|
||||
|
||||
async function addCompactionPart(sessionID: SessionID, messageID: MessageID) {
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
messageID,
|
||||
|
|
@ -101,14 +122,14 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
await fill(session.id, 2)
|
||||
|
||||
const result = MessageV2.page({ sessionID: session.id, limit: 10 })
|
||||
expect(result).toBeDefined()
|
||||
expect(result.items).toBeArray()
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -117,7 +138,7 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 6)
|
||||
|
||||
const a = MessageV2.page({ sessionID: session.id, limit: 2 })
|
||||
|
|
@ -136,7 +157,7 @@ describe("MessageV2.page", () => {
|
|||
expect(c.more).toBe(false)
|
||||
expect(c.cursor).toBeUndefined()
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -145,13 +166,13 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 4)
|
||||
|
||||
const result = MessageV2.page({ sessionID: session.id, limit: 4 })
|
||||
expect(result.items.map((item) => item.info.id)).toEqual(ids)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -160,14 +181,14 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
|
||||
const result = MessageV2.page({ sessionID: session.id, limit: 10 })
|
||||
expect(result.items).toEqual([])
|
||||
expect(result.more).toBe(false)
|
||||
expect(result.cursor).toBeUndefined()
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -186,7 +207,7 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 3)
|
||||
|
||||
const result = MessageV2.page({ sessionID: session.id, limit: 3 })
|
||||
|
|
@ -194,7 +215,7 @@ describe("MessageV2.page", () => {
|
|||
expect(result.more).toBe(false)
|
||||
expect(result.cursor).toBeUndefined()
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -203,7 +224,7 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 5)
|
||||
|
||||
const result = MessageV2.page({ sessionID: session.id, limit: 1 })
|
||||
|
|
@ -211,7 +232,7 @@ describe("MessageV2.page", () => {
|
|||
expect(result.items[0].info.id).toBe(ids[ids.length - 1])
|
||||
expect(result.more).toBe(true)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -220,10 +241,10 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const [id] = await fill(session.id, 1)
|
||||
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: session.id,
|
||||
messageID: id,
|
||||
|
|
@ -235,7 +256,7 @@ describe("MessageV2.page", () => {
|
|||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0].parts).toHaveLength(2)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -244,7 +265,7 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 4, (i) => 1000.5 + i)
|
||||
|
||||
const a = MessageV2.page({ sessionID: session.id, limit: 2 })
|
||||
|
|
@ -253,7 +274,7 @@ describe("MessageV2.page", () => {
|
|||
expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2))
|
||||
expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2))
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -262,7 +283,7 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 4, () => 1000)
|
||||
|
||||
const a = MessageV2.page({ sessionID: session.id, limit: 2 })
|
||||
|
|
@ -273,7 +294,7 @@ describe("MessageV2.page", () => {
|
|||
expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2))
|
||||
expect(b.more).toBe(false)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -282,8 +303,8 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const a = await Session.create({})
|
||||
const b = await Session.create({})
|
||||
const a = await svc.create({})
|
||||
const b = await svc.create({})
|
||||
await fill(a.id, 3)
|
||||
await fill(b.id, 2)
|
||||
|
||||
|
|
@ -294,8 +315,8 @@ describe("MessageV2.page", () => {
|
|||
expect(resultA.items.every((item) => item.info.sessionID === a.id)).toBe(true)
|
||||
expect(resultB.items.every((item) => item.info.sessionID === b.id)).toBe(true)
|
||||
|
||||
await Session.remove(a.id)
|
||||
await Session.remove(b.id)
|
||||
await svc.remove(a.id)
|
||||
await svc.remove(b.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -304,7 +325,7 @@ describe("MessageV2.page", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 10)
|
||||
|
||||
const result = MessageV2.page({ sessionID: session.id, limit: 100 })
|
||||
|
|
@ -313,7 +334,7 @@ describe("MessageV2.page", () => {
|
|||
expect(result.more).toBe(false)
|
||||
expect(result.cursor).toBeUndefined()
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -324,13 +345,13 @@ describe("MessageV2.stream", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 5)
|
||||
|
||||
const items = Array.from(MessageV2.stream(session.id))
|
||||
expect(items.map((item) => item.info.id)).toEqual(ids.slice().reverse())
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -339,12 +360,12 @@ describe("MessageV2.stream", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
|
||||
const items = Array.from(MessageV2.stream(session.id))
|
||||
expect(items).toHaveLength(0)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -353,14 +374,14 @@ describe("MessageV2.stream", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 1)
|
||||
|
||||
const items = Array.from(MessageV2.stream(session.id))
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].info.id).toBe(ids[0])
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -369,7 +390,7 @@ describe("MessageV2.stream", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
await fill(session.id, 3)
|
||||
|
||||
const items = Array.from(MessageV2.stream(session.id))
|
||||
|
|
@ -378,7 +399,7 @@ describe("MessageV2.stream", () => {
|
|||
expect(item.parts[0].type).toBe("text")
|
||||
}
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -387,7 +408,7 @@ describe("MessageV2.stream", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 60)
|
||||
|
||||
const items = Array.from(MessageV2.stream(session.id))
|
||||
|
|
@ -395,7 +416,7 @@ describe("MessageV2.stream", () => {
|
|||
expect(items[0].info.id).toBe(ids[ids.length - 1])
|
||||
expect(items[59].info.id).toBe(ids[0])
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -404,7 +425,7 @@ describe("MessageV2.stream", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
await fill(session.id, 1)
|
||||
|
||||
const gen = MessageV2.stream(session.id)
|
||||
|
|
@ -414,7 +435,7 @@ describe("MessageV2.stream", () => {
|
|||
expect(first).toHaveProperty("done")
|
||||
expect(first.done).toBe(false)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -425,7 +446,7 @@ describe("MessageV2.parts", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const [id] = await fill(session.id, 1)
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
|
|
@ -433,7 +454,7 @@ describe("MessageV2.parts", () => {
|
|||
expect(result[0].type).toBe("text")
|
||||
expect((result[0] as MessageV2.TextPart).text).toBe("m0")
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -442,13 +463,13 @@ describe("MessageV2.parts", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const id = await addUser(session.id)
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
expect(result).toEqual([])
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -457,17 +478,17 @@ describe("MessageV2.parts", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const [id] = await fill(session.id, 1)
|
||||
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: session.id,
|
||||
messageID: id,
|
||||
type: "text",
|
||||
text: "second",
|
||||
})
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: session.id,
|
||||
messageID: id,
|
||||
|
|
@ -481,7 +502,7 @@ describe("MessageV2.parts", () => {
|
|||
expect((result[1] as MessageV2.TextPart).text).toBe("second")
|
||||
expect((result[2] as MessageV2.TextPart).text).toBe("third")
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -490,7 +511,7 @@ describe("MessageV2.parts", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
await Session.create({})
|
||||
await svc.create({})
|
||||
const result = MessageV2.parts(MessageID.ascending())
|
||||
expect(result).toEqual([])
|
||||
},
|
||||
|
|
@ -501,14 +522,14 @@ describe("MessageV2.parts", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const [id] = await fill(session.id, 1)
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
expect(result[0].sessionID).toBe(session.id)
|
||||
expect(result[0].messageID).toBe(id)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -519,7 +540,7 @@ describe("MessageV2.get", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const [id] = await fill(session.id, 1)
|
||||
|
||||
const result = MessageV2.get({ sessionID: session.id, messageID: id })
|
||||
|
|
@ -529,7 +550,7 @@ describe("MessageV2.get", () => {
|
|||
expect(result.parts).toHaveLength(1)
|
||||
expect((result.parts[0] as MessageV2.TextPart).text).toBe("m0")
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -538,13 +559,13 @@ describe("MessageV2.get", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
|
||||
expect(() => MessageV2.get({ sessionID: session.id, messageID: MessageID.ascending() })).toThrow(
|
||||
"NotFoundError",
|
||||
)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -553,16 +574,16 @@ describe("MessageV2.get", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const a = await Session.create({})
|
||||
const b = await Session.create({})
|
||||
const a = await svc.create({})
|
||||
const b = await svc.create({})
|
||||
const [id] = await fill(a.id, 1)
|
||||
|
||||
expect(() => MessageV2.get({ sessionID: b.id, messageID: id })).toThrow("NotFoundError")
|
||||
const result = MessageV2.get({ sessionID: a.id, messageID: id })
|
||||
expect(result.info.id).toBe(id)
|
||||
|
||||
await Session.remove(a.id)
|
||||
await Session.remove(b.id)
|
||||
await svc.remove(a.id)
|
||||
await svc.remove(b.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -571,10 +592,10 @@ describe("MessageV2.get", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const [id] = await fill(session.id, 1)
|
||||
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: session.id,
|
||||
messageID: id,
|
||||
|
|
@ -585,7 +606,7 @@ describe("MessageV2.get", () => {
|
|||
const result = MessageV2.get({ sessionID: session.id, messageID: id })
|
||||
expect(result.parts).toHaveLength(2)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -594,11 +615,11 @@ describe("MessageV2.get", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const uid = await addUser(session.id, "hello")
|
||||
const aid = await addAssistant(session.id, uid)
|
||||
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: session.id,
|
||||
messageID: aid,
|
||||
|
|
@ -611,7 +632,7 @@ describe("MessageV2.get", () => {
|
|||
expect(result.parts).toHaveLength(1)
|
||||
expect((result.parts[0] as MessageV2.TextPart).text).toBe("response")
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -620,14 +641,14 @@ describe("MessageV2.get", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const id = await addUser(session.id)
|
||||
|
||||
const result = MessageV2.get({ sessionID: session.id, messageID: id })
|
||||
expect(result.info.id).toBe(id)
|
||||
expect(result.parts).toEqual([])
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -638,7 +659,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 5)
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
|
|
@ -646,7 +667,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
// reversed from newest-first to chronological
|
||||
expect(result.map((item) => item.info.id)).toEqual(ids)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -655,13 +676,13 @@ describe("MessageV2.filterCompacted", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
|
||||
// Chronological: u1(+compaction part), a1(summary, parentID=u1), u2, a2
|
||||
// Stream (newest first): a2, u2, a1(adds u1 to completed), u1(in completed + compaction) -> break
|
||||
const u1 = await addUser(session.id, "first question")
|
||||
const a1 = await addAssistant(session.id, u1, { summary: true, finish: "end_turn" })
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: session.id,
|
||||
messageID: a1,
|
||||
|
|
@ -672,7 +693,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
|
||||
const u2 = await addUser(session.id, "new question")
|
||||
const a2 = await addAssistant(session.id, u2)
|
||||
await Session.updatePart({
|
||||
await svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: session.id,
|
||||
messageID: a2,
|
||||
|
|
@ -685,7 +706,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
expect(result[0].info.id).toBe(u1)
|
||||
expect(result.length).toBe(4)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -699,7 +720,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
|
||||
const u1 = await addUser(session.id, "hello")
|
||||
await addCompactionPart(session.id, u1)
|
||||
|
|
@ -708,7 +729,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
expect(result).toHaveLength(2)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -717,7 +738,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
|
||||
const u1 = await addUser(session.id, "hello")
|
||||
await addCompactionPart(session.id, u1)
|
||||
|
|
@ -733,7 +754,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
// Error assistant doesn't add to completed, so compaction boundary never triggers
|
||||
expect(result).toHaveLength(3)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -742,7 +763,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
|
||||
const u1 = await addUser(session.id, "hello")
|
||||
await addCompactionPart(session.id, u1)
|
||||
|
|
@ -754,7 +775,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
expect(result).toHaveLength(3)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -808,7 +829,7 @@ describe("MessageV2 consistency", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
await fill(session.id, 3)
|
||||
|
||||
const paged = MessageV2.page({ sessionID: session.id, limit: 10 })
|
||||
|
|
@ -818,7 +839,7 @@ describe("MessageV2 consistency", () => {
|
|||
expect(got.parts).toEqual(item.parts)
|
||||
}
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -827,14 +848,14 @@ describe("MessageV2 consistency", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const [id] = await fill(session.id, 1)
|
||||
|
||||
const got = MessageV2.get({ sessionID: session.id, messageID: id })
|
||||
const standalone = MessageV2.parts(id)
|
||||
expect(got.parts).toEqual(standalone)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -843,7 +864,7 @@ describe("MessageV2 consistency", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
await fill(session.id, 7)
|
||||
|
||||
const streamed = Array.from(MessageV2.stream(session.id))
|
||||
|
|
@ -861,7 +882,7 @@ describe("MessageV2 consistency", () => {
|
|||
|
||||
expect(streamed.map((m) => m.info.id)).toEqual(paged.map((m) => m.info.id))
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -870,7 +891,7 @@ describe("MessageV2 consistency", () => {
|
|||
await Instance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await svc.create({})
|
||||
const ids = await fill(session.id, 4)
|
||||
|
||||
const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
|
|
@ -878,7 +899,7 @@ describe("MessageV2 consistency", () => {
|
|||
|
||||
expect(filtered.map((m) => m.info.id)).toEqual(all.map((m) => m.info.id))
|
||||
|
||||
await Session.remove(session.id)
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ function makeHttp() {
|
|||
Layer.provide(SystemPrompt.defaultLayer),
|
||||
Layer.provideMerge(deps),
|
||||
),
|
||||
)
|
||||
).pipe(Layer.provide(summary))
|
||||
}
|
||||
|
||||
const it = testEffect(makeHttp())
|
||||
|
|
@ -384,25 +384,23 @@ it.live("loop calls LLM and returns assistant message", () =>
|
|||
it.live("static loop returns assistant text through local provider", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* ({ llm }) {
|
||||
const session = yield* Effect.promise(() =>
|
||||
Session.create({
|
||||
title: "Prompt provider",
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
}),
|
||||
)
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({
|
||||
title: "Prompt provider",
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
})
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
}),
|
||||
)
|
||||
yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
|
||||
yield* llm.text("world")
|
||||
|
||||
const result = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: session.id }))
|
||||
const result = yield* prompt.loop({ sessionID: session.id })
|
||||
expect(result.info.role).toBe("assistant")
|
||||
expect(result.parts.some((part) => part.type === "text" && part.text === "world")).toBe(true)
|
||||
expect(yield* llm.hits).toHaveLength(1)
|
||||
|
|
@ -415,40 +413,36 @@ it.live("static loop returns assistant text through local provider", () =>
|
|||
it.live("static loop consumes queued replies across turns", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* ({ llm }) {
|
||||
const session = yield* Effect.promise(() =>
|
||||
Session.create({
|
||||
title: "Prompt provider turns",
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
}),
|
||||
)
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({
|
||||
title: "Prompt provider turns",
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
})
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello one" }],
|
||||
}),
|
||||
)
|
||||
yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello one" }],
|
||||
})
|
||||
|
||||
yield* llm.text("world one")
|
||||
|
||||
const first = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: session.id }))
|
||||
const first = yield* prompt.loop({ sessionID: session.id })
|
||||
expect(first.info.role).toBe("assistant")
|
||||
expect(first.parts.some((part) => part.type === "text" && part.text === "world one")).toBe(true)
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello two" }],
|
||||
}),
|
||||
)
|
||||
yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello two" }],
|
||||
})
|
||||
|
||||
yield* llm.text("world two")
|
||||
|
||||
const second = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: session.id }))
|
||||
const second = yield* prompt.loop({ sessionID: session.id })
|
||||
expect(second.info.role).toBe("assistant")
|
||||
expect(second.parts.some((part) => part.type === "text" && part.text === "world two")).toBe(true)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import path from "path"
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { fileURLToPath } from "url"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { Session } from "../../src/session"
|
||||
|
|
@ -12,6 +13,12 @@ import { tmpdir } from "../fixture/fixture"
|
|||
|
||||
Log.init({ print: false })
|
||||
|
||||
function run<A, E>(fx: Effect.Effect<A, E, SessionPrompt.Service | Session.Service>) {
|
||||
return Effect.runPromise(
|
||||
fx.pipe(Effect.scoped, Effect.provide(Layer.mergeAll(SessionPrompt.defaultLayer, Session.defaultLayer))),
|
||||
)
|
||||
}
|
||||
|
||||
function defer<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void
|
||||
const promise = new Promise<T>((done) => {
|
||||
|
|
@ -104,34 +111,39 @@ describe("session.prompt missing file", () => {
|
|||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
|
||||
const missing = path.join(tmp.path, "does-not-exist.ts")
|
||||
const msg = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{ type: "text", text: "please review @does-not-exist.ts" },
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${missing}`,
|
||||
filename: "does-not-exist.ts",
|
||||
},
|
||||
],
|
||||
})
|
||||
const missing = path.join(tmp.path, "does-not-exist.ts")
|
||||
const msg = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{ type: "text", text: "please review @does-not-exist.ts" },
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${missing}`,
|
||||
filename: "does-not-exist.ts",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (msg.info.role !== "user") throw new Error("expected user message")
|
||||
if (msg.info.role !== "user") throw new Error("expected user message")
|
||||
|
||||
const hasFailure = msg.parts.some(
|
||||
(part) => part.type === "text" && part.synthetic && part.text.includes("Read tool failed to read"),
|
||||
)
|
||||
expect(hasFailure).toBe(true)
|
||||
const hasFailure = msg.parts.some(
|
||||
(part) => part.type === "text" && part.synthetic && part.text.includes("Read tool failed to read"),
|
||||
)
|
||||
expect(hasFailure).toBe(true)
|
||||
|
||||
await Session.remove(session.id)
|
||||
},
|
||||
yield* sessions.remove(session.id)
|
||||
}),
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -149,39 +161,44 @@ describe("session.prompt missing file", () => {
|
|||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
|
||||
const missing = path.join(tmp.path, "still-missing.ts")
|
||||
const msg = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${missing}`,
|
||||
filename: "still-missing.ts",
|
||||
},
|
||||
{ type: "text", text: "after-file" },
|
||||
],
|
||||
})
|
||||
const missing = path.join(tmp.path, "still-missing.ts")
|
||||
const msg = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${missing}`,
|
||||
filename: "still-missing.ts",
|
||||
},
|
||||
{ type: "text", text: "after-file" },
|
||||
],
|
||||
})
|
||||
|
||||
if (msg.info.role !== "user") throw new Error("expected user message")
|
||||
if (msg.info.role !== "user") throw new Error("expected user message")
|
||||
|
||||
const stored = await MessageV2.get({
|
||||
sessionID: session.id,
|
||||
messageID: msg.info.id,
|
||||
})
|
||||
const text = stored.parts.filter((part) => part.type === "text").map((part) => part.text)
|
||||
const stored = MessageV2.get({
|
||||
sessionID: session.id,
|
||||
messageID: msg.info.id,
|
||||
})
|
||||
const text = stored.parts.filter((part) => part.type === "text").map((part) => part.text)
|
||||
|
||||
expect(text[0]?.startsWith("Called the Read tool with the following input:")).toBe(true)
|
||||
expect(text[1]?.includes("Read tool failed to read")).toBe(true)
|
||||
expect(text[2]).toBe("after-file")
|
||||
expect(text[0]?.startsWith("Called the Read tool with the following input:")).toBe(true)
|
||||
expect(text[1]?.includes("Read tool failed to read")).toBe(true)
|
||||
expect(text[2]).toBe("after-file")
|
||||
|
||||
await Session.remove(session.id)
|
||||
},
|
||||
yield* sessions.remove(session.id)
|
||||
}),
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -197,31 +214,36 @@ describe("session.prompt special characters", () => {
|
|||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const template = "Read @file#name.txt"
|
||||
const parts = await SessionPrompt.resolvePromptParts(template)
|
||||
const fileParts = parts.filter((part) => part.type === "file")
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
const template = "Read @file#name.txt"
|
||||
const parts = yield* prompt.resolvePromptParts(template)
|
||||
const fileParts = parts.filter((part) => part.type === "file")
|
||||
|
||||
expect(fileParts.length).toBe(1)
|
||||
expect(fileParts[0].filename).toBe("file#name.txt")
|
||||
expect(fileParts[0].url).toContain("%23")
|
||||
expect(fileParts.length).toBe(1)
|
||||
expect(fileParts[0].filename).toBe("file#name.txt")
|
||||
expect(fileParts[0].url).toContain("%23")
|
||||
|
||||
const decodedPath = fileURLToPath(fileParts[0].url)
|
||||
expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt"))
|
||||
const decodedPath = fileURLToPath(fileParts[0].url)
|
||||
expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt"))
|
||||
|
||||
const message = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts,
|
||||
noReply: true,
|
||||
})
|
||||
const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||
const textParts = stored.parts.filter((part) => part.type === "text")
|
||||
const hasContent = textParts.some((part) => part.text.includes("special content"))
|
||||
expect(hasContent).toBe(true)
|
||||
const message = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts,
|
||||
noReply: true,
|
||||
})
|
||||
const stored = MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||
const textParts = stored.parts.filter((part) => part.type === "text")
|
||||
const hasContent = textParts.some((part) => part.text.includes("special content"))
|
||||
expect(hasContent).toBe(true)
|
||||
|
||||
await Session.remove(session.id)
|
||||
},
|
||||
yield* sessions.remove(session.id)
|
||||
}),
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -273,21 +295,26 @@ describe("session.prompt regression", () => {
|
|||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({ title: "Prompt regression" })
|
||||
const result = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
parts: [{ type: "text", text: "Where is SessionProcessor?" }],
|
||||
})
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "Prompt regression" })
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
parts: [{ type: "text", text: "Where is SessionProcessor?" }],
|
||||
})
|
||||
|
||||
expect(result.info.role).toBe("assistant")
|
||||
expect(result.parts.some((part) => part.type === "text" && part.text.includes("processor.ts"))).toBe(true)
|
||||
expect(result.info.role).toBe("assistant")
|
||||
expect(result.parts.some((part) => part.type === "text" && part.text.includes("processor.ts"))).toBe(true)
|
||||
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
expect(msgs.filter((msg) => msg.info.role === "assistant")).toHaveLength(1)
|
||||
expect(calls).toBe(1)
|
||||
},
|
||||
const msgs = yield* sessions.messages({ sessionID: session.id })
|
||||
expect(msgs.filter((msg) => msg.info.role === "assistant")).toHaveLength(1)
|
||||
expect(calls).toBe(1)
|
||||
}),
|
||||
),
|
||||
})
|
||||
} finally {
|
||||
server.stop(true)
|
||||
|
|
@ -342,36 +369,45 @@ describe("session.prompt regression", () => {
|
|||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({ title: "Prompt cancel regression" })
|
||||
const run = SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
parts: [{ type: "text", text: "Cancel me" }],
|
||||
})
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "Prompt cancel regression" })
|
||||
const task = Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
parts: [{ type: "text", text: "Cancel me" }],
|
||||
}),
|
||||
)
|
||||
|
||||
await ready.promise
|
||||
await SessionPrompt.cancel(session.id)
|
||||
yield* Effect.promise(() => ready.promise)
|
||||
yield* prompt.cancel(session.id)
|
||||
|
||||
const result = await Promise.race([
|
||||
run,
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("timed out waiting for cancel")), 1000),
|
||||
),
|
||||
])
|
||||
const result = yield* Effect.promise(() =>
|
||||
Promise.race([
|
||||
task,
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("timed out waiting for cancel")), 1000),
|
||||
),
|
||||
]),
|
||||
)
|
||||
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.error?.name).toBe("MessageAbortedError")
|
||||
}
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.error?.name).toBe("MessageAbortedError")
|
||||
}
|
||||
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const last = msgs.findLast((msg) => msg.info.role === "assistant")
|
||||
expect(last?.info.role).toBe("assistant")
|
||||
if (last?.info.role === "assistant") {
|
||||
expect(last.info.error?.name).toBe("MessageAbortedError")
|
||||
}
|
||||
},
|
||||
const msgs = yield* sessions.messages({ sessionID: session.id })
|
||||
const last = msgs.findLast((msg) => msg.info.role === "assistant")
|
||||
expect(last?.info.role).toBe("assistant")
|
||||
if (last?.info.role === "assistant") {
|
||||
expect(last.info.error?.name).toBe("MessageAbortedError")
|
||||
}
|
||||
}),
|
||||
),
|
||||
})
|
||||
} finally {
|
||||
server.stop(true)
|
||||
|
|
@ -399,45 +435,50 @@ describe("session.prompt agent variant", () => {
|
|||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
|
||||
const other = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: { providerID: ProviderID.make("opencode"), modelID: ModelID.make("kimi-k2.5-free") },
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
if (other.info.role !== "user") throw new Error("expected user message")
|
||||
expect(other.info.model.variant).toBeUndefined()
|
||||
const other = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: { providerID: ProviderID.make("opencode"), modelID: ModelID.make("kimi-k2.5-free") },
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
if (other.info.role !== "user") throw new Error("expected user message")
|
||||
expect(other.info.model.variant).toBeUndefined()
|
||||
|
||||
const match = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello again" }],
|
||||
})
|
||||
if (match.info.role !== "user") throw new Error("expected user message")
|
||||
expect(match.info.model).toEqual({
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-5.2"),
|
||||
variant: "xhigh",
|
||||
})
|
||||
expect(match.info.model.variant).toBe("xhigh")
|
||||
const match = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello again" }],
|
||||
})
|
||||
if (match.info.role !== "user") throw new Error("expected user message")
|
||||
expect(match.info.model).toEqual({
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-5.2"),
|
||||
variant: "xhigh",
|
||||
})
|
||||
expect(match.info.model.variant).toBe("xhigh")
|
||||
|
||||
const override = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
variant: "high",
|
||||
parts: [{ type: "text", text: "hello third" }],
|
||||
})
|
||||
if (override.info.role !== "user") throw new Error("expected user message")
|
||||
expect(override.info.model.variant).toBe("high")
|
||||
const override = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
variant: "high",
|
||||
parts: [{ type: "text", text: "hello third" }],
|
||||
})
|
||||
if (override.info.role !== "user") throw new Error("expected user message")
|
||||
expect(override.info.model.variant).toBe("high")
|
||||
|
||||
await Session.remove(session.id)
|
||||
},
|
||||
yield* sessions.remove(session.id)
|
||||
}),
|
||||
),
|
||||
})
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.OPENAI_API_KEY
|
||||
|
|
@ -451,24 +492,33 @@ describe("session.agent-resolution", () => {
|
|||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const err = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "nonexistent-agent-xyz",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
}).then(
|
||||
() => undefined,
|
||||
(e) => e,
|
||||
)
|
||||
expect(err).toBeDefined()
|
||||
expect(err).not.toBeInstanceOf(TypeError)
|
||||
expect(NamedError.Unknown.isInstance(err)).toBe(true)
|
||||
if (NamedError.Unknown.isInstance(err)) {
|
||||
expect(err.data.message).toContain('Agent not found: "nonexistent-agent-xyz"')
|
||||
}
|
||||
},
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
const err = yield* Effect.promise(() =>
|
||||
Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "nonexistent-agent-xyz",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
}),
|
||||
).then(
|
||||
() => undefined,
|
||||
(e) => e,
|
||||
),
|
||||
)
|
||||
expect(err).toBeDefined()
|
||||
expect(err).not.toBeInstanceOf(TypeError)
|
||||
expect(NamedError.Unknown.isInstance(err)).toBe(true)
|
||||
if (NamedError.Unknown.isInstance(err)) {
|
||||
expect(err.data.message).toContain('Agent not found: "nonexistent-agent-xyz"')
|
||||
}
|
||||
}),
|
||||
),
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
|
|
@ -476,22 +526,31 @@ describe("session.agent-resolution", () => {
|
|||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const err = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "nonexistent-agent-xyz",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
}).then(
|
||||
() => undefined,
|
||||
(e) => e,
|
||||
)
|
||||
expect(NamedError.Unknown.isInstance(err)).toBe(true)
|
||||
if (NamedError.Unknown.isInstance(err)) {
|
||||
expect(err.data.message).toContain("build")
|
||||
}
|
||||
},
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
const err = yield* Effect.promise(() =>
|
||||
Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "nonexistent-agent-xyz",
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
}),
|
||||
).then(
|
||||
() => undefined,
|
||||
(e) => e,
|
||||
),
|
||||
)
|
||||
expect(NamedError.Unknown.isInstance(err)).toBe(true)
|
||||
if (NamedError.Unknown.isInstance(err)) {
|
||||
expect(err.data.message).toContain("build")
|
||||
}
|
||||
}),
|
||||
),
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
|
|
@ -499,24 +558,33 @@ describe("session.agent-resolution", () => {
|
|||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const err = await SessionPrompt.command({
|
||||
sessionID: session.id,
|
||||
command: "nonexistent-command-xyz",
|
||||
arguments: "",
|
||||
}).then(
|
||||
() => undefined,
|
||||
(e) => e,
|
||||
)
|
||||
expect(err).toBeDefined()
|
||||
expect(err).not.toBeInstanceOf(TypeError)
|
||||
expect(NamedError.Unknown.isInstance(err)).toBe(true)
|
||||
if (NamedError.Unknown.isInstance(err)) {
|
||||
expect(err.data.message).toContain('Command not found: "nonexistent-command-xyz"')
|
||||
expect(err.data.message).toContain("init")
|
||||
}
|
||||
},
|
||||
fn: () =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
const err = yield* Effect.promise(() =>
|
||||
Effect.runPromise(
|
||||
prompt.command({
|
||||
sessionID: session.id,
|
||||
command: "nonexistent-command-xyz",
|
||||
arguments: "",
|
||||
}),
|
||||
).then(
|
||||
() => undefined,
|
||||
(e) => e,
|
||||
),
|
||||
)
|
||||
expect(err).toBeDefined()
|
||||
expect(err).not.toBeInstanceOf(TypeError)
|
||||
expect(NamedError.Unknown.isInstance(err)).toBe(true)
|
||||
if (NamedError.Unknown.isInstance(err)) {
|
||||
expect(err.data.message).toContain('Command not found: "nonexistent-command-xyz"')
|
||||
expect(err.data.message).toContain("init")
|
||||
}
|
||||
}),
|
||||
),
|
||||
})
|
||||
}, 30000)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,43 +1,62 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Session } from "../../src/session"
|
||||
import { Session as SessionNs } from "../../src/session"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID } from "../../src/session/schema"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
Log.init({ print: false })
|
||||
|
||||
function create(input?: SessionNs.CreateInput) {
|
||||
return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input)))
|
||||
}
|
||||
|
||||
function get(id: SessionID) {
|
||||
return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(id)))
|
||||
}
|
||||
|
||||
function remove(id: SessionID) {
|
||||
return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.remove(id)))
|
||||
}
|
||||
|
||||
function updateMessage<T extends MessageV2.Info>(msg: T) {
|
||||
return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
|
||||
}
|
||||
|
||||
function updatePart<T extends MessageV2.Part>(part: T) {
|
||||
return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updatePart(part)))
|
||||
}
|
||||
|
||||
describe("session.created event", () => {
|
||||
test("should emit session.created event when session is created", async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
let eventReceived = false
|
||||
let receivedInfo: Session.Info | undefined
|
||||
let receivedInfo: SessionNs.Info | undefined
|
||||
|
||||
const unsub = Bus.subscribe(Session.Event.Created, (event) => {
|
||||
const unsub = Bus.subscribe(SessionNs.Event.Created, (event) => {
|
||||
eventReceived = true
|
||||
receivedInfo = event.properties.info as Session.Info
|
||||
receivedInfo = event.properties.info as SessionNs.Info
|
||||
})
|
||||
|
||||
const session = await Session.create({})
|
||||
|
||||
const info = await create({})
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
unsub()
|
||||
|
||||
expect(eventReceived).toBe(true)
|
||||
expect(receivedInfo).toBeDefined()
|
||||
expect(receivedInfo?.id).toBe(session.id)
|
||||
expect(receivedInfo?.projectID).toBe(session.projectID)
|
||||
expect(receivedInfo?.directory).toBe(session.directory)
|
||||
expect(receivedInfo?.title).toBe(session.title)
|
||||
expect(receivedInfo?.id).toBe(info.id)
|
||||
expect(receivedInfo?.projectID).toBe(info.projectID)
|
||||
expect(receivedInfo?.directory).toBe(info.directory)
|
||||
expect(receivedInfo?.title).toBe(info.title)
|
||||
|
||||
await Session.remove(session.id)
|
||||
await remove(info.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -48,18 +67,16 @@ describe("session.created event", () => {
|
|||
fn: async () => {
|
||||
const events: string[] = []
|
||||
|
||||
const unsubCreated = Bus.subscribe(Session.Event.Created, () => {
|
||||
const unsubCreated = Bus.subscribe(SessionNs.Event.Created, () => {
|
||||
events.push("created")
|
||||
})
|
||||
|
||||
const unsubUpdated = Bus.subscribe(Session.Event.Updated, () => {
|
||||
const unsubUpdated = Bus.subscribe(SessionNs.Event.Updated, () => {
|
||||
events.push("updated")
|
||||
})
|
||||
|
||||
const session = await Session.create({})
|
||||
|
||||
const info = await create({})
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
unsubCreated()
|
||||
unsubUpdated()
|
||||
|
||||
|
|
@ -67,7 +84,7 @@ describe("session.created event", () => {
|
|||
expect(events).toContain("updated")
|
||||
expect(events.indexOf("created")).toBeLessThan(events.indexOf("updated"))
|
||||
|
||||
await Session.remove(session.id)
|
||||
await remove(info.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -80,12 +97,12 @@ describe("step-finish token propagation via Bus event", () => {
|
|||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const info = await create({})
|
||||
|
||||
const messageID = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
await updateMessage({
|
||||
id: messageID,
|
||||
sessionID: session.id,
|
||||
sessionID: info.id,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: "user",
|
||||
|
|
@ -110,15 +127,14 @@ describe("step-finish token propagation via Bus event", () => {
|
|||
const partInput = {
|
||||
id: PartID.ascending(),
|
||||
messageID,
|
||||
sessionID: session.id,
|
||||
sessionID: info.id,
|
||||
type: "step-finish" as const,
|
||||
reason: "stop",
|
||||
cost: 0.005,
|
||||
tokens,
|
||||
}
|
||||
|
||||
await Session.updatePart(partInput)
|
||||
|
||||
await updatePart(partInput)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(received).toBeDefined()
|
||||
|
|
@ -134,7 +150,7 @@ describe("step-finish token propagation via Bus event", () => {
|
|||
expect(received).not.toBe(partInput)
|
||||
|
||||
unsub()
|
||||
await Session.remove(session.id)
|
||||
await remove(info.id)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
|
@ -146,17 +162,17 @@ describe("Session", () => {
|
|||
test("remove works without an instance", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const session = await Instance.provide({
|
||||
const info = await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => Session.create({ title: "remove-without-instance" }),
|
||||
fn: () => create({ title: "remove-without-instance" }),
|
||||
})
|
||||
|
||||
await expect(async () => {
|
||||
await Session.remove(session.id)
|
||||
await remove(info.id)
|
||||
}).not.toThrow()
|
||||
|
||||
let missing = false
|
||||
await Session.get(session.id).catch(() => {
|
||||
await get(info.id).catch(() => {
|
||||
missing = true
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Session } from "../../src/session"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { Log } from "../../src/util/log"
|
||||
|
|
@ -20,51 +21,63 @@ async function withInstance<T>(fn: () => Promise<T>): Promise<T> {
|
|||
})
|
||||
}
|
||||
|
||||
function run<A, E>(fx: Effect.Effect<A, E, SessionPrompt.Service | Session.Service>) {
|
||||
return Effect.runPromise(
|
||||
fx.pipe(Effect.scoped, Effect.provide(Layer.mergeAll(SessionPrompt.defaultLayer, Session.defaultLayer))),
|
||||
)
|
||||
}
|
||||
|
||||
describe("StructuredOutput Integration", () => {
|
||||
test.skipIf(!hasApiKey)(
|
||||
"produces structured output with simple schema",
|
||||
async () => {
|
||||
await withInstance(async () => {
|
||||
const session = await Session.create({ title: "Structured Output Test" })
|
||||
await withInstance(() =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "Structured Output Test" })
|
||||
|
||||
const result = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What is 2 + 2? Provide a simple answer.",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "json_schema",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
answer: { type: "number", description: "The numerical answer" },
|
||||
explanation: { type: "string", description: "Brief explanation" },
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What is 2 + 2? Provide a simple answer.",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "json_schema",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
answer: { type: "number", description: "The numerical answer" },
|
||||
explanation: { type: "string", description: "Brief explanation" },
|
||||
},
|
||||
required: ["answer"],
|
||||
},
|
||||
retryCount: 0,
|
||||
},
|
||||
required: ["answer"],
|
||||
},
|
||||
retryCount: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Verify structured output was captured (only on assistant messages)
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.structured).toBeDefined()
|
||||
expect(typeof result.info.structured).toBe("object")
|
||||
// Verify structured output was captured (only on assistant messages)
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.structured).toBeDefined()
|
||||
expect(typeof result.info.structured).toBe("object")
|
||||
|
||||
const output = result.info.structured as any
|
||||
expect(output.answer).toBe(4)
|
||||
const output = result.info.structured as any
|
||||
expect(output.answer).toBe(4)
|
||||
|
||||
// Verify no error was set
|
||||
expect(result.info.error).toBeUndefined()
|
||||
}
|
||||
// Verify no error was set
|
||||
expect(result.info.error).toBeUndefined()
|
||||
}
|
||||
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
})
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
60000,
|
||||
)
|
||||
|
|
@ -72,62 +85,68 @@ describe("StructuredOutput Integration", () => {
|
|||
test.skipIf(!hasApiKey)(
|
||||
"produces structured output with nested objects",
|
||||
async () => {
|
||||
await withInstance(async () => {
|
||||
const session = await Session.create({ title: "Nested Schema Test" })
|
||||
await withInstance(() =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "Nested Schema Test" })
|
||||
|
||||
const result = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Tell me about Anthropic company in a structured format.",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "json_schema",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
company: {
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Tell me about Anthropic company in a structured format.",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "json_schema",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
founded: { type: "number" },
|
||||
company: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
founded: { type: "number" },
|
||||
},
|
||||
required: ["name", "founded"],
|
||||
},
|
||||
products: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
required: ["name", "founded"],
|
||||
},
|
||||
products: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
required: ["company"],
|
||||
},
|
||||
retryCount: 0,
|
||||
},
|
||||
required: ["company"],
|
||||
},
|
||||
retryCount: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Verify structured output was captured (only on assistant messages)
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.structured).toBeDefined()
|
||||
const output = result.info.structured as any
|
||||
// Verify structured output was captured (only on assistant messages)
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.structured).toBeDefined()
|
||||
const output = result.info.structured as any
|
||||
|
||||
expect(output.company).toBeDefined()
|
||||
expect(output.company.name).toBe("Anthropic")
|
||||
expect(typeof output.company.founded).toBe("number")
|
||||
expect(output.company).toBeDefined()
|
||||
expect(output.company.name).toBe("Anthropic")
|
||||
expect(typeof output.company.founded).toBe("number")
|
||||
|
||||
if (output.products) {
|
||||
expect(Array.isArray(output.products)).toBe(true)
|
||||
}
|
||||
if (output.products) {
|
||||
expect(Array.isArray(output.products)).toBe(true)
|
||||
}
|
||||
|
||||
// Verify no error was set
|
||||
expect(result.info.error).toBeUndefined()
|
||||
}
|
||||
// Verify no error was set
|
||||
expect(result.info.error).toBeUndefined()
|
||||
}
|
||||
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
})
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
60000,
|
||||
)
|
||||
|
|
@ -135,35 +154,41 @@ describe("StructuredOutput Integration", () => {
|
|||
test.skipIf(!hasApiKey)(
|
||||
"works with text outputFormat (default)",
|
||||
async () => {
|
||||
await withInstance(async () => {
|
||||
const session = await Session.create({ title: "Text Output Test" })
|
||||
await withInstance(() =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "Text Output Test" })
|
||||
|
||||
const result = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Say hello.",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "text",
|
||||
},
|
||||
})
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Say hello.",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "text",
|
||||
},
|
||||
})
|
||||
|
||||
// Verify no structured output (text mode) and no error
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.structured).toBeUndefined()
|
||||
expect(result.info.error).toBeUndefined()
|
||||
}
|
||||
// Verify no structured output (text mode) and no error
|
||||
expect(result.info.role).toBe("assistant")
|
||||
if (result.info.role === "assistant") {
|
||||
expect(result.info.structured).toBeUndefined()
|
||||
expect(result.info.error).toBeUndefined()
|
||||
}
|
||||
|
||||
// Verify we got a response with parts
|
||||
expect(result.parts.length).toBeGreaterThan(0)
|
||||
// Verify we got a response with parts
|
||||
expect(result.parts.length).toBeGreaterThan(0)
|
||||
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
})
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
60000,
|
||||
)
|
||||
|
|
@ -171,47 +196,53 @@ describe("StructuredOutput Integration", () => {
|
|||
test.skipIf(!hasApiKey)(
|
||||
"stores outputFormat on user message",
|
||||
async () => {
|
||||
await withInstance(async () => {
|
||||
const session = await Session.create({ title: "OutputFormat Storage Test" })
|
||||
await withInstance(() =>
|
||||
run(
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "OutputFormat Storage Test" })
|
||||
|
||||
await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What is 1 + 1?",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "json_schema",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
result: { type: "number" },
|
||||
yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What is 1 + 1?",
|
||||
},
|
||||
],
|
||||
format: {
|
||||
type: "json_schema",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
result: { type: "number" },
|
||||
},
|
||||
required: ["result"],
|
||||
},
|
||||
retryCount: 3,
|
||||
},
|
||||
required: ["result"],
|
||||
},
|
||||
retryCount: 3,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Get all messages from session
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const userMessage = messages.find((m) => m.info.role === "user")
|
||||
// Get all messages from session
|
||||
const messages = yield* sessions.messages({ sessionID: session.id })
|
||||
const userMessage = messages.find((m) => m.info.role === "user")
|
||||
|
||||
// Verify outputFormat was stored on user message
|
||||
expect(userMessage).toBeDefined()
|
||||
if (userMessage?.info.role === "user") {
|
||||
expect(userMessage.info.format).toBeDefined()
|
||||
expect(userMessage.info.format?.type).toBe("json_schema")
|
||||
if (userMessage.info.format?.type === "json_schema") {
|
||||
expect(userMessage.info.format.retryCount).toBe(3)
|
||||
}
|
||||
}
|
||||
// Verify outputFormat was stored on user message
|
||||
expect(userMessage).toBeDefined()
|
||||
if (userMessage?.info.role === "user") {
|
||||
expect(userMessage.info.format).toBeDefined()
|
||||
expect(userMessage.info.format?.type).toBe("json_schema")
|
||||
if (userMessage.info.format?.type === "json_schema") {
|
||||
expect(userMessage.info.format.retryCount).toBe(3)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
})
|
||||
// Clean up
|
||||
// Note: Not removing session to avoid race with background SessionSummary.summarize
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
60000,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue