refactor(runtime): replace PersistentRuntime decorator with EventObserver#2552
Merged
dgageot merged 2 commits intodocker:mainfrom Apr 28, 2026
Merged
Conversation
4e82c39 to
186782f
Compare
rumpl
previously approved these changes
Apr 28, 2026
186782f to
9806a83
Compare
rumpl
previously approved these changes
Apr 28, 2026
…e decorator
Replaces the wrapping-decorator pattern (LocalRuntime + 200-line
PersistentRuntime + Runtime interface triplet) with a small
observer chain owned by LocalRuntime itself.
- New EventObserver interface with three lifecycle hooks:
OnRunStart(ctx, sess), OnEvent(ctx, sess, e), OnRunEnd(ctx, sess).
Documented contract: synchronous, in registration order, no
error return; observers log internally and never block the
run loop on long work.
- New WithEventObserver(o) Opt for layering custom observers
(telemetry, audit, metrics, A2A forward, ...). Nil observers
are silently ignored so a conditional caller can pass nil
without a guard.
- LocalRuntime.RunStream fans events through the observer chain
between the inner producer goroutine and the consumer's
channel. Fast-path when no observers are registered: the inner
channel is returned directly, so a no-observer runtime pays
exactly the overhead it did before this commit.
- PersistenceObserver replaces persistent_runtime.go's handleEvent
state machine. Same per-event-type switch, same sub-session and
SessionScoped-mismatch filters, same streamingState lifecycle —
just decoupled from runtime construction. Auto-registered as
the first observer in NewLocalRuntime (so user-supplied
observers see the post-persistence view).
- pkg/runtime/persistent_runtime.go is gone (194 lines).
- runtime.New() becomes a thin alias for NewLocalRuntime()
returning the Runtime interface, kept for source compatibility
with the ~10 callers in cmd/, examples/, e2e/, pkg/chatserver.
- cmd/root/run.go drops its (*runtime.PersistentRuntime) type
assertion in favour of localRt.TitleGenerator(), which has
been on the Runtime interface all along.
Net effect: 200 lines deleted, the dual-runtime-class confusion
gone, and OTel/audit/metrics observers are now one struct + one
WithEventObserver call away. Existing PersistenceObserver tests
are exercised by every runtime test that runs a stream (the
default in-memory store keeps the auto-registration path covered).
New observer_test.go pins the lifecycle contract: OnRunStart and
OnRunEnd fire exactly once, OnEvent sees every event in order, and
multiple observers fire in registration order. The fnObserver
test helper makes future observer-related tests one-liners.
Assisted-By: docker-agent
Subset of PR docker#2552 commit 9806a83 that survives rebase: drops EventObserver.OnRunEnd, moves OnRunStart fan-out into observe(), drops the no-observers fast path, makes TokenUsageEvent implement SessionScoped. The approval-source ceremony cleanup no longer applies; that surface moved to pkg/runtime/toolexec on origin/main. Assisted-By: docker-agent
9806a83 to
73f8a28
Compare
rumpl
approved these changes
Apr 28, 2026
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
Collapses the
Runtimeinterface /LocalRuntime/PersistentRuntimetriplet (~200 lines of wrapping decorator) into a single runtime type with an observer chain. Persistence becomes the stockEventObserverimplementation, auto-registered against the configured session store. Custom observers (telemetry, audit, metrics, A2A forward, transcript writers, …) now compose alongside viaWithEventObserverinstead of forking the runtime.Net change after both commits: +664 / −370 = +294 lines including new tests and rich doc comments. The actual
pkg/runtimesource is smaller: the deletedpersistent_runtime.go(194 lines) is replaced byobserver.go(81) +persistence_observer.go(170) = 251 lines, but those carry the new public API and ~80 lines of Go-doc.Commits
1.
refactor(runtime): introduce EventObserver, collapse PersistentRuntime decorator(70d4c78e7)EventObserverinterface (originally three methods:OnRunStart/OnEvent/OnRunEnd; trimmed in commit 2).WithEventObserver(o)opt for layering custom observers. Nil observers silently ignored so a conditional caller can pass nil without a guard.LocalRuntime.RunStreamfans events through the observer chain between the inner producer goroutine and the consumer's channel.PersistenceObserverreplacespersistent_runtime.go'shandleEventstate machine. Same per-event-type switch, same sub-session andSessionScoped-mismatch filters, same streaming-message lifecycle — just decoupled from runtime construction. Auto-registered as the first observer inNewLocalRuntimeso user-supplied observers see the post-persistence view.pkg/runtime/persistent_runtime.godeleted (194 lines).runtime.New()becomes a thin alias forNewLocalRuntime()returning theRuntimeinterface, kept for source compatibility with the ~10 callers incmd/,examples/,e2e/,pkg/chatserver.cmd/root/run.godrops its(*runtime.PersistentRuntime)type assertion in favour oflocalRt.TitleGenerator(), which has been on theRuntimeinterface all along.observer_test.gopins the lifecycle contract.2.
refactor(runtime): trim observer + approval-source ceremony(4e82c39fd)Five small cleanups, no feature changes; net −60 lines on top of the introduction commit:
EventObserver.OnRunEndOnRunStartfan-out intoobserve()RunStreamloses its bespoke iteration loop.observe()PersistenceObserveris auto-registered against the default in-memory store, so the branch was dead in practice.TokenUsageEventimplementSessionScoped(one-method addition)PersistenceObserver.OnEvent; the existingSessionScopedfilter at the top ofOnEventnow catches it uniformly with every other cross-session event.permissionCheckerallowSourceFor/denySourceForwere fragile string-equality lookups against magic strings duplicated inpermissionCheckers(). Replaced withdisplayName/allowSource/denySourcefields populated at construction time — typos caught at compile time.Plus minor: inline
streamingStatereset, foldMessageAddedEvent's two error-logging branches into one, dropfnObserver.onEndfrom the test helper, replace the now-obsolete approval-source mapper test with one that asserts on the struct fields.Why this matters
Before:
After:
The dual-runtime-class confusion is gone, and OTel/audit/metrics observers are now one struct + one
WithEventObservercall away rather than a fork of the wrapping decorator.Verification
go build ./...✅go vet ./...✅go test -short ./pkg/...✅ (every package green)go test ./e2e/... -run TestRuntime_MultiAgent✅ — the test that specifically exercises the persistence + sub-session-filter contract that PR fix(#2053): prevent corrupted session history from breaking Anthropic API calls #2058 introduced; same behaviour preserved.observer_test.goandon_tool_approval_decision_test.gopin the new contracts.Assisted-By: docker-agent