Build
Build an agent
Receive messages, call your own model, reply in the right place and handle long-running work.
Every Pacerelle agent follows the same loop: receive a message, do the work, reply. The SDK takes care of the connection and the encryption; you only write the handler.
client.onMessage(async (message, agent) => {
const answer = await doTheWork(message.text);
await agent.reply(message, { text: answer });
});If you haven't connected an agent yet, start with the Quickstart.
The message object
Your handler receives the decrypted message and a client to answer with.
| JavaScript | Python | Description |
|---|---|---|
id | id | Unique message ID. Use it to reply and as an idempotency key. |
conversationId | conversation_id | The conversation the message belongs to. |
from | from_id | The sender — the person (or agent) to answer. |
text | text | The message text. |
attachments | attachments | Files sent with the message. See Files and media. |
widgetResponse | widget_response | Present when the message is an answer to a widget. |
replyToMessageId | reply_to_message_id | Set when the sender replied to a specific message. |
Reply in the right place
Always answer with the incoming message's conversation and sender, and pass its ID as the message you reply to. The SDK then routes the answer to the exact device that asked, even if another message arrived in the meantime.
// reply() fills in the conversation, the recipient and the source message.
await agent.reply(message, { text: "Done." });
// Equivalent, fully explicit:
await agent.sendMessage({
conversationId: message.conversationId,
to: message.from,
replyToMessageId: message.id,
text: "Done.",
});You can send several messages for one request — a quick acknowledgment, then the result.
Connect a model
Pacerelle doesn't care which model you use. This example uses the OpenAI SDK, which also works with any OpenAI-compatible endpoint — including a local Ollama server. It keeps a short history per conversation so the model remembers context.
import OpenAI from "openai";
import {
createNodeAgentGatewayClient,
createNodeSignalSnapshotStore,
} from "@pacerelle/sdk/node";
// Reads OPENAI_API_KEY. For Ollama: new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "ollama" })
const openai = new OpenAI();
const histories = new Map(); // conversationId → recent messages
const agentId = process.env.PACERELLE_AGENT_ID;
const state = createNodeSignalSnapshotStore(agentId);
const client = createNodeAgentGatewayClient({
agentId,
token: process.env.PACERELLE_AGENT_TOKEN,
e2ee: true,
signalSnapshot: state.signalSnapshot,
onSignalSnapshot: state.onSignalSnapshot,
});
client.onMessage(async (message, agent) => {
if (message.widgetResponse || !message.text) return;
const history = histories.get(message.conversationId) ?? [];
history.push({ role: "user", content: message.text });
// Shows "typing…" in the app while the model thinks.
agent.sendTyping({ conversationId: message.conversationId, to: message.from });
const completion = await openai.chat.completions.create({
model: process.env.OPENAI_MODEL,
messages: [
{ role: "system", content: "You are a helpful assistant running on my computer." },
...history.slice(-20),
],
});
const answer = completion.choices[0].message.content ?? "";
history.push({ role: "assistant", content: answer });
histories.set(message.conversationId, history);
await agent.reply(message, { text: answer });
});
await client.connect();Add your model settings next to the Pacerelle configuration:
PACERELLE_AGENT_ID=agent_...
PACERELLE_AGENT_TOKEN=...
OPENAI_API_KEY=sk-...
OPENAI_MODEL=your-model-nameSwap the OpenAI call for Anthropic, Google, Mistral, LangChain, LlamaIndex, CrewAI or your own code. The Pacerelle part — receive, reply — stays exactly the same.
Show progress
For anything that takes more than a few seconds, let the user know the agent is working:
- Typing indicator —
agent.sendTyping({ conversationId, to })in JavaScript. - Quick acknowledgment — reply
On it…right away, then send the result. - Progress bar — a progress widget you update as work advances.
Long-running work
Messages are handled one at a time, in order. When your handler returns successfully, the SDK marks the message as handled and acknowledges it to the relay. If the handler throws or the process crashes first, the message can be delivered again later.
For work that takes minutes or hours:
- Save the job somewhere durable (a file, a database, a queue) before the handler returns.
- Return quickly, optionally with a first reply such as
Started — I'll report back. - When the job finishes, send the result with
replyToMessageIdset to the original message ID.
Use the message id as an idempotency key so a redelivered message never runs a job twice. See
Production checklist.
Read earlier messages
After a restart, the JavaScript SDK can fetch the recent messages your agent received in a
conversation to rebuild context. Pass before or beforeId to page further back.
const recent = await client.listConversationMessages({
conversationId: message.conversationId,
limit: 50,
skipUndecodable: true,
});