Repository navigation
Python: [Bug]: FunctionTool.parse_result and _make_dumpable do not handle tuples (corrupts rich Content and Pydantic models) #9488
Copy link
Copy link
Open
Labels
agentsUsage: [Issues, PRs], Target: Single agentUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
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):
- Multimodal/Rich Content is corrupted:
FunctionTool.parse_resultstrictly checksisinstance(result, list)and ignorestuple. Consequently, returning a tuple ofContentitems falls through tojson.dumps(..., default=str). RichContentitems (e.g. URI, image, audio) are flattened into a single text content item containing broken strings like["Analysis complete", "Content(type=uri)"]. - Pydantic models in tuples produce invalid JSON:
_make_dumpabledoes not recurse intotuple. Callingjson.dumps(..., default=str)on a tuple of Pydantic models serializes them using Python'sstr()/__repr__(e.g.["name='sample'"]) instead of valid JSON objects ([{"name": "sample"}]), causing schema/JSON validation failures when payloads reach LLM APIs.
Expected Behavior
FunctionTool.parse_resultshould recognize tuples ofContentitems (just like it does for lists) and return individualContentinstances (e.g.[Content(type='text'), Content(type='uri')])._make_dumpableshould recurse into tuples (converting models to dicts), ensuring tuples of Pydantic models serialize to valid JSON[{"name": "sample"}].
Steps to Reproduce
- Create a function tool that returns a tuple of
Contentitems:(Content.from_text("..."), Content.from_uri("...", media_type="image/png")). - Invoke
FunctionTool.parse_result(...)on the returned tuple. - Observe that a single
textContent item is returned withContent(type=uri)stringified inside the text payload instead of preserving the separateuriContent item. - Similarly, pass a tuple of Pydantic models
(Item(name="sample"),)toFunctionTool.parse_resultand 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 checksisinstance(value, list). Should checkisinstance(value, (list, tuple)). - Line 1403 (
parse_result): checksisinstance(result, list) and any(isinstance(item, Content) for item in result). Should checkisinstance(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.
Reactions are currently unavailable
Metadata
Metadata
Labels
agentsUsage: [Issues, PRs], Target: Single agentUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
Type
Projects
- StatusShow more project fieldsNo status
Activity
github-actions commentedon Oct 18, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
python/packages/core/agent_framework/_tools.py::FunctionTool.parse_resultand_make_dumpablefail when given tuples containingContentor Pydantic models. Rich content collapses into one text item, while models serialize through Python string representations. Minimal repro: callFunctionTool.parse_resultwith(Content.from_text(...), Content.from_uri(...))and(Item(name="sample"),).python/packages/core/tests/core/test_types.py1.0.0b2510161.22.0Kaap10 commentedon Oct 18, 2026
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!
Madin-H23 commentedon Oct 22, 2026
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
mainsource (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 inFunctionTool:parse_resultonly treats a sequence as "a list of content" whenisinstance(result, list) and any(isinstance(item, Content) for item in result). A tuple ofContentitems misses that check, so it falls through to the generic branch,_make_dumpableleaves the tuple untouched, andjson.dumps(..., default=str)stringifies each item — which is exactly the"Content(type=uri)"output in the report._make_dumpablerecurses intolistanddictbut not intotuple, so a tuple of Pydantic models reachesjson.dumpsunchanged and each model is rendered withstr(), giving["name='sample'"]instead of[{"name": "sample"}].Those two fixes are complementary: the tuple of models has no
Contentin it, so it goes down the generic path and is fixed by the_make_dumpablerecursion, while the tuple ofContentitems 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 textContentholding the whole JSON array, instead of the per-item path that produces oneContentper element. If the tuple branch skips that guard, a tuple of models would come back as oneContentper model while the equivalent list still comes back as a singleContentwith 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
tupleinstance, 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 mixingContentand non-Contentitems (the per-item path stringifies the non-Contentones, 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.
Kaap10 commentedon Oct 22, 2026
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.
eavanvalkenburg commentedon Oct 26, 2026
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.
eavanvalkenburg commentedon Oct 26, 2026
/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.
Kaap10 commentedon Oct 27, 2026
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!