diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index ea3aac34807d..9e37a7a72195 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -66,6 +66,13 @@ export const EditTool = Tool.define( return { description: DESCRIPTION, parameters: Parameters, + normalizeArguments: (args: unknown) => + Tool.normalizeAliases(args, { + filePath: ["file_path", "path"], + oldString: ["old_string", "oldText"], + newString: ["new_string", "newText"], + replaceAll: ["replace_all"], + }), execute: (params: Schema.Schema.Type, ctx: Tool.Context) => Effect.gen(function* () { if (!params.filePath) { diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index f072773fad2d..53fd1b053b83 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -58,6 +58,7 @@ export interface Def< description: string parameters: Parameters jsonSchema?: JSONSchema7 + normalizeArguments?(args: unknown): unknown execute(args: Schema.Schema.Type, ctx: Context): Effect.Effect> formatValidationError?(error: unknown): string } @@ -94,6 +95,21 @@ export type InferDef = ? Def : never +export function normalizeAliases(input: unknown, aliases: Record): unknown { + if (!isRecord(input)) return input + const output = { ...input } + for (const [key, names] of Object.entries(aliases)) { + if (output[key] !== undefined) continue + const name = names.find((alias) => input[alias] !== undefined) + if (name) output[key] = input[name] + } + return output +} + +function isRecord(input: unknown): input is Record { + return typeof input === "object" && input !== null && !Array.isArray(input) +} + function wrap, Result extends Metadata>( id: string, init: Init, @@ -116,7 +132,7 @@ function wrap, Result extends Metadat ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}), } return Effect.gen(function* () { - const decoded = yield* decode(args).pipe( + const decoded = yield* decode(toolInfo.normalizeArguments ? toolInfo.normalizeArguments(args) : args).pipe( Effect.mapError( (error) => new InvalidArgumentsError({ diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index c2be73ab1cdb..b1ffc69dbb4a 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -35,6 +35,11 @@ export const WriteTool = Tool.define( return { description: DESCRIPTION, parameters: Parameters, + normalizeArguments: (args: unknown) => + Tool.normalizeAliases(args, { + filePath: ["file_path", "path"], + content: ["fileContent", "contents"], + }), execute: (params: { content: string; filePath: string }, ctx: Tool.Context) => Effect.gen(function* () { const instance = yield* InstanceState.context diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 3f644ed53dde..5d431bd715fe 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -54,6 +54,12 @@ const run = Effect.fn("EditToolTest.run")(function* ( return yield* tool.execute(args, next) }) +const runUnknown = Effect.fn("EditToolTest.runUnknown")(function* (args: unknown, next: Tool.Context = ctx) { + const tool = yield* init() + const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType + return yield* execute(args, next) +}) + const fail = Effect.fn("EditToolTest.fail")(function* (args: Tool.InferParameters) { const exit = yield* run(args).pipe(Effect.exit) if (Exit.isFailure(exit)) { @@ -157,6 +163,18 @@ describe("tool.edit", () => { }), ) + it.instance("normalizes common model argument aliases", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "alias.txt") + yield* put(filepath, "old content") + + yield* runUnknown({ file_path: filepath, old_string: "old", new_string: "new" }) + + expect(yield* load(filepath)).toBe("new content") + }), + ) + it.instance("replaces the first visible line in BOM files", () => Effect.gen(function* () { const test = yield* TestInstance diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 08f156092b18..bd31b90e3532 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -55,6 +55,12 @@ const run = Effect.fn("WriteToolTest.run")(function* ( return yield* tool.execute(args, next) }) +const runUnknown = Effect.fn("WriteToolTest.runUnknown")(function* (args: unknown, next: Tool.Context = ctx) { + const tool = yield* init() + const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType + return yield* execute(args, next) +}) + describe("tool.write", () => { describe("new file creation", () => { it.instance("writes content to new file", () => @@ -71,6 +77,18 @@ describe("tool.write", () => { }), ) + it.instance("normalizes common model argument aliases", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "alias.txt") + + yield* runUnknown({ path: filepath, fileContent: "alias content" }) + + const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8")) + expect(content).toBe("alias content") + }), + ) + it.instance("creates parent directories if needed", () => Effect.gen(function* () { const test = yield* TestInstance