#10272·agno

[Bug] BaseRunOutputEvent.from_dict does not rebuild citations, so a restored event carries a dict instead of Citations

Author: BlueX888Created Sep 17, 2026Updated Sep 17, 2026

Description

BaseRunOutputEvent.from_dict (libs/agno/agno/run/base.py:249) rebuilds tool, tools, images, videos, audio, files, response_audio and image, but has no branch for citations, even though to_dict writes that field:

  • to_dict (base.py:185-189) writes self.citations.model_dump(exclude_none=True) - a plain dict.
  • from_dict never pops/validates citations, so the value passes through unchanged into the event. The resulting event has citations as a dict, and typing-aware access such as event.citations.urls raises AttributeError.

citations is the only entry of _HAND_SERIALIZED_FIELDS (base.py:88) that to_dict writes and from_dict does not rebuild.

The asymmetry is visible against the run-output path: RunOutput.from_dict (libs/agno/agno/run/agent.py:889-890) does Citations.model_validate(citations), and TeamRunOutput.from_dict does the same, so the same payload round-trips with a Citations object there and with a dict on the event.

Reachability: run_output_event_from_dict (libs/agno/agno/run/agent.py:609) dispatches to BaseRunOutputEvent.from_dict and is the parser for the AgentOS client SSE stream (libs/agno/agno/client/os.py:650, :736, and the team/workflow variants at :927, :1049, :1235, :1336), whose server counterpart frames events with event.to_json() (libs/agno/agno/os/utils.py:304, :308, :344). It is also the parser for the event list inside a stored run (RunOutput.from_dict -> run/agent.py:877, TeamRunOutput.from_dict -> run/team.py:979). Events that carry citations on these paths are built by create_run_content_event / create_run_completed_event (libs/agno/agno/utils/events.py:721, and the team variants), with libs/agno/agno/agent/_response.py:1460 passing citations=model_response_event.citations for streaming content events.

Steps to Reproduce

python
import json

from agno.models.message import Citations, UrlCitation
from agno.run.agent import RunContentEvent, run_output_event_from_dict

event = RunContentEvent(
    content="hello",
    citations=Citations(urls=[UrlCitation(url="https://example.com/a", title="A")]),
)

restored = run_output_event_from_dict(json.loads(event.to_json()))
print(type(restored.citations).__name__)
print(restored.citations.urls)
  1. Build a run event with a Citations object.
  2. Serialize it with to_json() and parse it back with run_output_event_from_dict.
  3. Read restored.citations.urls.

Agent Configuration (if applicable)

Not required to reproduce; the failure is in the event (de)serializer. It shows up with any agent or team whose model returns citations (web-search / RAG providers) when the events are streamed to an AgentOS client or persisted with stream_events=True and reloaded.

Expected Behavior

type(event).from_dict(event.to_dict()) and run_output_event_from_dict(json.loads(event.to_json())) should return a Citations instance on the event, the same way RunOutput.from_dict / TeamRunOutput.from_dict already do.

Concrete basis:

  • The field annotation is citations: Optional[Citations] on RunContentEvent (libs/agno/agno/run/agent.py:254), RunCompletedEvent (run/agent.py:281) and the team equivalents (libs/agno/agno/run/team.py:249, :276, :780), i.e. a model object, not a mapping.
  • Every other hand-serialized field of the same event class is rebuilt in from_dict; the entries immediately above and below the missing location (image, additional_input) both have their branch.

Actual Behavior

citations comes back as a dict and typed access fails:

to_dict citations: {'urls': [{'url': 'https://example.com/a', 'title': 'A'}]}
event  type: dict
event  AttributeError: 'dict' object has no attribute 'urls'
runout type: Citations
runout urls: url='https://example.com/a' title='A'

The last two lines are the same payload through RunOutput.from_dict, which validates the field.

Screenshots or Logs (if applicable)

Not applicable - the console output above is the full evidence.

Environment

- OS: macOS 26.6.2 (Darwin 25.6.0)
- Agno Version: 3.0.9 (editable install), commit a71c4d0 (main)
- External Dependency Versions: pydantic 2.13.5, pytest 9.1.1
- Additional Environment Details: Python 3.12.14

Possible Solutions (optional)

In BaseRunOutputEvent.from_dict, pop citations before construction and validate it, matching the surrounding branches:

python
citations = data.pop("citations", None)
if citations:
    data["citations"] = Citations.model_validate(citations)

Citations is already imported at the top of base.py (it is used by to_dict on the line above), so no import change is needed. Because BaseAgentRunEvent, BaseTeamRunEvent and BaseWorkflowRunOutputEvent all inherit from_dict from this class, one branch covers agent, team and workflow events.

Additional Context (optional)

  • Collision check: no existing issue or PR covers this field. The nearest open work is #10205 / the pending media fix, which is about model_validate coercing base64 media content strings into bytes for images/videos/audio/response_audio; it does not touch citations.
  • I am happy to open a PR with a round-trip regression test for the event path if that is welcome.
  • AI disclosure: this report was drafted with AI assistance (Claude Code). I reproduced and verified every command and output quoted above in a local checkout of the commit named in Environment.