diff --git a/README.md b/README.md index 656e9b4..2a4302e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ grok fix the flaky test in src/foo.test.ts | **Grok-native** | Defaults tuned for Grok; models like **`grok-code-fast-1`**, **`grok-4-1-fast`**, flagship and fast variants—run `grok models` for the full menu. | | **X + web search** | **`search_x`** and **`search_web`** tools—live posts and docs without pretending the internet stopped in 2023. | | **Sub-agents (default behavior)** | Foreground **`task`** delegation (e.g. explore vs general) plus background **`delegate`** for read-only deep dives—parallelize like you mean it. | +| **Custom sub-agents** | Define named agents with **`subAgents`** in **`~/.grok/user-settings.json`** and manage them from the TUI with **`/agents`**. | | **Remote control** | Pair **Telegram** from the TUI (`/remote-control` → Telegram): DM your bot, **`/pair`**, approve the code in-terminal. Keep the CLI running while you ping it from your phone. | | **No “mystery meat” UI** | OpenTUI React terminal UI—fast, keyboard-driven, not whatever glitchy thing you’re thinking of. | | **Skills** | Agent Skills under **`.agents/skills//SKILL.md`** (project) or **`~/.agents/skills/`** (user). Use **`/skills`** in the TUI to list what’s installed. | @@ -129,6 +130,22 @@ grok -k your_key_here { "apiKey": "your_key_here" } ``` +Optional **`subAgents`** — custom foreground sub-agents. Each entry needs **`name`**, **`model`**, and **`instruction`**: + +```json +{ + "subAgents": [ + { + "name": "security-review", + "model": "grok-code-fast-1", + "instruction": "Prioritize security implications and suggest concrete fixes." + } + ] +} +``` + +Names cannot be `general` or `explore` because those are reserved for the built-in sub-agents. + Optional: **`GROK_BASE_URL`** (default `https://api.x.ai/v1`), **`GROK_MODEL`**, **`GROK_MAX_TOKENS`**. --- diff --git a/package.json b/package.json index 4921e9e..b625baf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grok-dev", - "version": "1.0.0-rc3", + "version": "1.0.0-rc4", "description": "An open-source AI coding agent powered by Grok, built with Bun and OpenTUI.", "type": "module", "main": "dist/index.js", diff --git a/src/agent/agent.ts b/src/agent/agent.ts index ae94b2f..d15c867 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -31,7 +31,7 @@ import type { WorkspaceInfo, } from "../types/index"; import { loadCustomInstructions } from "../utils/instructions"; -import { loadMcpServers } from "../utils/settings"; +import { type CustomSubagentConfig, loadMcpServers, loadValidSubAgents } from "../utils/settings"; import { discoverSkills, formatSkillsForPrompt } from "../utils/skills"; import { type CompactionSettings, @@ -119,7 +119,7 @@ TOOLS: - process_logs: View recent output from a background process by ID. - process_stop: Stop a background process by ID. - process_list: List all background processes with status and uptime. -- task: Delegate a focused foreground task to a sub-agent. Use general for multi-step execution and explore for fast read-only research. +- task: Delegate a focused foreground task to a sub-agent. Use general for multi-step execution, explore for fast read-only research, or a configured custom sub-agent name when listed under CUSTOM SUB-AGENTS. - delegate: Launch a read-only background agent for longer research while you continue working. - delegation_read: Retrieve a completed background delegation result by ID. - delegation_list: List running and completed background delegations. Do not poll it repeatedly. @@ -143,6 +143,7 @@ DEFAULT DELEGATION POLICY: - Prefer delegate for longer-running read-only exploration when you can keep making progress without blocking. - Use the explore sub-agent for read-only investigation, reviews, research, and "how does this work?" tasks. - Use the general sub-agent for delegated work that may need editing files, running commands, or producing a concrete implementation. +- Use a matching custom sub-agent when the task fits one of the configured specializations. - Never use delegate for tasks that should edit files or make shell changes. - When a background delegation is running, do not wait idly and do not spam delegation_list(). Continue useful work. - Do not wait for the user to explicitly ask for a sub-agent when delegation would clearly help. @@ -153,6 +154,7 @@ EXAMPLES: - "research how auth works" -> delegate to explore first - "investigate why this test fails" -> delegate to explore first, then continue with findings - "refactor this module" -> delegate a focused part to general when helpful +- Recurring specialized workflows -> use the matching custom sub-agent via task IMPORTANT: - Prefer edit_file for surgical changes to existing files — it shows a clean diff. @@ -168,6 +170,7 @@ ${ENVIRONMENT} TOOLS: - read_file: Read file contents for analysis. - bash: ONLY for searching (find, grep, ls) — NEVER modify files. +- task: Delegate a focused task to a sub-agent when deeper research or specialized analysis would help. - generate_plan: ALWAYS use this to present your plan. Creates an interactive UI with steps and questions. BEHAVIOR: @@ -186,6 +189,7 @@ ${ENVIRONMENT} TOOLS: - read_file: Read file contents for context. - bash: ONLY for searching (find, grep, ls) — NEVER modify. +- task: Delegate a focused task to a sub-agent when specialized analysis or deeper investigation would help. BEHAVIOR: - Answer the user's question directly and thoroughly @@ -195,7 +199,33 @@ BEHAVIOR: - Focus on explanation, not execution`, }; -function buildSystemPrompt(cwd: string, mode: AgentMode, planContext?: string | null): string { +function findCustomSubagent( + agent: string, + subagents: CustomSubagentConfig[] = loadValidSubAgents(), +): CustomSubagentConfig | undefined { + return ( + subagents.find((item) => item.name === agent) ?? + subagents.find((item) => item.name.toLowerCase() === agent.toLowerCase()) + ); +} + +function formatCustomSubagentsPromptSection(subagents: CustomSubagentConfig[]): string { + if (subagents.length === 0) return ""; + + const lines = subagents.map((agent) => { + const instruction = agent.instruction.trim() || "(none)"; + return `### ${agent.name}\n- model: ${agent.model}\n- instruction:\n${instruction}`; + }); + + return `\n\nCUSTOM SUB-AGENTS:\nUser-defined foreground sub-agents from ~/.grok/user-settings.json. When one matches the task, call the task tool with agent set to the exact name.\n\n${lines.join("\n\n")}\n`; +} + +function buildSystemPrompt( + cwd: string, + mode: AgentMode, + planContext?: string | null, + subagents?: CustomSubagentConfig[], +): string { const custom = loadCustomInstructions(cwd); const customSection = custom ? `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above alongside standard instructions.\n` @@ -203,38 +233,48 @@ function buildSystemPrompt(cwd: string, mode: AgentMode, planContext?: string | const skillsText = formatSkillsForPrompt(discoverSkills(cwd)); const skillsSection = skillsText ? `\n\n${skillsText}\n` : ""; + const subagentsSection = formatCustomSubagentsPromptSection(subagents ?? loadValidSubAgents()); const planSection = planContext ? `\n\nAPPROVED PLAN:\nThe following plan has been approved by the user. Execute it now.\n${planContext}\n` : ""; - return `${MODE_PROMPTS[mode]}${customSection}${skillsSection}${planSection} + return `${MODE_PROMPTS[mode]}${customSection}${skillsSection}${subagentsSection}${planSection} Current working directory: ${cwd}`; } -function buildSubagentPrompt(request: TaskRequest, cwd: string): string { - const mode: AgentMode = request.agent === "explore" ? "ask" : "agent"; - const role = - request.agent === "explore" +function buildSubagentPrompt( + request: TaskRequest, + cwd: string, + custom: CustomSubagentConfig | null, + subagents?: CustomSubagentConfig[], +): string { + const isExplore = request.agent === "explore"; + const mode: AgentMode = isExplore ? "ask" : "agent"; + const role = custom + ? `You are the custom sub-agent "${custom.name}". You can investigate, edit files, and run commands unless the delegated task says otherwise.` + : request.agent === "explore" ? "You are the Explore sub-agent. You are read-only and focus on fast codebase research." : "You are the General sub-agent. You can investigate, edit files, and run commands to complete delegated work."; - const rules = - request.agent === "explore" - ? [ - "Do not create, modify, or delete files.", - "Prefer `read_file` and search commands over broad shell exploration.", - "Return concise findings for the parent agent.", - ] - : [ - "Work only on the delegated task below.", - "Use tools directly instead of narrating your intent.", - "Return a concise summary for the parent agent with key outcomes and any open risks.", - ]; + const rules = isExplore + ? [ + "Do not create, modify, or delete files.", + "Prefer `read_file` and search commands over broad shell exploration.", + "Return concise findings for the parent agent.", + ] + : [ + "Work only on the delegated task below.", + "Use tools directly instead of narrating your intent.", + "Return a concise summary for the parent agent with key outcomes and any open risks.", + ]; + + const instructionLines = custom?.instruction.trim() ? ["", "SUB-AGENT INSTRUCTIONS:", custom.instruction.trim()] : []; return [ role, + ...instructionLines, "", "You are helping a parent agent. Do not address the end user directly.", "Focus tightly on the delegated scope and summarize what matters back to the parent agent.", @@ -243,7 +283,7 @@ function buildSubagentPrompt(request: TaskRequest, cwd: string): string { "", `Delegated task: ${request.description}`, "", - buildSystemPrompt(cwd, mode), + buildSystemPrompt(cwd, mode, undefined, subagents), ].join("\n"); } @@ -486,14 +526,34 @@ export class Agent { ): Promise { const provider = this.requireProvider(); const signal = abortSignal; - const childMode: AgentMode = request.agent === "explore" ? "ask" : "agent"; + const agentKey = String(request.agent); + const isExplore = agentKey === "explore"; + const isGeneral = agentKey === "general"; + const subagents = loadValidSubAgents(); + const custom = !isExplore && !isGeneral ? findCustomSubagent(agentKey, subagents) : undefined; + + if (!isExplore && !isGeneral && !custom) { + const message = `Unknown sub-agent "${agentKey}". Use general, explore, or a configured name from ~/.grok/user-settings.json.`; + return { + success: false, + output: message, + task: { + agent: agentKey, + description: request.description, + summary: message, + }, + }; + } + + const childMode: AgentMode = isExplore ? "ask" : "agent"; const childBash = new BashTool(this.bash.getCwd()); const childBaseTools = createTools(childBash, provider, childMode); - const initialDetail = request.agent === "explore" ? "Scanning the codebase" : "Planning delegated work"; + const initialDetail = isExplore ? "Scanning the codebase" : "Planning delegated work"; let assistantText = ""; let lastActivity = initialDetail; let childTools: ToolSet = childBaseTools; let closeMcp: (() => Promise) | undefined; + const childModelId = isExplore ? "grok-4-1-fast" : custom ? custom.model : this.modelId; onActivity?.(initialDetail); @@ -509,17 +569,17 @@ export class Agent { } const result = streamText({ - model: provider(request.agent === "explore" ? "grok-4-1-fast" : this.modelId), - system: buildSubagentPrompt(request, childBash.getCwd()), + model: provider(childModelId), + system: buildSubagentPrompt(request, childBash.getCwd(), custom ?? null, subagents), messages: [{ role: "user", content: request.prompt }], tools: childTools, - stopWhen: stepCountIs(Math.min(this.maxToolRounds, request.agent === "explore" ? 60 : 120)), + stopWhen: stepCountIs(Math.min(this.maxToolRounds, isExplore ? 60 : 120)), maxRetries: 0, abortSignal: signal, - temperature: request.agent === "explore" ? 0.2 : 0.5, + temperature: isExplore ? 0.2 : 0.5, maxOutputTokens: Math.min(this.maxTokens, 8_192), onFinish: ({ totalUsage }) => { - this.recordUsage(totalUsage, "task"); + this.recordUsage(totalUsage, "task", childModelId); }, }); @@ -728,7 +788,8 @@ export class Agent { this.messageSeqs.push(null); const provider = this.requireProvider(); - const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext); + const subagents = loadValidSubAgents(); + const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext, subagents); const modelInfo = getModelInfo(this.modelId); this.planContext = null; let attemptedOverflowRecovery = false; @@ -761,6 +822,7 @@ export class Agent { this.runDelegation(request, combineAbortSignals(signal, abortSignal)), readDelegation: (id) => this.readDelegation(id), listDelegations: () => this.listDelegations(), + subagents, }); let tools: ToolSet = baseTools; if (this.mode === "agent") { diff --git a/src/grok/tools.ts b/src/grok/tools.ts index a7b72e2..4b447ff 100644 --- a/src/grok/tools.ts +++ b/src/grok/tools.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import type { BashTool } from "../tools/bash"; import { editFile, readFile, writeFile } from "../tools/file"; import type { AgentMode, TaskRequest, ToolResult } from "../types/index"; +import { type CustomSubagentConfig, loadValidSubAgents } from "../utils/settings"; import type { XaiProvider } from "./client"; const RESPONSES_SEARCH_MODEL = "grok-4-fast-non-reasoning"; @@ -12,6 +13,7 @@ interface CreateToolsOptions { runDelegation?: (request: TaskRequest, abortSignal?: AbortSignal) => Promise; readDelegation?: (id: string) => Promise; listDelegations?: () => Promise; + subagents?: CustomSubagentConfig[]; } export function createTools( @@ -25,12 +27,14 @@ export function createTools( const runResponsesSearch = async ( query: string, toolName: "web_search" | "x_search", + abortSignal?: AbortSignal, ): Promise<{ success: boolean; output: string }> => { try { const { text } = await generateText({ model: provider.responses(RESPONSES_SEARCH_MODEL), maxOutputTokens: 4096, prompt: query, + abortSignal, tools: { ...(toolName === "web_search" ? { web_search: provider.tools.webSearch() } : {}), ...(toolName === "x_search" ? { x_search: provider.tools.xSearch() } : {}), @@ -67,11 +71,11 @@ export function createTools( "Run the command as a background process. Returns immediately with a process ID. Use process_logs/process_stop/process_list to manage it.", ), }), - execute: async ({ command, timeout, background }) => { + execute: async ({ command, timeout, background }, { abortSignal }) => { if (background) { return bash.startBackground(command); } - const result = await bash.execute(command, timeout); + const result = await bash.execute(command, timeout, abortSignal); return { success: result.success, output: result.success @@ -131,8 +135,8 @@ export function createTools( inputSchema: z.object({ query: z.string().describe("The search query"), }), - execute: async ({ query }) => { - return runResponsesSearch(query, "web_search"); + execute: async ({ query }, { abortSignal }) => { + return runResponsesSearch(query, "web_search", abortSignal); }, }), @@ -142,14 +146,42 @@ export function createTools( inputSchema: z.object({ query: z.string().describe("The search query"), }), - execute: async ({ query }) => { - return runResponsesSearch(query, "x_search"); + execute: async ({ query }, { abortSignal }) => { + return runResponsesSearch(query, "x_search", abortSignal); }, }), }; const tools: ToolSet = { ...base }; + if (options.runTask) { + const customNames = (options.subagents ?? loadValidSubAgents()).map((agent) => agent.name); + const taskAgentEnum = ["general", "explore", ...customNames] as [string, ...string[]]; + const customHint = + customNames.length > 0 + ? ` You may also use these user-defined sub-agents by exact name: ${customNames.join(", ")}.` + : ""; + + tools.task = tool({ + description: `Delegate a focused foreground task to a sub-agent. Prefer this proactively for review, research, investigation, and code quality work instead of waiting for the user to request a sub-agent. Use \`general\` for multi-step execution and \`explore\` for fast read-only investigation.${customHint} Provide a short description plus a detailed prompt for the child agent.`, + inputSchema: z.object({ + agent: z + .enum(taskAgentEnum) + .default("general") + .describe( + customNames.length > 0 + ? "Built-in general or explore, or a configured custom sub-agent name from user settings" + : "Which sub-agent to use", + ), + description: z.string().describe("A short label for the delegated task, such as 'Deep code quality analysis'"), + prompt: z.string().describe("Detailed instructions for the sub-agent to complete"), + }), + execute: async ({ agent, description, prompt }, { abortSignal }) => { + return options.runTask!({ agent, description, prompt }, abortSignal); + }, + }); + } + if (mode === "agent") { tools.write_file = tool({ description: @@ -176,23 +208,6 @@ export function createTools( }, }); - if (options.runTask) { - tools.task = tool({ - description: - "Delegate a focused foreground task to a sub-agent. Prefer this proactively for review, research, investigation, and code quality work instead of waiting for the user to request a sub-agent. Use `general` for multi-step execution and `explore` for fast read-only investigation. Provide a short description plus a detailed prompt for the child agent.", - inputSchema: z.object({ - agent: z.enum(["general", "explore"]).default("general").describe("Which sub-agent to use"), - description: z - .string() - .describe("A short label for the delegated task, such as 'Deep code quality analysis'"), - prompt: z.string().describe("Detailed instructions for the sub-agent to complete"), - }), - execute: async ({ agent, description, prompt }, { abortSignal }) => { - return options.runTask!({ agent, description, prompt }, abortSignal); - }, - }); - } - if (options.runDelegation) { tools.delegate = tool({ description: diff --git a/src/tools/bash.ts b/src/tools/bash.ts index 921b104..04d47ee 100644 --- a/src/tools/bash.ts +++ b/src/tools/bash.ts @@ -3,11 +3,8 @@ import { createReadStream, createWriteStream } from "fs"; import { mkdtemp, rm, stat, unlink } from "fs/promises"; import os from "os"; import path from "path"; -import { promisify } from "util"; import type { ToolResult } from "../types/index"; -const execAsync = promisify(exec); - const MAX_TAIL_BYTES = 8_192; const MAX_BACKGROUND_PROCESSES = 8; @@ -41,7 +38,7 @@ export class BashTool { return this.tmpDir; } - async execute(command: string, timeout = 30_000): Promise { + async execute(command: string, timeout = 30_000, abortSignal?: AbortSignal): Promise { try { if (command.startsWith("cd ")) { const dir = command @@ -62,18 +59,74 @@ export class BashTool { } } - const { stdout, stderr } = await execAsync(command, { - cwd: this.cwd, - timeout, - maxBuffer: 10 * 1024 * 1024, - env: { ...process.env, FORCE_COLOR: "0" }, - }); + if (abortSignal?.aborted) { + return { success: false, error: "[Cancelled]" }; + } - const output = stdout + (stderr ? `\nSTDERR: ${stderr}` : ""); - return { - success: true, - output: output.trim() || "Command executed successfully (no output)", - }; + return await new Promise((resolve) => { + let settled = false; + let aborted = false; + let forceKillTimer: ReturnType | undefined; + + const finish = (result: ToolResult) => { + if (settled) return; + settled = true; + if (forceKillTimer) clearTimeout(forceKillTimer); + abortSignal?.removeEventListener("abort", onAbort); + resolve(result); + }; + + const child = exec( + command, + { + cwd: this.cwd, + timeout, + maxBuffer: 10 * 1024 * 1024, + env: { ...process.env, FORCE_COLOR: "0" }, + }, + (err, stdout, stderr) => { + if (aborted || abortSignal?.aborted) { + finish({ success: false, error: "[Cancelled]" }); + return; + } + + const output = stdout + (stderr ? `\nSTDERR: ${stderr}` : ""); + if (err) { + if (output.trim()) { + finish({ success: false, error: output.trim() }); + return; + } + finish({ success: false, error: `Command failed: ${err.message}` }); + return; + } + + finish({ + success: true, + output: output.trim() || "Command executed successfully (no output)", + }); + }, + ); + + const onAbort = () => { + aborted = true; + try { + child.kill("SIGTERM"); + } catch { + finish({ success: false, error: "[Cancelled]" }); + return; + } + + forceKillTimer = setTimeout(() => { + try { + child.kill("SIGKILL"); + } catch { + /* already exited */ + } + }, 1_000); + }; + + abortSignal?.addEventListener("abort", onAbort, { once: true }); + }); } catch (err: unknown) { if (err && typeof err === "object" && "stdout" in err) { const execErr = err as { stdout?: string; stderr?: string; message: string }; diff --git a/src/types/index.ts b/src/types/index.ts index 858e894..20f39a9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -29,14 +29,16 @@ export interface Plan { questions?: PlanQuestion[]; } +export type BuiltinSubagentId = "general" | "explore"; + export interface TaskRequest { - agent: "general" | "explore"; + agent: BuiltinSubagentId | string; description: string; prompt: string; } export interface TaskRun { - agent: "general" | "explore"; + agent: string; description: string; summary: string; activity?: string; @@ -53,7 +55,7 @@ export interface DelegationRun { } export interface SubagentStatus { - agent: "general" | "explore"; + agent: string; description: string; detail: string; } diff --git a/src/ui/agents-modal.tsx b/src/ui/agents-modal.tsx new file mode 100644 index 0000000..b7f4634 --- /dev/null +++ b/src/ui/agents-modal.tsx @@ -0,0 +1,271 @@ +import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core"; +import { type RefObject, useEffect, useRef } from "react"; +import { MODELS } from "../grok/models"; +import type { CustomSubagentConfig } from "../utils/settings"; +import type { Theme } from "./theme"; + +const EDITOR_KEYBINDINGS = [{ name: "return", action: "submit" as const }]; + +export type SubagentBrowseRow = { kind: "agent"; agent: CustomSubagentConfig }; +export const SUBAGENT_EDITOR_FIELDS = ["name", "model", "instruction"] as const; +export type SubagentEditorField = (typeof SUBAGENT_EDITOR_FIELDS)[number]; + +export function buildSubagentBrowseRows(agents: CustomSubagentConfig[], query: string): SubagentBrowseRow[] { + const q = query.trim().toLowerCase(); + const filtered = q + ? agents.filter( + (agent) => + agent.name.toLowerCase().includes(q) || + agent.model.toLowerCase().includes(q) || + agent.instruction.toLowerCase().includes(q), + ) + : agents; + + return filtered.map((agent) => ({ kind: "agent" as const, agent })); +} + +function bottomAlignedModalTop(height: number, panelHeight: number): number { + return Math.max(2, Math.floor((height - panelHeight) / 2)); +} + +function syncRef(ref: RefObject, value: string): void { + ref.current?.clear(); + if (value) { + ref.current?.insertText(value); + } +} + +export function SubagentsBrowserModal({ + t, + width, + height, + selectedIndex, + searchQuery, + rows, +}: { + t: Theme; + width: number; + height: number; + selectedIndex: number; + searchQuery: string; + rows: SubagentBrowseRow[]; +}) { + const listRef = useRef(null); + + useEffect(() => { + const selected = rows[selectedIndex]; + if (!selected) return; + + listRef.current?.scrollChildIntoView(`subagent-${selected.agent.name}`); + }, [rows, selectedIndex]); + + const itemCount = Math.max(rows.length, 1); + const contentHeight = itemCount + 8; + const panelHeight = Math.min(contentHeight, Math.floor(height * 0.6)); + const panelWidth = Math.min(60, width - 6); + const overlayBg = "#000000cc" as string; + + return ( + + + + + {"Custom sub-agents"} + + {"esc"} + + + + {searchQuery || {"Search by name, model..."}} + + + + {rows.map((row, idx) => { + const selected = idx === selectedIndex; + + return ( + + + + {row.agent.name} + + {row.agent.model} + + + ); + })} + {rows.length === 0 ? ( + + {"No custom sub-agents yet"} + + ) : null} + + + + {"enter "} + {"open selected · "} + {"ctrl+a "} + {"add"} + + + + + ); +} + +export function SubagentEditorModal({ + t, + width, + height, + draft, + focusedField, + modelIndex, + error, + title, + nameRef, + instructionRef, + onSubmit, + showRemoveHint, +}: { + t: Theme; + width: number; + height: number; + draft: { name: string; instruction: string }; + focusedField: SubagentEditorField; + modelIndex: number; + error: string | null; + title: string; + nameRef: RefObject; + instructionRef: RefObject; + onSubmit: () => void; + showRemoveHint?: boolean; +}) { + const model = MODELS[modelIndex] ?? MODELS[0]; + const panelWidth = Math.min(68, width - 6); + const panelHeight = Math.min(28, Math.floor(height * 0.75)); + const overlayBg = "#000000cc" as string; + + useEffect(() => { + syncRef(nameRef, draft.name); + syncRef(instructionRef, draft.instruction); + }, [draft, nameRef, instructionRef]); + + return ( + + + + + {title} + + {"esc back"} + + + + {"Name (task tool agent value)"} + +