Skip to content

Commit 62e9f99

Browse files
committed
Make apply patch more exactly the same as openai's tool
1 parent 213bba5 commit 62e9f99

File tree

7 files changed

+182
-250
lines changed

7 files changed

+182
-250
lines changed

.agents/types/tools.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,18 @@ export interface AddMessageParams {
7070
}
7171

7272
/**
73-
* Apply edits using a Codex-style patch envelope.
73+
* Apply a file operation (create, update, or delete) using Codex-style apply_patch format.
7474
*/
7575
export interface ApplyPatchParams {
76-
/** Patch text in Codex apply_patch format. */
77-
patch: string
76+
/** The file operation to perform. */
77+
operation: {
78+
/** Operation type: create_file, update_file, or delete_file */
79+
type: 'create_file' | 'update_file' | 'delete_file'
80+
/** File path relative to project root */
81+
path: string
82+
/** Diff content. Required for create_file and update_file. Lines prefixed with + for creates, unified diff with @@ hunks for updates. */
83+
diff?: string
84+
}
7885
}
7986

8087
/**

agents/types/tools.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,18 @@ export interface AddMessageParams {
7272
}
7373

7474
/**
75-
* Apply edits using a Codex-style patch envelope.
75+
* Apply a file operation (create, update, or delete) using Codex-style apply_patch format.
7676
*/
7777
export interface ApplyPatchParams {
78-
/** Patch text in Codex apply_patch format. */
79-
patch: string
78+
/** The file operation to perform. */
79+
operation: {
80+
/** Operation type: create_file, update_file, or delete_file */
81+
type: 'create_file' | 'update_file' | 'delete_file'
82+
/** File path relative to project root */
83+
path: string
84+
/** Diff content. Required for create_file and update_file. Lines prefixed with + for creates, unified diff with @@ hunks for updates. */
85+
diff?: string
86+
}
8087
}
8188

8289
/**

cli/src/components/tools/apply-patch.tsx

Lines changed: 27 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -7,71 +7,26 @@ import { useTheme } from '../../hooks/use-theme'
77
import type { ToolRenderConfig } from './types'
88

99
type PatchOperation =
10-
| { type: 'add'; path: string }
11-
| { type: 'delete'; path: string }
12-
| { type: 'update'; path: string; moveTo?: string; hunks: string }
13-
14-
function parsePatchOperations(rawPatch: string): PatchOperation[] {
15-
const normalized = rawPatch.replace(/\r\n/g, '\n')
16-
const lines = normalized.split('\n')
17-
if (lines.length < 2) return []
18-
if (lines[0] !== '*** Begin Patch') return []
19-
20-
const ops: PatchOperation[] = []
21-
let i = 1
22-
const endIndex = lines.length - 1
23-
24-
while (i < endIndex) {
25-
const line = lines[i]
26-
if (!line) {
27-
i++
28-
continue
29-
}
30-
31-
if (line.startsWith('*** Add File: ')) {
32-
const filePath = line.slice('*** Add File: '.length)
33-
i++
34-
while (i < endIndex && !lines[i].startsWith('*** ')) {
35-
i++
36-
}
37-
ops.push({ type: 'add', path: filePath })
38-
continue
39-
}
40-
41-
if (line.startsWith('*** Delete File: ')) {
42-
const filePath = line.slice('*** Delete File: '.length)
43-
ops.push({ type: 'delete', path: filePath })
44-
i++
45-
continue
46-
}
47-
48-
if (line.startsWith('*** Update File: ')) {
49-
const filePath = line.slice('*** Update File: '.length)
50-
i++
51-
52-
let moveTo: string | undefined
53-
if (i < endIndex && lines[i].startsWith('*** Move to: ')) {
54-
moveTo = lines[i].slice('*** Move to: '.length)
55-
i++
56-
}
57-
58-
const hunkLines: string[] = []
59-
while (i < endIndex && !lines[i].startsWith('*** ')) {
60-
if (lines[i] !== '*** End of File') {
61-
hunkLines.push(lines[i])
62-
}
63-
i++
64-
}
65-
66-
const hunks = hunkLines.join('\n').trim()
67-
ops.push({ type: 'update', path: filePath, moveTo, hunks })
68-
continue
69-
}
70-
71-
i++
10+
| { type: 'create_file'; path: string; diff: string }
11+
| { type: 'update_file'; path: string; diff: string }
12+
| { type: 'delete_file'; path: string }
13+
14+
function parseOperation(input: unknown): PatchOperation | null {
15+
if (!input || typeof input !== 'object') return null
16+
const op = (input as { operation?: unknown }).operation
17+
if (!op || typeof op !== 'object') return null
18+
const { type, path, diff } = op as Record<string, unknown>
19+
if (typeof type !== 'string' || typeof path !== 'string') return null
20+
if (type === 'create_file' && typeof diff === 'string') {
21+
return { type: 'create_file', path, diff }
7222
}
73-
74-
return ops
23+
if (type === 'update_file' && typeof diff === 'string') {
24+
return { type: 'update_file', path, diff }
25+
}
26+
if (type === 'delete_file') {
27+
return { type: 'delete_file', path }
28+
}
29+
return null
7530
}
7631

7732
interface EditHeaderProps {
@@ -101,24 +56,19 @@ interface PatchOperationItemProps {
10156
}
10257

10358
const PatchOperationItem = ({ operation }: PatchOperationItemProps) => {
104-
if (operation.type === 'add') {
59+
if (operation.type === 'create_file') {
10560
return <EditHeader name="Create" filePath={operation.path} />
10661
}
10762

108-
if (operation.type === 'delete') {
63+
if (operation.type === 'delete_file') {
10964
return <EditHeader name="Delete" filePath={operation.path} />
11065
}
11166

112-
const destination =
113-
operation.moveTo && operation.moveTo !== operation.path
114-
? `${operation.path}${operation.moveTo}`
115-
: operation.path
116-
11767
return (
11868
<box style={{ flexDirection: 'column', width: '100%' }}>
119-
<EditHeader name="Edit" filePath={destination} />
69+
<EditHeader name="Edit" filePath={operation.path} />
12070
<box style={{ paddingLeft: 2, width: '100%' }}>
121-
<DiffViewer diffText={operation.hunks} />
71+
<DiffViewer diffText={operation.diff} />
12272
</box>
12373
</box>
12474
)
@@ -128,31 +78,18 @@ export const ApplyPatchComponent = defineToolComponent({
12878
toolName: 'apply_patch',
12979

13080
render(toolBlock): ToolRenderConfig {
131-
const patch =
132-
toolBlock.input &&
133-
typeof toolBlock.input === 'object' &&
134-
'patch' in toolBlock.input &&
135-
typeof (toolBlock.input as { patch?: unknown }).patch === 'string'
136-
? (toolBlock.input as { patch: string }).patch
137-
: ''
138-
139-
const operations = patch ? parsePatchOperations(patch) : []
81+
const operation = parseOperation(toolBlock.input)
14082

141-
if (operations.length === 0) {
83+
if (!operation) {
14284
return { content: null }
14385
}
14486

14587
return {
14688
content: (
14789
<box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
148-
{operations.map((operation, index) => (
149-
<PatchOperationItem
150-
key={`${operation.type}-${operation.path}-${index}`}
151-
operation={operation}
152-
/>
153-
))}
90+
<PatchOperationItem operation={operation} />
15491
</box>
15592
),
15693
}
15794
},
158-
})
95+
})

common/src/templates/initial-agents-dir/types/tools.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,18 @@ export interface AddMessageParams {
7272
}
7373

7474
/**
75-
* Apply edits using a Codex-style patch envelope.
75+
* Apply a file operation (create, update, or delete) using Codex-style apply_patch format.
7676
*/
7777
export interface ApplyPatchParams {
78-
/** Patch text in Codex apply_patch format. */
79-
patch: string
78+
/** The file operation to perform. */
79+
operation: {
80+
/** Operation type: create_file, update_file, or delete_file */
81+
type: 'create_file' | 'update_file' | 'delete_file'
82+
/** File path relative to project root */
83+
path: string
84+
/** Diff content. Required for create_file and update_file. Lines prefixed with + for creates, unified diff with @@ hunks for updates. */
85+
diff?: string
86+
}
8087
}
8188

8289
/**

common/src/tools/params/tool/apply-patch.ts

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const applyPatchResultSchema = z.union([
1010
applied: z.array(
1111
z.object({
1212
file: z.string(),
13-
action: z.enum(['add', 'update', 'delete', 'move']),
13+
action: z.enum(['add', 'update', 'delete']),
1414
}),
1515
),
1616
}),
@@ -21,30 +21,81 @@ export const applyPatchResultSchema = z.union([
2121

2222
const toolName = 'apply_patch'
2323
const endsAgentStep = false
24+
25+
const operationSchema = z.discriminatedUnion('type', [
26+
z.object({
27+
type: z.literal('create_file'),
28+
path: z.string().min(1, 'Path cannot be empty'),
29+
diff: z.string().min(1, 'Diff cannot be empty'),
30+
}),
31+
z.object({
32+
type: z.literal('update_file'),
33+
path: z.string().min(1, 'Path cannot be empty'),
34+
diff: z.string().min(1, 'Diff cannot be empty'),
35+
}),
36+
z.object({
37+
type: z.literal('delete_file'),
38+
path: z.string().min(1, 'Path cannot be empty'),
39+
}),
40+
])
41+
42+
export type ApplyPatchOperation = z.infer<typeof operationSchema>
43+
2444
const inputSchema = z
2545
.object({
26-
patch: z
27-
.string()
28-
.min(1, 'Patch cannot be empty')
29-
.describe('Patch text in Codex apply_patch format.'),
46+
operation: operationSchema.describe(
47+
'The file operation to perform. type is one of create_file, update_file, or delete_file.',
48+
),
3049
})
31-
.describe('Apply a unified-diff style multi-file patch.')
50+
.describe('Apply a file operation (create, update, or delete).')
3251

3352
const description = `
34-
Use this tool to edit files using Codex-style patch format.
53+
Use this tool to apply file operations using Codex-style apply_patch format.
54+
55+
Each call performs a single operation on one file.
56+
57+
Operation types:
58+
- create_file: Create a new file. Requires path and diff (lines prefixed with +).
59+
- update_file: Update an existing file. Requires path and diff (unified diff with @@ hunks).
60+
- delete_file: Delete a file. Requires only path.
61+
62+
Example (create):
63+
${$getNativeToolCallExampleString({
64+
toolName,
65+
inputSchema,
66+
input: {
67+
operation: {
68+
type: 'create_file',
69+
path: 'hello.txt',
70+
diff: '@@\n+Hello world\n',
71+
},
72+
},
73+
endsAgentStep,
74+
})}
3575
36-
Patch format:
37-
- Start with *** Begin Patch
38-
- End with *** End Patch
39-
- Use file ops: *** Add File, *** Update File, *** Delete File
40-
- Use @@ hunks inside update operations
76+
Example (update):
77+
${$getNativeToolCallExampleString({
78+
toolName,
79+
inputSchema,
80+
input: {
81+
operation: {
82+
type: 'update_file',
83+
path: 'lib/fib.py',
84+
diff: '@@\n-def fib(n):\n+def fibonacci(n):\n if n <= 1:\n return n\n- return fib(n-1) + fib(n-2)\n+ return fibonacci(n-1) + fibonacci(n-2)\n',
85+
},
86+
},
87+
endsAgentStep,
88+
})}
4189
42-
Example:
90+
Example (delete):
4391
${$getNativeToolCallExampleString({
4492
toolName,
4593
inputSchema,
4694
input: {
47-
patch: `*** Begin Patch\n*** Add File: hello.txt\n+Hello world\n*** End Patch`,
95+
operation: {
96+
type: 'delete_file',
97+
path: 'old-file.txt',
98+
},
4899
},
49100
endsAgentStep,
50101
})}

sdk/e2e/utils/e2e-mocks.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,11 @@ function buildMockToolCall(params: {
204204
return {
205205
toolName: 'apply_patch',
206206
input: {
207-
patch:
208-
'*** Begin Patch\n' +
209-
'*** Add File: hello-from-apply-patch.txt\n' +
210-
'+hello from apply_patch\n' +
211-
'*** End Patch',
207+
operation: {
208+
type: 'create_file' as const,
209+
path: 'hello-from-apply-patch.txt',
210+
diff: '@@\n+hello from apply_patch\n',
211+
},
212212
},
213213
}
214214
}

0 commit comments

Comments
 (0)