feat(runner): dependency-aware parallel tool execution#44
feat(runner): dependency-aware parallel tool execution#44windsornguyen merged 2 commits intonextfrom
Conversation
PR SummaryMedium Risk Overview Refactors Adds unit tests covering parallel timing, dependency ordering (chain/diamond), cycle fallback, error/message recording, and edge cases like empty calls and unknown deps ( Written by Cursor Bugbot for commit 5d0ce6d. This will update automatically on new commits. Configure here. |
| if isinstance(result, Exception): | ||
| # Already recorded in messages by _run_one_async. | ||
| pass | ||
| sorter.done(call_id) |
There was a problem hiding this comment.
Gather silently swallows unrecorded exceptions in parallel path
Low Severity
When multiple tools run in the asyncio.gather path with return_exceptions=True, any exception that escapes _run_one_async (not caught by its internal except Exception) is captured as a result but never recorded in messages. The comment "Already recorded in messages by _run_one_async" is incorrect for this case. Meanwhile sorter.done(call_id) is still called, so dependent tools proceed without the prerequisite's result message — leading to a malformed conversation. In the single-tool path, the same exception propagates correctly and aborts execution. This inconsistency means the same failure mode is a clear error for one tool but silent data loss for two or more.
🔬 Verification Test
Why verification test was not possible: The development VM was unreachable during analysis (all tool calls returned "Pod exists but exec-daemon is unreachable"). However, this bug is demonstrable through static analysis: the pass on line 142 takes no corrective action (no message recorded, no re-raise), and sorter.done(call_id) on line 143 runs unconditionally regardless of whether the exception was recorded. The comment on line 141 is provably incorrect — _run_one_async only records errors inside its own except Exception block, and any exception that escapes was by definition NOT caught there.
Additional Locations (1)
|
Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.
Or push these changes by commenting: Preview (bae5551f54)diff --git a/src/dedalus_labs/lib/runner/_scheduler.py b/src/dedalus_labs/lib/runner/_scheduler.py
--- a/src/dedalus_labs/lib/runner/_scheduler.py
+++ b/src/dedalus_labs/lib/runner/_scheduler.py
@@ -137,9 +137,15 @@
)
for call_id, result in zip(ready, results):
- if isinstance(result, Exception):
- # Already recorded in messages by _run_one_async.
- pass
+ if isinstance(result, BaseException):
+ if isinstance(result, Exception):
+ # Already recorded in messages by _run_one_async.
+ pass
+ else:
+ # BaseException subclass (e.g. KeyboardInterrupt,
+ # CancelledError) not caught by _run_one_async —
+ # re-raise to match single-tool path behavior.
+ raise result
sorter.done(call_id) |
|
Need to tighten up the types. |



Summary
Dependency-aware parallel local tool execution. The SDK topo-sorts pending tool calls by declared dependencies and fires independent tools concurrently via asyncio.gather. 5x speedup on independent tools.
Changes
_scheduler.py: topo-sort via graphlib.TopologicalSorter, layer-by-layer parallel dispatch, cycle fallbackcore.py: all 4 execution paths (async/sync x streaming/non-streaming) delegate to schedulercore.py: _ModelConfig replaced with api_kwargs passthrough -- full CompletionCreateParamsBase mirrored on run()Test plan