v1.3.0: Bedrock translator refactor, usage metrics, session restore fixes, and predefined rules

Major changes:

- **Bedrock translator architecture**: Extract orchestration logic from `impl.rs` into
  a dedicated `translator.rs` module. Rename `convert_request.rs` → `request_translator.rs`
  and `stream.rs` → `response_translator.rs` for clarity. Remove `tool_docs.rs` (inlined).
  Remove `fallback_to_warp` setting and server fallback path — Bedrock is now the sole backend.

- **Unknown tool handling**: The response translator now detects hallucinated/unknown tool
  calls from the model and synthesizes error tool_results so the conversation doesn't
  deadlock waiting for a result that will never come.

- **Usage display overhaul**: Replace credit-based usage display with detailed token metrics
  showing context window %, cache hit rate (read/write/miss), and estimated cost in dollars.
  Add `total_input_tokens`, `total_cache_read_tokens`, `total_cache_write_tokens`, and
  `cache_miss_tokens` accessors to `AIConversation`.

- **Predefined rules system**: Add `predefined_rules.rs` with 11 system-defined behavioral
  rules that are auto-seeded on first launch. Add "Add Predefined Rules" button to the
  Rules UI for re-adding them later. Track seeding state via `has_seeded_predefined_rules`
  setting.

- **Session restore improvements**: Rename database file from `warp.sqlite` to
  `galaxy.sqlite` with automatic migration from both same-directory and state_dir legacy
  paths. Improve CWD persistence by falling back to `session_startup_path` for agent-mode
  and fresh tabs. Add extensive session-save/restore logging.

- **Shell bootstrap rebrand**: Rename `WARP_INITIAL_WORKING_DIR` environment variable to
  `GALAXY_INITIAL_WORKING_DIR` across bash, zsh, and fish bootstrap scripts.

- **Model defaults**: Change default Bedrock model from Opus 4.7 to Opus 4.6.
  Add `context_window_for_model()` helper with model-aware context sizes.
  Remove `is_bedrock_model()` (no longer needed without server fallback).

- **User query persistence**: The response translator now emits a `UserQuery` proto message
  at stream start so the user's prompt persists across sessions for conversation titles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Ward
2026-05-20 15:15:28 -05:00
co-authored by Claude Opus 4.6
parent ec99146ccc
commit eaa2ddc75e
38 changed files with 1687 additions and 1129 deletions
+45 -35
View File
@@ -18,7 +18,6 @@ use crate::ai::blocklist::block::view_impl::common::{
use crate::ai::blocklist::inline_action::aws_bedrock_credentials_error::AwsBedrockCredentialsErrorView;
use crate::ai::blocklist::inline_action::create_or_edit_document::CreateOrEditDocumentAction;
use crate::ai::blocklist::secret_redaction::SecretRedactionState;
use crate::ai::blocklist::view_util::format_credits;
use crate::ai::skills::SkillOpenOrigin;
use crate::ai::skills::{
icon_override_for_skill_name, render_skill_button, skill_path_from_file_path,
@@ -3195,13 +3194,8 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
return Empty::new().finish();
};
// If this conversation has no usage metadata (e.g. a forked conversation from
// mid-way through a prior conversation where the server did not send
// ConversationUsageMetadata), avoid rendering the usage button entirely.
let has_any_usage = conversation.credits_spent() > 0.0
|| conversation.credits_spent_for_last_block().is_some()
|| !conversation.token_usage().is_empty()
|| conversation.tool_usage_metadata().total_tool_calls() > 0;
let has_any_usage = conversation.total_tokens() > 0
|| conversation.total_cost_cents() > 0.0;
if !has_any_usage {
return Empty::new().finish();
}
@@ -3215,29 +3209,37 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
Icon::ChevronRight
};
let total_credits_spent = conversation.credits_spent();
let mut credit_usage_text = format_credits(total_credits_spent);
if let Some(credits_spent_for_last_block) = conversation.credits_spent_for_last_block() {
// Only show the credits spent for the last block if it is different from the total credits spent
// and we spent a non-zero amount of credits for the last block.
// Avoid showing the credits spent for the last block if the request failed, as we refund user
// credits in that case (so no credits were in fact spent).
if credits_spent_for_last_block > 0.0
&& total_credits_spent != credits_spent_for_last_block
&& props.model.status(app).error().is_none()
{
// If the first part of the decimal is 0, we just display the whole number.
if credits_spent_for_last_block.fract() < 0.1 {
credit_usage_text = format!(
"{credit_usage_text} (+{})",
credits_spent_for_last_block.trunc() as i32
);
} else {
credit_usage_text =
format!("{credit_usage_text} (+{credits_spent_for_last_block:.1})");
}
}
}
let context_usage = conversation.context_window_usage();
let total_input = conversation.total_input_tokens();
let cache_read = conversation.total_cache_read_tokens();
let cache_write = conversation.total_cache_write_tokens();
let cache_miss = conversation.cache_miss_tokens();
let cost_cents = conversation.total_cost_cents();
let max_context: u32 = if context_usage > 0.0 {
(total_input as f32 / context_usage).round() as u32
} else {
200_000
};
let context_pct = context_usage * 100.0;
let cache_total = cache_read + cache_write + cache_miss;
let cache_hit_pct = if cache_total > 0 {
(cache_read as f64 / cache_total as f64) * 100.0
} else {
0.0
};
let usage_text = format!(
"Context: {:.1}% ({} / {}) | Cache: {:.1}% (R: {}, W: {}, M: {}) | Cost: ${:.2}",
context_pct,
format_token_count(total_input),
format_token_count(max_context),
cache_hit_pct,
format_token_count(cache_read),
format_token_count(cache_write),
format_token_count(cache_miss),
cost_cents / 100.0,
);
let icon_size = icon_size(app);
let button_row = Flex::row()
@@ -3246,7 +3248,7 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
.with_child(
Container::new(
Text::new_inline(
credit_usage_text,
usage_text,
appearance.ui_font_family(),
appearance.monospace_font_size(),
)
@@ -3265,7 +3267,6 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
)
.with_child(
Container::new(
// Expansion icon
ConstrainedBox::new(
expansion_icon
.to_galaxyui_icon(
@@ -3299,10 +3300,9 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
.with_background(background)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)));
// Show tooltip on hover or while clicked
let mut stack = Stack::new().with_child(content.finish());
let tooltip = ui_builder
.tool_tip("Show credit usage details".to_string())
.tool_tip("Show usage details".to_string())
.build()
.finish();
stack.add_positioned_overlay_child(
@@ -3328,6 +3328,16 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
.finish()
}
fn format_token_count(tokens: u32) -> String {
if tokens >= 1_000_000 {
format!("{:.1}M", tokens as f64 / 1_000_000.0)
} else if tokens >= 1_000 {
format!("{:.1}k", tokens as f64 / 1_000.0)
} else {
format!("{tokens}")
}
}
pub fn action_icon<V: View>(
action_id: &AIAgentActionId,
action_model: &ModelHandle<BlocklistAIActionModel>,
@@ -18,7 +18,6 @@ use crate::{
},
network::NetworkStatus,
report_error, send_telemetry_from_ctx,
server::server_api::ServerApiProvider,
settings::ai::AISettings,
};
use settings::Setting;
@@ -100,7 +99,6 @@ impl ResponseStream {
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(),
fallback_to_warp: *settings.bedrock_fallback_to_warp.value(),
}.with_external_fallbacks())
}
@@ -110,7 +108,6 @@ impl ResponseStream {
can_attempt_resume_on_error: bool,
ctx: &mut ModelContext<Self>,
) -> Self {
let server_api = ServerApiProvider::as_ref(ctx).get();
let (cancellation_tx, cancellation_rx) = oneshot::channel();
let start_time = Local::now();
@@ -119,13 +116,7 @@ impl ResponseStream {
let params_clone = params.clone();
let _ = ctx.spawn(
async move {
generate_multi_agent_output(
server_api,
bedrock_config,
params_clone,
cancellation_rx,
)
.await
generate_multi_agent_output(bedrock_config, params_clone, cancellation_rx).await
},
move |me, stream, ctx| {
me.handle_response_stream_result(request_id, stream, ctx);
@@ -194,11 +185,9 @@ 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 server_api = ServerApiProvider::as_ref(ctx).get();
let _ = ctx.spawn(
async move {
generate_multi_agent_output(server_api, bedrock_config, params, cancellation_rx)
.await
generate_multi_agent_output(bedrock_config, params, cancellation_rx).await
},
move |me, stream, ctx| {
me.handle_response_stream_result(request_id, stream, ctx);
+1 -1
View File
@@ -67,7 +67,7 @@ pub(crate) use view_util::{
NEW_AGENT_PANE_LABEL,
};
pub(crate) use view_util::{format_credits, format_token_count};
pub(crate) use view_util::{format_cost_cents, format_credits, format_token_count};
pub use crate::ai::blocklist::block::{secret_redaction, AIBlockResponseRating, TextLocation};
pub use block::keyboard_navigable_buttons;
@@ -1,5 +1,5 @@
use crate::ai::blocklist::usage::render_context_window_usage_icon;
use crate::ai::blocklist::view_util::{format_cost_cents, format_credits, format_token_count};
use crate::ai::blocklist::view_util::{format_cost_cents, format_token_count};
use crate::appearance::Appearance;
use crate::persistence::model::{
token_usage_category_display_name, ModelTokenUsage, FULL_TERMINAL_USE_CATEGORY,
@@ -26,10 +26,6 @@ pub enum DisplayMode {
}
pub struct ConversationUsageInfo {
pub credits_spent: f32,
// Credits spent over the last block, where the block comprises
// all agent outputs since the most recent user input.
pub credits_spent_for_last_block: Option<f32>,
pub tool_calls: i32,
pub models: Vec<ModelTokenUsage>,
pub context_window_usage: f32,
@@ -144,28 +140,10 @@ impl ConversationUsageView {
));
values.push(render_section_header("".to_string(), appearance));
if self.display_mode == DisplayMode::Footer
&& self.usage_info.credits_spent_for_last_block.is_some()
{
let last_block_credits = self.usage_info.credits_spent_for_last_block.unwrap();
labels.push(render_label_text(
"Credits spent (last response)",
appearance,
));
if self.usage_info.estimated_cost_cents > 0.0 {
labels.push(render_label_text("Estimated cost", appearance));
values.push(render_value_text(
format_credits(last_block_credits),
appearance,
));
labels.push(render_label_text("Credits spent (total)", appearance));
values.push(render_value_text(
format_credits(self.usage_info.credits_spent),
appearance,
));
} else {
labels.push(render_label_text("Credits spent", appearance));
values.push(render_value_text(
format_credits(self.usage_info.credits_spent),
format_cost_cents(self.usage_info.estimated_cost_cents),
appearance,
));
}
@@ -307,14 +285,6 @@ impl ConversationUsageView {
appearance,
));
}
if self.usage_info.estimated_cost_cents > 0.0 {
labels.push(render_label_text("Estimated cost", appearance));
values.push(render_value_text(
format_cost_cents(self.usage_info.estimated_cost_cents),
appearance,
));
}
}
labels.push(render_label_text("Context window used", appearance));