[Bug] BaseRunOutputEvent.from_dict does not rebuild citations, so a restored event carries a dict instead of Citations
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) writesself.citations.model_dump(exclude_none=True)- a plaindict.from_dictnever pops/validatescitations, so the value passes through unchanged into the event. The resulting event hascitationsas adict, and typing-aware access such asevent.citations.urlsraisesAttributeError.
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
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)- Build a run event with a
Citationsobject. - Serialize it with
to_json()and parse it back withrun_output_event_from_dict. - 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]onRunContentEvent(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.14Possible Solutions (optional)
In BaseRunOutputEvent.from_dict, pop citations before construction and validate it, matching
the surrounding branches:
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_validatecoercing base64 mediacontentstrings into bytes forimages/videos/audio/response_audio; it does not touchcitations. - 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.
Source: agno-agi/agno