feat(agent): add opt-in live event streaming to AgentBuilder#161
Merged
niclaslindstedt merged 1 commit intomainfrom Apr 18, 2026
Merged
feat(agent): add opt-in live event streaming to AgentBuilder#161niclaslindstedt merged 1 commit intomainfrom
niclaslindstedt merged 1 commit intomainfrom
Conversation
Library callers of `AgentBuilder::exec()` previously lost the live per-step stderr tail that a shell-out-to-`zag` wrapper produced — `exec` captured everything into `AgentOutput` silently with no way to observe the session while it ran. - Adds `AgentBuilder::stream_events_to_stderr(ListenFormat)` for a drop-in replacement for the old live tail, and `AgentBuilder::on_log_event(fn)` for custom consumers. - Lifts `SessionLogCoordinator` startup into the builder behind `SessionLogMode` (`Disabled` / `Auto` / `External`) so library users get the JSONL session log for free when they opt in. - Populates `AgentOutput::log_path` so callers that prefer to tail the log themselves (via `zag_orch::listen::tail_session_log`) can find the file without guessing. - Taps `SessionLogWriter::emit` with an optional per-event callback; fires outside the writer mutex so subscribers can't deadlock. - Moves `ListenFormat` + `format_event_*` helpers from `zag-orch` down into `zag-agent` (dep direction is `zag-orch -> zag-agent`). `zag-orch::listen` re-exports them for backward compatibility. Default for `AgentBuilder` is `SessionLogMode::Disabled`, so existing Rust library consumers see no behaviour change. The CLI path (`AgentFactory`-based) is untouched and continues to manage its own coordinator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A zag user reported that migrating from a shell-out-to-
zagwrapper (whichtailed the CLI's stderr) to the in-process
AgentBuilder::exec().awaitAPIremoved the live per-step agent output they used to see in their terminal.
execcaptured everything intoAgentOutputsilently;output.resultwasstill there for saves/dependency injection, but there was no intermediate
stream. Their migration note said "A follow-up can restore live visibility
by tailing the zag session log in parallel" — and for that plan to work the
library had to start writing a session log when
execruns. It didn't.This PR makes live visibility a one-line opt-in on the builder and aligns
the library with CLAUDE.md's "libraries are the source of truth" rule.
Library-side usage
Or with a custom callback:
Changes
zag-agent/src/builder.rs):enable_session_log(bool)/session_log(SessionLogMode)— opt-inon_log_event(fn)— generic per-event callbackstream_events_to_stderr(ListenFormat)— convenience tail-to-stderrstream_show_thinking(bool)— include reasoning eventsSessionLogMode—Disabled(default) /Auto/External(coord).Externallets advanced callers (zag-serve, review/plan handlers) handin a pre-built coordinator so we don't double-start.
SessionLogWriter::emit— single chokepoint, firesoutside the writer mutex so subscribers can't deadlock or block peer
emitters.
AgentOutput::log_path— newOption<String>populated whenlogging ran so callers can
tail_session_logthe file themselves. Marked#[serde(default, skip_serializing_if = "Option::is_none")]so all sixbindings stay JSON-compatible.
ListenFormat+format_event_*fromzag-orch/src/listen.rsdown into
zag-agent/src/listen.rs. Dep direction iszag-orch → zag-agent, so the builder couldn't reach them before.zag-orch::listenre-exports the moved items for backward compatibility.
live_adapter_for_providerfromzag-cliintozag-agent(the per-provider adapters already lived there).
Backwards compatibility
SessionLogModeforAgentBuilderisDisabled, so existinglibrary consumers see no side effects unless they opt in.
AgentFactory+ own coordinator) is untouched.zag-servepath (AgentFactorydirect) is untouched.zagCLI — unaffected. They'll silently ignore the new
log_pathJSON field.Test plan
make build— workspace compilesmake clippy— zero warnings (cargo clippy --workspace --all-targets -- -D warnings)make test— all 1039 tests passzag-agent/src/mock_integration_tests.rs:test_exec_without_session_log_leaves_log_path_nonetest_exec_with_enable_session_log_populates_log_pathtest_on_log_event_receives_lifecycle_eventstest_on_log_event_implicitly_enables_session_logtest_stream_events_to_stderr_implicitly_enables_session_logzag listen --latestin a second terminal while abuilder-driven exec is running and confirm the log is identical before
and after this PR.
AgentBuilder::new().provider("claude").stream_events_to_stderr(ListenFormat::Text).exec(...)and confirm per-step events appear live on stderr.
https://claude.ai/code/session_019yzV2V34EeL9TJjtw5PaRL