refactor(tools): unify tool APIs and result handling (#38367)
This commit is contained in:
parent
8cac010bac
commit
79c1544072
133 changed files with 3602 additions and 2770 deletions
|
|
@ -27,7 +27,7 @@ const echo = Tool.make({
|
|||
description: "Echo the input",
|
||||
input: Schema.Struct({ id: Schema.Number }),
|
||||
output: Schema.Number,
|
||||
run: (input: { id: number }) => Effect.succeed(input.id),
|
||||
execute: (input: { id: number }) => Effect.succeed(input.id),
|
||||
})
|
||||
const withTool = (code: string) => Effect.runPromise(CodeMode.make({ tools: { host: { echo } } }).execute(code))
|
||||
const toolError = async (code: string) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||
import { Cause, Effect, Schema } from "effect"
|
||||
import { CodeMode, Tool, toolError } from "../src/index.js"
|
||||
|
||||
const run = (tool: Tool.Definition<never>) =>
|
||||
const run = (tool: Tool.Tool<never>) =>
|
||||
Effect.runPromise(CodeMode.make({ tools: { host: { call: tool } } }).execute("return await tools.host.call({})"))
|
||||
|
||||
class UnsafeHostError extends Schema.TaggedErrorClass<UnsafeHostError>()("UnsafeHostError", {
|
||||
|
|
@ -16,7 +16,7 @@ describe("CodeMode host failure boundary", () => {
|
|||
description: "Fail safely",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.fail(toolError("Authorized request was refused")),
|
||||
execute: () => Effect.fail(toolError("Authorized request was refused")),
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ describe("CodeMode host failure boundary", () => {
|
|||
description: "Fail safely",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.fail(toolError("File not found: /tmp/report.json")),
|
||||
execute: () => Effect.fail(toolError("File not found: /tmp/report.json")),
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ describe("CodeMode host failure boundary", () => {
|
|||
description: "Fail internally",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => failure,
|
||||
execute: () => failure,
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ describe("CodeMode host failure boundary", () => {
|
|||
description: "Return invalid output",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ safe: Schema.String }),
|
||||
run: () => Effect.succeed({ safe: 1, secret } as unknown as { readonly safe: string }),
|
||||
execute: () => Effect.succeed({ safe: 1, secret } as unknown as { readonly safe: string }),
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ describe("CodeMode host failure boundary", () => {
|
|||
description: "Return hostile output",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Unknown,
|
||||
run: () =>
|
||||
execute: () =>
|
||||
Effect.succeed(
|
||||
new Proxy(
|
||||
{},
|
||||
|
|
@ -118,7 +118,7 @@ describe("CodeMode host failure boundary", () => {
|
|||
description: "Refuse",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.fail(toolError("Refused")),
|
||||
execute: () => Effect.fail(toolError("Refused")),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
@ -145,7 +145,7 @@ describe("CodeMode host failure boundary", () => {
|
|||
description: "Interrupt",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.interrupt,
|
||||
execute: () => Effect.interrupt,
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
@ -166,7 +166,7 @@ describe("CodeMode tool-call observation", () => {
|
|||
description: "Look up a value",
|
||||
input: Schema.Struct({ query: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: ({ query }) => Effect.succeed(query),
|
||||
execute: ({ query }) => Effect.succeed(query),
|
||||
})
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
|
|
@ -189,7 +189,7 @@ describe("CodeMode tool-call observation", () => {
|
|||
description: "Look up a value",
|
||||
input: Schema.Struct({ query: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)),
|
||||
execute: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)),
|
||||
})
|
||||
|
||||
const runtime = CodeMode.make({
|
||||
|
|
@ -430,7 +430,7 @@ describe("CodeMode schema flexibility", () => {
|
|||
properties: { id: { type: "string" }, count: { type: "number" } },
|
||||
required: ["id"],
|
||||
},
|
||||
run: (input) =>
|
||||
execute: (input) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(input)
|
||||
return { echoed: input }
|
||||
|
|
@ -442,14 +442,14 @@ describe("CodeMode schema flexibility", () => {
|
|||
{
|
||||
path: "adapter.call",
|
||||
description: "Call an adapter-described tool",
|
||||
signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise<unknown>",
|
||||
signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise<void>",
|
||||
},
|
||||
])
|
||||
|
||||
// JSON Schema is render-only: mistyped input passes through unvalidated.
|
||||
const result = await Effect.runPromise(runtime.execute(`return await tools.adapter.call({ id: 42 })`))
|
||||
expect(result.ok).toBe(true)
|
||||
if (result.ok) expect(result.value).toStrictEqual({ echoed: { id: 42 } })
|
||||
if (result.ok) expect(result.value).toBeNull()
|
||||
expect(observed).toStrictEqual([{ id: 42 }])
|
||||
})
|
||||
|
||||
|
|
@ -458,7 +458,7 @@ describe("CodeMode schema flexibility", () => {
|
|||
const call = Tool.make({
|
||||
description: "Observe raw input",
|
||||
input: { type: "object" },
|
||||
run: (input) =>
|
||||
execute: (input) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(input)
|
||||
return "ok"
|
||||
|
|
@ -483,7 +483,7 @@ describe("CodeMode schema flexibility", () => {
|
|||
const find = Tool.make({
|
||||
description: "Find things",
|
||||
input: Schema.Struct({ query: Schema.optionalKey(Schema.String), limit: Schema.optionalKey(Schema.Number) }),
|
||||
run: (input) =>
|
||||
execute: (input) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(input)
|
||||
return "ok"
|
||||
|
|
@ -517,7 +517,7 @@ describe("CodeMode schema flexibility", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
run: () => Effect.succeed({ login: "kit", id: 7 }),
|
||||
execute: () => Effect.succeed({ login: "kit", id: 7 }),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { users: { lookup } } })
|
||||
|
||||
|
|
@ -534,18 +534,18 @@ describe("CodeMode schema flexibility", () => {
|
|||
if (result.ok) expect(result.value).toStrictEqual({ login: "kit", id: 7 })
|
||||
})
|
||||
|
||||
test("Effect Schema output without an input transform still renders unknown when omitted", async () => {
|
||||
test("Effect Schema output without an input transform renders void when omitted", async () => {
|
||||
const ping = Tool.make({
|
||||
description: "Ping",
|
||||
input: Schema.Struct({ host: Schema.String }),
|
||||
run: () => Effect.succeed("pong"),
|
||||
execute: () => Effect.succeed("pong"),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { net: { ping } } })
|
||||
expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise<unknown>")
|
||||
expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise<void>")
|
||||
|
||||
const result = await Effect.runPromise(runtime.execute(`return await tools.net.ping({ host: "example.test" })`))
|
||||
expect(result.ok).toBe(true)
|
||||
if (result.ok) expect(result.value).toBe("pong")
|
||||
if (result.ok) expect(result.value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -554,7 +554,7 @@ describe("CodeMode public contract", () => {
|
|||
description: "Look up an order by ID",
|
||||
input: Schema.Struct({ id: Schema.String }),
|
||||
output: Schema.Struct({ id: Schema.String, status: Schema.String }),
|
||||
run: ({ id }) => Effect.succeed({ id, status: "open" }),
|
||||
execute: ({ id }) => Effect.succeed({ id, status: "open" }),
|
||||
})
|
||||
const tools = { orders: { lookup } }
|
||||
const source = `return await tools.orders.lookup({ id: "order_42" })`
|
||||
|
|
@ -577,7 +577,7 @@ describe("CodeMode public contract", () => {
|
|||
description: "echo",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Number,
|
||||
run: () => Effect.succeed(1),
|
||||
execute: () => Effect.succeed(1),
|
||||
})
|
||||
const effect = CodeMode.execute({
|
||||
tools: { host: { echo } },
|
||||
|
|
@ -634,7 +634,7 @@ describe("CodeMode public contract", () => {
|
|||
description: "Resolve a library ID",
|
||||
input: Schema.Struct({ libraryName: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: ({ libraryName }) => Effect.succeed(`/resolved/${libraryName}`),
|
||||
execute: ({ libraryName }) => Effect.succeed(`/resolved/${libraryName}`),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||
|
||||
|
|
@ -760,18 +760,18 @@ describe("CodeMode public contract", () => {
|
|||
expect(instructions).not.toContain("search(")
|
||||
})
|
||||
|
||||
test("uses one ranked search returning complete definitions for large catalogs", async () => {
|
||||
test("uses one ranked search returning complete tools for large catalogs", async () => {
|
||||
const upload = Tool.make({
|
||||
description: "Upload one readable local file to the current Discord thread",
|
||||
input: Schema.Struct({ path: Schema.String }),
|
||||
output: Schema.Struct({ sent: Schema.Boolean }),
|
||||
run: () => Effect.succeed({ sent: true }),
|
||||
execute: () => Effect.succeed({ sent: true }),
|
||||
})
|
||||
const generate = Tool.make({
|
||||
description: "Generate an image and upload it to the current Discord thread",
|
||||
input: Schema.Struct({ prompt: Schema.String }),
|
||||
output: Schema.Struct({ sent: Schema.Boolean }),
|
||||
run: () => Effect.succeed({ sent: true }),
|
||||
execute: () => Effect.succeed({ sent: true }),
|
||||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } },
|
||||
|
|
@ -865,7 +865,7 @@ describe("CodeMode public contract", () => {
|
|||
description: `Numbered tool ${index}`,
|
||||
input: Schema.Struct({ id: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
|
|
@ -911,7 +911,7 @@ describe("CodeMode public contract", () => {
|
|||
description,
|
||||
input: Schema.Struct({ id: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
|
|
@ -954,13 +954,13 @@ describe("CodeMode public contract", () => {
|
|||
properties: { attachment: { type: "string", description: "Local path of the payload to send" } },
|
||||
required: ["attachment"],
|
||||
},
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const other = Tool.make({
|
||||
description: "Rename the workspace",
|
||||
input: Schema.Struct({ name: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { files: { upload, other } } })
|
||||
|
||||
|
|
@ -990,7 +990,7 @@ describe("CodeMode public contract", () => {
|
|||
description,
|
||||
input: Schema.Struct({ id: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
|
|
@ -1029,7 +1029,7 @@ describe("CodeMode public contract", () => {
|
|||
description,
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
// Deliberately declared out of alphabetical order.
|
||||
const runtime = CodeMode.make({
|
||||
|
|
@ -1071,7 +1071,7 @@ describe("CodeMode public contract", () => {
|
|||
description: "Cheap",
|
||||
input: Schema.Struct({ q: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const expensive = Tool.make({
|
||||
description:
|
||||
|
|
@ -1081,7 +1081,7 @@ describe("CodeMode public contract", () => {
|
|||
anotherEvenLongerParameterName: Schema.Number,
|
||||
}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
// Round 1 places alpha.cheap (~17 estimated tokens) and beta.cheap (~17); in round 2
|
||||
// alpha.expensive does not fit, which marks only alpha done - it must NOT prevent
|
||||
|
|
@ -1112,7 +1112,7 @@ describe("CodeMode public contract", () => {
|
|||
},
|
||||
required: ["id"],
|
||||
} as const,
|
||||
run: () => Effect.succeed("ok"),
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: { records: { lookup: documented } },
|
||||
|
|
@ -1130,7 +1130,7 @@ describe("CodeMode public contract", () => {
|
|||
description: "Double a number",
|
||||
input: Schema.Struct({ value: Schema.NumberFromString }),
|
||||
output: Schema.NumberFromString,
|
||||
run: ({ value }) =>
|
||||
execute: ({ value }) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(value)
|
||||
return String(value * 2)
|
||||
|
|
@ -1226,7 +1226,7 @@ describe("CodeMode public contract", () => {
|
|||
description: "Count invocations",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Number,
|
||||
run: () => Effect.succeed(1),
|
||||
execute: () => Effect.succeed(1),
|
||||
})
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const echo = (description: string) =>
|
|||
description,
|
||||
input: Schema.Struct({ value: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: ({ value }) => Effect.succeed(value),
|
||||
execute: ({ value }) => Effect.succeed(value),
|
||||
})
|
||||
|
||||
const tools = {
|
||||
|
|
|
|||
|
|
@ -143,12 +143,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
const remove = toolAt(api.tools, "users.remove")
|
||||
|
||||
expect(api.skipped).toEqual([])
|
||||
if (
|
||||
!Tool.isDefinition(get) ||
|
||||
!Tool.isDefinition(create) ||
|
||||
!Tool.isDefinition(search) ||
|
||||
!Tool.isDefinition(remove)
|
||||
) {
|
||||
if (!Tool.isTool(get) || !Tool.isTool(create) || !Tool.isTool(search) || !Tool.isTool(remove)) {
|
||||
throw new Error("happy-path fixture did not generate every operation")
|
||||
}
|
||||
expect(inputTypeScript(get)).toBe(
|
||||
|
|
@ -241,23 +236,23 @@ describe("OpenAPI.fromSpec", () => {
|
|||
expect(toolAt(result.tools, "v2.session.create")).not.toBeUndefined()
|
||||
|
||||
const sessionGet = toolAt(result.tools, "v2.session.get")
|
||||
expect(Tool.isDefinition(sessionGet)).toBe(true)
|
||||
if (!Tool.isDefinition(sessionGet)) throw new Error("v2.session.get was not generated")
|
||||
expect(Tool.isTool(sessionGet)).toBe(true)
|
||||
if (!Tool.isTool(sessionGet)) throw new Error("v2.session.get was not generated")
|
||||
expect(inputTypeScript(sessionGet)).toBe("{ sessionID: string }")
|
||||
expect(outputTypeScript(sessionGet)).toContain("id: string")
|
||||
expect(outputTypeScript(sessionGet)).toContain("additions: number")
|
||||
|
||||
const switchAgent = toolAt(result.tools, "v2.session.switchAgent")
|
||||
expect(Tool.isDefinition(switchAgent)).toBe(true)
|
||||
if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated")
|
||||
expect(Tool.isTool(switchAgent)).toBe(true)
|
||||
if (!Tool.isTool(switchAgent)) throw new Error("v2.session.switchAgent was not generated")
|
||||
expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }")
|
||||
|
||||
const instructionPut = toolAt(result.tools, "v2.session.instructions.entry.put")
|
||||
expect(Tool.isDefinition(instructionPut)).toBe(true)
|
||||
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
|
||||
expect(Tool.isTool(instructionPut)).toBe(true)
|
||||
if (!Tool.isTool(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
|
||||
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
|
||||
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
|
||||
expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false)
|
||||
expect(Tool.isTool(toolAt(result.tools, "v2.pty.connect"))).toBe(false)
|
||||
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
|
||||
|
|
@ -278,9 +273,9 @@ describe("OpenAPI.fromSpec", () => {
|
|||
},
|
||||
})
|
||||
|
||||
expect(Tool.isDefinition(toolAt(result.tools, "group.item"))).toBe(true)
|
||||
expect(Tool.isDefinition(toolAt(result.tools, "group_item_2"))).toBe(true)
|
||||
expect(Tool.isDefinition(toolAt(result.tools, "group.operation.other"))).toBe(true)
|
||||
expect(Tool.isTool(toolAt(result.tools, "group.item"))).toBe(true)
|
||||
expect(Tool.isTool(toolAt(result.tools, "group_item_2"))).toBe(true)
|
||||
expect(Tool.isTool(toolAt(result.tools, "group.operation.other"))).toBe(true)
|
||||
})
|
||||
|
||||
test("synthesizes flat operation IDs from methods and paths", () => {
|
||||
|
|
@ -305,7 +300,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
"deleteUsersById",
|
||||
"getOrganizationsByOrganizationidUsersById",
|
||||
]) {
|
||||
expect(Tool.isDefinition(toolAt(tools, path))).toBe(true)
|
||||
expect(Tool.isTool(toolAt(tools, path))).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -330,7 +325,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
"test",
|
||||
)
|
||||
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
expect(inputTypeScript(tool)).toBe("{ limit: number }")
|
||||
})
|
||||
|
||||
|
|
@ -358,8 +353,8 @@ describe("OpenAPI.fromSpec", () => {
|
|||
})
|
||||
const search = toolAt(result.tools, "search")
|
||||
|
||||
expect(Tool.isDefinition(search)).toBe(true)
|
||||
if (!Tool.isDefinition(search)) throw new Error("search was not generated")
|
||||
expect(Tool.isTool(search)).toBe(true)
|
||||
if (!Tool.isTool(search)) throw new Error("search was not generated")
|
||||
expect(inputTypeScript(search)).toBe("{ value?: string | null }")
|
||||
const schema: unknown = search.input
|
||||
const input = isRecord(schema) ? schema : {}
|
||||
|
|
@ -397,14 +392,14 @@ describe("OpenAPI.fromSpec", () => {
|
|||
"test",
|
||||
)
|
||||
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.output)) throw new Error("test output was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.output)) throw new Error("test output was not generated")
|
||||
expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } })
|
||||
})
|
||||
|
||||
test("projects read-only and write-only properties by schema direction", () => {
|
||||
for (const version of ["3.0.3", "3.1.0"]) {
|
||||
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec(version) }).tools, "users.create")
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input) || !isRecord(tool.output)) {
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input) || !isRecord(tool.output)) {
|
||||
throw new Error(`users.create was not generated for OpenAPI ${version}`)
|
||||
}
|
||||
|
||||
|
|
@ -467,7 +462,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
expect(inputTypeScript(tool)).toBe("{ name: string }")
|
||||
})
|
||||
|
|
@ -518,7 +513,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
const properties = isRecord(tool.input.properties) ? tool.input.properties : {}
|
||||
const record = isRecord(properties.record) ? properties.record : {}
|
||||
const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {}
|
||||
|
|
@ -567,7 +562,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
expect(inputTypeScript(tool)).toBe("{ name: string }")
|
||||
})
|
||||
|
|
@ -607,7 +602,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {}
|
||||
const node = isRecord(definitions.Node) ? definitions.Node : {}
|
||||
|
||||
|
|
@ -648,7 +643,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {}
|
||||
const leaf = isRecord(definitions[`C${depth - 1}`]) ? definitions[`C${depth - 1}`] : {}
|
||||
|
||||
|
|
@ -686,7 +681,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
expect(inputTypeScript(tool)).toBe("{ name: string }")
|
||||
}
|
||||
|
|
@ -725,7 +720,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
const properties = isRecord(tool.input.properties) ? tool.input.properties : {}
|
||||
const record: Record<string, unknown> = isRecord(properties.record) ? properties.record : {}
|
||||
|
||||
|
|
@ -763,7 +758,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
const properties = isRecord(tool.input.properties) ? tool.input.properties : {}
|
||||
const choice: Record<string, unknown> = isRecord(properties.choice) ? properties.choice : {}
|
||||
const pick: Record<string, unknown> = isRecord(properties.pick) ? properties.pick : {}
|
||||
|
|
@ -807,7 +802,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
const properties = isRecord(tool.input.properties) ? tool.input.properties : {}
|
||||
const record = isRecord(properties.record) ? properties.record : {}
|
||||
|
||||
|
|
@ -836,7 +831,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
expect(inputTypeScript(tool)).toBe("{ filter: { state: string } }")
|
||||
})
|
||||
|
|
@ -866,7 +861,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
expect(inputTypeScript(tool)).toBe("{ filter: { value: string } }")
|
||||
})
|
||||
|
|
@ -901,7 +896,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated")
|
||||
const properties = isRecord(tool.input.properties) ? tool.input.properties : {}
|
||||
const body = isRecord(properties.body) ? properties.body : {}
|
||||
const allOf = Array.isArray(body.allOf) ? body.allOf : []
|
||||
|
|
@ -923,11 +918,11 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}),
|
||||
)
|
||||
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec("3.1.0") }).tools, "users.create")
|
||||
if (!Tool.isDefinition(tool)) throw new Error("users.create was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("users.create was not generated")
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
tool
|
||||
.run({
|
||||
.execute({
|
||||
id: "ignored-top-level",
|
||||
generated: "ignored-generated",
|
||||
name: "Ada",
|
||||
|
|
@ -1022,10 +1017,12 @@ describe("OpenAPI.fromSpec", () => {
|
|||
test("serializes deep-object query parameters from the opencode fixture", async () => {
|
||||
const client = recordingClient(() => json({ directory: "/tmp" }))
|
||||
const location = toolAt(OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools, "v2.location.get")
|
||||
if (!Tool.isDefinition(location)) throw new Error("v2.location.get was not generated")
|
||||
if (!Tool.isTool(location)) throw new Error("v2.location.get was not generated")
|
||||
|
||||
await Effect.runPromise(
|
||||
location.run({ location: { directory: "/tmp", workspace: "workspace-1" } }).pipe(Effect.provide(client.layer)),
|
||||
location
|
||||
.execute({ location: { directory: "/tmp", workspace: "workspace-1" } })
|
||||
.pipe(Effect.provide(client.layer)),
|
||||
)
|
||||
|
||||
const url = new URL(client.requests[0]!.url)
|
||||
|
|
@ -1058,11 +1055,11 @@ describe("OpenAPI.fromSpec", () => {
|
|||
},
|
||||
})
|
||||
const tool = toolAt(result.tools, "items")
|
||||
if (!Tool.isDefinition(tool)) throw new Error("items was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("items was not generated")
|
||||
|
||||
await Effect.runPromise(
|
||||
tool
|
||||
.run({
|
||||
.execute({
|
||||
keys: ["a!", "b*"],
|
||||
tags: ["x", "y"],
|
||||
filter: { state: "open", page: 2 },
|
||||
|
|
@ -1081,9 +1078,9 @@ describe("OpenAPI.fromSpec", () => {
|
|||
expect(url.searchParams.get("nullable")).toBe("null")
|
||||
expect(url.searchParams.get("constructor")).toBe("safe")
|
||||
expect(client.requests[0]!.headers.meta).toBe("a=b,c=d")
|
||||
await expect(Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
"unsupported nested value",
|
||||
)
|
||||
await expect(
|
||||
Effect.runPromise(tool.execute({ keys: [undefined] }).pipe(Effect.provide(client.layer))),
|
||||
).rejects.toThrow("unsupported nested value")
|
||||
})
|
||||
|
||||
test("preserves ordered exploded and deep-object query parameters", async () => {
|
||||
|
|
@ -1101,11 +1098,11 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
await Effect.runPromise(
|
||||
tool
|
||||
.run({
|
||||
.execute({
|
||||
tags: ["first value", "second&value"],
|
||||
filter: { state: "open now", page: 2 },
|
||||
location: { directory: "/tmp/a b", workspace: "work&1" },
|
||||
|
|
@ -1116,14 +1113,14 @@ describe("OpenAPI.fromSpec", () => {
|
|||
expect(client.requests[0]?.url).toBe(
|
||||
`${baseUrl}/test?tags=first+value&tags=second%26value&state=open+now&page=2&location%5Bdirectory%5D=%2Ftmp%2Fa+b&location%5Bworkspace%5D=work%261`,
|
||||
)
|
||||
await expect(Effect.runPromise(tool.run({ tags: [{}] }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
await expect(Effect.runPromise(tool.execute({ tags: [{}] }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
"Parameter 'tags' contains an unsupported nested value.",
|
||||
)
|
||||
await expect(
|
||||
Effect.runPromise(tool.run({ filter: { state: {} } }).pipe(Effect.provide(client.layer))),
|
||||
Effect.runPromise(tool.execute({ filter: { state: {} } }).pipe(Effect.provide(client.layer))),
|
||||
).rejects.toThrow("Query parameter 'filter' contains an unsupported nested value.")
|
||||
await expect(
|
||||
Effect.runPromise(tool.run({ location: { directory: [] } }).pipe(Effect.provide(client.layer))),
|
||||
Effect.runPromise(tool.execute({ location: { directory: [] } }).pipe(Effect.provide(client.layer))),
|
||||
).rejects.toThrow("Deep-object parameter 'location' contains an unsupported nested value.")
|
||||
expect(client.requests).toHaveLength(1)
|
||||
})
|
||||
|
|
@ -1203,9 +1200,9 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"getTest",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))
|
||||
await Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer)))
|
||||
|
||||
expect(inputTypeScript(tool)).toBe("{}")
|
||||
expect(client.requests[0]!.headers.authorization).toBe("Bearer secret")
|
||||
|
|
@ -1240,9 +1237,9 @@ describe("OpenAPI.fromSpec", () => {
|
|||
authenticated([{ key: [] }], { key: { type: "apiKey", in: "query", name: "__proto__" } }).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(prototype)) throw new Error("prototype auth tool was not generated")
|
||||
if (!Tool.isTool(prototype)) throw new Error("prototype auth tool was not generated")
|
||||
|
||||
await Effect.runPromise(prototype.run({}).pipe(Effect.provide(client.layer)))
|
||||
await Effect.runPromise(prototype.execute({}).pipe(Effect.provide(client.layer)))
|
||||
expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret")
|
||||
|
||||
const duplicate = toolAt(
|
||||
|
|
@ -1252,8 +1249,8 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated")
|
||||
await expect(Effect.runPromise(duplicate.run({}).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
if (!Tool.isTool(duplicate)) throw new Error("duplicate auth tool was not generated")
|
||||
await expect(Effect.runPromise(duplicate.execute({}).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
"multiple credentials",
|
||||
)
|
||||
|
||||
|
|
@ -1278,8 +1275,8 @@ describe("OpenAPI.fromSpec", () => {
|
|||
},
|
||||
})
|
||||
const alternativeTool = toolAt(alternative.tools, "test")
|
||||
if (!Tool.isDefinition(alternativeTool)) throw new Error("supported auth alternative was not generated")
|
||||
await Effect.runPromise(alternativeTool.run({}).pipe(Effect.provide(client.layer)))
|
||||
if (!Tool.isTool(alternativeTool)) throw new Error("supported auth alternative was not generated")
|
||||
await Effect.runPromise(alternativeTool.execute({}).pipe(Effect.provide(client.layer)))
|
||||
expect(client.requests.at(-1)?.headers.authorization).toBe("Bearer secret")
|
||||
})
|
||||
|
||||
|
|
@ -1290,9 +1287,9 @@ describe("OpenAPI.fromSpec", () => {
|
|||
servers: [{ url: "https://document.example" }],
|
||||
} satisfies Document
|
||||
const tool = toolAt(OpenAPI.fromSpec({ spec }).tools, "test")
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))
|
||||
await Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer)))
|
||||
expect(client.requests[0]?.url).toBe("https://operation.example/v1/test")
|
||||
|
||||
const invalid = OpenAPI.fromSpec({ spec, baseUrl: "https://example.com/api?tenant=one" })
|
||||
|
|
@ -1363,10 +1360,10 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
await expect(
|
||||
Effect.runPromise(tool.run({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))),
|
||||
Effect.runPromise(tool.execute({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))),
|
||||
).rejects.toThrow("unsupported nested value")
|
||||
expect(resolutions).toEqual([])
|
||||
expect(client.requests).toEqual([])
|
||||
|
|
@ -1389,33 +1386,33 @@ describe("OpenAPI.fromSpec", () => {
|
|||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
|
||||
await Effect.runPromise(tool.run({ body: { name: "updated" } }).pipe(Effect.provide(client.layer)))
|
||||
await Effect.runPromise(tool.execute({ body: { name: "updated" } }).pipe(Effect.provide(client.layer)))
|
||||
expect(client.requests[0]!.headers["content-type"]).toBe("application/merge-patch+json")
|
||||
const cyclic: Record<string, unknown> = {}
|
||||
cyclic.self = cyclic
|
||||
await expect(Effect.runPromise(tool.run({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
await expect(Effect.runPromise(tool.execute({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
"Invalid JSON body",
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects oversized and malformed JSON responses", async () => {
|
||||
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: singleOperation({}) }).tools, "test")
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
const oversized = recordingClient(
|
||||
() => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }),
|
||||
)
|
||||
const malformed = recordingClient(() => new Response("{", { headers: { "content-type": "application/json" } }))
|
||||
const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1)))
|
||||
|
||||
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow(
|
||||
await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow(
|
||||
"response exceeds 50 MiB",
|
||||
)
|
||||
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow(
|
||||
await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow(
|
||||
"returned malformed JSON",
|
||||
)
|
||||
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow(
|
||||
await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow(
|
||||
"response exceeds 50 MiB",
|
||||
)
|
||||
})
|
||||
|
|
@ -1428,11 +1425,11 @@ describe("OpenAPI.fromSpec", () => {
|
|||
},
|
||||
})
|
||||
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec }).tools, "test")
|
||||
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
|
||||
if (!Tool.isTool(tool)) throw new Error("test was not generated")
|
||||
const client = recordingClient(() => new Response("123", { headers: { "content-type": "text/plain" } }))
|
||||
|
||||
expect(outputTypeScript(tool)).toBe("string | null")
|
||||
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123")
|
||||
await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123")
|
||||
})
|
||||
|
||||
test("fails missing required parameters before auth and network", async () => {
|
||||
|
|
@ -1497,13 +1494,13 @@ describe("OpenAPI.fromSpec", () => {
|
|||
const update = toolAt(tools, "things.update")
|
||||
const echo = toolAt(tools, "echo")
|
||||
|
||||
expect(Tool.isDefinition(update)).toBe(true)
|
||||
if (!Tool.isDefinition(update)) throw new Error("things.update was not generated")
|
||||
expect(Tool.isTool(update)).toBe(true)
|
||||
if (!Tool.isTool(update)) throw new Error("things.update was not generated")
|
||||
expect(inputTypeScript(update)).toBe(
|
||||
"{ path_id: string; query_id: string; path_id_2?: string; header_id: string; body_id: string }",
|
||||
)
|
||||
expect(Tool.isDefinition(echo)).toBe(true)
|
||||
if (!Tool.isDefinition(echo)) throw new Error("echo was not generated")
|
||||
expect(Tool.isTool(echo)).toBe(true)
|
||||
if (!Tool.isTool(echo)) throw new Error("echo was not generated")
|
||||
expect(inputTypeScript(echo)).toBe("{ body: string }")
|
||||
|
||||
const runtime = CodeMode.make({ tools })
|
||||
|
|
@ -1584,13 +1581,13 @@ describe("OpenAPI.fromSpec", () => {
|
|||
|
||||
for (const name of ["optional", "dictionary", "composed", "nullable"]) {
|
||||
const tool = toolAt(tools, `body.${name}`)
|
||||
expect(Tool.isDefinition(tool)).toBe(true)
|
||||
if (!Tool.isDefinition(tool)) throw new Error(`body.${name} was not generated`)
|
||||
expect(Tool.isTool(tool)).toBe(true)
|
||||
if (!Tool.isTool(tool)) throw new Error(`body.${name} was not generated`)
|
||||
const input = isRecord(tool.input) ? tool.input : {}
|
||||
expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual(["body"])
|
||||
}
|
||||
const optional = toolAt(tools, "body.optional")
|
||||
if (!Tool.isDefinition(optional)) throw new Error("body.optional was not generated")
|
||||
if (!Tool.isTool(optional)) throw new Error("body.optional was not generated")
|
||||
expect(inputTypeScript(optional)).toBe("{ body?: { name: string } }")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const echoTool = (trace: Trace) =>
|
|||
description: "Echo an id immediately",
|
||||
input: Schema.Struct({ id: Schema.Number }),
|
||||
output: Schema.Number,
|
||||
run: ({ id }) =>
|
||||
execute: ({ id }) =>
|
||||
Effect.sync(() => {
|
||||
trace.starts.push(id)
|
||||
trace.completed += 1
|
||||
|
|
@ -46,7 +46,7 @@ const gatedTool = (trace: Trace, gate: (id: number) => Deferred.Deferred<void>)
|
|||
description: "Echo an id once its gate opens",
|
||||
input: Schema.Struct({ id: Schema.Number }),
|
||||
output: Schema.Number,
|
||||
run: ({ id }) =>
|
||||
execute: ({ id }) =>
|
||||
Effect.gen(function* () {
|
||||
trace.starts.push(id)
|
||||
trace.active += 1
|
||||
|
|
@ -70,7 +70,7 @@ const openTool = (gate: (id: number) => Deferred.Deferred<void>) =>
|
|||
description: "Open the gate for an id",
|
||||
input: Schema.Struct({ id: Schema.Number }),
|
||||
output: Schema.Boolean,
|
||||
run: ({ id }) => Deferred.succeed(gate(id), undefined),
|
||||
execute: ({ id }) => Deferred.succeed(gate(id), undefined),
|
||||
})
|
||||
|
||||
const pendingTool = (trace: Trace) =>
|
||||
|
|
@ -78,7 +78,7 @@ const pendingTool = (trace: Trace) =>
|
|||
description: "Never settle",
|
||||
input: Schema.Struct({ id: Schema.Number }),
|
||||
output: Schema.Number,
|
||||
run: ({ id }) =>
|
||||
execute: ({ id }) =>
|
||||
Effect.gen(function* () {
|
||||
trace.starts.push(id)
|
||||
trace.active += 1
|
||||
|
|
@ -98,14 +98,14 @@ const failingTool = Tool.make({
|
|||
description: "Always refuse",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.fail(toolError("Lookup refused")),
|
||||
execute: () => Effect.fail(toolError("Lookup refused")),
|
||||
})
|
||||
|
||||
const interruptedTool = Tool.make({
|
||||
description: "Interrupt this call",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.interrupt,
|
||||
execute: () => Effect.interrupt,
|
||||
})
|
||||
|
||||
const completedTool = (trace: Trace) =>
|
||||
|
|
@ -113,7 +113,7 @@ const completedTool = (trace: Trace) =>
|
|||
description: "Return the number of completed calls",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Number,
|
||||
run: () => Effect.succeed(trace.completed),
|
||||
execute: () => Effect.succeed(trace.completed),
|
||||
})
|
||||
|
||||
/** Never settles, and holds interruption cleanup for `cleanupMs` so completion cleanup can outlast a timeout. */
|
||||
|
|
@ -122,7 +122,7 @@ const stubbornTool = (trace: Trace) =>
|
|||
description: "Never settle; clean up slowly when interrupted",
|
||||
input: Schema.Struct({ cleanupMs: Schema.Number }),
|
||||
output: Schema.Number,
|
||||
run: ({ cleanupMs }) =>
|
||||
execute: ({ cleanupMs }) =>
|
||||
Effect.never.pipe(
|
||||
Effect.onInterrupt(() =>
|
||||
Effect.andThen(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ const listIssues = Tool.make({
|
|||
},
|
||||
required: ["owner"],
|
||||
},
|
||||
run: () => Effect.succeed("[]"),
|
||||
output: {},
|
||||
execute: () => Effect.succeed("[]"),
|
||||
})
|
||||
|
||||
// An Effect Schema tool whose field annotations must flow through the emitted JSON Schema.
|
||||
|
|
@ -31,7 +32,7 @@ const lookupOrder = Tool.make({
|
|||
output: Schema.Struct({
|
||||
status: Schema.String.annotate({ description: "Current order status" }),
|
||||
}),
|
||||
run: () => Effect.succeed({ status: "open" }),
|
||||
execute: () => Effect.succeed({ status: "open" }),
|
||||
})
|
||||
|
||||
describe("pretty signature rendering", () => {
|
||||
|
|
@ -261,7 +262,7 @@ describe("non-identifier property names render as quoted keys", () => {
|
|||
properties: { "content-type": { type: "string" } },
|
||||
required: ["content-type"],
|
||||
} as const,
|
||||
run: () => Effect.succeed({ "content-type": "text/plain" }),
|
||||
execute: () => Effect.succeed({ "content-type": "text/plain" }),
|
||||
})
|
||||
expect(inputTypeScript(tool)).toContain('"foo-bar"?: string')
|
||||
expect(outputTypeScript(tool)).toBe('{ "content-type": string }')
|
||||
|
|
@ -272,7 +273,7 @@ describe("non-identifier property names render as quoted keys", () => {
|
|||
const tool = Tool.make({
|
||||
description: "Schema tool with awkward field names",
|
||||
input: Schema.Struct({ "foo-bar": Schema.String, plain: Schema.optionalKey(Schema.Number) }),
|
||||
run: () => Effect.succeed(null),
|
||||
execute: () => Effect.succeed(null),
|
||||
})
|
||||
expect(inputTypeScript(tool)).toBe('{ "foo-bar": string; plain?: number }')
|
||||
expect(inputTypeScript(tool, true)).toBe(["{", ' "foo-bar": string,', " plain?: number,", "}"].join("\n"))
|
||||
|
|
@ -306,7 +307,7 @@ describe("union schemas render every alternative", () => {
|
|||
},
|
||||
} as const,
|
||||
output: { anyOf: [{ type: "number" }, { type: "boolean" }] } as const,
|
||||
run: () => Effect.succeed(1),
|
||||
execute: () => Effect.succeed(1),
|
||||
})
|
||||
expect(inputTypeScript(tool)).toBe("{ value?: string | number }")
|
||||
expect(outputTypeScript(tool)).toBe("number | boolean")
|
||||
|
|
@ -417,7 +418,8 @@ describe("non-identifier tool paths", () => {
|
|||
},
|
||||
required: ["query", "libraryName"],
|
||||
} as const,
|
||||
run: () => Effect.succeed("/reactjs/react.dev"),
|
||||
output: {},
|
||||
execute: () => Effect.succeed("/reactjs/react.dev"),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ describe("RegExp", () => {
|
|||
description: "Decorate a string",
|
||||
input: Schema.String,
|
||||
output: Schema.String,
|
||||
run: (input) => Effect.succeed(`[${input}]`),
|
||||
execute: (input) => Effect.succeed(`[${input}]`),
|
||||
})
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
|
|
@ -1028,7 +1028,7 @@ describe("CodeMode values at intra-CodeMode checkpoints", () => {
|
|||
const capture = Tool.make({
|
||||
description: "Capture the exact input the host receives",
|
||||
input: { type: "object" },
|
||||
run: (input) =>
|
||||
execute: (input) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(input)
|
||||
return "ok"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const echo = (description: string, result: string) =>
|
|||
description,
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed(result),
|
||||
execute: () => Effect.succeed(result),
|
||||
})
|
||||
|
||||
const value = async (runtime: CodeMode.Runtime, code: string) => {
|
||||
|
|
@ -88,7 +88,7 @@ describe("callable namespaces", () => {
|
|||
expect(diagnostic.message).toContain("Unknown tool 'issues.missing'")
|
||||
})
|
||||
|
||||
test("a namespace without its own definition stays non-callable", async () => {
|
||||
test("a namespace without its own tool stays non-callable", async () => {
|
||||
const nested = CodeMode.make({ tools: { "issues.list": echo("List issues", "list") } })
|
||||
const diagnostic = await failure(nested, `return await tools.issues({})`)
|
||||
expect(diagnostic.kind).toBe("UnknownTool")
|
||||
|
|
@ -114,9 +114,9 @@ describe("blocked member names on tool paths", () => {
|
|||
expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["constructor"])
|
||||
})
|
||||
|
||||
test("a literal __proto__ key cannot poison a namespace into a fake definition", async () => {
|
||||
test("a literal __proto__ key cannot poison a namespace into a fake tool", async () => {
|
||||
const poisoned = CodeMode.make({
|
||||
tools: { ns: { "__proto__": echo("Hidden", "hidden"), real: echo("Real tool", "real") } },
|
||||
tools: { ns: { __proto__: echo("Hidden", "hidden"), real: echo("Real tool", "real") } },
|
||||
})
|
||||
expect(poisoned.catalog().map((tool) => tool.path)).toEqual(["ns.real"])
|
||||
expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real")
|
||||
|
|
@ -138,7 +138,7 @@ describe("empty segments", () => {
|
|||
})
|
||||
|
||||
describe("canonical path collisions", () => {
|
||||
test("the last definition supplied for a canonical path wins", async () => {
|
||||
test("the last tool supplied for a canonical path wins", async () => {
|
||||
const runtime = CodeMode.make({
|
||||
tools: { "issues.list": echo("First", "first"), issues: { list: echo("Second", "second") } },
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue