tool-stream.test.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { describe, expect } from "bun:test"
  2. import { Effect } from "effect"
  3. import { isLLMError } from "../src/schema"
  4. import { ToolStream } from "../src/protocols/utils/tool-stream"
  5. import { it } from "./lib/effect"
  6. const ADAPTER = "test-route"
  7. describe("ToolStream", () => {
  8. it.effect("starts from OpenAI-style deltas and finalizes parsed input", () =>
  9. Effect.gen(function* () {
  10. const first = ToolStream.appendOrStart(
  11. ADAPTER,
  12. ToolStream.empty<number>(),
  13. 0,
  14. { id: "call_1", name: "lookup", text: '{"query"' },
  15. "missing tool",
  16. )
  17. if (ToolStream.isError(first)) return yield* first
  18. const second = ToolStream.appendOrStart(ADAPTER, first.tools, 0, { text: ':"weather"}' }, "missing tool")
  19. if (ToolStream.isError(second)) return yield* second
  20. const finished = yield* ToolStream.finish(ADAPTER, second.tools, 0)
  21. expect(first.events).toEqual([
  22. { type: "tool-input-start", id: "call_1", name: "lookup" },
  23. { type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' },
  24. ])
  25. expect(second.events).toEqual([{ type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' }])
  26. expect(finished).toEqual({
  27. tools: {},
  28. events: [
  29. { type: "tool-input-end", id: "call_1", name: "lookup" },
  30. { type: "tool-call", id: "call_1", name: "lookup", input: { query: "weather" } },
  31. ],
  32. })
  33. }),
  34. )
  35. it.effect("fails appendExisting when the provider skipped the tool start", () =>
  36. Effect.gen(function* () {
  37. const error = ToolStream.appendExisting(ADAPTER, ToolStream.empty<number>(), 0, "{}", "missing tool")
  38. expect(isLLMError(error)).toBe(true)
  39. if (ToolStream.isError(error))
  40. expect(error).toMatchObject({ _tag: "LLM.MalformedResponse", message: "missing tool" })
  41. }),
  42. )
  43. it.effect("uses final input override without losing accumulated deltas", () =>
  44. Effect.gen(function* () {
  45. const tools = ToolStream.start(ToolStream.empty<string>(), "item_1", {
  46. id: "call_1",
  47. name: "lookup",
  48. input: '{"query":"partial"}',
  49. })
  50. const finished = yield* ToolStream.finishWithInput(ADAPTER, tools, "item_1", '{"query":"final"}')
  51. expect(finished).toEqual({
  52. tools: {},
  53. events: [
  54. { type: "tool-input-end", id: "call_1", name: "lookup" },
  55. { type: "tool-call", id: "call_1", name: "lookup", input: { query: "final" } },
  56. ],
  57. })
  58. }),
  59. )
  60. it.effect("preserves providerExecuted and clears all tools", () =>
  61. Effect.gen(function* () {
  62. const first: ToolStream.State<number> = ToolStream.start(ToolStream.empty<number>(), 0, {
  63. id: "call_1",
  64. name: "lookup",
  65. input: "{}",
  66. })
  67. const tools = ToolStream.start(first, 1, {
  68. id: "call_2",
  69. name: "web_search",
  70. input: '{"query":"docs"}',
  71. providerExecuted: true,
  72. })
  73. const finished = yield* ToolStream.finishAll(ADAPTER, tools)
  74. expect(finished).toEqual({
  75. tools: {},
  76. events: [
  77. { type: "tool-input-end", id: "call_1", name: "lookup" },
  78. { type: "tool-call", id: "call_1", name: "lookup", input: {} },
  79. { type: "tool-input-end", id: "call_2", name: "web_search" },
  80. {
  81. type: "tool-call",
  82. id: "call_2",
  83. name: "web_search",
  84. input: { query: "docs" },
  85. providerExecuted: true,
  86. },
  87. ],
  88. })
  89. }),
  90. )
  91. })