Skip to content

fix(email): make --trace write a real trace on the daemon-relay path - #3346

Draft
github-actions[bot] wants to merge 1 commit into
mainfrom
autofix/issue-3345
Draft

fix(email): make --trace write a real trace on the daemon-relay path#3346
github-actions[bot] wants to merge 1 commit into
mainfrom
autofix/issue-3345

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

gaia email --trace was advertised in --help but did nothing — the flag was parsed and then dropped, so you got no trace file, no warning, and no way to capture what the email agent actually did during a run. It now writes a JSON trace of the run into the current directory (email_trace_<timestamp>_<run-id>.json) and prints the path, so anyone benchmarking or debugging the email agent can see the full request, every tool call and result, and the final answer.

Closes #3345

Test plan

  • python -m pytest tests/unit/test_agent_query.py tests/unit/test_email_cli.py -q passes
  • python util/lint.py --all passes
  • With Lemonade running and the Google connector configured: gaia email --trace -q 'summarize my unread emails' prints 📝 trace written to … and the named file contains the tool calls the run made
  • gaia email -q 'summarize my unread emails' (no --trace) leaves no JSON file behind
  • gaia email --trace -i writes one trace file per turn

⚠️ Needs manual validation — unit tests cover the trace writer against a
stubbed relay, but nothing here exercises a real daemon + email sidecar. A
maintainer should run the three gaia email steps above against a live
Lemonade + Google connector before merging.

🔍 Technical details

Root cause: gaia email -q relays through the daemon via
gaia.daemon.agent_query.run_query() (src/gaia/cli.py:4633), whose signature had no
trace parameter. args.trace came from the shared parent_parser, was never read on
this path, and never reached the request.

What changed:

  • src/gaia/daemon/agent_query.pyrun_query(..., trace=False). There is no
    in-process agent to introspect on the relay path, so the canonical SSE stream is the
    trace: _consume() takes an optional sink and _write_trace() dumps
    {agent_id, run_id, recorded_at, request, terminal_type, final_answer, error_detail, events}
    to ./<agent>_trace_<timestamp>_<run_id>.json. run_id is in the filename so two REPL
    turns in the same second can't collide. The path is echoed on stderr
    (ConsoleRenderer.on_trace_written) and returned as QueryOutcome.trace_path.
  • Traces are written on the failure paths too — Ctrl-C, and a stream that ends without a
    terminal event (the CLI-synthesized error frame is recorded alongside the real
    events). A trace is most useful exactly when the run broke.
  • Per the fail-loudly rule, an unwritable CWD raises a DaemonError naming the path and
    the fix rather than skipping the file.
  • src/gaia/cli.pyhandle_email_command reads args.trace and passes it to
    run_query; _email_interactive takes trace and writes one file per turn.
  • Docs: --trace row added to the email options tables in docs/reference/cli.mdx and
    docs/guides/email.mdx, with a note that a trace contains real email content.

Verified: 4 new tests in tests/unit/test_agent_query.py (file written with the full
event stream, nothing written without the flag, partial capture when the stream has no
terminal event) and 1 in tests/unit/test_email_cli.py (the flag reaches run_query);
37 tests across the email/agent-query suites pass. Also exercised run_query(trace=True)
against a stubbed relay to confirm the file contents. Black + isort clean; pylint's 2
remaining errors are pre-existing and unrelated (PIL.Image.LANCZOS, a try-except-raise
in the UI router).

Only gaia email uses run_query today, so no other CLI surface changes behavior.

gaia email --trace was parsed and dropped: the query path relays through the
daemon, and run_query() had no trace parameter, so the flag silently behaved
like omitting it. The relay path has no in-process agent to introspect, so the
canonical SSE stream is the trace — run_query now records the request plus every
status/token/tool_call/tool_result/final event to
./<agent>_trace_<timestamp>_<run_id>.json, echoes the path on stderr, and
returns it as QueryOutcome.trace_path. Interrupted runs and streams that die
without a terminal event still write what they saw, which is when a trace is
worth most.

Closes #3345
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]: gaia email --trace is a no-op on the daemon-relay query path

0 participants