Skip to content

Commit b9979a7

Browse files
fix: add loop detection for same-agent back-to-back invocations in 1.3.0
In agent-framework 1.3.0, GroupChatBuilder makes the Coordinator internal to AgentBasedGroupChatOrchestrator - its responses never appear in the streaming loop, making our coordinator-based loop detection dead code. Additionally, _start_agent_if_needed silently returned when the same agent was selected consecutively, preventing max_rounds from counting. Fixes: - Use response_id from AgentResponseUpdate to detect new invocations of the same agent (even when executor_id hasn't changed) - Add participant-side loop detection: if same agent runs 5+ consecutive times, force termination with descriptive message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d0397f7 commit b9979a7

1 file changed

Lines changed: 55 additions & 3 deletions

File tree

src/processor/src/libs/agent_framework/groupchat_orchestrator.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ def __init__(
265265

266266
# Streaming response buffer
267267
self._last_executor_id: str | None = None
268+
self._last_response_id: str | None = None
268269
self._current_agent_response: list[str] = []
269270
self._current_agent_start_time: datetime | None = None
270271

@@ -781,7 +782,14 @@ async def _handle_agent_update(
781782
agent_name = author_name or self._normalize_executor_id(
782783
getattr(event, "agent_id", None) or ""
783784
)
784-
await self._start_agent_if_needed(agent_name, stream_callback, callback)
785+
# Detect new invocations of the same agent via response_id change.
786+
# In 1.3.0 with GroupChatBuilder, the Coordinator is internal — we only
787+
# see participant events. When the same participant runs back-to-back,
788+
# the executor_id is identical but response_id differs per invocation.
789+
response_id = getattr(event, "response_id", None)
790+
await self._start_agent_if_needed(
791+
agent_name, stream_callback, callback, response_id=response_id
792+
)
785793
self._append_text_chunk(event)
786794
await self._process_tool_calls(event, agent_name, stream_callback)
787795

@@ -797,11 +805,29 @@ async def _start_agent_if_needed(
797805
agent_name: str,
798806
stream_callback: AgentResponseStreamCallback | None,
799807
callback: AgentResponseCallback | None,
808+
response_id: str | None = None,
800809
) -> None:
801-
"""Handle agent switches and emit a message-start stream event."""
802-
if agent_name == self._last_executor_id:
810+
"""Handle agent switches and emit a message-start stream event.
811+
812+
In agent-framework 1.3.0 with GroupChatBuilder, the Coordinator is
813+
internal — our streaming loop only sees participant events. When the
814+
same participant is selected back-to-back, the executor_id is identical
815+
but ``response_id`` differs per invocation. We use this to detect new
816+
turns of the same agent.
817+
"""
818+
# Detect same-agent new invocation via response_id change.
819+
is_new_response = (
820+
response_id is not None
821+
and self._last_response_id is not None
822+
and response_id != self._last_response_id
823+
)
824+
if agent_name == self._last_executor_id and not is_new_response:
825+
# Same agent, same response — just accumulating streaming chunks.
803826
return
804827

828+
if response_id is not None:
829+
self._last_response_id = response_id
830+
805831
# Complete and save previous agent's response
806832
if self._last_executor_id and self._current_agent_response:
807833
await self._complete_agent_response(self._last_executor_id, callback)
@@ -1106,6 +1132,32 @@ async def _complete_agent_response(
11061132

11071133
self.agent_responses.append(response)
11081134

1135+
# Participant-side loop detection. In 1.3.0 with GroupChatBuilder, the
1136+
# Coordinator is internal (its responses never appear in our streaming
1137+
# loop), so the Coordinator-based loop detection below (streak >= 3) is
1138+
# dead code. Instead, detect loops by checking if the last N responses
1139+
# are all from the same non-Coordinator agent.
1140+
if agent_name != self.coordinator_name:
1141+
consecutive_same = 0
1142+
for r in reversed(self.agent_responses):
1143+
if r.agent_name == agent_name:
1144+
consecutive_same += 1
1145+
else:
1146+
break
1147+
if consecutive_same >= 5:
1148+
logger.warning(
1149+
"[LOOP] Same agent '%s' ran %d consecutive times; forcing termination.",
1150+
agent_name,
1151+
consecutive_same,
1152+
)
1153+
self._request_forced_termination(
1154+
reason=(
1155+
f"Loop detected: '{agent_name}' ran {consecutive_same} "
1156+
f"consecutive times without any other agent participating"
1157+
),
1158+
termination_type="hard_timeout",
1159+
)
1160+
11091161
# Mark progress on any non-Coordinator completion. This is used to ensure loop
11101162
# detection only triggers when the Coordinator is repeating itself *and* the
11111163
# rest of the conversation is not advancing.

0 commit comments

Comments
 (0)