#!/usr/bin/env python3 """Plot the production trace inter-turn gap distribution. Inter-turn gap = next_turn.request_ready_time_ms - prev_turn.request_end_time_ms (i.e. T_external: the wall-clock between a turn finishing and the next turn of the same session arriving). This is the tool-call latency + any pause, not the conflated arrival-to-arrival interval. Data is pre-computed on dash0 by scripts/agentic_gap.py and cached under ``analysis/characterization/data/agentic_inter_turn_gap.json`` (~23 KB). """ from __future__ import annotations import argparse import json from pathlib import Path import matplotlib.pyplot as plt import numpy as np def load(cache_path: Path) -> tuple[np.ndarray, np.ndarray, dict]: d = json.loads(cache_path.read_text()) samples = d["cdf_samples"] xs = np.array([s["gap_s"] for s in samples]) ys = np.array([s["rank_pct"] for s in samples]) return xs, ys, d def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( "--agentic-data", default="analysis/characterization/data/agentic_inter_turn_gap.json", ) parser.add_argument( "--chatbot-data", default="analysis/characterization/data/chatbot_inter_turn_gap.json", ) parser.add_argument("--out", default="figs/f3a_inter_turn_gap.png") args = parser.parse_args() a_xs, a_ys, a_d = load(Path(args.agentic_data)) c_xs, c_ys, c_d = load(Path(args.chatbot_data)) fig, ax = plt.subplots(figsize=(10, 5.5)) ax.plot(a_xs, a_ys, color="#1f77b4", lw=2.4, label=f"agentic (n={a_d['n_gaps']:,} gaps, " f"{a_d['n_sessions']:,} multi-turn sessions, Qwen3-Coder)") ax.plot(c_xs, c_ys, color="#c44e52", lw=2.4, label=f"chatbot (n={c_d['n_gaps']:,} gaps, " f"{c_d['n_sessions']:,} multi-turn sessions, qwen3-max)") for d, color, side in [(a_d, "#1f4e79", "left"), (c_d, "#7a1d1d", "right")]: for pct, key in [(50, "p50")]: v = d["stats_s"][key] ax.scatter([v], [pct], color=color, s=55, zorder=5) xt = 8 if side == "left" else -110 yt = -10 if side == "left" else 8 ax.annotate(f"p50 = {v:.2g}s", xy=(v, pct), xytext=(xt, yt), textcoords="offset points", fontsize=10, color=color) refs = [ ("lmetric TTFT p90 = 15.7s", 15.7, "#888"), ("unified TTFT p90 = 7.3s", 7.3, "#444"), ] for label, v, color in refs: ax.axvline(v, color=color, ls=":", lw=1.2, alpha=0.7) ax.text(v * 1.05, 5, label, fontsize=8.5, color=color, rotation=90, va="bottom") ax.set_xscale("log") ax.set_xlim(0.05, 5000) ax.set_ylim(0, 102) ax.set_xlabel( "Inter-turn gap T_external (s, log scale) โ€” next.ready โˆ’ prev.end" ) ax.set_ylabel("Cumulative % of inter-turn intervals") ap = a_d["stats_s"] cp = c_d["stats_s"] af = a_d["fraction_below"] cf = c_d["fraction_below"] ax.set_title( f"Agentic vs chatbot inter-turn external gap โ€” agentic has a " f"sub-second tool-call mode chatbot lacks\n" f"agentic p50={ap['p50']:.2g}s, frac<1s={af['1.0s']*100:.0f}%, " f"frac<5s={af['5.0s']*100:.0f}% ยท " f"chatbot p50={cp['p50']:.2g}s, frac<1s={cf['1.0s']*100:.0f}%, " f"frac<5s={cf['5.0s']*100:.0f}%" ) ax.grid(True, which="both", alpha=0.3) ax.legend(loc="lower right", framealpha=0.92, fontsize=9) out_path = Path(args.out) out_path.parent.mkdir(parents=True, exist_ok=True) fig.savefig(out_path, dpi=150, bbox_inches="tight") print(f"wrote {out_path}") if __name__ == "__main__": main()