Skip to main content

vtcode_utility_tool_specs/
collaboration.rs

1//! Collaboration and human-in-the-loop tool schemas.
2
3use serde_json::{Value, json};
4
5/// Model-visible description of the `agent` tool.
6pub 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/// Model-visible description of the `ask_session` tool.
162pub const ASK_SESSION_DESCRIPTION: &str = "Ask a question of a peer CLI session running in this workspace. The peer sees the question on its next turn and can reply with answer_session; answers arrive back as notices. Use list_sessions to discover live peers first. Delivery is best-effort: the peer must be running to receive it.";
163
164#[must_use]
165pub fn ask_session_parameters() -> Value {
166    json!({
167        "type": "object",
168        "required": ["session", "question"],
169        "properties": {
170            "session": {"type": "string", "description": "Target peer session id (see list_sessions)."},
171            "question": {"type": "string", "description": "Question text for the peer session."},
172            "ttl_secs": {"type": "integer", "description": "Seconds the question stays deliverable. Defaults to 3600; clamped to 60..86400."}
173        }
174    })
175}
176
177/// Model-visible description of the `answer_session` tool.
178pub const ANSWER_SESSION_DESCRIPTION: &str = "Answer a question received from a peer CLI session. Pass the message_id of the ask_session message you are replying to so the asker can match the reply.";
179
180#[must_use]
181pub fn answer_session_parameters() -> Value {
182    json!({
183        "type": "object",
184        "required": ["message_id", "answer"],
185        "properties": {
186            "message_id": {"type": "string", "description": "Message id of the question being answered."},
187            "answer": {"type": "string", "description": "Answer text for the asking session."},
188            "ttl_secs": {"type": "integer", "description": "Seconds the answer stays deliverable. Defaults to 3600; clamped to 60..86400."}
189        }
190    })
191}
192
193/// Model-visible description of the `list_sessions` tool.
194pub const LIST_SESSIONS_DESCRIPTION: &str = "List live peer CLI sessions in this workspace that can receive ask_session messages. Returns session ids with their last-seen times.";
195
196#[must_use]
197pub fn list_sessions_parameters() -> Value {
198    json!({
199        "type": "object",
200        "properties": {
201            "include_self": {"type": "boolean", "description": "Include the calling session in the results.", "default": false}
202        }
203    })
204}
205
206pub fn request_user_input_description() -> &'static str {
207    "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."
208}
209
210#[must_use]
211pub fn request_user_input_parameters() -> Value {
212    json!({
213        "type": "object",
214        "additionalProperties": false,
215        "required": ["questions"],
216        "properties": {
217            "questions": {
218                "type": "array",
219                "description": "Questions to show the user (1-3). Prefer 1 unless multiple independent decisions block progress.",
220                "minItems": 1,
221                "maxItems": 3,
222                "items": {
223                    "type": "object",
224                    "additionalProperties": false,
225                    "required": ["id", "header", "question"],
226                    "properties": {
227                        "id": {
228                            "type": "string",
229                            "description": "Stable identifier for mapping answers (snake_case)."
230                        },
231                        "header": {
232                            "type": "string",
233                            "description": "Short header label shown in the UI (12 or fewer chars)."
234                        },
235                        "question": {
236                            "type": "string",
237                            "description": "Single-sentence prompt shown to the user."
238                        },
239                        "focus_area": {
240                            "type": "string",
241                            "description": "Optional short topic hint used to bias auto-suggested choices when options are omitted."
242                        },
243                        "analysis_hints": {
244                            "type": "array",
245                            "description": "Optional weakness/risk hints used by the UI to generate suggested options.",
246                            "items": {
247                                "type": "string"
248                            },
249                            "maxItems": 8
250                        },
251                        "options": {
252                            "type": "array",
253                            "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.",
254                            "minItems": 2,
255                            "maxItems": 3,
256                            "items": {
257                                "type": "object",
258                                "additionalProperties": false,
259                                "required": ["label", "description"],
260                                "properties": {
261                                    "label": {
262                                        "type": "string",
263                                        "description": "User-facing label (1-5 words)."
264                                    },
265                                    "description": {
266                                        "type": "string",
267                                        "description": "One short sentence explaining impact/tradeoff if selected."
268                                    }
269                                }
270                            }
271                        }
272                    }
273                }
274            }
275        }
276    })
277}
278
279/// Reasoning effort values accepted for a child agent override: every value
280/// `ReasoningEffortLevel::parse` accepts, including `none` (the child sends no
281/// reasoning configuration), taken from the single list in `vtcode-commons`.
282pub const SUBAGENT_REASONING_EFFORT_VALUES: &[&str] = vtcode_commons::reasoning::constants::PARSEABLE_LEVELS;
283
284const 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.";
285
286fn agent_type_description(prefix: &str, suffix: &str) -> String {
287    format!(
288        "{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}"
289    )
290}
291
292fn reasoning_effort_schema(prefix: &str) -> Value {
293    json!({
294        "type": "string",
295        "enum": SUBAGENT_REASONING_EFFORT_VALUES,
296        "description": format!(
297            "{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."
298        )
299    })
300}
301
302fn collaboration_input_item_schema() -> Value {
303    json!({
304        "type": "object",
305        "properties": {
306            "type": {
307                "type": "string",
308                "description": "Optional label naming the content field this item carries."
309            },
310            "text": {"type": "string", "description": "Inline text passed to the child as-is."},
311            "path": {"type": "string", "description": "Workspace file path the child should read; rendered as \"Reference: <path>\"."},
312            "name": {"type": "string", "description": "Name of a referenced symbol, agent, or resource, passed as-is."},
313            "image_url": {"type": "string", "description": "Image URL; rendered as \"Image: <url>\"."}
314        },
315        "additionalProperties": false
316    })
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use serde_json::json;
323
324    #[test]
325    fn collaboration_schemas_keep_structured_items_consistent() {
326        let spawn_items = &spawn_agent_parameters()["properties"]["items"]["items"];
327        let send_items = &send_input_parameters()["properties"]["items"]["items"];
328
329        assert_eq!(spawn_items, send_items);
330        assert_eq!(spawn_items["additionalProperties"], json!(false));
331        assert_eq!(spawn_items["properties"]["image_url"]["type"], json!("string"));
332        // The label is informational; the child reads only the content fields.
333        assert!(spawn_items["properties"]["type"].get("enum").is_none());
334        for field in ["type", "text", "path", "name", "image_url"] {
335            let description = spawn_items["properties"][field]["description"].as_str().unwrap_or_default();
336            assert!(!description.is_empty(), "item field {field} needs a description");
337        }
338    }
339
340    #[test]
341    fn collaboration_schemas_document_agent_type_sources_and_reasoning_enum() {
342        let schemas = [
343            agent_parameters(),
344            spawn_agent_parameters(),
345            spawn_background_subprocess_parameters(),
346        ];
347        for schema in &schemas {
348            let agent_type = schema["properties"]["agent_type"]["description"].as_str().unwrap_or_default();
349            assert!(agent_type.contains("Subagents section of the system prompt"));
350            assert!(agent_type.contains("built-ins: default, explorer, worker"));
351            assert!(agent_type.contains(".vtcode/agents"));
352            assert!(agent_type.contains("Defaults to the agent the user explicitly mentioned"));
353
354            let reasoning = &schema["properties"]["reasoning_effort"];
355            assert_eq!(reasoning["enum"], json!(SUBAGENT_REASONING_EFFORT_VALUES));
356            assert!(reasoning["description"].as_str().unwrap_or_default().contains("When omitted"));
357        }
358        assert_eq!(
359            agent_parameters()["properties"]["items"]["description"],
360            send_input_parameters()["properties"]["items"]["description"]
361        );
362    }
363    #[test]
364    fn collaboration_schemas_expose_updated_agent_description_text() {
365        let spawn = spawn_agent_parameters();
366        let spawn_background = spawn_background_subprocess_parameters();
367        let send = send_input_parameters();
368        let wait = wait_agent_parameters();
369
370        assert_eq!(spawn["properties"]["message"]["description"], json!("Task prompt for the child agent."));
371        assert_eq!(send["properties"]["id"]["description"], json!("Child agent id to message."));
372        assert_eq!(
373            spawn["properties"]["background"]["description"],
374            json!("Run agent in background. Returns immediately.")
375        );
376        assert_eq!(
377            spawn_background["properties"]["message"]["description"],
378            json!("Task prompt for the background subprocess.")
379        );
380        assert_eq!(
381            wait["properties"]["ids"]["description"],
382            json!(
383                "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."
384            )
385        );
386        assert_eq!(
387            wait["properties"]["timeout_ms"]["description"],
388            json!("Optional wait timeout in milliseconds. Uses the session default timeout when omitted.")
389        );
390    }
391
392    #[test]
393    fn session_messaging_schemas_require_routing_fields() {
394        let ask = ask_session_parameters();
395        assert_eq!(ask["required"], json!(["session", "question"]));
396        assert_eq!(ask["properties"]["session"]["type"], json!("string"));
397        assert_eq!(ask["properties"]["question"]["type"], json!("string"));
398
399        let answer = answer_session_parameters();
400        assert_eq!(answer["required"], json!(["message_id", "answer"]));
401        assert_eq!(answer["properties"]["message_id"]["type"], json!("string"));
402
403        let list = list_sessions_parameters();
404        assert!(list.get("required").is_none(), "list takes optional filters only");
405        assert_eq!(list["properties"]["include_self"]["type"], json!("boolean"));
406    }
407
408    #[test]
409    fn request_user_input_schema_preserves_description_field_name() {
410        let schema = request_user_input_parameters();
411
412        assert_eq!(schema["required"], json!(["questions"]));
413        assert_eq!(
414            schema["properties"]["questions"]["items"]["properties"]["options"]["items"]["required"],
415            json!(["label", "description"])
416        );
417        assert_eq!(
418            schema["properties"]["questions"]["items"]["properties"]["options"]["items"]["properties"]["description"]["type"],
419            json!("string")
420        );
421    }
422}