Skip to content

Commit 0c4e6bb

Browse files
Rook1exraychen911
authored andcommitted
fix(teams): fix parallel delegation signal loss and enable streaming output (#45)
1 parent 7419f22 commit 0c4e6bb

1 file changed

Lines changed: 58 additions & 18 deletions

File tree

trpc_agent_sdk/teams/_team_agent.py

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,9 @@ async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event,
414414
iteration += 1
415415
last_event: Optional[Event] = None
416416

417+
# Collect ALL non-partial events from leader for delegation signal extraction
418+
all_leader_events: List[Event] = []
419+
417420
# Update activity tracking
418421
current_activity = "leader planning"
419422

@@ -467,6 +470,7 @@ async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event,
467470
# LongRunningEvent is partial, so will not enter below logic.
468471
if not event.partial:
469472
last_event = event
473+
all_leader_events.append(event)
470474
# Collect text from leader's response
471475
event_text = self._extract_text_from_event(event)
472476
if event_text:
@@ -486,8 +490,13 @@ async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event,
486490
logger.debug("TeamAgent: Leader requested transfer to: %s", transfer_to_agent)
487491
return
488492

489-
# Try to extract delegation signals from last event
490-
signals = self._extract_delegation_signals(last_event)
493+
# Try to extract delegation signals from ALL leader events
494+
# (not just last_event, because when tools execute sequentially,
495+
# each tool response is a separate event and only the last one
496+
# would be captured by last_event)
497+
signals: List[DelegationSignal] = []
498+
for leader_event in all_leader_events:
499+
signals.extend(self._extract_delegation_signals(leader_event))
491500

492501
if signals:
493502
# Delegation detected, execute member agent(s)
@@ -786,9 +795,12 @@ async def _execute_delegations_parallel(
786795
is_member_mode: bool = False,
787796
context_lock: Optional[asyncio.Lock] = None,
788797
) -> AsyncGenerator[Event, None]:
789-
"""Execute multiple delegations in parallel.
798+
"""Execute multiple delegations in parallel with streaming output.
799+
800+
Uses an asyncio.Queue so that events from whichever member finishes
801+
first are yielded immediately, instead of waiting for all members to
802+
complete before yielding any events.
790803
791-
Used when parallel_execution=True and there are multiple delegation signals.
792804
Each _execute_delegation call handles its own interaction recording.
793805
794806
Args:
@@ -800,25 +812,53 @@ async def _execute_delegations_parallel(
800812
context_lock: Lock for thread-safe access to team_run_context.
801813
802814
Yields:
803-
Events from all member executions.
815+
Events from member executions, streamed as soon as each member
816+
produces them.
804817
"""
818+
# Use a queue to stream events from concurrent tasks as they arrive
819+
queue: asyncio.Queue = asyncio.Queue()
820+
# Sentinel object to signal that a producer task has finished
821+
_DONE = object()
805822

806-
async def run_delegation(signal: DelegationSignal) -> List[Event]:
807-
"""Run a single delegation and collect events."""
808-
events: List[Event] = []
809-
async for event in self._execute_delegation(ctx, signal, team_run_context, message_builder, is_member_mode,
810-
context_lock):
811-
events.append(event)
812-
return events
823+
async def run_delegation(signal: DelegationSignal) -> None:
824+
"""Run a single delegation and push events to the queue."""
825+
try:
826+
async for event in self._execute_delegation(
827+
ctx,
828+
signal,
829+
team_run_context,
830+
message_builder,
831+
is_member_mode,
832+
context_lock,
833+
):
834+
await queue.put(event)
835+
except Exception as exc:
836+
# Push the exception so the consumer can re-raise it
837+
await queue.put(exc)
838+
finally:
839+
await queue.put(_DONE)
813840

814841
# Run all delegations in parallel
815-
logger.debug("TeamAgent: Executing %s delegations in parallel", len(signals))
816-
results = await asyncio.gather(*[run_delegation(signal) for signal in signals])
842+
logger.debug("TeamAgent: Executing %s delegations in parallel (streaming)", len(signals))
843+
tasks = [asyncio.create_task(run_delegation(signal)) for signal in signals]
844+
finished_count = 0
845+
total = len(tasks)
846+
847+
# Consume events from the queue as they arrive
848+
while finished_count < total:
849+
item = await queue.get()
850+
if item is _DONE:
851+
finished_count += 1
852+
elif isinstance(item, Exception):
853+
# Cancel remaining tasks and re-raise the first exception
854+
for task in tasks:
855+
task.cancel()
856+
raise item
857+
else:
858+
yield item
817859

818-
# Yield all events (interaction records already added by _execute_delegation)
819-
for events in results:
820-
for event in events:
821-
yield event
860+
# Ensure all tasks are cleaned up (handles edge cases)
861+
await asyncio.gather(*tasks, return_exceptions=True)
822862

823863
def _find_member_by_name(self, name: str) -> Optional[BaseAgent]:
824864
"""Find a member agent by name.

0 commit comments

Comments
 (0)