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:
Ryan Ward
2026-05-21 11:59:37 -05:00
co-authored by Claude Opus 4.6
parent eaa2ddc75e
commit 6f54e2cb30
229 changed files with 2506 additions and 2634 deletions
+180 -14
View File
@@ -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,
}
}