Skip to content

Commit 94e8163

Browse files
committed
feat: implement delegate tool for background agent management
- Add delegate tool with list/launch/status operations - Validate agent names against available configs - Run agents silently in background with output capture - Store execution state in ~/.aws/amazonq/.subagents/ - Include PID monitoring for crash detection - Require experimental flag enablement
1 parent 0de1451 commit 94e8163

File tree

11 files changed

+590
-147
lines changed

11 files changed

+590
-147
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ xcuserdata/
88
.DS_Store
99
node_modules/
1010
.eslintcache
11+
12+
# Conversation/data files
13+
delegate
14+
delegatetool
1115
yarn-error.log
1216
yarn.lock
1317
package-lock.json

crates/chat-cli/src/cli/chat/cli/experiment.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ static AVAILABLE_EXPERIMENTS: &[Experiment] = &[
5050
description: "Enables Q to create todo lists that can be viewed and managed using /todos",
5151
setting_key: Setting::EnabledTodoList,
5252
},
53+
Experiment {
54+
name: "Delegate",
55+
description: "Enables launching and managing asynchronous subagent processes",
56+
setting_key: Setting::EnabledDelegate,
57+
},
5358
];
5459

5560
#[derive(Debug, PartialEq, Args)]

crates/chat-cli/src/cli/chat/tool_manager.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ use crate::cli::chat::server_messenger::{
7171
UpdateEventMessage,
7272
};
7373
use crate::cli::chat::tools::custom_tool::CustomTool;
74+
use crate::cli::chat::tools::delegate::Delegate;
7475
use crate::cli::chat::tools::execute::ExecuteCommand;
7576
use crate::cli::chat::tools::fs_read::FsRead;
7677
use crate::cli::chat::tools::fs_write::FsWrite;
@@ -728,6 +729,9 @@ impl ToolManager {
728729
if !crate::cli::chat::tools::todo::TodoList::is_enabled(os) {
729730
tool_specs.remove("todo_list");
730731
}
732+
if !os.database.settings.get_bool(Setting::EnabledDelegate).unwrap_or(false) {
733+
tool_specs.remove("delegate");
734+
}
731735

732736
#[cfg(windows)]
733737
{
@@ -873,6 +877,7 @@ impl ToolManager {
873877
"thinking" => Tool::Thinking(serde_json::from_value::<Thinking>(value.args).map_err(map_err)?),
874878
"knowledge" => Tool::Knowledge(serde_json::from_value::<Knowledge>(value.args).map_err(map_err)?),
875879
"todo_list" => Tool::Todo(serde_json::from_value::<TodoList>(value.args).map_err(map_err)?),
880+
"delegate" => Tool::Delegate(serde_json::from_value::<Delegate>(value.args).map_err(map_err)?),
876881
// Note that this name is namespaced with server_name{DELIMITER}tool_name
877882
name => {
878883
// Note: tn_map also has tools that underwent no transformation. In otherwords, if
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
mod agent;
2+
mod execution;
3+
mod types;
4+
mod ui;
5+
6+
// Re-export types for external use
7+
use std::io::Write;
8+
9+
use agent::{
10+
list_available_agents,
11+
load_agent_execution,
12+
request_user_approval,
13+
validate_agent_availability,
14+
};
15+
use execution::{spawn_agent_process, status_agent, status_all_agents};
16+
use ui::display_default_agent_warning;
17+
use eyre::{
18+
Result,
19+
eyre,
20+
};
21+
use serde::{Deserialize, Serialize};
22+
use strum::{Display, EnumString};
23+
24+
use crate::cli::chat::tools::{
25+
InvokeOutput,
26+
OutputKind,
27+
};
28+
use crate::database::settings::Setting;
29+
use crate::os::Os;
30+
31+
const DEFAULT_AGENT: &str = "default";
32+
const ALL_AGENTS: &str = "all";
33+
34+
#[derive(Debug, Clone, Serialize, Deserialize)]
35+
pub struct Delegate {
36+
/// Operation to perform: launch, status, or list
37+
pub operation: String,
38+
/// Agent name to use (optional - uses "default" if not specified)
39+
#[serde(default)]
40+
pub agent: Option<String>,
41+
/// Task description (required for launch operation)
42+
#[serde(default)]
43+
pub task: Option<String>,
44+
}
45+
46+
#[derive(Debug, Display, EnumString)]
47+
#[strum(serialize_all = "lowercase")]
48+
enum Operation {
49+
Launch,
50+
Status,
51+
List,
52+
}
53+
54+
#[allow(unused_imports)]
55+
pub use types::{
56+
AgentConfig,
57+
AgentExecution,
58+
AgentStatus,
59+
};
60+
61+
impl Delegate {
62+
pub async fn invoke(&self, os: &Os, _output: &mut impl Write) -> Result<InvokeOutput> {
63+
if !is_enabled(os) {
64+
return Ok(InvokeOutput {
65+
output: OutputKind::Text(
66+
"Delegate tool is experimental and not enabled. Use /experiment to enable it.".to_string(),
67+
),
68+
});
69+
}
70+
71+
// Validate operation first
72+
let operation = self.operation.parse::<Operation>()
73+
.map_err(|_| eyre!("Invalid operation. Use: launch, status, or list"))?;
74+
75+
// Validate required fields based on operation
76+
match operation {
77+
Operation::Launch => {
78+
if self.task.is_none() {
79+
return Err(eyre!("Task description is required for launch operation"));
80+
}
81+
if self.agent.is_none() {
82+
return Err(eyre!("Agent name is required for launch operation. Use 'list' operation to see available agents, then specify agent name."));
83+
}
84+
85+
// Validate agent name exists
86+
let agent_name = self.agent.as_ref().unwrap();
87+
if agent_name != DEFAULT_AGENT {
88+
let available_agents = list_available_agents(os).await?;
89+
if !available_agents.contains(agent_name) {
90+
return Err(eyre!(
91+
"Agent '{}' not found. Available agents: default, {}. Use exact names only.",
92+
agent_name,
93+
available_agents.join(", ")
94+
));
95+
}
96+
}
97+
},
98+
Operation::Status | Operation::List => {
99+
// No additional validation needed
100+
}
101+
}
102+
103+
let agent_name = self.get_agent_name();
104+
105+
let result = match operation {
106+
Operation::Launch => {
107+
let task = self.task.as_ref().unwrap(); // Safe due to validation above
108+
launch_agent(os, agent_name, task).await?
109+
},
110+
Operation::Status => {
111+
if agent_name == ALL_AGENTS {
112+
status_all_agents(os).await?
113+
} else {
114+
status_agent(os, agent_name).await?
115+
}
116+
},
117+
Operation::List => {
118+
list_agents(os).await?
119+
},
120+
};
121+
122+
Ok(InvokeOutput {
123+
output: OutputKind::Text(result),
124+
})
125+
}
126+
127+
pub fn queue_description(&self, output: &mut impl Write) -> Result<()> {
128+
if let Ok(operation) = self.operation.parse::<Operation>() {
129+
match operation {
130+
Operation::Launch => writeln!(output, "Delegating task to agent")?,
131+
Operation::Status => writeln!(output, "Checking agent status")?,
132+
Operation::List => writeln!(output, "Listing available agents")?,
133+
}
134+
} else {
135+
writeln!(
136+
output,
137+
"Invalid operation '{}'. Use: launch, status, or list",
138+
self.operation
139+
)?;
140+
}
141+
Ok(())
142+
}
143+
144+
fn get_agent_name(&self) -> &str {
145+
if let Ok(operation) = self.operation.parse::<Operation>() {
146+
match operation {
147+
Operation::Launch => {
148+
// Agent is required for launch (validated above)
149+
self.agent.as_deref().unwrap_or("")
150+
},
151+
Operation::Status => self.agent.as_deref().unwrap_or(ALL_AGENTS),
152+
Operation::List => "", // Agent name not needed for list operation
153+
}
154+
} else {
155+
self.agent.as_deref().unwrap_or("")
156+
}
157+
}
158+
}
159+
160+
async fn list_agents(os: &Os) -> Result<String> {
161+
let agents = list_available_agents(os).await?;
162+
if agents.is_empty() {
163+
Ok("No custom agents configured. Only 'default' agent is available.".to_string())
164+
} else {
165+
Ok(format!("Available agents: default, {}", agents.join(", ")))
166+
}
167+
}
168+
169+
async fn launch_agent(os: &Os, agent: &str, task: &str) -> Result<String> {
170+
validate_agent_availability(os, agent).await?;
171+
172+
// Check if agent is already running
173+
if let Some(execution) = load_agent_execution(os, agent).await? {
174+
if execution.status == AgentStatus::Running {
175+
return Err(eyre::eyre!("Agent '{}' is already running. Use status operation to check progress or wait for completion.", agent));
176+
}
177+
}
178+
179+
if agent == DEFAULT_AGENT {
180+
// Show warning for default agent but no approval needed
181+
display_default_agent_warning()?;
182+
} else {
183+
// Show agent info and require approval for specific agents
184+
request_user_approval(os, agent, task).await?;
185+
}
186+
187+
let _execution = spawn_agent_process(os, agent, task).await?;
188+
Ok(format_launch_success(agent, task))
189+
}
190+
191+
fn format_launch_success(agent: &str, task: &str) -> String {
192+
format!(
193+
"✓ Agent '{}' launched successfully.\nTask: {}\n\nUse 'status' operation to check progress.",
194+
agent, task
195+
)
196+
}
197+
198+
fn is_enabled(os: &Os) -> bool {
199+
os.database.settings.get_bool(Setting::EnabledDelegate).unwrap_or(false)
200+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use serde::{
2+
Deserialize,
3+
Serialize,
4+
};
5+
use strum::{Display, EnumString};
6+
7+
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Display, EnumString)]
8+
#[strum(serialize_all = "lowercase")]
9+
#[serde(rename_all = "lowercase")]
10+
pub enum AgentStatus {
11+
Running,
12+
Completed,
13+
Failed,
14+
}
15+
16+
impl Default for AgentStatus {
17+
fn default() -> Self {
18+
Self::Running
19+
}
20+
}
21+
22+
impl AgentStatus {
23+
// No methods currently needed - all functionality is in format_status
24+
}
25+
26+
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
27+
pub struct AgentExecution {
28+
#[serde(default)]
29+
pub agent: String,
30+
#[serde(default)]
31+
pub task: String,
32+
#[serde(default)]
33+
pub status: AgentStatus,
34+
#[serde(default)]
35+
pub launched_at: String,
36+
#[serde(default)]
37+
pub completed_at: Option<String>,
38+
#[serde(default)]
39+
pub pid: u32,
40+
#[serde(default)]
41+
pub exit_code: Option<i32>,
42+
#[serde(default)]
43+
pub output: String,
44+
}
45+
46+
impl AgentExecution {
47+
pub fn format_status(&self) -> String {
48+
match self.status {
49+
AgentStatus::Running => {
50+
format!("Agent '{}' is still running. Please wait...", self.agent)
51+
},
52+
AgentStatus::Completed => {
53+
format!("Agent '{}' completed successfully.\n\nOutput:\n{}",
54+
self.agent, self.output)
55+
},
56+
AgentStatus::Failed => {
57+
format!("Agent '{}' failed.\nExit code: {}\n\nError:\n{}",
58+
self.agent,
59+
self.exit_code.unwrap_or(-1),
60+
self.output)
61+
},
62+
}
63+
}
64+
}
65+
66+
#[derive(Debug, Deserialize)]
67+
pub struct AgentConfig {
68+
pub description: Option<String>,
69+
#[serde(rename = "allowedTools")]
70+
pub allowed_tools: Vec<String>,
71+
}

0 commit comments

Comments
 (0)