Message Part Grouping
Overview
This feature is experimental and the API may change in future versions.
The Parent ID Grouping feature allows you to group related message parts together by assigning them a common parentId
. This is useful for organizing content hierarchically, such as grouping research sources with their findings, or organizing multi-step tool executions.
Use it in your application
Use the MessagePrimitive.Unstable_PartsGroupedByParentId
component instead of the regular MessagePrimitive.Parts
component:
const ParentIdGroup: FC<
PropsWithChildren<{ parentId: string | undefined; indices: number[] }>
> = ({ parentId, indices, children }) => {
if (!parentId) {
// Ungrouped parts - render directly
return <>{children}</>;
}
return (
<div className="bg-muted/20 my-2 rounded-lg border p-4">
<div className="mb-2 text-sm font-medium">
Group: {parentId} ({indices.length} parts)
</div>
<div className="space-y-2">{children}</div>
</div>
);
};
const AssistantMessage: FC = () => {
return (
<MessagePrimitive.Root className="...">
<div className="...">
<MessagePrimitive.Unstable_PartsGroupedByParentId
components={{ Group: ParentIdGroup }}
/>
</div>
<AssistantActionBar />
<BranchPicker className="..." />
</MessagePrimitive.Root>
);
};
How it works
-
Message parts with parent IDs: Add a
parentId
field to your message parts:{ type: "text", text: "Research finding about climate change", parentId: "climate-research" }
-
Automatic grouping: Parts with the same
parentId
are automatically grouped together -
Ordering: Groups appear in the order of the first occurrence of each parent ID
-
Ungrouped parts: Parts without a
parentId
appear after all grouped parts
Setting Parent IDs
In Python (assistant-stream)
Use the with_parent_id()
method on the RunController:
from assistant_stream import create_run
async def my_run(controller):
# Regular message part
controller.append_text("Starting research...")
# Grouped parts with parent ID
research_controller = controller.with_parent_id("research-123")
await research_controller.add_tool_call("search", {"query": "climate data"})
research_controller.append_source({
"id": "source-1",
"url": "https://example.com/climate-data",
"title": "Climate Data Report"
})
research_controller.append_text("Key findings from the research:")
research_controller.append_text("• Global temperatures rising")
research_controller.append_text("• Sea levels increasing")
# Back to ungrouped content
controller.append_text("In conclusion...")
In TypeScript (assistant-stream)
Use the withParentId()
method on the AssistantStreamController:
import { createAssistantStream } from "@assistant-ui/react/assistant-stream";
const stream = createAssistantStream(async (controller) => {
// Regular message part
controller.appendText("Starting research...");
// Grouped parts with parent ID
const researchController = controller.withParentId("research-123");
await researchController.addToolCallPart({
toolName: "search",
args: { query: "climate data" },
});
researchController.appendSource({
type: "source",
id: "source-1",
url: "https://example.com/climate-data",
title: "Climate Data Report",
});
researchController.appendText("Key findings from the research:");
researchController.appendText("• Global temperatures rising");
researchController.appendText("• Sea levels increasing");
// Back to ungrouped content
controller.appendText("In conclusion...");
});
With External Store
When using the external store runtime, include the parentId
in your message parts:
const messages = [
{
role: "assistant",
content: [
{
type: "text",
text: "Let me search for information...",
},
{
type: "tool-call",
toolCallId: "call-1",
toolName: "search",
args: { query: "climate change" },
parentId: "search-results",
},
{
type: "source",
sourceType: "url",
id: "source-1",
url: "https://example.com",
title: "Climate Report",
parentId: "search-results",
},
{
type: "text",
text: "Based on the search results:",
parentId: "search-results",
},
],
},
];
Props
The Group component receives the following props:
parentId
: The parent ID shared by all parts in this group (undefined for ungrouped parts)indices
: Array of indices for the parts in this groupchildren
: The rendered message part components
Examples
Collapsible Research Groups
import { useState } from "react";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
const CollapsibleGroup: FC<
PropsWithChildren<{ parentId: string | undefined; indices: number[] }>
> = ({ parentId, indices, children }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
if (!parentId) return <>{children}</>;
return (
<div className="my-2 overflow-hidden rounded-lg border">
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="hover:bg-muted/50 flex w-full items-center justify-between p-3"
>
<span>Research Group ({indices.length} items)</span>
{isCollapsed ? <ChevronDownIcon /> : <ChevronUpIcon />}
</button>
{!isCollapsed && <div className="border-t p-3">{children}</div>}
</div>
);
};
Labeled Groups with Icons
const LabeledGroup: FC<
PropsWithChildren<{ parentId: string | undefined; indices: number[] }>
> = ({ parentId, indices, children }) => {
if (!parentId) return <>{children}</>;
const getGroupLabel = (id: string) => {
if (id.includes("research")) return "🔍 Research";
if (id.includes("analysis")) return "📊 Analysis";
if (id.includes("summary")) return "📝 Summary";
return "📁 " + id;
};
return (
<div className="bg-muted/30 my-3 rounded-lg p-4">
<h4 className="mb-2 text-sm font-semibold">{getGroupLabel(parentId)}</h4>
<div className="space-y-2">{children}</div>
</div>
);
};
Use Cases
Parent ID grouping is particularly useful for:
- Research assistants: Group sources, findings, and analysis together
- Multi-step processes: Organize related tool calls and their results
- Hierarchical content: Create nested or categorized information structures
- Context preservation: Keep related information visually connected