Adapters
Translate native state and commands at one edge; keep the original runtime available.
Pick the smallest edge
A normalizer converts a native snapshot to ChatMessage[]. An adapter also binds commands and capabilities. A store bridge subscribes to an existing owner. None creates a model connection or executes a tool.
| You already have | Use | State owner |
|---|---|---|
| Canonical messages | Direct view props | Application |
| Native records | chat-normalizers | Native client/application |
| A React SDK hook | useChatAdapter(adapter, hook, options) | SDK hook |
| A subscribed store | createChatRuntimeStore(source) | Existing store |
| An AG-UI client | createAgUiRuntime(client, options) | @ag-ui/client |
See runtime architecture for a complete store example and choose a runtime for native hook recipes. All paths render the same Message parts; there is no provider-specific view tree.
Add a format without adding an engine
The @ghost-ui/core/chat-normalizers entry has no React or SDK dependency. Its pure functions accept ordered native records and preserve native IDs. Supply getId(record) only when the upstream record has no ID; derive it from a durable application key, never an array index or a fresh UUID during rendering.
mapPart(part, message) returns a canonical part, null to deliberately omit it, or undefined for the built-in mapping. Raw reasoning is withheld unless classifyReasoning(part) explicitly returns provider-summary or application-summary. Keep secrets out of all client snapshots, including metadata.
| Function | Input |
|---|---|
normalizeLangChainMessages | BaseMessage instances or serialized message state, including removal records |
normalizeAgUiMessages | Native AG-UI client's reconstructed messages |
normalizeEveMessages | Eve hook's reconstructed messages |
normalizeOpenCodeMessages | SDK session.messages() records with info and parts |
normalizeOpenRouterItems | Ordered replacement items from the Agent SDK |
normalizeAdkEvents | Ordered ADK events; partial previews coalesce until the authoritative segment |
normalizeA2aMessages / normalizeA2aTask | A2A v1 messages / task with status, history and artifacts |
normalizeManagedAgentEvents | Ordered buffered session facts, replayed or tailed |
normalizeAssistantStreamMessage(id, value) | Message assembled by assistant-stream |
replaceChatRecordsById(records, getId) | Latest record per ID at its first insertion position |
Unknown content becomes an explicit unknown or data part. Native non-reasoning parts and metadata remain available for an application renderer; the native source remains authoritative. Normalization does not guarantee every native feature has a default visual representation.
Extend the same view
Use MessageParts.renderers for an application-owned part without forking the message renderer. This example validates a structured plan and composes the existing Agent todo list. Runtime code can supply this same canonical data part;its shape belongs to the application.
Keep the part and task IDs stable as state changes. The renderer adds no execution or state reducer. For native content, use mapPart to produce this application data only after validating that native schema;do not assume every protocol supports arbitrary custom content tags.
Add commands only when their meaning matches
Implement ChatAdapter<TBinding>.getSnapshot(binding, options) for another hook. Return canonical messages, identity, a ChatController and truthful capability declarations. The custom adapter example shows the full interface. Ghost's React binding guards current context and pending commands; do not rebuild that guard in each SDK integration.
Retrying the latest answer, forking at a checkpoint and rerunning a task are different operations. Keep checkpoint/branch, permission, auth, artifact and session APIs on the native object when generic commands would erase their meaning. Bind attachments only with real conversion; bind approval acknowledgement separately from tool execution. Unsupported capabilities stay unsupported or unknown.
Diagnose a mapping
A missing message usually means a missing stable ID or a changed native shape. A visible unsupported-content notice means you need a deliberate mapPart or MessageParts.renderers choice. A disabled action can come from native availability or application policy; inspect both. See stability before upgrading an SDK.