diff --git a/aborist/cli.py b/aborist/cli.py index c97fb9c..2d6b142 100644 --- a/aborist/cli.py +++ b/aborist/cli.py @@ -794,6 +794,32 @@ def _render_query_human(result: dict, question: str) -> str: f"answer {result.get('answer_chars', 0):,} chars" ) + # Per-phase timings — surfaces where the per-call cost lands. + # Hermes-bound queries should show `llm_ms` dominating; if + # search_ms or persist_ms creeps up that's a retrieval / WAL + # signal the operator wants visible. Cache-hit rows skip llm + # entirely so the breakdown also tells you which path you're + # paying for. + timings = result.get("timings") or {} + if timings: + parts: list[str] = [] + order = ( + ("cache_lookup_ms", "cache"), + ("search_ms", "search"), + ("context_ms", "context"), + ("llm_ms", "llm"), + ("persist_ms", "persist"), + ) + for key, label in order: + v = timings.get(key) + if isinstance(v, (int, float)) and v > 0: + parts.append(f"{label} {v / 1000:.2f}s") + total = timings.get("total_ms") + if isinstance(total, (int, float)): + parts.append(f"**total {total / 1000:.2f}s**") + if parts: + lines.append("timings: " + " · ".join(parts)) + # Reference-frame notes (Ticket #000002). When detect_frame # classified the query as `reference`, surface the named work # so an operator knows the substrate routed to a fictional