Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 36 additions & 31 deletions crates/relayburn-cli/src/commands/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,7 @@ fn run_inner(globals: &GlobalArgs, args: CompareArgs) -> Result<i32> {
if args.include_partial {
if let Some(raw) = args.fidelity.as_deref() {
if raw != "partial" {
return Err(anyhow!(
"--include-partial conflicts with --fidelity {raw}"
));
return Err(anyhow!("--include-partial conflicts with --fidelity {raw}"));
}
}
min_fidelity = FidelityClass::Partial;
Expand All @@ -128,7 +126,9 @@ fn run_inner(globals: &GlobalArgs, args: CompareArgs) -> Result<i32> {
// per-command so the global JSON take-precedence rule in the TS CLI
// becomes "explicit conflict" here — same exit code, same message.
if globals.json && args.csv {
return Err(anyhow!("--json and --csv are mutually exclusive; pick one."));
return Err(anyhow!(
"--json and --csv are mutually exclusive; pick one."
));
}

// 4. Provider filter. Lower-cased CSV; turns whose effective provider
Expand Down Expand Up @@ -238,12 +238,7 @@ fn run_inner(globals: &GlobalArgs, args: CompareArgs) -> Result<i32> {
print!("{csv}");
return Ok(0);
}
let tty = render_tty(
&table,
analyzed_turns,
min_fidelity,
&fidelity_summary,
);
let tty = render_tty(&table, analyzed_turns, min_fidelity, &fidelity_summary);
print!("{tty}");
Ok(0)
}
Expand Down Expand Up @@ -490,13 +485,24 @@ fn build_json(
/// `preserve_order` feature).
fn fidelity_summary_to_value(s: &FidelitySummary) -> Value {
let mut by_class = serde_json::Map::new();
for key in &["full", "usage-only", "aggregate-only", "cost-only", "partial"] {
for key in &[
"full",
"usage-only",
"aggregate-only",
"cost-only",
"partial",
] {
let cls = parse_fidelity(key).unwrap();
let n = s.by_class.get(&cls).copied().unwrap_or(0);
by_class.insert((*key).to_string(), Value::from(n));
}
let mut by_granularity = serde_json::Map::new();
for key in &["per-turn", "per-message", "per-session-aggregate", "cost-only"] {
for key in &[
"per-turn",
"per-message",
"per-session-aggregate",
"cost-only",
] {
let g = match *key {
"per-turn" => UsageGranularity::PerTurn,
"per-message" => UsageGranularity::PerMessage,
Expand Down Expand Up @@ -589,15 +595,9 @@ fn render_csv(table: &CompareTable) -> String {
c.one_shot_turns.to_string(),
c.priced_turns.to_string(),
num_csv(c.total_cost, 6),
c.cost_per_turn
.map(|v| num_csv(v, 6))
.unwrap_or_default(),
c.one_shot_rate
.map(|v| num_csv(v, 4))
.unwrap_or_default(),
c.cache_hit_rate
.map(|v| num_csv(v, 4))
.unwrap_or_default(),
c.cost_per_turn.map(|v| num_csv(v, 6)).unwrap_or_default(),
c.one_shot_rate.map(|v| num_csv(v, 4)).unwrap_or_default(),
c.cache_hit_rate.map(|v| num_csv(v, 4)).unwrap_or_default(),
c.median_retries
.map(|v| {
// `String(n)` for numbers; JS prints integers as-is.
Expand Down Expand Up @@ -662,7 +662,10 @@ fn render_tty(
) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push(String::new());
lines.push(format!("turns analyzed: {}", format_uint(analyzed_turns as u64)));
lines.push(format!(
"turns analyzed: {}",
format_uint(analyzed_turns as u64)
));

let excluded = compute_excluded(summary, minimum);
if excluded.total > 0 {
Expand All @@ -671,9 +674,8 @@ fn render_tty(
lines.push(String::new());

if table.models.is_empty() || table.categories.is_empty() {
lines.push(
"no data to compare (need turns spanning ≥1 model and ≥1 activity).".to_string(),
);
lines
.push("no data to compare (need turns spanning ≥1 model and ≥1 activity).".to_string());
lines.push(String::new());
return lines.join("\n");
}
Expand Down Expand Up @@ -751,10 +753,7 @@ fn render_tty(
// Coverage notes.
let mut notes: Vec<String> = Vec::new();
for cat in &table.categories {
let any_has_data = table
.models
.iter()
.any(|m| !cell_for(m, cat).no_data);
let any_has_data = table.models.iter().any(|m| !cell_for(m, cat).no_data);
if !any_has_data {
continue;
}
Expand Down Expand Up @@ -931,7 +930,10 @@ mod tests {

#[test]
fn parse_fidelity_known_classes() {
assert!(matches!(parse_fidelity("full").unwrap(), FidelityClass::Full));
assert!(matches!(
parse_fidelity("full").unwrap(),
FidelityClass::Full
));
assert!(matches!(
parse_fidelity("usage-only").unwrap(),
FidelityClass::UsageOnly
Expand All @@ -941,7 +943,10 @@ mod tests {

#[test]
fn display_model_name_strips_provider_prefix() {
assert_eq!(display_model_name("anthropic/claude-sonnet-4-6"), "claude-sonnet-4-6");
assert_eq!(
display_model_name("anthropic/claude-sonnet-4-6"),
"claude-sonnet-4-6"
);
assert_eq!(display_model_name("claude-haiku-4-5"), "claude-haiku-4-5");
}
}
18 changes: 11 additions & 7 deletions crates/relayburn-cli/src/commands/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,12 @@ fn render_mermaid(graph: &FlowGraph) -> String {
for node in &graph.nodes {
let safe_id = mermaid_id(&node.id);
let label = mermaid_label(&node.label, node.turn_number);
writeln!(out, " {safe_id}[\"{label}\"]:::{}", mermaid_class(node.kind)).unwrap();
writeln!(
out,
" {safe_id}[\"{label}\"]:::{}",
mermaid_class(node.kind)
)
.unwrap();
}

if !graph.edges.is_empty() {
Expand Down Expand Up @@ -277,11 +282,7 @@ fn render_svg(graph: &FlowGraph) -> String {
let height = max_y + LEGEND_HEIGHT + SVG_MARGIN * 2;

let mut out = String::with_capacity(4096);
writeln!(
out,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
)
.unwrap();
writeln!(out, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>").unwrap();
writeln!(
out,
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" viewBox=\"0 0 {width} {height}\" font-family=\"-apple-system,Segoe UI,Helvetica,sans-serif\" font-size=\"11\">"
Expand Down Expand Up @@ -518,7 +519,10 @@ const _ASSERT_LAYOUT_CONSTANTS: () = {
#[cfg(test)]
mod tests {
use super::*;
use relayburn_sdk::{flow_graph_from_trees, FlowOpts as SdkFlowOpts, SpanAttrValue, SpanKind, SpanNode, TurnSpanTree};
use relayburn_sdk::{
flow_graph_from_trees, FlowOpts as SdkFlowOpts, SpanAttrValue, SpanKind, SpanNode,
TurnSpanTree,
};

fn turn_root() -> SpanNode {
SpanNode::new(SpanKind::Turn, "turn")
Expand Down
32 changes: 13 additions & 19 deletions crates/relayburn-cli/src/commands/hotspots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ fn resolve_pattern_selection(raw: &str) -> Result<Vec<String>, String> {
}
let mut out: Vec<String> = Vec::new();
for piece in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
if !PATTERN_KINDS.iter().any(|k| *k == piece) {
if !PATTERN_KINDS.contains(&piece) {
return Err(format!(
"unknown --patterns value \"{}\". Valid: {}",
piece,
Expand Down Expand Up @@ -1156,10 +1156,7 @@ fn format_bytes(bytes: u64) -> String {

// ---- sort helpers ---------------------------------------------------------

fn sort_file<'a>(
rows: &'a [FileAggregation],
rank_by: RankBy,
) -> (&'static str, Vec<&'a FileAggregation>) {
fn sort_file(rows: &[FileAggregation], rank_by: RankBy) -> (&'static str, Vec<&FileAggregation>) {
let mut out: Vec<&FileAggregation> = rows.iter().collect();
match rank_by {
RankBy::Cost => (
Expand All @@ -1168,49 +1165,46 @@ fn sort_file<'a>(
out,
),
RankBy::Bytes => {
out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes));
out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes));
("Top files by output bytes", out)
}
}
}

fn sort_bash<'a>(
rows: &'a [BashAggregation],
rank_by: RankBy,
) -> (&'static str, Vec<&'a BashAggregation>) {
fn sort_bash(rows: &[BashAggregation], rank_by: RankBy) -> (&'static str, Vec<&BashAggregation>) {
let mut out: Vec<&BashAggregation> = rows.iter().collect();
match rank_by {
RankBy::Cost => ("Top exact Bash commands by cost", out),
RankBy::Bytes => {
out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes));
out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes));
("Top exact Bash commands by output bytes", out)
}
}
}

fn sort_bash_verb<'a>(
rows: &'a [BashVerbAggregation],
fn sort_bash_verb(
rows: &[BashVerbAggregation],
rank_by: RankBy,
) -> (&'static str, Vec<&'a BashVerbAggregation>) {
) -> (&'static str, Vec<&BashVerbAggregation>) {
let mut out: Vec<&BashVerbAggregation> = rows.iter().collect();
match rank_by {
RankBy::Cost => ("Top Bash verbs by cost", out),
RankBy::Bytes => {
out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes));
out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes));
("Top Bash verbs by output bytes", out)
}
}
}

fn sort_subagent<'a>(
rows: &'a [SubagentAggregation],
fn sort_subagent(
rows: &[SubagentAggregation],
rank_by: RankBy,
) -> (&'static str, Vec<&'a SubagentAggregation>) {
) -> (&'static str, Vec<&SubagentAggregation>) {
let mut out: Vec<&SubagentAggregation> = rows.iter().collect();
match rank_by {
RankBy::Cost => ("Top subagent calls by cost", out),
RankBy::Bytes => {
out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes));
out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes));
("Top subagent calls by output bytes", out)
}
}
Expand Down
13 changes: 7 additions & 6 deletions crates/relayburn-cli/src/commands/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,13 @@ fn run_watch(globals: &GlobalArgs, args: &IngestArgs) -> i32 {
});

let progress_for_error = progress_for_loop.clone();
let on_error: relayburn_sdk::ErrorSink = Arc::new(move |err: &anyhow::Error| match &progress_for_error {
Some(p) => p.suspend(|| {
eprintln!("[burn] ingest: {err}");
}),
None => eprintln!("[burn] ingest: {err}"),
});
let on_error: relayburn_sdk::ErrorSink =
Arc::new(move |err: &anyhow::Error| match &progress_for_error {
Some(p) => p.suspend(|| {
eprintln!("[burn] ingest: {err}");
}),
None => eprintln!("[burn] ingest: {err}"),
});

// Default to the `notify`-backed FS-event driver against the
// three session-store roots ingest scans. Falls back to polling
Expand Down
16 changes: 11 additions & 5 deletions crates/relayburn-cli/src/commands/mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,12 @@ impl Server {
}
};
if !value.is_object() {
write_response(&error_envelope(&Value::Null, -32600, "invalid request", None));
write_response(&error_envelope(
&Value::Null,
-32600,
"invalid request",
None,
));
return;
}

Expand Down Expand Up @@ -421,11 +426,12 @@ impl Server {

// Mirror TS: when no override and no registered default, surface
// a more descriptive note than the SDK's generic one.
if payload.session_id.is_none() && override_id.is_none() && self.default_session_id.is_none()
if payload.session_id.is_none()
&& override_id.is_none()
&& self.default_session_id.is_none()
{
payload.note = Some(
"no session id provided and server was not registered with one".to_string(),
);
payload.note =
Some("no session id provided and server was not registered with one".to_string());
}

let value = serde_json::to_value(&payload).unwrap_or(Value::Null);
Expand Down
10 changes: 2 additions & 8 deletions crates/relayburn-cli/src/commands/overhead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,11 +368,7 @@ fn format_line_range(start: u64, end: u64) -> String {
// `burn overhead deltas` (#432)
// ---------------------------------------------------------------------------

fn run_deltas(
globals: &GlobalArgs,
since: Option<String>,
args: OverheadDeltasArgs,
) -> i32 {
fn run_deltas(globals: &GlobalArgs, since: Option<String>, args: OverheadDeltasArgs) -> i32 {
let opts = ContextDeltaOpts {
session: args.session.clone(),
since: since.as_deref().and_then(parse_since_duration),
Expand Down Expand Up @@ -626,9 +622,7 @@ mod tests {
approx_bytes: 400,
truncated: false,
},
InterveningStep::Compaction {
tokens_freed: 5000,
},
InterveningStep::Compaction { tokens_freed: 5000 },
];
let s = driver_summary(&steps);
assert!(s.contains("compaction"));
Expand Down
Loading
Loading