feat: Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions
This commit is contained in:
@@ -123,6 +123,31 @@ pub async fn eval_tool_calls(
|
||||
if is_all_null {
|
||||
output = vec![];
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
let (has_escalations, summary) = {
|
||||
let cfg = config.read();
|
||||
if cfg.current_depth == 0 && let Some(ref queue) = cfg.root_escalation_queue && queue.has_pending() {
|
||||
(true, queue.pending_summary())
|
||||
} else {
|
||||
(false, vec![])
|
||||
}
|
||||
};
|
||||
|
||||
if has_escalations {
|
||||
let notification = json!({
|
||||
"pending_escalations": summary,
|
||||
"instruction": "Child agents are BLOCKED waiting for your reply. Call agent__reply_escalation for each pending escalation to unblock them."
|
||||
});
|
||||
let synthetic_call = ToolCall::new(
|
||||
"__escalation_notification".to_string(),
|
||||
json!({}),
|
||||
Some("escalation_check".to_string()),
|
||||
);
|
||||
output.push(ToolResult::new(synthetic_call, notification));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@@ -276,6 +301,8 @@ impl Functions {
|
||||
pub fn append_supervisor_functions(&mut self) {
|
||||
self.declarations
|
||||
.extend(supervisor::supervisor_function_declarations());
|
||||
self.declarations
|
||||
.extend(supervisor::escalation_function_declarations());
|
||||
}
|
||||
|
||||
pub fn append_teammate_functions(&mut self) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::{FunctionDeclaration, JsonSchema};
|
||||
use crate::client::{Model, ModelType, call_chat_completions};
|
||||
use crate::config::{Config, GlobalConfig, Input, Role, RoleLike};
|
||||
use crate::supervisor::escalation::EscalationQueue;
|
||||
use crate::supervisor::mailbox::{Envelope, EnvelopePayload, Inbox};
|
||||
use crate::supervisor::{AgentExitStatus, AgentHandle, AgentResult};
|
||||
use crate::utils::{AbortSignal, create_abort_signal};
|
||||
@@ -17,6 +18,37 @@ use uuid::Uuid;
|
||||
|
||||
pub const SUPERVISOR_FUNCTION_PREFIX: &str = "agent__";
|
||||
|
||||
pub fn escalation_function_declarations() -> Vec<FunctionDeclaration> {
|
||||
vec![FunctionDeclaration {
|
||||
name: format!("{SUPERVISOR_FUNCTION_PREFIX}reply_escalation"),
|
||||
description: "Reply to a pending escalation from a child agent. The child is blocked waiting for this reply. Use this after seeing pending_escalations notifications.".to_string(),
|
||||
parameters: JsonSchema {
|
||||
type_value: Some("object".to_string()),
|
||||
properties: Some(IndexMap::from([
|
||||
(
|
||||
"escalation_id".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
description: Some("The escalation ID from the pending_escalations notification".into()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"reply".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
description: Some("Your answer to the child agent's question. For ask/confirm questions, use the exact option text. For input questions, provide the text response.".into()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
])),
|
||||
required: Some(vec!["escalation_id".to_string(), "reply".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
agent: false,
|
||||
}]
|
||||
}
|
||||
|
||||
pub fn supervisor_function_declarations() -> Vec<FunctionDeclaration> {
|
||||
vec![
|
||||
FunctionDeclaration {
|
||||
@@ -285,6 +317,7 @@ pub async fn handle_supervisor_tool(
|
||||
"task_list" => handle_task_list(config),
|
||||
"task_complete" => handle_task_complete(config, args).await,
|
||||
"task_fail" => handle_task_fail(config, args),
|
||||
"reply_escalation" => handle_reply_escalation(config, args),
|
||||
_ => bail!("Unknown supervisor action: {action}"),
|
||||
}
|
||||
}
|
||||
@@ -377,6 +410,13 @@ async fn handle_spawn(config: &GlobalConfig, args: &Value) -> Result<Value> {
|
||||
|
||||
let child_inbox = Arc::new(Inbox::new());
|
||||
|
||||
{
|
||||
let mut cfg = config.write();
|
||||
if cfg.root_escalation_queue.is_none() {
|
||||
cfg.root_escalation_queue = Some(Arc::new(EscalationQueue::new()));
|
||||
}
|
||||
}
|
||||
|
||||
let child_config: GlobalConfig = {
|
||||
let mut child_cfg = config.read().clone();
|
||||
|
||||
@@ -664,6 +704,41 @@ fn handle_check_inbox(config: &GlobalConfig) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_reply_escalation(config: &GlobalConfig, args: &Value) -> Result<Value> {
|
||||
let escalation_id = args
|
||||
.get("escalation_id")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'escalation_id' is required"))?;
|
||||
let reply = args
|
||||
.get("reply")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'reply' is required"))?;
|
||||
|
||||
let queue = {
|
||||
let cfg = config.read();
|
||||
cfg.root_escalation_queue
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("No escalation queue available"))?
|
||||
};
|
||||
|
||||
match queue.take(escalation_id) {
|
||||
Some(request) => {
|
||||
let from_agent = request.from_agent_name.clone();
|
||||
let question = request.question.clone();
|
||||
let _ = request.reply_tx.send(reply.to_string());
|
||||
Ok(json!({
|
||||
"status": "ok",
|
||||
"message": format!("Reply sent to agent '{from_agent}' for escalation '{escalation_id}'"),
|
||||
"original_question": question,
|
||||
}))
|
||||
}
|
||||
None => Ok(json!({
|
||||
"status": "error",
|
||||
"message": format!("No pending escalation found with id '{escalation_id}'. It may have already been replied to or timed out."),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_task_create(config: &GlobalConfig, args: &Value) -> Result<Value> {
|
||||
let subject = args
|
||||
.get("subject")
|
||||
|
||||
Reference in New Issue
Block a user