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