1use serde_json::{Value, json};
4
5pub const AGENT_DESCRIPTION: &str = "Spawn and steer delegated child agents. Use action=spawn to delegate a scoped task, action=spawn_subprocess for a managed background subagent, action=send_input to continue a child, action=resume to reopen a completed child, action=wait for results, or action=close to cancel a child. spawn_subprocess runs a subagent defined with background: true in a separate VT Code process; shell commands, including long-running ones such as dev servers, go through exec_command, with background=true to keep them running.";
7
8#[must_use]
9pub fn agent_parameters() -> Value {
10 json!({
11 "type": "object",
12 "required": ["action"],
13 "properties": {
14 "action": {
15 "type": "string",
16 "enum": ["spawn", "spawn_subprocess", "send_input", "resume", "wait", "close"],
17 "description": "spawn: delegate a scoped task to a child agent (requires message). spawn_subprocess: run a subagent defined with background: true as a managed background VT Code process (requires message). send_input: send follow-up input to a running child (requires id + message or items). resume: reopen a completed or closed child from saved context (requires id). wait: block the current foreground turn until one or more children reach a terminal state, including managed background subprocess ids (requires ids). close: cancel and free a child's tool budget (requires id)."
18 },
19 "agent_type": {"type": "string", "description": agent_type_description("spawn or spawn_subprocess: ", " spawn_subprocess requires an agent defined with background: true.")},
20 "message": {"type": "string", "description": "spawn or spawn_subprocess: task prompt. send_input: follow-up prompt for the child."},
21 "items": {
22 "type": "array",
23 "description": ITEMS_DESCRIPTION,
24 "items": collaboration_input_item_schema()
25 },
26 "fork_context": {"type": "boolean", "description": "spawn: seed the child with the current thread history.", "default": false},
27 "model": {"type": "string", "description": "spawn or spawn_subprocess: model override. Omit to use parent model."},
28 "reasoning_effort": reasoning_effort_schema("spawn or spawn_subprocess: "),
29 "background": {"type": "boolean", "description": "spawn: run the child agent in background and return immediately.", "default": false},
30 "max_turns": {"type": "integer", "description": "spawn or spawn_subprocess: optional turn limit for the child."},
31 "id": {"type": "string", "description": "send_input, resume, or close: child agent id."},
32 "interrupt": {"type": "boolean", "description": "send_input: abort current child work and restart with this input; false queues it.", "default": false},
33 "ids": {
34 "type": "array",
35 "items": {"type": "string"},
36 "description": "wait: child agent ids to wait for, including managed background subprocess ids (background-<name>). Blocks the current foreground turn until one target reaches a terminal state or the wait times out."
37 },
38 "timeout_ms": {
39 "type": "integer",
40 "description": "wait: optional wait timeout in milliseconds. Uses the session default timeout when omitted."
41 }
42 }
43 })
44}
45
46#[must_use]
47pub fn spawn_agent_parameters() -> Value {
48 json!({
49 "type": "object",
50 "properties": {
51 "agent_type": {"type": "string", "description": agent_type_description("", "")},
52 "message": {"type": "string", "description": "Task prompt for the child agent."},
53 "items": {
54 "type": "array",
55 "description": ITEMS_DESCRIPTION,
56 "items": collaboration_input_item_schema()
57 },
58 "fork_context": {"type": "boolean", "description": "Seed the child with the current thread history.", "default": false},
59 "model": {
60 "type": "string",
61 "description": "Model override. Omit to use parent model."
62 },
63 "reasoning_effort": reasoning_effort_schema(""),
64 "background": {
65 "type": "boolean",
66 "description": "Run agent in background. Returns immediately.",
67 "default": false
68 },
69 "max_turns": {
70 "type": "integer",
71 "description": "Optional turn limit for this child. Values below 2 are promoted to 2 so the child can recover from an initial blocked or denied tool call."
72 }
73 }
74 })
75}
76
77#[must_use]
78pub fn spawn_background_subprocess_parameters() -> Value {
79 json!({
80 "type": "object",
81 "properties": {
82 "agent_type": {"type": "string", "description": agent_type_description("", " Requires an agent defined with background: true.")},
83 "message": {"type": "string", "description": "Task prompt for the background subprocess."},
84 "items": {
85 "type": "array",
86 "description": ITEMS_DESCRIPTION,
87 "items": collaboration_input_item_schema()
88 },
89 "model": {
90 "type": "string",
91 "description": "Model override. Omit to use parent model."
92 },
93 "reasoning_effort": reasoning_effort_schema(""),
94 "max_turns": {
95 "type": "integer",
96 "description": "Optional turn limit for the launched background subprocess task before it reports readiness. Values below 4 are promoted to 4 for background launches."
97 }
98 }
99 })
100}
101
102#[must_use]
103pub fn send_input_parameters() -> Value {
104 json!({
105 "type": "object",
106 "required": ["id"],
107 "properties": {
108 "id": {"type": "string", "description": "Child agent id to message."},
109 "message": {"type": "string", "description": "Follow-up prompt for the child."},
110 "items": {
111 "type": "array",
112 "description": ITEMS_DESCRIPTION,
113 "items": collaboration_input_item_schema()
114 },
115 "interrupt": {"type": "boolean", "description": "When true, abort current child work and restart with this input. When false (default), queue the input; if the child is already running, it starts the child's next turn after the current turn completes.", "default": false}
116 }
117 })
118}
119
120#[must_use]
121pub fn wait_agent_parameters() -> Value {
122 json!({
123 "type": "object",
124 "required": ["ids"],
125 "properties": {
126 "ids": {
127 "type": "array",
128 "items": {"type": "string"},
129 "description": "Child agent ids to wait for, including managed background subprocess ids (background-<name>). This blocks the current foreground turn until one target reaches a terminal state or the wait times out."
130 },
131 "timeout_ms": {
132 "type": "integer",
133 "description": "Optional wait timeout in milliseconds. Uses the session default timeout when omitted."
134 }
135 }
136 })
137}
138
139#[must_use]
140pub fn resume_agent_parameters() -> Value {
141 json!({
142 "type": "object",
143 "required": ["id"],
144 "properties": {
145 "id": {"type": "string", "description": "Child agent id to resume."}
146 }
147 })
148}
149
150#[must_use]
151pub fn close_agent_parameters() -> Value {
152 json!({
153 "type": "object",
154 "required": ["id"],
155 "properties": {
156 "id": {"type": "string", "description": "Child agent id to close."}
157 }
158 })
159}
160
161#[must_use]
162pub fn request_user_input_description() -> &'static str {
163 "Request user input for one to three short questions. Blocks the agent loop until the user responds. Returns the user's answers mapped by question id. Canonical HITL tool for the Planning workflow."
164}
165
166#[must_use]
167pub fn request_user_input_parameters() -> Value {
168 json!({
169 "type": "object",
170 "additionalProperties": false,
171 "required": ["questions"],
172 "properties": {
173 "questions": {
174 "type": "array",
175 "description": "Questions to show the user (1-3). Prefer 1 unless multiple independent decisions block progress.",
176 "minItems": 1,
177 "maxItems": 3,
178 "items": {
179 "type": "object",
180 "additionalProperties": false,
181 "required": ["id", "header", "question"],
182 "properties": {
183 "id": {
184 "type": "string",
185 "description": "Stable identifier for mapping answers (snake_case)."
186 },
187 "header": {
188 "type": "string",
189 "description": "Short header label shown in the UI (12 or fewer chars)."
190 },
191 "question": {
192 "type": "string",
193 "description": "Single-sentence prompt shown to the user."
194 },
195 "focus_area": {
196 "type": "string",
197 "description": "Optional short topic hint used to bias auto-suggested choices when options are omitted."
198 },
199 "analysis_hints": {
200 "type": "array",
201 "description": "Optional weakness/risk hints used by the UI to generate suggested options.",
202 "items": {
203 "type": "string"
204 },
205 "maxItems": 8
206 },
207 "options": {
208 "type": "array",
209 "description": "Optional 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option; the UI provides that automatically. If omitted, the UI auto-suggests options using question text and hints.",
210 "minItems": 2,
211 "maxItems": 3,
212 "items": {
213 "type": "object",
214 "additionalProperties": false,
215 "required": ["label", "description"],
216 "properties": {
217 "label": {
218 "type": "string",
219 "description": "User-facing label (1-5 words)."
220 },
221 "description": {
222 "type": "string",
223 "description": "One short sentence explaining impact/tradeoff if selected."
224 }
225 }
226 }
227 }
228 }
229 }
230 }
231 }
232 })
233}
234
235pub const SUBAGENT_REASONING_EFFORT_VALUES: &[&str] = vtcode_commons::reasoning::constants::PARSEABLE_LEVELS;
239
240const ITEMS_DESCRIPTION: &str = "Structured context items for the child. Each item carries one content field. Items are used only when message is empty; each item contributes its first non-empty field in the order text, path, name, image_url.";
241
242fn agent_type_description(prefix: &str, suffix: &str) -> String {
243 format!(
244 "{prefix}Subagent name listed in the Subagents section of the system prompt (built-ins: default, explorer, worker; custom agents come from .vtcode/agents, .claude/agents, or .codex/agents in the workspace or home directory). Defaults to the agent the user explicitly mentioned, otherwise default.{suffix}"
245 )
246}
247
248fn reasoning_effort_schema(prefix: &str) -> Value {
249 json!({
250 "type": "string",
251 "enum": SUBAGENT_REASONING_EFFORT_VALUES,
252 "description": format!(
253 "{prefix}reasoning effort override for the child. When omitted, the child uses its agent definition's reasoning effort, or the parent's when the definition sets none."
254 )
255 })
256}
257
258fn collaboration_input_item_schema() -> Value {
259 json!({
260 "type": "object",
261 "properties": {
262 "type": {
263 "type": "string",
264 "description": "Optional label naming the content field this item carries."
265 },
266 "text": {"type": "string", "description": "Inline text passed to the child as-is."},
267 "path": {"type": "string", "description": "Workspace file path the child should read; rendered as \"Reference: <path>\"."},
268 "name": {"type": "string", "description": "Name of a referenced symbol, agent, or resource, passed as-is."},
269 "image_url": {"type": "string", "description": "Image URL; rendered as \"Image: <url>\"."}
270 },
271 "additionalProperties": false
272 })
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use serde_json::json;
279
280 #[test]
281 fn collaboration_schemas_keep_structured_items_consistent() {
282 let spawn_items = &spawn_agent_parameters()["properties"]["items"]["items"];
283 let send_items = &send_input_parameters()["properties"]["items"]["items"];
284
285 assert_eq!(spawn_items, send_items);
286 assert_eq!(spawn_items["additionalProperties"], json!(false));
287 assert_eq!(spawn_items["properties"]["image_url"]["type"], json!("string"));
288 assert!(spawn_items["properties"]["type"].get("enum").is_none());
290 for field in ["type", "text", "path", "name", "image_url"] {
291 let description = spawn_items["properties"][field]["description"].as_str().unwrap_or_default();
292 assert!(!description.is_empty(), "item field {field} needs a description");
293 }
294 }
295
296 #[test]
297 fn collaboration_schemas_document_agent_type_sources_and_reasoning_enum() {
298 let schemas = [
299 agent_parameters(),
300 spawn_agent_parameters(),
301 spawn_background_subprocess_parameters(),
302 ];
303 for schema in &schemas {
304 let agent_type = schema["properties"]["agent_type"]["description"].as_str().unwrap_or_default();
305 assert!(agent_type.contains("Subagents section of the system prompt"));
306 assert!(agent_type.contains("built-ins: default, explorer, worker"));
307 assert!(agent_type.contains(".vtcode/agents"));
308 assert!(agent_type.contains("Defaults to the agent the user explicitly mentioned"));
309
310 let reasoning = &schema["properties"]["reasoning_effort"];
311 assert_eq!(reasoning["enum"], json!(SUBAGENT_REASONING_EFFORT_VALUES));
312 assert!(reasoning["description"].as_str().unwrap_or_default().contains("When omitted"));
313 }
314 assert_eq!(
315 agent_parameters()["properties"]["items"]["description"],
316 send_input_parameters()["properties"]["items"]["description"]
317 );
318 }
319 #[test]
320 fn collaboration_schemas_expose_updated_agent_description_text() {
321 let spawn = spawn_agent_parameters();
322 let spawn_background = spawn_background_subprocess_parameters();
323 let send = send_input_parameters();
324 let wait = wait_agent_parameters();
325
326 assert_eq!(spawn["properties"]["message"]["description"], json!("Task prompt for the child agent."));
327 assert_eq!(send["properties"]["id"]["description"], json!("Child agent id to message."));
328 assert_eq!(
329 spawn["properties"]["background"]["description"],
330 json!("Run agent in background. Returns immediately.")
331 );
332 assert_eq!(
333 spawn_background["properties"]["message"]["description"],
334 json!("Task prompt for the background subprocess.")
335 );
336 assert_eq!(
337 wait["properties"]["ids"]["description"],
338 json!(
339 "Child agent ids to wait for, including managed background subprocess ids (background-<name>). This blocks the current foreground turn until one target reaches a terminal state or the wait times out."
340 )
341 );
342 assert_eq!(
343 wait["properties"]["timeout_ms"]["description"],
344 json!("Optional wait timeout in milliseconds. Uses the session default timeout when omitted.")
345 );
346 }
347
348 #[test]
349 fn request_user_input_schema_preserves_description_field_name() {
350 let schema = request_user_input_parameters();
351
352 assert_eq!(schema["required"], json!(["questions"]));
353 assert_eq!(
354 schema["properties"]["questions"]["items"]["properties"]["options"]["items"]["required"],
355 json!(["label", "description"])
356 );
357 assert_eq!(
358 schema["properties"]["questions"]["items"]["properties"]["options"]["items"]["properties"]["description"]["type"],
359 json!("string")
360 );
361 }
362}