import { describe, expect } from "bun:test" import { Effect, Layer, Schema } from "effect" import { Agent } from "../../src/agent/agent" import { MessageID, SessionID } from "../../src/session/schema" import { Tool } from "@/tool/tool" import { Truncate } from "@/tool/truncate" import { testEffect } from "../lib/effect" const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer)) const params = Schema.Struct({ input: Schema.String }) function makeTool(id: string, executeFn?: () => void) { return { description: "test tool", parameters: params, execute() { executeFn?.() return Effect.succeed({ title: "test", output: "ok", metadata: {} }) }, } } describe("Tool.define", () => { it.effect("object-defined tool does not mutate the original init object", () => Effect.gen(function* () { const original = makeTool("test") const originalExecute = original.execute const info = yield* Tool.define("test-tool", Effect.succeed(original)) yield* info.init() yield* info.init() yield* info.init() expect(original.execute).toBe(originalExecute) }), ) it.effect("effect-defined tool returns fresh objects and is unaffected", () => Effect.gen(function* () { const info = yield* Tool.define( "test-fn-tool", Effect.succeed(() => Effect.succeed(makeTool("test"))), ) const first = yield* info.init() const second = yield* info.init() expect(first).not.toBe(second) }), ) it.effect("object-defined tool returns distinct objects per init() call", () => Effect.gen(function* () { const info = yield* Tool.define("test-copy", Effect.succeed(makeTool("test"))) const first = yield* info.init() const second = yield* info.init() expect(first).not.toBe(second) }), ) it.effect("execute receives decoded parameters", () => Effect.gen(function* () { const parameters = Schema.Struct({ count: Schema.NumberFromString.pipe(Schema.optional, Schema.withDecodingDefaultType(Effect.succeed(5))), }) const calls: Array> = [] const info = yield* Tool.define( "test-decoded", Effect.succeed({ description: "test tool", parameters, execute(args: Schema.Schema.Type) { calls.push(args) return Effect.succeed({ title: "test", output: "ok", metadata: { truncated: false } }) }, }), ) const ctx: Tool.Context = { sessionID: SessionID.descending(), messageID: MessageID.ascending(), agent: "build", abort: new AbortController().signal, messages: [], metadata() { return Effect.void }, ask() { return Effect.void }, } const tool = yield* info.init() const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType yield* execute({}, ctx) yield* execute({ count: "7" }, ctx) expect(calls).toEqual([{ count: 5 }, { count: 7 }]) }), ) })