425 lines
15 KiB
TypeScript
425 lines
15 KiB
TypeScript
import { assert, describe, it } from "@effect/vitest"
|
|
import { Effect, Option } from "effect"
|
|
import { Prompt, ResponseIdTracker } from "effect/unstable/ai"
|
|
|
|
const systemMessage = (text: string) => Prompt.systemMessage({ content: text })
|
|
|
|
const userMessage = (text: string) => Prompt.userMessage({ content: [Prompt.textPart({ text })] })
|
|
|
|
const assistantMessage = (text: string) => Prompt.assistantMessage({ content: [Prompt.textPart({ text })] })
|
|
|
|
const assistantToolCallMessage = (id: string) =>
|
|
Prompt.assistantMessage({
|
|
content: [
|
|
Prompt.makePart("tool-call", {
|
|
id,
|
|
name: "test_tool",
|
|
params: { query: "query" },
|
|
providerExecuted: false
|
|
})
|
|
]
|
|
})
|
|
|
|
const toolResultMessage = (id: string) =>
|
|
Prompt.toolMessage({
|
|
content: [
|
|
Prompt.toolResultPart({
|
|
id,
|
|
name: "test_tool",
|
|
result: { ok: true },
|
|
isFailure: false
|
|
})
|
|
]
|
|
})
|
|
|
|
const assertPreparedSome = (
|
|
prepared: Option.Option<ResponseIdTracker.PrepareResult>,
|
|
previousResponseId: string,
|
|
prompt: Prompt.Prompt
|
|
) => {
|
|
assert.isTrue(Option.isSome(prepared))
|
|
if (Option.isSome(prepared)) {
|
|
assert.strictEqual(prepared.value.previousResponseId, previousResponseId)
|
|
assert.deepStrictEqual(prepared.value.prompt, prompt)
|
|
}
|
|
}
|
|
|
|
describe("ResponseIdTracker", () => {
|
|
it.effect("returns None for a fresh tracker", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const msg1 = userMessage("msg1")
|
|
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("prepares only the new user message after a tracked first turn", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const msg1 = userMessage("msg1")
|
|
const asst = assistantMessage("done")
|
|
const msg2 = userMessage("msg2")
|
|
|
|
tracker.markParts([msg1], "resp_123")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1, asst, msg2]))
|
|
|
|
assertPreparedSome(prepared, "resp_123", Prompt.fromMessages([msg2]))
|
|
}))
|
|
|
|
it.effect("shares tracker state through its Context service layer", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const msg1 = userMessage("msg1")
|
|
const msg1New = userMessage("msg1-new")
|
|
const asst = assistantMessage("done")
|
|
const msg2 = userMessage("msg2")
|
|
|
|
tracker.markParts([msg1], "resp_123")
|
|
tracker.clearUnsafe()
|
|
tracker.markParts([msg1New], "resp_456")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1New, asst, msg2]))
|
|
|
|
assertPreparedSome(prepared, "resp_456", Prompt.fromMessages([msg2]))
|
|
}))
|
|
|
|
it.effect("keeps latest response id when markParts is called twice", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const msg1 = userMessage("msg1")
|
|
const asst = assistantMessage("done")
|
|
const msg2 = userMessage("msg2")
|
|
|
|
tracker.markParts([msg1], "resp_1")
|
|
tracker.markParts([msg1], "resp_2")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1, asst, msg2]))
|
|
|
|
assertPreparedSome(prepared, "resp_2", Prompt.fromMessages([msg2]))
|
|
}))
|
|
|
|
it.effect("returns None after clear", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const msg1 = userMessage("msg1")
|
|
const asst = assistantMessage("done")
|
|
const msg2 = userMessage("msg2")
|
|
|
|
tracker.markParts([msg1], "resp_123")
|
|
tracker.clearUnsafe()
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1, asst, msg2]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("recovers after clear with new marks", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const msg1 = userMessage("msg1")
|
|
const msg1New = userMessage("msg1-new")
|
|
const asst = assistantMessage("done")
|
|
const msg2 = userMessage("msg2")
|
|
|
|
tracker.markParts([msg1], "resp_123")
|
|
tracker.clearUnsafe()
|
|
tracker.markParts([msg1New], "resp_456")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1New, asst, msg2]))
|
|
|
|
assertPreparedSome(prepared, "resp_456", Prompt.fromMessages([msg2]))
|
|
}))
|
|
|
|
it.effect("remains usable after concurrent markParts and clear", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const raceMessage = userMessage("race")
|
|
|
|
const markPartsRace = Effect.gen(function*() {
|
|
for (let i = 0; i < 200; i++) {
|
|
tracker.markParts([raceMessage], "resp_race")
|
|
yield* Effect.yieldNow
|
|
}
|
|
})
|
|
|
|
const clearRace = Effect.gen(function*() {
|
|
for (let i = 0; i < 200; i++) {
|
|
tracker.clearUnsafe()
|
|
yield* Effect.yieldNow
|
|
}
|
|
})
|
|
|
|
yield* Effect.all([
|
|
markPartsRace,
|
|
clearRace
|
|
], {
|
|
concurrency: "unbounded",
|
|
discard: true
|
|
})
|
|
|
|
const msg1 = userMessage("msg1")
|
|
const asst = assistantMessage("done")
|
|
const msg2 = userMessage("msg2")
|
|
tracker.markParts([msg1], "resp_final")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1, asst, msg2]))
|
|
|
|
assertPreparedSome(prepared, "resp_final", Prompt.fromMessages([msg2]))
|
|
}))
|
|
|
|
it.effect("returns None for first turn with system and user messages", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns None when tracked prompt has no assistant message", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns incremental user follow-up after assistant", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1, user2]))
|
|
|
|
assertPreparedSome(prepared, "resp_1", Prompt.fromMessages([user2]))
|
|
}))
|
|
|
|
it.effect("returns incremental tool results after assistant tool call", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantToolCallMessage("call_1")
|
|
const tool1 = toolResultMessage("call_1")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1, tool1]))
|
|
|
|
assertPreparedSome(prepared, "resp_1", Prompt.fromMessages([tool1]))
|
|
}))
|
|
|
|
it.effect("returns incremental tool results plus user message in multi-step flow", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantToolCallMessage("call_1")
|
|
const tool1 = toolResultMessage("call_1")
|
|
const user2 = userMessage("user2")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1, tool1, user2]))
|
|
|
|
assertPreparedSome(prepared, "resp_1", Prompt.fromMessages([tool1, user2]))
|
|
}))
|
|
|
|
it.effect("returns None when no new messages follow assistant", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantMessage("assistant1")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns None in multi-turn prompt with no messages after last assistant", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
const asst2 = assistantMessage("assistant2")
|
|
|
|
tracker.markParts([sys, user1, asst1, user2], "resp_2")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1, user2, asst2]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns None for empty prompt", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns None when system prompt object changes", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const sysNew = systemMessage("sys-new")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sysNew, user1, asst1, user2]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns None when user message object changes", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const user1Edited = userMessage("user1-edited")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1Edited, asst1, user2]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns None when multiple prefix messages diverge", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const sysNew = systemMessage("sys-new")
|
|
const user1Edited = userMessage("user1-edited")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sysNew, user1Edited, asst1, user2]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("clears stale state after divergence and recovers when the new prefix is re-marked", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const sysNew = systemMessage("sys-new")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const diverged = tracker.prepareUnsafe(Prompt.fromMessages([sysNew, user1, asst1, user2]))
|
|
assert.isTrue(Option.isNone(diverged))
|
|
|
|
tracker.markParts([sysNew, user1], "resp_2")
|
|
const recovered = tracker.prepareUnsafe(Prompt.fromMessages([sysNew, user1, asst1, user2]))
|
|
|
|
assertPreparedSome(recovered, "resp_2", Prompt.fromMessages([user2]))
|
|
}))
|
|
|
|
it.effect("supports full lifecycle across two turns", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
const asst2 = assistantMessage("assistant2")
|
|
const user3 = userMessage("user3")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
const firstPrepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1, user2]))
|
|
assertPreparedSome(firstPrepared, "resp_1", Prompt.fromMessages([user2]))
|
|
|
|
tracker.markParts([sys, user1, asst1, user2], "resp_2")
|
|
const secondPrepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1, user2, asst2, user3]))
|
|
assertPreparedSome(secondPrepared, "resp_2", Prompt.fromMessages([user3]))
|
|
}))
|
|
|
|
it.effect("returns None for structurally-equal but identity-different prefix message", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const msg1 = userMessage("msg1")
|
|
const msg1Copy = userMessage("msg1")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const msg2 = userMessage("msg2")
|
|
|
|
tracker.markParts([msg1], "resp_1")
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([msg1Copy, asst1, msg2]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("returns None after explicit clear", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sys = systemMessage("sys")
|
|
const user1 = userMessage("user1")
|
|
const asst1 = assistantMessage("assistant1")
|
|
const user2 = userMessage("user2")
|
|
|
|
tracker.markParts([sys, user1], "resp_1")
|
|
tracker.clearUnsafe()
|
|
const prepared = tracker.prepareUnsafe(Prompt.fromMessages([sys, user1, asst1, user2]))
|
|
|
|
assert.isTrue(Option.isNone(prepared))
|
|
}))
|
|
|
|
it.effect("isolates two conversations on the same tracker", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sysA = systemMessage("sysA")
|
|
const userA1 = userMessage("userA1")
|
|
const asstA1 = assistantMessage("asstA1")
|
|
const userA2 = userMessage("userA2")
|
|
const sysB = systemMessage("sysB")
|
|
const userB1 = userMessage("userB1")
|
|
const asstB1 = assistantMessage("asstB1")
|
|
const userB2 = userMessage("userB2")
|
|
|
|
tracker.markParts([sysA, userA1], "resp_A1")
|
|
tracker.markParts([sysB, userB1], "resp_B1")
|
|
|
|
const preparedA = tracker.prepareUnsafe(Prompt.fromMessages([sysA, userA1, asstA1, userA2]))
|
|
assertPreparedSome(preparedA, "resp_A1", Prompt.fromMessages([userA2]))
|
|
|
|
const preparedB = tracker.prepareUnsafe(Prompt.fromMessages([sysB, userB1, asstB1, userB2]))
|
|
assertPreparedSome(preparedB, "resp_B1", Prompt.fromMessages([userB2]))
|
|
}))
|
|
|
|
it.effect("does not leak tracked state across conversations without assistant turns", () =>
|
|
Effect.gen(function*() {
|
|
const tracker = yield* ResponseIdTracker.make
|
|
const sysA = systemMessage("sysA")
|
|
const userA1 = userMessage("userA1")
|
|
const asstA1 = assistantMessage("asstA1")
|
|
const userA2 = userMessage("userA2")
|
|
const sysB = systemMessage("sysB")
|
|
const userB1 = userMessage("userB1")
|
|
|
|
tracker.markParts([sysA, userA1], "resp_A1")
|
|
tracker.markParts([sysB, userB1], "resp_B1")
|
|
|
|
const preparedA = tracker.prepareUnsafe(Prompt.fromMessages([sysA, userA1, asstA1, userA2]))
|
|
assertPreparedSome(preparedA, "resp_A1", Prompt.fromMessages([userA2]))
|
|
|
|
const preparedB = tracker.prepareUnsafe(Prompt.fromMessages([sysB, userB1]))
|
|
assert.isTrue(Option.isNone(preparedB))
|
|
}))
|
|
})
|