You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 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.
Description
Why the run-level switch does not cover this
FunctionInvocationConfiguration["allow_concurrent_invocation"] = Falseexecutes 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, andshare_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=Falsewould fix that by serializing everything — including the independentmicrosoft_docs_searchlookups 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:
@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.@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", …)andshare_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