Bump version to 1.6.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4ba9706e35
commit
59cfd0e2f5
@@ -482,7 +482,9 @@ impl BlocklistAIActionModel {
|
||||
.and_then(|queue| queue.front())
|
||||
.cloned()
|
||||
else {
|
||||
log::info!("[tool-debug] try_to_execute_available_actions: no more pending actions");
|
||||
log::info!(
|
||||
"[tool-debug] try_to_execute_available_actions: no more pending actions"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -739,14 +739,20 @@ impl BlocklistAIActionExecutor {
|
||||
);
|
||||
match execution {
|
||||
AnyActionExecution::NotReady => {
|
||||
log::info!("[tool-debug] try_to_execute_action: NOT READY - action_id={:?}", action_id);
|
||||
log::info!(
|
||||
"[tool-debug] try_to_execute_action: NOT READY - action_id={:?}",
|
||||
action_id
|
||||
);
|
||||
TryExecuteResult::NotExecuted {
|
||||
reason: NotExecutedReason::NotReady,
|
||||
action: Box::new(action_clone),
|
||||
}
|
||||
}
|
||||
AnyActionExecution::InvalidAction => {
|
||||
log::error!("[tool-debug] try_to_execute_action: INVALID ACTION - action_id={:?}", action_id);
|
||||
log::error!(
|
||||
"[tool-debug] try_to_execute_action: INVALID ACTION - action_id={:?}",
|
||||
action_id
|
||||
);
|
||||
debug_assert!(false, "Tried to execute AIAgentAction with wrong executor.");
|
||||
TryExecuteResult::NotExecuted {
|
||||
reason: NotExecutedReason::NotReady,
|
||||
|
||||
@@ -55,7 +55,8 @@ impl AskUserQuestionExecutor {
|
||||
|
||||
// For child agent conversations, route the question to the parent
|
||||
// for silent auto-answer instead of presenting UI to the user.
|
||||
if let Some(parent_conversation_id) = self.parent_conversation_id(input.conversation_id, ctx)
|
||||
if let Some(parent_conversation_id) =
|
||||
self.parent_conversation_id(input.conversation_id, ctx)
|
||||
{
|
||||
let question_text = questions
|
||||
.iter()
|
||||
@@ -86,14 +87,12 @@ impl AskUserQuestionExecutor {
|
||||
async move { receiver.recv().await },
|
||||
|result, _ctx| match result {
|
||||
Ok(AskUserQuestionDecision::Completed(answers)) => {
|
||||
AIAgentActionResultType::AskUserQuestion(
|
||||
AskUserQuestionResult::Success { answers },
|
||||
)
|
||||
AIAgentActionResultType::AskUserQuestion(AskUserQuestionResult::Success {
|
||||
answers,
|
||||
})
|
||||
}
|
||||
Ok(AskUserQuestionDecision::Cancelled) | Err(_) => {
|
||||
AIAgentActionResultType::AskUserQuestion(
|
||||
AskUserQuestionResult::Cancelled,
|
||||
)
|
||||
AIAgentActionResultType::AskUserQuestion(AskUserQuestionResult::Cancelled)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -141,12 +141,18 @@ impl CallMCPToolExecutor {
|
||||
};
|
||||
|
||||
let Some(reconnecting_peer) = templatable_peer else {
|
||||
log::error!("[tool-debug] CallMCPToolExecutor: MCP server for tool '{}' NOT FOUND", name_owned);
|
||||
log::error!(
|
||||
"[tool-debug] CallMCPToolExecutor: MCP server for tool '{}' NOT FOUND",
|
||||
name_owned
|
||||
);
|
||||
return ActionExecution::Sync(AIAgentActionResultType::CallMCPTool(
|
||||
CallMCPToolResult::Error("MCP server for tool not found".to_owned()),
|
||||
));
|
||||
};
|
||||
log::info!("[tool-debug] CallMCPToolExecutor: found MCP server peer for tool '{}'", name_owned);
|
||||
log::info!(
|
||||
"[tool-debug] CallMCPToolExecutor: found MCP server peer for tool '{}'",
|
||||
name_owned
|
||||
);
|
||||
|
||||
let name_owned_inner = name_owned.clone();
|
||||
ActionExecution::new_async(
|
||||
|
||||
@@ -111,7 +111,11 @@ impl FileGlobExecutor {
|
||||
else {
|
||||
return ActionExecution::InvalidAction;
|
||||
};
|
||||
log::info!("[tool-debug] FileGlobExecutor::execute: patterns={:?}, path={:?}", patterns, path);
|
||||
log::info!(
|
||||
"[tool-debug] FileGlobExecutor::execute: patterns={:?}, path={:?}",
|
||||
patterns,
|
||||
path
|
||||
);
|
||||
|
||||
// If the path is not provided, use the current working directory.
|
||||
let path = path.clone().unwrap_or_else(|| ".".to_string());
|
||||
|
||||
@@ -252,7 +252,11 @@ impl GrepExecutor {
|
||||
else {
|
||||
return ActionExecution::InvalidAction;
|
||||
};
|
||||
log::info!("[tool-debug] GrepExecutor::execute: queries={:?}, path={:?}", queries, path);
|
||||
log::info!(
|
||||
"[tool-debug] GrepExecutor::execute: queries={:?}, path={:?}",
|
||||
queries,
|
||||
path
|
||||
);
|
||||
|
||||
let shell_launch_data = self.active_session.as_ref(ctx).shell_launch_data(ctx);
|
||||
let shell_type = self.active_session.as_ref(ctx).shell_type(ctx);
|
||||
|
||||
@@ -151,10 +151,16 @@ impl RequestFileEditsExecutor {
|
||||
else {
|
||||
return ActionExecution::InvalidAction;
|
||||
};
|
||||
log::info!("[tool-debug] RequestFileEditsExecutor::execute: action_id={:?}", id);
|
||||
log::info!(
|
||||
"[tool-debug] RequestFileEditsExecutor::execute: action_id={:?}",
|
||||
id
|
||||
);
|
||||
|
||||
let Some(diff_view) = self.diff_views.get(id) else {
|
||||
log::warn!("[tool-debug] RequestFileEditsExecutor: no diff view found for action_id={:?}", id);
|
||||
log::warn!(
|
||||
"[tool-debug] RequestFileEditsExecutor: no diff view found for action_id={:?}",
|
||||
id
|
||||
);
|
||||
return ActionExecution::NotReady;
|
||||
};
|
||||
|
||||
|
||||
@@ -68,7 +68,9 @@ impl StartAgentExecutor {
|
||||
let history_model = BlocklistAIHistoryModel::handle(ctx);
|
||||
ctx.subscribe_to_model(&history_model, Self::handle_history_event);
|
||||
|
||||
Self { pending: Vec::new() }
|
||||
Self {
|
||||
pending: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_history_event(
|
||||
@@ -91,8 +93,7 @@ impl StartAgentExecutor {
|
||||
let parent_id = conversation.parent_conversation_id();
|
||||
// Find the first pending entry that matches this parent and hasn't been assigned a child yet.
|
||||
if let Some(pending) = self.pending.iter_mut().find(|p| {
|
||||
p.child_conversation_id.is_none()
|
||||
&& parent_id == Some(p.parent_conversation_id)
|
||||
p.child_conversation_id.is_none() && parent_id == Some(p.parent_conversation_id)
|
||||
}) {
|
||||
pending.child_conversation_id = Some(*new_conversation_id);
|
||||
}
|
||||
@@ -100,17 +101,19 @@ impl StartAgentExecutor {
|
||||
BlocklistAIHistoryEvent::ConversationServerTokenAssigned {
|
||||
conversation_id, ..
|
||||
} => {
|
||||
let Some(idx) = self.pending.iter().position(|p| {
|
||||
p.child_conversation_id.as_ref() == Some(conversation_id)
|
||||
}) else {
|
||||
let Some(idx) = self
|
||||
.pending
|
||||
.iter()
|
||||
.position(|p| p.child_conversation_id.as_ref() == Some(conversation_id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Don't remove yet if we're waiting for completion — we need
|
||||
// the entry to stay so UpdatedConversationStatus can find it.
|
||||
if self.pending[idx].wait_for_completion {
|
||||
// Just log and continue — we'll resolve on Success status.
|
||||
let conversation = BlocklistAIHistoryModel::as_ref(ctx)
|
||||
.conversation(conversation_id);
|
||||
let conversation =
|
||||
BlocklistAIHistoryModel::as_ref(ctx).conversation(conversation_id);
|
||||
let agent_id = conversation
|
||||
.and_then(|c| c.orchestration_agent_id())
|
||||
.or_else(|| {
|
||||
@@ -132,8 +135,8 @@ impl StartAgentExecutor {
|
||||
return;
|
||||
}
|
||||
let pending = self.pending.remove(idx);
|
||||
let conversation = BlocklistAIHistoryModel::as_ref(ctx)
|
||||
.conversation(conversation_id);
|
||||
let conversation =
|
||||
BlocklistAIHistoryModel::as_ref(ctx).conversation(conversation_id);
|
||||
// orchestration_agent_id() uses run_id in v2 mode, which won't
|
||||
// exist for locally-spawned Bedrock child agents. Fall back to
|
||||
// the server conversation token (set by the stream Init event)
|
||||
@@ -195,9 +198,11 @@ impl StartAgentExecutor {
|
||||
BlocklistAIHistoryEvent::UpdatedConversationStatus {
|
||||
conversation_id, ..
|
||||
} => {
|
||||
let Some(idx) = self.pending.iter().position(|p| {
|
||||
p.child_conversation_id.as_ref() == Some(conversation_id)
|
||||
}) else {
|
||||
let Some(idx) = self
|
||||
.pending
|
||||
.iter()
|
||||
.position(|p| p.child_conversation_id.as_ref() == Some(conversation_id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let history = BlocklistAIHistoryModel::as_ref(ctx);
|
||||
@@ -227,10 +232,9 @@ impl StartAgentExecutor {
|
||||
.map(|t| t.as_str().to_string())
|
||||
})
|
||||
.unwrap_or_else(|| conversation_id.to_string());
|
||||
let _ = pending.sender.try_send(StartAgentDecision::Completed {
|
||||
agent_id,
|
||||
output,
|
||||
});
|
||||
let _ = pending
|
||||
.sender
|
||||
.try_send(StartAgentDecision::Completed { agent_id, output });
|
||||
}
|
||||
status => {
|
||||
let error_msg = start_agent_error_message_for_status(
|
||||
|
||||
@@ -72,7 +72,7 @@ fn execute_returns_error_when_child_startup_is_blocked_before_initialization() {
|
||||
assert_eq!(
|
||||
executor
|
||||
.pending
|
||||
.as_ref()
|
||||
.first()
|
||||
.and_then(|pending| pending.child_conversation_id),
|
||||
Some(child_conversation_id)
|
||||
);
|
||||
@@ -102,7 +102,7 @@ fn execute_returns_error_when_child_startup_is_blocked_before_initialization() {
|
||||
));
|
||||
|
||||
executor.read(&app, |executor, _| {
|
||||
assert!(executor.pending.is_none());
|
||||
assert!(executor.pending.is_empty());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,10 +60,7 @@ pub fn render_subagent_inline_panel(
|
||||
};
|
||||
|
||||
let status = conversation.status().clone();
|
||||
let agent_name = conversation
|
||||
.agent_name()
|
||||
.unwrap_or("Subagent")
|
||||
.to_string();
|
||||
let agent_name = conversation.agent_name().unwrap_or("Subagent").to_string();
|
||||
|
||||
let panel_bg = blended_colors::neutral_2(theme);
|
||||
|
||||
@@ -76,13 +73,7 @@ pub fn render_subagent_inline_panel(
|
||||
let header_expanded = state.is_expanded;
|
||||
column.add_child(
|
||||
Hoverable::new(header_mouse_state, move |_mouse_state| {
|
||||
render_panel_header(
|
||||
&agent_name,
|
||||
&header_status,
|
||||
header_expanded,
|
||||
panel_bg,
|
||||
app,
|
||||
)
|
||||
render_panel_header(&agent_name, &header_status, header_expanded, panel_bg, app)
|
||||
})
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AIBlockAction::ToggleSubagentPanel {
|
||||
@@ -137,12 +128,11 @@ fn render_panel_header(
|
||||
let mut left_side = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
let (icon, icon_color) = status.status_icon_and_color(theme);
|
||||
let status_icon_element = ConstrainedBox::new(
|
||||
galaxyui::elements::Icon::new(icon.into(), icon_color).finish(),
|
||||
)
|
||||
.with_width(icon_size(app))
|
||||
.with_height(icon_size(app))
|
||||
.finish();
|
||||
let status_icon_element =
|
||||
ConstrainedBox::new(galaxyui::elements::Icon::new(icon.into(), icon_color).finish())
|
||||
.with_width(icon_size(app))
|
||||
.with_height(icon_size(app))
|
||||
.finish();
|
||||
|
||||
left_side.add_child(
|
||||
Container::new(status_icon_element)
|
||||
@@ -192,11 +182,7 @@ fn render_panel_header(
|
||||
.with_width(icon_size(app))
|
||||
.with_height(icon_size(app))
|
||||
.finish();
|
||||
right_side.add_child(
|
||||
Container::new(chevron)
|
||||
.with_margin_right(4.)
|
||||
.finish(),
|
||||
);
|
||||
right_side.add_child(Container::new(chevron).with_margin_right(4.).finish());
|
||||
|
||||
header_row.add_child(right_side.finish());
|
||||
|
||||
@@ -208,10 +194,7 @@ fn render_panel_header(
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn collect_mini_transcript(
|
||||
conversation_id: &AIConversationId,
|
||||
app: &AppContext,
|
||||
) -> Vec<String> {
|
||||
fn collect_mini_transcript(conversation_id: &AIConversationId, app: &AppContext) -> Vec<String> {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let Some(conversation) = history_model.conversation(conversation_id) else {
|
||||
return vec![];
|
||||
@@ -290,10 +273,7 @@ fn render_mini_transcript(
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn get_completion_summary(
|
||||
conversation_id: &AIConversationId,
|
||||
app: &AppContext,
|
||||
) -> Option<String> {
|
||||
fn get_completion_summary(conversation_id: &AIConversationId, app: &AppContext) -> Option<String> {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let conversation = history_model.conversation(conversation_id)?;
|
||||
|
||||
@@ -317,11 +297,7 @@ fn get_completion_summary(
|
||||
None
|
||||
}
|
||||
|
||||
fn render_summary_footer(
|
||||
summary: &str,
|
||||
_background: ColorU,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
fn render_summary_footer(summary: &str, _background: ColorU, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let surface = theme.surface_2();
|
||||
|
||||
@@ -391,12 +391,12 @@ pub(super) struct AIBlockStateHandles {
|
||||
/// Mouse state handle for the fork conversation button
|
||||
fork_conversation_handle: MouseStateHandle,
|
||||
|
||||
|
||||
/// Mouse state handles per citation.
|
||||
/// A given citation should only appear once per block.
|
||||
footer_citation_chip_handles: HashMap<AIAgentCitation, MouseStateHandle>,
|
||||
orchestration_navigation_card_handles: HashMap<AIAgentActionId, MouseStateHandle>,
|
||||
pub(super) subagent_panel_states: HashMap<AIAgentActionId, super::agent_view::subagent_inline_panel::SubagentPanelState>,
|
||||
pub(super) subagent_panel_states:
|
||||
HashMap<AIAgentActionId, super::agent_view::subagent_inline_panel::SubagentPanelState>,
|
||||
|
||||
references_section_collapsible_handle: MouseStateHandle,
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ use super::{
|
||||
cli_controller::{CLISubagentController, CLISubagentEvent, UserTakeOverReason},
|
||||
model::{AIBlockModel, AIBlockModelImpl, AIBlockOutputStatus},
|
||||
view_impl::common::{
|
||||
render_switch_control_to_user_button, render_warping_indicator,
|
||||
random_load_output_message, render_switch_control_to_user_button, render_warping_indicator,
|
||||
render_warping_indicator_base, ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText,
|
||||
WarpingIndicatorProps, WarpingProps, random_load_output_message,
|
||||
WAITING_FOR_USER_INPUT_MESSAGE,
|
||||
WarpingIndicatorProps, WarpingProps, WAITING_FOR_USER_INPUT_MESSAGE,
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
|
||||
@@ -474,8 +474,10 @@ pub fn render_warping_indicator<V: View>(
|
||||
} else {
|
||||
// Show elapsed timer alongside the random Galaxy status message.
|
||||
if let Some(start_time) = props.warping_start_time {
|
||||
non_shimmering_text =
|
||||
Some(format!(" ({})", format_elapsed_compact(start_time.elapsed())));
|
||||
non_shimmering_text = Some(format!(
|
||||
" ({})",
|
||||
format_elapsed_compact(start_time.elapsed())
|
||||
));
|
||||
}
|
||||
props.default_warping_text.clone()
|
||||
}
|
||||
@@ -3430,19 +3432,59 @@ pub struct FindContext<'a> {
|
||||
|
||||
/// A palette of colors for the user avatar silhouette, randomly selected per session.
|
||||
const USER_AVATAR_PALETTE: &[ColorU] = &[
|
||||
ColorU { r: 99, g: 179, b: 237, a: 255 }, // blue
|
||||
ColorU { r: 129, g: 230, b: 217, a: 255 }, // teal
|
||||
ColorU { r: 183, g: 148, b: 244, a: 255 }, // purple
|
||||
ColorU { r: 252, g: 165, b: 165, a: 255 }, // red/coral
|
||||
ColorU { r: 251, g: 191, b: 36, a: 255 }, // amber
|
||||
ColorU { r: 110, g: 231, b: 183, a: 255 }, // green
|
||||
ColorU { r: 249, g: 168, b: 212, a: 255 }, // pink
|
||||
ColorU { r: 253, g: 186, b: 116, a: 255 }, // orange
|
||||
ColorU {
|
||||
r: 99,
|
||||
g: 179,
|
||||
b: 237,
|
||||
a: 255,
|
||||
}, // blue
|
||||
ColorU {
|
||||
r: 129,
|
||||
g: 230,
|
||||
b: 217,
|
||||
a: 255,
|
||||
}, // teal
|
||||
ColorU {
|
||||
r: 183,
|
||||
g: 148,
|
||||
b: 244,
|
||||
a: 255,
|
||||
}, // purple
|
||||
ColorU {
|
||||
r: 252,
|
||||
g: 165,
|
||||
b: 165,
|
||||
a: 255,
|
||||
}, // red/coral
|
||||
ColorU {
|
||||
r: 251,
|
||||
g: 191,
|
||||
b: 36,
|
||||
a: 255,
|
||||
}, // amber
|
||||
ColorU {
|
||||
r: 110,
|
||||
g: 231,
|
||||
b: 183,
|
||||
a: 255,
|
||||
}, // green
|
||||
ColorU {
|
||||
r: 249,
|
||||
g: 168,
|
||||
b: 212,
|
||||
a: 255,
|
||||
}, // pink
|
||||
ColorU {
|
||||
r: 253,
|
||||
g: 186,
|
||||
b: 116,
|
||||
a: 255,
|
||||
}, // orange
|
||||
];
|
||||
|
||||
fn session_avatar_color() -> ColorU {
|
||||
use std::sync::OnceLock;
|
||||
use rand::Rng;
|
||||
use std::sync::OnceLock;
|
||||
static COLOR: OnceLock<ColorU> = OnceLock::new();
|
||||
*COLOR.get_or_init(|| {
|
||||
let idx = rand::thread_rng().gen_range(0..USER_AVATAR_PALETTE.len());
|
||||
|
||||
@@ -11,9 +11,8 @@ use super::{blocklist_image_asset_source, ResolvedBlocklistImageSources};
|
||||
use super::{
|
||||
collect_visual_markdown_lightbox_collection, compute_visual_section_width,
|
||||
format_elapsed_compact, inline_image_source_label, lightbox_trigger_for_section,
|
||||
query_prefix_highlight_len, render_scrollable_collapsible_content,
|
||||
text_sections_with_indices, CollapsibleElementState, CollapsibleExpansionState,
|
||||
VisualMarkdownLightboxCollection,
|
||||
query_prefix_highlight_len, render_scrollable_collapsible_content, text_sections_with_indices,
|
||||
CollapsibleElementState, CollapsibleExpansionState, VisualMarkdownLightboxCollection,
|
||||
};
|
||||
use crate::{
|
||||
ai::agent::{
|
||||
@@ -308,7 +307,10 @@ fn format_elapsed_compact_shows_minutes_under_120_minutes() {
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(90)), "1m");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(120)), "2m");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(45 * 60)), "45m");
|
||||
assert_eq!(format_elapsed_compact(Duration::from_secs(119 * 60 + 59)), "119m");
|
||||
assert_eq!(
|
||||
format_elapsed_compact(Duration::from_secs(119 * 60 + 59)),
|
||||
"119m"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -433,9 +433,7 @@ pub(super) fn render_start_agent(
|
||||
}
|
||||
if let Some(card_data) = child_conversation_card_data {
|
||||
// Render inline subagent panel instead of navigation card
|
||||
if let Some(panel_state) =
|
||||
props.state_handles.subagent_panel_states.get(action_id)
|
||||
{
|
||||
if let Some(panel_state) = props.state_handles.subagent_panel_states.get(action_id) {
|
||||
column.add_child(
|
||||
crate::ai::blocklist::agent_view::subagent_inline_panel::render_subagent_inline_panel(
|
||||
panel_state,
|
||||
|
||||
@@ -85,8 +85,8 @@ use crate::{
|
||||
},
|
||||
requested_command::RequestedCommand,
|
||||
search_codebase::SearchCodebaseView,
|
||||
summarization::SummarizationView,
|
||||
suggested_unit_tests::SuggestedUnitTestsView,
|
||||
summarization::SummarizationView,
|
||||
web_fetch::WebFetchView,
|
||||
web_search::WebSearchView,
|
||||
},
|
||||
@@ -127,8 +127,8 @@ use super::{
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Expanded, Fill, Flex, FormattedTextElement, Hoverable, MainAxisAlignment,
|
||||
MainAxisSize, ParentElement, Radius, Shrinkable, Text, Wrap,
|
||||
Expanded, Fill, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
ParentElement, Radius, Shrinkable, Text, Wrap,
|
||||
},
|
||||
keymap::Keystroke,
|
||||
platform::{Cursor, OperatingSystem},
|
||||
@@ -217,9 +217,7 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
.iter()
|
||||
.any(|i| matches!(i, AIAgentInput::SummarizeConversation { .. }));
|
||||
if is_summarize_input {
|
||||
let key = crate::ai::agent::MessageId::new(
|
||||
"__summarization_inline_view__".to_string(),
|
||||
);
|
||||
let key = crate::ai::agent::MessageId::new("__summarization_inline_view__".to_string());
|
||||
if let Some(summarization_view) = props.summarization_views.get(&key) {
|
||||
output_items.add_child(ChildView::new(summarization_view).finish());
|
||||
}
|
||||
@@ -827,8 +825,7 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
if let Some(summarization_view) =
|
||||
props.summarization_views.get(&output_message.id)
|
||||
{
|
||||
output_items
|
||||
.add_child(ChildView::new(summarization_view).finish());
|
||||
output_items.add_child(ChildView::new(summarization_view).finish());
|
||||
} else if !are_all_text_sections_empty(&text.sections) {
|
||||
let header_text = "Conversation summarized".to_string();
|
||||
if let Some(element) = render_collapsible_block(
|
||||
@@ -3178,7 +3175,6 @@ fn render_response_footer(props: Props, app: &AppContext) -> Option<Box<dyn Elem
|
||||
flex.add_child(fork_button);
|
||||
}
|
||||
|
||||
|
||||
// Review changes button.
|
||||
if props.has_accepted_edits && !props.shared_session_status.is_viewer() {
|
||||
// Only show Review Changes button if we're in a git repository
|
||||
@@ -3209,7 +3205,6 @@ fn render_response_footer(props: Props, app: &AppContext) -> Option<Box<dyn Elem
|
||||
Some(flex.finish().with_content_item_spacing().finish())
|
||||
}
|
||||
|
||||
|
||||
pub fn action_icon<V: View>(
|
||||
action_id: &AIAgentActionId,
|
||||
action_model: &ModelHandle<BlocklistAIActionModel>,
|
||||
|
||||
+500
-114
@@ -73,7 +73,7 @@ use itertools::Itertools;
|
||||
use parking_lot::FairMutex;
|
||||
use pending_response_streams::PendingResponseStreams;
|
||||
use session_sharing_protocol::common::ParticipantId;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use warp_multi_agent_api::{message, Task, ToolType};
|
||||
@@ -168,6 +168,60 @@ pub enum BlocklistAIControllerEvent {
|
||||
FreeTierLimitCheckTriggered,
|
||||
}
|
||||
|
||||
/// Tracks recent failed action signatures for loop detection.
|
||||
/// When the same tool+input pattern fails repeatedly, we inject
|
||||
/// corrective instructions to break the cycle.
|
||||
#[derive(Debug, Clone)]
|
||||
struct LoopDetectionEntry {
|
||||
/// Discriminant of the action result type (e.g. RequestCommandOutput, ApplyFileDiffs)
|
||||
tool_discriminant: std::mem::Discriminant<AIAgentActionResultType>,
|
||||
/// Hash of the action's identifying input (command string, file paths, etc.)
|
||||
input_hash: u64,
|
||||
/// Human-readable description of what failed
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct LoopDetectionState {
|
||||
recent_failures: VecDeque<LoopDetectionEntry>,
|
||||
}
|
||||
|
||||
const LOOP_DETECTION_WINDOW: usize = 10;
|
||||
const LOOP_DETECTION_THRESHOLD: usize = 3;
|
||||
|
||||
impl LoopDetectionState {
|
||||
fn record_failure(&mut self, entry: LoopDetectionEntry) {
|
||||
self.recent_failures.push_back(entry);
|
||||
if self.recent_failures.len() > LOOP_DETECTION_WINDOW {
|
||||
self.recent_failures.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_loop(&self) -> Option<&LoopDetectionEntry> {
|
||||
use std::collections::HashMap as CountMap;
|
||||
let mut counts: CountMap<
|
||||
(std::mem::Discriminant<AIAgentActionResultType>, u64),
|
||||
(usize, usize),
|
||||
> = CountMap::new();
|
||||
for (idx, entry) in self.recent_failures.iter().enumerate() {
|
||||
let key = (entry.tool_discriminant, entry.input_hash);
|
||||
let counter = counts.entry(key).or_insert((0, 0));
|
||||
counter.0 += 1;
|
||||
counter.1 = idx; // Track most recent occurrence
|
||||
}
|
||||
for ((_disc, _hash), (count, latest_idx)) in &counts {
|
||||
if *count >= LOOP_DETECTION_THRESHOLD {
|
||||
return self.recent_failures.get(*latest_idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.recent_failures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RequestInput {
|
||||
pub conversation_id: AIConversationId,
|
||||
@@ -317,6 +371,9 @@ pub struct BlocklistAIController {
|
||||
pending_auto_resume_handles: HashMap<AIConversationId, SpawnedFutureHandle>,
|
||||
/// Passive conversations explicitly requested to follow up after actions complete.
|
||||
pending_passive_follow_ups: HashSet<AIConversationId>,
|
||||
|
||||
/// Per-conversation loop detection state for preventing recursive tool failures.
|
||||
loop_detection: HashMap<AIConversationId, LoopDetectionState>,
|
||||
/// Passive suggestion results that should be included with the next request
|
||||
/// for a given conversation (e.g. accepted/iterated code diffs that weren't
|
||||
/// auto-resumed).
|
||||
@@ -555,6 +612,7 @@ impl BlocklistAIController {
|
||||
pending_auto_resume_handles: HashMap::new(),
|
||||
pending_passive_follow_ups: HashSet::new(),
|
||||
pending_passive_suggestion_results: HashMap::new(),
|
||||
loop_detection: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,6 +1084,9 @@ impl BlocklistAIController {
|
||||
is_queued_prompt: bool,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// User sending a new query resets loop detection — fresh context.
|
||||
self.loop_detection.remove(&conversation_id);
|
||||
|
||||
let is_viewer = self
|
||||
.terminal_model
|
||||
.lock()
|
||||
@@ -1418,6 +1479,9 @@ impl BlocklistAIController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop detection: record failures and check for repeated patterns
|
||||
let loop_warning = self.check_and_record_loop_detection(conversation_id, &finished_results);
|
||||
|
||||
// Check whether any result will trigger a server-side subagent (e.g. CLI
|
||||
// subagent for LRC), or if one is already active. If so, we must not
|
||||
// piggyback orchestration events because the subagent cannot interpret
|
||||
@@ -1447,6 +1511,34 @@ impl BlocklistAIController {
|
||||
ctx,
|
||||
);
|
||||
|
||||
// If a loop was detected, inject a corrective instruction alongside
|
||||
// the action results so the model avoids repeating the same failure.
|
||||
if let Some(warning_msg) = loop_warning {
|
||||
log::warn!(
|
||||
"[loop-detection] Injecting corrective instruction for conversation {:?}: {}",
|
||||
conversation_id,
|
||||
warning_msg
|
||||
);
|
||||
if let Some(conversation) =
|
||||
BlocklistAIHistoryModel::as_ref(ctx).conversation(&conversation_id)
|
||||
{
|
||||
let root_task_id = conversation.get_root_task_id().clone();
|
||||
request_input
|
||||
.input_messages
|
||||
.entry(root_task_id)
|
||||
.or_default()
|
||||
.push(AIAgentInput::UserQuery {
|
||||
query: warning_msg,
|
||||
context: Arc::from([]),
|
||||
static_query_type: None,
|
||||
referenced_attachments: HashMap::new(),
|
||||
user_query_mode: UserQueryMode::Normal,
|
||||
running_command: None,
|
||||
intended_agent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Include any pending orchestration events in this follow-up rather
|
||||
// than waiting for a separate idle injection turn. Skip when a server
|
||||
// subagent is or will be active — events will be delivered via the idle
|
||||
@@ -1495,6 +1587,67 @@ impl BlocklistAIController {
|
||||
self.pending_passive_follow_ups.remove(&conversation_id);
|
||||
}
|
||||
|
||||
/// Records failed actions into the loop detection state and returns a
|
||||
/// corrective instruction if a loop is detected.
|
||||
fn check_and_record_loop_detection(
|
||||
&mut self,
|
||||
conversation_id: AIConversationId,
|
||||
results: &[AIAgentActionResult],
|
||||
) -> Option<String> {
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let state = self.loop_detection.entry(conversation_id).or_default();
|
||||
let mut has_success = false;
|
||||
|
||||
for result in results {
|
||||
if result.result.is_failed() {
|
||||
let discriminant = std::mem::discriminant(&result.result);
|
||||
// Use a stable description that includes the tool type and the *input*
|
||||
// (command, file paths, etc.) but NOT the variable output, so the same
|
||||
// failing command with different output is still recognized as a loop.
|
||||
let description = result.result.loop_description();
|
||||
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
discriminant.hash(&mut hasher);
|
||||
description.hash(&mut hasher);
|
||||
let input_hash = hasher.finish();
|
||||
|
||||
state.record_failure(LoopDetectionEntry {
|
||||
tool_discriminant: discriminant,
|
||||
input_hash,
|
||||
description: description.clone(),
|
||||
});
|
||||
} else if result.result.is_successful() {
|
||||
has_success = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we had at least one success in this batch, clear loop state —
|
||||
// the agent is making progress.
|
||||
if has_success {
|
||||
state.clear();
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check for loops
|
||||
if let Some(looping_entry) = state.detect_loop() {
|
||||
let warning = format!(
|
||||
"[SYSTEM] Loop detected: the same action has failed {} or more times consecutively. \
|
||||
Do NOT repeat this action or any similar approach.\n\n\
|
||||
Failing action: {}\n\n\
|
||||
Take a completely different approach to accomplish the goal. \
|
||||
If you cannot find an alternative, explain to the user what is failing and why.",
|
||||
LOOP_DETECTION_THRESHOLD,
|
||||
looping_entry.description
|
||||
);
|
||||
// Clear the state so we don't keep injecting on every subsequent turn
|
||||
state.clear();
|
||||
Some(warning)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the EventsReady signal. Checks readiness, drains
|
||||
/// pending events from the service, and injects them into the conversation.
|
||||
fn handle_pending_events_ready(
|
||||
@@ -1566,9 +1719,8 @@ impl BlocklistAIController {
|
||||
conversation_id: AIConversationId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let events = OrchestrationEventService::handle(ctx).update(ctx, |svc, _ctx| {
|
||||
svc.drain_subagent_events(&conversation_id)
|
||||
});
|
||||
let events = OrchestrationEventService::handle(ctx)
|
||||
.update(ctx, |svc, _ctx| svc.drain_subagent_events(&conversation_id));
|
||||
|
||||
for event in events {
|
||||
match event.detail {
|
||||
@@ -1577,17 +1729,16 @@ impl BlocklistAIController {
|
||||
question_text,
|
||||
options,
|
||||
} => {
|
||||
let answer = options.first().cloned().unwrap_or_else(|| {
|
||||
format!("Proceed with: {}", question_text)
|
||||
});
|
||||
let answer = options
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("Proceed with: {}", question_text));
|
||||
|
||||
OrchestrationEventService::handle(ctx).update(ctx, |svc, ctx| {
|
||||
svc.route_answer_to_subagent(source_conversation_id, answer, ctx);
|
||||
});
|
||||
}
|
||||
PendingEventDetail::SubagentAnswer {
|
||||
answer_text,
|
||||
} => {
|
||||
PendingEventDetail::SubagentAnswer { answer_text } => {
|
||||
self.complete_ask_user_question_with_answer(answer_text, ctx);
|
||||
}
|
||||
PendingEventDetail::SubagentCompletionSummary => {}
|
||||
@@ -1604,7 +1755,10 @@ impl BlocklistAIController {
|
||||
) {
|
||||
use ai::agent::action_result::AskUserQuestionAnswerItem;
|
||||
|
||||
let executor = self.action_model.as_ref(ctx).ask_user_question_executor(ctx);
|
||||
let executor = self
|
||||
.action_model
|
||||
.as_ref(ctx)
|
||||
.ask_user_question_executor(ctx);
|
||||
let answer_item = AskUserQuestionAnswerItem::Answered {
|
||||
question_id: String::new(),
|
||||
selected_options: vec![answer_text.clone()],
|
||||
@@ -1963,7 +2117,8 @@ impl BlocklistAIController {
|
||||
parent_agent_id,
|
||||
agent_name,
|
||||
bedrock_history,
|
||||
bedrock_compact_summary,
|
||||
bedrock_tool_result_archive,
|
||||
bedrock_progressive_summary,
|
||||
) = {
|
||||
let Some(conversation) = history_model
|
||||
.as_ref(ctx)
|
||||
@@ -1987,7 +2142,8 @@ impl BlocklistAIController {
|
||||
conversation.parent_agent_id().map(str::to_string),
|
||||
conversation.agent_name().map(str::to_string),
|
||||
conversation.bedrock_message_history().to_vec(),
|
||||
conversation.compact_summary().map(str::to_string),
|
||||
conversation.tool_result_archive().to_vec(),
|
||||
conversation.progressive_summary().map(str::to_string),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -2055,11 +2211,8 @@ impl BlocklistAIController {
|
||||
request_params.parent_agent_id = parent_agent_id;
|
||||
request_params.agent_name = agent_name;
|
||||
request_params.bedrock_message_history = bedrock_history;
|
||||
request_params.bedrock_compact_summary = bedrock_compact_summary;
|
||||
request_params.is_summarization = request_input
|
||||
.all_inputs()
|
||||
.any(|input| matches!(input, AIAgentInput::SummarizeConversation { .. }));
|
||||
|
||||
request_params.bedrock_tool_result_archive = bedrock_tool_result_archive;
|
||||
request_params.bedrock_progressive_summary = bedrock_progressive_summary;
|
||||
let server_conversation_token_for_identifiers =
|
||||
conversation_data.server_conversation_token.clone();
|
||||
|
||||
@@ -2084,13 +2237,9 @@ impl BlocklistAIController {
|
||||
let input_contains_user_query = request_input
|
||||
.all_inputs()
|
||||
.any(|input| input.is_user_query());
|
||||
let input_is_summarization = request_input
|
||||
.all_inputs()
|
||||
.any(|input| matches!(input, AIAgentInput::SummarizeConversation { .. }));
|
||||
ctx.subscribe_to_model(&response_stream, move |me, event, ctx| {
|
||||
me.handle_response_stream_event(
|
||||
input_contains_user_query,
|
||||
input_is_summarization,
|
||||
event,
|
||||
&response_stream_clone,
|
||||
ctx,
|
||||
@@ -2280,7 +2429,6 @@ impl BlocklistAIController {
|
||||
fn handle_response_stream_event(
|
||||
&mut self,
|
||||
did_input_contain_user_query: bool,
|
||||
is_summarization_request: bool,
|
||||
event: &ResponseStreamEvent,
|
||||
response_stream: &ModelHandle<ResponseStream>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
@@ -2384,69 +2532,32 @@ impl BlocklistAIController {
|
||||
Some(sent.clone())
|
||||
}
|
||||
});
|
||||
if let Some(new_history) = new_history {
|
||||
if let Some(mut new_history) = new_history {
|
||||
let history_model = BlocklistAIHistoryModel::handle(ctx);
|
||||
history_model.update(ctx, |history_model, _| {
|
||||
if let Some(conversation) = history_model.conversation_mut(&conversation_id) {
|
||||
// If this was a summarization request, compact the
|
||||
// history to just the summary instead of keeping
|
||||
// the full message list. This is what actually
|
||||
// frees up context window space.
|
||||
let is_summarization = is_summarization_request;
|
||||
|
||||
if is_summarization {
|
||||
// Extract the assistant's summary from the last
|
||||
// message in the history (the response).
|
||||
let summary_text = new_history
|
||||
if let Some(conversation) =
|
||||
history_model.conversation_mut(&conversation_id)
|
||||
{
|
||||
let skip = conversation.messages_summarized_up_to();
|
||||
if skip > 0 && skip <= new_history.len() {
|
||||
let drained: Vec<_> = new_history
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|msg| {
|
||||
use crate::ai::bedrock::convert::{
|
||||
MessageContent, MessageRole,
|
||||
};
|
||||
if msg.role == MessageRole::Assistant {
|
||||
if let MessageContent::Text(text) =
|
||||
&msg.content
|
||||
{
|
||||
Some(text.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(summary) = summary_text {
|
||||
log::info!(
|
||||
"[bedrock] Compacted conversation history from {} messages to system-level summary",
|
||||
new_history.len()
|
||||
);
|
||||
conversation.set_compact_summary(Some(summary.clone()));
|
||||
*conversation.bedrock_message_history_mut() = Vec::new();
|
||||
|
||||
let estimated_tokens = (summary.len() / 4) as u32;
|
||||
let max_context = crate::ai::bedrock::response_translator::context_window_for_model("claude-opus-4-6-20250514[1m]");
|
||||
let new_usage = estimated_tokens as f32 / max_context as f32;
|
||||
conversation.set_context_window_usage(new_usage);
|
||||
conversation.set_current_context_tokens(estimated_tokens);
|
||||
log::info!(
|
||||
"[bedrock] Post-compact context estimate: ~{} tokens ({:.1}% of context window)",
|
||||
estimated_tokens,
|
||||
new_usage * 100.0
|
||||
);
|
||||
} else {
|
||||
*conversation.bedrock_message_history_mut() =
|
||||
new_history;
|
||||
}
|
||||
.take(skip)
|
||||
.cloned()
|
||||
.collect();
|
||||
conversation.archive_tool_results(drained);
|
||||
let reconciled = new_history.split_off(skip);
|
||||
conversation.reset_messages_summarized_up_to();
|
||||
*conversation.bedrock_message_history_mut() =
|
||||
reconciled;
|
||||
} else {
|
||||
*conversation.bedrock_message_history_mut() =
|
||||
new_history;
|
||||
log::info!(
|
||||
"[bedrock] Updated conversation bedrock history: {} messages",
|
||||
conversation.bedrock_message_history().len()
|
||||
);
|
||||
}
|
||||
log::info!(
|
||||
"[bedrock] Updated conversation bedrock history: {} messages",
|
||||
conversation.bedrock_message_history().len()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2488,22 +2599,24 @@ impl BlocklistAIController {
|
||||
});
|
||||
}
|
||||
|
||||
let mut renderable_error: RenderableAIError =
|
||||
if let AIApiError::Stream { stream_type, source } = e.as_ref() {
|
||||
if *stream_type == "bedrock_converse"
|
||||
&& is_bedrock_credentials_error(&source.to_string())
|
||||
{
|
||||
let model_name =
|
||||
response_stream.as_ref(ctx).model_id().to_string();
|
||||
RenderableAIError::AwsBedrockCredentialsExpiredOrInvalid {
|
||||
model_name,
|
||||
}
|
||||
} else {
|
||||
e.as_ref().into()
|
||||
let mut renderable_error: RenderableAIError = if let AIApiError::Stream {
|
||||
stream_type,
|
||||
source,
|
||||
} = e.as_ref()
|
||||
{
|
||||
if *stream_type == "bedrock_converse"
|
||||
&& is_bedrock_credentials_error(&source.to_string())
|
||||
{
|
||||
let model_name = response_stream.as_ref(ctx).model_id().to_string();
|
||||
RenderableAIError::AwsBedrockCredentialsExpiredOrInvalid {
|
||||
model_name,
|
||||
}
|
||||
} else {
|
||||
e.as_ref().into()
|
||||
};
|
||||
}
|
||||
} else {
|
||||
e.as_ref().into()
|
||||
};
|
||||
if let RenderableAIError::Other {
|
||||
will_attempt_resume,
|
||||
waiting_for_network,
|
||||
@@ -2555,7 +2668,9 @@ impl BlocklistAIController {
|
||||
log::warn!("Conversation not found.");
|
||||
return;
|
||||
};
|
||||
let new_exchange_ids: Vec<_> = conversation.new_exchange_ids_for_response(&stream_id).collect();
|
||||
let new_exchange_ids: Vec<_> = conversation
|
||||
.new_exchange_ids_for_response(&stream_id)
|
||||
.collect();
|
||||
log::info!(
|
||||
"[bedrock-debug] AfterStreamFinished: stream_id={:?}, conversation_id={:?}, new_exchange_ids count={}",
|
||||
stream_id, conversation_id, new_exchange_ids.len()
|
||||
@@ -2963,42 +3078,311 @@ impl BlocklistAIController {
|
||||
ctx.emit(BlocklistAIControllerEvent::FreeTierLimitCheckTriggered);
|
||||
}
|
||||
|
||||
// Auto-compact: trigger summarization when context window usage >= 85%.
|
||||
let should_auto_compact = {
|
||||
// Progressive summarization: when context window usage >= 85% and we have
|
||||
// more than 100 messages, summarize the oldest messages while keeping the
|
||||
// most recent 100 verbatim. This runs as a background Bedrock call — no UI,
|
||||
// no exchange created, no tool execution shown.
|
||||
let should_progressive_summarize = {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(ctx);
|
||||
history_model
|
||||
.conversation(&conversation_id)
|
||||
.is_some_and(|conversation| {
|
||||
let is_summarization_request = conversation
|
||||
.latest_exchange()
|
||||
.is_some_and(|exchange| {
|
||||
let is_summarization_request =
|
||||
conversation.latest_exchange().is_some_and(|exchange| {
|
||||
exchange
|
||||
.input
|
||||
.iter()
|
||||
.any(|i| matches!(i, AIAgentInput::SummarizeConversation { .. }))
|
||||
});
|
||||
conversation.context_window_usage() >= 0.85
|
||||
&& !conversation.has_pending_auto_compact()
|
||||
&& !conversation.has_pending_progressive_summary()
|
||||
&& !is_summarization_request
|
||||
&& conversation.bedrock_message_history().len() > 100
|
||||
})
|
||||
};
|
||||
|
||||
if should_auto_compact {
|
||||
log::info!(
|
||||
"[auto-compact] Context window usage >= 85% for conversation {:?}, triggering summarization",
|
||||
conversation_id
|
||||
);
|
||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, _| {
|
||||
if let Some(conversation) = history_model.conversation_mut(&conversation_id) {
|
||||
conversation.set_has_pending_auto_compact(true);
|
||||
}
|
||||
});
|
||||
self.send_slash_command_request(
|
||||
SlashCommandRequest::Summarize { prompt: None },
|
||||
ctx,
|
||||
);
|
||||
if should_progressive_summarize {
|
||||
self.trigger_progressive_summarization(conversation_id, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_progressive_summarization(
|
||||
&mut self,
|
||||
conversation_id: AIConversationId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
use crate::ai::bedrock::client::{BedrockClient, BedrockClientConfig};
|
||||
use crate::ai::bedrock::convert::{ConversationMessage, MessageContent, MessageRole};
|
||||
use crate::ai::bedrock::response_translator::{
|
||||
context_window_for_model, estimate_cost_cents,
|
||||
};
|
||||
use crate::settings::ai::AISettings;
|
||||
use settings::Setting;
|
||||
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
if !*settings.bedrock_enabled.value() {
|
||||
return;
|
||||
}
|
||||
|
||||
let config = BedrockClientConfig {
|
||||
auth_method: *settings.bedrock_auth_method.value(),
|
||||
profile: settings.bedrock_profile.value().clone(),
|
||||
region: settings.bedrock_region.value().clone(),
|
||||
access_key_id: settings.bedrock_access_key_id.value().clone(),
|
||||
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
||||
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
||||
}
|
||||
.with_external_fallbacks();
|
||||
|
||||
let cross_region = config.cross_region_inference;
|
||||
// Use Sonnet for summarization — cheaper and fast enough for this task
|
||||
let model_id = "us.anthropic.claude-sonnet-4-6-20250514-v1:0".to_string();
|
||||
|
||||
let history_model = BlocklistAIHistoryModel::handle(ctx);
|
||||
|
||||
// Extract the messages to summarize and set the guard flag
|
||||
let (messages_to_summarize, existing_summary, messages_count) = {
|
||||
let history = history_model.as_ref(ctx);
|
||||
let Some(conversation) = history.conversation(&conversation_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let history_len = conversation.bedrock_message_history().len();
|
||||
let split_point = history_len.saturating_sub(100);
|
||||
if split_point == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let msgs: Vec<ConversationMessage> =
|
||||
conversation.bedrock_message_history()[..split_point].to_vec();
|
||||
let existing = conversation.progressive_summary().map(str::to_string);
|
||||
|
||||
(msgs, existing, split_point)
|
||||
};
|
||||
|
||||
history_model.update(ctx, |history_model, _| {
|
||||
if let Some(conversation) = history_model.conversation_mut(&conversation_id) {
|
||||
conversation.set_has_pending_progressive_summary(true);
|
||||
}
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"[progressive-summary] Triggering for conversation {:?}: summarizing {} messages, keeping last 100",
|
||||
conversation_id,
|
||||
messages_count
|
||||
);
|
||||
|
||||
// Build the summarization input
|
||||
let mut summarize_content = String::new();
|
||||
if let Some(ref prior) = existing_summary {
|
||||
summarize_content.push_str("<prior-summary>\n");
|
||||
summarize_content.push_str(prior);
|
||||
summarize_content.push_str("\n</prior-summary>\n\n");
|
||||
}
|
||||
summarize_content.push_str("<messages-to-summarize>\n");
|
||||
fn safe_truncate(s: &str, max_chars: usize) -> String {
|
||||
if s.len() <= max_chars {
|
||||
s.to_string()
|
||||
} else {
|
||||
let trunc = s.chars().take(max_chars).collect::<String>();
|
||||
format!("{trunc}... [truncated, {len} total chars]", len = s.len())
|
||||
}
|
||||
}
|
||||
|
||||
for msg in &messages_to_summarize {
|
||||
let role_str = match msg.role {
|
||||
MessageRole::User => "User",
|
||||
MessageRole::Assistant => "Assistant",
|
||||
};
|
||||
let content_str = match &msg.content {
|
||||
MessageContent::Text(t) => t.clone(),
|
||||
MessageContent::ToolUse { name, input, .. } => {
|
||||
format!("[Tool Call: {}] {}", name, input)
|
||||
}
|
||||
MessageContent::ToolResult { content, .. } => safe_truncate(content, 2000),
|
||||
MessageContent::MultiPart(parts) => {
|
||||
use crate::ai::bedrock::convert::ContentPart;
|
||||
parts
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
ContentPart::Text(t) => t.clone(),
|
||||
ContentPart::ToolUse { name, input, .. } => {
|
||||
format!("[Tool: {}] {}", name, input)
|
||||
}
|
||||
ContentPart::ToolResult { content, .. } => safe_truncate(content, 2000),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
};
|
||||
summarize_content.push_str(&format!("[{}]: {}\n", role_str, content_str));
|
||||
}
|
||||
summarize_content.push_str("</messages-to-summarize>");
|
||||
|
||||
let summarize_prompt = "Summarize the following conversation history. Preserve:\n\
|
||||
- All decisions made and their rationale\n\
|
||||
- All file paths modified and what was changed\n\
|
||||
- All tool calls with their significant results (commands run, files read, errors encountered)\n\
|
||||
- Current task state and any pending work\n\
|
||||
- Technical details, code patterns, and architecture discussed\n\n\
|
||||
Be comprehensive. This summary will be the only record of these exchanges.";
|
||||
|
||||
let summarize_messages = vec![ConversationMessage {
|
||||
role: MessageRole::User,
|
||||
content: MessageContent::Text(format!("{}\n\n{}", summarize_prompt, summarize_content)),
|
||||
}];
|
||||
|
||||
// Spawn the background Bedrock call
|
||||
let model_id_clone = model_id.clone();
|
||||
ctx.spawn(
|
||||
async move {
|
||||
let client = BedrockClient::from_config(config).await?;
|
||||
client
|
||||
.converse_collect(
|
||||
&model_id_clone,
|
||||
summarize_messages,
|
||||
None,
|
||||
16000,
|
||||
cross_region,
|
||||
)
|
||||
.await
|
||||
},
|
||||
move |me, result, ctx| {
|
||||
let history_model = BlocklistAIHistoryModel::handle(ctx);
|
||||
match result {
|
||||
Ok((summary_text, input_tokens, output_tokens)) => {
|
||||
log::info!(
|
||||
"[progressive-summary] Completed for {:?}: {} chars, input={} output={} tokens",
|
||||
conversation_id,
|
||||
summary_text.len(),
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
);
|
||||
|
||||
let cost_cents = estimate_cost_cents(
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
0,
|
||||
0,
|
||||
&model_id,
|
||||
);
|
||||
|
||||
// Use the conversation's active model for context window sizing,
|
||||
// not the summarizer model.
|
||||
let active_model_id = crate::ai::llms::LLMPreferences::as_ref(ctx)
|
||||
.get_active_base_model(ctx, Some(me.terminal_view_id))
|
||||
.id
|
||||
.to_string();
|
||||
|
||||
history_model.update(ctx, |history_model, _| {
|
||||
if let Some(conversation) =
|
||||
history_model.conversation_mut(&conversation_id)
|
||||
{
|
||||
// Drain the summarized messages from history
|
||||
let drain_count =
|
||||
messages_count.min(conversation.bedrock_message_history().len());
|
||||
let drained: Vec<_> = conversation
|
||||
.bedrock_message_history()
|
||||
.iter()
|
||||
.take(drain_count)
|
||||
.cloned()
|
||||
.collect();
|
||||
conversation.archive_tool_results(drained);
|
||||
conversation
|
||||
.bedrock_message_history_mut()
|
||||
.drain(0..drain_count);
|
||||
|
||||
conversation
|
||||
.set_progressive_summary(Some(summary_text.clone()), drain_count);
|
||||
conversation.set_has_pending_progressive_summary(false);
|
||||
|
||||
// Estimate new context window usage
|
||||
let summary_tokens = (summary_text.len() / 4) as u32;
|
||||
let remaining_msgs_tokens: u32 = conversation
|
||||
.bedrock_message_history()
|
||||
.iter()
|
||||
.map(|m| match &m.content {
|
||||
MessageContent::Text(t) => (t.len() / 4) as u32,
|
||||
MessageContent::ToolUse { input, .. } => {
|
||||
(input.to_string().len() / 4) as u32 + 20
|
||||
}
|
||||
MessageContent::ToolResult { content, .. } => {
|
||||
(content.len() / 4) as u32
|
||||
}
|
||||
MessageContent::MultiPart(parts) => {
|
||||
use crate::ai::bedrock::convert::ContentPart;
|
||||
parts
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
ContentPart::Text(t) => (t.len() / 4) as u32,
|
||||
ContentPart::ToolUse { input, .. } => {
|
||||
(input.to_string().len() / 4) as u32
|
||||
}
|
||||
ContentPart::ToolResult { content, .. } => {
|
||||
(content.len() / 4) as u32
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
let max_ctx = context_window_for_model(&active_model_id);
|
||||
let new_usage =
|
||||
(summary_tokens + remaining_msgs_tokens) as f32 / max_ctx as f32;
|
||||
conversation.set_context_window_usage(new_usage);
|
||||
conversation
|
||||
.set_current_context_tokens(summary_tokens + remaining_msgs_tokens);
|
||||
|
||||
log::info!(
|
||||
"[progressive-summary] Post-summary: ~{} tokens ({:.1}% of {} context), {} messages retained",
|
||||
summary_tokens + remaining_msgs_tokens,
|
||||
new_usage * 100.0,
|
||||
active_model_id,
|
||||
conversation.bedrock_message_history().len()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Update cost tracking
|
||||
history_model.update(ctx, |history_model, _| {
|
||||
use warp_multi_agent_api::response_event::stream_finished;
|
||||
let token_usage = vec![stream_finished::TokenUsage {
|
||||
model_id: "bedrock".to_string(),
|
||||
total_input: input_tokens,
|
||||
output: output_tokens,
|
||||
input_cache_read: 0,
|
||||
input_cache_write: 0,
|
||||
cost_in_cents: cost_cents,
|
||||
}];
|
||||
history_model.update_conversation_cost_and_usage_for_request(
|
||||
conversation_id,
|
||||
None,
|
||||
token_usage,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"[progressive-summary] Failed for {:?}: {:?}",
|
||||
conversation_id,
|
||||
e
|
||||
);
|
||||
history_model.update(ctx, |history_model, _| {
|
||||
if let Some(conversation) =
|
||||
history_model.conversation_mut(&conversation_id)
|
||||
{
|
||||
conversation.set_has_pending_progressive_summary(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let _ = me;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for BlocklistAIController {
|
||||
@@ -3028,7 +3412,9 @@ fn is_bedrock_credentials_error(msg: &str) -> bool {
|
||||
|| (lower.contains("sso/cache") && lower.contains("notfound"))
|
||||
|| (lower.contains("sso/cache") && lower.contains("no such file"))
|
||||
|| (lower.contains("accessdenied")
|
||||
&& (lower.contains("token") || lower.contains("credential") || lower.contains("security")))
|
||||
&& (lower.contains("token")
|
||||
|| lower.contains("credential")
|
||||
|| lower.contains("security")))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -92,14 +92,17 @@ impl ResponseStream {
|
||||
return None;
|
||||
}
|
||||
let auth_method = *settings.bedrock_auth_method.value();
|
||||
Some(BedrockClientConfig {
|
||||
auth_method,
|
||||
profile: settings.bedrock_profile.value().clone(),
|
||||
region: settings.bedrock_region.value().clone(),
|
||||
access_key_id: settings.bedrock_access_key_id.value().clone(),
|
||||
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
||||
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
||||
}.with_external_fallbacks())
|
||||
Some(
|
||||
BedrockClientConfig {
|
||||
auth_method,
|
||||
profile: settings.bedrock_profile.value().clone(),
|
||||
region: settings.bedrock_region.value().clone(),
|
||||
access_key_id: settings.bedrock_access_key_id.value().clone(),
|
||||
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
||||
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
||||
}
|
||||
.with_external_fallbacks(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
@@ -190,14 +193,15 @@ impl ResponseStream {
|
||||
self.current_request_id = Some(request_id);
|
||||
let params = self.params.clone();
|
||||
let bedrock_config = Self::bedrock_config_if_applicable(params.model.as_str(), ctx);
|
||||
let _ = ctx.spawn(
|
||||
async move {
|
||||
generate_multi_agent_output(bedrock_config, params, cancellation_rx).await
|
||||
},
|
||||
move |me, stream, ctx| {
|
||||
me.handle_response_stream_result(request_id, stream, ctx);
|
||||
},
|
||||
);
|
||||
let _ =
|
||||
ctx.spawn(
|
||||
async move {
|
||||
generate_multi_agent_output(bedrock_config, params, cancellation_rx).await
|
||||
},
|
||||
move |me, stream, ctx| {
|
||||
me.handle_response_stream_result(request_id, stream, ctx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Cancels the stream. The conversation_id is preserved in the emitted event for async handling.
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::{
|
||||
},
|
||||
blocklist::agent_view::AgentViewEntryOrigin,
|
||||
},
|
||||
search::slash_command_menu::static_commands::commands,
|
||||
terminal::input::slash_commands::SlashCommandTrigger,
|
||||
BlocklistAIHistoryModel,
|
||||
};
|
||||
@@ -33,9 +32,6 @@ pub enum SlashCommandRequest {
|
||||
repos: Vec<String>,
|
||||
use_current_dir: bool,
|
||||
},
|
||||
Summarize {
|
||||
prompt: Option<String>,
|
||||
},
|
||||
FetchReviewComments {
|
||||
repo_path: String,
|
||||
},
|
||||
@@ -55,13 +51,6 @@ impl SlashCommandRequest {
|
||||
return Some(Self::InitProjectRules);
|
||||
}
|
||||
|
||||
// Check if query starts with /compact and route to summarize conversation
|
||||
if let Some(prompt) = query.strip_prefix(commands::COMPACT.name) {
|
||||
return Some(Self::Summarize {
|
||||
prompt: prompt.strip_prefix(' ').map(String::from),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -85,7 +74,6 @@ impl SlashCommandRequest {
|
||||
ctx,
|
||||
);
|
||||
let entrypoint = self.entrypoint();
|
||||
let is_summarize = matches!(self, Self::Summarize { .. });
|
||||
let inputs = self.input(context, controller.context_model.as_ref(ctx), ctx);
|
||||
if inputs.is_empty() {
|
||||
return;
|
||||
@@ -155,14 +143,12 @@ impl SlashCommandRequest {
|
||||
});
|
||||
}
|
||||
// Emit SentRequest event to trigger buffer clearing
|
||||
if is_summarize {
|
||||
ctx.emit(BlocklistAIControllerEvent::SentRequest {
|
||||
contains_user_query: true,
|
||||
is_queued_prompt,
|
||||
model_id,
|
||||
stream_id,
|
||||
});
|
||||
}
|
||||
ctx.emit(BlocklistAIControllerEvent::SentRequest {
|
||||
contains_user_query: true,
|
||||
is_queued_prompt,
|
||||
model_id,
|
||||
stream_id,
|
||||
});
|
||||
}
|
||||
Err(e) => log::error!("Failed to send agent slash command request: {e:?}"),
|
||||
}
|
||||
@@ -174,8 +160,7 @@ impl SlashCommandRequest {
|
||||
app: &AppContext,
|
||||
) -> Option<AIConversationId> {
|
||||
match self {
|
||||
Self::Summarize { .. }
|
||||
| Self::CreateEnvironment { .. }
|
||||
Self::CreateEnvironment { .. }
|
||||
| Self::InvokeSkill { .. }
|
||||
| Self::FetchReviewComments { .. } => controller
|
||||
.context_model
|
||||
@@ -226,9 +211,6 @@ impl SlashCommandRequest {
|
||||
repo_paths: repos,
|
||||
}]
|
||||
}
|
||||
SlashCommandRequest::Summarize { prompt, .. } => {
|
||||
vec![AIAgentInput::SummarizeConversation { prompt }]
|
||||
}
|
||||
SlashCommandRequest::FetchReviewComments { repo_path } => {
|
||||
vec![AIAgentInput::FetchReviewComments { repo_path, context }]
|
||||
}
|
||||
@@ -263,7 +245,6 @@ impl SlashCommandRequest {
|
||||
SlashCommandRequest::InitProjectRules => EntrypointType::InitProjectRules,
|
||||
SlashCommandRequest::CreateNewProject { .. }
|
||||
| SlashCommandRequest::CreateEnvironment { .. }
|
||||
| SlashCommandRequest::Summarize { .. }
|
||||
| SlashCommandRequest::FetchReviewComments { .. }
|
||||
| SlashCommandRequest::InvokeSkill { .. } => EntrypointType::UserInitiated,
|
||||
}
|
||||
|
||||
@@ -1095,6 +1095,8 @@ impl BlocklistAIHistoryModel {
|
||||
// The event cursor belongs to the source conversation's run; the
|
||||
// forked conversation will establish its own cursor.
|
||||
last_event_sequence: None,
|
||||
progressive_summary: None,
|
||||
messages_summarized_up_to: 0,
|
||||
};
|
||||
let forked_conversation_id = AIConversationId::new();
|
||||
if let Err(e) = sqlite_sender.send(ModelEvent::UpdateMultiAgentConversation {
|
||||
@@ -1250,6 +1252,8 @@ impl BlocklistAIHistoryModel {
|
||||
// The event cursor belongs to the source conversation's run; the
|
||||
// forked conversation will establish its own cursor.
|
||||
last_event_sequence: None,
|
||||
progressive_summary: None,
|
||||
messages_summarized_up_to: 0,
|
||||
};
|
||||
|
||||
let forked_conversation_id = AIConversationId::new();
|
||||
|
||||
@@ -328,6 +328,10 @@ fn create_server_metadata(
|
||||
credits_spent_for_last_block: None,
|
||||
token_usage: vec![],
|
||||
tool_usage_metadata: Default::default(),
|
||||
total_cache_read_tokens: 0,
|
||||
total_cache_write_tokens: 0,
|
||||
total_cache_miss_tokens: 0,
|
||||
total_cost_cents: 0.0,
|
||||
};
|
||||
|
||||
ServerAIConversationMetadata {
|
||||
@@ -1133,6 +1137,8 @@ fn test_find_by_token_after_insert_forked_conversation_from_tasks() {
|
||||
run_id: None,
|
||||
autoexecute_override: None,
|
||||
last_event_sequence: None,
|
||||
progressive_summary: None,
|
||||
messages_summarized_up_to: 0,
|
||||
};
|
||||
let tasks = vec![warp_multi_agent_api::Task {
|
||||
id: "root-task".to_string(),
|
||||
|
||||
@@ -11,7 +11,7 @@ pub(crate) mod requested_command_attribution;
|
||||
pub(crate) mod requested_script;
|
||||
pub(super) mod search_codebase;
|
||||
pub(crate) mod search_results_common;
|
||||
pub(super) mod summarization;
|
||||
pub(crate) mod suggested_unit_tests;
|
||||
pub(super) mod summarization;
|
||||
pub(super) mod web_fetch;
|
||||
pub(super) mod web_search;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::elements::shimmering_text::{ShimmerConfig, ShimmeringTextElement, ShimmeringTextStateHandle};
|
||||
use galaxyui::elements::shimmering_text::{
|
||||
ShimmerConfig, ShimmeringTextElement, ShimmeringTextStateHandle,
|
||||
};
|
||||
use galaxyui::elements::{
|
||||
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element, Flex,
|
||||
MainAxisAlignment, ParentElement, Radius, Shrinkable, Text,
|
||||
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element, Flex, MainAxisAlignment,
|
||||
ParentElement, Radius, Shrinkable, Text,
|
||||
};
|
||||
use galaxyui::r#async::{SpawnedFutureHandle, Timer};
|
||||
use galaxyui::{AppContext, Entity, SingletonEntity, View, ViewContext};
|
||||
@@ -73,20 +75,14 @@ impl SummarizationView {
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
// Clock loader icon (magenta, matches InProgress convention)
|
||||
let icon_element = galaxyui::elements::Icon::new(
|
||||
Icon::ClockLoader.into(),
|
||||
theme.ansi_fg_magenta(),
|
||||
)
|
||||
.finish();
|
||||
let icon_element =
|
||||
galaxyui::elements::Icon::new(Icon::ClockLoader.into(), theme.ansi_fg_magenta())
|
||||
.finish();
|
||||
let icon_box = ConstrainedBox::new(icon_element)
|
||||
.with_width(icon_size(app))
|
||||
.with_height(icon_size(app))
|
||||
.finish();
|
||||
header_row.add_child(
|
||||
Container::new(icon_box)
|
||||
.with_margin_right(8.)
|
||||
.finish(),
|
||||
);
|
||||
header_row.add_child(Container::new(icon_box).with_margin_right(8.).finish());
|
||||
|
||||
// Shimmering "Summarizing conversation..." text
|
||||
let base_color = theme.disabled_text_color(header_background).into_solid();
|
||||
@@ -133,20 +129,13 @@ impl SummarizationView {
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
// Checkmark-style icon for completed
|
||||
let icon_element = galaxyui::elements::Icon::new(
|
||||
Icon::Check.into(),
|
||||
theme.ansi_fg_green(),
|
||||
)
|
||||
.finish();
|
||||
let icon_element =
|
||||
galaxyui::elements::Icon::new(Icon::Check.into(), theme.ansi_fg_green()).finish();
|
||||
let icon_box = ConstrainedBox::new(icon_element)
|
||||
.with_width(icon_size(app))
|
||||
.with_height(icon_size(app))
|
||||
.finish();
|
||||
header_row.add_child(
|
||||
Container::new(icon_box)
|
||||
.with_margin_right(8.)
|
||||
.finish(),
|
||||
);
|
||||
header_row.add_child(Container::new(icon_box).with_margin_right(8.).finish());
|
||||
|
||||
let elapsed = self.start_time.elapsed();
|
||||
let elapsed_text = format_elapsed(elapsed);
|
||||
|
||||
@@ -145,6 +145,8 @@ fn ai_conversation_new_restored_preserves_last_event_sequence() {
|
||||
run_id: None,
|
||||
autoexecute_override: None,
|
||||
last_event_sequence: Some(42),
|
||||
progressive_summary: None,
|
||||
messages_summarized_up_to: 0,
|
||||
};
|
||||
let conversation =
|
||||
AIConversation::new_restored(AIConversationId::new(), vec![task], Some(data))
|
||||
|
||||
@@ -578,9 +578,15 @@ impl OrchestrationEventService {
|
||||
if let Some(parent_id) = parent_id {
|
||||
// Merge child's token usage and costs into parent
|
||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, _ctx| {
|
||||
let child_usage: Option<(HashMap<String, api::response_event::stream_finished::TokenUsage>, crate::ai::agent::RequestCost)> = history_model
|
||||
.conversation(&conversation_id)
|
||||
.map(|c| (c.total_token_usage_by_model().clone(), c.total_request_cost()));
|
||||
let child_usage: Option<(
|
||||
HashMap<String, api::response_event::stream_finished::TokenUsage>,
|
||||
crate::ai::agent::RequestCost,
|
||||
)> = history_model.conversation(&conversation_id).map(|c| {
|
||||
(
|
||||
c.total_token_usage_by_model().clone(),
|
||||
c.total_request_cost(),
|
||||
)
|
||||
});
|
||||
if let Some((token_usage, request_cost)) = child_usage {
|
||||
if let Some(parent) = history_model.conversation_mut(&parent_id) {
|
||||
parent.merge_child_usage_raw(&token_usage, request_cost);
|
||||
@@ -589,11 +595,7 @@ impl OrchestrationEventService {
|
||||
});
|
||||
|
||||
if summary.is_some() {
|
||||
self.route_subagent_completion_summary(
|
||||
conversation_id,
|
||||
parent_id,
|
||||
ctx,
|
||||
);
|
||||
self.route_subagent_completion_summary(conversation_id, parent_id, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1271,9 +1273,7 @@ impl OrchestrationEventService {
|
||||
event_id: Uuid::new_v4().to_string(),
|
||||
source_agent_id: "parent".to_string(),
|
||||
attempt_count: 0,
|
||||
detail: PendingEventDetail::SubagentAnswer {
|
||||
answer_text,
|
||||
},
|
||||
detail: PendingEventDetail::SubagentAnswer { answer_text },
|
||||
};
|
||||
|
||||
self.pending_events
|
||||
|
||||
@@ -317,11 +317,7 @@ impl PassiveSuggestionsModel {
|
||||
.lock()
|
||||
.block_list()
|
||||
.block_at(block_completed.index)
|
||||
.and_then(|block| {
|
||||
block
|
||||
.agent_view_visibility()
|
||||
.agent_view_conversation_id()
|
||||
});
|
||||
.and_then(|block| block.agent_view_visibility().agent_view_conversation_id());
|
||||
|
||||
let prompt = format!(
|
||||
"The command `{}` failed. Diagnose the error and suggest a fix.",
|
||||
@@ -436,6 +432,7 @@ impl PassiveSuggestionsModel {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Entity for PassiveSuggestionsModel {
|
||||
|
||||
@@ -2,9 +2,7 @@ use crate::ai::bedrock::convert::{ContentPart, ConversationMessage, MessageConte
|
||||
use crate::appearance::Appearance;
|
||||
use crate::ui_components::blended_colors;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Container, CornerRadius, CrossAxisAlignment, Flex, ParentElement, Radius, Text,
|
||||
},
|
||||
elements::{Container, CornerRadius, CrossAxisAlignment, Flex, ParentElement, Radius, Text},
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
@@ -60,13 +58,9 @@ impl View for ContextWindowView {
|
||||
format_tokens(estimated_tokens as u32),
|
||||
);
|
||||
column = column.with_child(
|
||||
Text::new(
|
||||
header_text,
|
||||
appearance.ui_font_family(),
|
||||
font_size + 1.0,
|
||||
)
|
||||
.with_color(label_color)
|
||||
.finish(),
|
||||
Text::new(header_text, appearance.ui_font_family(), font_size + 1.0)
|
||||
.with_color(label_color)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// Messages — show full content, no truncation.
|
||||
@@ -89,7 +83,11 @@ impl View for ContextWindowView {
|
||||
// Full content
|
||||
let content_text = match &msg.content {
|
||||
MessageContent::Text(t) => t.clone(),
|
||||
MessageContent::ToolUse { name, tool_use_id, input } => {
|
||||
MessageContent::ToolUse {
|
||||
name,
|
||||
tool_use_id,
|
||||
input,
|
||||
} => {
|
||||
format!(
|
||||
"[ToolUse] name={}, id={}\ninput={}",
|
||||
name, tool_use_id, input
|
||||
@@ -112,13 +110,21 @@ impl View for ContextWindowView {
|
||||
ContentPart::Text(t) => {
|
||||
out.push_str(&format!("[Part {} Text] {}\n", pi, t));
|
||||
}
|
||||
ContentPart::ToolUse { name, tool_use_id, input } => {
|
||||
ContentPart::ToolUse {
|
||||
name,
|
||||
tool_use_id,
|
||||
input,
|
||||
} => {
|
||||
out.push_str(&format!(
|
||||
"[Part {} ToolUse] name={}, id={}, input={}\n",
|
||||
pi, name, tool_use_id, input
|
||||
));
|
||||
}
|
||||
ContentPart::ToolResult { tool_use_id, content, is_error } => {
|
||||
ContentPart::ToolResult {
|
||||
tool_use_id,
|
||||
content,
|
||||
is_error,
|
||||
} => {
|
||||
out.push_str(&format!(
|
||||
"[Part {} ToolResult] id={}, error={}\n{}\n",
|
||||
pi, tool_use_id, is_error, content
|
||||
|
||||
@@ -261,8 +261,8 @@ impl ConversationUsageView {
|
||||
}
|
||||
|
||||
// Cache usage (cumulative session totals)
|
||||
let total_cache = self.usage_info.total_cache_read_tokens
|
||||
+ self.usage_info.total_cache_write_tokens;
|
||||
let total_cache =
|
||||
self.usage_info.total_cache_read_tokens + self.usage_info.total_cache_write_tokens;
|
||||
if total_cache > 0 {
|
||||
if self.usage_info.total_cache_read_tokens > 0 {
|
||||
labels.push(render_label_text("Cache read", appearance));
|
||||
@@ -290,10 +290,7 @@ impl ConversationUsageView {
|
||||
/ total_cache_ops as f32)
|
||||
* 100.0;
|
||||
labels.push(render_label_text("Cache hit rate", appearance));
|
||||
values.push(render_value_text(
|
||||
format!("{:.1}%", hit_rate),
|
||||
appearance,
|
||||
));
|
||||
values.push(render_value_text(format!("{:.1}%", hit_rate), appearance));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user