v1.4.0: Auto-compact streaming, Bedrock summarization support, subagent orchestration, and Galaxy rebrand continuation
Major features: - Auto-compact: triggers conversation summarization when context window >= 85%, compacts Bedrock message history to a summary pair, and tracks live context tokens - Bedrock summarization: plumbs `is_summarization` flag through translator/client/response pipeline, handles SummarizeConversation input type, and marks `summarized` in metadata - Session restore: rebuilds bedrock_message_history from persisted task messages via newly-public `convert_proto_message`, preventing empty history on reconnect - Subagent orchestration: adds SubagentQuestion/Answer/CompletionSummary event types, parent-child question routing with depth limits, retry counting, and drain methods - Summarization UI: inline SummarizationView in AI blocks with progress/finished states Refactors: - Rename WarpTheme → GalaxyTheme across ~100 files (rebrand continuation) - Rename warp_home_config_dir → galaxy_home_config_dir and related path functions - Predefined rules: replace "System Defined Rule #N" with descriptive names (e.g. "Correctness Over Speed", "Never Guess") and add lookup helpers - Usage view: replace cumulative input/output token display with live context tokens, cache hit rate calculation, and separate cache read/write stats - Telemetry: remove verbose doc comments, simplify trait definitions - Facts view: simplify delete permission check (always allow local deletion) - Remove warp_managed_paths_watcher.rs (dead code) 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
eaa2ddc75e
commit
6f54e2cb30
@@ -19,6 +19,8 @@ use warp_multi_agent_api as api;
|
||||
|
||||
const MAX_RETRY_ATTEMPTS: i32 = 3;
|
||||
const MAX_PENDING_LIFECYCLE_EVENTS_PER_TARGET: usize = 200;
|
||||
pub const MAX_SUBAGENT_RETRIES: u8 = 3;
|
||||
const MAX_SUBAGENT_QUESTION_DEPTH: u8 = 3;
|
||||
|
||||
/// Stage associated with a lifecycle error detail.
|
||||
/// This keeps persisted/runtime metadata consistent across API payloads and DB rows.
|
||||
@@ -64,6 +66,23 @@ pub enum PendingEventDetail {
|
||||
Lifecycle {
|
||||
event: api::AgentEvent,
|
||||
},
|
||||
/// A subagent is asking its parent a question (routed from AskUserQuestion).
|
||||
SubagentQuestion {
|
||||
source_conversation_id: AIConversationId,
|
||||
question_text: String,
|
||||
options: Vec<String>,
|
||||
depth: u8,
|
||||
},
|
||||
/// The parent's answer to a subagent's question.
|
||||
SubagentAnswer {
|
||||
target_conversation_id: AIConversationId,
|
||||
answer_text: String,
|
||||
},
|
||||
/// A subagent reporting its completion summary to the parent.
|
||||
SubagentCompletionSummary {
|
||||
source_conversation_id: AIConversationId,
|
||||
summary_text: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A queued event consumed by the controller.
|
||||
@@ -930,29 +949,49 @@ impl OrchestrationEventService {
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut lifecycle_events = Vec::new();
|
||||
for event in &deliverable {
|
||||
let mut server_bound_events = Vec::new();
|
||||
for event in deliverable {
|
||||
match &event.detail {
|
||||
PendingEventDetail::Message {
|
||||
message_id,
|
||||
addresses,
|
||||
subject,
|
||||
message_body,
|
||||
} => messages.push(ReceivedMessageInput {
|
||||
message_id: message_id.clone(),
|
||||
sender_agent_id: event.source_agent_id.clone(),
|
||||
addresses: addresses.clone(),
|
||||
subject: subject.clone(),
|
||||
message_body: message_body.clone(),
|
||||
}),
|
||||
PendingEventDetail::Lifecycle { event } => lifecycle_events.push(event.clone()),
|
||||
} => {
|
||||
messages.push(ReceivedMessageInput {
|
||||
message_id: message_id.clone(),
|
||||
sender_agent_id: event.source_agent_id.clone(),
|
||||
addresses: addresses.clone(),
|
||||
subject: subject.clone(),
|
||||
message_body: message_body.clone(),
|
||||
});
|
||||
server_bound_events.push(event);
|
||||
}
|
||||
PendingEventDetail::Lifecycle { event: _ } => {
|
||||
lifecycle_events.push(
|
||||
if let PendingEventDetail::Lifecycle { event: e } = &event.detail {
|
||||
e.clone()
|
||||
} else {
|
||||
unreachable!()
|
||||
},
|
||||
);
|
||||
server_bound_events.push(event);
|
||||
}
|
||||
// Local-only subagent events are consumed directly by the controller,
|
||||
// not converted to AIAgentInput or awaited for server echo.
|
||||
PendingEventDetail::SubagentQuestion { .. }
|
||||
| PendingEventDetail::SubagentAnswer { .. }
|
||||
| PendingEventDetail::SubagentCompletionSummary { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to awaiting echo for delivery confirmation.
|
||||
self.awaiting_server_echo_events
|
||||
.entry(conversation_id)
|
||||
.or_default()
|
||||
.extend(deliverable);
|
||||
// Only server-bound events need echo confirmation.
|
||||
if !server_bound_events.is_empty() {
|
||||
self.awaiting_server_echo_events
|
||||
.entry(conversation_id)
|
||||
.or_default()
|
||||
.extend(server_bound_events);
|
||||
}
|
||||
|
||||
let mut inputs = Vec::new();
|
||||
if !messages.is_empty() {
|
||||
@@ -966,6 +1005,34 @@ impl OrchestrationEventService {
|
||||
inputs
|
||||
}
|
||||
|
||||
/// Drain only the local subagent events (Question/Answer/Summary) for a conversation.
|
||||
/// These are not sent to the server and are consumed directly by the controller.
|
||||
pub fn drain_subagent_events(
|
||||
&mut self,
|
||||
conversation_id: &AIConversationId,
|
||||
) -> Vec<PendingEvent> {
|
||||
let Some(pending) = self.pending_events.get_mut(conversation_id) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut subagent_events = Vec::new();
|
||||
pending.retain(|event| match &event.detail {
|
||||
PendingEventDetail::SubagentQuestion { .. }
|
||||
| PendingEventDetail::SubagentAnswer { .. }
|
||||
| PendingEventDetail::SubagentCompletionSummary { .. } => {
|
||||
subagent_events.push(event.clone());
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
|
||||
if pending.is_empty() {
|
||||
self.pending_events.remove(conversation_id);
|
||||
}
|
||||
|
||||
subagent_events
|
||||
}
|
||||
|
||||
/// Moves all awaiting events back to pending for retry after a failed
|
||||
/// send attempt. Increments attempt counts and drops events that have
|
||||
/// exhausted their retry limit.
|
||||
@@ -1109,6 +1176,101 @@ impl OrchestrationEventService {
|
||||
self.awaiting_server_echo_events.remove(&conversation_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Route a subagent's AskUserQuestion to the parent conversation for silent auto-answer.
|
||||
pub fn route_subagent_question_to_parent(
|
||||
&mut self,
|
||||
child_conversation_id: AIConversationId,
|
||||
parent_conversation_id: AIConversationId,
|
||||
question_text: String,
|
||||
options: Vec<String>,
|
||||
depth: u8,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if depth >= MAX_SUBAGENT_QUESTION_DEPTH {
|
||||
log::warn!(
|
||||
"Subagent question depth limit reached for conversation {:?}",
|
||||
child_conversation_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let event = PendingEvent {
|
||||
event_id: Uuid::new_v4().to_string(),
|
||||
source_agent_id: child_conversation_id.to_string(),
|
||||
attempt_count: 0,
|
||||
detail: PendingEventDetail::SubagentQuestion {
|
||||
source_conversation_id: child_conversation_id,
|
||||
question_text,
|
||||
options,
|
||||
depth,
|
||||
},
|
||||
};
|
||||
|
||||
self.pending_events
|
||||
.entry(parent_conversation_id)
|
||||
.or_default()
|
||||
.push(event);
|
||||
|
||||
ctx.emit(OrchestrationEventServiceEvent::EventsReady {
|
||||
conversation_id: parent_conversation_id,
|
||||
});
|
||||
}
|
||||
|
||||
/// Route the parent's answer back to the child subagent.
|
||||
pub fn route_answer_to_subagent(
|
||||
&mut self,
|
||||
child_conversation_id: AIConversationId,
|
||||
answer_text: String,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let event = PendingEvent {
|
||||
event_id: Uuid::new_v4().to_string(),
|
||||
source_agent_id: "parent".to_string(),
|
||||
attempt_count: 0,
|
||||
detail: PendingEventDetail::SubagentAnswer {
|
||||
target_conversation_id: child_conversation_id,
|
||||
answer_text,
|
||||
},
|
||||
};
|
||||
|
||||
self.pending_events
|
||||
.entry(child_conversation_id)
|
||||
.or_default()
|
||||
.push(event);
|
||||
|
||||
ctx.emit(OrchestrationEventServiceEvent::EventsReady {
|
||||
conversation_id: child_conversation_id,
|
||||
});
|
||||
}
|
||||
|
||||
/// Route a subagent's completion summary to the parent conversation.
|
||||
pub fn route_subagent_completion_summary(
|
||||
&mut self,
|
||||
child_conversation_id: AIConversationId,
|
||||
parent_conversation_id: AIConversationId,
|
||||
summary_text: String,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let event = PendingEvent {
|
||||
event_id: Uuid::new_v4().to_string(),
|
||||
source_agent_id: child_conversation_id.to_string(),
|
||||
attempt_count: 0,
|
||||
detail: PendingEventDetail::SubagentCompletionSummary {
|
||||
source_conversation_id: child_conversation_id,
|
||||
summary_text,
|
||||
},
|
||||
};
|
||||
|
||||
self.pending_events
|
||||
.entry(parent_conversation_id)
|
||||
.or_default()
|
||||
.push(event);
|
||||
|
||||
ctx.emit(OrchestrationEventServiceEvent::EventsReady {
|
||||
conversation_id: parent_conversation_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// `None` means \"subscribe to all lifecycle types\" (input omitted).
|
||||
@@ -1135,6 +1297,10 @@ fn did_event_round_trip_through_server(
|
||||
PendingEventDetail::Lifecycle { event } => {
|
||||
echoed_lifecycle_event_ids.contains(event.event_id.as_str())
|
||||
}
|
||||
// Local-only events never round-trip through the server.
|
||||
PendingEventDetail::SubagentQuestion { .. }
|
||||
| PendingEventDetail::SubagentAnswer { .. }
|
||||
| PendingEventDetail::SubagentCompletionSummary { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user