tool-stream.test.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { describe, expect } from "bun:test"
  2. import { Effect } from "effect"
  3. import { LLMError } 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(error).toBeInstanceOf(LLMError)
  39. if (ToolStream.isError(error)) expect(error.reason.message).toBe("missing tool")
  40. }),
  41. )
  42. it.effect("uses final input override without losing accumulated deltas", () =>
  43. Effect.gen(function* () {
  44. const tools = ToolStream.start(ToolStream.empty<string>(), "item_1", {
  45. id: "call_1",
  46. name: "lookup",
  47. input: '{"query":"partial"}',
  48. })
  49. const finished = yield* ToolStream.finishWithInput(ADAPTER, tools, "item_1", '{"query":"final"}')
  50. expect(finished).toEqual({
  51. tools: {},
  52. events: [
  53. { type: "tool-input-end", id: "call_1", name: "lookup" },
  54. { type: "tool-call", id: "call_1", name: "lookup", input: { query: "final" } },
  55. ],
  56. })
  57. }),
  58. )
  59. it.effect("preserves providerExecuted and clears all tools", () =>
  60. Effect.gen(function* () {
  61. const first: ToolStream.State<number> = ToolStream.start(ToolStream.empty<number>(), 0, {
  62. id: "call_1",
  63. name: "lookup",
  64. input: "{}",
  65. })
  66. const tools = ToolStream.start(first, 1, {
  67. id: "call_2",
  68. name: "web_search",
  69. input: '{"query":"docs"}',
  70. providerExecuted: true,
  71. })
  72. const finished = yield* ToolStream.finishAll(ADAPTER, tools)
  73. expect(finished).toEqual({
  74. tools: {},
  75. events: [
  76. { type: "tool-input-end", id: "call_1", name: "lookup" },
  77. { type: "tool-call", id: "call_1", name: "lookup", input: {} },
  78. { type: "tool-input-end", id: "call_2", name: "web_search" },
  79. {
  80. type: "tool-call",
  81. id: "call_2",
  82. name: "web_search",
  83. input: { query: "docs" },
  84. providerExecuted: true,
  85. },
  86. ],
  87. })
  88. }),
  89. )
  90. })