You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@burr.apache.org by eb...@apache.org on 2026/10/17 15:34:06 UTC

(burr) branch main updated: fix(core): pass __context/__tracer to async and streaming class-based actions (#937)

This is an automated email from the ASF dual-hosted git repository.

elijahbenizzy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/burr.git


The following commit(s) were added to refs/heads/main by this push:
     new c17ddbf0 fix(core): pass __context/__tracer to async and streaming class-based actions (#937)
c17ddbf0 is described below

commit c17ddbf05d86035fdef0b76deedfdb63870d00f5
Author: breken <su...@brekfuz.com>
AuthorDate: Sat Oct 17 23:34:01 2026 +0800

    fix(core): pass __context/__tracer to async and streaming class-based actions (#937)
    
    * Pass __context/__tracer to async and streaming class-based actions
    
    A class-based action that declares `__context` (or `__tracer`) in its
    run signature has that parameter name-mangled by Python, e.g. to
    `_MyAction__context`. The sync run path remaps the injected input to
    the mangled name, but the async run path and both multi-step streaming
    paths passed `__context=` as-is, so the action failed with
    `TypeError: run() got an unexpected keyword argument '__context'`.
    
    Apply the same remap in `_arun_function` and in the sync and async
    multi-step streaming runners.
    
    Generated-by: Claude Code (Claude Opus 5.5)
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    
    * Note the missing __context/__tracer remap in single-step runners
    
    The single-step runners call run_and_update / stream_run_and_update with
    the injected inputs as-is, so a SingleStepAction that names __context in
    its signature still gets a TypeError. SingleStepAction is internal and
    in-tree subclasses take **run_kwargs, so leave a TODO at each site rather
    than change behavior here.
    
    Generated-by: Claude Code (Claude Opus 5.5)
    Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: breken-ai <31...@users.noreply.github.com>
    Co-authored-by: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
 burr/core/application.py       | 21 ++++++---
 tests/core/test_application.py | 99 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 115 insertions(+), 5 deletions(-)

diff --git a/burr/core/application.py b/burr/core/application.py
index 5d54636b..91ef8b1b 100644
--- a/burr/core/application.py
+++ b/burr/core/application.py
@@ -158,6 +158,14 @@ def _remap_dunder_parameters(
     return inputs
 
 
+def _remap_injected_inputs(method: Callable, inputs: Dict[str, Any]) -> Dict[str, Any]:
+    """Remaps the framework-injected ``__context``/``__tracer`` inputs to the name-mangled
+    parameter names a class-based action's method may use. See ``_remap_dunder_parameters``."""
+    if "__context" in inputs or "__tracer" in inputs:
+        return _remap_dunder_parameters(method, inputs, ["__context", "__tracer"])
+    return inputs
+
+
 def _run_function(function: Function, state: State, inputs: Dict[str, Any], name: str) -> dict:
     """Runs a function, returning the result of running the function.
     Note this restricts the keys in the state to only those that the
@@ -176,9 +184,7 @@ def _run_function(function: Function, state: State, inputs: Dict[str, Any], name
         )
     state_to_use = state.subset(*function.reads)
     function.validate_inputs(inputs)
-    if "__context" in inputs or "__tracer" in inputs:
-        # potentially need to remap the __context & __tracer variables
-        inputs = _remap_dunder_parameters(function.run, inputs, ["__context", "__tracer"])
+    inputs = _remap_injected_inputs(function.run, inputs)
     result = function.run(state_to_use, **inputs)
     _validate_result(result, name)
     return result
@@ -191,6 +197,7 @@ async def _arun_function(
     Async version of the above."""
     state_to_use = state.subset(*function.reads)
     function.validate_inputs(inputs)
+    inputs = _remap_injected_inputs(function.run, inputs)
     result = await function.run(state_to_use, **inputs)
     _validate_result(result, name)
     return result
@@ -311,6 +318,7 @@ def _run_single_step_action(
     """
     # TODO -- guard all reads/writes with a subset of the state
     action.validate_inputs(inputs)
+    # TODO: __context/__tracer are not remapped here (see _remap_injected_inputs).
     result, new_state = _adjust_single_step_output(
         action.run_and_update(state, **inputs), action.name, action.schema
     )
@@ -334,6 +342,7 @@ def _run_single_step_streaming_action(
     action.validate_inputs(inputs)
     stream_initialize_time = system.now()
     first_stream_start_time = None
+    # TODO: __context/__tracer are not remapped here (see _remap_injected_inputs).
     generator = action.stream_run_and_update(state, **inputs)
     result = None
     state_update = None
@@ -402,6 +411,7 @@ async def _arun_single_step_streaming_action(
     action.validate_inputs(inputs)
     stream_initialize_time = system.now()
     first_stream_start_time = None
+    # TODO: __context/__tracer are not remapped here (see _remap_injected_inputs).
     generator = action.stream_run_and_update(state, **inputs)
     result = None
     state_update = None
@@ -477,7 +487,7 @@ def _run_multi_step_streaming_action(
     """
     action.validate_inputs(inputs)
     stream_initialize_time = system.now()
-    generator = action.stream_run(state, **inputs)
+    generator = action.stream_run(state, **_remap_injected_inputs(action.stream_run, inputs))
     result = None
     first_stream_start_time = None
     count = 0
@@ -535,7 +545,7 @@ async def _arun_multi_step_streaming_action(
     """Runs a multi-step streaming action in async. See the synchronous version for more details."""
     action.validate_inputs(inputs)
     stream_initialize_time = system.now()
-    generator = action.stream_run(state, **inputs)
+    generator = action.stream_run(state, **_remap_injected_inputs(action.stream_run, inputs))
     result = None
     first_stream_start_time = None
     count = 0
@@ -587,6 +597,7 @@ async def _arun_single_step_action(
     """Runs a single step action in async. See the synchronous version for more details."""
     state_to_use = state
     action.validate_inputs(inputs)
+    # TODO: __context/__tracer are not remapped here (see _remap_injected_inputs).
     result, new_state = _adjust_single_step_output(
         await action.run_and_update(state_to_use, **inputs), action.name, action.schema
     )
diff --git a/tests/core/test_application.py b/tests/core/test_application.py
index 5a252f77..efb7d205 100644
--- a/tests/core/test_application.py
+++ b/tests/core/test_application.py
@@ -4256,6 +4256,105 @@ def test_remap_context_variable_without_mangled_context():
     assert _remap_dunder_parameters(_action.run, inputs, ["__context", "__tracer"]) == expected
 
 
+class AsyncActionWithContext(Action):
+    """Class-based async action whose run() takes the injected ``__context``.
+    Python name-mangles the parameter to ``_AsyncActionWithContext__context``."""
+
+    @property
+    def reads(self) -> list[str]:
+        return []
+
+    @property
+    def writes(self) -> list[str]:
+        return ["app_id"]
+
+    @property
+    def inputs(self) -> list[str]:
+        return ["__context"]
+
+    async def run(self, state: State, __context: ApplicationContext) -> dict:
+        return {"app_id": __context.app_id}
+
+    def update(self, result: dict, state: State) -> State:
+        return state.update(**result)
+
+
+class StreamingActionWithContext(StreamingAction):
+    @property
+    def reads(self) -> list[str]:
+        return []
+
+    @property
+    def writes(self) -> list[str]:
+        return ["app_id"]
+
+    @property
+    def inputs(self) -> list[str]:
+        return ["__context"]
+
+    def stream_run(
+        self, state: State, __context: ApplicationContext
+    ) -> Generator[dict, None, None]:
+        yield {"app_id": __context.app_id}
+
+    def update(self, result: dict, state: State) -> State:
+        return state.update(**result)
+
+
+class AsyncStreamingActionWithContext(AsyncStreamingAction):
+    @property
+    def reads(self) -> list[str]:
+        return []
+
+    @property
+    def writes(self) -> list[str]:
+        return ["app_id"]
+
+    @property
+    def inputs(self) -> list[str]:
+        return ["__context"]
+
+    async def stream_run(self, state: State, __context: ApplicationContext) -> AsyncGenerator:
+        yield {"app_id": __context.app_id}
+
+    def update(self, result: dict, state: State) -> State:
+        return state.update(**result)
+
+
+def _build_context_app(action_: Action) -> Application:
+    return (
+        ApplicationBuilder()
+        .with_actions(ctx_action=action_)
+        .with_transitions()
+        .with_entrypoint("ctx_action")
+        .with_identifiers(app_id="context-app-id")
+        .build()
+    )
+
+
+async def test_astep_class_based_action_receives_context():
+    app = _build_context_app(AsyncActionWithContext())
+    _, result, state = await app.astep()
+    assert result == {"app_id": "context-app-id"}
+    assert state["app_id"] == "context-app-id"
+
+
+def test_stream_result_class_based_streaming_action_receives_context():
+    app = _build_context_app(StreamingActionWithContext())
+    _, container = app.stream_result(halt_after=["ctx_action"])
+    result, state = container.get()
+    assert result == {"app_id": "context-app-id"}
+    assert state["app_id"] == "context-app-id"
+
+
+async def test_astream_result_class_based_async_streaming_action_receives_context():
+    app = _build_context_app(AsyncStreamingActionWithContext())
+    _, container = await app.astream_result(halt_after=["ctx_action"])
+    result, state = await container.get()
+    assert result == {"app_id": "context-app-id"}
+    assert state["app_id"] == "context-app-id"
+
+
 async def test_async_application_builder_initialize_raises_on_broken_persistor():
     """Persisters should return None when there is no state to be loaded and the default used."""
     await asyncio.sleep(0.00001)