feat: display subagent tool calls in CLI and UI (#6535)

Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
Rabi Mishra
2026-02-04 02:26:59 +05:30
committed by GitHub
parent 7ee634eaa7
commit 1e645c0212
5 changed files with 486 additions and 114 deletions
+44
View File
@@ -21,6 +21,7 @@ use tokio_util::task::AbortOnDropHandle;
pub use self::export::message_to_markdown; pub use self::export::message_to_markdown;
pub use builder::{build_session, SessionBuilderConfig}; pub use builder::{build_session, SessionBuilderConfig};
use console::Color; use console::Color;
use goose::agents::subagent_handler::SUBAGENT_TOOL_REQUEST_TYPE;
use goose::agents::AgentEvent; use goose::agents::AgentEvent;
use goose::permission::permission_confirmation::PrincipalType; use goose::permission::permission_confirmation::PrincipalType;
use goose::permission::Permission; use goose::permission::Permission;
@@ -1537,6 +1538,49 @@ fn handle_mcp_notification(
) { ) {
match notification { match notification {
ServerNotification::LoggingMessageNotification(log_notif) => { ServerNotification::LoggingMessageNotification(log_notif) => {
if let Some(obj) = log_notif.params.data.as_object() {
if obj.get("type").and_then(|v| v.as_str()) == Some(SUBAGENT_TOOL_REQUEST_TYPE) {
if let (Some(subagent_id), Some(tool_call)) = (
obj.get("subagent_id").and_then(|v| v.as_str()),
obj.get("tool_call").and_then(|v| v.as_object()),
) {
let tool_name = tool_call
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let arguments = tool_call
.get("arguments")
.and_then(|v| v.as_object())
.cloned();
if interactive {
let _ = progress_bars.hide();
}
if is_stream_json_mode {
emit_stream_event(&StreamEvent::Notification {
extension_id: extension_id.to_string(),
data: NotificationData::Log {
message: output::format_subagent_tool_call_message(
subagent_id,
tool_name,
),
},
});
return;
}
if !is_json_mode {
output::render_subagent_tool_call(
subagent_id,
tool_name,
arguments.as_ref(),
debug,
);
return;
}
}
}
}
let (formatted, subagent_id, notif_type) = let (formatted, subagent_id, notif_type) =
format_logging_notification(&log_notif.params.data, debug); format_logging_notification(&log_notif.params.data, debug);
+97 -10
View File
@@ -613,21 +613,108 @@ fn render_default_request(call: &CallToolRequestParams, debug: bool) {
println!(); println!();
} }
fn split_tool_name(tool_name: &str) -> (String, String) {
let parts: Vec<_> = tool_name.rsplit("__").collect();
let tool = parts.first().copied().unwrap_or("unknown");
let extension = parts
.split_first()
.map(|(_, s)| s.iter().rev().copied().collect::<Vec<_>>().join("__"))
.unwrap_or_default();
(tool.to_string(), extension)
}
pub fn format_subagent_tool_call_message(subagent_id: &str, tool_name: &str) -> String {
let short_id = subagent_id.rsplit('_').next().unwrap_or(subagent_id);
let (tool, extension) = split_tool_name(tool_name);
if extension.is_empty() {
format!("[subagent:{}] {}", short_id, tool)
} else {
format!("[subagent:{}] {} | {}", short_id, tool, extension)
}
}
pub fn render_subagent_tool_call(
subagent_id: &str,
tool_name: &str,
arguments: Option<&JsonObject>,
debug: bool,
) {
if tool_name == "code_execution__execute_code" {
let tool_graph = arguments
.and_then(|args| args.get("tool_graph"))
.and_then(Value::as_array)
.filter(|arr| !arr.is_empty());
if let Some(tool_graph) = tool_graph {
return render_subagent_tool_graph(subagent_id, tool_graph);
}
}
let tool_header = format!(
"─── {} ──────────────────────────",
style(format_subagent_tool_call_message(subagent_id, tool_name))
.magenta()
.dim()
);
println!();
println!("{}", tool_header);
print_params(&arguments.cloned(), 0, debug);
println!();
}
fn render_subagent_tool_graph(subagent_id: &str, tool_graph: &[Value]) {
let short_id = subagent_id.rsplit('_').next().unwrap_or(subagent_id);
let count = tool_graph.len();
let plural = if count == 1 { "" } else { "s" };
println!();
println!(
"─── {} {} tool call{} | {} ──────────────────────────",
style(format!("[subagent:{}]", short_id)).cyan(),
style(count).cyan(),
plural,
style("execute_code").magenta().dim()
);
for (i, node) in tool_graph.iter().filter_map(Value::as_object).enumerate() {
let tool = node
.get("tool")
.and_then(Value::as_str)
.unwrap_or("unknown");
let desc = node
.get("description")
.and_then(Value::as_str)
.unwrap_or("");
let deps: Vec<_> = node
.get("depends_on")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_u64)
.map(|d| (d + 1).to_string())
.collect();
let deps_str = if deps.is_empty() {
String::new()
} else {
format!(" (uses {})", deps.join(", "))
};
println!(
" {}. {}: {}{}",
style(i + 1).dim(),
style(tool).cyan(),
style(desc).green(),
style(deps_str).dim()
);
}
println!();
}
// Helper functions // Helper functions
fn print_tool_header(call: &CallToolRequestParams) { fn print_tool_header(call: &CallToolRequestParams) {
let parts: Vec<_> = call.name.rsplit("__").collect(); let (tool, extension) = split_tool_name(&call.name);
let tool_header = format!( let tool_header = format!(
"─── {} | {} ──────────────────────────", "─── {} | {} ──────────────────────────",
style(parts.first().unwrap_or(&"unknown")), style(tool),
style( style(extension).magenta().dim(),
parts
.split_first()
.map(|(_, s)| s.iter().rev().copied().collect::<Vec<_>>().join("__"))
.unwrap_or_else(|| "unknown".to_string())
)
.magenta()
.dim(),
); );
println!(); println!();
println!("{}", tool_header); println!("{}", tool_header);
+261 -96
View File
@@ -1,12 +1,18 @@
use crate::{ use crate::{
agents::{subagent_task_config::TaskConfig, Agent, AgentConfig, AgentEvent, SessionConfig}, agents::{subagent_task_config::TaskConfig, Agent, AgentConfig, AgentEvent, SessionConfig},
conversation::{message::Message, Conversation}, conversation::{
message::{Message, MessageContent},
Conversation,
},
prompt_template::render_template, prompt_template::render_template,
recipe::Recipe, recipe::Recipe,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures::StreamExt; use futures::StreamExt;
use rmcp::model::{ErrorCode, ErrorData}; use rmcp::model::{
ErrorCode, ErrorData, LoggingLevel, LoggingMessageNotification,
LoggingMessageNotificationMethod, LoggingMessageNotificationParam, ServerNotification,
};
use serde::Serialize; use serde::Serialize;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
@@ -15,18 +21,17 @@ use tokio_util::sync::CancellationToken;
use tracing::{debug, info}; use tracing::{debug, info};
#[derive(Serialize)] #[derive(Serialize)]
struct SubagentPromptContext { pub struct SubagentPromptContext {
max_turns: usize, pub max_turns: usize,
subagent_id: String, pub subagent_id: String,
task_instructions: String, pub task_instructions: String,
tool_count: usize, pub tool_count: usize,
available_tools: String, pub available_tools: String,
} }
type AgentMessagesFuture = type AgentMessagesFuture =
Pin<Box<dyn Future<Output = Result<(Conversation, Option<String>)>> + Send>>; Pin<Box<dyn Future<Output = Result<(Conversation, Option<String>)>> + Send>>;
/// Standalone function to run a complete subagent task with output options
pub async fn run_complete_subagent_task( pub async fn run_complete_subagent_task(
config: AgentConfig, config: AgentConfig,
recipe: Recipe, recipe: Recipe,
@@ -35,16 +40,43 @@ pub async fn run_complete_subagent_task(
session_id: String, session_id: String,
cancellation_token: Option<CancellationToken>, cancellation_token: Option<CancellationToken>,
) -> Result<String, anyhow::Error> { ) -> Result<String, anyhow::Error> {
let (messages, final_output) = run_complete_subagent_task_with_notifications(
get_agent_messages(config, recipe, task_config, session_id, cancellation_token) config,
.await recipe,
.map_err(|e| { task_config,
ErrorData::new( return_last_only,
ErrorCode::INTERNAL_ERROR, session_id,
format!("Failed to execute task: {}", e), cancellation_token,
None, None,
) )
})?; .await
}
pub async fn run_complete_subagent_task_with_notifications(
config: AgentConfig,
recipe: Recipe,
task_config: TaskConfig,
return_last_only: bool,
session_id: String,
cancellation_token: Option<CancellationToken>,
notification_tx: Option<tokio::sync::mpsc::UnboundedSender<rmcp::model::ServerNotification>>,
) -> Result<String, anyhow::Error> {
let (messages, final_output) = get_agent_messages_with_notifications(
config,
recipe,
task_config,
session_id,
cancellation_token,
notification_tx,
)
.await
.map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to execute task: {}", e),
None,
)
})?;
if let Some(output) = final_output { if let Some(output) = final_output {
return Ok(output); return Ok(output);
@@ -67,40 +99,35 @@ pub async fn run_complete_subagent_task(
let all_text_content: Vec<String> = messages let all_text_content: Vec<String> = messages
.iter() .iter()
.flat_map(|message| { .flat_map(|message| {
message.content.iter().filter_map(|content| { message.content.iter().filter_map(|content| match content {
match content { crate::conversation::message::MessageContent::Text(text_content) => {
crate::conversation::message::MessageContent::Text(text_content) => { Some(text_content.text.clone())
Some(text_content.text.clone()) }
} crate::conversation::message::MessageContent::ToolResponse(tool_response) => {
crate::conversation::message::MessageContent::ToolResponse( if let Ok(result) = &tool_response.tool_result {
tool_response, let texts: Vec<String> = result
) => { .content
// Extract text from tool response .iter()
if let Ok(result) = &tool_response.tool_result { .filter_map(|content| {
let texts: Vec<String> = result if let rmcp::model::RawContent::Text(raw_text_content) =
.content &content.raw
.iter() {
.filter_map(|content| { Some(raw_text_content.text.clone())
if let rmcp::model::RawContent::Text(raw_text_content) = } else {
&content.raw None
{ }
Some(raw_text_content.text.clone()) })
} else { .collect();
None if !texts.is_empty() {
} Some(format!("Tool result: {}", texts.join("\n")))
})
.collect();
if !texts.is_empty() {
Some(format!("Tool result: {}", texts.join("\n")))
} else {
None
}
} else { } else {
None None
} }
} else {
None
} }
_ => None,
} }
_ => None,
}) })
}) })
.collect(); .collect();
@@ -111,12 +138,15 @@ pub async fn run_complete_subagent_task(
Ok(response_text) Ok(response_text)
} }
fn get_agent_messages( pub const SUBAGENT_TOOL_REQUEST_TYPE: &str = "subagent_tool_request";
fn get_agent_messages_with_notifications(
config: AgentConfig, config: AgentConfig,
recipe: Recipe, recipe: Recipe,
task_config: TaskConfig, task_config: TaskConfig,
session_id: String, session_id: String,
cancellation_token: Option<CancellationToken>, cancellation_token: Option<CancellationToken>,
notification_tx: Option<tokio::sync::mpsc::UnboundedSender<rmcp::model::ServerNotification>>,
) -> AgentMessagesFuture { ) -> AgentMessagesFuture {
Box::pin(async move { Box::pin(async move {
let system_instructions = recipe.instructions.clone().unwrap_or_default(); let system_instructions = recipe.instructions.clone().unwrap_or_default();
@@ -128,11 +158,11 @@ fn get_agent_messages(
let agent = Arc::new(Agent::with_config(config)); let agent = Arc::new(Agent::with_config(config));
agent agent
.update_provider(task_config.provider, &session_id) .update_provider(task_config.provider.clone(), &session_id)
.await .await
.map_err(|e| anyhow!("Failed to set provider on sub agent: {}", e))?; .map_err(|e| anyhow!("Failed to set provider on sub agent: {}", e))?;
for extension in task_config.extensions { for extension in &task_config.extensions {
if let Err(e) = agent.add_extension(extension.clone(), &session_id).await { if let Err(e) = agent.add_extension(extension.clone(), &session_id).await {
debug!( debug!(
"Failed to add extension '{}' to subagent: {}", "Failed to add extension '{}' to subagent: {}",
@@ -147,24 +177,8 @@ fn get_agent_messages(
.apply_recipe_components(recipe.sub_recipes.clone(), recipe.response.clone(), true) .apply_recipe_components(recipe.sub_recipes.clone(), recipe.response.clone(), true)
.await; .await;
let tools = agent.list_tools(&session_id, None).await; let subagent_prompt =
let subagent_prompt = render_template( build_subagent_prompt(&agent, &task_config, &session_id, system_instructions).await?;
"subagent_system.md",
&SubagentPromptContext {
max_turns: task_config
.max_turns
.expect("TaskConfig always sets max_turns"),
subagent_id: session_id.clone(),
task_instructions: system_instructions,
tool_count: tools.len(),
available_tools: tools
.iter()
.map(|t| t.name.to_string())
.collect::<Vec<_>>()
.join(", "),
},
)
.map_err(|e| anyhow!("Failed to render subagent system prompt: {}", e))?;
agent.override_system_prompt(subagent_prompt).await; agent.override_system_prompt(subagent_prompt).await;
let user_message = Message::user().with_text(user_task); let user_message = Message::user().with_text(user_task);
@@ -182,35 +196,186 @@ fn get_agent_messages(
retry_config: recipe.retry, retry_config: recipe.retry,
}; };
let mut stream = agent conversation = run_subagent_stream(
.reply(user_message, session_config, cancellation_token) agent.clone(),
.await user_message,
.map_err(|e| anyhow!("Failed to get reply from agent: {}", e))?; session_config,
while let Some(message_result) = stream.next().await { cancellation_token,
match message_result { &session_id,
Ok(AgentEvent::Message(msg)) => conversation.push(msg), &notification_tx,
Ok(AgentEvent::McpNotification(_)) | Ok(AgentEvent::ModelChange { .. }) => {} conversation,
Ok(AgentEvent::HistoryReplaced(updated_conversation)) => { )
conversation = updated_conversation; .await?;
}
Err(e) => {
tracing::error!("Error receiving message from subagent: {}", e);
break;
}
}
}
let final_output = if has_response_schema { let final_output = get_final_output(&agent, has_response_schema).await;
agent
.final_output_tool
.lock()
.await
.as_ref()
.and_then(|tool| tool.final_output.clone())
} else {
None
};
Ok((conversation, final_output)) Ok((conversation, final_output))
}) })
} }
async fn build_subagent_prompt(
agent: &Agent,
task_config: &TaskConfig,
session_id: &str,
system_instructions: String,
) -> Result<String> {
let tools = agent.list_tools(session_id, None).await;
render_template(
"subagent_system.md",
&SubagentPromptContext {
max_turns: task_config
.max_turns
.expect("TaskConfig always sets max_turns"),
subagent_id: session_id.to_string(),
task_instructions: system_instructions,
tool_count: tools.len(),
available_tools: tools
.iter()
.map(|t| t.name.to_string())
.collect::<Vec<_>>()
.join(", "),
},
)
.map_err(|e| anyhow!("Failed to render subagent system prompt: {}", e))
}
async fn run_subagent_stream(
agent: Arc<Agent>,
user_message: Message,
session_config: SessionConfig,
cancellation_token: Option<CancellationToken>,
session_id: &str,
notification_tx: &Option<tokio::sync::mpsc::UnboundedSender<rmcp::model::ServerNotification>>,
mut conversation: Conversation,
) -> Result<Conversation> {
let mut stream = crate::session_context::with_session_id(Some(session_id.to_string()), async {
agent
.reply(user_message, session_config, cancellation_token)
.await
})
.await
.map_err(|e| anyhow!("Failed to get reply from agent: {}", e))?;
while let Some(message_result) = stream.next().await {
match message_result {
Ok(AgentEvent::Message(msg)) => {
if let Some(ref tx) = notification_tx {
for content in &msg.content {
if let Some(notif) = create_tool_notification(content, session_id) {
if tx.send(notif).is_err() {
debug!("Notification receiver dropped for subagent {}", session_id);
}
}
}
}
conversation.push(msg);
}
Ok(AgentEvent::McpNotification(_)) => {}
Ok(AgentEvent::ModelChange { .. }) => {}
Ok(AgentEvent::HistoryReplaced(updated_conversation)) => {
conversation = updated_conversation;
}
Err(e) => {
tracing::error!("Error receiving message from subagent: {}", e);
break;
}
}
}
Ok(conversation)
}
async fn get_final_output(agent: &Agent, has_response_schema: bool) -> Option<String> {
if has_response_schema {
agent
.final_output_tool
.lock()
.await
.as_ref()
.and_then(|tool| tool.final_output.clone())
} else {
None
}
}
fn create_tool_notification(
content: &MessageContent,
subagent_id: &str,
) -> Option<ServerNotification> {
if let MessageContent::ToolRequest(req) = content {
let tool_call = req.tool_call.as_ref().ok()?;
Some(ServerNotification::LoggingMessageNotification(
LoggingMessageNotification {
method: LoggingMessageNotificationMethod,
params: LoggingMessageNotificationParam {
level: LoggingLevel::Info,
logger: Some(format!("subagent:{}", subagent_id)),
data: serde_json::json!({
"type": SUBAGENT_TOOL_REQUEST_TYPE,
"subagent_id": subagent_id,
"tool_call": {
"name": tool_call.name,
"arguments": tool_call.arguments
}
}),
},
extensions: Default::default(),
},
))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::{create_tool_notification, SUBAGENT_TOOL_REQUEST_TYPE};
use crate::conversation::message::MessageContent;
use rmcp::model::{CallToolRequestParams, ServerNotification};
use serde_json::json;
#[test]
fn create_tool_notification_for_tool_request() {
let tool_call = CallToolRequestParams {
meta: None,
task: None,
name: "developer__shell".to_string().into(),
arguments: Some(json!({"command": "ls"}).as_object().unwrap().clone()),
};
let content = MessageContent::tool_request("req1", Ok(tool_call));
let notification =
create_tool_notification(&content, "session_1").expect("expected notification");
let ServerNotification::LoggingMessageNotification(log_notif) = notification else {
panic!("expected logging notification");
};
let data = log_notif
.params
.data
.as_object()
.expect("expected object data");
assert_eq!(
data.get("type").and_then(|v| v.as_str()),
Some(SUBAGENT_TOOL_REQUEST_TYPE)
);
assert_eq!(
data.get("subagent_id").and_then(|v| v.as_str()),
Some("session_1")
);
let tool_call = data
.get("tool_call")
.and_then(|v| v.as_object())
.expect("expected tool_call object");
assert_eq!(
tool_call.get("name").and_then(|v| v.as_str()),
Some("developer__shell")
);
}
#[test]
fn create_tool_notification_ignores_non_tool_request() {
let content = MessageContent::text("hello");
assert!(create_tool_notification(&content, "session_1").is_none());
}
}
+15 -8
View File
@@ -4,12 +4,14 @@ use std::path::PathBuf;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures::FutureExt; use futures::FutureExt;
use rmcp::model::{Content, ErrorCode, ErrorData, Tool}; use rmcp::model::{Content, ErrorCode, ErrorData, ServerNotification, Tool};
use serde::Deserialize; use serde::Deserialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::agents::subagent_handler::run_complete_subagent_task; use crate::agents::subagent_handler::run_complete_subagent_task_with_notifications;
use crate::agents::subagent_task_config::TaskConfig; use crate::agents::subagent_task_config::TaskConfig;
use crate::agents::tool_execution::ToolCallResult; use crate::agents::tool_execution::ToolCallResult;
use crate::agents::AgentConfig; use crate::agents::AgentConfig;
@@ -31,7 +33,7 @@ Make sure your last message provides a comprehensive summary of:
Be concise but complete. Be concise but complete.
"#; "#;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct SubagentParams { pub struct SubagentParams {
pub instructions: Option<String>, pub instructions: Option<String>,
pub subrecipe: Option<String>, pub subrecipe: Option<String>,
@@ -46,7 +48,7 @@ fn default_summary() -> bool {
true true
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct SubagentSettings { pub struct SubagentSettings {
pub provider: Option<String>, pub provider: Option<String>,
pub model: Option<String>, pub model: Option<String>,
@@ -224,29 +226,33 @@ pub fn handle_subagent_tool(
}; };
let config = config.clone(); let config = config.clone();
let (notification_tx, notification_rx) = mpsc::unbounded_channel();
ToolCallResult { ToolCallResult {
notification_stream: None, notification_stream: Some(Box::new(UnboundedReceiverStream::new(notification_rx))),
result: Box::new( result: Box::new(
execute_subagent( execute_subagent_with_notifications(
config, config,
recipe, recipe,
task_config, task_config,
parsed_params, parsed_params,
working_dir, working_dir,
cancellation_token, cancellation_token,
notification_tx,
) )
.boxed(), .boxed(),
), ),
} }
} }
async fn execute_subagent( async fn execute_subagent_with_notifications(
config: AgentConfig, config: AgentConfig,
recipe: Recipe, recipe: Recipe,
task_config: TaskConfig, task_config: TaskConfig,
params: SubagentParams, params: SubagentParams,
working_dir: PathBuf, working_dir: PathBuf,
cancellation_token: Option<CancellationToken>, cancellation_token: Option<CancellationToken>,
notification_tx: mpsc::UnboundedSender<ServerNotification>,
) -> Result<rmcp::model::CallToolResult, ErrorData> { ) -> Result<rmcp::model::CallToolResult, ErrorData> {
let session = config let session = config
.session_manager .session_manager
@@ -270,13 +276,14 @@ async fn execute_subagent(
data: None, data: None,
})?; })?;
let result = run_complete_subagent_task( let result = run_complete_subagent_task_with_notifications(
config, config,
recipe, recipe,
task_config, task_config,
params.summary, params.summary,
session.id, session.id,
cancellation_token, cancellation_token,
Some(notification_tx),
) )
.await; .await;
@@ -291,10 +291,79 @@ interface Progress {
message?: string; message?: string;
} }
interface SubagentToolRequestData {
type: 'subagent_tool_request';
subagent_id: string;
tool_call: {
name: string;
arguments?: { tool_graph?: ToolGraphNode[] };
};
}
const isSubagentToolRequestData = (data: unknown): data is SubagentToolRequestData => {
if (!data || typeof data !== 'object') {
return false;
}
const record = data as Record<string, unknown>;
if (record.type !== 'subagent_tool_request') {
return false;
}
if (typeof record.subagent_id !== 'string') {
return false;
}
if (!record.tool_call || typeof record.tool_call !== 'object') {
return false;
}
const toolCall = record.tool_call as Record<string, unknown>;
return typeof toolCall.name === 'string';
};
const formatSubagentToolCall = (data: SubagentToolRequestData): string => {
const subagentId = data.subagent_id;
const toolCall = data.tool_call;
const toolCallName = toolCall.name;
const shortId = subagentId?.split('_').pop() || subagentId;
const parts = toolCallName.split('__').reverse();
const toolName = parts[0] || 'unknown';
const extensionName = parts.slice(1).reverse().join('__') || '';
const toolGraph = toolCall.arguments?.tool_graph;
if (toolName === 'execute_code' && toolGraph && toolGraph.length > 0) {
const plural = toolGraph.length === 1 ? '' : 's';
const header = `[subagent:${shortId}] ${toolGraph.length} tool call${plural} | execute_code`;
const lines = toolGraph.map((node, idx) => {
const deps =
node.depends_on && node.depends_on.length > 0
? ` (uses ${node.depends_on.map((d) => d + 1).join(', ')})`
: '';
return ` ${idx + 1}. ${node.tool}: ${node.description}${deps}`;
});
return [header, ...lines].join('\n');
}
return extensionName
? `[subagent:${shortId}] ${toolName} | ${extensionName}`
: `[subagent:${shortId}] ${toolName}`;
};
const logToString = (logMessage: NotificationEvent) => { const logToString = (logMessage: NotificationEvent) => {
const message = logMessage.message as { method: string; params: unknown }; const message = logMessage.message as { method: string; params: unknown };
const params = message.params as Record<string, unknown>; const params = message.params as Record<string, unknown>;
if (
params &&
params.data &&
typeof params.data === 'object' &&
'type' in params.data &&
params.data.type === 'subagent_tool_request'
) {
if (isSubagentToolRequestData(params.data)) {
return formatSubagentToolCall(params.data);
}
}
// Special case for the developer system shell logs // Special case for the developer system shell logs
if ( if (
params && params &&