@aseity/agent-chat-react
v0.9.2
Published
Headless React runtime for agent chat UI streams.
Readme
@aseity/agent-chat-react
Headless React runtime for agent chat UI streams.
This package owns transport adaptation, SSE parsing, optimistic user messages, agent event reduction, and React subscription hooks. It intentionally does not ship visual chat components.
Historical assistant errors are preserved by hydrateMessages() and
store.hydrate(), including through the ky transport's protocol validation.
The restored message exposes the supplied error with its code, message
(including an empty string), and optional retry limit. Messages without error
metadata remain unchanged. Applications decide how to display business error
codes; the SDK does not generate product-specific error text.
Current Runtime Tradeoffs
- SSE reconnects are bounded only for failures that happen before the stream opens. If a connection opens and then drops immediately while a turn is still active, the retry counter is reset and the client can continue reconnecting. This is an accepted product tradeoff for the current single-user/single-backend deployment shape; do not treat it as a bug unless the deployment model changes or reconnect loops become user-visible.
- The ky transport ends an SSE stream after 45 seconds without receiving any
bytes. Heartbeat comments count as activity, so long-running agent work does
not trigger reconnects while the server heartbeat remains healthy. Set
eventStreamInactivityTimeoutMshigher than the server heartbeat interval when creating the transport if the server configuration differs.
'use client';
import { useAgentChat } from '@aseity/agent-chat-react';
import { createKyAgentTransport } from '@aseity/agent-chat-react/transport/ky';
import { apiClient } from './http';
const transport = createKyAgentTransport(apiClient, {
createSessionBody: { agentId: 'max' },
eventStreamInactivityTimeoutMs: 45_000,
});
// Dedicated agents share the same transport with a trusted relative base path.
const simulationTransport = createKyAgentTransport(apiClient, {
basePath: 'simulation-agent',
});
export function Chat() {
const chat = useAgentChat({
transport,
autoCreate: true,
throttleMs: 50,
});
return (
<form
onSubmit={event => {
event.preventDefault();
const data = new FormData(event.currentTarget);
void chat.sendMessage(String(data.get('message') ?? ''));
event.currentTarget.reset();
}}
>
{chat.messages.map(message => (
<div key={message.id}>{message.type === 'user' ? message.text : message.status}</div>
))}
<input name="message" disabled={!chat.canSend} />
<button disabled={!chat.canSend}>Send</button>
</form>
);
}For long conversations, render chat.messageIds and subscribe per row with
useAgentMessage(chat.store, id).
useAgent connects to the event stream when a loaded session is running, so a
page opened mid-turn follows the turn live. Pass autoConnect: false for a view
that shows only persisted history: a running session then stays disconnected,
including after reload(), until the caller sends or calls connect().
Migrating from 0.3
This minor release changes the pre-1.0 transport contract:
- Send receipts and
pendingSenduseturnIdinstead ofrunId. Custom transports must return{ sessionId, turnId, messageId, status: 'running' }. - The default collection path is
v1/sessions. Replace theagentIdtransport option withcreateSessionBody: { agentId }.createSessionBodyis posted as supplied; a custombasePathstill overrides the collection path. - History comes from
GET {basePath}/{sessionId}/items, not/messages. OverridingbasePathdoes not restore the old response shape. Applications retaining older APIs can overridelistMessagesandsendMessagein theirAgentChatTransport, as Max's chat service does. - Sessions can carry
agentId,mode,currentTurn, and string-valuedmetadata. Hosts map session/turnwaitingandstoppingtorunning. interruptedis accepted in both turn-end events and persisted history.
Items response
The ky entry point exports SessionItemsPage, SessionItem, and
SessionTurnSummary for host implementations. The response envelope is
{ items, turns, nextCursor, prevCursor }. Items are chronological; nextCursor
is passed as after until it is null, so initial hydration reaches the current
turn. prevCursor is reserved for older-history navigation; the SDK does not
provide infinite scrolling.
Each item carries id: "<turnId>:<seq>" and turnId. Sequence numbers are
compared numerically. The content fields are:
| type | fields |
| --- | --- |
| user_message | text, optional attachments: Attachment[] |
| assistant_text | text |
| reasoning | text, optional durationMs |
| tool_call | toolUseId, toolName, status, optional input, result, display, parentId |
Tool status uses the protocol's ToolStatus. parentId, when present, identifies
another item's id. AskUserQuestion is a tool_call; its input and result retain
the question and answer. Reasoning contains only saved text.
Each page supplies the involved turns with id, seq, status, and
userMessageId, plus optional session id, timestamps, error, and pending
interactions. Turn status is running or a protocol TurnStopStatus. Turns are
grouped into assistant messages; user inputs retain userMessageId so optimistic
messages keep their identity. Empty turns and error metadata are preserved.
Attachment descriptors (id, displayName, mimeType, byteSize, kind) are
preserved during pagination and reloads, including inputs with empty text. Send
requests still carry attachments: [{ assetPointer }]; download URLs are not
part of the history contract.
Repeated items and turn summaries across pages use the last received value.
validateProtocol validates the converted SessionMessages and SSE payloads;
turn association and item identity are required even when validation is disabled.
Reasoning duration
The store measures reasoning locally from receipt of part_add with
initial.kind: 'thinking' until part_complete, using a monotonic clock. Completed
reasoning parts expose integer-millisecond duration_ms while the rest of the turn
can still be running. Turn termination also settles unfinished reasoning timers.
There is no ticking timer or elapsed-time update on every delta.
These durations measure client receipt intervals, including connection delays; buffered replay can produce very short durations. Full turn replay restarts local measurement. History hydration replaces local durations with the snapshot's optional recorded values, which may differ or be absent.
Event replay
Reuse the transport instance across reconnects. A transport instance serves one
active store per session; independent concurrent stores need separate transport
instances so their replay cursors cannot advance each other. It records each delivered SSE
frame's id: per session and sends it as Last-Event-ID on reconnect. Invalid or
incomplete frames do not advance the cursor; an empty id: clears it. A history
reload clears that session's cursor because hydration replaces streamed parts
and needs a full replay. The server must replay from turn_start when it cannot
resume the supplied id. session_event.current_turn is preserved for application
callbacks; the store continues to reconstruct the active turn and interactions
from replayed events.
Workspace artifacts
The host emits turn_artifacts after collecting a turn's workspace artifacts and
before turn_end. The event name, turn identity and collection timing are unchanged:
{
type: 'turn_artifacts',
turn_id: 'turn_1',
artifacts: [
{ entryPath: 'reports/index.html', artifactType: 'webpage' },
{ entryPath: 'slides/index.html', artifactType: 'presentation' },
{ entryPath: 'simulations/pendulum.sim', artifactType: 'simulation' },
],
}ArtifactReference contains a nonempty entryPath and an artifactType of
document, webpage, presentation, spreadsheet, image, exercise or
simulation. The host supplies
the full relative entry path within the session workspace. Identity is scoped to
the session: the same path in another session identifies a different artifact.
Paths are kept intact; matching filenames in different directories are distinct.
The store replaces the referenced turn's collection and deduplicates by full
entryPath, retaining the last reference for a repeated path. An empty array
clears that turn's collection. The same path can appear independently in multiple
turns. AssistantMessage.artifacts and SessionTurnSummary.artifacts preserve
these references through history loading and pagination, including turns with no
assistant text. Hydration applies the same per-turn deduplication as event replay.
References identify current workspace files, not library entries or content snapshots. The host resolves content and access permissions; the SDK does not save files to a library or define preview routes.
Migrating from 0.6.x
Upgrade @aseity/agent-ui-protocol and @aseity/agent-chat-react together to
0.7.0. Replace { id } in events and historical turn summaries with
{ entryPath, artifactType }; the old shape is no longer supported. Attachments
and other message fields are unchanged.
The SDK's transport reads history afresh on every listMessages call and has no
persistent history cache or cache format version. Hosts that cache history must
bump their own cache/projection format version and discard old { id } data
before hydrating the store, including when protocol validation is disabled.
The default transport validation rejects legacy references; it does not migrate
them. Harnex must upgrade its server history projection and client cache together.
