@codehz/conversation
v0.2.4
Published
`@codehz/conversation` is a Bun-first conversation sync library for AI applications. It provides:
Readme
@codehz/conversation
@codehz/conversation is a Bun-first conversation sync library for AI applications. It provides:
- canonical message history with branching active-path state
- run lifecycle and durable streaming events
- snapshot + event-log recovery
- audience-aware projection (
public/protected/private) - structured input request / submission envelope
- host event channel (shared seq axis with conversation events)
- trace sidecar with independent
TraceStore - plugin pipeline (command/event interception, projection, lifecycle hooks)
- run driver host (driver registration, match, HostAPI facade)
- Bun WebSocket sync with audience-aware projection
- React client/hooks for vNext conversation state
The library owns protocol and synchronization. The host owns agent execution, routing, auth, storage partitioning, and tool implementations.
Install
bun installBuild and test
bun run build
bun run typecheck
bun testPackage surfaces
| Import path | Description |
| ----------------------------------- | -------------------------------------------------------------------- |
| @codehz/conversation | vNext — all exported types, engine, server, react, trace |
| @codehz/conversation/vnext | Same as default (vNext entry point) |
| @codehz/conversation/vnext/core | Core types, reducer, projection, commands |
| @codehz/conversation/vnext/server | ConversationEngine, storage contracts, sync server, plugin, driver |
| @codehz/conversation/vnext/react | ConversationClient, React hooks, ConversationProvider |
| @codehz/conversation/vnext/trace | Trace types, store contract, projection helpers |
Core concepts
Messages
Canonical history is stored as Message nodes:
- roles:
system | user | assistant | tool - tree shape via
parentId - optional
runId - ordered structured
parts - protocol-level
Visibility: "public" | "protected" | "private"
MessagePart is a closed union:
textreasoningtool-calltool-resultfilesourcedatastep-marker
Runs
Runs are first-class records with status, checkpoints, visibility, and output message ids. A single run may materialize multiple messages.
Visibility & Audience
Messages, parts, runs, host events, and traces carry Visibility:
public: visible to all audiencesprotected: visible tocontrolled/host/systemaudiencesprivate: visible only tohost/systemaudiences
The host provides a ProjectionPolicy to determine per-audience visibility and field-level projection. The Audience is declared at connection time.
Persistence
Storage is host-owned through three separate contracts:
EventStore: append-only event log with atomic batch writesSnapshotStore: full-state snapshot storageTraceStore: independent trace record storage
interface EventStore {
append(envelopes: readonly EventEnvelope[]): Promise<void>;
getEvents(fromSeq: number): Promise<readonly EventEnvelope[]>;
getLastSeq(): Promise<number>;
}Snapshots are part of the protocol contract. Recovery loads the latest snapshot, then replays tail events.
Structured Input
RunInputRequest supports message | form | approval | selection | custom kinds. Submissions use an InputSubmissionEnvelope that wraps typed payloads.
Host Events
HostEvent shares the same seq axis as ConversationEvent, ensuring deterministic replay order. Host events are projected through the same ProjectionPolicy as conversation events.
Trace
Trace records are stored in an independent TraceStore. Summaries are emitted into the main sync stream as trace.summaryAppended events for real-time client delivery.
Typical host flow
Client submission creates a user message and a pending run. A registered RunDriver (or manual subscribe) handles run.requested:
Recommended high-level entry points:
engine.startRun(...)for host-initiated runshost.beginAssistantMessage(...)for streaming assistant output
Using a RunDriver (vNext)
const driverHost = new DriverHost();
driverHost.register(
new CallbackDriver("echo-agent", async (ctx) => {
const { run, host } = ctx;
await host.transitionRun(run.id, "running");
const writer = await host.beginAssistantMessage({
runId: run.id,
parentId: ctx.trigger.id,
});
await writer.text("Hello!");
await writer.done();
await host.transitionRun(run.id, "completed");
}),
);
const engine = new ConversationEngine({
eventStore,
snapshotStore,
traceStore,
compactionPolicy,
metrics,
driverHost,
});Using subscribe (manual)
engine.subscribe((envelope) => {
if (envelope.kind === "conversation" && envelope.event.type === "run.requested") {
void startAgent(envelope.event.payload.runId);
}
});React client
ConversationClient handles:
- WebSocket connection and reconnect
lastSeqpersistencesync.snapshot+ incremental replay- gap/reset handling
- generic
sendCommandfor all command types
In React, mount the client under ConversationProvider. The provider owns connect() / disconnect() by default, so components do not need a manual useEffect just to open the socket.
Hooks expose projected state:
useConversation<T>(selector),useMessage(id),useActiveMessageIds()useRun(runId),useActiveRun(),useRunsByStatus(status)useMessageBlocks(messageId),usePendingInput(runId),useRunToolEvents(runId)useVariantGroup(messageId)useHostEvents(),useHostEventsByNamespace(namespace)useTraceSummaries(),useTraceSummariesByRun(runId)useConnectionStatus(),useSendCommand(),useReconnect()
Minimal host example
The example under examples/minimal-host demonstrates vNext:
- structured message input
- host events (agent lifecycle)
- trace sidecar (agent traces)
- driver registration (
CallbackDriver) - audience-aware sync
Run it with:
cd examples/minimal-host
bun install
bun startThen open http://localhost:3000.
References
- DESIGN.md - vNext protocol and architecture
