Skip to content

.NET: [Bug]: ChatOptions.Tools snapshot vs in-place mutation mid-run; FunctionInvocationResult vs exceptions (dynamic tooling)` #5325

@torsilver

Description

@torsilver

Description

Summary

We integrate Microsoft.Extensions.AI (currently 10.5.x) behind Microsoft.Agents.AI ChatClientAgent for a host-based copilot. We use dynamic tooling: only a bootstrap tool subset is sent first; the model calls activate_tools to add more tools in the same agent run. We hit friction around ChatOptions.Clone copying the Tools list, stale snapshots vs FunctionInvokingChatClient, and tool failures surfaced as FunctionInvocationResult instead of exceptions.

Related upstream discussions: #7217, #7218.

Environment

Microsoft.Extensions.AI: 10.5.x (upgraded from 10.4 while investigating tool resolution)

Microsoft.Agents.AI: 1.0.0

Scenario: same streaming ChatClientAgent session; tools expanded after activate_tools without restarting the agent.

  1. ChatOptions.Tools and Clone / snapshot consistency

Observed: After activate_tools succeeds, a subsequent model tool_calls for a newly activated function sometimes fails with tool not found (FunctionInvocationStatus.NotFound), while logs show ChatOptions.Tools count still matching an older snapshot.

Our understanding: ChatOptions.Clone() copies the Tools collection. If the app mutates the live tool list bound to the current turn but outer layers still hold a cloned ChatOptions with an old Tools reference, FunctionInvokingChatClient may still resolve against the stale list.

Our workaround: A thin IChatClient decorator immediately inside FunctionInvokingChatClient that, on every GetResponseAsync / GetStreamingResponseAsync, patches options.Tools from the current authoritative tool list for that pass (same mutable list the app updates after activation).

Ask: Please document (or formalize) the contract for:

When ChatOptions is cloned along the pipeline, and whether Tools is shared or snapshotted.

How FunctionInvokingChatClient is expected to behave if Tools is updated in place during a single multi-step run.

If the intended pattern is “always replace ChatOptions entirely,” say so; if “in-place list mutation is supported,” clarify how layers should observe updates.

  1. FunctionInvocationResult vs thrown exceptions

Observed: Tool failures (including NotFound) may not propagate as thrown exceptions; they appear as FunctionInvokingChatClient.FunctionInvocationResult with Status / Exception.

Our workaround: Middleware inspects FunctionInvocationResult and maps NotFound, Exception, etc., to user-visible / telemetry-friendly messages—try/catch alone is insufficient.

Ask: Document prominently that middleware must handle the envelope type, not only exceptions; optionally provide a small helper or guideline for middleware authors.

Minimal repro sketch (conceptual)

Register bootstrap tools + an activate_tools tool that adds entries to the same List used as ChatOptions.Tools (or replaces the list).

In one run: model calls activate_tools, then calls a newly activated tool by name.

If any layer still uses a cloned ChatOptions from before activation, NotFound can reproduce.

We can trim this to a standalone public console sample if maintainers want a repro (our application repo is private; we won’t link it in the issue).

Code Sample

Error Messages / Stack Traces

Package Versions

Microsoft.Extensions.AI (currently 10.5.x)

.NET Version

net10.0

Additional Context

No response

Metadata

Metadata

Assignees

Labels

.NETbugSomething isn't working

Type

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions