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
});
| Option | Type | Default | Description |
|---|
baseUrl | string | required | API base URL |
apiKey | string | — | Project API key (sk_live_... or sk_test_...) |
apiPrefix | string | auto | /v1 for cloud, /api/v1 for self-hosted |
fetch | typeof fetch | globalThis.fetch | Custom 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:
| Property | Type | Description |
|---|
sessionId | string | null | Session ID from the x-session-id response header |
aborted | boolean | Whether abort() was called |
askUser | AskUserPayload | null | Structured questions when finish_reason is "ask_user" |
missionPreview | MissionPreviewPayload | null | Proposed 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
| Field | Type | Description |
|---|
messages | ChatCompletionMessage[] | Conversation history — each message content can be a plain string or ContentPart[] |
agent | string | Target agent name (omit for orchestrator) |
sessionId | string | Resume an existing session |
stream | boolean | Enable streaming (set automatically by the SDK) |
model | string | Ignored — 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.
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
| Method | Description |
|---|
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:
| Status | Description |
|---|
connecting | Opening the SSE connection |
connected | Connected and receiving events |
reconnecting | Connection lost, retrying with exponential backoff |
disconnected | Manually disconnected via disconnect() |
error | Connection 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:
| Event | Payload |
|---|
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.