Skip to content

fix(tools): route bound task and batch tools' sync path through the bound runtime - #5861

Merged
WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-bind-batch-tools-sync-path-bypasses-submitter
Sep 25, 2026
Merged

WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-bind-batch-tools-sync-path-bypasses-submitter

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Why

get_available_tools wraps the process-wide tool singletons in place with a sync func (backend/packages/harness/deerflow/tools/tools.py:72, inside _ensure_sync_invocable_tool, reached from lines 172 and 305) so synchronous agent callers can invoke them. bind_task_tool (backend/packages/harness/deerflow/tools/builtins/task_tool.py:457 on main) and _bind_batch_tool (backend/packages/harness/deerflow/tools/builtins/batch_task_tool.py:84 on main) rebind only the coroutine of their copies, so each copy inherits the singleton's func — a sync wrapper around the unbound coroutine. A synchronous invocation of a bound copy therefore bypassed the explicit SDK submitter / execution capacity and fell through to the process-global one — the exact fallback bind_batch_tools' docstring rules out ("never fall through to another application's process-global submitter") — or raised NotImplementedError in a fresh process where the singleton had never been wrapped.

What changed

  • bind_task_tool and _bind_batch_tool now also rebind the copy's func to make_sync_tool_wrapper(bound_coroutine, ...) — the same helper _ensure_sync_invocable_tool uses — so both invocation paths of a bound copy go through the bound coroutine and its runtime's own submitter/capacity.
  • Bound copies now always expose a usable sync path, even when created before any sync wrap has happened.

Surface area

  • Agents / LangGraph — tool binding under backend/packages/harness/deerflow/tools/ (no prompt changes, no new data sources in text)
  • Frontend UI
  • Backend API
  • Sandbox
  • Skills
  • Dependencies
  • Default behavior change
  • Docs / tests / CI only

Bug fix verification

  • Test path that reproduces the bug:
    • backend/tests/test_batch_task_tool.py::test_bound_batch_tools_sync_path_uses_the_explicit_submitter
    • backend/tests/test_batch_task_tool.py::test_bound_batch_tools_expose_a_sync_invocation_path
    • backend/tests/test_task_tool_core_logic.py::test_bound_task_tool_sync_path_uses_the_explicit_capacity
  • Did it go red on main and green on this branch? yes — with the two source changes reverted (tests kept), all three fail: the batch sync path returns a fallback-submitter result instead of the explicit submitter's, bound copies have func is None, and the task sync path raises KeyError: 'execution_capacity'. All three pass on this branch.

Validation

All commands run in backend/:

  • make lint → All checks passed! / 1633 files already formatted
  • New regression tests → 3 passed
    PYTHONPATH=. uv run pytest tests/test_batch_task_tool.py::test_bound_batch_tools_sync_path_uses_the_explicit_submitter tests/test_batch_task_tool.py::test_bound_batch_tools_expose_a_sync_invocation_path tests/test_task_tool_core_logic.py::test_bound_task_tool_sync_path_uses_the_explicit_capacity
  • Module tests → 103 passed
    PYTHONPATH=. uv run pytest tests/test_batch_task_tool.py tests/test_task_tool_core_logic.py
  • Related suites → 32 passed (tests/test_batch_acceptance.py), 14 passed (tests/test_mcp_sync_wrapper.py tests/test_batch_extension_snapshot.py)
  • Full default suite (PYTHONPATH=. uv run pytest -m "not live" --ignore=tests/blocking_io tests/ -q): 19474 passed, 17 failed on this branch vs 19471 passed, 20 failed with the fix reverted — the 3 extra failures without the fix are the new regression tests; the 17 remaining failures are pre-existing on main in this environment (browser automation / web-fetch / extension-install tests, untouched by this change).
  • Also verified that LangGraph's injected-arg detection (_get_all_injected_args) still resolves runtime and tool_call_id on the bound copies, since the new wrapper's __wrapped__ chain points at the bound coroutine: runtime and tool_call_id are detected for batch_task/task, runtime for batch_status/cancel_batch, matching the singletons.

AI assistance

Tool(s) used: Claude Code (autonomous coding agent, submitted from the BlueX888 account)

How you used it: The agent diagnosed the root cause, wrote the fix and the regression tests, and ran every validation command listed above; this PR text was also drafted by the agent.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

…ound runtime

bind_task_tool and _bind_batch_tool copied their process-wide singletons
with only the coroutine rebound. Once get_available_tools had wrapped a
singleton in place with a sync func, the copy inherited that wrapper
around the unbound coroutine, so synchronous invocation bypassed the
explicit SDK submitter/capacity and fell through to the process-global
one -- or raised NotImplementedError in a fresh process where the
singleton was never wrapped.

Rebind the copy's func to a sync wrapper around the bound coroutine so
both invocation paths use the runtime's own submitter and capacity.
@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines labels Sep 25, 2026

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 426a017 — no blocking findings; the fix looks correct and well-tested.

Verification performed:

  • Reproduced the bug: with the two source changes reverted to the base revision, all three new regression tests fail (KeyError: 'execution_capacity' on the task sync path, fallback-submitter result / func is None on the batch sync path); all pass with the fix applied, and the full test_batch_task_tool.py + test_task_tool_core_logic.py + test_batch_acceptance.py suites pass (135 tests).
  • Checked the concern about LangGraph's injected-arg detection on the bound copies: langgraph.prebuilt.tool_node._get_all_injected_args returns identical results for the singleton, the old bound copy, and the new bound copy (runtime + tool_call_id for batch_task, runtime for batch_status/cancel_batch), so the new sync wrapper does not disturb ToolNode injection.
  • Searched for other model_copy sites that rebind only coroutine; bind_task_tool and _bind_batch_tool are the only ones with this pattern, and _clone_tool_with_description clones keep the original coroutine so they are unaffected.
  • The monkeypatch snapshot idiom (setattr(tool, "func", tool.func) before the in-place _ensure_sync_invocable_tool wrap) correctly restores the process-wide singletons after each test, and ruff format --check / ruff check are clean on all four changed files.

The change is minimal and matches the existing sync-wrapper machinery (make_sync_tool_wrapper) already used by _ensure_sync_invocable_tool.

@WillemJiang WillemJiang added this to the 2.2.0 milestone Sep 25, 2026
@WillemJiang
WillemJiang merged commit 5242fcf into bytedance:main Sep 25, 2026
16 checks passed
hawkli-1994 pushed a commit to rongxinzy/deer-flow that referenced this pull request Sep 27, 2026
…ound runtime (bytedance#5861)

bind_task_tool and _bind_batch_tool copied their process-wide singletons
with only the coroutine rebound. Once get_available_tools had wrapped a
singleton in place with a sync func, the copy inherited that wrapper
around the unbound coroutine, so synchronous invocation bypassed the
explicit SDK submitter/capacity and fell through to the process-global
one -- or raised NotImplementedError in a fresh process where the
singleton was never wrapped.

Rebind the copy's func to a sync wrapper around the bound coroutine so
both invocation paths use the runtime's own submitter and capacity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants