From 198ca7b4282ca4c1bd69b666b852480ef93af01b Mon Sep 17 00:00:00 2001 From: DABH Date: Tue, 11 Aug 2026 00:31:38 -0500 Subject: [PATCH 1/5] Accept dict run_config in openai_agents Temporal runner --- CHANGELOG.md | 5 ++ .../contrib/openai_agents/_openai_runner.py | 30 ++++++- tests/contrib/openai_agents/test_openai.py | 86 ++++++++++++++++++- 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358bd9bb2..afdb4a18b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,11 @@ to include examples, links to docs, or any other relevant information. without separately installing `mcp`. Previously the import failed with an `ImportError` because `google.adk.tools.mcp_tool` only exports `McpToolset` when `mcp` is installed. +- `temporalio.contrib.openai_agents` now accepts a plain `dict` for + `run_config` inside workflows, matching openai-agents >= 0.19.0, which + accepts `dict` run configs at its public runner boundaries and includes + `dict` in the `run_config` type. Previously the Temporal runner assumed a + `RunConfig` instance and failed on attribute access. ### Security diff --git a/temporalio/contrib/openai_agents/_openai_runner.py b/temporalio/contrib/openai_agents/_openai_runner.py index 478217c8f..388cf1f32 100644 --- a/temporalio/contrib/openai_agents/_openai_runner.py +++ b/temporalio/contrib/openai_agents/_openai_runner.py @@ -99,6 +99,31 @@ def _has_sandbox_agent(agent: Agent[Any], seen: set[int] | None = None) -> bool: return False +def _coerce_run_config(value: object) -> RunConfig: + """Normalize ``run_config`` the way ``agents.run`` does at its public + runner boundaries. + + openai-agents >= 0.19 also accepts a plain dict for ``run_config``. This + mirrors ``agents.run_config._coerce_run_config`` (which older versions + lack) so dict configs behave identically through the Temporal runner. + """ + if isinstance(value, RunConfig): + return value + if not isinstance(value, dict): + raise TypeError( + f"run_config must be a RunConfig instance or a dict, got {type(value).__name__}" + ) + field_names = { + config_field.name + for config_field in dataclasses.fields(RunConfig) + if config_field.init + } + unknown_fields = sorted(str(name) for name in value if name not in field_names) + if unknown_fields: + raise TypeError(f"Unknown run_config settings: {', '.join(unknown_fields)}") + return RunConfig(**value) + + class TemporalOpenAIRunner(AgentRunner): """Temporal Runner for OpenAI agents. @@ -148,8 +173,9 @@ def _prepare_workflow_run( raise ValueError("Temporal workflows don't support SQLite sessions.") run_config = kwargs.get("run_config") - if run_config is None: - run_config = RunConfig() + run_config = ( + RunConfig() if run_config is None else _coerce_run_config(run_config) + ) if run_config.model and not isinstance(run_config.model, _TemporalModelStub): if not isinstance(run_config.model, str): diff --git a/tests/contrib/openai_agents/test_openai.py b/tests/contrib/openai_agents/test_openai.py index df12685f0..970403ffe 100644 --- a/tests/contrib/openai_agents/test_openai.py +++ b/tests/contrib/openai_agents/test_openai.py @@ -89,7 +89,10 @@ ) from temporalio.contrib.openai_agents._invoke_model_activity import _build_tool from temporalio.contrib.openai_agents._model_parameters import ModelSummaryProvider -from temporalio.contrib.openai_agents._openai_runner import _convert_agent +from temporalio.contrib.openai_agents._openai_runner import ( + _coerce_run_config, + _convert_agent, +) from temporalio.contrib.openai_agents._temporal_model_stub import ( _TemporalModelStub, ) @@ -2160,6 +2163,87 @@ async def test_run_config_models(client: Client): assert provider.model_names == {"gpt-4o"} +@workflow.defn +class DictRunConfigWorkflow: + """Same agents as MultipleModelWorkflow, but passes run_config as a plain + dict, which openai-agents >= 0.19 accepts at its public runner boundaries.""" + + @workflow.run + async def run(self) -> str: + underling = Agent[None]( + name="Underling", + instructions="You do all the work you are told.", + ) + + starting_agent = Agent[None]( + name="Lazy Assistant", + model="gpt-4o-mini", + instructions="You delegate all your work to another agent.", + handoffs=[underling], + ) + # Typed as Any so this also type-checks against openai-agents + # versions whose run_config annotation does not include dict. + dict_run_config: Any = {"model": "gpt-4o"} + result = await Runner.run( + starting_agent=starting_agent, + input="Have you cleaned the store room yet?", + run_config=dict_run_config, + ) + return result.final_output + + +async def test_dict_run_config_models(client: Client): + # A dict run_config must behave identically to the equivalent + # RunConfig(model="gpt-4o") in test_run_config_models above. + provider = AssertDifferentModelProvider(multiple_models_mock_model()) + async with AgentEnvironment( + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=120) + ), + model_provider=provider, + ) as env: + client = env.applied_on_client(client) + + async with new_worker( + client, + DictRunConfigWorkflow, + ) as worker: + workflow_handle = await client.start_workflow( + DictRunConfigWorkflow.run, + id=f"dict-run-config-model-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=10), + ) + result = await workflow_handle.result() + + # Only the model from the runconfig override is used + assert provider.model_names == {"gpt-4o"} + assert ( + result + == "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?" + ) + + +def test_coerce_run_config_validation(): + # Mirrors upstream agents' normalization: equivalent RunConfig out of a + # dict, and the same TypeErrors for invalid input. + coerced = _coerce_run_config({"model": "gpt-4o", "workflow_name": "wf"}) + assert isinstance(coerced, RunConfig) + assert coerced.model == "gpt-4o" + assert coerced.workflow_name == "wf" + + run_config = RunConfig(model="gpt-4o") + assert _coerce_run_config(run_config) is run_config + + with pytest.raises(TypeError, match="Unknown run_config settings: bogus_setting"): + _coerce_run_config({"model": "gpt-4o", "bogus_setting": True}) + + with pytest.raises( + TypeError, match="run_config must be a RunConfig instance or a dict, got int" + ): + _coerce_run_config(42) + + async def test_summary_provider(client: Client): class SummaryProvider(ModelSummaryProvider): def provide( From 7e3f199b85c977cc93758ab9821df46caf681661 Mon Sep 17 00:00:00 2001 From: DABH Date: Tue, 11 Aug 2026 00:57:20 -0500 Subject: [PATCH 2/5] Reword changelog entry as conformance fix --- CHANGELOG.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afdb4a18b..21ac12fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,11 +44,14 @@ to include examples, links to docs, or any other relevant information. without separately installing `mcp`. Previously the import failed with an `ImportError` because `google.adk.tools.mcp_tool` only exports `McpToolset` when `mcp` is installed. -- `temporalio.contrib.openai_agents` now accepts a plain `dict` for - `run_config` inside workflows, matching openai-agents >= 0.19.0, which - accepts `dict` run configs at its public runner boundaries and includes - `dict` in the `run_config` type. Previously the Temporal runner assumed a - `RunConfig` instance and failed on attribute access. +- `temporalio.contrib.openai_agents` no longer crashes when a plain `dict` + is passed for `run_config`. openai-agents >= 0.19.0 accepts `dict` run + configs at its public runner API and normalizes them inside each concrete + runner's `run()`; because the Temporal runner implements that runner + interface and inspects `run_config` before delegating, it now performs the + same normalization upstream does. Previously such calls — valid vanilla + openai-agents code on >= 0.19.0 — failed with `AttributeError` under + Temporal. ### Security From 46b434ff5b2fe06073cdfc8f05e78309426fbd66 Mon Sep 17 00:00:00 2001 From: David Hyde Date: Tue, 11 Aug 2026 00:58:19 -0500 Subject: [PATCH 3/5] Clean up wording --- CHANGELOG.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ac12fcb..d146f045b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,13 +45,8 @@ to include examples, links to docs, or any other relevant information. `ImportError` because `google.adk.tools.mcp_tool` only exports `McpToolset` when `mcp` is installed. - `temporalio.contrib.openai_agents` no longer crashes when a plain `dict` - is passed for `run_config`. openai-agents >= 0.19.0 accepts `dict` run - configs at its public runner API and normalizes them inside each concrete - runner's `run()`; because the Temporal runner implements that runner - interface and inspects `run_config` before delegating, it now performs the - same normalization upstream does. Previously such calls — valid vanilla - openai-agents code on >= 0.19.0 — failed with `AttributeError` under - Temporal. + is passed for `run_config`. (openai-agents >= 0.19.0 accepts `dict` run + configs at its public runner API) ### Security From ad0a4256a9a4c4877238167b2341ddaf61daaca3 Mon Sep 17 00:00:00 2001 From: David Hyde Date: Tue, 11 Aug 2026 01:00:33 -0500 Subject: [PATCH 4/5] Clean up comment --- temporalio/contrib/openai_agents/_openai_runner.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/temporalio/contrib/openai_agents/_openai_runner.py b/temporalio/contrib/openai_agents/_openai_runner.py index 388cf1f32..ea2e6e5df 100644 --- a/temporalio/contrib/openai_agents/_openai_runner.py +++ b/temporalio/contrib/openai_agents/_openai_runner.py @@ -100,12 +100,9 @@ def _has_sandbox_agent(agent: Agent[Any], seen: set[int] | None = None) -> bool: def _coerce_run_config(value: object) -> RunConfig: - """Normalize ``run_config`` the way ``agents.run`` does at its public - runner boundaries. + """openai-agents >= 0.19 also accepts a plain dict for ``run_config``. - openai-agents >= 0.19 also accepts a plain dict for ``run_config``. This - mirrors ``agents.run_config._coerce_run_config`` (which older versions - lack) so dict configs behave identically through the Temporal runner. + This function normalizes to a RunConfig instance. """ if isinstance(value, RunConfig): return value From bfa2c1e0d9136773ac41b4f181950871de68f098 Mon Sep 17 00:00:00 2001 From: DABH Date: Tue, 11 Aug 2026 01:01:25 -0500 Subject: [PATCH 5/5] Assert mock response via shared fixture constant --- tests/contrib/openai_agents/test_openai.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/contrib/openai_agents/test_openai.py b/tests/contrib/openai_agents/test_openai.py index 970403ffe..25597ee55 100644 --- a/tests/contrib/openai_agents/test_openai.py +++ b/tests/contrib/openai_agents/test_openai.py @@ -2077,13 +2077,14 @@ def get_model(self, model_name: str | None) -> Model: return self._model +MULTIPLE_MODELS_FINAL_RESPONSE = "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?" + + def multiple_models_mock_model(): return TestModel.returning_responses( [ ResponseBuilders.tool_call("{}", "transfer_to_underling"), - ResponseBuilders.output_message( - "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?" - ), + ResponseBuilders.output_message(MULTIPLE_MODELS_FINAL_RESPONSE), ] ) @@ -2218,10 +2219,7 @@ async def test_dict_run_config_models(client: Client): # Only the model from the runconfig override is used assert provider.model_names == {"gpt-4o"} - assert ( - result - == "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?" - ) + assert result == MULTIPLE_MODELS_FINAL_RESPONSE def test_coerce_run_config_validation():