agent: default unset scope to global (safe by default)

Only tools that explicitly declare scope="write" get path-based
parallelism. Unset scope is now global (full barrier), avoiding the
"path is a lie" problem where a tool has a path arg but modifies
other files (e.g., lsp_rename).

Tools must opt in to parallelism:
- scope=read: never conflicts
- scope=write: conflicts on same path only
- scope=global (or unset): serialization barrier

Added scope=write to file_edit.meta and file_write.meta.
This commit is contained in:
Ollie Agent 2026-08-11 07:57:22 +02:00
parent 586edc1332
commit 2870da593d
3 changed files with 13 additions and 12 deletions

View File

@ -417,22 +417,21 @@ func execToolCalls(rt *Runtime, ctx TurnCtx, toolCalls []backend.ToolCall, resul
const conflictKeyGlobal = "\x00GLOBAL"
// toolConflictKeys returns the set of resource keys a tool call touches.
// Scope "read" tools return nil (never conflict). Scope "global" tools
// return the global sentinel. Scope "write" (or empty) tools return their
// file path, or global if no path is detectable.
// Scope "read" tools return nil (never conflict). Scope "write" tools
// return their file path (or global if no path detectable). Everything
// else (scope "global" or unset) is a full serialization barrier.
func toolConflictKeys(rt *Runtime, call backend.ToolCall) []string {
scope := rt.ToolMeta[call.Name].Scope
if scope == "read" {
return nil // reads never conflict
}
if scope == "global" {
if scope == "write" {
if p := extractFilePath(call.Arguments); p != "" {
return []string{p}
}
return []string{conflictKeyGlobal}
}
// "write" or unset: infer from path argument
if p := extractFilePath(call.Arguments); p != "" {
return []string{p}
}
// No detectable path: global barrier
// "global", empty, or anything else: full barrier
return []string{conflictKeyGlobal}
}

View File

@ -2,7 +2,7 @@
"description": "Replace text in a file. Has fuzzy matching (whitespace/indent flexible).",
"outputFormat": "diff",
"resetsCounter": true,
"prompt": "## file_edit\n\nReplace text in a file. Has fuzzy matching (whitespace/indent flexible).\n\n**Args**: `path`, `old_string`, `new_string` (required); `replace_all` (optional)\n\n```\nfile_edit(path=\"/abs/path\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"/abs/path\", old_string=\"old text\", new_string=\"new text\", replace_all=\"true\")\n```\n\n**Constraints**: Absolute paths only. Errors if `old_string` matches multiple locations — add surrounding context to disambiguate, or use `replace_all=true`.",
"prompt": "## file_edit\n\nReplace text in a file. Has fuzzy matching (whitespace/indent flexible).\n\n**Args**: `path`, `old_string`, `new_string` (required); `replace_all` (optional)\n\n```\nfile_edit(path=\"/abs/path\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"/abs/path\", old_string=\"old text\", new_string=\"new text\", replace_all=\"true\")\n```\n\n**Constraints**: Absolute paths only. Errors if `old_string` matches multiple locations \u2014 add surrounding context to disambiguate, or use `replace_all=true`.",
"args": {
"type": "object",
"required": [
@ -28,5 +28,6 @@
"description": "Set to true/1/yes to replace all occurrences"
}
}
}
},
"scope": "write"
}

View File

@ -19,5 +19,6 @@
"description": "File content to write"
}
}
}
},
"scope": "write"
}