Skip to content

fix: restore citations in BaseRunOutputEvent.from_dict - #10273

Open
BlueX888 wants to merge 1 commit into
agno-agi:mainfrom
BlueX888:fix/prep-event-from-dict-citations-left-as-dict
Open

BlueX888 wants to merge 1 commit into
agno-agi:mainfrom
BlueX888:fix/prep-event-from-dict-citations-left-as-dict

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Summary

BaseRunOutputEvent.to_dict writes citations as a plain dict (self.citations.model_dump(exclude_none=True), libs/agno/agno/run/base.py:185-189), but from_dict (libs/agno/agno/run/base.py:249) had no citations branch, so the value was passed straight into the event. A round-tripped event therefore carried a dict, and typed access such as event.citations.urls raised AttributeError: 'dict' object has no attribute 'urls'.

citations was the only entry of _HAND_SERIALIZED_FIELDS (base.py:88) that to_dict writes and from_dict does not rebuild; the neighbouring hand-serialized fields (image, additional_input) both have their branch. RunOutput.from_dict (libs/agno/agno/run/agent.py:889-890) and TeamRunOutput.from_dict (run/team.py:1037-1038) already call Citations.model_validate, so the same payload round-tripped fine on the run output and broke on the event.

Reachable through run_output_event_from_dict (run/agent.py:609), which parses AgentOS client SSE events (agno/client/os.py:650, :736, :927, :1049, :1235, :1336) and the event list inside a stored run (run/agent.py:877, run/team.py:979).

Fixes #10272

Changes

  • libs/agno/agno/run/base.py: pop citations in BaseRunOutputEvent.from_dict and rebuild it with Citations.model_validate, matching the adjacent image/images/audio branches and RunOutput.from_dict. No import change: Citations is already imported in the module and used by the to_dict branch above. BaseAgentRunEvent, BaseTeamRunEvent and BaseWorkflowRunOutputEvent all inherit this from_dict, so agent, team and workflow events are covered.
  • libs/agno/tests/unit/run/test_run_events.py: new test_run_content_event_includes_citations round-trips a RunContentEvent through to_dict/from_dict and through run_output_event_from_dict(json.loads(event.to_json())), asserting the restored citations is a Citations and reading .urls[0].

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Improvement
  • Model update
  • Other:

Testing

Dev venv from ./scripts/dev_setup.sh (agno + agnoctl editable), base commit a71c4d0.

Reproduction before the fix (source reverted to origin/main, new test kept) —
pytest libs/agno/tests/unit/run/test_run_events.py::test_run_content_event_includes_citations:

>       assert isinstance(reconstructed.citations, Citations)
E       AssertionError: assert False
E        +  where False = isinstance({'urls': [{'url': 'https://example.com/a', 'title': 'A'}]}, <class 'agno.models.message.Citations'>)
libs/agno/tests/unit/run/test_run_events.py:234: AssertionError
1 failed

The same payload through RunOutput.from_dict returns a Citations on the unpatched tree, which is the asymmetry this fixes.

After the fix:

  • pytest libs/agno/tests/unit/run/test_run_events.py → 17 passed
  • pytest libs/agno/tests/unit/run → 234 passed, 1 warning
  • pytest libs/agno/tests/unit -q --continue-on-collection-errors → 15298 passed, 229 failed, 497 skipped, 397 errors. The same command on the same tree with only the source change reverted gives 230 failed / 15297 passed, and diffing the two FAILED lists leaves exactly one difference: this new test. The remaining failures and errors are pre-existing on a71c4d0 in this environment (optional deps not installed: surrealdb, weaviate, pymongo, redis, anthropic, numpy, bs4, Google client libraries, ...); I did not triage them individually.
  • ./scripts/format.sh → clean for all targets (ruff format + import sort). Note: it also reformatted libs/agno/tests/unit/vectordb/test_elasticsearch.py, a pre-existing formatting drift on main unrelated to this change, which I reverted. ruff format --check on the two changed files: clean.
  • ./scripts/validate.sh → ruff check clean, mypy — Success: no issues found in 1055 source files, agnoctl clean, cookbook pattern check 0 violations.

Checklist

  • Code complies with style guidelines
  • Ran format/validation scripts (./scripts/format.sh and ./scripts/validate.sh)
  • Self-review completed
  • Documentation updated (comments, docstrings) — not applicable, no public API or docstring change
  • Examples and guides: Relevant cookbook examples have been included or updated (if applicable) — not applicable, no cookbook change
  • Tested in clean environment
  • Tests added/updated (if applicable)

Duplicate and AI-Generated PR Check

  • I have searched existing open pull requests and confirmed that no other PR already addresses this issue
  • If a similar PR exists, I have explained below why this PR is a better approach — no similar PR exists
  • Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) — yes, see disclosure

Additional Notes

AI disclosure: this change was generated with Claude Code and reviewed, tested and verified by me before submission, as required by CONTRIBUTING.md (Pull Request Guidelines, item 5). Every command and output quoted above was run in a local checkout; the issue #10272 body carries the same disclosure.

The adjacent base64/media field handling in BaseRunOutputEvent.from_dict is tracked separately (#10205) and is not touched here.

Collision check before writing the fix: no open PR in the repository modifies libs/agno/agno/run/base.py, and no existing issue or PR covers citations in the event round-trip.

to_dict serializes citations as a plain dict, but from_dict had no
citations branch, so a round-tripped event kept a dict and
event.citations.urls raised AttributeError. RunOutput.from_dict already
validates it; do the same for run events.
@BlueX888
BlueX888 force-pushed the fix/prep-event-from-dict-citations-left-as-dict branch from 29957b0 to 09d5925 Compare October 25, 2026 16:13

This branch has not been deployed

No deployments
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] BaseRunOutputEvent.from_dict does not rebuild citations, so a restored event carries a dict instead of Citations

1 participant