mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(codemode): derive empty tool inputs from the encoded schema
This commit is contained in:
@@ -264,15 +264,12 @@ export const inputProperties = <R>(tool: Tool<R>): Array<InputProperty> => {
|
||||
export const inputTypeScript = <R>(tool: Tool<R>, pretty = false): string =>
|
||||
isEffectSchema(tool.input) ? toTypeScript(tool.input, false, pretty) : jsonSchemaToTypeScript(tool.input, pretty)
|
||||
|
||||
// Effect 4.0 models an empty Struct as a non-nullish object, so inspect its AST.
|
||||
// Effect 4.0 models an empty Struct as a non-nullish object, so inspect the encoded AST callers supply.
|
||||
// Raw JSON Schema inputs retain the compact `{}` rendering check.
|
||||
export const isEmptyInput = <R>(tool: Tool<R>): boolean => {
|
||||
if (!isEffectSchema(tool.input)) return inputTypeScript(tool) === "{}"
|
||||
return (
|
||||
SchemaAST.isObjects(tool.input.ast) &&
|
||||
tool.input.ast.propertySignatures.length === 0 &&
|
||||
tool.input.ast.indexSignatures.length === 0
|
||||
)
|
||||
const encoded = SchemaAST.toEncoded(tool.input.ast)
|
||||
return SchemaAST.isObjects(encoded) && encoded.propertySignatures.length === 0 && encoded.indexSignatures.length === 0
|
||||
}
|
||||
|
||||
export const outputTypeScript = <R>(tool: Tool<R>, pretty = false): string =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Effect, Schema, SchemaGetter } from "effect"
|
||||
import { CodeMode, Tool } from "../src/index.js"
|
||||
import {
|
||||
decodeInput,
|
||||
@@ -878,6 +878,73 @@ describe("JSDoc signatures in catalogs and search results", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty input signatures agree with decoding", () => {
|
||||
test.each([
|
||||
{ name: "a plain empty struct", input: Schema.Struct({}) },
|
||||
{
|
||||
name: "an annotated empty struct",
|
||||
input: Schema.Struct({}).annotate({ identifier: "Empty", description: "No input" }),
|
||||
},
|
||||
{ name: "a checked empty struct", input: Schema.Struct({}).check(Schema.makeFilter(() => true)) },
|
||||
{
|
||||
name: "an empty encoded struct transformed to a non-empty struct",
|
||||
input: Schema.Struct({}).pipe(
|
||||
Schema.decodeTo(Schema.Struct({ token: Schema.String }), {
|
||||
decode: SchemaGetter.transform(() => ({ token: "x" })),
|
||||
encode: SchemaGetter.transform(() => ({})),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ name: "a raw JSON Schema empty object", input: { type: "object", properties: {} } },
|
||||
])("$name advertises () and runs with zero arguments", async ({ input }) => {
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
ping: Tool.make({ description: "Ping", input, output: Schema.String, execute: () => Effect.succeed("pong") }),
|
||||
},
|
||||
})
|
||||
const signature = "tools.ping(): Promise<string>"
|
||||
expect(runtime.catalog[0]?.signature).toBe(signature)
|
||||
const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.ping" })'))
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) throw new Error("search failed")
|
||||
expect(result.value).toMatchObject({ items: [{ signature }] })
|
||||
expect(await Effect.runPromise(runtime.execute("return await tools.ping()"))).toMatchObject({
|
||||
ok: true,
|
||||
value: "pong",
|
||||
})
|
||||
})
|
||||
|
||||
test("a required encoded input transformed to an empty struct keeps its required fields", async () => {
|
||||
const consume = Tool.make({
|
||||
description: "Consume token",
|
||||
input: Schema.Struct({ token: Schema.String }).pipe(
|
||||
Schema.decodeTo(Schema.Struct({}), {
|
||||
decode: SchemaGetter.transform(() => ({})),
|
||||
encode: SchemaGetter.transform(() => ({ token: "x" })),
|
||||
}),
|
||||
),
|
||||
output: Schema.String,
|
||||
execute: () => Effect.succeed("consumed"),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { consume } })
|
||||
const signature = "tools.consume({\n token: string,\n}): Promise<string>"
|
||||
expect(() => decodeInput(consume, {})).toThrow()
|
||||
expect(runtime.catalog[0]?.signature).toBe(signature)
|
||||
const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.consume" })'))
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) throw new Error("search failed")
|
||||
expect(result.value).toMatchObject({ items: [{ signature }] })
|
||||
expect(await Effect.runPromise(runtime.execute("return await tools.consume()"))).toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "InvalidToolInput" },
|
||||
})
|
||||
expect(await Effect.runPromise(runtime.execute('return await tools.consume({ token: "t" })'))).toMatchObject({
|
||||
ok: true,
|
||||
value: "consumed",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("non-identifier tool paths", () => {
|
||||
const resolveLibrary = Tool.make({
|
||||
description: "Resolve a Context7 library ID",
|
||||
|
||||
Reference in New Issue
Block a user