

Persistent threads humans and agents share — resumable token streams, a transcript in your own database, typing indicators and read receipts from the same log, and handoff as a state transition. Built on Durable Objects.
The client SDK is read-and-subscribe. The agent SDK streams. Handoff is one PATCH.
The SDK fetches a signed token from your authEndpoint, joins the thread channel, and hands you a live view — snapshot immediately, then every change.
const chat = new PingerchipsChat(KEY, { authEndpoint: '/api/chat/auth' });
const session = await chat.connect(threadId, {
clientId: 'user-99',
afterLogId: loadCheckpoint(threadId),
});send, edit, and regenerate each resolve when the server acks and carry the channel serial. The transcript is just session.view.getMessages().
const run = await session.view.send('my invoice shows two charges');
session.on('run:start', () => showTyping());
session.on('change', ({ logId }) => save(logId));The agent connects with the app secret, loads the conversation, streams its reply into the thread, and ends the run — committed as one message.
const run = session.createRun(runId);
await run.start(ownerClientId);
let text = '';
for await (const c of llm) {
await run.write(text += c.text);
}
await run.end('complete');Handoff is a thread state transition, not a separate system. The human joins the thread the agent is already in. Both see it live; the transcript stays one continuous thread.
await fetch(
`/api/chat/admin/threads/${threadId}/handoff`,
{ method: 'PATCH', body: JSON.stringify(
{ assignee: 'agent-jane' }) }
);const chat = new PingerchipsChat(KEY, { authEndpoint: '/api/chat/auth' });
const session = await chat.connect(threadId, {
clientId: 'user-99',
afterLogId: loadCheckpoint(threadId),
});const run = await session.view.send('my invoice shows two charges');
session.on('run:start', () => showTyping());
session.on('change', ({ logId }) => save(logId));const run = session.createRun(runId);
await run.start(ownerClientId);
let text = '';
for await (const c of llm) {
await run.write(text += c.text);
}
await run.end('complete');await fetch(
`/api/chat/admin/threads/${threadId}/handoff`,
{ method: 'PATCH', body: JSON.stringify(
{ assignee: 'agent-jane' }) }
);const chat = new PingerchipsChat(KEY, { authEndpoint: '/api/chat/auth' });
const session = await chat.connect(threadId, {
clientId: 'user-99',
afterLogId: loadCheckpoint(threadId),
});Tokens ride a 40 ms server-side rollup into a durable slot, compacted to one message on run end. Reload the tab mid-answer — it picks up where it left off.
Every message committed to your database. Query it with SQL, export it, purge it on your schedule.
clientId is server-verified from the token claim and stamped on every message. It can't be spoofed.
@pingerchips/ai is a drop-in ChatTransport. The component body doesn't change; the conversation becomes a persisted, resumable thread. See it →
Same primitives for a Python agent backend.
The thread, the persistence, the streaming, and the handoff are one SDK. You write the agent.
Bot answers from the same thread the human can grab. The transcript is one continuous conversation in your Postgres — customer, bot, agent, in order.
a weekendStream the model's answer into the thread. The user reloads mid-response and it picks up from the last token — no regeneration, no wasted spend.
~2 hoursSeveral humans and a bot in one thread. clientId is server-verified and stamped on every message, so you always know who said what.
an afternoonThreads, participants, read receipts, typing indicators, and permanent history — from the SDK, not four services.
a weekThe agent runs for minutes past your serverless timeout. It streams into the thread from a worker; the UI follows every step; it resumes after a crash.
a daySwap the Vercel AI SDK transport for @pingerchips/ai. The component doesn't change; the conversation becomes persisted, resumable, and multi-device.
20 minutesHumans and agents in one conversation, in your database. Self-hostable, free while in beta.