Skip to content

Collapse Message/ToolMessage handling into generic Component handling #1550

Description

@AngeloDanducci

Context

Follow-up to #1542 (allow components to declare their chat role via format_for_llm) and its umbrella #1030 (message serialization). During that work we explored whether the special-cased Message/ToolMessage handling could be collapsed into the generic Component handling - i.e. move message serialization entirely onto the TemplateRepresentation -> Message path so a plain custom Component could express everything a Message can.

The blocker: collapsing Message into Component would regress the #1389 ToolMessage work.

#1389 made tool results round-trip to OpenAI-compatible providers as spec-strict role: "tool" messages with a matching tool_call_id. TemplateRepresentation has since grown role, tool_calls, tool_call_id, and thinking, so a plain Component can already produce a serialization-shaped role: "tool" Message via message_from_template_representation. But ToolMessage still carries live, typed, non-serializable state that TemplateRepresentation cannot express, and that state is consumed by code paths other than the OpenAI serializer.

  • mellea/stdlib/components/chat.py lines 261-296 (ToolMessage) - ToolMessage stores _tool (the full ModelToolCall), _tool_output (the raw Python return value of the tool, not a string), arguments, and name. TemplateRepresentation only carries serialization (tool_calls: list[dict], tool_call_id: str) - it has no channel for a live ModelToolCall or a typed tool-output object. A Component -> Message conversion therefore cannot reconstruct a ToolMessage; the typed state is lost.

  • mellea/stdlib/functional.py (transform/atransform, ~lines 484, 498-507, 1263, 1277-1286, 1377, 1408) - these paths depend on the live ToolMessage state, not on its serialized form: type(output._tool_output) is type(obj) matches the raw return value by Python type, and chosen_tool._tool.args / chosen_tool._tool.name read the ModelToolCall directly. If tool results were only ever plain Messages reconstructed from a TemplateRepresentation, transform would lose its type-matching and argument-introspection and regress.

  • mellea/formatters/chat_formatter.py (case Message(): return c) and mellea/stdlib/components/chat.py (as_chat_history / as_generic_chat_history) - the Message short-circuit returns the object verbatim, so ToolMessage subtype identity (and its _tool.tool_call_id) survives to the backend payload. A collapse that routed every turn through TemplateRepresentation would drop this short-circuit and, with it, the subtype.

What a real collapse would require (for the future design discussion)

  1. A way for the intermediate representation to carry live objects, not just serialization - e.g. an optional ModelToolCall / raw tool-output slot on TemplateRepresentation, or a separate live-state channel. This is a departure from the images/audio/role precedent, which are all serialization-shaped
  2. A decision on whether a component declaring tool semantics should materialize a ToolMessage (so functional.transform's type-matching keeps working) rather than a plain Message, and how it would supply the raw _tool_output
  3. Keeping the functional.py transform/atransform contract (_tool_output type-match, _tool.args/_tool.name introspection) intact through whatever new path replaces the Message short-circuit

Notes

Metadata

Metadata

Labels

needs-designProblem is clear; implementation approach needs design discussion before work starts

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions