diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py index 8b65da4e202..6b8e243b558 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py @@ -275,6 +275,9 @@ def __init__( autonomous_mode_prompt: Prompt to provide to the agent when continuing in autonomous mode. This will guide the agent in the absence of user input. autonomous_mode_turn_limit: Maximum number of autonomous turns before requesting user input. + ``0`` returns control to the user as soon as the agent responds + without a handoff; the default limit is only used when this is + left as ``None``. """ self._auto_handoff_middleware = _AutoHandoffMiddleware(handoffs) cloned_agent = self._prepare_agent_with_handoffs(agent, handoffs, self._auto_handoff_middleware) @@ -288,7 +291,9 @@ def __init__( # Autonomous mode members self._autonomous_mode = autonomous_mode self._autonomous_mode_prompt = autonomous_mode_prompt or _AUTONOMOUS_MODE_DEFAULT_PROMPT - self._autonomous_mode_turn_limit = autonomous_mode_turn_limit or _DEFAULT_AUTONOMOUS_TURN_LIMIT + self._autonomous_mode_turn_limit = ( + _DEFAULT_AUTONOMOUS_TURN_LIMIT if autonomous_mode_turn_limit is None else autonomous_mode_turn_limit + ) self._autonomous_mode_turns = 0 def _prepare_agent_with_handoffs( @@ -938,6 +943,8 @@ def with_autonomous_mode( in autonomous mode. If not provided, a default prompt will be used. turn_limits: Optional mapping of agent identifiers/factory names to maximum number of autonomous turns before returning control to the user. If not provided, a default turn limit will be used. + A limit of ``0`` disables autonomous continuation for that agent instead of falling back + to the default. """ self._autonomous_mode = True self._autonomous_mode_prompts = prompts or {} diff --git a/python/packages/orchestrations/tests/test_handoff.py b/python/packages/orchestrations/tests/test_handoff.py index 5410363524c..a9c01bd6a2d 100644 --- a/python/packages/orchestrations/tests/test_handoff.py +++ b/python/packages/orchestrations/tests/test_handoff.py @@ -1327,6 +1327,40 @@ async def test_autonomous_mode_resumes_user_input_on_turn_limit(): assert requests[0].source_executor_id == worker.name +async def test_autonomous_mode_zero_turn_limit_returns_control_immediately(): + """A turn limit of 0 means no autonomous continuation, not the default turn limit.""" + triage_client = MockChatClient(name="triage", handoff_to="worker") + worker_client = MockChatClient(name="worker") + triage: Agent[Any] = Agent( + id="triage", + name="triage", + client=triage_client, + require_per_service_call_history_persistence=True, + ) + worker: Agent[Any] = Agent( + id="worker", + name="worker", + client=worker_client, + require_per_service_call_history_persistence=True, + ) + + workflow = ( + HandoffBuilder(participants=_as_handoff_agents(triage, worker), termination_condition=lambda _: False) + .with_start_agent(_as_handoff_agent(triage)) + .with_autonomous_mode(agents=[worker], turn_limits={resolve_agent_id(worker): 0}) + .build() + ) + + events = await _drain(workflow.run("Start", stream=True)) + + # Swallowing 0 into the default limit lets the agent run on for 50 injected prompts, + # so the worker's client being called once is what distinguishes the two readings. + assert len(worker_client.received_messages) == 1, "A zero turn limit must not grant autonomous turns" + requests = [ev for ev in events if ev.type == "request_info"] + assert requests and len(requests) == 1, "A zero turn limit should request user input at once" + assert requests[0].source_executor_id == worker.name + + def test_build_fails_without_start_agent(): """Verify that build() raises ValueError when with_start_agent() was not called.""" triage = MockHandoffAgent(name="triage")