Skip to content

fix(core): remap injected __context/__tracer for single-step actions - #1026

Open
xihaian251 wants to merge 4 commits into
apache:mainfrom
xihaian251:fix/single-step-injected-input-remap
Open

xihaian251 wants to merge 4 commits into
apache:mainfrom
xihaian251:fix/single-step-injected-input-remap

Conversation

@xihaian251

Copy link
Copy Markdown

Summary

Class-based actions that follow the documented pattern for reading app_id / partition_key /
sequence_id — declaring __context in inputs and in the run method
(docs/concepts/actions.rst, "add __context to the inputs property as well as the run method") —
fail at runtime on every single-step action:

TypeError: MyAction.run_and_update() got an unexpected keyword argument '__context'

This affects SingleStepAction, SingleStepStreamingAction, and therefore every @action-decorated
function, across step(), astep(), stream_result() and astream_result().

Root Cause

Python name-mangles a parameter written __context inside a class body to _MyClass__context. The
framework injects the dependency under the literal key __context, so the call site has to rename it.
_remap_injected_inputs does exactly that and is already applied in four runners (_run_function,
_arun_function, _run_multi_step_streaming_action, _arun_multi_step_streaming_action). The four
single-step runners did not call it at all — each carried the note
# TODO: __context/__tracer are not remapped here (see _remap_injected_inputs).

Measured on current main, one action per runner, all in the documented shape:

runner entry point on main
_run_function / _arun_function (remapped) step() / astep() works
_run_multi_step_streaming_action / _arun_… (remapped) stream_result() / astream_result() works
_run_single_step_action step() TypeError
_arun_single_step_action astep() TypeError
_run_single_step_streaming_action stream_result() TypeError
_arun_single_step_streaming_action astream_result() TypeError

A second problem surfaced in _remap_dunder_parameters while wiring this up. It selected the injected
parameter by scanning the signature for any name ending with the dunder and taking the first match.
A class-based action that also declares a regular input ending with that suffix therefore gets its own
parameter treated as the injected one, silently:

def run(self, _user__context, __context): ...   # both end with "__context"
# inputs {"__context": CTX, "_user__context": USER}
# -> {"_user__context": CTX}   # USER is gone; _MyClass__context never bound

That is reachable on main today through the already-remapped runners, and applying the remap to four
more runners would have widened it, so it is fixed in the same change.

Changes

burr/core/application.py

  • _run_single_step_action, _arun_single_step_action, _run_single_step_streaming_action,
    _arun_single_step_streaming_action now call _remap_injected_inputs(...) on the method they are
    about to invoke, replacing the four TODOs. It is placed after validate_inputs, which must see
    the literal __context name the action declared.
  • _remap_dunder_parameters no longer scans the signature. It derives the single candidate name the
    way CPython does — _ + the defining class name with leading underscores stripped + the dunder —
    and uses it only if that parameter actually exists. The defining class comes from __qualname__,
    not type(self), so an inherited run() correctly keeps the base class' mangled name. When the
    parameter kept its literal name the method was not defined in a class body, so nothing is renamed.
  • __tracer goes through the same helper and the same vars_to_remap list, so it is covered by the
    same change.

No public API, signature, or configuration changes.

Tests

12 new tests in tests/core/test_application.py, reusing the existing _build_context_app helper and
the style of the three class-based context tests already present:

  • five end-to-end delivery tests, one per runner plus step() on the multi-step path (that one passes
    on main too — it closes the remaining coverage hole among the working runners and shows the change
    does not alter behaviour where behaviour was already right);
  • unit tests pinning the mangling rules: user__context, _user__context, another class' mangled
    name, a class name with leading underscores, an inherited method, __tracer, and the precedence when
    a caller passes the mangled name explicitly;
  • one end-to-end test asserting a single-step action receives the injected context and its own
    _user__context input unchanged;
  • one test asserting a function-based @action(..., __context) action still binds the literal name.

Local results (Windows, repo-pinned toolchain — these are my runs, not GitHub Actions):

command main (3f16c1e6) this branch
pytest tests/core/test_application.py 142 passed 154 passed
pytest tests/core tests/tracking 439 passed / 2 skipped 451 passed / 2 skipped
the 12 new tests only 7 failed / 5 passed 12 passed
CPython 3.10.22 / 3.11.16 / 3.12.14 / 3.13.1 — 451 passed / 2 skipped on all four

tests/core/test_graphviz_display.py fails 9 / passes 3 on both main and this branch here — no
graphviz binary in my environment — and is excluded from the numbers above.

The 7 that fail on main are the four single-step delivery tests, the two clobber unit tests, and the
end-to-end clobber test — i.e. every behaviour this PR changes has a test that is red before and green
after. The other 5 pass on main by design: they pin behaviour that was already correct and must not
break (the working multi-step delivery path, the class-name leading-underscore rule, inherited methods
resolving to the defining class, injected-value precedence, and function-based actions). I am stating
that explicitly rather than reporting 12 red tests.

Mutation validation. Removing the added _remap_injected_inputs(...) call from each runner in turn
makes exactly that runner's test fail and no other — 4/4 killed, source byte-restored. Reverting the
mangling derivation to the old endswith scan turns the two clobber tests red. So the tests distinguish
the defective behaviour from the corrected one rather than merely asserting success.

Formatting: repo-pinned black 23.11.0 --line-length=100, flake8 6.1.0 and
python scripts/check_asf_headers.py are clean on the changed files. isort 5.12.0 --profile black
flags one pre-existing file I did not touch (tests/integrations/test_burr_hamilton.py), identically on
main.

Hosted CI: not yet run for this head at the time of writing; I will report back if anything comes
back red.

Compatibility

  • Action paths that currently raise (TypeError) now work — that is the fix.
  • Existing function-based actions are unaffected: their __context parameter is not mangled, so the
    remap is a no-op. This is asserted by a test, and by tests/core staying green.
  • Existing class-based actions that already worked keep working; the three pre-existing context tests
    pass unchanged.
  • A regular user input is no longer mistaken for an injected parameter, so the previous silent
    overwrite of e.g. _user__context becomes correct binding. Any program that depended on that
    overwrite was already losing the injected context, so nothing usable changes.
  • No new public API, no deprecation needed.

Limitations

  • S3TrackingClient and the OpenTelemetry integration are not exercised here; their extras
    (boto3, opentelemetry-sdk) are not installed in my environment. They are affected only insofar as
    they receive injected inputs through the same runners.
  • The whole tests/ directory cannot be collected locally without the optional integrations
    (openai, pymongo, redis, pandas, ray, opentelemetry, langchain*, asyncpg, haystack,
    hamilton), so I ran tests/core + tests/tracking, which is the subset requiring no extras.
  • Mangling truncates very long class names; if that ever produced a name my candidate does not match,
    the result is no remapping (a visible TypeError), not a silent mis-binding.

Historical Context

#439 ("app.astep does not work with declared context as input") reported the same symptom for a
function-based action and was closed as completed in 2024-11-30. That case works and is covered by
existing tests. It did not cover the class-based single-step runners — the four sites that carried the
TODOs are still failing on main today, which is what this PR addresses.

AI Disclosure

I used AI assistance (an agent running in the Qoder Desktop IDE) for parts of this work: building the
reproduction grid across action flavours, cross-checking the mangling rules against CPython's actual
behaviour, drafting and running the mutation checks, and wording assistance on this description. The
defect analysis, all test runs, and the final code were verified by me locally — the numbers above come
from runs I performed, and the code change is small enough to review line by line. Happy to answer
anything the diff does not make self-evident.

Checklist

  • PR has an informative and human-readable title (this will be pulled into the release notes)
  • Changes are limited to a single goal (no scope creep)
  • Code passed the pre-commit check & code is left cleaner/nicer than when first encountered.
  • Any change in functionality is tested
  • New functions are documented (with a description, list of inputs, and expected output)
  • Placeholder code is flagged / future TODOs are captured in comments — the four TODOs this removes
  • Project documentation has been updated if adding/changing functionality. — no change needed; the
    documented behaviour is now the actual behaviour

B added 4 commits November 3, 2026 12:42
The four single-step runners (_run_single_step_action,
_arun_single_step_action, _run_single_step_streaming_action,
_arun_single_step_streaming_action) passed the framework-injected
"__context"/"__tracer" keys straight to the action method. A class-based
action that follows docs/concepts/actions.rst and declares the parameter
as __context has that name mangled to _ClassName__context, so the call
raised "TypeError: ... got an unexpected keyword argument '__context'".

The other four runners already solve this with _remap_injected_inputs, so
this just applies the same call after validate_inputs. Function-based
actions are unaffected: their parameter name is not mangled, so the
remap is a no-op.
…ameters

The mangled-name lookup accepted any parameter whose name *ends* with
"__context"/"__tracer". A class-based action that also declares a real
input whose name happens to end with the dunder (e.g. ``user__context``)
matched first, so the framework then moved the injected ``__context``
value onto that parameter and clobbered the user's own input.

Require the prefix before the dunder to start with an underscore, which
is what Python's own mangling produces (``_ClassName__context``).
Function-based actions keep using the literal name and are unaffected.
Requiring an underscore before the dunder was not enough. CPython produces
"_ClassName__context" (leading underscores of the class name stripped), so a
user input named "_user__context" still matched, and so did a name that is
merely another class' mangled form -- both were silently overwritten while
the real parameter stayed unbound.

Compute the single candidate name instead of scanning the signature: take the
class from __qualname__, which records the class whose body defines the method,
so an inherited run() correctly keeps the base class' name. When the parameter
kept its literal name the method was not defined in a class body, and no
remapping is needed.

Covers __tracer by the same path, leaves function-based actions untouched, and
pins the injected-value precedence when a caller passes the mangled name.
@github-actions github-actions Bot added the area/core Application, State, Graph, Actions label Nov 3, 2026
@xihaian251

Copy link
Copy Markdown
Author

Hi maintainers,

Could someone with repository permissions please approve and run the two workflows currently gated for this fork PR?

Both runs are currently marked action_required for head 5a585a4c07c3cfa6ab4e7851c37763f334729db4. Neither has started any jobs.

Separately, the documentation workflow for this PR ended with startup_failure:
https://github.com/apache/burr/actions/runs/41586689760

The same workflow has also ended with startup_failure on recent main runs, including this push run:
https://github.com/apache/burr/actions/runs/41425638442

Neither documentation run has any jobs, so this appears to be a workflow-startup issue rather than a test failure introduced by this PR.

Could someone with the necessary permissions please investigate the workflow startup failure and let me know whether any action is needed from my side?

The PR description records the local validation results and their limitations separately. I am not treating any of these hosted checks as passing.

Thank you!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Application, State, Graph, Actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant