Skip to main content
The @polpo-ai/sdk package provides a typed HTTP client, SSE streaming, and chat completions for interacting with Polpo from any TypeScript or JavaScript environment.

Install

npm install @polpo-ai/sdk

Quick start

import { PolpoClient } from "@polpo-ai/sdk";

const client = new PolpoClient({
  baseUrl: "https://{project}.polpo.cloud",
  apiKey: "sk_live_...", // your project API key
});

// Talk to an agent
const stream = client.chatCompletionsStream({
  agent: "backend-dev",
  messages: [{ role: "user", content: "Refactor the auth module" }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

console.log("\nSession:", stream.sessionId);

Configuration

const client = new PolpoClient({
  baseUrl: "https://{project}.polpo.cloud",  // Polpo
  apiKey: "sk_live_...",            // project API key
});
OptionTypeDefaultDescription
baseUrlstringrequiredAPI base URL
apiKeystringProject API key (sk_live_... or sk_test_...)
apiPrefixstringauto/v1 for cloud, /api/v1 for self-hosted
fetchtypeof fetchglobalThis.fetchCustom fetch implementation
The SDK auto-detects the API prefix based on the base URL. Cloud (*.polpo.cloud) uses /v1, self-hosted uses /api/v1. Override with apiPrefix if needed.

Chat completions

The completions API is OpenAI-compatible. Target a specific agent with the agent field, or omit it to talk to the orchestrator.

Streaming

const stream = client.chatCompletionsStream({
  agent: "researcher",
  messages: [{ role: "user", content: "Find recent papers on RAG" }],
  sessionId: "existing-session-id", // optional — resume a conversation
});

for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta?.content;
  if (delta) process.stdout.write(delta);
}

// Metadata available after streaming
console.log("Session ID:", stream.sessionId);
console.log("Aborted?", stream.aborted);
The ChatCompletionStream is an AsyncIterable that also exposes metadata:
PropertyTypeDescription
sessionIdstring | nullSession ID from the x-session-id response header
abortedbooleanWhether abort() was called
askUserAskUserPayload | nullStructured questions when finish_reason is "ask_user"
missionPreviewMissionPreviewPayload | nullProposed mission when finish_reason is "mission_preview"

Aborting a stream

const stream = client.chatCompletionsStream({ agent: "dev", messages });

setTimeout(() => stream.abort(), 10_000); // cancel after 10s

for await (const chunk of stream) {
  // loop exits cleanly when aborted
}

Non-streaming

const response = await client.chatCompletions({
  agent: "backend-dev",
  messages: [{ role: "user", content: "Explain the auth flow" }],
});

console.log(response.choices[0].message.content);

Request fields

FieldTypeDescription
messagesChatCompletionMessage[]Conversation history — each message content can be a plain string or ContentPart[]
agentstringTarget agent name (omit for orchestrator)
sessionIdstringResume an existing session
streambooleanEnable streaming (set automatically by the SDK)
modelstringIgnored — Polpo uses the agent’s configured model

Sessions

Sessions persist conversation history across multiple completions calls. Without an x-session-id header, every request creates a new session. To continue a conversation, pass the session ID explicitly:
// First request — a new session is created
const stream = client.chatCompletionsStream({
  agent: "dev",
  messages: [{ role: "user", content: "Hello" }],
});
for await (const chunk of stream) { /* ... */ }
const sid = stream.sessionId; // capture the session ID

// Subsequent requests — pass sessionId to continue the conversation
const stream2 = client.chatCompletionsStream({
  agent: "dev",
  sessionId: sid,
  messages: [{ role: "user", content: "Follow up question" }],
});
There is no automatic session reuse. If you omit sessionId, a brand-new session is created every time. To force a new session even when passing a header, use sessionId: "new".
// List all sessions
const { sessions } = await client.getSessions();

// Get messages for a session
const { session, messages } = await client.getSessionMessages("session-id");

// Rename a session
await client.renameSession("session-id", "Auth refactor discussion");

// Delete a session
await client.deleteSession("session-id");

Agents

// List all agents
const agents = await client.getAgents();

// Get a specific agent
const agent = await client.getAgent("backend-dev");

// Add a new agent
await client.addAgent({
  name: "qa-engineer",
  role: "Writes and runs tests",
  model: "anthropic/claude-sonnet-4-5",
  allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
});

// Update an agent
await client.updateAgent("qa-engineer", { maxTurns: 200 });

// Remove an agent
await client.removeAgent("qa-engineer");

// List teams
const teams = await client.getTeams();

Memory

// Shared memory (all agents)
const { content } = await client.getMemory();
await client.saveMemory("Updated project context...");

// Per-agent memory
const agentMem = await client.getAgentMemory("backend-dev");
await client.saveAgentMemory("backend-dev", "Prefers functional patterns...");

Vault

Manage encrypted credentials. The API never exposes raw secret values in list operations.
// Save a credential
await client.saveVaultEntry({
  agent: "emailer",
  service: "smtp",
  type: "smtp",
  label: "Company SMTP",
  credentials: { host: "smtp.company.com", user: "bot@company.com", pass: "..." },
});

// List entries (metadata only — no secret values)
const entries = await client.listVaultEntries("emailer");

// Update specific fields (merge, not replace)
await client.patchVaultEntry("emailer", "smtp", {
  credentials: { pass: "new-password" },
});

// Remove
await client.removeVaultEntry("emailer", "smtp");

File attachments

To attach a file to a chat message, upload it via the files API and reference the path:
// 1. Upload the file to the workspace
const result = await client.uploadFile(myFile, "workspace");

// 2. Reference it in a chat completion
const stream = client.chatCompletionsStream({
  agent: "coder",
  messages: [{
    role: "user",
    content: [
      { type: "text", text: "Analyze this report" },
      { type: "file", file_id: `workspace/${myFile.name}` },
    ],
  }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
The agent receives the file path and uses its tools (read_file, analyze_image) to access the content.

Client-side tools

Some agent tools are handled by your client instead of the server. When the agent calls a client-side tool, the stream returns finish_reason: "tool_calls" with the tool call data. Your client processes it and sends the result back.
const stream = client.chatCompletionsStream({
  agent: "coder",
  messages: [{ role: "user", content: "Deploy the app" }],
});

for await (const chunk of stream) {
  const choice = chunk.choices[0];

  // Normal text
  if (choice?.delta?.content) {
    process.stdout.write(choice.delta.content);
  }

  // Client-side tool call (e.g. ask_user_question)
  if (choice?.finish_reason === "tool_calls" && choice.delta.tool_calls?.length) {
    const tc = choice.delta.tool_calls[0];
    const args = JSON.parse(tc.function.arguments);

    // Handle the tool (show UI, collect input, etc.)
    const answer = await handleToolCall(tc.function.name, args);

    // Send the result back
    const resumed = client.chatCompletionsStream({
      agent: "coder",
      messages: [
        { role: "user", content: "Deploy the app" },
        { role: "tool", tool_call_id: tc.id, name: tc.function.name, content: answer },
      ],
    });

    for await (const chunk of resumed) {
      process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
    }
  }
}
The built-in client-side tool is ask_user_question — the agent sends structured questions with selectable options, and your client collects the user’s response.

Files

Browse and manage the agent workspace filesystem.
// List workspace roots (top-level directories)
const roots = await client.getFileRoots();

// List entries in a directory
const entries = await client.listFiles("/src");

// Preview a file (returns content for text files, metadata for binary)
const preview = await client.previewFile("/src/index.ts");

// Read a file (returns raw Response)
const response = await client.readFile("/src/index.ts");
const text = await response.text();

// Read a file and trigger browser download
const downloadResponse = await client.readFile("/dist/bundle.js", true);

// Upload a file
await client.uploadFile("/src/config.json", myFile, "config.json");

// Create a directory
await client.createDirectory("/src/utils");

// Rename a file or directory
await client.renameFile("/src/old-name.ts", "new-name.ts");

// Delete a file or directory
await client.deleteFile("/src/obsolete.ts");

// Search files by name
const results = await client.searchFiles("config", "/src", 20);

File methods reference

MethodDescription
listFiles(path?)List entries in a directory
previewFile(path, maxLines?)Preview file content (text) or metadata (binary)
readFile(path, download?)Get raw file contents as a Response
uploadFile(destPath, file, filename)Upload a file via multipart form data
createDirectory(path)Create a new directory
renameFile(path, newName)Rename a file or directory
deleteFile(path)Delete a file or directory
searchFiles(query?, root?, limit?)Search files by name

ETag caching

File reads (readFile) return ETag headers. Browsers automatically send If-None-Match on subsequent requests and receive 304 Not Modified (zero transfer) when the content has not changed.

Skills

// List skills with agent assignments
const skills = await client.getSkills();

// Create a new skill
await client.createSkill({
  name: "code-review",
  description: "Reviews PRs for style and correctness",
  instructions: "Check for bugs, style issues, and missing tests.",
});

// Install skills from a registry or URL
await client.installSkills({
  skills: ["@polpo-ai/skill-code-review", "@polpo-ai/skill-testing"],
});

// Assign a skill to an agent
await client.assignSkill("code-review", "backend-dev");

// Unassign
await client.unassignSkill("code-review", "backend-dev");

// Delete a skill
await client.deleteSkill("code-review");

Schedules

Create and manage scheduled tasks (cron-based).
// Create a schedule
await client.createSchedule({
  name: "nightly-report",
  cron: "0 2 * * *",
  taskTemplate: {
    title: "Generate nightly report",
    agent: "reporter",
  },
});

// Update a schedule
await client.updateSchedule("nightly-report", {
  cron: "0 3 * * *",
  enabled: false,
});

// Delete a schedule
await client.deleteSchedule("nightly-report");

Playbooks

Manage reusable task playbooks.
// Create a playbook
await client.createPlaybook({
  name: "onboard-repo",
  description: "Analyze a new repo and set up agents",
  steps: [
    { title: "Scan codebase", agent: "analyst" },
    { title: "Create agents", agent: "orchestrator" },
  ],
});

// Delete a playbook
await client.deletePlaybook("onboard-repo");

Real-time events (SSE)

Subscribe to live events from your project using the EventSourceManager.
import { EventSourceManager } from "@polpo-ai/sdk";

const events = new EventSourceManager({
  url: client.getEventsUrl(), // includes auth
  onEvent: (event) => {
    console.log(`[${event.event}]`, event.data);
  },
  onStatusChange: (status) => {
    console.log("Connection:", status);
  },
});

events.connect();

// Later...
events.disconnect();

Connection status

The onStatusChange callback receives one of:
StatusDescription
connectingOpening the SSE connection
connectedConnected and receiving events
reconnectingConnection lost, retrying with exponential backoff
disconnectedManually disconnected via disconnect()
errorConnection failed

Filtering events

Pass event type prefixes to getEventsUrl to receive only specific events:
const url = client.getEventsUrl(["agent:", "session:"]);

Event types

Events follow the pattern category:action. Key categories for agents:
EventPayload
agent:spawned{ taskId, agentName, taskTitle }
agent:finished{ taskId, agentName, exitCode, duration, sessionId }
agent:activity{ taskId, agentName, tool, file, summary }
session:created{ sessionId, title }
message:added{ sessionId, messageId, role }

Error handling

All methods throw PolpoApiError on failure:
import { PolpoApiError } from "@polpo-ai/sdk";

try {
  await client.getAgent("nonexistent");
} catch (err) {
  if (err instanceof PolpoApiError) {
    console.log(err.message);    // human-readable message
    console.log(err.code);       // "NOT_FOUND", "AUTH_REQUIRED", etc.
    console.log(err.statusCode); // HTTP status
  }
}

Request deduplication

Concurrent identical GET requests are automatically deduplicated. If you call getAgents() twice before the first resolves, only one HTTP request is made and both callers receive the same result.