@darqlabs/curator-sdk
v0.1.4
Published
Official Curator SDK — talk to your deployed agents from server-side JavaScript or TypeScript.
Readme
@darqlabs/curator-sdk
Official Curator SDK.
Integrate with your deployed agents. Works in Node, the browser, and modern edge runtimes (anywhere fetch exists).
const reply = await curator.agent("support-bot").run("How do I reset my password?")
//console.log(reply.content)Integration guide
Before you start
You'll need three things from your Curator dashboard:
- An API key. Generate one at Settings → Developer. Each key is scoped to a single environment (production, staging, or development) — pick the one you're integrating against.
- Your project slug.
- A deployed agent's identifier slug.
1. Install
npm install @darqlabs/curator-sdk
# or
pnpm add @darqlabs/curator-sdk
# or
yarn add @darqlabs/curator-sdkRequires Node 18+ or any runtime with a global fetch. Ships ESM + CommonJS — modern bundlers tree-shake it automatically.
2. Initialize the client
Create one Curator instance and reuse it for the lifetime of your process.
import { Curator } from "@darqlabs/curator-sdk"
export const curator = new Curator({
apiKey: "CURATOR_API_KEY",
project: "default", // your project slug
environment: "production", // 'production' | 'staging' | 'development'
})3. Send your first message
For a single round-trip, use agent.run():
const reply = await curator
.agent("support-bot")
.run("Hi, I lost my receipt — can you resend it for order #4823?")
console.log(reply.content)
// reply.conversationId — store this if you want to follow up
// reply.runId — useful for log correlation
// reply.sequence — per-conversation message index
// reply.metadata — structured result from the agent, or null (see below)4. Multi-turn conversations
For ongoing chat, use agent.chat():
const chat = curator.agent("support-bot").chat()
await chat.send("I'd like to upgrade my plan.")
await chat.send("Actually, what's included in the Pro tier?")
console.log(chat.id) // "conv_8f2a1c4b9e0d"5. Resume a conversation later
Store chat.id somewhere durable, then reattach with .conversation(id):
const chat = curator.agent("support-bot").conversation(conversationId)
const reply = await chat.send("Following up on what we discussed yesterday…")You can also fetch the prior message history:
const page = await chat.history({ limit: 50 })
for (const msg of page.messages) {
console.log(`[${msg.role}] ${msg.content}`)
}
if (page.hasMore) {
const next = await chat.history({ limit: 50, beforeSequence: page.oldestSequence! })
}6. Handle errors
The SDK throws typed errors for non-success states. Catch the ones you care about:
import {
CuratorApprovalRequiredError,
CuratorAgentError,
CuratorTimeoutError,
CuratorAuthError,
CuratorNotFoundError,
} from "@darqlabs/curator-sdk"
try {
const reply = await chat.send("Refund my last order.")
// …use reply.content
} catch (err) {
if (err instanceof CuratorApprovalRequiredError) {
// The agent paused waiting for human approval. Resolution happens in
// the Curator dashboard — surface a friendly message to your user.
showToUser(
`That action needs administrator approval. Please contact your ` +
`administrator to review the request for "${err.approval.tool_name}".`,
)
} else if (err instanceof CuratorTimeoutError) {
// The wait expired but the agent is still working in the background.
// Either give the user a "still thinking…" message, or keep waiting:
const reply = await err.continueWaiting(180000)
} else if (err instanceof CuratorAgentError) {
// Terminal agent failure — LLM quota, provider auth, etc.
// err.failureClass is one of: provider_quota_exhausted | provider_auth |
// provider_bad_request | provider_rate_limited | provider_unavailable |
// internal | unknown. err.isPermanent tells you whether retrying helps.
log.error({ err }, "agent run failed")
} else if (err instanceof CuratorAuthError) {
// 401 — bad / revoked API key.
} else if (err instanceof CuratorNotFoundError) {
// 404 — wrong deployment slug, project, or conversation id.
} else {
throw err
}
}All errors inherit from CuratorError, so a single catch (err: CuratorError) works as a backstop if you'd rather handle them generically.
7. Attach files
Give the SDK the bytes, get back a reference ready to attach:
const fileRef = await curator.files.upload({
filename: "sales-2026-q2.csv",
mimeType: "text/csv",
data: csvBlob, // Blob, File, ArrayBuffer, Uint8Array, or Buffer
})
const reply = await chat.send("What does this CSV show?", {
files: [fileRef],
})Two-step — useful when you want to drive the PUT yourself (progress UI, resumable uploads, custom transport):
const { uploadUrl, headers, fileReference } = await curator.files.createUpload({
filename: "sales-2026-q2.csv",
mimeType: "text/csv",
size: csvBlob.size,
})
await fetch(uploadUrl, { method: "PUT", headers, body: csvBlob })
const reply = await chat.send("What does this CSV show?", {
files: [fileReference],
})Common patterns
Stateful chat in a web app
const chat = userSession.conversationId
? curator.agent("support-bot").conversation(userSession.conversationId)
: curator.agent("support-bot").chat()
const reply = await chat.send(userInput)
if (!userSession.conversationId) {
await persistConversationId(userSession.id, chat.id!)
}
return reply.contentLong-running runs
Raise the timeout per-call (server cap 5 minutes):
const reply = await chat.send("Do a deep research pass on this brief.", {
timeoutMs: 300000,
})Structured results (metadata)
An agent whose instructions define a structured output — a booking draft, an extracted form, a plan — returns that object on reply.metadata alongside the conversational reply.content. It's null for plain conversational agents. The shape is whatever the agent emits (schema-agnostic); validate it in your app (e.g. with a zod schema).
const reply = await chat.send("Book Acme from Fontana to Ontario on July 1.")
console.log(reply.content) // "I've started the booking draft…"
console.log(reply.metadata) // { bookingDraft: { origin: {…}, … }, changedFields: [...], clarification: null }Live updates: chat.send() returns only the final snapshot. To render progress as the agent fills the object in — turn by turn, before it replies — stream instead and handle state events:
for await (const event of chat.stream("Book Acme from Fontana to Ontario.")) {
if (event.type === "delta") appendText(event.text) // prose tokens
else if (event.type === "state") renderDraft(event.metadata) // live structured snapshot, every turn
else if (event.type === "done") finalize(event.message) // message.metadata = final snapshot
}Using React? @darqlabs/curator-sdk-react's <CuratorChat onState={…} /> wires this up for you.
Using from a browser
CORS: browser requests hit the Curator API directly, so your origin must be on the allowlist. Add it in Settings → Developer.
Optional: verified end-user identity
You don't need this to get started. An API key + project is all it takes to talk to your agents — everything above works without any user-level identity.
Turn this on only when you want Curator to know which of your users is driving a conversation — for per-user policies, audit attribution, or tenant isolation. It's opt-in per org: until an operator enables it in Settings → Developer → End-user auth, the SDK ignores endUserJwt entirely and requests authenticate on the API key alone.
When enabled, your backend mints a short-lived JWT per user and hands it to the SDK. The server verifies it against your configured JWKS endpoint:
const curator = new Curator({
apiKey: process.env.CURATOR_API_KEY!,
project: "default",
// Static token, or a function the SDK calls per request to refresh it.
endUserJwt: () => mintEndUserJwtForCurrentUser(),
})Standing up a JWKS endpoint is not required for MVP — skip this section and revisit it when you need verified identity.
API reference
new Curator(options)
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| apiKey | string | — | Required. curator_live_* (production) or curator_test_* (staging / development), from Settings → Developer. |
| project | string | — | Default project slug. Required unless every call passes { system: true }. |
| environment | 'production' \| 'staging' \| 'development' | 'production' | Default environment. Must match the environment your apiKey was issued for. |
| baseUrl | string | managed host | Override for self-hosted. |
| defaultTimeoutMs | number | 120000 | Server-capped at 300000. |
| defaultHeaders | Record<string,string> | {} | Sent on every request. |
| fetch | typeof fetch | global | Inject a custom fetch. |
| endUserJwt | string \| () => string \| Promise<string> | — | Optional. Only needed if your org has turned on verified end-user identity (see below). Leave it unset otherwise. |
curator.agent(slug, locator?)
Returns an Agent bound to the given deployment. locator accepts { project?, environment?, system? } to override the constructor defaults for this address.
agent.run(content, opts?) → Promise<AssistantMessage>
Send a single message and return the reply. Equivalent to agent.chat().send(content, opts).
agent.chat() → Chat
Returns a new chat handle for multi-turn conversations.
agent.conversation(id) → Chat
Reattach to an existing conversation by id.
chat.send(content, opts?) → Promise<AssistantMessage>
opts.files?: FileAttachment[] — attachments.
opts.timeoutMs?: number — override the wait timeout for this call.
Returns the final assistant message. message.metadata holds the agent's structured result (or null).
chat.stream(content, opts?) → AsyncIterable<StreamEvent>
Same send, streamed. Yields events as they arrive:
| event.type | Payload | Terminal |
| --- | --- | --- |
| delta | text — incremental prose tokens | no |
| state | metadata — complete structured snapshot for this turn | no |
| tool_call.started / tool_call.completed | tool call id / name | no |
| done | message (with message.metadata) | yes |
| error | failureClass, message | yes |
| paused_for_approval | approval | yes |
state fires on every turn the agent reports structured state — see Structured results.
chat.history(opts?) → Promise<HistoryPage>
opts.limit?: number — page size (server cap 500).
opts.beforeSequence?: number — cursor; pass page.oldestSequence from the previous page.
chat.id
Conversation id, or null until the first send.
curator.files.upload(input) → Promise<FileAttachment>
One-shot upload. input is { filename, mimeType, data, size? } where data is a Blob, File, ArrayBuffer, Uint8Array, or Buffer. Returns the reference to pass into chat.send(..., { files: [...] }).
curator.files.createUpload(input) → Promise<PresignUploadResult>
Two-step: returns { uploadUrl, method, headers, expiresAt, fileReference }. PUT the bytes to uploadUrl with the returned headers, then attach fileReference to a send.
Errors
| Class | Thrown when |
| --- | --- |
| CuratorAuthError | 401 — bad / revoked API key. |
| CuratorForbiddenError | 403 — permission denied. |
| CuratorNotFoundError | 404 — unknown deployment / conversation. |
| CuratorDeploymentRetiredError | 410 — deployment retired. |
| CuratorRateLimitError | 429 — back off. |
| CuratorApprovalRequiredError | Run paused for human approval. |
| CuratorAgentError | Terminal agent failure (LLM quota, etc.). failureClass + isPermanent. |
| CuratorTimeoutError | Wait expired. continueWaiting() resumes. |
| CuratorError | Base class — everything above extends this. |
Troubleshooting
CuratorAuthError: Invalid or revoked API key — double-check the key in Settings → Developer. The SDK sends it as Authorization: Bearer <key>.
CuratorNotFoundError: Deployment not found — verify (project slug, environment, deployment slug) all match what the Deployments tab shows. Missing one of these is the #1 cause.
CuratorTimeoutError on every call — your agent is taking longer than defaultTimeoutMs. Raise the timeout (new Curator({ defaultTimeoutMs: 300000 })) or call err.continueWaiting() once.
Browser requests fail with a CORS error — add your origin to the allowlist in Settings → Developer. The SDK isn't doing anything unusual; the request gets blocked before it reaches Curator.
Approval-required pauses on every send — your agent has tools that require human approval. Resolve them once in the dashboard and adjust the deployment's policy if you don't want this gate in your environment.
