You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 #1389ToolMessage 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-shapedrole: "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)
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
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
Keeping the functional.pytransform/atransform contract (_tool_output type-match, _tool.args/_tool.name introspection) intact through whatever new path replaces the Message short-circuit
Notes
Open question for the design discussion: is TemplateRepresentation the right home for live/typed state at all, or should Message/ToolMessage remain a deliberately-special "already-materialized" case that bypasses the intermediate representation (the current case Message(): return c short-circuit)? The exploration leans toward the latter being intentional, not accidental
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-casedMessage/ToolMessagehandling could be collapsed into the genericComponenthandling - i.e. move message serialization entirely onto theTemplateRepresentation->Messagepath so a plain customComponentcould express everything aMessagecan.The blocker: collapsing
MessageintoComponentwould regress the #1389ToolMessagework.#1389 made tool results round-trip to OpenAI-compatible providers as spec-strict
role: "tool"messages with a matchingtool_call_id.TemplateRepresentationhas since grownrole,tool_calls,tool_call_id, andthinking, so a plainComponentcan already produce a serialization-shapedrole: "tool"Messageviamessage_from_template_representation. ButToolMessagestill carries live, typed, non-serializable state thatTemplateRepresentationcannot express, and that state is consumed by code paths other than the OpenAI serializer.mellea/stdlib/components/chat.pylines 261-296 (ToolMessage) -ToolMessagestores_tool(the fullModelToolCall),_tool_output(the raw Python return value of the tool, not a string),arguments, andname.TemplateRepresentationonly carries serialization (tool_calls: list[dict],tool_call_id: str) - it has no channel for a liveModelToolCallor a typed tool-output object. AComponent->Messageconversion therefore cannot reconstruct aToolMessage; 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 liveToolMessagestate, not on its serialized form:type(output._tool_output) is type(obj)matches the raw return value by Python type, andchosen_tool._tool.args/chosen_tool._tool.nameread theModelToolCalldirectly. If tool results were only ever plainMessages reconstructed from aTemplateRepresentation,transformwould lose its type-matching and argument-introspection and regress.mellea/formatters/chat_formatter.py(case Message(): return c) andmellea/stdlib/components/chat.py(as_chat_history/as_generic_chat_history) - theMessageshort-circuit returns the object verbatim, soToolMessagesubtype identity (and its_tool.tool_call_id) survives to the backend payload. A collapse that routed every turn throughTemplateRepresentationwould drop this short-circuit and, with it, the subtype.What a real collapse would require (for the future design discussion)
ModelToolCall/ raw tool-output slot onTemplateRepresentation, or a separate live-state channel. This is a departure from theimages/audio/roleprecedent, which are all serialization-shapedToolMessage(sofunctional.transform's type-matching keeps working) rather than a plainMessage, and how it would supply the raw_tool_outputfunctional.pytransform/atransformcontract (_tool_outputtype-match,_tool.args/_tool.nameintrospection) intact through whatever new path replaces theMessageshort-circuitNotes
TemplateRepresentationthe right home for live/typed state at all, or shouldMessage/ToolMessageremain a deliberately-special "already-materialized" case that bypasses the intermediate representation (the currentcase Message(): return cshort-circuit)? The exploration leans toward the latter being intentional, not accidentalrole: "tool"/tool_call_id), feat: allow components to declare their chat role via format_for_llm #1542 (component-declared role)