Skip to content

Add composable function-invocation middleware to the tool autocall loop - #638

Open
PratikDhanave (PratikDhanave) wants to merge 4 commits into
microsoft:mainfrom
PratikDhanaveFork:function-invocation-middleware
Open

Add composable function-invocation middleware to the tool autocall loop#638
PratikDhanave (PratikDhanave) wants to merge 4 commits into
microsoft:mainfrom
PratikDhanaveFork:function-invocation-middleware

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

What

Adds a per-tool-call interception point to the automatic tool-calling loop.

  • New agent.FunctionInvocationContext (Function, Arguments, CallContent, Result, Terminate, Iteration) and agent.FunctionInvocationMiddleware in agent/middleware.go.
  • New FunctionMiddleware []agent.FunctionInvocationMiddleware on toolautocall.Config, threaded onto the autocall struct and composed around tool.Call inside processFunctionCall.

A middleware can:

  • mutate Arguments before next runs (the tool sees the mutated args),
  • replace the invocation result by returning a different value from next,
  • set Terminate to stop the loop after the current round of results is returned.

The chain composes outermost-first, matching the run-level runChain composition: the first configured middleware runs first and wraps the rest. Iteration carries the zero-based tool-calling round.

Why

The run-level Middleware only wraps a whole RunFunc; there was no way to intercept an individual tool invocation. This is a real gap versus the other SDKs:

  • .NET: FunctionInvocationDelegatingAgent + AIAgentBuilder.Use(function callback) intercept each function call.
  • Python: FunctionMiddleware does the same.

This brings the Go port to parity for that per-call hook while keeping the change surgical (no behavior change when FunctionMiddleware is empty).

How it's tested

TestFunctionInvoking_FunctionMiddleware in the canonical autocall_test.go registers two middleware and, driving the existing agenttest harness, asserts:

  • (a) arguments mutated before next are what the tool observes,
  • (b) a result replaced after next is what the FunctionResultContent carries,
  • (c) Terminate stops the loop (the provider is called exactly twice; the extra turn is never consumed),
  • Iteration increments across rounds and the chain composes outermost-first.

go build ./..., go vet ./agent/..., and go test ./agent/... pass.

Open design questions

Opening as a draft to align on the API before finalizing:

  • API shape / placement: types live in the agent package (alongside run-level Middleware) and are wired only through toolautocall.Config. Should there also be an agent-builder-level Use(...) convenience mirroring .NET's AIAgentBuilder.Use, or is Config wiring sufficient?
  • Result source of truth: currently the value returned from the chain becomes the function result (Result is populated for inspection). .NET mutates context.Result in place — should we standardize on one?
  • Concurrency under AllowConcurrentInvocations: each call gets its own FunctionInvocationContext, so per-call state is isolated, but user middleware sharing state across parallel calls must synchronize themselves. Do we want to document this contract, guarantee ordering, or serialize middleware regardless of the concurrency setting (as .NET's delegating agent effectively does)?
  • Terminate semantics: today Terminate stops the loop after the current round's results are streamed back. Should a terminating call instead short-circuit remaining calls in the same round?

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the public-api-change Pull Request changes public APIs label Jul 24, 2026
Introduce FunctionInvocationContext and FunctionInvocationMiddleware in the
agent package, exposing a per-tool-call interception point that the run-level
Middleware cannot express. Wire a FunctionMiddleware chain into the toolautocall
Config so middleware can mutate arguments before the tool runs, replace the
result afterward, and set Terminate to stop the tool-calling loop. The chain
composes outermost-first, mirroring the run-level middleware composition.

This matches .NET's FunctionInvocationDelegatingAgent and Python's
FunctionMiddleware, which both intercept individual function invocations.
@github-actions

This comment has been minimized.

@PratikDhanave
PratikDhanave (PratikDhanave) marked this pull request as ready for review August 4, 2026 06:06
@PratikDhanave
PratikDhanave (PratikDhanave) requested a review from a team as a code owner August 4, 2026 06:06
Copilot AI lite review requested due to automatic review settings August 4, 2026 06:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new per-tool-call middleware hook to the toolautocall harness so callers can intercept each tool invocation (mutate arguments, wrap/replace results, and optionally request loop termination) without changing behavior when no middleware is configured.

Changes:

  • Introduces agent.FunctionInvocationContext and agent.FunctionInvocationMiddleware for per-tool-call interception.
  • Wires FunctionMiddleware through toolautocall.Config and composes it around tool.Call in the autocall loop (with Iteration and Terminate support).
  • Adds a focused test validating argument mutation, result replacement, outermost-first composition, iteration counting, and termination behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
agent/middleware.go Adds new per-invocation middleware context + middleware function type in the agent package.
agent/harness/toolautocall/autocall.go Threads FunctionMiddleware into autocall execution and composes middleware around each tool call; adds termination propagation for the main loop.
agent/harness/toolautocall/autocall_test.go Adds TestFunctionInvoking_FunctionMiddleware to validate middleware composition and semantics.

Comment on lines 684 to 689
for i, fc := range funcCalls {
go func() {
defer wg.Done()
parallelResults[i] = f.processFunctionCall(ctx, tools, fc)
parallelResults[i] = f.processFunctionCall(ctx, tools, fc, iteration)
}()
}
return nil, errCount, nil
}
newMsg, errCount, err := f.processFunctionCalls(ctx, tools, funcCalls, errCount)
newMsg, errCount, _, err := f.processFunctionCalls(ctx, tools, funcCalls, errCount, 0)
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added area:agent Changes files in the agent area size:large At most 300 changed lines across at most 10 files pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added failed-auto-risk Automatic risk classification was inconclusive or failed and removed pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress risk:medium Contained production impact requiring normal review depth and removed failed-auto-risk Automatic risk classification was inconclusive or failed pending-auto-risk Automatic risk classification is in progress labels Aug 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Parity Review — public-api-change confirmed ✅

This PR ports per-call function-invocation middleware to Go. The upstream equivalents are:

  • Python: FunctionMiddleware / FunctionInvocationContext in python/packages/core/agent_framework/_middleware.py
  • .NET: FunctionInvocationDelegatingAgent + AIAgentBuilder.Use(function callback) in dotnet/src/Microsoft.Agents.AI/

New exported Go surface

Go identifier Upstream equivalent
agent.FunctionInvocationContext Python FunctionInvocationContext / .NET FunctionInvocationContext (from Microsoft.Extensions.AI)
agent.FunctionInvocationMiddleware (func type) Python FunctionMiddlewareCallable / .NET Func<AIAgent, FunctionInvocationContext, ...>
toolautocall.Config.FunctionMiddleware []agent.FunctionInvocationMiddleware Python middleware= on Agent / .NET AIAgentBuilder.Use(...)

Parity assessment

The core semantics — intercept each tool call, mutate arguments before next, observe/replace the result after next, and optionally terminate the loop — are present in all three SDKs. No behavior-divergence parity issues found.

Minor observations (not blocking)

  1. metadata / AdditionalProperties field missing — Python's FunctionInvocationContext exposes a metadata: dict for middleware-to-middleware state sharing. .NET's FunctionInvocationContext (from Microsoft.Extensions.AI) has AdditionalProperties. Go's FunctionInvocationContext currently omits this. This is a minor gap worth noting for a future iteration.

  2. session field missing — Python's FunctionInvocationContext carries the current AgentSession. Go has no equivalent field. This is a minor gap; no parity concern today since Go sessions are threaded differently.

  3. agent reference in .NET callback — .NET's Use callback receives the AIAgent instance as a first parameter. Go's FunctionInvocationMiddleware does not. The PR's open design question about Use(...) builder-level convenience mirrors this; the current Config-wiring approach is functionally equivalent. No parity issue.

  4. FunctionInvocationMiddleware placement — Types live in the agent package and are wired via toolautocall.Config, which mirrors Python's Agent(middleware=[...]) and is consistent with Go idioms. The public-api-change label is correctly applied.

Conclusion

The PR brings Go to parity with Python and .NET for per-call function-invocation interception. No divergence in default behavior, enablement, or observable semantics was found. The parity-approved label is warranted pending resolution of the open design questions noted in the PR description.

Generated by Go API Consistency Review Agent · sonnet46 · 56.4 AIC · ⌖ 5.22 AIC · ⊞ 6K ·

@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Changes files in the agent area parity-approved Go API consistency review found no parity issues public-api-change Pull Request changes public APIs risk:medium Contained production impact requiring normal review depth size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants