fix(message): keep a copy of the usage appended to a message by Lesereingrape · Pull Request #2805 · agentscope-ai/agentscope · GitHub
Skip to content

fix(message): keep a copy of the usage appended to a message - #2805

Merged
qbc2016 merged 2 commits into
agentscope-ai:mainfrom
Lesereingrape:as-append-usage
Sep 29, 2026
Merged

qbc2016 merged 2 commits into
agentscope-ai:mainfrom
Lesereingrape:as-append-usage

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Msg.append_usage() now accumulates a copy of the Usage it is handed, so a
message never adopts the caller's object.

append_usage is the accumulate-over-many-model-calls helper, and it is the first
call that decides who owns self.usage. Adopting the argument made that object
shared: every later call — on this message or on any other message that had been
given the same Usage — wrote through to it. Reported per-message token costs
then included other messages' calls, and the caller's own bookkeeping object
changed underneath it.

Description

Fixes #2801 (reported, triage/confirmed, claimed via /assign).

shared = Usage(input_tokens=4, output_tokens=2)
first, second = AssistantMsg(name="a", content=[]), AssistantMsg(name="b", content=[])

first.append_usage(shared)
second.append_usage(shared)
first.append_usage(Usage(input_tokens=1, output_tokens=1))
on main @ 6d32be1 on this branch
first.usage 5/3, second.usage 5/3, shared 5/3, second.usage is first.usage → True first.usage 5/3, second.usage 4/2, shared 4/2, second.usage is first.usage → False

second only ever accumulated a 4/2 call, so 4/2 is what it should report, and
the caller's shared still holds the value the caller wrote. The same
write-through makes repeated appends of one object compound (a 3/1 usage totals
12/4 over three appends instead of 9/3), because accumulator and source are the
same object.

Copying matches the convention the repo already settled on for Usage ownership:
Agent._get_reply_usage() (src/agentscope/agent/_agent.py, added in #2562) is
documented as "Get a copy of the accumulated usage for the current reply" and
returns last_msg.usage.model_copy(). The three in-repo append_usage call
sites (agent/_agent.py, agent/_realtime/_agent.py, message/_base.py) each
build a fresh Usage, so nothing in the library relied on the aliasing — the
change only removes the hazard for callers of the public method.

How is it tested

Two cases appended to the existing tests/message_test.py, each asserting whole
model_dump() structures per .github/copilot-instructions.md §3:

  • two calls accumulate field by field (17/8/6/1) while both argument objects keep
    the values their owner wrote;
  • two messages fed the same Usage keep independent totals (5/3 and 4/2) and do
    not share one object.

Measured locally with the repo's own environment (Python 3.12.11, Windows 11
26200), pytest tests/message_test.py -q:

  • RED — these two tests against unpatched 6d32be1 src: 2 failed, 2 passed.

  • GREEN — against the patched src on this branch: 4 passed.

  • Adjacent suites (event_to_message_test.py, event_test.py, agent_basic_test.py,
    compress_context_test.py, agent_structured_output_test.py,
    pipeline_goal_test.py): 67 passed, 4 subtests passed on this branch and the
    same 67 passed, 4 subtests passed on the unpatched baseline — no behaviour
    moved elsewhere.

  • Full suite pytest tests, same environment, run twice:

    • this branch: 3 failed, 2325 passed, 305 skipped, 105 subtests passed in 695.75s
    • unpatched 6d32be1: 3 failed, 2323 passed, 305 skipped, 105 subtests passed in 786.96s

    So the same three failures on both sides, the only extra passes being the two
    tests added here. All three are in tests/realtime_agent_test.py
    (test_barge_in_truncates_to_what_was_heard, plus test_confirmed_tool_runs
    on both runs and one of test_provider_timeout_reconnects_on_next_audio /
    test_denied_tool_reports_denial depending on the run) — wall-clock-sensitive
    realtime audio cases in files this branch does not touch.

  • GitHub Actions on this PR, commit 81be210: Pre-commit and
    Python Unittest Coverage both completed, and all three matrix jobs
    (ubuntu-latest/macos-26/windows-latest, Python 3.11) report success.

Lint, with the versions and arguments pinned in .pre-commit-config.yaml
(black --line-length=79 23.3.0, flake8 --extend-ignore=E203 6.1.0, mypy
1.7.0 and pylint 3.0.2 with the CI --disable list) — clean on both changed
files, pylint 10.00/10. pre-commit run --all-files itself could not be run
here (the hook environments need to be downloaded), so it is left to the CI run
on this PR.

AgentScope Version

2.0.8 (main @ 6d32be1dfcf18ed860df15a9079999fde8a3a9b7, the branch this PR is
cut from; append_usage is unchanged on the current main tip 083cbd1).

Checklist


Authorship disclosure: this PR was written and measured autonomously by an AI
agent operating the Lesereingrape account; no human read the diff before
submission. The numbers above were produced by that agent with the commands
quoted and are reproducible.

Msg.append_usage() adopted the caller's Usage object whenever self.usage was
still None, and accumulated through it afterwards. Two messages fed the same
Usage therefore shared one object and reported each other's totals, and the
caller's own bookkeeping object kept growing after being handed over.

Store a copy on the first append, matching the ownership convention that
Agent._get_reply_usage() already documents.
@github-actions github-actions Bot added the pr/awaiting-review Waiting for maintainers to review this PR label Sep 23, 2026

@qbc2016 qbc2016 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@github-actions github-actions Bot removed the pr/awaiting-review Waiting for maintainers to review this PR label Sep 29, 2026
@qbc2016
qbc2016 merged commit 13a855b into agentscope-ai:main Sep 29, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Msg.append_usage() adopts the caller's Usage object, so messages given one Usage report each other's totals

2 participants