1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7pub const MAX_WAIT_SECONDS: u64 = 3_600;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct SourceRange {
14 pub file: PathBuf,
15 pub start: u64,
16 pub end: u64,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct SubagentToolRequest {
24 pub request_id: String,
25 pub created_at_ms: i64,
26 pub action: SubagentToolAction,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(
31 tag = "action",
32 content = "params",
33 rename_all = "snake_case",
34 deny_unknown_fields
35)]
36pub enum SubagentToolAction {
37 ListProfiles,
38 Spawn {
39 task_name: String,
40 instructions: String,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 profile_id: Option<String>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 model: Option<String>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 effort: Option<String>,
47 #[serde(default)]
51 working_directory: PathBuf,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 context: Option<String>,
54 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 files: Vec<SourceRange>,
56 },
57 ListAgents,
58 SendInput {
59 child_session_id: String,
60 message: String,
61 },
62 WaitAgents {
63 child_session_ids: Vec<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 timeout_seconds: Option<u64>,
66 },
67 InterruptAgent {
68 child_session_id: String,
69 },
70 CloseAgent {
71 child_session_id: String,
72 },
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct SubagentToolResult {
78 pub request_id: String,
79 pub completed_at_ms: i64,
80 pub is_error: bool,
81 pub message: String,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(deny_unknown_fields)]
87pub struct SubagentRecord {
88 pub child_session_id: String,
89 pub parent_session_id: String,
90 pub task_name: String,
91 pub profile_id: String,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub model: Option<String>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub effort: Option<String>,
96 pub working_directory: PathBuf,
101 pub initial_prompt: String,
103 pub request_key: String,
104 pub created_at: String,
105 #[serde(
109 default,
110 skip_serializing_if = "Option::is_none",
111 rename = "delivered_turn"
112 )]
113 pub noticed_turn: Option<u64>,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum SubagentStatus {
120 Preparing,
121 Running,
122 InputRequired,
123 Completed,
124 Failed,
125 Interrupted,
126 Stopped,
127}
128
129impl SubagentRecord {
130 #[must_use]
131 pub fn is_child(&self, session_id: &str) -> bool {
132 self.child_session_id == session_id
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn noticed_turn_keeps_the_stored_delivered_turn_field_name() {
142 let record: SubagentRecord = serde_json::from_str(
143 r#"{"child_session_id":"c","parent_session_id":"p","task_name":"t","profile_id":"pr","working_directory":".","initial_prompt":"i","request_key":"k","created_at":"2026-09-15","delivered_turn":3}"#,
144 )
145 .expect("stored relation payloads remain readable");
146 assert_eq!(record.noticed_turn, Some(3));
147 let encoded = serde_json::to_value(&record).expect("record encodes");
148 assert_eq!(encoded["delivered_turn"], 3);
149 assert!(
150 encoded.get("noticed_turn").is_none(),
151 "the wire field name must stay historical: {encoded}"
152 );
153 }
154}