From 4ea9d79da07433e7e29006f286ca3cee2e576b90 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 27 Feb 2026 09:42:06 +0900 Subject: [PATCH 1/3] Python: Add regression tests for #3948 - Entry JoinExecutor initializes Workflow.Inputs Add tests verifying that when workflow.run() is called with a dict or string input, the Entry node (JoinExecutor with kind: 'Entry') correctly initializes Workflow.Inputs via _ensure_state_initialized so that: - Expressions like =inputs.age resolve to the correct value - Conditions like =Local.age < 13 evaluate based on actual input (not blank/0) - String inputs populate both inputs.input and System.LastMessage.Text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/test_workflow_factory.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/python/packages/declarative/tests/test_workflow_factory.py b/python/packages/declarative/tests/test_workflow_factory.py index 25c1249a50..5b42e0effe 100644 --- a/python/packages/declarative/tests/test_workflow_factory.py +++ b/python/packages/declarative/tests/test_workflow_factory.py @@ -159,6 +159,81 @@ async def test_execute_if_workflow(self): _text_outputs = [str(o) for o in outputs if isinstance(o, str) or hasattr(o, "data")] # noqa: F841 assert any("Condition was true" in str(o) for o in outputs) + @pytest.mark.asyncio + async def test_entry_join_executor_initializes_workflow_inputs(self): + """Regression test for #3948: Entry JoinExecutor must initialize Workflow.Inputs. + + When workflow.run() is called with a dict input, the Entry node (JoinExecutor + with kind: 'Entry') must call _ensure_state_initialized so that Workflow.Inputs + is populated. Without this, expressions like =inputs.age resolve to blank and + conditions like =Local.age < 13 always evaluate as true (blank treated as 0). + """ + factory = WorkflowFactory() + workflow = factory.create_workflow_from_yaml(""" +name: entry-inputs-test +actions: + - kind: SetValue + id: get_age + path: Local.age + value: =inputs.age + - kind: If + id: check_age + condition: =Local.age < 13 + then: + - kind: SendActivity + activity: + text: child + else: + - kind: SendActivity + activity: + text: adult +""") + + # age=8 -> child branch + result_child = await workflow.run({"age": 8}) + outputs_child = result_child.get_outputs() + assert any("child" in str(o) for o in outputs_child), ( + f"Expected 'child' for age=8 but got: {outputs_child}" + ) + assert not any("adult" in str(o) for o in outputs_child), ( + f"Did not expect 'adult' for age=8 but got: {outputs_child}" + ) + + # age=25 -> adult branch (bug: blank treated as 0 made this always go to child) + result_adult = await workflow.run({"age": 25}) + outputs_adult = result_adult.get_outputs() + assert any("adult" in str(o) for o in outputs_adult), ( + f"Expected 'adult' for age=25 but got: {outputs_adult}" + ) + assert not any("child" in str(o) for o in outputs_adult), ( + f"Did not expect 'child' for age=25 but got: {outputs_adult}" + ) + + @pytest.mark.asyncio + async def test_entry_join_executor_initializes_workflow_inputs_string(self): + """Regression test for #3948: Entry JoinExecutor must initialize Workflow.Inputs for string input. + + When workflow.run() is called with a string input, Workflow.Inputs.input and + System.LastMessage.Text should be set correctly. + """ + factory = WorkflowFactory() + workflow = factory.create_workflow_from_yaml(""" +name: entry-string-inputs-test +actions: + - kind: SetValue + path: Local.msg + value: =inputs.input + - kind: SendActivity + activity: + text: =Local.msg +""") + + result = await workflow.run("hello-world") + outputs = result.get_outputs() + assert any("hello-world" in str(o) for o in outputs), ( + f"Expected 'hello-world' in outputs but got: {outputs}" + ) + class TestWorkflowFactoryAgentRegistration: """Tests for agent registration.""" From 5d9ba0ce7ac9cdad74b47e037f55cf181939aeb4 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 27 Feb 2026 09:42:20 +0900 Subject: [PATCH 2/3] Apply pre-commit auto-fixes --- .../declarative/tests/test_workflow_factory.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/python/packages/declarative/tests/test_workflow_factory.py b/python/packages/declarative/tests/test_workflow_factory.py index 5b42e0effe..f08f5993e5 100644 --- a/python/packages/declarative/tests/test_workflow_factory.py +++ b/python/packages/declarative/tests/test_workflow_factory.py @@ -192,9 +192,7 @@ async def test_entry_join_executor_initializes_workflow_inputs(self): # age=8 -> child branch result_child = await workflow.run({"age": 8}) outputs_child = result_child.get_outputs() - assert any("child" in str(o) for o in outputs_child), ( - f"Expected 'child' for age=8 but got: {outputs_child}" - ) + assert any("child" in str(o) for o in outputs_child), f"Expected 'child' for age=8 but got: {outputs_child}" assert not any("adult" in str(o) for o in outputs_child), ( f"Did not expect 'adult' for age=8 but got: {outputs_child}" ) @@ -202,9 +200,7 @@ async def test_entry_join_executor_initializes_workflow_inputs(self): # age=25 -> adult branch (bug: blank treated as 0 made this always go to child) result_adult = await workflow.run({"age": 25}) outputs_adult = result_adult.get_outputs() - assert any("adult" in str(o) for o in outputs_adult), ( - f"Expected 'adult' for age=25 but got: {outputs_adult}" - ) + assert any("adult" in str(o) for o in outputs_adult), f"Expected 'adult' for age=25 but got: {outputs_adult}" assert not any("child" in str(o) for o in outputs_adult), ( f"Did not expect 'child' for age=25 but got: {outputs_adult}" ) @@ -230,9 +226,7 @@ async def test_entry_join_executor_initializes_workflow_inputs_string(self): result = await workflow.run("hello-world") outputs = result.get_outputs() - assert any("hello-world" in str(o) for o in outputs), ( - f"Expected 'hello-world' in outputs but got: {outputs}" - ) + assert any("hello-world" in str(o) for o in outputs), f"Expected 'hello-world' in outputs but got: {outputs}" class TestWorkflowFactoryAgentRegistration: From 2e402a583a891bea2278c400f4e66f1b4b26a67a Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 27 Feb 2026 10:54:42 +0900 Subject: [PATCH 3/3] Fix D420 and RUF070 lint errors across packages --- .../agent_framework_azurefunctions/_app.py | 6 +++--- .../packages/core/agent_framework/_types.py | 12 +++++------ .../agent_framework/_workflows/_workflow.py | 21 +++++++------------ .../agent_framework_devui/_deployment.py | 3 +-- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py index 01735e28d1..c7d8552b24 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py @@ -612,11 +612,11 @@ def get_agent( context: Durable Functions orchestration context invoking the agent. agent_name: Name of the agent registered on this app. - Raises: - ValueError: If the requested agent has not been registered. - Returns: DurableAIAgent[AgentTask] wrapper bound to the orchestration context. + + Raises: + ValueError: If the requested agent has not been registered. """ normalized_name = str(agent_name) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 37ee9f1138..3df0bb20fb 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -93,13 +93,13 @@ def detect_media_type_from_base64( This will look at the actual data to determine the media_type and not at the URI prefix. Will also not compare those two values. - Raises: - ValueError: If not exactly 1 of data_bytes, data_str, or data_uri is provided, or if base64 decoding fails. - Returns: The detected media type (e.g., 'image/png', 'audio/wav', 'application/pdf') or None if the format is not recognized. + Raises: + ValueError: If not exactly 1 of data_bytes, data_str, or data_uri is provided, or if base64 decoding fails. + Examples: .. code-block:: python @@ -670,6 +670,9 @@ def from_uri( additional_properties: Optional additional properties. raw_representation: Optional raw representation from an underlying implementation. + Returns: + A Content instance with type="data" for data URIs or type="uri" for external URIs. + Raises: ContentError: If the URI is not valid. @@ -693,9 +696,6 @@ def from_uri( raw_base64_string }" ) - - Returns: - A Content instance with type="data" for data URIs or type="uri" for external URIs. """ return cls( **_validate_uri(uri, media_type), diff --git a/python/packages/core/agent_framework/_workflows/_workflow.py b/python/packages/core/agent_framework/_workflows/_workflow.py index f545fbe5d8..7e7578e1c4 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow.py +++ b/python/packages/core/agent_framework/_workflows/_workflow.py @@ -333,11 +333,9 @@ async def _run_workflow_with_tracing( span.add_event(OtelAttr.WORKFLOW_STARTED) # Emit explicit start/status events to the stream with _framework_event_origin(): - started = WorkflowEvent.started() - yield started + yield WorkflowEvent.started() with _framework_event_origin(): - in_progress = WorkflowEvent.status(WorkflowRunState.IN_PROGRESS) - yield in_progress + yield WorkflowEvent.status(WorkflowRunState.IN_PROGRESS) # Reset context for a new run if supported if reset_context: @@ -372,18 +370,15 @@ async def _run_workflow_with_tracing( if event.type == "request_info" and not emitted_in_progress_pending: emitted_in_progress_pending = True with _framework_event_origin(): - pending_status = WorkflowEvent.status(WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS) - yield pending_status + yield WorkflowEvent.status(WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS) # Workflow runs until idle - emit final status based on whether requests are pending if saw_request: with _framework_event_origin(): - terminal_status = WorkflowEvent.status(WorkflowRunState.IDLE_WITH_PENDING_REQUESTS) - yield terminal_status + yield WorkflowEvent.status(WorkflowRunState.IDLE_WITH_PENDING_REQUESTS) else: with _framework_event_origin(): - terminal_status = WorkflowEvent.status(WorkflowRunState.IDLE) - yield terminal_status + yield WorkflowEvent.status(WorkflowRunState.IDLE) span.add_event(OtelAttr.WORKFLOW_COMPLETED) except Exception as exc: @@ -394,11 +389,9 @@ async def _run_workflow_with_tracing( # Surface structured failure details before propagating exception details = WorkflowErrorDetails.from_exception(exc) with _framework_event_origin(): - failed_event = WorkflowEvent.failed(details) - yield failed_event + yield WorkflowEvent.failed(details) with _framework_event_origin(): - failed_status = WorkflowEvent.status(WorkflowRunState.FAILED) - yield failed_status + yield WorkflowEvent.status(WorkflowRunState.FAILED) span.add_event( name=OtelAttr.WORKFLOW_ERROR, attributes={ diff --git a/python/packages/devui/agent_framework_devui/_deployment.py b/python/packages/devui/agent_framework_devui/_deployment.py index 45f99a315a..db2de27ecf 100644 --- a/python/packages/devui/agent_framework_devui/_deployment.py +++ b/python/packages/devui/agent_framework_devui/_deployment.py @@ -92,8 +92,7 @@ async def deploy(self, config: DeploymentConfig, entity_path: Path) -> AsyncGene break # Get event from queue with short timeout - event = await asyncio.wait_for(event_queue.get(), timeout=0.1) - yield event + yield await asyncio.wait_for(event_queue.get(), timeout=0.1) except asyncio.TimeoutError: # No event in queue, continue waiting continue