Skip to content

[Feature]: per-tool concurrency groups, so a pair of dependent tools can serialize without disabling parallelism everywhere #8763

Description

Description

Why the run-level switch does not cover this

FunctionInvocationConfiguration["allow_concurrent_invocation"] = False executes a response's calls in model order. It is per client, and all-or-nothing.

Our defect was two named tools racing: exchange_write(path, content) writing into a blob-backed store, and share_files_with_subagent(agent, paths) reading those paths back out. A call-batching model emits them in one message; the read raced the still-running upload and reported "not found in source store" for a file a listing tool showed moments later.

Setting allow_concurrent_invocation=False would fix that by serializing everything — including the independent microsoft_docs_search lookups the same agent routinely batches two or three at a time, where concurrency is the whole point. We would pay that latency on every turn to fix an interaction between two tools out of roughly forty. So the switch exists, we have measured what it would cost, and we are deliberately not using it; the app keeps a lock that the framework cannot see.

What we are asking for

A way for a tool to declare that it must not run concurrently with a named set of peers. Shape is upstream's call; the two obvious ones:

  • A concurrency group on the tool — @tool(concurrency_group="agent-files"), with the invocation loop serializing calls that share a group and running everything else in parallel as today. This is what Python: feat(core): add tool concurrency groups and sequential execution order #8204 proposed.
  • A declared dependency — @tool(must_follow={"exchange_write"}), which is more expressive but needs cycle detection, so probably worse value for the complexity.

The group form composes with allow_concurrent_invocation: the switch stays the blunt instrument, the group is the scalpel.

Why it belongs in the framework rather than in every app

The lock we hold is correct but invisible. It lives in application code, it is easy to forget when a third tool joins the pair, and nothing fails loudly when someone does forget — the race is timing-dependent and reproduces only with models that batch calls, which is a property of the model, not the code under test. A declaration on the tool is checkable, local to the tool that has the constraint, and survives refactoring. A framework that owns the parallel execution is the only layer that can honour it.

Reproduction, for the record

Observed live in our application with a call-batching model on an OpenAI-compatible endpoint: exchange_write("briefs/x.md", …) and share_files_with_subagent("DevOps Engineer", ["briefs/x.md"]) in one assistant message; the share reported the file missing, and the model's next-round listing showed it present. Deterministic-looking to the user, and never reproducible with a model that emits one call per round.

Code Sample

Language/SDK

Both

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agent

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions