Skip to content

Commit cff7495

Browse files
committed
fix review comment and add more comments
1 parent 3537b62 commit cff7495

File tree

2 files changed

+40
-0
lines changed

2 files changed

+40
-0
lines changed

src/agents/run.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,8 @@ def run_sync(
736736

737737
if already_running_loop is not None:
738738
# This method is only expected to run when no loop is already active.
739+
# (Each thread has its own default loop; concurrent sync runs should happen on
740+
# different threads. In a single thread use the async API to interleave work.)
739741
raise RuntimeError(
740742
"AgentRunner.run_sync() cannot be called when an event loop is already running."
741743
)
@@ -752,6 +754,7 @@ def run_sync(
752754
# We intentionally leave the default loop open even if we had to create one above. Session
753755
# instances and other helpers stash loop-bound primitives between calls and expect to find
754756
# the same default loop every time run_sync is invoked on this thread.
757+
# Schedule the async run on the default loop so that we can manage cancellation explicitly.
755758
task = default_loop.create_task(
756759
self.run(
757760
starting_agent,
@@ -767,13 +770,22 @@ def run_sync(
767770
)
768771

769772
try:
773+
# Drive the coroutine to completion, harvesting the final RunResult.
770774
return default_loop.run_until_complete(task)
771775
except BaseException:
776+
# If the sync caller aborts (KeyboardInterrupt, etc.), make sure the scheduled task
777+
# does not linger on the shared loop by cancelling it and waiting for completion.
772778
if not task.done():
773779
task.cancel()
774780
with contextlib.suppress(asyncio.CancelledError):
775781
default_loop.run_until_complete(task)
776782
raise
783+
finally:
784+
if not default_loop.is_closed():
785+
# The loop stays open for subsequent runs, but we still need to flush any pending
786+
# async generators so their cleanup code executes promptly.
787+
with contextlib.suppress(RuntimeError):
788+
default_loop.run_until_complete(default_loop.shutdown_asyncgens())
777789

778790
def run_streamed(
779791
self,

tests/test_agent_runner_sync.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,31 @@ def interrupt_once(future):
118118
monkeypatch.undo()
119119
fresh_event_loop_policy.set_event_loop(None)
120120
test_loop.close()
121+
122+
123+
def test_run_sync_finalizes_async_generators(monkeypatch, fresh_event_loop_policy):
124+
runner = AgentRunner()
125+
cleanup_markers: list[str] = []
126+
127+
async def fake_run(self, *_args, **_kwargs):
128+
async def agen():
129+
try:
130+
yield None
131+
finally:
132+
cleanup_markers.append("done")
133+
134+
gen = agen()
135+
await gen.__anext__()
136+
return "ok"
137+
138+
monkeypatch.setattr(AgentRunner, "run", fake_run, raising=False)
139+
140+
test_loop = asyncio.new_event_loop()
141+
fresh_event_loop_policy.set_event_loop(test_loop)
142+
143+
try:
144+
runner.run_sync(Agent(name="test-agent"), "input")
145+
assert cleanup_markers == ["done"], "Async generators must be finalized after run_sync returns."
146+
finally:
147+
fresh_event_loop_policy.set_event_loop(None)
148+
test_loop.close()

0 commit comments

Comments
 (0)