Skip to main content

vtcode_config/subagents/
builtin.rs

1//! Built-in primary and subagent specifications.
2
3use std::collections::BTreeMap;
4use std::sync::LazyLock;
5
6use vtcode_commons::reasoning::ReasoningEffortLevel;
7
8use crate::constants::tools;
9use crate::constants::ui;
10use crate::core::permissions::{AgentPermissionsConfig, PermissionDefault};
11use crate::core::tools::ToolPolicy;
12
13use super::{AgentMode, SubagentSource, SubagentSpec};
14
15const BUILTIN_DEFAULT_AGENT: &str = r#"You are the default VT Code execution subagent.
16
17Work directly, keep context isolated from the parent session, and return concise summaries.
18Match the repository's local patterns, verify changes, and avoid unrelated edits.
19If a file is referenced, read it before answering; base claims about code on what you have read.
20Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
21Do not add features, refactor code, or make improvements beyond what was asked.
22Verify your work by running the smallest relevant check before reporting completion."#;
23
24const BUILTIN_EXPLORER_AGENT: &str = r#"You are a fast read-only exploration subagent.
25
26Search the codebase, inspect relevant files, and return concise findings with file references.
27Do not modify files or take mutating actions.
28Read files before making claims about their contents.
29Use structural search and grep over shell exploration when possible.
30Return findings with file paths and line numbers for easy navigation."#;
31
32const BUILTIN_WORKER_AGENT: &str = r#"You are a write-capable worker subagent.
33
34Handle bounded implementation work, verify results, and return a concise outcome summary with
35any important risks or follow-up items.
36Read files before editing them or describing what they contain.
37Only make changes that are directly requested. Keep solutions simple and focused.
38Do not add features, refactor surrounding code, or make improvements beyond the scope.
39Verify your changes by running relevant tests or checks before reporting completion.
40If calls repeat without progress, re-plan instead of retrying identically."#;
41
42const BUILTIN_BUILD_PRIMARY_AGENT: &str = r#"You are the build agent.
43
44Understand the user's request, then use available tools to inspect context and make changes.
45Read files before editing. Search before guessing at code structure.
46Edit, write, and run commands directly when the request is clear.
47Keep changes focused on what was asked. Do not add unrelated features or refactors.
48When planning is needed, state the plan briefly before implementation. When the user only wants
49discussion or review, do not edit files.
50Report changed files, validation, and remaining risks clearly."#;
51
52const BUILTIN_AUTO_PRIMARY_AGENT: &str = r#"You are the auto agent.
53
54Work autonomously within the active permission policy, taking direct action when the request is clear.
55Inspect the relevant repository context before editing, keep changes focused, and verify with the
56narrowest useful checks before reporting completion.
57Pause for user input when the scope is ambiguous, risky, or outside the requested work."#;
58
59const BUILTIN_COORDINATOR_PRIMARY_AGENT: &str = r#"You are the coordinator agent.
60
61Define explicit matrix tasks, dependencies, resource capacities, timeouts, and verification commands.
62Delegate all shell execution, file changes, and verification to scheduler-owned matrix workers.
63Use matrix create to persist a specification, then start to freeze it and dispatch work.
64Delegate discovery to a read-only explorer before making repository claims, while the matrix is idle.
65During active execution, use matrix status and control actions; do not spawn independent workers.
66Resolve failed checks, permission denials, exhausted budgets, and unsafe retries with the user.
67Worker summaries are not verification evidence. Report success only after matrix final verification.
68Pause stops dispatch; cancel is terminal. Preserve sandbox, approvals, and existing budgets."#;
69
70const DISCUSSION_FIRST_GUIDANCE: &str = r#"Be discussion-first. Clarify scope, constraints, contradictions, and options before implementation.
71Resolve ordinary ambiguity from repository evidence when possible; ask the user directly only when material ambiguity is critical.
72Stop researching when existing evidence supports a decision."#;
73
74const BUILTIN_PLAN_AGENT_ROLE: &str = r#"You are a read-only planning agent.
75
76Use repository-grounded, read-only discovery to gather the minimum context needed to support a plan or design decision.
77Return findings, risks, and constraints clearly, with specific code references and file paths.
78Read relevant files before making claims about the codebase.
79Use structural search to find patterns across the repository.
80When ready, emit exactly one final <proposed_plan> block for review.
81Never write the plan file with shell or file-editing tools; the runtime persists the plan and tracker artifacts.
82When the user asks for implementation, present the plan and wait for explicit user approval before implementation instead of suggesting an immediate edit."#;
83
84const BUILTIN_DUCK_PRIMARY_AGENT_ROLE: &str = r#"You are the duck agent.
85
86Do not edit files; you are for rubber-ducking only.
87If the user asks for edits, suggest pressing Tab to switch to the Build agent for implementation."#;
88/// Cached built-in subagent specifications.
89///
90/// Built once on first access and reused for every subsequent discovery call.
91/// The constructors below (`builtin_primary_build_agent`, etc.) each allocate
92/// a `SubagentSpec` with a `BTreeMap` of policy overrides and a cloned prompt
93/// string; reconstructing all of them on every `discover_subagents` call is the
94/// expensive part, so the result is memoized here. Callers still receive an
95/// owned `Vec` (cloned from this cache) so they can mutate freely without
96/// touching the shared canonical specs.
97static BUILTIN_SUBAGENTS: LazyLock<Vec<SubagentSpec>> = LazyLock::new(builtin_subagents_inner);
98
99pub fn builtin_subagents() -> Vec<SubagentSpec> {
100    BUILTIN_SUBAGENTS.clone()
101}
102
103fn builtin_subagents_inner() -> Vec<SubagentSpec> {
104    vec![
105        builtin_primary_build_agent(),
106        builtin_primary_auto_agent(),
107        builtin_primary_coordinator_agent(),
108        builtin_primary_duck_agent(),
109        builtin_plan_agent(),
110        SubagentSpec {
111            name: "default".to_string(),
112            description: "Default inheriting subagent for general delegated work.".to_string(),
113            prompt: BUILTIN_DEFAULT_AGENT.to_string(),
114            tools: None,
115            disallowed_tools: Vec::new(),
116            model: Some("inherit".to_string()),
117            color: Some("blue".to_string()),
118            reasoning_effort: None,
119            permissions: mutating_agent_permissions(),
120            skills: Vec::new(),
121            mcp_servers: Vec::new(),
122            hooks: None,
123            background: false,
124            mode: AgentMode::Subagent,
125            max_turns: None,
126            nickname_candidates: vec!["default".to_string()],
127            initial_prompt: None,
128            memory: None,
129            isolation: None,
130            aliases: Vec::new(),
131            source: SubagentSource::Builtin,
132            file_path: None,
133            warnings: Vec::new(),
134            tool_policy_overrides: BTreeMap::new(),
135        },
136        SubagentSpec {
137            name: "explorer".to_string(),
138            description: "Read-only exploration specialist. Use proactively for code search, file discovery, and repository understanding.".to_string(),
139            prompt: BUILTIN_EXPLORER_AGENT.to_string(),
140            tools: Some(builtin_readonly_tool_ids()),
141            disallowed_tools: builtin_readonly_disallowed_tool_ids(),
142            model: Some("small".to_string()),
143            color: Some("cyan".to_string()),
144            reasoning_effort: Some(ReasoningEffortLevel::Low),
145            permissions: readonly_agent_permissions(),
146            skills: Vec::new(),
147            mcp_servers: Vec::new(),
148            hooks: None,
149            background: false,
150            mode: AgentMode::Subagent,
151            max_turns: None,
152            nickname_candidates: vec!["explore".to_string(), "search".to_string()],
153            initial_prompt: None,
154            memory: None,
155            isolation: None,
156            aliases: vec!["explore".to_string()],
157            source: SubagentSource::Builtin,
158            file_path: None,
159            warnings: Vec::new(),
160            tool_policy_overrides: BTreeMap::new(),
161        },
162        SubagentSpec {
163            name: "worker".to_string(),
164            description: "Write-capable execution subagent for bounded implementation or multi-step action.".to_string(),
165            prompt: BUILTIN_WORKER_AGENT.to_string(),
166            tools: None,
167            disallowed_tools: Vec::new(),
168            model: Some("inherit".to_string()),
169            color: Some("magenta".to_string()),
170            reasoning_effort: None,
171            permissions: mutating_agent_permissions(),
172            skills: Vec::new(),
173            mcp_servers: Vec::new(),
174            hooks: None,
175            background: false,
176            mode: AgentMode::Subagent,
177            max_turns: None,
178            nickname_candidates: vec!["general".to_string(), "worker".to_string()],
179            initial_prompt: None,
180            memory: None,
181            isolation: None,
182            aliases: vec!["general".to_string(), "general-purpose".to_string()],
183            source: SubagentSource::Builtin,
184            file_path: None,
185            warnings: Vec::new(),
186            tool_policy_overrides: BTreeMap::new(),
187        },
188    ]
189}
190
191pub fn builtin_primary_build_agent() -> SubagentSpec {
192    SubagentSpec {
193        name: "build".to_string(),
194        description: "Built-in implementation agent for the main session.".to_string(),
195        prompt: BUILTIN_BUILD_PRIMARY_AGENT.to_string(),
196        tools: None,
197        disallowed_tools: Vec::new(),
198        model: Some("inherit".to_string()),
199        color: Some(ui::AGENT_COLOR_BUILD.to_string()),
200        reasoning_effort: None,
201        permissions: mutating_agent_permissions(),
202        skills: Vec::new(),
203        mcp_servers: Vec::new(),
204        hooks: None,
205        background: false,
206        mode: AgentMode::Primary,
207        max_turns: None,
208        nickname_candidates: vec!["build".to_string(), "builder".to_string()],
209        initial_prompt: None,
210        memory: None,
211        isolation: None,
212        aliases: vec!["builder".to_string()],
213        source: SubagentSource::Builtin,
214        file_path: None,
215        warnings: Vec::new(),
216        tool_policy_overrides: {
217            let mut m = BTreeMap::new();
218            m.insert("exec_command".to_string(), ToolPolicy::Allow);
219            m
220        },
221    }
222}
223
224pub fn builtin_primary_auto_agent() -> SubagentSpec {
225    SubagentSpec {
226        name: "auto".to_string(),
227        description: "Built-in autonomous implementation agent for the main session.".to_string(),
228        prompt: BUILTIN_AUTO_PRIMARY_AGENT.to_string(),
229        tools: None,
230        disallowed_tools: Vec::new(),
231        model: Some("inherit".to_string()),
232        color: Some(ui::AGENT_COLOR_AUTO.to_string()),
233        reasoning_effort: None,
234        permissions: auto_agent_permissions(),
235        skills: Vec::new(),
236        mcp_servers: Vec::new(),
237        hooks: None,
238        background: false,
239        mode: AgentMode::Primary,
240        max_turns: None,
241        nickname_candidates: vec!["auto".to_string()],
242        initial_prompt: None,
243        memory: None,
244        isolation: None,
245        aliases: vec!["autonomous".to_string()],
246        source: SubagentSource::Builtin,
247        file_path: None,
248        warnings: Vec::new(),
249        tool_policy_overrides: {
250            let mut m = BTreeMap::new();
251            m.insert("exec_command".to_string(), ToolPolicy::Allow);
252            m
253        },
254    }
255}
256
257/// Opt-in orchestration role; worker permissions remain inherited from the session.
258pub fn builtin_primary_coordinator_agent() -> SubagentSpec {
259    let mut spec = builtin_primary_build_agent();
260    spec.name = "coordinator".to_string();
261    spec.description = "Deterministic local matrix orchestration for the main session.".to_string();
262    spec.prompt = BUILTIN_COORDINATOR_PRIMARY_AGENT.to_string();
263    spec.tools = Some(
264        [
265            "matrix",
266            tools::REQUEST_USER_INPUT,
267            "agent",
268            tools::RECORD_DECISION,
269            tools::TASK_TRACKER,
270        ]
271        .into_iter()
272        .map(str::to_string)
273        .collect(),
274    );
275    spec.nickname_candidates = vec!["coordinator".to_string()];
276    spec.aliases.clear();
277    spec.tool_policy_overrides.clear();
278    spec
279}
280
281pub fn builtin_plan_agent() -> SubagentSpec {
282    SubagentSpec {
283        name: "plan".to_string(),
284        description: "Built-in read-only planning agent definition.".to_string(),
285        prompt: format!("{DISCUSSION_FIRST_GUIDANCE}\n\n{BUILTIN_PLAN_AGENT_ROLE}"),
286        tools: Some(builtin_primary_readonly_tool_ids()),
287        disallowed_tools: builtin_readonly_disallowed_tool_ids(),
288        model: Some("inherit".to_string()),
289        color: Some(ui::AGENT_COLOR_PLAN.to_string()),
290        reasoning_effort: None,
291        permissions: plan_agent_permissions(),
292        skills: Vec::new(),
293        mcp_servers: Vec::new(),
294        hooks: None,
295        background: false,
296        mode: AgentMode::Primary,
297        max_turns: None,
298        nickname_candidates: vec!["plan".to_string(), "planner".to_string()],
299        initial_prompt: None,
300        memory: None,
301        isolation: None,
302        aliases: vec!["planner".to_string()],
303        source: SubagentSource::Builtin,
304        file_path: None,
305        warnings: Vec::new(),
306        tool_policy_overrides: BTreeMap::new(),
307    }
308}
309
310pub fn builtin_primary_duck_agent() -> SubagentSpec {
311    SubagentSpec {
312        name: "duck".to_string(),
313        description: "Built-in discussion-first agent for the main session.".to_string(),
314        prompt: format!("{DISCUSSION_FIRST_GUIDANCE}\n\n{BUILTIN_DUCK_PRIMARY_AGENT_ROLE}"),
315        tools: Some(builtin_primary_readonly_tool_ids()),
316        disallowed_tools: builtin_readonly_disallowed_tool_ids(),
317        model: Some("inherit".to_string()),
318        color: Some(ui::AGENT_COLOR_DUCK.to_string()),
319        reasoning_effort: None,
320        permissions: readonly_interview_agent_permissions(),
321        skills: Vec::new(),
322        mcp_servers: Vec::new(),
323        hooks: None,
324        background: false,
325        mode: AgentMode::Primary,
326        max_turns: None,
327        nickname_candidates: vec!["duck".to_string()],
328        initial_prompt: None,
329        memory: None,
330        isolation: None,
331        aliases: Vec::new(),
332        source: SubagentSource::Builtin,
333        file_path: None,
334        warnings: Vec::new(),
335        tool_policy_overrides: BTreeMap::new(),
336    }
337}
338
339fn builtin_readonly_tool_ids() -> Vec<String> {
340    // The direct read tools must be listed explicitly: read-only agents deny
341    // `bash`, so the wire shaper hides `exec_command` for them — without
342    // grep_file/read_file/list_files their effective catalog collapses to
343    // bare `code_search` (the turn_912/913 planning failure shape). The
344    // planning profile and Interactive surface intersect this list further
345    // (e.g. read_file/list_files stay hidden on Interactive).
346    vec![
347        tools::CODE_SEARCH.to_string(),
348        tools::EXEC_COMMAND.to_string(),
349        tools::GREP_FILE.to_string(),
350        tools::READ_FILE.to_string(),
351        tools::LIST_FILES.to_string(),
352    ]
353}
354
355/// Readonly tools for primary agents that interact with the user directly.
356/// Extends the base readonly set with `request_user_input` so plan/duck can
357/// ask clarifying questions.
358fn builtin_primary_readonly_tool_ids() -> Vec<String> {
359    let mut ids = builtin_readonly_tool_ids();
360    ids.push(tools::REQUEST_USER_INPUT.to_string());
361    ids.push(tools::RECORD_DECISION.to_string());
362    ids
363}
364
365fn builtin_readonly_disallowed_tool_ids() -> Vec<String> {
366    Vec::new()
367}
368
369pub(super) fn mutating_agent_permissions() -> AgentPermissionsConfig {
370    AgentPermissionsConfig::new(PermissionDefault::Ask)
371}
372
373fn auto_agent_permissions() -> AgentPermissionsConfig {
374    AgentPermissionsConfig::new(PermissionDefault::Auto)
375}
376
377pub(super) fn readonly_agent_permissions() -> AgentPermissionsConfig {
378    let mut permissions = AgentPermissionsConfig::new(PermissionDefault::Deny);
379    permissions.allow = vec!["read".to_string()];
380    permissions
381}
382
383/// Read-only agents that talk to the user (duck, plan) also list
384/// `request_user_input` and `record_decision` in their tool set. Allow both
385/// explicitly: the default-deny fallback otherwise rejects their semantic
386/// kind "other" and hides them from the wire catalog.
387pub(super) fn readonly_interview_agent_permissions() -> AgentPermissionsConfig {
388    let mut permissions = readonly_agent_permissions();
389    permissions.allow.push("request_user_input".to_string());
390    permissions.allow.push(tools::RECORD_DECISION.to_string());
391    permissions
392}
393
394/// Plan-agent permissions extend the read-only interview set with `bash` so
395/// `exec_command` stays on the wire and read-only shell inspection (`rg`,
396/// `cat`, `wc`, ...) works while planning. This stays read-only in practice:
397/// selecting the plan agent always activates the planning workflow, whose
398/// dispatch gate (`ToolRegistry::is_planning_active_allowed` +
399/// `assess_plan_mode_read_only_bash_command`) hard-blocks every mutating
400/// command before execution — the bash permission only admits the tool, the
401/// planning gate keeps it read-only. Explorer/duck stay bash-denied via
402/// [`readonly_agent_permissions`]/[`readonly_interview_agent_permissions`].
403fn plan_agent_permissions() -> AgentPermissionsConfig {
404    let mut permissions = readonly_interview_agent_permissions();
405    permissions.allow.push("bash".to_string());
406    permissions
407}