Skip to content

Tool results: Make ToolResultOutput typed, support multipart output#153

Closed
msullivan wants to merge 2 commits into
mainfrom
file-part-tool
Closed

Tool results: Make ToolResultOutput typed, support multipart output#153
msullivan wants to merge 2 commits into
mainfrom
file-part-tool

Conversation

@msullivan
Copy link
Copy Markdown
Contributor

@msullivan msullivan commented May 28, 2026

The core goal here is to support image output from tools, so that we
can support a read file tool that returns an image, or a screenshot
tool, etc.

The approach I take is that the result of a tool becomes a tagged
union, similar to how AI SDK does it.

  ToolResultOutput =
    | TextOutput            # {type:"text",  value:str}
    | JsonOutput            # {type:"json",  value:Any}
    | ContentOutput         # {type:"content", value:list[TextPart|FilePart]}
    | ErrorTextOutput       # {type:"error-text",  value:str}
    | ErrorJsonOutput       # {type:"error-json",  value:Any}
    | ExecutionDeniedOutput # {type:"execution-denied", reason?:str}

Tool return processing will then preserve a ToolResultOutput while
coercing other types into it. Currently, BaseModel gets preserved as
part of JsonOutput (because UI outbound still depends on it), which is
not really correct and should be revisisted.

BACKWARDS COMPATABILITY: This is a compatability break, though, since
it changes the type of result. There is soem compatability goo
though where we still try to accept "raw" results on input to
ToolResultPart, so that loading old saved messages will still work. I
don't know if anybody in the world would benefit from this other than
my saved tau sessions though so maybe we don't need it?

The core goal here is to support image output from tools, so that we
can support a read file tool that returns an image, or a screenshot
tool, etc.

The approach I take is that the result of a tool becomes a tagged
union, similar to how AI SDK does it.

New types (src/ai/types/messages.py):

  ToolResultOutput =
    | TextOutput            # {type:"text",  value:str}
    | JsonOutput            # {type:"json",  value:Any}
    | ContentOutput         # {type:"content", value:list[TextPart|FilePart]}
    | ErrorTextOutput       # {type:"error-text",  value:str}
    | ErrorJsonOutput       # {type:"error-json",  value:Any}
    | ExecutionDeniedOutput # {type:"execution-denied", reason?:str}

Tool return processing will then preserve a ToolResultOutput while
coercing other types into it. Currently, `BaseModel` gets preserved as
part of JsonOutput (because UI outbound still depends on it), which is
not really correct and should be revisisted.

BACKWARDS COMPATABILITY: This is a compatability break, though, since
it changes the type of `result`. There is soem compatability goo
though where we still try to accept "raw" results on input to
ToolResultPart, so that loading old saved messages will still work.  I
don't know if anybody in the world would benefit from this other than
my saved `tau` sessions though so maybe we don't need it?
@msullivan msullivan requested a review from anbuzin May 28, 2026 17:28
@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-python Ready Ready Preview, Comment May 28, 2026 7:46pm

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant