Skip to content

Python: [Bug]: FunctionTool.parse_result and _make_dumpable do not handle tuples (corrupts rich Content and Pydantic models) #9488

Open

Description

Observed Behavior

When a tool function returns multiple values as a tuple (e.g. return text_content, image_content or returning a tuple of Pydantic models):

  1. Multimodal/Rich Content is corrupted: FunctionTool.parse_result strictly checks isinstance(result, list) and ignores tuple. Consequently, returning a tuple of Content items falls through to json.dumps(..., default=str). Rich Content items (e.g. URI, image, audio) are flattened into a single text content item containing broken strings like ["Analysis complete", "Content(type=uri)"].
  2. Pydantic models in tuples produce invalid JSON: _make_dumpable does not recurse into tuple. Calling json.dumps(..., default=str) on a tuple of Pydantic models serializes them using Python's str()/__repr__ (e.g. ["name='sample'"]) instead of valid JSON objects ([{"name": "sample"}]), causing schema/JSON validation failures when payloads reach LLM APIs.

Expected Behavior

  1. FunctionTool.parse_result should recognize tuples of Content items (just like it does for lists) and return individual Content instances (e.g. [Content(type='text'), Content(type='uri')]).
  2. _make_dumpable should recurse into tuples (converting models to dicts), ensuring tuples of Pydantic models serialize to valid JSON [{"name": "sample"}].

Steps to Reproduce

  1. Create a function tool that returns a tuple of Content items: (Content.from_text("..."), Content.from_uri("...", media_type="image/png")).
  2. Invoke FunctionTool.parse_result(...) on the returned tuple.
  3. Observe that a single text Content item is returned with Content(type=uri) stringified inside the text payload instead of preserving the separate uri Content item.
  4. Similarly, pass a tuple of Pydantic models (Item(name="sample"),) to FunctionTool.parse_result and observe that the serialized text contains Python representation syntax ["name='sample'"] instead of valid JSON.

Minimal Reproduction

from agent_framework._tools import FunctionTool
from agent_framework._types import Content
from pydantic import BaseModel

class Item(BaseModel):
    name: str

# Case 1: Returning a tuple of Content items (text + image/URI)
c1 = Content.from_text("Analysis complete")
c2 = Content.from_uri("https://example.com/chart.png", media_type="image/png")

result = FunctionTool.parse_result((c1, c2))
print("Result items count:", len(result))
print("Content types returned:", [c.type for c in result])
print("Extracted text payload:", result[0].text)

# Case 2: Returning a tuple of Pydantic models
model_result = FunctionTool.parse_result((Item(name="sample"),))
print("Model JSON payload:", model_result[0].text)```

### Error Messages and Stack Traces

```text
Observed outputs from reproduction:

Case 1 (Rich Content corrupted):
Result items count: 1
Content types returned: ['text']
Extracted text payload: ["Analysis complete", "Content(type=uri)"]

Case 2 (Invalid JSON):
Model JSON payload: ["name='sample'"]

Package Versions

agent-framework==1.0.0b251016 (current main branch) agent-framework-core

Python Version

Python 3.12 (also affects 3.10 and 3.11)

Operating System

Windows

Regression

No

Additional Context

In packages/core/agent_framework/_tools.py:

  • Line 1360 (_make_dumpable): only checks isinstance(value, list). Should check isinstance(value, (list, tuple)).
  • Line 1403 (parse_result): checks isinstance(result, list) and any(isinstance(item, Content) for item in result). Should check isinstance(result, (list, tuple)).

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I personally verified this behavior and the reproduction details are authentic.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 18, 2026
  3. github-actions commented on Oct 18, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    Repro: python/packages/core/agent_framework/_tools.py::FunctionTool.parse_result and _make_dumpable fail when given tuples containing Content or Pydantic models. Rich content collapses into one text item, while models serialize through Python string representations. Minimal repro: call FunctionTool.parse_result with (Content.from_text(...), Content.from_uri(...)) and (Item(name="sample"),).

    • Failing test: python/packages/core/tests/core/test_types.py
    • Files examined: python/packages/core/agent_framework/_tools.py, python/packages/core/tests/core/test_types.py, python/packages/core/pyproject.toml
    • Tests run: test_parse_result_pydantic_model_in_tuple, test_parse_result_tuple_of_content, equivalent list tests, direct minimal reproduction
    • Reported version: 1.0.0b251016
    • Current version: 1.22.0
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
  5. Kaap10 commented on Oct 18, 2026

    @Kaap10
    Author

    I have verified this locally and have the fix and unit tests ready. I would love to submit a PR for this once approved. Please let me know if I can go ahead!

  6. Madin-H23 commented on Oct 22, 2026

    @Madin-H23

    Thanks for the report — I am not opening a pull request for this, the fix is yours. Here is an independent read of the current main source (1fe5467), so the review decision has one more data point. I checked this by reading the code, not by running it.

    The mechanism is real, and it is two separate list-keyed sites in FunctionTool:

    • parse_result only treats a sequence as "a list of content" when isinstance(result, list) and any(isinstance(item, Content) for item in result). A tuple of Content items misses that check, so it falls through to the generic branch, _make_dumpable leaves the tuple untouched, and json.dumps(..., default=str) stringifies each item — which is exactly the "Content(type=uri)" output in the report.
    • _make_dumpable recurses into list and dict but not into tuple, so a tuple of Pydantic models reaches json.dumps unchanged and each model is rendered with str(), giving ["name='sample'"] instead of [{"name": "sample"}].

    Those two fixes are complementary: the tuple of models has no Content in it, so it goes down the generic path and is fixed by the _make_dumpable recursion, while the tuple of Content items is fixed by the sequence check.

    One shape constraint that is easy to get wrong: the any(isinstance(item, Content) ...) guard is what keeps a list of Pydantic models on the generic path, i.e. one text Content holding the whole JSON array, instead of the per-item path that produces one Content per element. If the tuple branch skips that guard, a tuple of models would come back as one Content per model while the equivalent list still comes back as a single Content with the full JSON — the two would then disagree on shape. Mirroring the list semantics exactly (same guard, same per-item handling) keeps them consistent.

    A few edges worth covering in the tests: a namedtuple (it is a tuple instance, so it will take the tuple path and lose its named fields), a tuple nested inside a list (the list recursion must reach into it), a tuple mixing Content and non-Content items (the per-item path stringifies the non-Content ones, same as the list path does), and a tuple of models next to the equivalent list, asserting both produce the same JSON.

    I can run the pipeline tests on Windows against a branch if that would help the review.

  7. Kaap10 commented on Oct 22, 2026

    @Kaap10
    Author

    Thanks H23 (@Madin-H23) for the detailed review and test edge-case suggestions! I'll make sure the tests cover mixed-content tuples, models in tuples matching list serialization, and nested structures.

  8. eavanvalkenburg commented on Oct 26, 2026

    @eavanvalkenburg
    Member

    We will consider tuple tool-result support as core MAF behavior. Thank you Vardhman Gupta (@Kaap10) for waiting for agreement and preparing coverage. The intended tuple/list equivalence and compatibility boundaries need to be agreed before implementation; a small diff alone does not settle provider-visible result shapes. PR #9679 already proposes overlapping work from another contributor, so planning should coordinate that proposal with this issue rather than invite a competing PR. This consideration does not promise implementation or timing, and is not yet agreement on a specific patch.

  9. eavanvalkenburg commented on Oct 26, 2026

    @eavanvalkenburg
    Member

    /plan Establish a bounded core contract for tuple function-tool results, coordinated with waiting issue author Vardhman Gupta (@Kaap10) and existing overlapping PR #9679 by ssccinng. Do not start another implementation or decide ownership by PR chronology. Compare current list semantics and result_parser escape hatch with tuple/list equivalence in FunctionTool.parse_result and _make_dumpable. Preserve the rich-content any(Content) guard so tuples/lists of models remain one JSON-array text result rather than per-model content items. Cover direct/mixed/empty/nested tuples, Content media preservation, Pydantic objects and namedtuple/subclass compatibility; explicitly decide whether namedtuple fields are intentionally lost instead of silently broadening to every Sequence/iterator. The current model repr-string output is valid JSON with the wrong intended shape, not malformed JSON. Trace provider-bound history and caller-visible results, including streaming updates/finalization and non-streaming paths, against docs/specs/004-python-function-calling-loop.md. Identify affected scenario rows, package/provider validation, typing and core-team review requirements. Assess the existing PR against that contract without treating this request as patch approval. Recommend the smallest compatible behavior before implementation; no ready label or new assignment while design is pending.

  10. Kaap10 commented on Oct 27, 2026

    @Kaap10
    Author

    Thanks Eduard van Valkenburg (@eavanvalkenburg) for the update and for coordinating the contract design. Completely agree on establishing the tuple/list equivalence and edge cases (especially namedtuples and streaming) first. Happy to follow the plan and help verify once the contract is set!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions