Runtime architecture
Connect a hook or application store through shared state, commands and explicit capabilities.
Choose an integration
Keep direct ChatMessage[] and callback props when your view already has the data it needs. Use useChatAdapter for an SDK hook; use createChatRuntimeStore for a store with subscriptions. Both return the same messages, controller, identity and capability contract. The native runtime recipes use installed hooks and clients without mirroring their state into Ghost.
Import the pure bridge/types from @ghost-ui/core/chat-runtime and optional React hooks/context from @ghost-ui/core/chat-runtime-react. No SDK is a dependency of either entry. React bindings remain React. Follow AI installation for view dependencies and styles.
For backend-specific choices, see Choose an AI runtime. Adapters covers pure projections and native bindings; threads covers identity and lifecycle.
First external store
The following application owns a local note list; no model or server is implied. Store instances are created per mounted notebook rather than shared across requests. Ghost subscribes and guards commands; it does not own the list.
getSnapshot must return the same immutable object until the source changes. Mutating an array in place does not publish a new snapshot. subscribe returns cleanup. For SSR, supply getServerSnapshot and hydrate with the same initial data; omitting it makes the store binding client-only. An existing Redux/Zustand/custom store can expose these same methods. The pure bridge delegates subscriptions and caches projections; it does not create persistence or a message reducer.
ChatRuntimeProvider accepts either store or an already-produced value snapshot. useChatRuntime() reads that composition context. useChatRuntimeStore(store) works without a provider. Existing views continue to take explicit props.
Identity and commands
identity requires a stable threadId and nonnegative safe-integer revision; include runId when available. Advance the revision when replacing the context, including A→B→A. Do not invent a run ID from message position or a stream ending. Ordinary streamed text updates can retain the same context revision.
Commands read the current source before acting. A stale snapshot cannot send into a replacement context, and a captured edit command uses the latest controller/history. Duplicate pending commands are rejected. An accepted send may finish after the SDK starts streaming; that transition does not revoke it. A pure store remains host-owned when a React subscriber unmounts. The hook adapter invalidates its committed binding on unmount or adapter replacement.
Tool approval uses controller.respondToToolApproval({ messageId, toolCallId, approvalId, approved }). It verifies the current pending request, rejects automatic/stale decisions and waits for application acknowledgement before allowing the same decision again. It does not execute the tool. inputRequests reuses InputRequestController, including its request revision; it adds no parallel clarification engine.
Capabilities and application policy
Each capability reports supported with explicit available, unsupported, or unknown; optional reason explains a restriction. Retry also names target: "latest" | "message". Omitted features remain unknown. Supported commands must actually be bound. The SDK adapters report native support and current busy state; availability can disable a supported action for application policy. These declarations do not authorize backend operations.
The capability keys are send, stop, retry, edit, attachments, toolApproval and clarification. Inspect support before availability. The older adapter capabilities flags describe native normalization and remain compatible; use the runtime snapshot for command availability. Cancellation requests, approval decisions and completed execution remain distinct facts.
Outbound attachment conversion
Both SDK bindings default to text send. Supply attachments: { send(submission, context) } when your application can convert the same stable attachment IDs into SDK-native files. The runtime rejects pending, failed or cancelled attachments. The converter owns uploads, allowed formats, credentials and transport cancellation. After asynchronous preparation, call context.assertCurrent() before dispatch so preparation cannot cross into a replacement conversation. The runtime does not discard files to make a text send succeed.
Add another adapter
Implement ChatAdapter<TBinding>.getSnapshot(binding, options). The input can use your provider's own field names; no messages/status shape or SDK union is required in core. Return canonical messages, a ChatController, the caller's identity and truthful capabilities. Preserve source IDs/order and unknown data; application schemas remain outside the core part union.
Use this adapter with useChatAdapter(recordsAdapter, binding, options), or supply canonical inputs to an external store. Implement only supported features and preserve explicit application availability. Keep protocol parsing and command payload conversion at this edge. There is no plugin registry or required managed conversation engine.
Reference and troubleshooting
ChatRuntimeInput contains identity, messages, controller, optional capability declarations, attachments and inputRequests. ChatRuntimeSnapshot exposes read-only identity/messages, guarded controller, complete capabilities and optional guarded input requests. createChatRuntimeBinding(getCurrent) is the lower-level projection function for a custom host; keep one binding per owner so pending guards survive updates.
ChatRuntimeError.code is invalid, stale, unsupported, unavailable or duplicate. Handle command failures in the application. A guard cannot stop a host callback from applying its own late network result: check identity/revision where the host publishes data. For a constantly rerendering store, check snapshot caching; for a missing action, inspect support, availability and the bound command before changing the UI. See AI contracts for parts, disclosure and extension data.