mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
agent: Show full subagent output if no concurrent tool calls (#50478)
Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
parent
8cd192ec3a
commit
ef60143e7a
8 changed files with 286 additions and 173 deletions
|
|
@ -2,14 +2,42 @@ mod connection;
|
|||
mod diff;
|
||||
mod mention;
|
||||
mod terminal;
|
||||
use action_log::{ActionLog, ActionLogTelemetry};
|
||||
use agent_client_protocol::{self as acp};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashSet;
|
||||
pub use connection::*;
|
||||
pub use diff::*;
|
||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::FormatOnSave;
|
||||
use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};
|
||||
use markdown::Markdown;
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use project::{AgentLocation, Project, git_store::GitStoreCheckpoint};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string_pretty;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::ops::Range;
|
||||
use std::process::ExitStatus;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use task::{Shell, ShellBuilder};
|
||||
pub use terminal::*;
|
||||
use text::Bias;
|
||||
use ui::App;
|
||||
use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Key used in ACP ToolCall meta to store the tool's programmatic name.
|
||||
/// This is a workaround since ACP's ToolCall doesn't have a dedicated name field.
|
||||
pub const TOOL_NAME_META_KEY: &str = "tool_name";
|
||||
|
||||
/// Key used in ACP ToolCall meta to store the session id when a subagent is spawned.
|
||||
pub const SUBAGENT_SESSION_ID_META_KEY: &str = "subagent_session_id";
|
||||
|
||||
/// Helper to extract tool name from ACP meta
|
||||
pub fn tool_name_from_meta(meta: &Option<acp::Meta>) -> Option<SharedString> {
|
||||
meta.as_ref()
|
||||
|
|
@ -18,51 +46,31 @@ pub fn tool_name_from_meta(meta: &Option<acp::Meta>) -> Option<SharedString> {
|
|||
.map(|s| SharedString::from(s.to_owned()))
|
||||
}
|
||||
|
||||
/// Helper to extract subagent session id from ACP meta
|
||||
pub fn subagent_session_id_from_meta(meta: &Option<acp::Meta>) -> Option<acp::SessionId> {
|
||||
meta.as_ref()
|
||||
.and_then(|m| m.get(SUBAGENT_SESSION_ID_META_KEY))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| acp::SessionId::from(s.to_string()))
|
||||
}
|
||||
|
||||
/// Helper to create meta with tool name
|
||||
pub fn meta_with_tool_name(tool_name: &str) -> acp::Meta {
|
||||
acp::Meta::from_iter([(TOOL_NAME_META_KEY.into(), tool_name.into())])
|
||||
}
|
||||
use collections::HashSet;
|
||||
pub use connection::*;
|
||||
pub use diff::*;
|
||||
use language::language_settings::FormatOnSave;
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string_pretty;
|
||||
|
||||
use task::{Shell, ShellBuilder};
|
||||
pub use terminal::*;
|
||||
/// Key used in ACP ToolCall meta to store the session id and message indexes
|
||||
pub const SUBAGENT_SESSION_INFO_META_KEY: &str = "subagent_session_info";
|
||||
|
||||
use action_log::{ActionLog, ActionLogTelemetry};
|
||||
use agent_client_protocol::{self as acp};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};
|
||||
use markdown::Markdown;
|
||||
use project::{AgentLocation, Project, git_store::GitStoreCheckpoint};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::ops::Range;
|
||||
use std::process::ExitStatus;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use text::Bias;
|
||||
use ui::App;
|
||||
use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle};
|
||||
use uuid::Uuid;
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SubagentSessionInfo {
|
||||
/// The session id of the subagent sessiont that was spawned
|
||||
pub session_id: acp::SessionId,
|
||||
/// The index of the message of the start of the "turn" run by this tool call
|
||||
pub message_start_index: usize,
|
||||
/// The index of the output of the message that the subagent has returned
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_end_index: Option<usize>,
|
||||
}
|
||||
|
||||
/// Helper to extract subagent session id from ACP meta
|
||||
pub fn subagent_session_info_from_meta(meta: &Option<acp::Meta>) -> Option<SubagentSessionInfo> {
|
||||
meta.as_ref()
|
||||
.and_then(|m| m.get(SUBAGENT_SESSION_INFO_META_KEY))
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserMessage {
|
||||
|
|
@ -223,7 +231,7 @@ pub struct ToolCall {
|
|||
pub raw_input_markdown: Option<Entity<Markdown>>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
pub tool_name: Option<SharedString>,
|
||||
pub subagent_session_id: Option<acp::SessionId>,
|
||||
pub subagent_session_info: Option<SubagentSessionInfo>,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
|
|
@ -262,7 +270,7 @@ impl ToolCall {
|
|||
|
||||
let tool_name = tool_name_from_meta(&tool_call.meta);
|
||||
|
||||
let subagent_session = subagent_session_id_from_meta(&tool_call.meta);
|
||||
let subagent_session_info = subagent_session_info_from_meta(&tool_call.meta);
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.tool_call_id,
|
||||
|
|
@ -277,7 +285,7 @@ impl ToolCall {
|
|||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
tool_name,
|
||||
subagent_session_id: subagent_session,
|
||||
subagent_session_info,
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
|
@ -310,8 +318,8 @@ impl ToolCall {
|
|||
self.status = status.into();
|
||||
}
|
||||
|
||||
if let Some(subagent_session_id) = subagent_session_id_from_meta(&meta) {
|
||||
self.subagent_session_id = Some(subagent_session_id);
|
||||
if let Some(subagent_session_info) = subagent_session_info_from_meta(&meta) {
|
||||
self.subagent_session_info = Some(subagent_session_info);
|
||||
}
|
||||
|
||||
if let Some(title) = title {
|
||||
|
|
@ -402,7 +410,7 @@ impl ToolCall {
|
|||
|
||||
pub fn is_subagent(&self) -> bool {
|
||||
self.tool_name.as_ref().is_some_and(|s| s == "spawn_agent")
|
||||
|| self.subagent_session_id.is_some()
|
||||
|| self.subagent_session_info.is_some()
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
|
|
@ -1528,7 +1536,7 @@ impl AcpThread {
|
|||
raw_input_markdown: None,
|
||||
raw_output: None,
|
||||
tool_name: None,
|
||||
subagent_session_id: None,
|
||||
subagent_session_info: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
return Ok(());
|
||||
|
|
@ -1690,10 +1698,14 @@ impl AcpThread {
|
|||
|
||||
pub fn tool_call_for_subagent(&self, session_id: &acp::SessionId) -> Option<&ToolCall> {
|
||||
self.entries.iter().find_map(|entry| match entry {
|
||||
AgentThreadEntry::ToolCall(tool_call)
|
||||
if tool_call.subagent_session_id.as_ref() == Some(session_id) =>
|
||||
{
|
||||
Some(tool_call)
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
if let Some(subagent_session_info) = &tool_call.subagent_session_info
|
||||
&& &subagent_session_info.session_id == session_id
|
||||
{
|
||||
Some(tool_call)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1748,6 +1748,10 @@ impl SubagentHandle for NativeSubagentHandle {
|
|||
self.session_id.clone()
|
||||
}
|
||||
|
||||
fn num_entries(&self, cx: &App) -> usize {
|
||||
self.subagent_thread.read(cx).num_messages()
|
||||
}
|
||||
|
||||
fn send(&self, message: String, cx: &AsyncApp) -> Task<Result<String>> {
|
||||
let thread = self.subagent_thread.clone();
|
||||
let acp_thread = self.acp_thread.clone();
|
||||
|
|
@ -1832,7 +1836,7 @@ impl SubagentHandle for NativeSubagentHandle {
|
|||
if content.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(content)
|
||||
Some( content)
|
||||
}
|
||||
})
|
||||
.context("No response from subagent")
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ impl crate::TerminalHandle for FakeTerminalHandle {
|
|||
|
||||
struct FakeSubagentHandle {
|
||||
session_id: acp::SessionId,
|
||||
wait_for_summary_task: Shared<Task<String>>,
|
||||
send_task: Shared<Task<String>>,
|
||||
}
|
||||
|
||||
impl SubagentHandle for FakeSubagentHandle {
|
||||
|
|
@ -167,8 +167,12 @@ impl SubagentHandle for FakeSubagentHandle {
|
|||
self.session_id.clone()
|
||||
}
|
||||
|
||||
fn num_entries(&self, _cx: &App) -> usize {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn send(&self, _message: String, cx: &AsyncApp) -> Task<Result<String>> {
|
||||
let task = self.wait_for_summary_task.clone();
|
||||
let task = self.send_task.clone();
|
||||
cx.background_spawn(async move { Ok(task.await) })
|
||||
}
|
||||
}
|
||||
|
|
@ -273,8 +277,17 @@ async fn test_echo(cx: &mut TestAppContext) {
|
|||
|
||||
let events = events.collect().await;
|
||||
thread.update(cx, |thread, _cx| {
|
||||
assert_eq!(thread.last_message().unwrap().role(), Role::Assistant);
|
||||
assert_eq!(thread.last_message().unwrap().to_markdown(), "Hello\n")
|
||||
assert_eq!(
|
||||
thread.last_received_or_pending_message().unwrap().role(),
|
||||
Role::Assistant
|
||||
);
|
||||
assert_eq!(
|
||||
thread
|
||||
.last_received_or_pending_message()
|
||||
.unwrap()
|
||||
.to_markdown(),
|
||||
"Hello\n"
|
||||
)
|
||||
});
|
||||
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
|
||||
}
|
||||
|
|
@ -426,9 +439,15 @@ async fn test_thinking(cx: &mut TestAppContext) {
|
|||
|
||||
let events = events.collect().await;
|
||||
thread.update(cx, |thread, _cx| {
|
||||
assert_eq!(thread.last_message().unwrap().role(), Role::Assistant);
|
||||
assert_eq!(
|
||||
thread.last_message().unwrap().to_markdown(),
|
||||
thread.last_received_or_pending_message().unwrap().role(),
|
||||
Role::Assistant
|
||||
);
|
||||
assert_eq!(
|
||||
thread
|
||||
.last_received_or_pending_message()
|
||||
.unwrap()
|
||||
.to_markdown(),
|
||||
indoc! {"
|
||||
<think>Think</think>
|
||||
Hello
|
||||
|
|
@ -706,7 +725,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
|
|||
thread.update(cx, |thread, _cx| {
|
||||
assert!(
|
||||
thread
|
||||
.last_message()
|
||||
.last_received_or_pending_message()
|
||||
.unwrap()
|
||||
.as_agent_message()
|
||||
.unwrap()
|
||||
|
|
@ -743,7 +762,7 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
|
|||
if let Ok(ThreadEvent::ToolCall(tool_call)) = event {
|
||||
thread.update(cx, |thread, _cx| {
|
||||
// Look for a tool use in the thread's last message
|
||||
let message = thread.last_message().unwrap();
|
||||
let message = thread.last_received_or_pending_message().unwrap();
|
||||
let agent_message = message.as_agent_message().unwrap();
|
||||
let last_content = agent_message.content.last().unwrap();
|
||||
if let AgentMessageContent::ToolUse(last_tool_use) = last_content {
|
||||
|
|
@ -1213,7 +1232,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
|
|||
assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]);
|
||||
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let last_message = thread.last_message().unwrap();
|
||||
let last_message = thread.last_received_or_pending_message().unwrap();
|
||||
let agent_message = last_message.as_agent_message().unwrap();
|
||||
let text = agent_message
|
||||
.content
|
||||
|
|
@ -1919,7 +1938,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
|||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let message = thread.last_message().unwrap();
|
||||
let message = thread.last_received_or_pending_message().unwrap();
|
||||
let agent_message = message.as_agent_message().unwrap();
|
||||
assert_eq!(
|
||||
agent_message.content,
|
||||
|
|
@ -1988,7 +2007,7 @@ async fn test_terminal_tool_cancellation_captures_output(cx: &mut TestAppContext
|
|||
|
||||
// Verify the tool result contains the terminal output, not just "Tool canceled by user"
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let message = thread.last_message().unwrap();
|
||||
let message = thread.last_received_or_pending_message().unwrap();
|
||||
let agent_message = message.as_agent_message().unwrap();
|
||||
|
||||
let tool_use = agent_message
|
||||
|
|
@ -2144,7 +2163,7 @@ async fn verify_thread_recovery(
|
|||
|
||||
let events = events.collect::<Vec<_>>().await;
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let message = thread.last_message().unwrap();
|
||||
let message = thread.last_received_or_pending_message().unwrap();
|
||||
let agent_message = message.as_agent_message().unwrap();
|
||||
assert_eq!(
|
||||
agent_message.content,
|
||||
|
|
@ -2453,7 +2472,7 @@ async fn test_terminal_tool_stopped_via_terminal_card_button(cx: &mut TestAppCon
|
|||
|
||||
// Verify the tool result indicates user stopped
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let message = thread.last_message().unwrap();
|
||||
let message = thread.last_received_or_pending_message().unwrap();
|
||||
let agent_message = message.as_agent_message().unwrap();
|
||||
|
||||
let tool_use = agent_message
|
||||
|
|
@ -2548,7 +2567,7 @@ async fn test_terminal_tool_timeout_expires(cx: &mut TestAppContext) {
|
|||
|
||||
// Verify the tool result indicates timeout, not user stopped
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let message = thread.last_message().unwrap();
|
||||
let message = thread.last_received_or_pending_message().unwrap();
|
||||
let agent_message = message.as_agent_message().unwrap();
|
||||
|
||||
let tool_use = agent_message
|
||||
|
|
@ -3444,7 +3463,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|||
events.collect::<Vec<_>>().await;
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
assert_eq!(
|
||||
thread.last_message(),
|
||||
thread.last_received_or_pending_message(),
|
||||
Some(Message::Agent(AgentMessage {
|
||||
content: vec![AgentMessageContent::Text("Done".into())],
|
||||
tool_results: IndexMap::default(),
|
||||
|
|
|
|||
|
|
@ -605,7 +605,12 @@ pub trait TerminalHandle {
|
|||
}
|
||||
|
||||
pub trait SubagentHandle {
|
||||
/// The session ID of this subagent thread
|
||||
fn id(&self) -> acp::SessionId;
|
||||
/// The current number of entries in the thread.
|
||||
/// Useful for knowing where the next turn will begin
|
||||
fn num_entries(&self, cx: &App) -> usize;
|
||||
/// Runs a turn for a given message and returns both the response and the index of that output message.
|
||||
fn send(&self, message: String, cx: &AsyncApp) -> Task<Result<String>>;
|
||||
}
|
||||
|
||||
|
|
@ -1324,7 +1329,16 @@ impl Thread {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn last_message(&self) -> Option<Message> {
|
||||
pub fn last_message(&self) -> Option<&Message> {
|
||||
self.messages.last()
|
||||
}
|
||||
|
||||
pub fn num_messages(&self) -> usize {
|
||||
self.messages.len()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn last_received_or_pending_message(&self) -> Option<Message> {
|
||||
if let Some(message) = self.pending_message.clone() {
|
||||
Some(Message::Agent(message))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use acp_thread::SUBAGENT_SESSION_ID_META_KEY;
|
||||
use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::Result;
|
||||
use gpui::{App, SharedString, Task};
|
||||
|
|
@ -24,6 +24,7 @@ use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream, ToolInput};
|
|||
///
|
||||
/// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct SpawnAgentToolInput {
|
||||
/// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives")
|
||||
pub label: String,
|
||||
|
|
@ -34,26 +35,46 @@ pub struct SpawnAgentToolInput {
|
|||
pub session_id: Option<acp::SessionId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SpawnAgentToolOutput {
|
||||
Success {
|
||||
session_id: acp::SessionId,
|
||||
output: String,
|
||||
session_info: SubagentSessionInfo,
|
||||
},
|
||||
Error {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
session_id: Option<acp::SessionId>,
|
||||
error: String,
|
||||
session_info: Option<SubagentSessionInfo>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SpawnAgentToolOutput> for LanguageModelToolResultContent {
|
||||
fn from(output: SpawnAgentToolOutput) -> Self {
|
||||
serde_json::to_string(&output)
|
||||
match output {
|
||||
SpawnAgentToolOutput::Success {
|
||||
session_id,
|
||||
output,
|
||||
session_info: _, // Don't show this to the model
|
||||
} => serde_json::to_string(
|
||||
&serde_json::json!({ "session_id": session_id, "output": output }),
|
||||
)
|
||||
.unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
|
||||
.into()
|
||||
.into(),
|
||||
SpawnAgentToolOutput::Error {
|
||||
session_id,
|
||||
error,
|
||||
session_info: _, // Don't show this to the model
|
||||
} => serde_json::to_string(
|
||||
&serde_json::json!({ "session_id": session_id, "error": error }),
|
||||
)
|
||||
.unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}"))
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,9 +127,10 @@ impl AgentTool for SpawnAgentTool {
|
|||
.map_err(|e| SpawnAgentToolOutput::Error {
|
||||
session_id: None,
|
||||
error: format!("Failed to receive tool input: {e}"),
|
||||
session_info: None,
|
||||
})?;
|
||||
|
||||
let (subagent, subagent_session_id) = cx.update(|cx| {
|
||||
let (subagent, mut session_info) = cx.update(|cx| {
|
||||
let subagent = if let Some(session_id) = input.session_id {
|
||||
self.environment.resume_subagent(session_id, cx)
|
||||
} else {
|
||||
|
|
@ -117,43 +139,48 @@ impl AgentTool for SpawnAgentTool {
|
|||
let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error {
|
||||
session_id: None,
|
||||
error: err.to_string(),
|
||||
session_info: None,
|
||||
})?;
|
||||
let subagent_session_id = subagent.id();
|
||||
let session_info = SubagentSessionInfo {
|
||||
session_id: subagent.id(),
|
||||
message_start_index: subagent.num_entries(cx),
|
||||
message_end_index: None,
|
||||
};
|
||||
|
||||
event_stream.subagent_spawned(subagent_session_id.clone());
|
||||
let meta = acp::Meta::from_iter([(
|
||||
SUBAGENT_SESSION_ID_META_KEY.into(),
|
||||
subagent_session_id.to_string().into(),
|
||||
)]);
|
||||
event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
|
||||
event_stream.subagent_spawned(subagent.id());
|
||||
event_stream.update_fields_with_meta(
|
||||
acp::ToolCallUpdateFields::new(),
|
||||
Some(acp::Meta::from_iter([(
|
||||
SUBAGENT_SESSION_INFO_META_KEY.into(),
|
||||
serde_json::json!(&session_info),
|
||||
)])),
|
||||
);
|
||||
|
||||
Ok((subagent, subagent_session_id))
|
||||
Ok((subagent, session_info))
|
||||
})?;
|
||||
|
||||
match subagent.send(input.message, cx).await {
|
||||
Ok(output) => {
|
||||
event_stream.update_fields(
|
||||
session_info.message_end_index =
|
||||
cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1)));
|
||||
event_stream.update_fields_with_meta(
|
||||
acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]),
|
||||
Some(acp::Meta::from_iter([(
|
||||
SUBAGENT_SESSION_INFO_META_KEY.into(),
|
||||
serde_json::json!(&session_info),
|
||||
)])),
|
||||
);
|
||||
Ok(SpawnAgentToolOutput::Success {
|
||||
session_id: subagent_session_id,
|
||||
session_id: session_info.session_id.clone(),
|
||||
session_info,
|
||||
output,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
let error = e.to_string();
|
||||
// workaround for now because the agent loop will always mark this as ToolCallStatus::Failed
|
||||
let canceled = error == "User canceled";
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![
|
||||
acp::ToolCallContent::Content(acp::Content::new(error.clone()).meta(
|
||||
acp::Meta::from_iter([("cancelled".into(), canceled.into())]),
|
||||
)),
|
||||
]));
|
||||
Err(SpawnAgentToolOutput::Error {
|
||||
session_id: Some(subagent_session_id),
|
||||
error,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(SpawnAgentToolOutput::Error {
|
||||
session_id: Some(session_info.session_id.clone()),
|
||||
error: e.to_string(),
|
||||
session_info: Some(session_info),
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -165,25 +192,29 @@ impl AgentTool for SpawnAgentTool {
|
|||
event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Result<()> {
|
||||
let session_id = match &output {
|
||||
SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id),
|
||||
SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(),
|
||||
let (content, session_info) = match output {
|
||||
SpawnAgentToolOutput::Success {
|
||||
output,
|
||||
session_info,
|
||||
..
|
||||
} => (output.into(), Some(session_info)),
|
||||
SpawnAgentToolOutput::Error {
|
||||
error,
|
||||
session_info,
|
||||
..
|
||||
} => (error.into(), session_info),
|
||||
};
|
||||
|
||||
if let Some(session_id) = session_id {
|
||||
event_stream.subagent_spawned(session_id.clone());
|
||||
let meta = acp::Meta::from_iter([(
|
||||
SUBAGENT_SESSION_ID_META_KEY.into(),
|
||||
session_id.to_string().into(),
|
||||
)]);
|
||||
event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta));
|
||||
}
|
||||
|
||||
let content = match &output {
|
||||
SpawnAgentToolOutput::Success { output, .. } => output.into(),
|
||||
SpawnAgentToolOutput::Error { error, .. } => error.into(),
|
||||
};
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![content]));
|
||||
let meta = session_info.map(|session_info| {
|
||||
acp::Meta::from_iter([(
|
||||
SUBAGENT_SESSION_INFO_META_KEY.into(),
|
||||
serde_json::json!(&session_info),
|
||||
)])
|
||||
});
|
||||
event_stream.update_fields_with_meta(
|
||||
acp::ToolCallUpdateFields::new().content(vec![content]),
|
||||
meta,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -872,7 +872,10 @@ impl ConnectionView {
|
|||
.entries()
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
AgentThreadEntry::ToolCall(call) => call.subagent_session_id.clone(),
|
||||
AgentThreadEntry::ToolCall(call) => call
|
||||
.subagent_session_info
|
||||
.as_ref()
|
||||
.map(|i| i.session_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
|
|||
|
|
@ -3923,7 +3923,7 @@ impl ThreadView {
|
|||
let thread = self.thread.clone();
|
||||
let comments_editor = self.thread_feedback.comments_editor.clone();
|
||||
|
||||
let primary = if entry_ix == total_entries - 1 {
|
||||
let primary = if entry_ix + 1 == total_entries {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(primary)
|
||||
|
|
@ -5002,15 +5002,20 @@ impl ThreadView {
|
|||
|
||||
div().w_full().map(|this| {
|
||||
if tool_call.is_subagent() {
|
||||
this.child(self.render_subagent_tool_call(
|
||||
active_session_id,
|
||||
entry_ix,
|
||||
tool_call,
|
||||
tool_call.subagent_session_id.clone(),
|
||||
focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
this.child(
|
||||
self.render_subagent_tool_call(
|
||||
active_session_id,
|
||||
entry_ix,
|
||||
tool_call,
|
||||
tool_call
|
||||
.subagent_session_info
|
||||
.as_ref()
|
||||
.map(|i| i.session_id.clone()),
|
||||
focus_handle,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
} else if has_terminals {
|
||||
this.children(tool_call.terminals().map(|terminal| {
|
||||
self.render_terminal_tool_call(
|
||||
|
|
@ -6667,6 +6672,34 @@ impl ThreadView {
|
|||
.into_any_element()
|
||||
}
|
||||
|
||||
/// This will return `true` if there were no other tool calls during the same turn as the given tool call (no concurrent tool calls).
|
||||
fn should_show_subagent_fullscreen(&self, tool_call: &ToolCall, cx: &App) -> bool {
|
||||
let parent_thread = self.thread.read(cx);
|
||||
|
||||
let Some(tool_call_index) = parent_thread
|
||||
.entries()
|
||||
.iter()
|
||||
.position(|e| matches!(e, AgentThreadEntry::ToolCall(tc) if tc.id == tool_call.id))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Some(AgentThreadEntry::ToolCall(_)) =
|
||||
parent_thread.entries().get(tool_call_index + 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(AgentThreadEntry::ToolCall(_)) = parent_thread
|
||||
.entries()
|
||||
.get(tool_call_index.saturating_sub(1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn render_subagent_expanded_content(
|
||||
&self,
|
||||
thread_view: &Entity<ThreadView>,
|
||||
|
|
@ -6677,29 +6710,7 @@ impl ThreadView {
|
|||
) -> impl IntoElement {
|
||||
const MAX_PREVIEW_ENTRIES: usize = 8;
|
||||
|
||||
let parent_thread = self.thread.read(cx);
|
||||
let mut started_subagent_count = 0usize;
|
||||
let mut turn_has_our_call = false;
|
||||
for entry in parent_thread.entries().iter() {
|
||||
match entry {
|
||||
AgentThreadEntry::UserMessage(_) => {
|
||||
if turn_has_our_call {
|
||||
break;
|
||||
}
|
||||
started_subagent_count = 0;
|
||||
turn_has_our_call = false;
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tc)
|
||||
if tc.is_subagent() && !matches!(tc.status, ToolCallStatus::Pending) =>
|
||||
{
|
||||
started_subagent_count += 1;
|
||||
if tc.id == tool_call.id {
|
||||
turn_has_our_call = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let should_show_subagent_fullscreen = self.should_show_subagent_fullscreen(tool_call, cx);
|
||||
|
||||
let subagent_view = thread_view.read(cx);
|
||||
let session_id = subagent_view.thread.read(cx).session_id().clone();
|
||||
|
|
@ -6725,11 +6736,22 @@ impl ThreadView {
|
|||
|
||||
let entries = subagent_view.thread.read(cx).entries();
|
||||
let total_entries = entries.len();
|
||||
let start_ix = if started_subagent_count > 1 {
|
||||
total_entries.saturating_sub(MAX_PREVIEW_ENTRIES)
|
||||
let mut entry_range = if let Some(info) = tool_call.subagent_session_info.as_ref() {
|
||||
info.message_start_index
|
||||
..info
|
||||
.message_end_index
|
||||
.map(|i| (i + 1).min(total_entries))
|
||||
.unwrap_or(total_entries)
|
||||
} else {
|
||||
0
|
||||
0..total_entries
|
||||
};
|
||||
if !should_show_subagent_fullscreen {
|
||||
entry_range.start = entry_range
|
||||
.end
|
||||
.saturating_sub(MAX_PREVIEW_ENTRIES)
|
||||
.max(entry_range.start);
|
||||
};
|
||||
let start_ix = entry_range.start;
|
||||
|
||||
let scroll_handle = self
|
||||
.subagent_scroll_handles
|
||||
|
|
@ -6741,12 +6763,14 @@ impl ThreadView {
|
|||
scroll_handle.scroll_to_bottom();
|
||||
}
|
||||
|
||||
let rendered_entries: Vec<AnyElement> = entries[start_ix..]
|
||||
let rendered_entries: Vec<AnyElement> = entries
|
||||
.get(entry_range)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| {
|
||||
let actual_ix = start_ix + i;
|
||||
subagent_view.render_entry(actual_ix, total_entries + 1, entry, window, cx)
|
||||
subagent_view.render_entry(actual_ix, total_entries, entry, window, cx)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
@ -6764,7 +6788,7 @@ impl ThreadView {
|
|||
.track_scroll(&scroll_handle)
|
||||
.children(rendered_entries),
|
||||
)
|
||||
.when(started_subagent_count > 1, |this| {
|
||||
.when(!should_show_subagent_fullscreen, |this| {
|
||||
this.h_56().child(overlay)
|
||||
})
|
||||
.into_any_element()
|
||||
|
|
|
|||
|
|
@ -126,14 +126,19 @@ impl EntryViewState {
|
|||
let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
|
||||
let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
|
||||
|
||||
let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) {
|
||||
views
|
||||
let views = if let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) {
|
||||
&mut tool_call.content
|
||||
} else {
|
||||
self.set_entry(index, Entry::empty());
|
||||
let Some(Entry::Content(views)) = self.entries.get_mut(index) else {
|
||||
self.set_entry(
|
||||
index,
|
||||
Entry::ToolCall(ToolCallEntry {
|
||||
content: HashMap::default(),
|
||||
}),
|
||||
);
|
||||
let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) else {
|
||||
unreachable!()
|
||||
};
|
||||
views
|
||||
&mut tool_call.content
|
||||
};
|
||||
|
||||
let is_tool_call_completed =
|
||||
|
|
@ -250,8 +255,8 @@ impl EntryViewState {
|
|||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
Entry::Content(response_views) => {
|
||||
for view in response_views.values() {
|
||||
Entry::ToolCall(ToolCallEntry { content }) => {
|
||||
for view in content.values() {
|
||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||
diff_editor.update(cx, |diff_editor, cx| {
|
||||
diff_editor.set_text_style_refinement(
|
||||
|
|
@ -305,25 +310,30 @@ impl AssistantMessageEntry {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolCallEntry {
|
||||
content: HashMap<EntityId, AnyEntity>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Entry {
|
||||
UserMessage(Entity<MessageEditor>),
|
||||
AssistantMessage(AssistantMessageEntry),
|
||||
Content(HashMap<EntityId, AnyEntity>),
|
||||
ToolCall(ToolCallEntry),
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn focus_handle(&self, cx: &App) -> Option<FocusHandle> {
|
||||
match self {
|
||||
Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
|
||||
Self::AssistantMessage(_) | Self::Content(_) => None,
|
||||
Self::AssistantMessage(_) | Self::ToolCall(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||
match self {
|
||||
Self::UserMessage(editor) => Some(editor),
|
||||
Self::AssistantMessage(_) | Self::Content(_) => None,
|
||||
Self::AssistantMessage(_) | Self::ToolCall(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,25 +360,21 @@ impl Entry {
|
|||
) -> Option<ScrollHandle> {
|
||||
match self {
|
||||
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
|
||||
Self::UserMessage(_) | Self::Content(_) => None,
|
||||
Self::UserMessage(_) | Self::ToolCall(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
||||
match self {
|
||||
Self::Content(map) => Some(map),
|
||||
Self::ToolCall(ToolCallEntry { content }) => Some(content),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self::Content(HashMap::default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn has_content(&self) -> bool {
|
||||
match self {
|
||||
Self::Content(map) => !map.is_empty(),
|
||||
Self::ToolCall(ToolCallEntry { content }) => !content.is_empty(),
|
||||
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue