From a402e9f01d2a7573c7b164f8711fb304c50d5fad Mon Sep 17 00:00:00 2001 From: Lanre Shittu <136805224+Shizoqua@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:17:54 +0000 Subject: [PATCH] Python: Reject request info filters that name agents outside the orchestration --- .../_concurrent.py | 7 ++++- .../_group_chat.py | 7 ++++- .../_orchestration_request_info.py | 18 +++++++++++ .../_sequential.py | 7 ++++- .../tests/test_orchestration_request_info.py | 30 +++++++++++++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py index c798f36f736..660cc7a9a05 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py @@ -18,7 +18,7 @@ from typing_extensions import Never from ._feature_usage import FeatureIndex -from ._orchestration_request_info import AgentApprovalExecutor +from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter from ._participant_output_config import ( UNSET, _coalesce_output_from, # pyright: ignore[reportPrivateUsage] @@ -382,6 +382,11 @@ def _resolve_participants(self) -> list[Executor]: else: raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.") + if self._request_info_enabled: + validate_request_info_filter( + self._request_info_filter, + [resolve_agent_id(p) for p in participants if not isinstance(p, Executor)], + ) return executors def build(self) -> Workflow: diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py index 48956b0c817..85750aeb149 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py @@ -51,7 +51,7 @@ TerminationCondition, ) from ._feature_usage import FeatureIndex -from ._orchestration_request_info import AgentApprovalExecutor +from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter from ._orchestrator_helpers import clean_conversation_for_handoff, extract_markdown_fence_bodies from ._participant_output_config import ( UNSET, @@ -1030,6 +1030,11 @@ def _resolve_participants(self) -> list[Executor]: f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}." ) + if self._request_info_enabled: + validate_request_info_filter( + self._request_info_filter, + [resolve_agent_id(p) for p in participants if not isinstance(p, Executor)], + ) return executors def build(self) -> Workflow: diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py index 205d110e63b..6c9bcdcb112 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py @@ -41,6 +41,24 @@ def resolve_request_info_filter(agents: list[str | SupportsAgentRun] | None) -> return result +def validate_request_info_filter(request_info_filter: set[str] | None, agent_ids: list[str]) -> None: + """Raise if the request info filter names an agent that is not a participant. + + Args: + request_info_filter: Agent IDs passed to ``with_request_info(agents=...)``. Empty or None means all agents. + agent_ids: IDs of the agent participants in the orchestration. + + Raises: + ValueError: If a name in the filter does not match any agent participant. + """ + unknown = sorted((request_info_filter or set()) - set(agent_ids)) + if unknown: + raise ValueError( + f"with_request_info() names agents that are not agent participants: " + f"{', '.join(repr(name) for name in unknown)}. Agent participants: {', '.join(agent_ids)}." + ) + + @dataclass class AgentRequestInfoResponse: """Response containing additional information requested from users for agents. diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py index 8c3eeb064f3..a92353998fb 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py @@ -32,7 +32,7 @@ from agent_framework._workflows._workflow_context import WorkflowContext from ._feature_usage import FeatureIndex -from ._orchestration_request_info import AgentApprovalExecutor +from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter from ._participant_output_config import ( UNSET, _coalesce_output_from, # pyright: ignore[reportPrivateUsage] @@ -230,6 +230,11 @@ def _resolve_participants(self) -> list[Executor]: else: raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.") + if self._request_info_enabled: + validate_request_info_filter( + self._request_info_filter, + [resolve_agent_id(p) for p in participants if not isinstance(p, Executor)], + ) return executors def build(self) -> Workflow: diff --git a/python/packages/orchestrations/tests/test_orchestration_request_info.py b/python/packages/orchestrations/tests/test_orchestration_request_info.py index 1ab40cd78bc..bbbaaaabd99 100644 --- a/python/packages/orchestrations/tests/test_orchestration_request_info.py +++ b/python/packages/orchestrations/tests/test_orchestration_request_info.py @@ -256,3 +256,33 @@ def test_propagate_request_enabled(self): executor = AgentApprovalExecutor(cast(SupportsAgentRun, agent)) assert executor._propagate_request is True # type: ignore + + +def _build_with_request_info(builder_name: str, agents: list[str]) -> Any: + from agent_framework.orchestrations import ConcurrentBuilder, GroupChatBuilder, SequentialBuilder + + participants = [ + cast(SupportsAgentRun, _TestAgent(id="drafter_id", name="drafter")), + cast(SupportsAgentRun, _TestAgent(id="publisher_id", name="publisher")), + ] + if builder_name == "sequential": + return SequentialBuilder(participants=participants).with_request_info(agents=agents).build() + if builder_name == "concurrent": + return ConcurrentBuilder(participants=participants).with_request_info(agents=agents).build() + return ( + GroupChatBuilder(participants=participants, selection_func=lambda state: "drafter") + .with_request_info(agents=agents) + .build() + ) + + +@pytest.mark.parametrize("builder_name", ["sequential", "concurrent", "group_chat"]) +class TestRequestInfoFilterValidation: + """Names passed to with_request_info(agents=...) must match an agent participant.""" + + def test_unknown_agent_name_raises(self, builder_name: str) -> None: + with pytest.raises(ValueError, match="'Publisher'"): + _build_with_request_info(builder_name, ["Publisher"]) + + def test_known_agent_name_builds(self, builder_name: str) -> None: + assert _build_with_request_info(builder_name, ["publisher"]) is not None