feat(coding-agent): added nonAbortable option and isolated task execution

- Added nonAbortable option to AgentTool interface for ignoring abort signals during tool execution.
- Added isolated option to task tool for running tasks in isolated git worktrees.
- Simplified tool parameter descriptions across all coding-agent tools for better readability.
- Updated find tool to always sort results by modification time and removed sortByMtime parameter.
- Changed web-fetch timeout default from 20s to 45s maximum and updated bash prompt to use cwd parameter.
- Removed model parameter from task and gemini-image tools to use session/provider defaults.
This commit is contained in:
can1357
2026-01-23 05:42:40 +01:00
parent dd9154e2c7
commit 19a7e33b4e
18 changed files with 116 additions and 194 deletions
+3
View File
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
### Added
- Added `nonAbortable` option to tools to ignore abort signals during execution
## [6.8.0] - 2026-01-20
### Changed
+1 -1
View File
@@ -410,7 +410,7 @@ async function executeToolCalls(
result = await tool.execute(
toolCall.id,
validatedArgs,
signal,
tool.nonAbortable ? undefined : signal,
(partialResult) => {
stream.push({
type: "tool_execution_update",
+2
View File
@@ -204,6 +204,8 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
label: string;
/** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
hidden?: boolean;
/** If true, tool execution ignores abort signals (runs to completion) */
nonAbortable?: boolean;
execute: (
toolCallId: string,
params: Static<TParameters>,
+5 -1
View File
@@ -1,7 +1,6 @@
# Changelog
## [Unreleased]
### Added
- Added `isolated` option to run tasks in isolated git worktrees
@@ -10,6 +9,11 @@
### Changed
- Simplified tool parameter descriptions across all tools for brevity
- Updated find tool to always sort results by modification time
- Changed web-fetch timeout default from 20s to 45s maximum
- Updated bash prompt to use `cwd` parameter instead of `workdir`
- Simplified tool prompt documentation for better readability
- Removed model parameter from task tool to use session model by default
- Removed model parameter from gemini-image tool to use provider defaults
- Improved variable display in task output with humanized keys for single variables
+9 -17
View File
@@ -31,29 +31,21 @@ import { ToolUIKit } from "./render-utils";
// =============================================================================
const OptionItem = Type.Object({
label: Type.String({ description: "Display label for this option" }),
label: Type.String({ description: "Display label" }),
});
const QuestionItem = Type.Object({
id: Type.String({ description: "Short identifier for this question (e.g., 'auth', 'cache')" }),
question: Type.String({ description: "The question text" }),
options: Type.Array(OptionItem, { description: "Options for this question" }),
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections for this question" })),
id: Type.String({ description: "Question ID, e.g. 'auth', 'cache'" }),
question: Type.String({ description: "Question text" }),
options: Type.Array(OptionItem, { description: "Available options" }),
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections" })),
});
const askSchema = Type.Object({
question: Type.Optional(Type.String({ description: "The question to ask the user" })),
options: Type.Optional(Type.Array(OptionItem, { description: "Available options for the user to choose from." })),
multi: Type.Optional(
Type.Boolean({
description: "Allow multiple options to be selected (default: false)",
}),
),
questions: Type.Optional(
Type.Array(QuestionItem, {
description: "Multiple questions to ask in sequence, each with their own options",
}),
),
question: Type.Optional(Type.String({ description: "Question to ask" })),
options: Type.Optional(Type.Array(OptionItem, { description: "Available options" })),
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections (default: false)" })),
questions: Type.Optional(Type.Array(QuestionItem, { description: "Multiple questions in sequence" })),
});
/** Result for a single question */
+3 -3
View File
@@ -18,9 +18,9 @@ import { formatTailTruncationNotice, type TruncationResult, truncateTail } from
export const BASH_DEFAULT_PREVIEW_LINES = 10;
const bashSchema = Type.Object({
command: Type.String({ description: "Bash command to execute" }),
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
cwd: Type.Optional(Type.String({ description: "Working directory for the command (default: current directory)" })),
command: Type.String({ description: "Command to execute" }),
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
});
export interface BashToolDetails {
+15 -26
View File
@@ -17,19 +17,13 @@ import { PREVIEW_LIMITS, ToolUIKit } from "./render-utils";
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
const findSchema = Type.Object({
pattern: Type.String({
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'",
}),
path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })),
pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', '**/*.json'" }),
path: Type.Optional(Type.String({ description: "Directory to search (default: cwd)" })),
limit: Type.Optional(Type.Number({ description: "Max results (default: 1000)" })),
hidden: Type.Optional(Type.Boolean({ description: "Include hidden files (default: true)" })),
sortByMtime: Type.Optional(
Type.Boolean({ description: "Sort results by modification time, most recent first (default: false)" }),
),
type: Type.Optional(
StringEnum(["file", "dir", "all"], {
description:
"Filter by type: 'file' for files only, 'dir' for directories only, 'all' for both (default: 'all')",
description: "Filter: file, dir, or all (default: all)",
}),
),
});
@@ -128,7 +122,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
_onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
_context?: AgentToolContext,
): Promise<AgentToolResult<FindToolDetails>> {
const { pattern, path: searchDir, limit, hidden, sortByMtime, type } = params;
const { pattern, path: searchDir, limit, hidden, type } = params;
return untilAborted(signal, async () => {
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
@@ -139,7 +133,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
const effectiveLimit = limit ?? DEFAULT_LIMIT;
const effectiveType = type ?? "all";
const includeHidden = hidden ?? true;
const shouldSortByMtime = sortByMtime ?? false;
// If custom operations provided with glob, use that instead of fd
if (this.customOps?.glob) {
@@ -318,24 +311,20 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
relativePath += "/";
}
// When sorting by mtime, keep files that fail to stat with mtime 0
if (shouldSortByMtime) {
try {
const fullPath = path.join(searchPath, relativePath);
const stat = await Bun.file(fullPath).stat();
relativized.push(relativePath);
mtimes.push(stat.mtimeMs);
} catch {
relativized.push(relativePath);
mtimes.push(0);
}
} else {
// Get mtime for sorting (files that fail to stat get mtime 0)
try {
const fullPath = path.join(searchPath, relativePath);
const stat = await Bun.file(fullPath).stat();
relativized.push(relativePath);
mtimes.push(stat.mtimeMs);
} catch {
relativized.push(relativePath);
mtimes.push(0);
}
}
// Sort by mtime if requested (most recent first)
if (shouldSortByMtime && relativized.length > 0) {
// Sort by mtime (most recent first)
if (relativized.length > 0) {
const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx] }));
indexed.sort((a, b) => b.mtime - a.mtime);
relativized.length = 0;
+13 -24
View File
@@ -25,34 +25,23 @@ import {
} from "./truncate";
const grepSchema = Type.Object({
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
type: Type.Optional(Type.String({ description: "File type filter (e.g., 'ts', 'rust', 'py')" })),
ignoreCase: Type.Optional(
Type.Boolean({ description: "Force case-insensitive search (default: false, uses smart-case otherwise)" }),
),
caseSensitive: Type.Optional(
Type.Boolean({ description: "Force case-sensitive search (default: false, disables smart-case)" }),
),
literal: Type.Optional(
Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" }),
),
multiline: Type.Optional(
Type.Boolean({ description: "Enable multiline matching for cross-line patterns (default: false)" }),
),
context: Type.Optional(
Type.Number({ description: "Number of lines to show before and after each match (default: 0)" }),
),
limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
pattern: Type.String({ description: "Search pattern (regex)" }),
path: Type.Optional(Type.String({ description: "Directory or file to search (default: cwd)" })),
glob: Type.Optional(Type.String({ description: "Glob filter, e.g. '*.ts', '**/*.spec.ts'" })),
type: Type.Optional(Type.String({ description: "File type filter, e.g. 'ts', 'py', 'rust'" })),
ignoreCase: Type.Optional(Type.Boolean({ description: "Force case-insensitive (default: smart-case)" })),
caseSensitive: Type.Optional(Type.Boolean({ description: "Force case-sensitive (default: smart-case)" })),
literal: Type.Optional(Type.Boolean({ description: "Treat pattern as literal, not regex (default: false)" })),
multiline: Type.Optional(Type.Boolean({ description: "Match across line boundaries (default: false)" })),
context: Type.Optional(Type.Number({ description: "Lines of context before/after match (default: 0)" })),
limit: Type.Optional(Type.Number({ description: "Max matches to return (default: 100)" })),
outputMode: Type.Optional(
StringEnum(["content", "files_with_matches", "count"], {
description:
"Output mode: 'content' shows matching lines, 'files_with_matches' shows only file paths, 'count' shows match counts per file (default: 'content')",
description: "Output format (default: content)",
}),
),
headLimit: Type.Optional(Type.Number({ description: "Limit output to first N results (default: unlimited)" })),
offset: Type.Optional(Type.Number({ description: "Skip first N results before applying headLimit (default: 0)" })),
headLimit: Type.Optional(Type.Number({ description: "Truncate output to first N results" })),
offset: Type.Optional(Type.Number({ description: "Skip first N results (default: 0)" })),
});
const DEFAULT_LIMIT = 100;
@@ -30,23 +30,21 @@ export const lspSchema = Type.Object({
"related_tests",
"reload_workspace",
],
{ description: "LSP action to perform" },
),
files: Type.Optional(Type.Array(Type.String({ description: "File paths for diagnostics" }))),
file: Type.Optional(Type.String({ description: "File path for file-specific actions" })),
line: Type.Optional(Type.Number({ description: "1-based line number" })),
column: Type.Optional(Type.Number({ description: "1-based column number" })),
end_line: Type.Optional(Type.Number({ description: "1-based end line number for ranges" })),
end_character: Type.Optional(Type.Number({ description: "1-based end column number for ranges" })),
query: Type.Optional(Type.String({ description: "Search query for symbols/SSR pattern" })),
new_name: Type.Optional(Type.String({ description: "New name for rename action" })),
replacement: Type.Optional(Type.String({ description: "Replacement for SSR action" })),
kind: Type.Optional(Type.String({ description: "Code action kind filter (quickfix, refactor, source)" })),
apply: Type.Optional(Type.Boolean({ description: "Apply edits instead of preview (default: true)" })),
action_index: Type.Optional(Type.Number({ description: "Index of code action to apply" })),
include_declaration: Type.Optional(
Type.Boolean({ description: "Include declaration in references (default: true)" }),
{ description: "LSP operation" },
),
files: Type.Optional(Type.Array(Type.String({ description: "File path" }))),
file: Type.Optional(Type.String({ description: "File path" })),
line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })),
column: Type.Optional(Type.Number({ description: "Column number (1-indexed)" })),
end_line: Type.Optional(Type.Number({ description: "End line for range (1-indexed)" })),
end_character: Type.Optional(Type.Number({ description: "End column for range (1-indexed)" })),
query: Type.Optional(Type.String({ description: "Search query or SSR pattern" })),
new_name: Type.Optional(Type.String({ description: "New name for rename" })),
replacement: Type.Optional(Type.String({ description: "Replacement text for SSR" })),
kind: Type.Optional(Type.String({ description: "Action kind: quickfix, refactor, source" })),
apply: Type.Optional(Type.Boolean({ description: "Apply edits (default: true)" })),
action_index: Type.Optional(Type.Number({ description: "Index of action to apply" })),
include_declaration: Type.Optional(Type.Boolean({ description: "Include declaration in refs (default: true)" })),
});
export type LspParams = Static<typeof lspSchema>;
@@ -90,27 +90,21 @@ export { ApplyPatchError, EditMatchError, ParseError } from "./types";
// ═══════════════════════════════════════════════════════════════════════════
const replaceEditSchema = Type.Object({
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
oldText: Type.String({
description: "Text to find and replace (high-confidence fuzzy matching for whitespace/indentation is always on)",
}),
newText: Type.String({ description: "New text to replace the old text with" }),
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences instead of requiring unique match" })),
path: Type.String({ description: "File path (relative or absolute)" }),
oldText: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
newText: Type.String({ description: "Replacement text" }),
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
});
const patchEditSchema = Type.Object({
path: Type.String({ description: "Path to the file" }),
path: Type.String({ description: "File path" }),
op: Type.Optional(
StringEnum(["create", "delete", "update"], {
description: "The operation to perform (Defaults to 'update')",
}),
),
rename: Type.Optional(Type.String({ description: "New path, if moving" })),
diff: Type.Optional(
Type.String({
description: "Diff hunk(s) for update. Full content for create.",
description: "Operation (default: update)",
}),
),
rename: Type.Optional(Type.String({ description: "New path for move" })),
diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
});
export type ReplaceParams = { path: string; oldText: string; newText: string; all?: boolean };
+6 -14
View File
@@ -41,22 +41,14 @@ function groupPreludeHelpers(helpers: PreludeHelper[]): PreludeCategory[] {
export const pythonSchema = Type.Object({
cells: Type.Array(
Type.Object({
code: Type.String({
description:
"Python code for this cell. Keep it focused (imports, helper, test, use). No narrative text—put explanations in the assistant message or in the cell title.",
}),
title: Type.Optional(
Type.String({ description: "Short label for the cell (e.g., 'imports', 'parse helper')." }),
),
code: Type.String({ description: "Python code to execute" }),
title: Type.Optional(Type.String({ description: "Cell label, e.g. 'imports', 'helper'" })),
}),
{
description:
"Python cells to execute sequentially. Each cell runs in the same kernel—imports and variables persist. Keep cells small: one logical step each (import, define, test, use). If a cell fails, fix only that cell; earlier cells' state remains.",
},
{ description: "Cells to execute sequentially in persistent kernel" },
),
timeoutMs: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 30000)" })),
cwd: Type.Optional(Type.String({ description: "Working directory for the command (default: current directory)" })),
reset: Type.Optional(Type.Boolean({ description: "Restart the kernel before executing this code" })),
timeoutMs: Type.Optional(Type.Number({ description: "Timeout in ms (default: 30000)" })),
cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
reset: Type.Optional(Type.Boolean({ description: "Restart kernel before execution" })),
});
export type PythonToolParams = Static<typeof pythonSchema>;
@@ -40,12 +40,12 @@ export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
/** Single task item for parallel execution */
export const taskItemSchema = Type.Object({
id: Type.String({
description: "Short task identifier for display (max 32 chars, CamelCase, e.g. 'SessionStore', 'WebFetchFix')",
description: "Task ID, CamelCase, max 32 chars",
maxLength: 32,
}),
description: Type.String({ description: "Short description for UI display" }),
description: Type.String({ description: "Short description for display" }),
vars: Type.Record(Type.String(), Type.String(), {
description: "Template variables to fill {{placeholders}} in context",
description: "Variables to fill {{placeholders}} in context",
}),
});
@@ -53,22 +53,13 @@ export type TaskItem = Static<typeof taskItemSchema>;
/** Task tool parameters */
export const taskSchema = Type.Object({
agent: Type.String({ description: "Agent type to use for all tasks" }),
context: Type.String({ description: "Template with {{placeholders}} filled by task vars" }),
isolated: Type.Optional(
Type.Boolean({
description: "Run each task in an isolated git worktree",
}),
),
agent: Type.String({ description: "Agent type for all tasks" }),
context: Type.String({ description: "Template with {{placeholders}} for vars" }),
isolated: Type.Optional(Type.Boolean({ description: "Run in isolated git worktree" })),
output: Type.Optional(
Type.Record(Type.String(), Type.Unknown(), {
description: "JTD schema for structured subagent output",
}),
Type.Record(Type.String(), Type.Unknown(), { description: "JTD schema for structured output" }),
),
tasks: Type.Array(taskItemSchema, {
description: "Tasks to run in parallel",
maxItems: MAX_PARALLEL_TASKS,
}),
tasks: Type.Array(taskItemSchema, { description: "Tasks to run in parallel", maxItems: MAX_PARALLEL_TASKS }),
});
export type TaskParams = Static<typeof taskSchema>;
@@ -872,11 +872,9 @@ async function renderUrl(
// =============================================================================
const webFetchSchema = Type.Object({
url: Type.String({ description: "The URL to fetch and render" }),
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 20, max: 120)" })),
raw: Type.Optional(
Type.Boolean({ description: "Return raw content without site-specific rendering or LLM-friendly transforms" }),
),
url: Type.String({ description: "URL to fetch" }),
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 20, max: 45)" })),
raw: Type.Optional(Type.Boolean({ description: "Return raw HTML without transforms" })),
});
export interface WebFetchToolDetails {
@@ -38,20 +38,16 @@ export const webSearchSchema = Type.Object({
query: Type.String({ description: "Search query" }),
provider: Type.Optional(
StringEnum(["auto", "exa", "anthropic", "perplexity"], {
description: "Search provider (auto-detected if omitted or set to auto)",
description: "Search provider (default: auto)",
}),
),
num_results: Type.Optional(Type.Number({ description: "Maximum number of results to return" })),
num_results: Type.Optional(Type.Number({ description: "Max results to return" })),
// Common (Anthropic & Perplexity)
system_prompt: Type.Optional(
Type.String({
description: "System prompt to guide response style",
}),
),
system_prompt: Type.Optional(Type.String({ description: "System prompt for response style" })),
max_tokens: Type.Optional(
Type.Number({
description: "Maximum tokens in response, 1-16384, default 4096 (Anthropic only)",
description: "Max tokens, 1-16384 (default: 4096, Anthropic only)",
minimum: 1,
maximum: 16384,
}),
@@ -60,29 +56,21 @@ export const webSearchSchema = Type.Object({
// Perplexity-specific
model: Type.Optional(
StringEnum(["sonar", "sonar-pro"], {
description: "Perplexity model - sonar (fast) or sonar-pro (comprehensive research)",
description: "Perplexity model: sonar or sonar-pro",
}),
),
search_recency_filter: Type.Optional(
StringEnum(["day", "week", "month", "year"], {
description: "Filter results by recency (Perplexity only)",
description: "Recency filter (Perplexity)",
}),
),
search_domain_filter: Type.Optional(
Type.Array(Type.String(), {
description: "Domain filter - include domains, prefix with - to exclude (Perplexity only)",
}),
Type.Array(Type.String(), { description: "Domain filter, prefix - to exclude (Perplexity)" }),
),
search_context_size: Type.Optional(
StringEnum(["low", "medium", "high"], {
description: "Context size for cost control (Perplexity only)",
}),
),
return_related_questions: Type.Optional(
Type.Boolean({
description: "Include follow-up question suggestions, default true (Perplexity only)",
}),
StringEnum(["low", "medium", "high"], { description: "Context size (Perplexity)" }),
),
return_related_questions: Type.Optional(Type.Boolean({ description: "Include related questions (Perplexity)" })),
});
export type WebSearchParams = {
@@ -13,7 +13,7 @@ Do NOT use Bash for:
</critical>
<instruction>
- Use `workdir` parameter to run commands in a specific directory instead of `cd dir && ...`
- Use `cwd` parameter to set working directory instead of `cd dir && ...`
- Paths with spaces must use double quotes: `cd "/path/with spaces"`
- For sequential dependent operations, chain with `&&`: `mkdir foo && cd foo && touch bar`
- For parallel independent operations, make multiple tool calls in one message
+10 -24
View File
@@ -1,32 +1,18 @@
# Read
Reads a file from the local filesystem. You can access any file directly by using this tool.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
Reads a file from the local filesystem.
<instruction>
- By default, it reads up to {{DEFAULT_MAX_LINES}} lines starting from the beginning of the file
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
- Any lines longer than 500 characters will be truncated
- By default, results omit line numbers. Use `lines: true` to include them
- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.
- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.
- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.
- This tool can only read files, not directories. To read a directory, use the ls tool.
- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.
- You will regularly be asked to read screenshots. If the user provides a path to a screenshot, ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
- Reads up to {{DEFAULT_MAX_LINES}} lines by default
- Use `offset` and `limit` for large files
- Use `lines: true` to include line numbers
- Supports images (PNG, JPG), PDFs, and Jupyter notebooks
- For directories, use the ls tool instead
- Parallelize reads when exploring related files
</instruction>
<important>
- Empty files trigger a warning
- Directory paths return an ls-style listing
- Missing files return an error with closest matches (gitignore respected)
</important>
<required>
- Parallelize reads when exploring related files
- Read before editing (required in current session)
- Trust user-provided paths; attempt the read
- Screenshots: Read tool renders images visually
- Skip re-reading after edits (Edit/Write report errors)
</required>
- Empty files trigger a warning
- Missing files return closest matches
</important>
@@ -3,14 +3,11 @@
Performs string replacements in files with fuzzy whitespace matching.
<instruction>
- Use the smallest edit that uniquely identifies the change. To toggle a checkbox: `- [ ] Task` → `- [x] Task`, not the entire line.
- If the old text is not unique, expand the replacement to a larger block (function/class/section) so it is unique.
- Use the smallest edit that uniquely identifies the change
- If `oldText` is not unique, expand to include more context or use `all: true` to replace all occurrences
- You must use your read tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- Fuzzy matching handles minor whitespace/indentation differences automatically - you don't need to match indentation exactly.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if old_string is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all to change every instance of old_string.
- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
- Fuzzy matching handles minor whitespace/indentation differences automatically
- Prefer editing existing files over creating new ones
</instruction>
<bash_alternatives>
@@ -53,10 +53,9 @@ Agents with `output="structured"` have a fixed schema enforced via frontmatter;
<parameters>
- `agent`: Agent type to use for all tasks
- `context`: Template with `{{placeholders}}` for multi-task. Each placeholder is filled from task vars.
- `model`: (optional) Model override for all tasks (fuzzy matching, e.g., "sonnet", "opus")
- `isolated`: (optional) Run each task in its own git worktree and return patches; patches are applied only if all apply cleanly.
- `tasks`: Array of `{id, description, vars}` - tasks to run in parallel (max {{MAX_PARALLEL_TASKS}}, {{MAX_CONCURRENCY}} concurrent)
- `id`: Short CamelCase identifier for display (max 20 chars, e.g., "SessionStore", "LspRefactor")
- `tasks`: Array of `{id, description, vars}` - tasks to run in parallel
- `id`: Short CamelCase identifier (max 32 chars, e.g., "SessionStore", "LspRefactor")
- `description`: Short human-readable description of what the task does
- `vars`: Object with keys matching `{{placeholders}}` in context
- `output`: (optional) JTD schema for structured subagent output (used by the complete tool)