Skip to content

Python: document the shape contract of FunctionInvocationContext.result - #8283

Merged
Eduard van Valkenburg (eavanvalkenburg) merged 2 commits into
microsoft:mainfrom
orangeCatDeveloper:docs/function-middleware-result-shape
Sep 11, 2026
Merged

Eduard van Valkenburg (eavanvalkenburg) merged 2 commits into
microsoft:mainfrom
orangeCatDeveloper:docs/function-middleware-result-shape

Conversation

@orangeCatDeveloper

Copy link
Copy Markdown
Contributor

Motivation & Context

FunctionInvocationContext.result is documented with one sentence that covers two
different contracts. After call_next() it holds FunctionTool.invoke's parsed output —
a list[Content] — but the same attribute accepts a raw value of any type when middleware
override the result. A middleware author reading "Function execution result. Can be
observed after calling call_next() to see the actual execution result" reasonably writes
if not isinstance(context.result, str): return, which silently never fires: no error, no
log line. The natural unit test for that middleware — build a context, assign
result = "text", assert — passes, because the write side really does accept a string.
Test and production disagree and nothing reports it.

The repo's own function-middleware sample models the wrong side of that contract, so the
mistake is inherited by imitation.

Description & Review Guide

  • What are the major changes?

The docstrings now describe the attribute as what it is: untyped. The pipeline assigns
FunctionTool.invoke's output at the innermost link of the chain only
(FunctionMiddlewarePipeline.execute), so an outer middleware observes whatever the inner
ones left behind, not the tool's output. The docs say that, name result_parser=SKIP_PARSING
as the one thing that changes the parsed shape, and state the override rule that is easy to
get wrong: only list[Content] and str survive Content.from_function_result intact —
a bare Content or a partially rebuilt list is stringified into one text item.

shared_state_middleware.py was f-stringing the list[Content] and assigning the string
back, which sent a Python repr to the model instead of the tool's text. It now rebuilds
the text items.

before:  final wire text: '[Call #1] [<agent_framework._types.Content object at 0x106cec440>]'

after:   final wire text: '[Call #1] Weather in Seattle: sunny'

Captured by driving the real FunctionMiddlewarePipeline with a real @tool and
FunctionTool.invoke as the final handler, then wrapping the result the way
_finalize_function_result does.

  • What is the impact of these changes?

No runtime behavior change in the framework — docstrings plus one sample. The sample's
output text changes, for the better.

test_middleware_context_result.py::test_function_middleware_post_execution_override
is worth a look while reviewing: its final_handler returns a str, so the middleware's
if "modify" in context.result passes there while the same check would be silently False
against FunctionTool.invoke's list[Content] in production. That is the issue's point
about stub-shaped middleware tests, in this repo's own suite. I left it alone rather than
grow this PR into a test rewrite.

Deliberately not covered: the issue's suggestion 2 (a public result_text accessor, which
would let security.py drop its private _ensure_content_list) and suggestion 3 (making
the type honest at the seam, a breaking change). Both are API decisions worth their own
issue rather than a doc PR. security.py's "typically list[Content]" hedge is left as
is — with the chain behavior now documented, that wording turns out to be accurate.

Related Issue

Part of #7575

Contribution Checklist

  • The code builds clean without any errors or warnings (ruff check / ruff format --check clean)
  • All unit tests pass, and I have added new tests where possible — no new tests: docstrings plus one sample, no framework behavior changes. I could not run the suite locally (no uv on this machine), leaving it to CI.
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

FunctionInvocationContext.result documents one contract but carries two:
middleware observe FunctionTool.invoke's parsed output and may write back a
value of any type, so a type check written from the docs silently never fires.
The shared-state sample modeled the wrong side and sent a Python repr of the
Content list to the model instead of the tool's text.

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.

🟡 Changes recommended

The documentation incorrectly omits the bare function_approval_request content that passes through unchanged.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Clarifies the Python function middleware result-shape contract and corrects the shared-state sample’s result handling.

Changes:

  • Documents parsed, raw, and overridden result shapes.
  • Preserves list[Content] when enhancing sample output.
File summaries
File Description
python/packages/core/agent_framework/_middleware.py Expands middleware result documentation.
python/samples/02-agents/middleware/shared_state_middleware.py Enhances text content without stringifying the list.
Review details

Suppressed comments (1)

python/packages/core/agent_framework/_middleware.py:782

  • The override rule omits the function_approval_request control object, which the invocation layer deliberately passes through as a bare Content instead of collapsing. Please include this exception so the method documentation agrees with the supported middleware approval path.
            middleware left. Overriding with anything but ``list[Content]`` or ``str``
            collapses the result into a single stringified text item.
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

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

A bare Content of type function_approval_request is returned unchanged by the
invocation layer on both the normal and termination paths, so the override rule
as written misdirects middleware that drive approval flows.
Merged via the queue into microsoft:main with commit a6eb299 Sep 11, 2026
40 checks passed
@orangeCatDeveloper
NekoPunch (orangeCatDeveloper) deleted the docs/function-middleware-result-shape branch September 11, 2026 08:42

This branch was previously deployed

1 inactive deployment
github-app-auth — d9327fbb Deployed Sep 11, 2026 by orangeCatDeveloper via add_label #22522
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants