Skip to content

Commit 2b3edf9

Browse files
feat(code): add cache and context status row (#5408)
Depends on #5407 The status footer now shows cache reads and writes, context usage, and cumulative cost on a second row. --- Cache metrics reuse pricing normalization and remain scoped to the active thread, including in-flight and offload usage. Made by [Open SWE](https://openswe.vercel.app/agents/019fee35-60e2-723d-ab02-fd45c20e28ed) ## References - Plan: https://openswe.vercel.app/agents/019fee35-60e2-723d-ab02-fd45c20e28ed/plan ## Screenshots <img width="749" height="135" alt="Screenshot 2026-08-11 at 2 49 02 PM" src="https://github.com/user-attachments/assets/849eedc5-c260-4ca1-807f-6dc78513aaff" /> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
1 parent 400099d commit 2b3edf9

9 files changed

Lines changed: 454 additions & 192 deletions

File tree

libs/code/deepagents_code/_session_stats.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ class SessionStats:
173173
output_tokens: int = 0
174174
"""Cumulative output tokens across all LLM requests."""
175175

176+
cache_read_tokens: int = 0
177+
"""Cumulative prompt tokens served from provider caches."""
178+
179+
cache_write_tokens: int = 0
180+
"""Cumulative prompt tokens written to provider caches."""
181+
176182
total_cost_usd: float = 0.0
177183
"""Cumulative estimated USD cost across priceable LLM requests."""
178184

@@ -202,6 +208,8 @@ def record_request(
202208
*,
203209
cost_usd: float | None = None,
204210
kind: UsageKind = "assistant",
211+
cache_read_tokens: int = 0,
212+
cache_write_tokens: int = 0,
205213
) -> None:
206214
"""Accumulate usage for one completed LLM request.
207215
@@ -223,10 +231,14 @@ def record_request(
223231
224232
Missing estimates leave monetary totals unchanged.
225233
kind: Request class used for `/cost` type breakdowns.
234+
cache_read_tokens: Input tokens served from provider caches.
235+
cache_write_tokens: Input tokens written to provider caches.
226236
"""
227237
self.request_count += 1
228238
self.input_tokens += input_toks
229239
self.output_tokens += output_toks
240+
self.cache_read_tokens += cache_read_tokens
241+
self.cache_write_tokens += cache_write_tokens
230242
if cost_usd is not None:
231243
self.total_cost_usd += cost_usd
232244
self.priced_request_count += 1
@@ -276,6 +288,8 @@ def retract_request(self, recorded: RecordedRequest) -> None:
276288
self.request_count -= 1
277289
self.input_tokens -= input_toks
278290
self.output_tokens -= output_toks
291+
self.cache_read_tokens -= recorded.cache_read_tokens
292+
self.cache_write_tokens -= recorded.cache_write_tokens
279293
if cost_usd is not None:
280294
self.total_cost_usd -= cost_usd
281295
self.priced_request_count -= 1
@@ -318,6 +332,8 @@ def merge(self, other: SessionStats) -> None:
318332
self.request_count += other.request_count
319333
self.input_tokens += other.input_tokens
320334
self.output_tokens += other.output_tokens
335+
self.cache_read_tokens += other.cache_read_tokens
336+
self.cache_write_tokens += other.cache_write_tokens
321337
self.total_cost_usd += other.total_cost_usd
322338
self.priced_request_count += other.priced_request_count
323339
self.wall_time_seconds += other.wall_time_seconds
@@ -364,6 +380,12 @@ class RecordedRequest:
364380
output_tokens: int
365381
"""Running output tokens recorded so far for the request."""
366382

383+
cache_read_tokens: int
384+
"""Running cache-read tokens recorded so far for the request."""
385+
386+
cache_write_tokens: int
387+
"""Running cache-write tokens recorded so far for the request."""
388+
367389
cost_usd: float | None
368390
"""Estimate for the whole request so far, or `None` when unpriceable."""
369391

@@ -654,13 +676,17 @@ def _move_request_to_named_model(
654676
provider,
655677
cost_usd=cost_usd,
656678
kind=previous.kind,
679+
cache_read_tokens=previous.cache_read_tokens,
680+
cache_write_tokens=previous.cache_write_tokens,
657681
)
658682
recorded_requests[request_id] = RecordedRequest(
659683
model_name=model_name,
660684
provider=provider,
661685
kind=previous.kind,
662686
input_tokens=previous.input_tokens,
663687
output_tokens=previous.output_tokens,
688+
cache_read_tokens=previous.cache_read_tokens,
689+
cache_write_tokens=previous.cache_write_tokens,
664690
cost_usd=cost_usd,
665691
usage_metadata=previous.usage_metadata,
666692
finalized=previous.finalized,
@@ -782,7 +808,7 @@ def record_message_usage(
782808
)
783809
return None
784810

785-
from deepagents_code.cost_tracking import estimate_cost
811+
from deepagents_code.cost_tracking import cache_token_counts, estimate_cost
786812

787813
model_name, provider = _resolve_usage_model(
788814
message,
@@ -816,6 +842,8 @@ def record_message_usage(
816842
# when the fallback is unpriceable, leave a priceable request showing no
817843
# cost at all.
818844
cost_usd = estimate_cost(accumulated_usage, model_name, provider)
845+
cache_reads, cache_writes = cache_token_counts(accumulated_usage)
846+
cache_write_tokens = sum(cache_writes)
819847

820848
stats.record_request(
821849
model_name,
@@ -824,6 +852,8 @@ def record_message_usage(
824852
provider,
825853
cost_usd=cost_usd,
826854
kind=kind,
855+
cache_read_tokens=cache_reads,
856+
cache_write_tokens=cache_write_tokens,
827857
)
828858
if request_id is not None:
829859
recorded_requests[request_id] = RecordedRequest(
@@ -832,6 +862,8 @@ def record_message_usage(
832862
kind=kind,
833863
input_tokens=input_count,
834864
output_tokens=output_count,
865+
cache_read_tokens=cache_reads,
866+
cache_write_tokens=cache_write_tokens,
835867
cost_usd=cost_usd,
836868
usage_metadata=accumulated_usage,
837869
finalized=not is_chunk,

libs/code/deepagents_code/app.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4651,6 +4651,7 @@ async def _post_paint_init(self) -> None:
46514651
self._ui_adapter._on_tokens_show = self._show_tokens
46524652
self._ui_adapter._on_session_cost = self._set_session_cost
46534653
self._ui_adapter._on_provisional_cost = self._add_provisional_cost
4654+
self._ui_adapter._on_usage_update = self._refresh_cache_display
46544655
self._ui_adapter._on_stream_complete = self._mark_thread_turn_completed
46554656

46564657
if self._server_startup_deferred:
@@ -7642,6 +7643,21 @@ def _show_pending_tokens(self) -> None:
76427643
if self._status_bar:
76437644
self._status_bar.show_pending_tokens()
76447645

7646+
def _refresh_cache_display(self) -> None:
7647+
"""Show active-thread cache totals and hit rate, including in-flight use."""
7648+
if self._status_bar is None:
7649+
return
7650+
inputs = self._thread_stats.input_tokens
7651+
reads = self._thread_stats.cache_read_tokens
7652+
writes = self._thread_stats.cache_write_tokens
7653+
if self._inflight_turn_stats is not None and (
7654+
self._inflight_thread_id == self._lc_thread_id
7655+
):
7656+
inputs += self._inflight_turn_stats.input_tokens
7657+
reads += self._inflight_turn_stats.cache_read_tokens
7658+
writes += self._inflight_turn_stats.cache_write_tokens
7659+
self._status_bar.set_cache_tokens(reads, writes, input_tokens=inputs)
7660+
76457661
def _set_session_cost(
76467662
self,
76477663
cost_usd: float,
@@ -7720,6 +7736,7 @@ def _reset_thread_usage(
77207736
has_restored_model_usage: Whether restored history contains model usage.
77217737
"""
77227738
self._thread_stats = SessionStats()
7739+
self._refresh_cache_display()
77237740
self._thread_restored_cost_usd = _coerce_session_cost_usd(cost_usd)
77247741
self._thread_has_restored_model_usage = (
77257742
has_restored_model_usage or self._thread_restored_cost_usd > 0
@@ -15388,6 +15405,7 @@ async def _drain(stream_input: Any) -> list[tuple[str, dict[str, Any]]]: # noqa
1538815405
self._session_stats.merge(offload_stats)
1538915406
if offload_thread_id == self._lc_thread_id:
1539015407
self._thread_stats.merge(offload_stats)
15408+
self._refresh_cache_display()
1539115409

1539215410
async def _remove_offload_artifacts(
1539315411
self,
@@ -15788,6 +15806,7 @@ def _sync_status_model(self) -> None:
1578815806
logger.debug("Screen stack empty during model sync", exc_info=True)
1578915807
if self._status_bar is None:
1579015808
return
15809+
self._status_bar.set_context_limit(settings.model_context_limit)
1579115810
if not provider or not model:
1579215811
logger.warning(
1579315812
"Settings missing model identity at status sync "
@@ -16340,6 +16359,7 @@ def _record_goal_grading_run(event: RubricEvaluationEnd) -> None:
1634016359
self._thread_stats.merge(turn_stats)
1634116360
self._inflight_turn_stats = None
1634216361
self._inflight_thread_id = None
16362+
self._refresh_cache_display()
1634316363
# Settle the display on the committed total. Only a completed turn
1634416364
# is read back: `durability="exit"` may drop an aborted turn's
1634516365
# writes, and the streamed totals already seen are closer to what

libs/code/deepagents_code/app.tcss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,9 @@ Screen {
146146

147147
/* Status bar */
148148
#status-bar {
149-
height: 1;
149+
height: 2;
150150
dock: bottom;
151+
padding: 0;
151152
}
152153

153154
/* Tool approval widgets */

libs/code/deepagents_code/cost_tracking.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,23 @@ def _clamp_cache_counts(
256256
)
257257

258258

259+
def cache_token_counts(
260+
usage_metadata: Mapping[str, Any] | None,
261+
) -> tuple[int, tuple[int, int, int]]:
262+
"""Return normalized cache reads and generic, 5m, and 1h writes."""
263+
if not usage_metadata:
264+
return 0, (0, 0, 0)
265+
input_tokens = _token_count(usage_metadata.get("input_tokens"))
266+
details = usage_metadata.get("input_token_details")
267+
if not isinstance(details, Mapping):
268+
return 0, (0, 0, 0)
269+
return _clamp_cache_counts(
270+
input_tokens,
271+
_token_count(details.get("cache_read")),
272+
_cache_write_counts(details),
273+
)
274+
275+
259276
def _clamped_detail(
260277
value: object,
261278
total: int,
@@ -1348,9 +1365,7 @@ def estimate_cost(
13481365
# numbers, so say so -- a silent clamp can materially undercount.
13491366
original_cache_read = cache_read_tokens
13501367
original_cache_writes = cache_writes
1351-
cache_read_tokens, cache_writes = _clamp_cache_counts(
1352-
input_tokens, cache_read_tokens, cache_writes
1353-
)
1368+
cache_read_tokens, cache_writes = cache_token_counts(usage_metadata)
13541369
if (
13551370
cache_read_tokens != original_cache_read
13561371
or cache_writes != original_cache_writes

libs/code/deepagents_code/tui/textual_adapter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,9 @@ def __init__(
776776
a second authority: every server total replaces what this accumulated.
777777
"""
778778

779+
self._on_usage_update: Callable[[], None] | None = None
780+
"""Called after streamed request usage changes."""
781+
779782
self._on_stream_complete: Callable[[], None] | None = None
780783
"""Called only after the agent stream reaches a clean end."""
781784

@@ -1834,6 +1837,8 @@ async def _after_automatic_compact() -> None:
18341837
),
18351838
recorded_requests=recorded_usage_requests,
18361839
)
1840+
if recorded_usage is not None and adapter._on_usage_update:
1841+
adapter._on_usage_update()
18371842
if recorded_usage is not None and (
18381843
recorded_usage.cost_usd is not None
18391844
and adapter._on_provisional_cost

0 commit comments

Comments
 (0)