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
89 changes: 87 additions & 2 deletions crates/tui/src/core/engine/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,84 @@ fn summarize_subagent_status(status: &serde_json::Value) -> String {
status.to_string()
}

/// The per-row `route:` line prints the child's effective provider/model
/// routing from the optional typed `child_route` receipt (upstream
/// 0c03b5a81 carries it on every compact status row; `resolved_profile_id`
/// is the receipt's connection identity). The emitter bounds the receipt
/// itself, but this renderer must not depend on that: one unbounded route
/// value would eat the per-row budget that keeps the eight-row fleet
/// summary bounded. Every string field is previewed and the assembled line
/// is hard-guarded, so the row stays short whatever a producer sent.
const SUBAGENT_ROUTE_FIELD_PREVIEW_CHARS: usize = 64;
const SUBAGENT_ROUTE_LINE_MAX_CHARS: usize = 200;

/// `None` when the receipt carries nothing printable (missing/empty fields,
/// a non-object non-string value) — the row then shows no route line rather
/// than a placeholder.
fn subagent_route_line(route: &serde_json::Value) -> Option<String> {
if let Some(object) = route.as_object() {
let field = |key: &str| -> Option<String> {
object
.get(key)
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| summarize_text(s, SUBAGENT_ROUTE_FIELD_PREVIEW_CHARS))
};
let provider = field("provider_id")?;
let model = field("model_id")?;
let mut line = format!(" route: {provider}/{model}");
let source = field("route_source");
let profile = field("resolved_profile_id");
match (source, profile) {
(Some(source), Some(profile)) => {
line.push_str(&format!(" (source={source}, profile={profile})"));
}
(Some(source), None) => line.push_str(&format!(" (source={source})")),
(None, Some(profile)) => line.push_str(&format!(" (profile={profile})")),
(None, None) => {}
}
// Hard guard: field previews alone do not bound the assembled line.
return Some(summarize_text(&line, SUBAGENT_ROUTE_LINE_MAX_CHARS));
}
// Defensive: a producer may serialize the receipt as a bare
// "provider/model" label; anything else shapeless is skipped.
let raw = route.as_str()?.trim();
if raw.is_empty() {
return None;
}
Some(summarize_text(
&format!(
" route: {}",
summarize_text(raw, SUBAGENT_ROUTE_FIELD_PREVIEW_CHARS)
),
SUBAGENT_ROUTE_LINE_MAX_CHARS,
))
}

fn summarize_subagent_snapshot(
snapshot: &serde_json::Value,
index: usize,
transcript_handle_fallback: Option<&str>,
child_route_fallback: Option<&serde_json::Value>,
) -> String {
// Session projections (`SubAgentSessionProjection`) keep `transcript_handle`
// on the outer envelope while the wrapped result row carries none, so the
// handle is captured before unwrapping and handed down as a fallback: the
// visible row prints the value the hint gate saw instead of a phantom.
// `child_route` mirrors that rule in reverse: compact fleet rows and the
// compact spawn receipt strip the `snapshot` wrapper and keep the receipt
// on the envelope, and a legacy projection can predate the wrapped row's
// own field — the wrapped value wins when both exist.
let outer_transcript_handle = snapshot
.get("transcript_handle")
.and_then(transcript_handle_row_value);
let outer_child_route = snapshot.get("child_route");
if let Some(inner) = snapshot.get("snapshot") {
let fallback = outer_transcript_handle
.as_deref()
.or(transcript_handle_fallback);
return summarize_subagent_snapshot(inner, index, fallback);
return summarize_subagent_snapshot(inner, index, fallback, outer_child_route);
}

let Some(obj) = snapshot.as_object() else {
Expand Down Expand Up @@ -203,8 +264,17 @@ fn summarize_subagent_snapshot(
.map(|s| summarize_text(s, 1_600));
let steps = obj.get("steps_taken").and_then(serde_json::Value::as_u64);
let duration_ms = obj.get("duration_ms").and_then(serde_json::Value::as_u64);
// The wrapped row's receipt outranks the envelope fallback; absent or
// unprintable values render no line at all (route is optional).
let route_line = obj
.get("child_route")
.or(child_route_fallback)
.and_then(subagent_route_line);

let mut lines = vec![format!("- {agent_id} ({agent_type}) status={status}")];
if let Some(route_line) = route_line {
lines.push(route_line);
}
if let Some(transcript_handle) = transcript_handle {
lines.push(format!(" transcript: {transcript_handle}"));
}
Expand Down Expand Up @@ -407,7 +477,7 @@ fn compact_subagent_tool_result_for_context(tool_name: &str, raw: &str) -> Optio
));
break;
}
out.push_str(&summarize_subagent_snapshot(snapshot, idx + 1, None));
out.push_str(&summarize_subagent_snapshot(snapshot, idx + 1, None, None));
out.push('\n');
}
Some(out.trim_end().to_string())
Expand Down Expand Up @@ -655,6 +725,21 @@ pub(crate) fn compact_tool_result_for_route(
return raw.to_string();
}

// A `read` result that already fit its byte budget carries a resume
// footer instead of mid-line truncation; compacting it again would
// discard content the budget deliberately kept. A result that somehow
// exceeded its declared budget still falls through to the ordinary
// limits below.
if output
.metadata
.as_ref()
.and_then(|metadata| metadata.get("read_budget_bytes"))
.and_then(serde_json::Value::as_u64)
.is_some_and(|budget| raw.len() as u64 <= budget)
{
return raw.to_string();
}

if let Some(summary) = compact_subagent_tool_result_for_context(tool_name, raw) {
return summary;
}
Expand Down
177 changes: 177 additions & 0 deletions crates/tui/src/core/engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17030,6 +17030,39 @@ fn evidence_bounded_preview_is_not_recompacted() {
assert!(context.contains("full output at /tmp/art_call.txt"));
}

#[test]
fn budgeted_read_result_is_not_truncated_a_second_time_by_the_context_compactor() {
// `read` bounds itself to a per-call byte budget and ends a
// budget-limited result with a footer naming the exact offset to
// continue from. The 12K context hard limit used to re-truncate that
// bounded result into a ~900-char snippet, discarding both the content
// and the continuation contract.
let content = format!(
"{}\n\n[Showing lines 1-51 of 200 (50KB limit). Use offset=52 to continue.]",
"r".repeat(40_000)
);
let budgeted = ToolResult::success(content.clone()).with_metadata(json!({
"evidence_routing": "inline",
"read_budget_bytes": content.len()
}));
let passed_through = compact_tool_result_for_context("deepseek-v3.2-128k", "read", &budgeted);
assert_eq!(passed_through, content);
assert!(passed_through.contains("Use offset=52 to continue"));

// The same bytes without a declared budget still take the ordinary path,
// which is what proves the metadata (not the tool name) did the work.
let unbudgeted = ToolResult::success(content.clone());
let compacted = compact_tool_result_for_context("deepseek-v3.2-128k", "read", &unbudgeted);
assert!(compacted.contains("output compacted to protect context"));

// A result that overran its own declared budget is not exempt.
let overrun = ToolResult::success(content).with_metadata(json!({
"read_budget_bytes": 1_000
}));
let compacted_overrun = compact_tool_result_for_context("deepseek-v3.2-128k", "read", &overrun);
assert!(compacted_overrun.contains("output compacted to protect context"));
}

#[test]
fn codex_tool_retention_uses_oauth_route_window_not_asmall_contract_model_window() {
let content = "route-effective context\n".repeat(900);
Expand Down Expand Up @@ -17507,6 +17540,150 @@ fn forkguard_subagent_projection_outer_handle_reaches_summary_row() {
);
}

// Upstream 0c03b5a81 lets every compact status row carry the typed
// `child_route` receipt, but the fork row summarizer dropped the field: the
// unscoped fleet surface never told the model which provider/model each
// child actually ran on. The row renderer must print a short `route:` line
// when the receipt is present and stay silent when it is not — and stay
// bounded even when a producer hands it an oversized receipt.
#[test]
fn forkguard_fleet_summary_rows_carry_bounded_child_route() {
let long_label = "p".repeat(400);
let fleet = json!({
"action": "status",
"count": 3,
"agents": [
{"agent_id": "agent_aaaa1111", "agent_type": "explore", "status": "Running",
"assignment": {"objective": "Map the rendering path"},
"child_route": {
"requested_type": "explore",
"resolved_profile_id": "conn-main",
"canonical_role": "general",
"provider_id": "atlas-main",
"model_id": "gpt-5",
"route_source": "requested_model",
"requested_reasoning": "medium",
"runtime_version": "1.0.0",
"runtime_build_sha": "abc123"
}},
{"agent_id": "agent_bbbb2222", "agent_type": "test", "status": "Completed",
"result": "12 tests green",
"child_route": {
"provider_id": long_label,
"model_id": long_label,
"route_source": long_label,
"resolved_profile_id": long_label
}},
{"agent_id": "agent_cccc3333", "agent_type": "general", "status": "Completed",
"result": "done"}
]
})
.to_string();
let context =
compact_tool_result_for_context("deepseek-v4-pro", "agent", &ToolResult::success(fleet));

assert!(
context.contains(" route: atlas-main/gpt-5 (source=requested_model, profile=conn-main)"),
"a row with a child_route receipt must print its provider/model routing:\n{context}"
);
let route_lines: Vec<&str> = context
.lines()
.filter(|line| line.trim_start().starts_with("route:"))
.collect();
assert_eq!(
route_lines.len(),
2,
"exactly the two rows that carry a receipt print a route line; a row \
without one must not get an empty placeholder:\n{context}"
);
for line in &route_lines {
assert!(
line.chars().count() <= 200,
"every route line must stay bounded:\n{line}"
);
}
assert!(
route_lines[1].contains("..."),
"oversized route fields must be preview-truncated:\n{}",
route_lines[1]
);
}

// The addressed projection wraps the result row in a `snapshot` envelope;
// the route read must survive that unwrap (the wrapped `SubAgentResult`
// carries its own receipt), fall through to the envelope when the wrapped
// row predates the field, and tolerate a bare string label.
#[test]
fn forkguard_subagent_projection_child_route_reaches_summary_row() {
let envelope_route = json!({
"provider_id": "atlas-main",
"model_id": "gpt-5",
"route_source": "requested_model"
});
let wrapped = json!({
"name": "scout",
"agent_id": "agent_aaaa1111",
"status": "Completed",
"child_route": envelope_route,
"snapshot": {
"agent_id": "agent_aaaa1111",
"agent_type": "explore",
"status": "Completed",
"result": "mapped the rendering path",
"child_route": {
"provider_id": "inner-provider",
"model_id": "inner-model",
"route_source": "fallback"
}
}
})
.to_string();
let context =
compact_tool_result_for_context("deepseek-v4-pro", "agent", &ToolResult::success(wrapped));
assert!(
context.contains(" route: inner-provider/inner-model (source=fallback)"),
"the wrapped row's route must reach the visible summary:\n{context}"
);

let envelope_only = json!({
"agent_id": "agent_aaaa1111",
"status": "Completed",
"child_route": envelope_route,
"snapshot": {
"agent_id": "agent_aaaa1111",
"agent_type": "explore",
"status": "Completed",
"result": "done"
}
})
.to_string();
let context = compact_tool_result_for_context(
"deepseek-v4-pro",
"agent",
&ToolResult::success(envelope_only),
);
assert!(
context.contains(" route: atlas-main/gpt-5"),
"the envelope route must fall through when the wrapped row lacks one:\n{context}"
);

let string_route = json!({
"agent_id": "agent_aaaa1111",
"status": "Completed",
"child_route": "atlas-main/gpt-5"
})
.to_string();
let context = compact_tool_result_for_context(
"deepseek-v4-pro",
"agent",
&ToolResult::success(string_route),
);
assert!(
context.contains(" route: atlas-main/gpt-5"),
"a bare string route label must still print, bounded:\n{context}"
);
}

// The hint names `transcript_handle`, so it must gate on rows the summarizer
// actually renders: past the eighth snapshot the receipt truncates, and a
// handle stranded on a truncated row would print no value at all — a phantom
Expand Down
18 changes: 16 additions & 2 deletions crates/tui/src/tools/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,9 +804,17 @@ impl ReadFileTool {
// but no hash or read-before-edit ceremony reaches the lowercase
// schema or result.
context.note_file_read(&file_path);
// The exact byte size this result was self-bounded to: the read
// budget bounds the window content, and the continuation footer (or
// the single-line fallback) rides on top of it. Declaring the final
// payload size lets the context compactor honor `read_budget_bytes`
// (raw bytes <= budget) so an already-bounded read is never truncated
// a second time on its way into the conversation.
let budgeted_bytes = output.len();
Ok(RichToolResult::plain(
ToolResult::success(output).with_metadata(json!({
"evidence_routing": "inline"
"evidence_routing": "inline",
"read_budget_bytes": budgeted_bytes
})),
))
}
Expand Down Expand Up @@ -1221,9 +1229,15 @@ fn render_line_window(
// The file tool self-bounds at 50 KiB and carries its own continuation
// contract (`next_start_line`), so the large-output spillover envelope
// must never re-wrap a read result with a second, weaker truncation.
// `read_budget_bytes` names the byte size this rendered result was
// self-bounded to (the visible-bytes budget bounds the window; the
// `<file>` wrapper and resume footer ride on top of it) so the context
// compactor passes it through instead of truncating it again.
let budgeted_bytes = output.len();
ToolResult::success(output).with_metadata(json!({
"evidence_routing": "inline",
"content_hash": content_hash
"content_hash": content_hash,
"read_budget_bytes": budgeted_bytes
}))
}

Expand Down
Loading
Loading