Skip to content

Add function invocation middleware with call identity - #1093

Merged
Quim Muntal (qmuntal) merged 9 commits into
microsoft:mainfrom
gabisonia:feat/tool-invocation-identity
Sep 22, 2026
Merged

Quim Muntal (qmuntal) merged 9 commits into
microsoft:mainfrom
gabisonia:feat/tool-invocation-identity

Conversation

@gabisonia

@gabisonia Irakli Gabisonia (gabisonia) commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Related to #949

Adds function-invocation middleware so applications can inspect the tool, call ID, arguments, result, and error for each call. Register callbacks in agent.Config.FunctionMiddlewares, separately from agent middleware. Automatic tool execution and Copilot apply the callbacks to function tools regardless of whether they come from configured tools, run options, context providers, or AdditionalTools. FuncTool.Call(ctx, args) stays unchanged.

audit := agent.FunctionInvocationMiddleware(func(
    next func(context.Context, *agent.FunctionInvocationContext) (any, error),
    ctx context.Context,
    invocation *agent.FunctionInvocationContext,
) (any, error) {
    log.Printf("Starting %s: %s", invocation.Function.Name(), invocation.CallID)
    result, err := next(ctx, invocation)
    log.Printf("Finished %s: %v", invocation.CallID, err)
    return result, err
})

Register the callback with agent.Config{FunctionMiddlewares: []agent.FunctionInvocationMiddleware{audit}}. There is no ordering requirement relative to context providers or agent middleware. Callbacks can replace arguments, inspect or replace returned results/errors, or skip the underlying tool by not calling next. Skipping one invocation does not stop the whole agent loop. Tool schemas and approval requirements are preserved, and callbacks run after approval. Call identity is exposed through FunctionInvocationContext.CallID; both toolautocall and Copilot supply it through internal context helpers.

#949 mentions normalized arguments; this implementation exposes raw JSON to match Go's existing FuncTool.Call API. Validation and normalization remain the underlying tool's responsibility.

Registration copies the callback slice and ignores nil callbacks. Execution uses internal tool wrappers with the first registered callback outermost. AdditionalTools remain absent from provider requests, and request tools retain precedence. Missing call IDs remain empty, and IDs are for correlation, not authorization or cross-run idempotency.

Validation: full race-enabled suite (go test -race ./...), targeted regression tests, and golangci-lint (zero issues). Tests are in existing files and cover concurrent calls, approval/session restoration, context propagation, callback ordering, argument/result changes, errors, and repeated runs. Provider tests cover allowing/blocking configured and context-provider tools through NewChatCompletionsAgent, and Copilot call IDs with and without trace contexts. An offline sample was also run locally.

@github-actions github-actions Bot added area:agent Changes files in the agent area area:internal Changes files in the internal area area:tool Changes files in the tool area size:xlarge More than 300 changed lines or 10 files kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure labels Sep 16, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the public-api-change Pull Request changes public APIs label Sep 16, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@gabisonia

Copy link
Copy Markdown
Contributor Author

Quim Muntal (@qmuntal) could you take a look at the API when you have time? I kept arguments as raw JSON to match FuncTool.Call. Would you prefer normalized arguments here?

@github-actions

This comment has been minimized.

@github-actions github-actions Bot 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.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • pypi.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "pypi.org"

See Network Configuration for more information.

Generated by Go API Consistency Review Agent · copilot · auto · 133 AIC · ⌖ 6.38 AIC · ⊞ 9.2K

Comment thread agent/middleware.go
@github-actions

This comment has been minimized.

@qmuntal Quim Muntal (qmuntal) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Like the overall approach 😄

Comment thread agent/middleware.go Outdated
// Multiple callbacks execute in registration order, with the first outermost.
// Each call gets its own FunctionInvocationContext; callbacks must synchronize
// shared application state if tools can execute concurrently.
type FunctionInvocationMiddleware func(ctx context.Context, invocation *FunctionInvocationContext, next FunctionInvocationFunc) (any, error)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

next should be the first parameter, matching Middleware ordering.

Comment thread agent/middleware.go Outdated
Comment thread agent/middleware.go Outdated
Comment thread tool/tool.go Outdated
Comment thread internal/toolcontext/context.go Outdated
@github-actions github-actions Bot removed the area:tool Changes files in the tool area label Sep 22, 2026
@gabisonia

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Updated the API and moved the internal helpers into one package. Also merged latest main.

@github-actions

This comment has been minimized.

@gabisonia
Irakli Gabisonia (gabisonia) marked this pull request as ready for review September 22, 2026 08:21
Copilot AI lite review requested due to automatic review settings September 22, 2026 08:21

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.

Copilot review overview

🟡 Changes recommended

The context-provider middleware test currently registers middleware too early to wrap the provider-added tool, causing the expected callback order to fail.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Adds function-invocation middleware with call-ID propagation, argument/result interception, approval compatibility, and support for additional tools.

Changes:

  • Adds invocation context and composable middleware.
  • Propagates call IDs through tool execution.
  • Expands coverage for concurrency, approvals, and additional tools.
File Summary
internal/​toolmiddleware/​wrapper.go Internal tool wrappers and call-ID utilities.
agent/​middleware.go Invocation context and middleware implementation.
agent/​harness/​toolautocall/​autocall.go Applies wrappers and propagates call IDs.
agent/​harness/​toolautocall/​autocall_test.go Tests identity, concurrency, errors, and additional tools.
agent/​harness/​toolautocall/​autocall_approval_test.go Tests approval-flow identity handling.
agent/​agent_test.go Composition tests require middleware registration through ProviderConfig.Middlewares for context-provider tools.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agent/agent_test.go Outdated

@qmuntal Quim Muntal (qmuntal) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  1. Context-provider tools bypass middleware with the standard constructors.
    Config.Middlewares runs agent.go:94-95, but the chat.go:80-98 doesn’t expose ProviderConfig.Middlewares, where the PR recommends registering it.

    Reproduced with NewChatCompletionsAgent: middleware blocked a configured tool, but the same tool supplied by a context provider executed with zero middleware callbacks. There needs to be an accessible registration point after context collection, or deferred wrapping of newly added tools.

  2. Copilot tool calls lose their call identity.
    The copilot.go:493-502 calls FuncTool.Call directly, without attaching the SDK’s ToolCallID. The middleware executes, but receives an empty CallID despite the SDK providing one. This path should attach the same internal invocation metadata as toolautocall.

@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/copilot Changes files in the provider / copilot area area:provider/openai Changes files in the provider / openai area labels Sep 22, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Sep 22, 2026

@qmuntal Quim Muntal (qmuntal) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I’d avoid adding ProviderMiddlewares. Could we use Config.FunctionMiddlewares and apply it around each tool call instead? That would cover context-provider tools and AdditionalTools without requiring users to get the middleware ordering right.

.NET and Python also separate function middleware registration from execution.

@gabisonia

Copy link
Copy Markdown
Contributor Author

Makes sense, I’ll switch to Config.FunctionMiddlewares so users don’t need to worry about ordering.

@github-actions

Copy link
Copy Markdown
Contributor

Scope: public API, user-visible behavior

Changed Go contract: agent.FunctionInvocationMiddleware, agent.FunctionInvocationContext (Function, CallID, Arguments), new Config.FunctionMiddlewares field, functionInvocationTool wrapping wired through internal/toolmiddleware.Wrapper into agent/harness/toolautocall (createToolsMap, processFunctionCall) and provider/copilotprovider (copilotTools, toCopilotTool).

Upstream evidence reviewed:

  • .NET: dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs (MiddlewareEnabledFunction.InvokeCoreAsync, MiddlewareEnabledTools) and FunctionInvocationDelegatingAgentBuilderExtensions.cs (AIAgentBuilder.Use(...) callback signature (AIAgent, FunctionInvocationContext, next, CancellationToken)).
  • Python: python/packages/core/agent_framework/_middleware.py (FunctionInvocationContext, FunctionMiddleware.process, FunctionMiddlewarePipeline.execute/_register_middleware) and python/packages/core/agent_framework/_tools.py (call_id correlation, middleware invocation only after approval/argument preparation, lines ~2080-2165).

Result: aligned.

Findings:

  1. Ordering — Go registers the first FunctionMiddlewares entry outermost (agent/middleware.go, slices.Backward); this matches Python's FunctionMiddlewarePipeline.execute (create_next_handler(0) invokes self._middleware[0] first) and .NET's MiddlewareEnabledTools/MiddlewareEnabledFunction chain (scope.Middleware.Insert(0, middleware) to preserve registration order).
  2. Approval semantics — Go's functionInvocationTool.Call only invokes middleware when the underlying tool actually executes, not when an approval request is generated; autocall_approval_test.go's TestFunctionInvoking_InvocationIdentityAfterApproval confirms zero middleware calls before approval. This matches .NET's FunctionInvokingChatClient-gated InvokeCoreAsync and Python's _tools.py invocation path, where middleware also only runs at actual invocation time.
  3. Tool source coverage — Go wraps tools regardless of origin (Config.Tools, context-provider-supplied tools, and toolautocall.Config.AdditionalTools), mirroring .NET's MiddlewareEnabledTools wrapping the live ChatOptions.Tools collection independent of how tools were added, and Python's middleware pipeline operating over the live tool list per invocation.
  4. Raw vs. normalized arguments — Go exposes raw JSON Arguments rather than a normalized/validated mapping (as Python's FunctionInvocationContext.arguments does after provisional validation). The PR description explicitly acknowledges this as an intentional, documented divergence to preserve Go's existing FuncTool.Call(ctx, args string) contract rather than reintroducing removed argument-normalization machinery. This is a reasonable language-specific choice, not a parity defect, but maintainers may want to track it against Expose tool invocation identity through delegating-agent middleware #949 if normalized-argument access becomes a future requirement.

No unconditional-default or opt-in-gating mismatches were found: FunctionMiddlewares is empty/nil by default and only takes effect when explicitly registered, consistent with .NET's opt-in AIAgentBuilder.Use(...) and Python's opt-in middleware= list, so there is no divergence in default behavior.

Generated by Go API Consistency Review Agent · copilot · auto · 93.4 AIC · ⌖ 4.79 AIC · ⊞ 9.2K · ◷

@qmuntal Quim Muntal (qmuntal) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!

@qmuntal
Quim Muntal (qmuntal) added this pull request to the merge queue Sep 22, 2026
Merged via the queue into microsoft:main with commit 0141976 Sep 22, 2026
28 checks passed
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 area:internal Changes files in the internal area area:provider/copilot Changes files in the provider / copilot area area:provider/openai Changes files in the provider / openai area area:provider Changes files in the provider area kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure parity-approved Go API consistency review found no parity issues public-api-change Pull Request changes public APIs size:xlarge More than 300 changed lines or 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants