Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions common/src/tools/params/__tests__/coerce-to-array.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { describe, expect, it } from 'bun:test'
import z from 'zod/v4'

import { coerceToArray } from '../utils'
import { coerceToArray, normalizeReplacementAliases } from '../utils'

describe('coerceToArray', () => {
it('passes through arrays unchanged', () => {
expect(coerceToArray(['a', 'b'])).toEqual(['a', 'b'])
expect(coerceToArray([{ old: 'x', new: 'y' }])).toEqual([{ old: 'x', new: 'y' }])
expect(coerceToArray([{ old: 'x', new: 'y' }])).toEqual([
{ old: 'x', new: 'y' },
])
expect(coerceToArray([])).toEqual([])
})

Expand All @@ -15,15 +17,20 @@ describe('coerceToArray', () => {
})

it('wraps a single object in an array', () => {
expect(coerceToArray({ old: 'x', new: 'y' })).toEqual([{ old: 'x', new: 'y' }])
expect(coerceToArray({ old: 'x', new: 'y' })).toEqual([
{ old: 'x', new: 'y' },
])
})

it('wraps a single number in an array', () => {
expect(coerceToArray(42)).toEqual([42])
})

it('parses a stringified JSON array', () => {
expect(coerceToArray('["file1.ts", "file2.ts"]')).toEqual(['file1.ts', 'file2.ts'])
expect(coerceToArray('["file1.ts", "file2.ts"]')).toEqual([
'file1.ts',
'file2.ts',
])
})

it('wraps a non-JSON string (does not parse as array)', () => {
Expand Down Expand Up @@ -116,3 +123,51 @@ describe('coerceToArray with Zod schemas', () => {
expect(coercedSchema).toEqual(plainSchema)
})
})

describe('normalizeReplacementAliases', () => {
it('maps old_str and new_str onto the documented replacement keys', () => {
expect(
normalizeReplacementAliases({
old_str: 'before',
new_str: 'after',
allowMultiple: true,
}),
).toEqual({
old_str: 'before',
new_str: 'after',
old: 'before',
new: 'after',
allowMultiple: true,
})
})

it('maps old_string and new_string onto the documented replacement keys', () => {
expect(
normalizeReplacementAliases({
old_string: 'before',
new_string: 'after',
}),
).toEqual({
old_string: 'before',
new_string: 'after',
old: 'before',
new: 'after',
})
})

it('does not overwrite documented replacement keys', () => {
expect(
normalizeReplacementAliases({
old: 'before',
new: 'after',
old_str: 'ignored',
new_str: 'ignored',
}),
).toEqual({
old: 'before',
new: 'after',
old_str: 'ignored',
new_str: 'ignored',
})
})
})
54 changes: 32 additions & 22 deletions common/src/tools/params/tool/propose-str-replace.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import z from 'zod/v4'

import { $getNativeToolCallExampleString, coerceToArray, jsonToolResultSchema } from '../utils'
import {
$getNativeToolCallExampleString,
coerceToArray,
jsonToolResultSchema,
normalizeReplacementAliases,
} from '../utils'

import type { $ToolParams } from '../../constants'

Expand Down Expand Up @@ -30,33 +35,38 @@ const inputSchema = z
z
.array(
z
.object({
old: z
.string()
.min(1, 'Old cannot be empty')
.describe(
`The string to replace. This must be an *exact match* of the string you want to replace, including whitespace and punctuation.`,
),
new: z
.string()
.describe(
`The string to replace the corresponding old string with. Can be empty to delete.`,
),
allowMultiple: z
.boolean()
.optional()
.default(false)
.describe(
'Whether to allow multiple replacements of old string.',
),
})
.preprocess(
normalizeReplacementAliases,
z.object({
old: z
.string()
.min(1, 'Old cannot be empty')
.describe(
`The string to replace. This must be an *exact match* of the string you want to replace, including whitespace and punctuation.`,
),
new: z
.string()
.describe(
`The string to replace the corresponding old string with. Can be empty to delete.`,
),
allowMultiple: z
.boolean()
.optional()
.default(false)
.describe(
'Whether to allow multiple replacements of old string.',
),
}),
)
.describe('Pair of old and new strings.'),
)
.min(1, 'Replacements cannot be empty'),
)
.describe('Array of replacements to make.'),
})
.describe(`Propose string replacements in a file without actually applying them.`)
.describe(
`Propose string replacements in a file without actually applying them.`,
)
const description = `
Propose edits to a file without actually applying them. Use this tool when you want to draft changes that will be reviewed before being applied.

Expand Down
50 changes: 29 additions & 21 deletions common/src/tools/params/tool/str-replace.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import z from 'zod/v4'

import { $getNativeToolCallExampleString, coerceToArray, jsonToolResultSchema } from '../utils'
import {
$getNativeToolCallExampleString,
coerceToArray,
jsonToolResultSchema,
normalizeReplacementAliases,
} from '../utils'

import type { $ToolParams } from '../../constants'

Expand Down Expand Up @@ -31,26 +36,29 @@ const inputSchema = z
z
.array(
z
.object({
old: z
.string()
.min(1, 'Old cannot be empty')
.describe(
`The string to replace. This must be an *exact match* of the string you want to replace, including whitespace and punctuation.`,
),
new: z
.string()
.describe(
`The string to replace the corresponding old string with. Can be empty to delete.`,
),
allowMultiple: z
.boolean()
.optional()
.default(false)
.describe(
'Whether to allow multiple replacements of old string.',
),
})
.preprocess(
normalizeReplacementAliases,
z.object({
old: z
.string()
.min(1, 'Old cannot be empty')
.describe(
`The string to replace. This must be an *exact match* of the string you want to replace, including whitespace and punctuation.`,
),
new: z
.string()
.describe(
`The string to replace the corresponding old string with. Can be empty to delete.`,
),
allowMultiple: z
.boolean()
.optional()
.default(false)
.describe(
'Whether to allow multiple replacements of old string.',
),
}),
)
.describe('Pair of old and new strings.'),
)
.min(1, 'Replacements cannot be empty'),
Expand Down
25 changes: 25 additions & 0 deletions common/src/tools/params/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@ export function coerceToArray(val: unknown): unknown {
return val
}

/**
* Handles common replacement-key aliases emitted by some models while keeping
* the documented schema stable.
*/
export function normalizeReplacementAliases(val: unknown): unknown {
if (val === null || typeof val !== 'object' || Array.isArray(val)) {
return val
}

const replacement = { ...(val as Record<string, unknown>) }
for (const [target, aliases] of [
['old', ['old_str', 'old_string']],
['new', ['new_str', 'new_string']],
] as const) {
if (replacement[target] !== undefined) {
continue
}
const alias = aliases.find((key) => typeof replacement[key] === 'string')
if (alias) {
replacement[target] = replacement[alias]
}
}
return replacement
}

/** Only used for generating tool call strings before all tools are defined.
*
* @param toolName - The name of the tool to call
Expand Down
Loading
Loading