Skip to content

Live agent connection (WebSocket)

Use @coframe/agent-client when you want to show the agent working in real time — streaming text, tool calls, approvals, and run status — and send messages back. It connects over a WebSocket with automatic reconnect and a ready-made React hook.

Terminal window
pnpm add @coframe/agent-client

useAgentChat handles the connection, reconnection, and transcript state for you. Point it at a session and render the result:

import { useAgentChat } from "@coframe/agent-client/react";
function AgentChat({ sessionId }: { sessionId: string }) {
const chat = useAgentChat({
url: `wss://editor.example.com/api/sessions/${sessionId}/socket`,
config: { sessionId },
});
return (
<div>
<p>Status: {chat.status}</p>
{chat.transcript.map((item, i) => (
<div key={i}>{renderTranscriptItem(item)}</div>
))}
{chat.pendingApproval && (
<button onClick={() => chat.approveTool(chat.pendingApproval!.toolCallId)}>
Approve {chat.pendingApproval.toolName}
</button>
)}
<button onClick={() => chat.sendUserMessage("Make the headline bolder")}>
Send
</button>
</div>
);
}

The hook returns the live status, the available tools, a rolling transcript, any pendingApproval / pendingElicitation, the last error, and actions: connect(), sendUserMessage(), approveTool(), rejectTool(), submitElicitation(), abort(), and disconnect(). It connects automatically on mount (pass autoConnect: false to opt out).

Drop down to AgentClient directly when you’re not using React or need finer control:

import { AgentClient } from "@coframe/agent-client";
const client = new AgentClient({
url: "wss://editor.example.com/api/sessions/my-session/socket",
config: { sessionId: "my-session" },
reconnect: { enabled: true, maxAttempts: 5, initialDelayMs: 500 },
});
await client.connect();
client.sendUserMessage("Add a testimonials section");
for await (const event of client.events()) {
switch (event.type) {
case "assistant-delta":
process.stdout.write(event.text);
break;
case "approval-required":
client.approveTool(event.toolCallId);
break;
case "run-finished":
console.log("\nrun", event.status);
break;
}
}

When reconnect.enabled is true, the events() iterator stays alive through error → reconnecting → ready cycles, so a dropped connection doesn’t end your loop.

The server streams a stable, app-facing event vocabulary — you render these without needing to know anything about the editor’s internals:

Event typeMeaning
run-startedA new agent turn began.
assistant-deltaIncremental assistant text — concatenate for a live view.
assistant-messageA finalized assistant message for the turn.
tool-callThe agent invoked a tool (toolCallId, toolName, args).
tool-resultA tool finished (ok, result).
approval-requiredA tool needs human approval before running.
elicitation-requiredThe agent is waiting for a structured answer (render a form).
statusA capability lifecycle update (e.g. “starting preview…”).
run-finishedThe turn reached a terminal state (completed / error / aborted / awaiting-input).

The vocabulary evolves additively — new event types and optional fields may appear without breaking existing clients, so always handle unknown types gracefully.

MethodSends
sendUserMessage(text)A user message.
sendUserContent(blocks)A user message with structured content blocks.
approveTool(toolCallId)Approve a pending tool call.
rejectTool(toolCallId, reason?)Reject a pending tool call.
abort()Cancel the current run.

If you’re implementing the protocol yourself, connect to:

wss://<host>/api/sessions/<sessionId>/socket

then exchange the handshake:

Client → Server: { "type": "open", "protocolVersion": "1", "config": { "sessionId": "..." } }
Server → Client: { "type": "ready", "protocolVersion": 1, "sessionId": "...", "tools": [...] }

After ready, events arrive wrapped in an envelope — { "type": "event", "seq": N, "event": { "type": "...", ... } } — and you send the commands above as JSON frames.

Connect to a real Coframe Agent backend and watch the protocol in action: