tool-define.test.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { describe, test, expect } from "bun:test"
  2. import { Effect, Layer, ManagedRuntime, Schema } from "effect"
  3. import { Agent } from "../../src/agent/agent"
  4. import { MessageID, SessionID } from "../../src/session/schema"
  5. import { Tool } from "@/tool/tool"
  6. import { Truncate } from "@/tool/truncate"
  7. const runtime = ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))
  8. const params = Schema.Struct({ input: Schema.String })
  9. function makeTool(id: string, executeFn?: () => void) {
  10. return {
  11. description: "test tool",
  12. parameters: params,
  13. execute() {
  14. executeFn?.()
  15. return Effect.succeed({ title: "test", output: "ok", metadata: {} })
  16. },
  17. }
  18. }
  19. describe("Tool.define", () => {
  20. test("object-defined tool does not mutate the original init object", async () => {
  21. const original = makeTool("test")
  22. const originalExecute = original.execute
  23. const info = await runtime.runPromise(Tool.define("test-tool", Effect.succeed(original)))
  24. await Effect.runPromise(info.init())
  25. await Effect.runPromise(info.init())
  26. await Effect.runPromise(info.init())
  27. expect(original.execute).toBe(originalExecute)
  28. })
  29. test("effect-defined tool returns fresh objects and is unaffected", async () => {
  30. const info = await runtime.runPromise(
  31. Tool.define(
  32. "test-fn-tool",
  33. Effect.succeed(() => Effect.succeed(makeTool("test"))),
  34. ),
  35. )
  36. const first = await Effect.runPromise(info.init())
  37. const second = await Effect.runPromise(info.init())
  38. expect(first).not.toBe(second)
  39. })
  40. test("object-defined tool returns distinct objects per init() call", async () => {
  41. const info = await runtime.runPromise(Tool.define("test-copy", Effect.succeed(makeTool("test"))))
  42. const first = await Effect.runPromise(info.init())
  43. const second = await Effect.runPromise(info.init())
  44. expect(first).not.toBe(second)
  45. })
  46. test("execute receives decoded parameters", async () => {
  47. const parameters = Schema.Struct({
  48. count: Schema.NumberFromString.pipe(Schema.optional, Schema.withDecodingDefaultType(Effect.succeed(5))),
  49. })
  50. const calls: Array<Schema.Schema.Type<typeof parameters>> = []
  51. const info = await runtime.runPromise(
  52. Tool.define(
  53. "test-decoded",
  54. Effect.succeed({
  55. description: "test tool",
  56. parameters,
  57. execute(args: Schema.Schema.Type<typeof parameters>) {
  58. calls.push(args)
  59. return Effect.succeed({ title: "test", output: "ok", metadata: { truncated: false } })
  60. },
  61. }),
  62. ),
  63. )
  64. const ctx: Tool.Context = {
  65. sessionID: SessionID.descending(),
  66. messageID: MessageID.ascending(),
  67. agent: "build",
  68. abort: new AbortController().signal,
  69. messages: [],
  70. metadata() {
  71. return Effect.void
  72. },
  73. ask() {
  74. return Effect.void
  75. },
  76. }
  77. const tool = await Effect.runPromise(info.init())
  78. const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType<typeof tool.execute>
  79. await Effect.runPromise(execute({}, ctx))
  80. await Effect.runPromise(execute({ count: "7" }, ctx))
  81. expect(calls).toEqual([{ count: 5 }, { count: 7 }])
  82. })
  83. })