[dotnet-port-api] Allow function middleware to replace tools - #1178
Michelle Clayton (michelle-clayton-work) wants to merge 2 commits into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Continuation validation can still permit panics, and replacement semantics conflict with the cited upstream contract.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
What changed in this PR
Adds function replacement support to invocation middleware for auto-called and provider-managed tools.
Changes:
- Invokes middleware-selected replacement tools.
- Adds invalid-continuation errors.
- Adds replacement-path tests.
| File | Description |
|---|---|
agent/middleware.go |
Implements replacement dispatch and validation. |
agent/agent_test.go |
Tests auto-called replacement tools. |
agent/middleware_test.go |
Tests provider-managed replacement tools. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if invocation == nil { | ||
| return nil, errors.New("agent: function invocation middleware called next with nil invocation") | ||
| } | ||
| if invocation.Function == nil { | ||
| return nil, errors.New("agent: function invocation middleware called next with nil function") |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent for #1178 · copilot · auto · 117.9 AIC · ⌖ 6.43 AIC · ⊞ 12K
Comments that could not be inline-anchored
agent/middleware.go:154
The Go port always runs every registered FunctionInvocationMiddleware after a replacement, unlike upstream .NET's default-skip behavior for pending callbacks.
Upstream .NET (FunctionInvocationDelegatingAgent.CoreLogicAsync, FunctionInvocationDelegatingAgent.cs#L293-L317) invokes the replacement function directly (`aw…
|
Copilot address PR feedback |
…terminal Co-authored-by: michelle-clayton-work <262183035+michelle-clayton-work@users.noreply.github.com>
Addressed in 2d445b3: every generated continuation now validates the invocation/function before invoking the next middleware, not just the terminal continuation, so an outer middleware calling |
API consistency review
Changed Go contract
Upstream evidence reviewed
Assessment
The nil-invocation/nil-function validation added at every middleware layer (
|
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent for #1178 · copilot · auto · 142.5 AIC · ⌖ 8.12 AIC · ⊞ 12K
| } | ||
| } | ||
| return next(ctx, invocation) | ||
| return validateContinuation(next)(ctx, invocation) |
There was a problem hiding this comment.
Parity gap: replaced Function bypasses/mutates behavior differently from upstream WrapWithPendingMiddleware semantics.
Go's invocation.Function replacement here (and the corresponding FunctionInvocationContext.Function doc, agent/middleware.go:46-51) makes the replacement permanently visible to every subsequent middleware layer, in both their "before" and "after" phases, with no restoration after next returns. agent/middleware_test.go:311-313 explicitly asserts that a later registered middleware (second) observes the replacement function as invocation.Function in its own "before" hook — i.e. replacing the function always propagates forward through the whole chain by default.
Upstream .NET (dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs, commit 173978ee93e0ffa5ef4ebdbfe2e94cd6f8e8a996) does the opposite by default:
targetBeforeCallback(line 268) captures the function the callback saw on entry.- The continuation compares the replacement by reference (
ReferenceEquals(target, targetBeforeCallback), line 327): if a callback replaced the function, the replacement is invoked directly, bypassing all remaining middleware layers in the chain (testRunAsync_FirstMiddlewareReplacesFunction_LaterMiddlewareNotInvokedAsync). - After each callback's
finallyblock runs,context.Functionis explicitly restored totargetBeforeCallback(line 301), so callers and telemetry that readcontext.Functionafternextreturns still see the originally requested function, not the replacement (testRunAsync_MiddlewareReplacesFunction_RequestedFunctionRestoredAsync). - Making later middleware observe/wrap the replacement is an explicit opt-in, done by calling
context.WrapWithPendingMiddleware(replacementFunction)(dotnet/src/Microsoft.Agents.AI/FunctionInvocationContextExtensions.cs) before assigning it — it is not the default.
The Go port inverts this default: replacement always cascades to later middleware and there is no restoration for middleware/telemetry running after next returns. This is an observable behavior divergence for any Go middleware chain with more than one function-invocation middleware registered (a common configuration demonstrated in this same test file).
Suggested resolution: either (a) make replacement visible only to the continuation actually invoked (bypassing later middleware by default, mirroring ReferenceEquals gating) and restore invocation.Function to the pre-callback value once each middleware layer's next call returns, or (b) if the Go design intentionally always propagates replacements forward (a reasonable simplification given Go's synchronous, non-ambient-context middleware chain), document this explicit divergence from the upstream default in the Function field doc comment so callers relying on cross-language parity are not surprised.
|
Michelle Clayton (@michelle-clayton-work) see the latest parity findings. |

Tip
Your pull request is ready to create! 🎉 ✅
Everything is OK—the changes have been pushed to branch
dotnet-port-api-function-middleware-replace-20260923-2bed435020dcd1c9. Please review the changes, including any protected files, before creating the pull request.Create the pull request
The original pull request description is below.
Summary
Allow
agent.FunctionInvocationMiddlewareto replaceFunctionInvocationContext.Functionbefore callingnext, so middleware can redirect an invocation to a differenttool.FuncToolwhile preserving the already-surfaced tool metadata and approval flow. The Go port also adds explicit errors for invalidnext(nil)/ nil-function continuations and covers both tool auto-calling and provider-managed tool execution with replacement tests.This aligns the Go SDK with the recent upstream .NET function-middleware capability added in upstream commit
173978ee93e0ffa5ef4ebdbfe2e94cd6f8e8a996from microsoft/agent-framework#8615.Ported .NET PRs
Breaking Changes
No.
Tests and Examples
go test ./agent/...agent/agent_test.goagent/middleware_test.goNotes
Closes #1169