tool-stream.test.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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.event).toEqual({ type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' })
  22. expect(second.event).toEqual({ type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' })
  23. expect(finished).toEqual({
  24. tools: {},
  25. event: { type: "tool-call", id: "call_1", name: "lookup", input: { query: "weather" } },
  26. })
  27. }),
  28. )
  29. it.effect("fails appendExisting when the provider skipped the tool start", () =>
  30. Effect.gen(function* () {
  31. const error = ToolStream.appendExisting(ADAPTER, ToolStream.empty<number>(), 0, "{}", "missing tool")
  32. expect(error).toBeInstanceOf(LLMError)
  33. if (ToolStream.isError(error)) expect(error.reason.message).toBe("missing tool")
  34. }),
  35. )
  36. it.effect("uses final input override without losing accumulated deltas", () =>
  37. Effect.gen(function* () {
  38. const tools = ToolStream.start(ToolStream.empty<string>(), "item_1", {
  39. id: "call_1",
  40. name: "lookup",
  41. input: '{"query":"partial"}',
  42. })
  43. const finished = yield* ToolStream.finishWithInput(ADAPTER, tools, "item_1", '{"query":"final"}')
  44. expect(finished).toEqual({
  45. tools: {},
  46. event: { type: "tool-call", id: "call_1", name: "lookup", input: { query: "final" } },
  47. })
  48. }),
  49. )
  50. it.effect("preserves providerExecuted and clears all tools", () =>
  51. Effect.gen(function* () {
  52. const first: ToolStream.State<number> = ToolStream.start(ToolStream.empty<number>(), 0, {
  53. id: "call_1",
  54. name: "lookup",
  55. input: "{}",
  56. })
  57. const tools = ToolStream.start(first, 1, {
  58. id: "call_2",
  59. name: "web_search",
  60. input: '{"query":"docs"}',
  61. providerExecuted: true,
  62. })
  63. const finished = yield* ToolStream.finishAll(ADAPTER, tools)
  64. expect(finished).toEqual({
  65. tools: {},
  66. events: [
  67. { type: "tool-call", id: "call_1", name: "lookup", input: {} },
  68. {
  69. type: "tool-call",
  70. id: "call_2",
  71. name: "web_search",
  72. input: { query: "docs" },
  73. providerExecuted: true,
  74. },
  75. ],
  76. })
  77. }),
  78. )
  79. })