@skopiklabs/sdk
v0.5.1
Published
Skopik SDK — sessions, messages, remotes, and the agent control plane.
Maintainers
Readme
@skopiklabs/sdk
One client for Skopik Sessions, Messages, Remotes, and control-plane resources. The root entry is browser-safe and hides transport management.
import { Skopik } from '@skopiklabs/sdk'
const skopik = new Skopik({ apiKey: process.env.SKOPIK_API_KEY })
const session = skopik.session({ agent: 'support', key: 'chat:42' })
const answer = await session.send('Summarize the latest customer thread')
console.log(answer.parts)Turn is awaitable and async iterable, so the same call supports final-answer
and live-snapshot styles:
for await (const message of session.send('Now draft a reply')) {
render(message)
}Transport
The SDK uses one multiplexed WebSocket for watching sessions, sending, approvals, lifecycle operations, notifications, and agent status. It reconnects from each subscription cursor.
Every subscription has an HTTP/SSE fallback carrying the same frames from the same cursor, so a blocked upgrade or a runtime without WebSockets degrades instead of failing:
| Call | WebSocket | Fallback |
| --- | --- | --- |
| session.send() | send op | POST /sessions[/{id}/messages] (SSE body) |
| session.watch(), sessions.stream() | { session } | GET /sessions/{id}/stream |
| notifications() | { org: 'notifications' } | GET /notifications/stream |
| watchAgents() | { org: 'agents' } | GET /agents/stream |
| approve(), cancel(), pause(), resume() | op | the REST equivalent |
Browsers and Node 22+ need no setup. On Node 20 and 21 the SDK loads the
optional ws peer if it is installed; otherwise it stays on HTTP/SSE. To pin
an implementation — a custom runtime, or a test double — pass it in:
import WebSocket from 'ws'
const skopik = new Skopik({ apiKey, webSocket: WebSocket })Pass webSocket: null to force the HTTP/SSE transport.
In a browser, authenticate with getToken (a short-lived token) rather than
apiKey: the socket carries its credential in a subprotocol, so an org API
key would be shipped to the client.
The existing camelCase resource client remains available on the same object:
await skopik.agents.create({ /* … */ })
await skopik.templates.create({ /* … */ })
await skopik.sessions.open({ prompt: 'Prepare a concise release draft.' })Resources
| Accessor | What it manages |
| --- | --- |
| skopik.agents | Agents |
| skopik.templates | Reusable agent templates |
| skopik.skills | The reusable skill catalog |
| skopik.sessions | Agent work: open, continue, send, stream, inspect, and cancel |
| skopik.automations | Scheduled / event-triggered sessions |
| skopik.runs | Runtime executor wire for serving sessions on Remotes |
| skopik.files | Files owned by Agents, Templates, Skills, and Sessions |
| skopik.search | Semantic and reference search |
| skopik.remotes | Remote compute mounts |
| skopik.apiKeys | API key management |
skopik.me() returns the authenticated caller (whoami). Workspace and identity
surfaces are first-party concerns. Each first-party app owns the small local
wrappers it needs; Console layers its user, org, and onboarding resources onto
this client.
Agents
const agent = await skopik.agents.upsert({
name: 'Release Helper',
description: 'Prepares release notes and follows deployment checklists.',
status: 'active',
defaultModelId: 'gpt-5',
canWrite: true,
dailyBudgetUsd: 20,
})
await skopik.files.writeText({
target: agent.agentId,
path: 'AGENTS.md',
}, {
content: '# Release Helper\n\nYou prepare release notes and checklist updates.',
contentType: 'text/markdown',
})
const stats = await skopik.agents.stats(agent.agentId, { window: '30d' })
console.log(stats.uptimeRatio, stats.usage.totalCostUsd)
for await (const status of skopik.watchAgents()) {
renderAgentStatus(status)
}watchAgents() uses one org-wide state+tail WebSocket subscription (or the
/agents/stream SSE twin) for the directory snapshot and live running /
idle / blocked / offline changes. agents.stats() is the durable
read-side rollup over Sessions, runtime leases, Problems, and usage events.
Templates and skills
Templates are reusable agent blueprints; skills are reusable capability
packages. Both catalogs live at the SDK root. Use upsert when the immutable
name should update an existing item or create it on first use.
const supportSkill = await skopik.skills.upsert({
name: 'support-policy',
displayName: 'Support Policy',
description: 'Applies the support escalation policy.',
tags: [ 'support' ],
})
// Author an agent template and add a file.
const template = await skopik.templates.upsert({
name: 'support-triage',
displayName: 'Support Triage',
description: 'Triages inbound support email and drafts replies.',
skills: [ supportSkill.skillId ],
instructions: 'Escalate security and billing issues instead of guessing.',
tags: [ 'support' ],
})
await skopik.files.writeText({
target: template.templateId,
path: 'AGENTS.md',
}, {
content: '# Support Triage\n\nYou triage inbound support requests.',
contentType: 'text/markdown',
})
// Spin up a new agent from a template.
const { agent } = await skopik.templates.use(template.slug, {
name: 'Support Bot',
})
// Browse and install skills.
const { data: skills } = await skopik.skills.list({ tag: 'email' })
await skopik.agents.skills.install(agent.agentId, { skillId: skills[0]!.skillId })Sessions
const result = await skopik.run({
agent: agent.agentId,
prompt: 'Summarize the shipped API and SDK changes into launch notes.',
waitTimeoutMs: 5 * 60_000,
})
console.log(result.text)
console.log(result.usage.totalTokens, result.usage.costUsd)
console.log(result.timing.durationMs)Client tools
A client tool is a function tool your application executes itself: the model
sees the definition, a call parks the session at
waiting { kind: "tool_outputs" }, and the session resumes when the result
is submitted. Declare tools with createClientTool — a zod schema or raw
JSON Schema — and give them an execute to have the SDK serve them
automatically inside skopik.run() and while session.watch() is consuming:
import { createClientTool, createSkopik } from '@skopiklabs/sdk'
import { z } from 'zod'
const lookupOrder = createClientTool({
name: 'lookup_order',
description: 'Look up an order in the local commerce database.',
inputSchema: z.object({ order_id: z.string() }),
execute: async ({ order_id }) => db.orders.find(order_id),
})
const result = await skopik.run({
agent: 'support-bot',
prompt: 'Where is order A1?',
clientTools: [ lookupOrder ],
})Tools without an execute surface for the caller to resolve — through
session.on('tool_call') handles, the pendingToolCalls head field, or the
raw endpoints:
const session = skopik.session({ agent: 'support-bot', clientTools: [ confirmRefund ] })
session.on('tool_call', async (call) => {
await call.submit(await confirmInUi(call.input)) // or call.error('declined')
})
// Raw resource surface: singles, batches, and error results.
await skopik.sessions.submitToolOutput(sessionId, { tool, toolCallId, output })
await skopik.sessions.submitToolOutputs(sessionId, [
{ tool: 'lookup_order', toolCallId: 'call_a', output: { status: 'shipped' } },
{ tool: 'check_stock', toolCallId: 'call_b', error: { code: 'timeout', message: 'inventory API timed out' } },
])Parallel calls resolve in any order; the parked execution wakes when the
awaiting step has no unresolved call left (delivery stays "queued" until
then). Sending a plain message to a waiting session supersedes its still-open
client-tool calls as tool errors.
For detached execution, open a session directly and attach to its resumable stream:
const session = await skopik.sessions.open({
template: 'templates/software-engineer',
skills: [ 'skill_123' ],
prompt: 'Prepare a concise release draft.',
})
for await (const frame of skopik.sessions.stream(session.sessionId)) {
console.log(frame.envelope)
}For recurring work, use skopik.automations (schedule- or event-triggered):
const automation = await skopik.automations.create({
name: 'Morning brief',
trigger: {
type: 'schedule',
schedule: { kind: 'cron', expression: '0 9 * * ? *', timezone: 'America/New_York' },
},
session: {
agent: 'agent_1',
prompt: 'Summarize overnight activity.',
policy: { completeWhen: 'agent_done' },
},
})
await skopik.automations.run(automation.automationId)React, AI SDK, and Remotes
The optional entry points keep framework and Node-only code out of the browser-safe root:
import { useSession } from '@skopiklabs/sdk/react'
import { SkopikChatTransport, toUIMessageStream } from '@skopiklabs/sdk/aisdk'
import { createRemote, createRemoteTool } from '@skopiklabs/sdk/remote'SkopikChatTransport is a useChat transport whose AI SDK chunks are an
explicit API-edge projection of the native Messages Protocol:
import { useChat } from '@ai-sdk/react'
import { SkopikChatTransport } from '@skopiklabs/sdk/aisdk'
const chat = useChat({
transport: new SkopikChatTransport({ client: skopik, sessionId }),
})The transport also carries the client-tool loop: with
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, results
recorded through addToolResult are submitted to tool_outputs and the
woken turn streams back into the chat.
Use toUIMessageStream when an existing AI SDK consumer needs a projected
ReadableStream; new Skopik-native UI can use useSession directly — it
exposes pendingToolCalls and submitToolOutput alongside the message
stream, and tools declared with an execute serve themselves while the hook
watches.
Files
const body = new TextEncoder().encode('# Release checklist\n')
const upload = await skopik.files.createUploadUrl({
target: agent.agentId,
path: 'references/release-checklist.md',
contentType: 'text/markdown',
contentLength: body.byteLength,
})
await fetch(upload.uploadUrl, {
method: 'PUT',
headers: upload.uploadHeaders,
body,
})
const file = await skopik.files.stat({
target: agent.agentId,
path: 'references/release-checklist.md',
})
console.log(file.etag, file.contentLength)
const changed = await skopik.files.readTextIfChanged(
{ target: file.target, path: file.path },
file.etag!,
)
if (changed === null) console.log('Already current')Pagination
Current v1 list routes return one page in { data }. The exported paginate
helper also follows page.nextCursor when a route exposes it.
import { paginate } from '@skopiklabs/sdk'
for await (const agent of paginate((params) => skopik.agents.list(params), { limit: 50 })) {
console.log(agent.agentId, agent.name)
}