Repository navigation
fix(core): process inputs before pre_run_step in the streaming entry points - #1018
xihaian251 wants to merge 1 commit into
Conversation
…points stream_result() and astream_result() dispatched pre_run_step from the first try block, before _process_inputs() had run, then delegated a non-streaming halt to _step()/_astep() -- which processed the same dict again. So on those two entry points the hook saw the caller's raw dict (undeclared keys present, injected __context/__tracer absent), a rejected step was announced and then closed, and the dependency factory ran twice with the first object discarded. Halting on a non-streaming action now hands the whole step to _step()/_astep() with _run_hooks=True, the same way step()/astep() run it, and the streaming branch processes the inputs before firing pre_run_step. _step and _astep are untouched, so this does not overlap the astep fix in apache#980/apache#995. Consequences, all measured: - _process_inputs runs exactly once per action execution on all six entry points, which also closes apache#1007 (this covers apache#1008's own regression test); - pre_run_step and the action receive the same processed dict, so TracerFactoryContextHook no longer builds a second TracerFactory; - a step rejected by input validation fires neither pre_run_step nor post_run_step and is no longer written to the tracker, matching step(). The error handler that apache#968 added still calls post_execute exactly once, and post_run_step comes from the delegate's own finally instead of the entry, so no pair fires twice and no exception type changes. Generated-by: Claude (Claude Opus 5) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
enekoreto
left a comment
There was a problem hiding this comment.
Thanks for filing #1017. The streaming part looks right to me. I checked out the branch (9b53e4e) on main (222b32c): with the reproduction from #1017, stream_result now gives the hook ['__context', 'value'], and with a reserved key or a missing input, stream_result and the streaming astream_result raise ValueError without firing pre_run_step or post_run_step, also with the local tracker attached.
But CI's test command fails on this branch alone: 14 of the new tests fail, on Python 3.13 and 3.10. They're the astep cases and the non-streaming astream_result/astream_iterate ones, which you describe as #979's scope. I merged the branch with #980 and with #995: both merge cleanly and tests/core passes. If this lands first, maybe mark those cases xfail pointing to #979, so main doesn't go red.
For the same reason, astream_result halting on a non-streaming action isn't fixed by this PR alone: the #1017 reproduction still shows ['noise', 'value'] there, and a missing input still fires pre_run_step and post_run_step once. So #1017 is only fully closed together with #980 or #995.
I ran this on Python 3.13 on macOS, and the tests also on 3.10.
|
Thanks for checking the branch on both Python versions and for testing the combinations with #980 and #995. I agree with the scope split: #1018 fixes the streaming-action paths in Since your combined runs show that #1018 passes Could you advise which of #980 or #995 should be the canonical fix for #979 to land first? I can then update #1018 against the resulting If the project prefers #1018 to land first, I can instead mark only the specific overlapping cases as temporary Thanks again for the careful review and independent testing. |
Application.stream_result()andApplication.astream_result()announce a step topre_run_stepbefore_process_inputs()has run, so the hook receives the caller's raw dictionary rather than the one the action gets.step()already does this in the right order.Why are these changes needed?
Both streaming entry points dispatch
pre_run_stepfrom the firsttryblock withinputs=inputs, then call_process_inputs()in the second one, then delegate a non-streaming halt to_step()/_astep()— which processes the same dictionary again. That single mis-ordering is behind three separate symptoms:__context/__tracerdo not. This is not only cosmetic —TracerFactoryContextHook.pre_run_stepreadsinputs["__tracer"]and, when absent, builds its ownTracerFactory, so on these paths the tracing context and the action run against two different objects.step()shares one._process_inputs()raises for reserved__-prefixed keys and missing required inputs, but by thenpre_run_stephas fired and the error handler firespost_run_steptoo, so the tracker records a step that never started.action_inputs, hands the rawinputsto the delegate, and throws the first result away — the injected dependency is constructed twice.Changes
Halting on a non-streaming action now hands the step to
_step()/_astep()with_run_hooks=True, from inside the firsttryblock — i.e. exactly howstep()/astep()run it. The delegate already processes the inputs before firingpre_run_step, so the entry stops firing the hook on that path at all, and stops processing inputs there: one pass, one announcement, one object.For the streaming branches,
_process_inputs()moves abovepre_run_stepand the hook is handedaction_inputs— the same dictionary the streaming runners are about to receive._step()and_astep()are not touched, so this does not overlap theastep()fix in #980/#995. No new parameter or internal flag is introduced; the diff removes more lines than it adds.pre_run_step's docstring (burr/lifecycle/base.py) now states whatinputsholds, since the behaviour was previously documented only by example. There is no CHANGELOG file in this repo, so the title carries the release-note text.How I tested this
New file
tests/core/test_lifecycle_input_semantics.py(69 cases, 10 skipped where an axis does not apply) coversstep,astep,stream_result,astream_result,stream_iterate,astream_iterate× sync/async/streaming actions × sync/async hooks × tracking on/off. It asserts four properties, none of which compare returned values:_process_inputscall count per action execution (wrapping patch);pre_run_step/post_run_stepcounts and their order relative to processing;__contextthe hook receives and the one the action receives —_context_factoryreturns a fresh instance per call, so identity is a direct public-API probe for "built once", and nothing private is reached into;stream_stateis left empty.Results:
main(222b32c3)The 14 remaining are all
astep()/astream_result()-on-a-non-streaming-action cases caused by_astepfiringpre_run_stepbefore processing — #979's own scope, owned by #980/#995, deliberately not touched here.tests/core+tests/tracking: 448 passed, 4 failed — the same four as onmain(test_graphviz_display.py,dotnot on PATH on this machine). #1008's own regression test (test_streaming_entry_point_injects_dependency_once) fails 2/2 onmainand passes 2/2 on this branch, so #1007 is covered here. Verified identically on Python 3.13.1 and 3.12.14 (in the current CI matrix; 3.9 is gone). black 23.11.0 / isort 5.12.0 / flake8 6.1.0 with the repo's own pre-commit args,git diff --check, andscripts/check_asf_headers.pyall clean.Negative controls: re-adding the entry's own
post_run_stepon top of the delegate's (the one way this change could double-announce) turns 14 failures into 27, caught by the pairing assertions; reverting the hoist turns 14 into 20, caught by the identity assertions. Without the pairing assertions the double-announce mutation passes unnoticed, which is why they are in the suite.Notes
pre_run_steporpost_run_step, and is therefore no longer written to the tracker. That is whatstep()already does, and the same change fix(core): process inputs before astep lifecycle hooks #980/fix(core): process and validate inputs before pre_run_step in astep (#979) #995 make forastep(); it does need a release-note line. Exception type and message are unchanged, and thepost_executehandler that fix: run post execute hooks when stream_result fails before streaming #968 added still runs exactly once.__context/__traceron these paths.step()has always passed them, so this aligns three entry points with one existing behaviour rather than adding a new exposure._process_inputs()below the non-streaming branch. This PR moves it above the hook, which fixes symptoms 1–3 at once and covers fix(core): process streaming inputs once #1008's test, so fix(core): process streaming inputs once #1008 can be dropped in favour of this one. I have noted it there and will leave whichever the maintainers prefer.post_run_stepfiring withoutpre_run_step, andTracerFactoryContextHook.post_run_stepindexestoken_pointer_map[(app_id, sequence_id)]with a bare subscript, which then replaces theValueErrorwith aKeyError— the astream_result() masks action exceptions with a tracker KeyError when the action is non-streaming #999 symptom again through a different hook. Measured; mentioned so nobody has to rediscover it.Checks
AI assistance: this change was drafted and verified with an AI coding assistant (Claude, via Qoder); the analysis, measurements and code are mine to stand behind and I have run every number reported above. Commit trailers record the same.