Report the failures a pipeline runs into - #5618
Merged
Merged
Conversation
Errors travel upstream from the processor that raised them, and a session's failure history is not what arrives at the end of that journey. A processor that answers for a failure itself stops the error there: a service switcher that fails over reports nothing further, and neither does one holding a failed service in reserve. Read that way, a bot that spent a session failing over between three TTS providers looks like a bot that never had a problem. ErrorObserver reads each error where it is raised, so the ones that were recovered from are recorded alongside the ones that surfaced. An error is reported once, however many processors it passes through, and named for the processor that raised it rather than the one that passed it along. Each ErrorEvent carries what failed and what it costs: the message, the category the failure was attributed to, the exception type behind it where there was one, and whether the processor can still do its job — which separates a bad minute from the end of a capability.
markbackman
force-pushed
the
error-observer
branch
from
September 3, 2026 11:32
921e7bf to
dce4aed
Compare
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
markbackman
force-pushed
the
error-observer
branch
from
September 3, 2026 11:47
dce4aed to
2e0ad94
Compare
aconchillo
approved these changes
Sep 4, 2026
Contributor
|
LGTM! |
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
Adds
ErrorObserver, which reports every error a pipeline raises throughon_erroras anErrorEvent.PipelineWorkeralready reports errors throughon_pipeline_error, and hands its handler the same frame this observer reads: the message, the category, the exception, and the processor that raised it. The difference is which errors get there. An error travels upstream from the processor that raised it, and a processor that answers for a failure itself stops it on the way:ServiceSwitcherswallows errors from the services it holds in reserve, and swallows an active service's error whenever the strategy responds by failing over. A bot that spent a session failing over between three TTS providers looks, throughon_pipeline_error, like a bot that never had a problem.This observer reads each error at the push that raises it, before anything downstream can answer for it, so a session's failure history holds the recoveries as well as the failures that surfaced. Use
on_pipeline_errorto hear about the errors the pipeline as a whole had to deal with, and this to hear about all of them.push_error— a separate error, and a second event, which is what happened.push_errorsettled them. AnErrorFrameassembled by hand carries neither, so it is attributed to the processor pushing it, withErrorCategory.UNKNOWNfor its cause. The field is neverNone, so a query never has to case on it.processor_usableis read after the verdict.push_error_framesettles usability before the frame travels, so the event carries the state the error left the processor in. This is what separates a bad minute from the end of a capability, and it is the non-deprecated answer to the questionfatalused to ask —fatalis removed in 2.0.0 and deliberately absent from the record.{ "message": "the provider said no", "category": "connectivity", "exception_type": "ConnectionError", "processor": "stt", "processor_usable": true, "timestamp": 1756944000.0 }exception_typeis there to group by: messages carry the particulars of a single occurrence, and are usually too specific to aggregate.An exception that crashes a processor task never becomes an
ErrorFrame, and so is outside what this observer oron_pipeline_errorcan see.Testing
uv run pytest tests/test_error_observer.py— 8 tests, covering the observer in a running pipeline (origin attribution, category inferred from the exception, usability after a permanent failure) and against frames pushed directly (reported once however far it travels, each error its own event, hand-assembled frames, non-error frames ignored).