Agent SDK
The server-side chat API for LLM processes — AgentSession and Run.
The agent side of a thread — for the server process that generates and streams assistant responses.
import { AgentSession } from 'pingerchips-js-server/agent-session.js';AgentSession lives in pingerchips-js-server (the server package), at the
subpath pingerchips-js-server/agent-session.js — it is not re-exported from
the package root. Building on the Vercel AI SDK? Use
@pingerchips/ai's
createAgentSession instead.
Agents authenticate with the App Secret, not a user token.
AgentSession
Constructor
new AgentSession(appKey: string, appSecret: string, options?: {
endpoint?: string; // ws host; default wss://queue.pingerchips.com/socket
// (or the PINGERCHIPS_ENDPOINT env var)
})connect(threadId, agentId)
connect(threadId: string, agentId: string): Promise<AgentThreadSession>Joins chat:v1:app:{appKey}:thread:{threadId} as an agent.
const agent = new AgentSession(
process.env.PINGERCHIPS_APP_KEY,
process.env.PINGERCHIPS_APP_SECRET,
);
const thread = await agent.connect('thread-uuid', 'claude-assistant');AgentThreadSession
Returned by agent.connect().
| Method | Description |
|---|---|
createRun(runId) → Run | Create a run handle. runId is required (it comes from the invocation that woke your agent — see below). |
close() | Leave the thread channel. |
Run
Returned by thread.createRun(runId). Drives one LLM generation.
| Method | Description |
|---|---|
start(ownerClientId?) → Promise | Announce run:start. |
loadConversation() → Promise<object[]> | Fetch the thread's messages. |
write(content) → Promise | Push the full accumulated assistant text so far. The server buffers and flushes to subscribers every ~40 ms. |
end(reason?) → Promise | End the run. reason: "complete" (default) | "cancelled" | "error". Triggers the final-message commit. |
suspend({ toolCallId, toolName, args }) → Promise | Pause for human tool approval. |
resume() → Promise | Resume after approval. |
close() | Detach the run handle. |
run.abortSignal — an AbortSignal that fires when the run is cancelled from
the client. Pass it to your LLM call.
The streaming method is run.write(content) — pass the whole string so far,
not the latest delta. There is no pushToken. run.end() takes a reason
string, not { content } — the final content is whatever you last write().
How an agent is invoked
Your agent doesn't listen for messages on the channel. Instead, when a user
sends a message, your backend receives an HTTP call (the "invocation") carrying
{ threadId, runId }. You spin up an AgentSession, createRun(runId), and
generate. The runId is minted by the client and passed straight through.
The @pingerchips/ai package models
this with an Invocation object and a Next.js route helper.
Full streaming example
import { AgentSession } from 'pingerchips-js-server/agent-session.js';
import Anthropic from '@anthropic-ai/sdk';
// Called from your invocation route with { threadId, runId } from the request body
export async function handleInvocation({ threadId, runId, userText }) {
const agent = new AgentSession(
process.env.PINGERCHIPS_APP_KEY,
process.env.PINGERCHIPS_APP_SECRET,
);
const thread = await agent.connect(threadId, 'claude-assistant');
const run = thread.createRun(runId);
await run.start();
let accumulated = '';
try {
const stream = new Anthropic().messages.stream(
{
model: 'claude-sonnet-5',
max_tokens: 2048,
messages: [{ role: 'user', content: userText }],
},
{ signal: run.abortSignal },
);
stream.on('text', async (delta) => {
accumulated += delta;
await run.write(accumulated); // full string so far
});
await stream.finalMessage();
await run.write(accumulated);
await run.end('complete');
} catch (err) {
await run.end('error');
} finally {
thread.close();
}
}Tool use with suspension
const run = thread.createRun(runId);
await run.start();
const response = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
tools: MY_TOOLS,
messages: history,
});
if (response.stop_reason === 'tool_use') {
const tc = response.content.find((b) => b.type === 'tool_use');
await run.suspend({
toolCallId: tc.id,
toolName: tc.name,
args: tc.input,
});
// The client approves via view.approveTool(runId, toolCallId).
// Your process waits (e.g. poll a queue, or subscribe to the tool-approval
// topic), then:
await run.resume();
const final = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
messages: [...history, /* tool result turn */],
});
await run.write(final.content[0].text);
}
await run.end('complete');