A delegation waits on its own sub-thread

What went wrong

The function invoker runs the tool calls of one model turn concurrently (AllowConcurrentInvocation = true in ChatClientAgentFactory). A delegate_to_agent call waits in two steps:

  1. It waits for the chat's delegation stream (IAgentChat.Delegations) to report that the sub-thread was created (Dispatched) or could not be created (Failed).
  2. It then reads that sub-thread's node until the round ends, bounded by WaitForDelegationResult's 10-minute backstop.

Step 1 took the next Dispatched or Failed on the stream and did not check whose it was. When two delegations ran in the same turn, both calls subscribed before either sub-thread existed. The first sub-thread to be created then settled both calls. The effects:

Nothing errored and nothing was logged. The model simply got a wrong tool result back. A related problem: the stream was a bare Subject, and concurrent delegations call OnNext from different threads, which breaks the ordering every subscriber's Where and Take relies on.

The fix

The CreateDelegationTools overload whose executeAsync takes no call id still exists, marked [Obsolete]. It keeps the old uncorrelated wait, because an implementation that never receives the id cannot stamp it. That wait is only right while one call is waiting for its start, and [Obsolete] warns only a caller that is recompiled. So a tool built by that overload refuses a delegate_to_agent call that arrives while another one is still waiting for its Dispatched or Failed: the call returns an Error: … tool result naming the overlap, and the model can call again. Once the first call's start has settled, the next call is accepted.

ConcurrentDelegationCorrelationTest covers this. It runs two calls on one chat and reports the second call's failure first. With the call-id filter removed, call A reports B's error and the test fails. A foreign event, emitted first, is the negative control: it settles neither call. TheUncorrelatedOverload_RefusesAnOverlappingStart covers the retired overload: the overlapping call is refused, the first call still reports its own result, and a call made after that start settled is accepted.

What this does not establish

MeshWeaver#5915's own incident was a delegate_to_agent call that ran 23.6 minutes until the 30-minute round cap cut it. By the code, a dispatched delegation returns within the 10-minute backstop, and a create that never answers fails within the request timeout. That leaves the following open:

Nothing here extends the round cap or adds a bound.