fix(agent): build a call-scoped pool for concurrent execution (#1793) - #1909
Merged
kyegomez merged 1 commit intoAug 20, 2026
Merged
Conversation
…ez#1793) run_concurrent_tasks and talk_to_multiple_agents submitted to self.executor, which __init__ never assigns. run_concurrent_tasks caught the resulting AttributeError, logged it and fell out of the function returning None; talk_to_multiple_agents had no handler and raised it to the caller. Neither method has worked. Both now open a ContextThreadPoolExecutor for the duration of the call, which is how the rest of the codebase runs concurrent work (heavy_swarm, majority_voting, multi_agent_router). A pool held on the Agent would keep idle threads alive for the process lifetime of every agent a swarm builds, so the attribute is not reinstated. _reinitialize_after_load assigned self.executor inside a `with` block, so the executor it stored had already been shut down on exit; that assignment is removed rather than repaired, since nothing reads the attribute now. run_concurrent_tasks also re-raises instead of returning None. The bare except returning None is what hid this bug: the existing coverage at TestAgentFeatures::test_agent_concurrent_execution asserts len(results) == 3 and died with "object of type 'NoneType' has no len()", which read as a missing-credentials failure rather than a broken method. That test passes now. Closes kyegomez#1793 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Steve-Dusty
force-pushed
the
fix/agent-concurrent-executor-1793
branch
from
August 19, 2026 04:04
8814897 to
8cd5ebb
Compare
Contributor
Author
|
Rebased onto Re-verified on the new base: |
6 tasks
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1793.
run_concurrent_tasksandtalk_to_multiple_agentsboth submit toself.executor, an attribute__init__never assigns. Neither method has ever worked. They now open aContextThreadPoolExecutorscoped to the call, matching how the rest of the codebase runs concurrent work.Problem
swarms/structs/agent.py:2151and:3407both callself.executor.submit(...). The only assignment toself.executoranywhere in the class is at:2488, inside_reinitialize_after_load— a method that runs only on state load, and which assigns the executor inside awithblock, so the object it stores is already shut down by the time the block exits.The two methods fail differently, and the first one is the reason this went unnoticed:
masterrun_concurrent_tasksAttributeError, logs it, falls out of the function → returnsNonetalk_to_multiple_agentsAttributeErrorto the callerOn
master@a5764139(v14.0.0):run_concurrent_tasksreturningNoneis what disguised the bug. The existing testTestAgentFeatures::test_agent_concurrent_execution(tests/structs/test_agent.py:392) assertslen(concurrent_responses) == 3and fails withTypeError: object of type 'NoneType' has no len()— which reads like a missing-credentials problem, not a method that cannot work.Note on the issue's stated reproducer
#1793 reports the symptom as
run(imgs=[...])raisingAttributeError. That is not reproducible on currentmaster:Agent.runacceptsimgs, but its body never referencesexecutor("executor" in inspect.getsource(Agent.run)→False), and there is norun_multiple_images/run_many_imagesmethod. That path appears to have been refactored since the issue was filed on 2026-08-01. The underlying defect the issue identifies —self.executornever assigned — is real and is what this PR fixes; only the entry point named in the reproducer has moved.Fix
with ContextThreadPoolExecutor(max_workers=os.cpu_count()), the pattern already used byheavy_swarm.py:689,majority_voting.py:286andmulti_agent_router.py:502.Agent-level pool would hold idle threads for the process lifetime of every agent a swarm constructs — aHierarchicalSwarmwith 100 workers would carry 100 pools. Call-scoped costs nothing when the methods are unused, which is the common case._reinitialize_after_loadis removed, not repaired. It stored an already-closed executor; leaving it would be a trap for the next reader.run_concurrent_tasksre-raises. The bareexceptthat logged and returnedNoneis the thing that hid this for so long.Behavior-change note
run_concurrent_taskspreviously returnedNonewhen anything raised; it now propagates the exception. Any caller relying on theNonereturn to mean "batch failed" must catch instead. Given the method could not complete successfully at all before this PR, no working caller can depend on that path.talk_to_multiple_agentskeeps its per-agent isolation — one failed conversation still contributesNonerather than failing the batch.Verification
TestConcurrentExecutionPooladded to the existingtests/structs/test_agent.py(no new file, per prior review guidance). Six network-free tests built onAgent.__new__— no model client, no provider call: result-per-task ordering, failure propagation, per-agent result ordering, isolation of one failing agent, absence of theexecutorattribute, and no thread-pool leak across repeated calls.master, 4 of the 6 fail. The other two (no_executor_attribute_is_required,pool_is_shut_down_after_each_call) pass vacuously there, sincemasternever creates a pool to leak.tests/structs/test_agent.pygoes from 57 passed / 20 failed / 8 errors on cleanmasterto 64 passed / 19 failed / 8 errors. The remaining failures and errors are the pre-existing live-API tests, identical on both sides; a set difference of the two failure lists shows nothing newly broken.TestAgentFeatures::test_agent_concurrent_executionnow passes without credentials — each run returns its error string, so the batch is a list of 3 and the assertion holds, which is what the test was written to check.black==24.2.0,ruff==0.2.1, line-length 70):black --checkclean on both files,ruff check .clean repo-wide. Ruff findings inagent.pydrop from 247 to 245.Not verified here: no live provider call was made — no API keys are present in this environment. Every test stubs
Agent.run/Agent.talk_toat the method boundary, so the executor plumbing is exercised for real while the LLM call is not.