Skip to content

Commit ddf3a06

Browse files
authored
relayburn-sdk: surface replacement savings on summary(); deprecate sdk@1.x (#366)
Widens `Summary` in the Rust SDK to include an optional `replacement_savings` field (calls, collapsedCalls, estimatedTokensSaved, byTool). Populated from `summarize_replacement_savings()` in `analyze/replacement_savings.rs`; elided when no replacement-tool calls exist in the queried window. Mirrors the new field into the napi-rs binding (`relayburn-sdk-node`) and the TypeScript .d.ts for `@relayburn/sdk@2.x`. Adds unit tests in `query_verbs.rs` asserting `Some(_)` for sessions with replacement calls and `None` for clean sessions. Adds a deprecation banner to `packages/sdk/README.md` and matching CHANGELOG entries in both `@relayburn/sdk` (1.x maintenance freeze) and `@relayburn/sdk` 2.x. Refs #365.
1 parent 7bcf94a commit ddf3a06

7 files changed

Lines changed: 155 additions & 9 deletions

File tree

crates/relayburn-sdk-node/src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,50 @@ pub struct SummaryModelRow {
399399
pub cost: f64,
400400
}
401401

402+
#[napi(object)]
403+
pub struct ReplacementSavingsToolRow {
404+
pub tool: String,
405+
pub calls: BigInt,
406+
pub collapsed_calls: BigInt,
407+
pub estimated_tokens_saved: BigInt,
408+
}
409+
410+
#[napi(object)]
411+
pub struct ReplacementSavingsSummary {
412+
pub calls: BigInt,
413+
pub collapsed_calls: BigInt,
414+
pub estimated_tokens_saved: BigInt,
415+
pub by_tool: Vec<ReplacementSavingsToolRow>,
416+
}
417+
418+
impl From<sdk::ReplacementSavingsSummary> for ReplacementSavingsSummary {
419+
fn from(s: sdk::ReplacementSavingsSummary) -> Self {
420+
ReplacementSavingsSummary {
421+
calls: u64_to_bigint(s.calls),
422+
collapsed_calls: u64_to_bigint(s.collapsed_calls),
423+
estimated_tokens_saved: u64_to_bigint(s.estimated_tokens_saved),
424+
by_tool: s
425+
.by_tool
426+
.into_iter()
427+
.map(|(tool, agg)| ReplacementSavingsToolRow {
428+
tool,
429+
calls: u64_to_bigint(agg.calls),
430+
collapsed_calls: u64_to_bigint(agg.collapsed_calls),
431+
estimated_tokens_saved: u64_to_bigint(agg.estimated_tokens_saved),
432+
})
433+
.collect(),
434+
}
435+
}
436+
}
437+
402438
#[napi(object)]
403439
pub struct Summary {
404440
pub total_tokens: BigInt,
405441
pub total_cost: f64,
406442
pub turn_count: BigInt,
407443
pub by_tool: Vec<SummaryToolRow>,
408444
pub by_model: Vec<SummaryModelRow>,
445+
pub replacement_savings: Option<ReplacementSavingsSummary>,
409446
}
410447

411448
impl From<sdk::Summary> for Summary {
@@ -433,6 +470,7 @@ impl From<sdk::Summary> for Summary {
433470
cost: r.cost,
434471
})
435472
.collect(),
473+
replacement_savings: s.replacement_savings.map(ReplacementSavingsSummary::from),
436474
}
437475
}
438476
}

crates/relayburn-sdk/src/analyze/replacement_savings.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,16 @@ pub struct ToolCallSavings {
5252
pub estimated_tokens_saved: u64,
5353
}
5454

55-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
55+
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
56+
#[serde(rename_all = "camelCase")]
5657
pub struct ToolSavingsAggregate {
5758
pub calls: u64,
5859
pub collapsed_calls: u64,
5960
pub estimated_tokens_saved: u64,
6061
}
6162

62-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
63+
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
64+
#[serde(rename_all = "camelCase")]
6365
pub struct ReplacementSavingsSummary {
6466
pub calls: u64,
6567
pub collapsed_calls: u64,

crates/relayburn-sdk/src/query_verbs.rs

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ use crate::analyze::{
2121
DetectToolCallPatternsOptions, DetectToolOutputBloatOptions, FidelitySummary, FileAggregation,
2222
GhostSurfaceFindingOptions, HotspotsOptions as AnalyzeHotspotsOptions, LoadedClaudeSettings,
2323
MarkdownSection, OverheadFile, OverheadFileKind, ParsedOverheadFile, PricingTable,
24-
ProviderFilter, SessionClaudeMdCost, SubagentAggregation, WasteFinding, aggregate_by_bash,
25-
aggregate_by_bash_verb, aggregate_by_file, aggregate_by_subagent, attribute_hotspots,
26-
attribute_overhead, build_compare_table, build_ghost_surface_inputs,
27-
build_trim_recommendations, cost_for_turn, detect_ghost_surface, detect_patterns,
28-
detect_tool_call_patterns, detect_tool_output_bloat, find_overhead_files,
24+
ProviderFilter, ReplacementSavingsSummary, SessionClaudeMdCost, SubagentAggregation,
25+
WasteFinding, aggregate_by_bash, aggregate_by_bash_verb, aggregate_by_file,
26+
aggregate_by_subagent, attribute_hotspots, attribute_overhead, build_compare_table,
27+
build_ghost_surface_inputs, build_trim_recommendations, cost_for_turn, detect_ghost_surface,
28+
detect_patterns, detect_tool_call_patterns, detect_tool_output_bloat, find_overhead_files,
2929
findings_from_patterns, ghost_surface_to_finding, has_minimum_fidelity, load_claude_settings,
3030
load_overhead_file, load_pricing, project_claude_settings_path,
3131
render_unified_diff_for_recommendation, sum_costs, summarize_fidelity,
32-
summarize_fidelity_from_iter, tool_call_pattern_to_finding, tool_output_bloat_to_finding,
33-
user_claude_settings_path,
32+
summarize_fidelity_from_iter, summarize_replacement_savings, tool_call_pattern_to_finding,
33+
tool_output_bloat_to_finding, user_claude_settings_path,
3434
};
3535
use crate::ledger::Query;
3636
use crate::reader::{
@@ -237,6 +237,8 @@ pub struct Summary {
237237
pub turn_count: u64,
238238
pub by_tool: Vec<SummaryToolRow>,
239239
pub by_model: Vec<SummaryModelRow>,
240+
#[serde(default, skip_serializing_if = "Option::is_none")]
241+
pub replacement_savings: Option<ReplacementSavingsSummary>,
240242
}
241243

242244
impl LedgerHandle {
@@ -307,6 +309,9 @@ fn compute_summary(turns: &[TurnRecord], pricing: &PricingTable) -> Summary {
307309
}
308310
}
309311

312+
let savings = summarize_replacement_savings(turns, None);
313+
let replacement_savings = if savings.calls > 0 { Some(savings) } else { None };
314+
310315
Summary {
311316
total_tokens,
312317
total_cost,
@@ -319,6 +324,7 @@ fn compute_summary(turns: &[TurnRecord], pricing: &PricingTable) -> Summary {
319324
.into_iter()
320325
.map(|k| by_model.remove(&k).unwrap())
321326
.collect(),
327+
replacement_savings,
322328
}
323329
}
324330

@@ -2421,4 +2427,83 @@ mod tests {
24212427
std::env::set_var("RELAYBURN_CONTENT_TTL_DAYS", v);
24222428
}
24232429
}
2430+
2431+
// -----------------------------------------------------------------------
2432+
// compute_summary — replacement_savings field
2433+
// -----------------------------------------------------------------------
2434+
2435+
fn make_turn_with_calls(calls: Vec<ToolCall>) -> TurnRecord {
2436+
TurnRecord {
2437+
v: 1,
2438+
source: SourceKind::ClaudeCode,
2439+
session_id: "s".to_string(),
2440+
session_path: None,
2441+
message_id: "m".to_string(),
2442+
turn_index: 0,
2443+
ts: "2026-04-20T00:00:00.000Z".to_string(),
2444+
model: "claude-sonnet-4-6".to_string(),
2445+
project: None,
2446+
project_key: None,
2447+
usage: Usage {
2448+
input: 100,
2449+
output: 50,
2450+
reasoning: 0,
2451+
cache_read: 0,
2452+
cache_create_5m: 0,
2453+
cache_create_1h: 0,
2454+
},
2455+
tool_calls: calls,
2456+
files_touched: None,
2457+
subagent: None,
2458+
stop_reason: None,
2459+
activity: None,
2460+
retries: None,
2461+
has_edits: None,
2462+
fidelity: None,
2463+
}
2464+
}
2465+
2466+
#[test]
2467+
fn compute_summary_replacement_savings_some_when_replacement_tool_present() {
2468+
let tc = ToolCall {
2469+
id: "tc-1".into(),
2470+
name: "relaywash__Search".into(),
2471+
target: None,
2472+
args_hash: "h".into(),
2473+
is_error: None,
2474+
edit_pre_hash: None,
2475+
edit_post_hash: None,
2476+
skill_name: None,
2477+
replaced_tools: Some(vec!["Glob".into(), "Grep".into(), "Read".into()]),
2478+
collapsed_calls: Some(9),
2479+
};
2480+
let turns = vec![make_turn_with_calls(vec![tc])];
2481+
let pricing = load_pricing(None);
2482+
let result = compute_summary(&turns, &pricing);
2483+
let savings = result.replacement_savings.expect("should have replacement_savings");
2484+
assert_eq!(savings.calls, 1);
2485+
assert_eq!(savings.collapsed_calls, 9);
2486+
assert!(!savings.by_tool.is_empty());
2487+
assert!(savings.by_tool.contains_key("relaywash__Search"));
2488+
}
2489+
2490+
#[test]
2491+
fn compute_summary_replacement_savings_none_when_no_replacement_tools() {
2492+
let tc = ToolCall {
2493+
id: "tc-1".into(),
2494+
name: "Bash".into(),
2495+
target: None,
2496+
args_hash: "h".into(),
2497+
is_error: None,
2498+
edit_pre_hash: None,
2499+
edit_post_hash: None,
2500+
skill_name: None,
2501+
replaced_tools: None,
2502+
collapsed_calls: None,
2503+
};
2504+
let turns = vec![make_turn_with_calls(vec![tc])];
2505+
let pricing = load_pricing(None);
2506+
let result = compute_summary(&turns, &pricing);
2507+
assert!(result.replacement_savings.is_none());
2508+
}
24242509
}

packages/sdk-node/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- `summary()` result now includes `replacementSavings` — a rollup of per-tool collapsed-call counts and tokens-saved estimates derived from `_meta`-annotated tool results. Omitted (field absent) when no replacement-tool calls exist in the queried window.
8+
59
## [2.0.0] - 2026-05-07
610

711
- Initial scaffolding: umbrella package layout (`@relayburn/sdk`) +

packages/sdk-node/src/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ export declare function summary(opts?: SummaryOptions): Promise<{
4747
turnCount: number;
4848
byTool: Array<{ tool: string; tokens: number | bigint; cost: number; count: number }>;
4949
byModel: Array<{ model: string; tokens: number | bigint; cost: number }>;
50+
replacementSavings?: {
51+
calls: number | bigint;
52+
collapsedCalls: number | bigint;
53+
estimatedTokensSaved: number | bigint;
54+
byTool: Array<{
55+
tool: string;
56+
calls: number | bigint;
57+
collapsedCalls: number | bigint;
58+
estimatedTokensSaved: number | bigint;
59+
}>;
60+
};
5061
}>
5162

5263
export interface SessionCostOptions {

packages/sdk/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to `@relayburn/sdk`.
44

55
## [Unreleased]
66

7+
### Deprecation
8+
9+
- `@relayburn/sdk@1.x` is now in maintenance-only mode. New query-surface work lands in `@relayburn/sdk@2.x` (napi-rs binding over the Rust `relayburn-sdk` crate). The 1.x type surface is frozen; see [#249](https://github.com/AgentWorkforce/burn/issues/249).
10+
711
## [1.10.0] - 2026-05-03
812

913
### Breaking Changes

packages/sdk/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
**Deprecated.** `@relayburn/sdk@1.x` is in maintenance-only mode. New work lands in `@relayburn/sdk@2.x` (the napi-rs binding over the Rust `relayburn-sdk` crate). Embedders should pin `^2.0.0` once it ships. The 1.x type surface is frozen and will not gain new fields. See [#249](https://github.com/AgentWorkforce/burn/issues/249) for the cutover schedule.
2+
13
# @relayburn/sdk
24

35
Embeddable Relayburn SDK for in-process ingestion and analysis. This package is the **source of truth** for the in-process query/compute surface — `@relayburn/mcp` and `@relayburn/cli` consume the SDK rather than duplicating its logic.

0 commit comments

Comments
 (0)