Pingerchips LogoPingerchips
Chat / SessionsReference

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().

MethodDescription
createRun(runId)RunCreate 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.

MethodDescription
start(ownerClientId?)PromiseAnnounce run:start.
loadConversation()Promise<object[]>Fetch the thread's messages.
write(content)PromisePush the full accumulated assistant text so far. The server buffers and flushes to subscribers every ~40 ms.
end(reason?)PromiseEnd the run. reason: "complete" (default) | "cancelled" | "error". Triggers the final-message commit.
suspend({ toolCallId, toolName, args })PromisePause for human tool approval.
resume()PromiseResume 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');

On this page