refactor: rename apply_patch tool

This commit is contained in:
Dax Raad 2026-07-07 16:27:03 -04:00
commit 5db320b02d
22 changed files with 48 additions and 46 deletions

View file

@ -200,6 +200,6 @@ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.no
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
// TODO: Add snapshots / undo after V2 snapshot design exists.
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits.
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
// Until then, edits are sequential and report partial application.
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.

View file

@ -41,7 +41,7 @@ Registrations are scoped:
## Permissions
The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `apply_patch` declare the shared `edit` action.
The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `patch` declare the shared `edit` action.
Definition filtering is catalog visibility, not execution authorization. A call still executes the captured leaf policy if it reaches settlement.

View file

@ -12,7 +12,7 @@ import { Patch } from "../patch"
import { PermissionV2 } from "../permission"
import { Tool } from "./tool"
export const name = "apply_patch"
export const name = "patch"
export const Input = Schema.Struct({
patchText: Schema.String.annotate({
@ -91,11 +91,11 @@ export const Plugin = {
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
const hunks = yield* Effect.try({
try: () => Patch.parse(input.patchText),
catch: (cause) => new ToolFailure({ message: `apply_patch verification failed: ${String(cause)}` }),
catch: (cause) => new ToolFailure({ message: `patch verification failed: ${String(cause)}` }),
})
if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" })
const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined)
if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" })
if (move) return yield* new ToolFailure({ message: "patch moves are not supported yet" })
const targets: Array<{ readonly hunk: Patch.Hunk; readonly target: LocationMutation.Target }> = []
for (const hunk of hunks)

View file

@ -191,10 +191,10 @@ const registryLayer = Layer.effect(
const registration = entries.at(-1)?.registration
if (registration) registrations.set(name, registration)
}
// OpenAI/GPT models use apply_patch; every other model uses edit and write.
// OpenAI/GPT models use patch; every other model uses edit and write.
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
for (const [name, registration] of registrations) {
const wrongEditTool = name === "apply_patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
const wrongEditTool = name === "patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
if (
wrongEditTool ||
(registration.deferred && !Flag.CODEMODE_ENABLED) ||

View file

@ -89,16 +89,16 @@ describe("ToolRegistry", () => {
read: make(),
edit: make("edit"),
write: make("edit"),
apply_patch: make("edit"),
patch: make("edit"),
})
const names = (model: ToolRegistry.MaterializeInput["model"]) =>
service
.materialize({ model })
.pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name)))
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "apply_patch"])
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "apply_patch"])
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "apply_patch"])
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "patch"])
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "patch"])
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "patch"])
expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual(["read", "edit", "write"])
}),
)

View file

@ -26,7 +26,7 @@ const applyPatchToolNode = makeLocationNode({
deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
})
const sessionID = SessionV2.ID.make("ses_apply_patch_tool_test")
const sessionID = SessionV2.ID.make("ses_patch_tool_test")
const assertions: PermissionV2.AssertInput[] = []
let denyAction: string | undefined
let failRemoveTarget: string | undefined
@ -132,10 +132,10 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
const call = (patchText: string, id = "call-apply-patch") => ({
sessionID,
...toolIdentity,
call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } },
call: { type: "tool-call" as const, id, name: "patch", input: { patchText } },
})
// apply_patch is only materialized for OpenAI/GPT models.
// patch is only materialized for OpenAI/GPT models.
const model = { id: "gpt-5", provider: "openai" }
const exists = (target: string) =>
@ -162,7 +162,7 @@ describe("ApplyPatchTool", () => {
withTool(tmp.path, (registry) =>
Effect.gen(function* () {
expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([
"apply_patch",
"patch",
])
const settled = yield* settleTool(
registry,
@ -241,7 +241,7 @@ describe("ApplyPatchTool", () => {
),
model,
),
).toEqual({ type: "error", value: "apply_patch moves are not supported yet" })
).toEqual({ type: "error", value: "patch moves are not supported yet" })
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
expect(assertions).toEqual([])
}),