npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ql-agent-sdk

v1.4.0

Published

TypeScript SDK for the Quanton Labs agent platform API (v1).

Readme

ql-agent-sdk

TypeScript SDK for the Quanton Labs agent platform API (v1). Integrate your application with 8 specialized AI agents, capabilities, workflows, and background tasks.

  • Zero runtime dependencies — built on native fetch and Web Streams
  • Full TypeScript — all request/response types exported
  • ESM + CJS — works in Node, Bun, Deno, Next.js, and edge runtimes

Base URL defaults to https://app.quantonlabs.com. Override with baseUrl option or the QL_BASE_URL environment variable.


Install

npm install ql-agent-sdk
# or
pnpm add ql-agent-sdk
# or
yarn add ql-agent-sdk

Requires Node.js 18+ (native fetch).


Quick Start

import { QLClient } from "ql-agent-sdk";

const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });

// Discover available agents
const { agents } = await ql.agents.list();
console.log(agents.map((a) => a.id));
// → ["governing", "finance", "marketing", "inventory", "customer", "developer", "operations", "research"]

// Chat with an agent (non-streaming)
const reply = await ql.agents.chat({
  agentId: "governing",
  messages: [{ role: "user", content: "Summarize this quarter's revenue." }],
});
console.log(reply.content);

Configuration

const ql = new QLClient({
  apiKey,               // required — your Bearer API key (ql_live_...)
  baseUrl,              // optional — defaults to QL_BASE_URL or https://app.quantonlabs.com
  fetch: customFetch,   // optional — override for tests/proxies/custom runtimes
});

The apiKey is held in a private field. It is never returned by the client, included in error messages, or logged.


Authentication

All API calls use Bearer token authentication with your API key:

Authorization: Bearer ql_live_your_key_here

Create keys in your Quanton Labs dashboard → API Keys page. Each key is scoped to a single workspace.

Scope Requirements

| Scope | Grants access to | |-------|-----------------| | workspace | ql.workspace.get(), ql.workspace.getModels(), ql.workspace.update(), ql.workspace.updateModels(), ql.workspace.listCatalogModels(), ql.workspace.verifyOpenRouterKey(), ql.workspace.listKeys() | | agents | ql.agents.list(), ql.agents.get() | | capabilities | ql.agents.capabilities(), ql.agents.runCapability() | | chat | ql.agents.chat(), ql.agents.chatStream() | | messages | ql.agents.history(), ql.agents.listMessages() | | workflows | ql.workflows.run(), ql.workflows.getRun() | | tasks | ql.tasks.create(), ql.tasks.get() | | * | All of the above (default for new keys) |


API Reference

Response Envelope

All non-streaming endpoints return a consistent envelope:

Success:

{ "ok": true, "data": { ... } }

Error:

{ "ok": false, "error": { "code": "not_found", "message": "Agent not found: foo" } }

The SDK unwraps data for you and throws a typed QLError on errors. You never see the envelope directly.


Workspace

ql.workspace.get() → Promise<Workspace>

Scope: workspace

Returns the workspace bound to your API key.

Response (200):

{
  "_id": "6adf926f6f87c9ab86f32c0e",
  "name": "Robin",
  "plan": "standard",
  "createdAt": "2026-10-26T17:48:31.038Z",
  "updatedAt": "2026-10-27T11:56:44.074Z"
}

TypeScript:

const ws = await ql.workspace.get();
ws._id;      // string
ws.name;     // string
ws.plan;     // "standard" | "premium" | "enterprise" | undefined
ws.createdAt; // string (ISO 8601)

ql.workspace.getModels() → Promise<WorkspaceModelsResult>

Scope: workspace

Returns the per-role model configuration (director panel): defaults for reasoning, instruct, response, vision, image, video plus ordered fallback lists. The OpenRouter key is always masked (keyMasked).

ql.workspace.update(input) / ql.workspace.updateModels(input) → Promise<Workspace>

Scope: workspace

Partially updates the workspace. update() accepts { name?, config?: { openrouter? } }; updateModels() is a shortcut that only touches config.openrouter. Model fields may be set to "" to clear a default; each fallbacks.<role> list holds at most 3 model ids.

await ql.workspace.update({ name: "Robinson Mfg. Co., Inc." });
await ql.workspace.updateModels({
  reasoning: "deepseek/deepseek-v4-flash-0731",
  vision: "anthropic/claude-sonnet-5.5",
  fallbacks: { reasoning: ["openai/gpt-oss-20b"] },
});

ql.workspace.listCatalogModels() → Promise<CatalogResult>

Scope: workspace

OpenRouter model catalog ({ id, name, contextLength, promptPrice, completionPrice }[]), cached server-side for 10 minutes. May return stale: true when upstream is down.

ql.workspace.verifyOpenRouterKey(key, model?, baseUrl?) → Promise<VerifyKeyResult>

Scope: workspace

Checks an OpenRouter key with a 1-token completion (default model openai/gpt-oss-20b). Returns { valid: true } or { valid: false, error }.

ql.workspace.listKeys() → Promise<KeysResult>

Scope: workspace

Lists the workspace's v1 API keys with rotation metadata. Every key is auto-deleted lifetimeDays (90) days after creation; rotatesAt/daysLeft show time remaining. Mint replacements from the Quanton Labs site when one expires.


Agents

ql.agents.list() → Promise<AgentListResult>

Scope: agents

Lists all 8 available agents with their capability counts.

Response (200):

{
  "agents": [
    {
      "id": "governing",
      "name": "Governing Agent",
      "domain": "orchestration",
      "description": "Coordinates all other agents...",
      "icon": "brain",
      "tags": ["coordination", "reasoning"],
      "capabilityCount": 20
    }
  ]
}

ql.agents.get(agentId) → Promise<AgentDetail>

Scope: agents

Returns an agent's details and full capability list.

Parameters:

  • agentId: string — agent slug (e.g. "finance", "governing") or ObjectId

Response (200):

{
  "agent": {
    "id": "finance",
    "name": "Finance Agent",
    "domain": "finance",
    "description": "Handles financial analysis...",
    "icon": "dollar-sign",
    "tags": ["accounting", "budgeting"]
  },
  "capabilities": [
    {
      "id": "invoice",
      "name": "Invoice Generator",
      "description": "Generate formatted invoices from data",
      "category": "documents",
      "params": [
        { "key": "data", "label": "Data", "kind": "textarea", "required": true }
      ],
      "templates": [
        { "label": "Monthly Report", "values": { "data": "Revenue | 12000\nCosts | 4000" } }
      ]
    }
  ]
}

Errors:

  • 404 not_found — agent slug doesn't exist

ql.agents.capabilities(agentId) → Promise<CapabilitiesResult>

Scope: capabilities

Returns just the capability list for an agent (lighter than get).

Response (200):

{
  "agentId": "finance",
  "capabilities": [ ... ]
}

ql.agents.runCapability(agentId, input) → Promise<RunCapabilityResult>

Scope: capabilities

Executes a specific capability (feature) on an agent.

Parameters:

  • agentId: string — agent slug
  • input.capId: string — capability ID (from the capabilities list)
  • input.params?: Record<string, unknown> — capability-specific parameters

Response (200):

{
  "result": {
    "type": "html",
    "html": "<div class=\"invoice\">...</div>"
  }
}

The result is a discriminated union — check result.type:

| type | Fields | When | |--------|--------|------| | "text" | content: string | Plain text output | | "json" | data: unknown | Structured JSON | | "html" | html: string | HTML content (renderable) | | "file" | url, filename, mime | Generated file (image, PDF, etc.) |

Example:

const { result } = await ql.agents.runCapability("finance", {
  capId: "invoice",
  params: { data: "Revenue | 12000\nCosts | 4000" },
});

if (result.type === "html") {
  document.body.innerHTML = result.html;
} else if (result.type === "json") {
  console.log(result.data);
} else if (result.type === "text") {
  console.log(result.content);
} else if (result.type === "file") {
  window.open(result.url); // download
}

Chat

ql.agents.chat(input) → Promise<ChatResult>

Scope: chat

Non-streaming chat with an agent. Waits for the full response.

Parameters:

  • agentId: string — agent slug
  • messages: ChatMessage[] — conversation history (newest last)
  • userId?: string — optional internal user ID (defaults to workspace)
  • response_format?: object — optional OpenRouter format hint

Response (200):

{
  "content": "Based on your data, Q3 revenue was $120K...",
  "toolCalls": 2,
  "iterations": 3,
  "duration": "4.21s"
}

| Field | Type | Description | |-------|------|-------------| | content | string | The agent's full text response | | toolCalls | number | How many tool calls were made during reasoning | | iterations | number | Tool loop iterations | | duration | string | Human-readable execution time |

Example:

const reply = await ql.agents.chat({
  agentId: "finance",
  messages: [
    { role: "user", content: "What was our profit margin last month?" },
  ],
});
console.log(reply.content);

ql.agents.chatStream(input, opts?) → AsyncIterable<ChatStreamEvent>

Scope: chat

Streaming chat — yields incremental events as the agent responds.

Parameters:

  • Same as chat() above
  • opts.signal?: AbortSignal — abort the stream at any time

Yielded events:

type ChatStreamEvent =
  | { type: "delta"; content?: string; reasoning?: string }  // incremental piece
  | { type: "done"; content: string; reasoning?: string };   // final accumulated text

| Event | When | Fields | |-------|------|--------| | delta | For each token/chunk from the model | content (text delta), reasoning (thinking delta, if model supports it) | | done | Once at the end | content (full accumulated text), reasoning (full reasoning, if any) |

Example:

import { QLClient } from "ql-agent-sdk";

const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
const controller = new AbortController();

// Optional: auto-abort after 30s
const timer = setTimeout(() => controller.abort(), 30_000);

let full = "";
try {
  for await (const ev of ql.agents.chatStream(
    { agentId: "governing", messages: [{ role: "user", content: "Explain quantum computing" }] },
    { signal: controller.signal },
  )) {
    if (ev.type === "delta") {
      process.stdout.write(ev.content ?? ""); // print incrementally
      full += ev.content ?? "";
    } else if (ev.type === "done") {
      full = ev.content; // final text (replaces accumulated)
    }
  }
} catch (err) {
  if ((err as any)?.code === "aborted") console.log("\n[stopped by user]");
} finally {
  clearTimeout(timer);
}

SSE wire format (if you ever need to parse it manually):

data: {"id":"gen-...","choices":[{"delta":{"content":"Hel"}}]}

data: {"id":"gen-...","choices":[{"delta":{"content":"lo"}}]}

data: [DONE]

Events are separated by blank lines. data: [DONE] signals the end.


History / Messages

ql.agents.history(agentId, opts?) → Promise<HistoryPage>

Scope: messages

Returns paginated conversation history for an agent (newest first).

Parameters:

  • agentId: string — agent slug
  • opts.limit?: number — page size, 1–100 (default 20)
  • opts.before?: string — cursor from previous page's nextBefore
  • opts.userId?: string — optional filter by user

Response (200):

{
  "agentId": "finance",
  "limit": 20,
  "nextBefore": "6ae0d96274f1cf895ef06e0c",
  "items": [
    {
      "id": "6ae0d963...",
      "role": "user",
      "content": "What was our profit margin?",
      "toolCalls": [],
      "status": "completed",
      "timestamp": "2026-10-27T14:32:00.000Z"
    },
    {
      "id": "6ae0d962...",
      "role": "assistant",
      "content": "Based on your records...",
      "reasoning": "Let me check the financial data...",
      "toolCalls": [{ "name": "db_query", ... }],
      "status": "completed",
      "timestamp": "2026-10-27T14:32:05.000Z"
    }
  ]
}

| Field | Description | |-------|-------------| | nextBefore | Cursor for next page. null when no more pages. | | items[].role | "user" or "assistant" | | items[].content | Message text (may be null for tool-only messages) | | items[].reasoning | Model's thinking (if supported by the model) | | items[].toolCalls | Array of tool invocations made during this message | | items[].status | "completed", "processing", or "failed" |

Walking all pages:

const allMessages = [];
let before: string | undefined;

do {
  const page = await ql.agents.history("finance", { limit: 50, before });
  allMessages.push(...page.items);
  before = page.nextBefore ?? undefined; // null when exhausted
} while (before);

console.log(`${allMessages.length} total messages`);

ql.agents.listMessages(params) → Promise<AgentMessagesPage>

Scope: messages

Returns the chronological message feed for a conversation — chat turns plus workflow artifacts (tool results, step progress, approval requests). Use this to poll live progress of a background run: call it without after for the latest N, then page forward with after.

Parameters:

  • params.agentId: string — agent slug or ObjectId (required)
  • params.after?: string — cursor = id of the last item you received (forward pagination). Omit for the latest N.
  • params.limit?: number — page size, 1–50 (default 20)
  • params.userId?: string — optional filter by user (defaults to the workspace id)

Response (200):

{
  "items": [
    {
      "id": "6ae0d963...",
      "role": "user",
      "kind": "chat",
      "content": "Import this CSV into users",
      "status": "completed",
      "timestamp": "2026-11-05T10:00:00.000Z"
    },
    {
      "id": "6ae0d964...",
      "role": "assistant",
      "kind": "workflow",
      "content": null,
      "taskId": "6ae0d965...",
      "toolCalls": [{ "name": "fetch_schema", "status": "completed", "result": { "collections": ["users"] } }],
      "status": "completed",
      "timestamp": "2026-11-05T10:00:02.000Z"
    },
    {
      "id": "6ae0d966...",
      "role": "assistant",
      "kind": "approval_request",
      "content": "Delete 3 rows from `users`? ...",
      "outputFormat": "json",
      "status": "completed",
      "timestamp": "2026-11-05T10:00:05.000Z"
    }
  ],
  "hasMore": true,
  "nextAfter": "6ae0d966..."
}

| Field | Description | |-------|-------------| | items[].kind | "chat" (default), "tool_result", "workflow", or "approval_request" | | items[].content | Message text (may be null for artifact-only entries) | | items[].outputFormat | Declared format of content: "text", "json", or "html" | | items[].toolCalls | Tool invocations; each may carry a truncated result once complete | | items[].taskId | Task id for entries produced by a background workflow run | | hasMore / nextAfter | Forward pagination: pass nextAfter as the next after |

Polling a live run (e.g. every 4s):

let after: string | undefined;
const timer = setInterval(async () => {
  const page = await ql.agents.listMessages({ agentId: "operations", limit: 20, after });
  for (const item of page.items) renderFeedItem(item); // tool chips, step progress, approvals…
  after = page.nextAfter;
}, 4000);

Workflows

ql.workflows.run(workflowId, input?) → Promise<WorkflowRunStart>

Scope: workflows

Triggers a workflow execution. Returns immediately with a run ID (HTTP 202).

Parameters:

  • workflowId: string — the workflow's ObjectId
  • input?: { input?: Record<string, unknown> } — optional input payload for the workflow

Response (202):

{
  "runId": "6ae101c8...",
  "status": "running"
}

Errors:

  • 400 validation_error — invalid workflow ID format
  • 404 not_found — workflow doesn't exist in this workspace

ql.workflows.getRun(runId) → Promise<WorkflowRun>

Scope: workflows

Polls a workflow run's status.

Response (200):

{
  "runId": "6ae101c8...",
  "status": "completed",
  "output": { "result": "..." },
  "error": null,
  "durationMs": 4523,
  "startedAt": "2026-10-27T14:32:00.000Z",
  "finishedAt": "2026-10-27T14:32:04.523Z"
}

| status value | Meaning | |----------------|---------| | "queued" | Waiting to start | | "running" | Currently executing | | "completed" | Finished successfully — check output | | "failed" | Execution error — check error | | "cancelled" | Was cancelled |

Polling example:

const start = await ql.workflows.run("6adf929d6f87c9ab86f32c19", { input: { a: 1 } });

// Poll until done
let run;
do {
  await new Promise((r) => setTimeout(r, 2000));
  run = await ql.workflows.getRun(start.runId);
} while (run.status === "running" || run.status === "queued");

if (run.status === "completed") console.log(run.output);
else if (run.status === "failed") console.error(run.error);

Tasks (Background)

ql.tasks.create(input) → Promise<TaskCreated>

Scope: tasks

Creates a background task. The task starts executing immediately on the server — you don't need to call another endpoint.

Parameters:

  • agentId: string — which agent handles this task
  • title: string — human-readable name
  • description?: string — optional longer description
  • input?: unknown — payload for the agent (string, or { message }, or { prompt })
  • priority?: "low" | "medium" | "high" | "urgent" — scheduling priority

Response (201):

{
  "taskId": "6ae101b7...",
  "status": "queued"
}

Errors:

  • 400 validation_error — missing agentId or title
  • 404 not_found — unknown agent slug

ql.tasks.get(taskId) → Promise<Task>

Scope: tasks

Single poll of a task's current state.

Response (200):

{
  "taskId": "6ae101b7...",
  "status": "completed",
  "title": "Monthly Report",
  "progress": {
    "current": 100,
    "total": 100,
    "message": "Completed",
    "updatedAt": "2026-10-27T19:53:23.117Z"
  },
  "result": "Here is your monthly report...",
  "output": "Here is your monthly report...",
  "error": null,
  "createdAt": "2026-10-27T19:53:16.756Z",
  "completedAt": "2026-10-27T19:53:23.117Z"
}

| status value | Meaning | Check field | |----------------|---------|-------------| | "queued" | Waiting to start | — | | "running" | In progress | progress.message for current step | | "retrying" | Failed, will retry automatically | error for what went wrong | | "completed" | Done successfully | result / output for the answer | | "failed" | Permanently failed (max retries) | error for the reason | | "cancelled" | Was cancelled | — |

ql.tasks.handle(taskId) → TaskHandle

Returns a handle with poll() and wait() methods.

handle.poll(signal?) — single fetch, returns current Task snapshot.

handle.wait(opts?) — polls until terminal status with backoff.

| Option | Default | Description | |--------|---------|-------------| | intervalMs | 2000 | Initial delay between polls (ms) | | maxIntervalMs | 15000 | Backoff cap (grows 1.5× per poll) | | timeoutMs | undefined (wait forever) | Throw after this many ms total | | signal | undefined | AbortSignal for cancellation | | onTick | undefined | Callback after each poll with latest snapshot |

Terminal states returned by wait():

  • task.status === "completed" → check task.result
  • task.status === "failed" → check task.error
  • task.status === "cancelled" → task was cancelled server-side

Non-terminal exits (throws QLError):

  • err.code === "timeout" → exceeded timeoutMs
  • err.code === "aborted" → signal was aborted

Example:

const created = await ql.tasks.create({
  agentId: "finance",
  title: "Quarterly Report",
  input: { message: "Generate a Q3 financial summary from our database." },
  priority: "high",
});

const handle = ql.tasks.handle(created.taskId);

try {
  const done = await handle.wait({
    intervalMs: 2000,
    maxIntervalMs: 10000,
    timeoutMs: 5 * 60_000, // 5 minute timeout
    onTick: (task) => {
      if (task.status === "running" && task.progress?.message) {
        process.stdout.write(`\r${task.progress.message}`);
      }
    },
  });

  if (done.status === "completed") {
    console.log("\n✓ Done:", done.result);
  } else if (done.status === "failed") {
    console.error("✗ Failed:", done.error);
  }
} catch (err) {
  if ((err as any).code === "timeout") console.error("Timed out after 5 min");
  if ((err as any).code === "aborted") console.log("Cancelled by user");
}

Note on execution: Tasks execute in the server's process. If the server restarts mid-execution, the task will be recovered and retried on next startup (up to maxRetries times). For mission-critical workloads with strict durability requirements, consider a persistent queue backend.


Videos (Tiered Generation)

Async video generation: submit a job, then poll until it completes. Reference images may be base64 data URLs or https URLs; reference (style) videos must be https URLs — upload local files with ql.files.upload first.

ql.videos.listModels() → Promise<VideoModelsResult>

Lists the tiered video models plus runtime engine availability: { models: VideoModelInfo[], engine: { available, detail } }. Each model reports its tier, supported aspect ratios/resolutions, duration range, reference-image/video limits, voiceover support, and pricing.

Example:

const { models, engine } = await ql.videos.listModels();
if (!engine.available) console.warn("Video engine unavailable:", engine.detail);
const recommended = models.find((m) => m.recommended && m.available);

ql.videos.generate(agentId, input) → Promise<VideoTaskCreated>

Starts a video job (HTTP 202) and returns { taskId, status: "queued" }.

| Field | Description | |-------|-------------| | prompt | The creative brief / script for the video | | model? | Explicit registry model id (wins over tier) | | tier? | Tier to resolve a model from when model is omitted | | images_list? | Product/reference images — base64 data URLs or https URLs (order defines IMG_0..N) | | video_files? | Reference (style) videos — https URLs only | | aspect_ratio? | e.g. "16:9", "9:16", "1:1" (default "16:9") | | duration? | Seconds, within the model's durationRange | | resolution? | e.g. "720p" / "1080p" (default "720p") | | generate_audio? | Bake a voiceover into the render (requires narration) | | narration? | Voiceover text (max 3000 chars) |

Example:

const created = await ql.videos.generate("marketing", {
  prompt: "A 10-second product reel for the new sneaker drop, studio lighting.",
  model: "motion/reel",
  images_list: ["data:image/png;base64,..."], // or https URLs
  video_files: ["https://cdn.example.com/style-ref.mp4"],
  aspect_ratio: "9:16",
  duration: 10,
  resolution: "1080p",
  generate_audio: true,
  narration: "Introducing the all-new AirFlex.",
});

ql.videos.getTask(taskId, signal?) → Promise<VideoTask>

Fetches the current job snapshot: { taskId, status, progress?, outputUrl?, mime?, ext?, durationSec?, error?, model?, tier?, latencyMs?, createdAt, completedAt }.

ql.videos.cancelTask(taskId) → Promise<VideoTask>

Cancels a queued/rendering job and returns the updated snapshot. Throws a QLError with code "conflict" (HTTP 409) if the job is already terminal.

ql.videos.handle(taskId) → VideoTaskHandle

Same poll() / wait(opts?) pattern as TaskHandle, polling /api/v1/video-tasks/:taskId. Terminal statuses: completed (check outputUrl), failed (check error), cancelled. Wait options and non-terminal exits (timeout / aborted) are identical to tasks.

Example:

const done = await ql.videos.handle(created.taskId).wait({
  intervalMs: 2000,
  timeoutMs: 10 * 60_000,
  onTick: (job) => {
    if (job.progress?.message) process.stdout.write(`\r${job.progress.message}`);
  },
});
if (done.status === "completed" && done.outputUrl) {
  console.log("Video ready:", `${ql.baseUrl}${done.outputUrl}`);
}

Audio (Text-to-Speech)

ql.audio.voices() → Promise<VoicesResult>

Lists available TTS voices: { voices: { id, name }[], source: "live" | "none", note? }. source === "none" means no provider key is configured server-side.

ql.audio.textToSpeech(agentId, input) → Promise<TtsResult>

Synchronous synthesis (no polling). Returns { url, voiceId, modelId, bytes, latencyMs } where url is a served media path (e.g. /generated/tts-….mp3).

| Field | Description | |-------|-------------| | text | Text to synthesize (max 3000 chars) | | voiceId? | Voice id from ql.audio.voices() (defaults to the platform voice) | | stability? | Voice stability 0..1 (default 0.5) | | similarityBoost? | Similarity boost 0..1 (default 0.75) |

Example:

const { voices } = await ql.audio.voices();
const result = await ql.audio.textToSpeech("marketing", {
  text: "Say hello to the all-new AirFlex.",
  voiceId: voices[0]?.id,
});
console.log(`${ql.baseUrl}${result.url}`);

Files (Media Upload)

ql.files.upload(file, opts?) → Promise<UploadedFile>

Uploads a media file as multipart form data (field "file") and returns { url, filename, size, contentType }. The returned url is a stable served path (e.g. /uploads/<workspace>/<file>) that can be used directly as a media input URL — e.g. in videos.generate({ video_files: [res.url] }).

| Option | Description | |--------|-------------| | filename? | Override the stored display name (extension is kept from the file) | | onProgress? | Coarse progress callback — fetch does not expose byte progress, so it reports 0 at start and 100 on completion only |

Example:

const blob = await file.arrayBuffer(); // or any Blob source
const uploaded = await ql.files.upload(new Blob([blob], { type: "video/mp4" }), {
  filename: "style-ref.mp4",
});

Error Handling

All API failures throw a typed QLError:

import { QLClient, QLError } from "ql-agent-sdk";

try {
  await ql.agents.chat({ agentId: "finance", messages: [...] });
} catch (err) {
  if (err instanceof QLError) {
    console.error(err.status);   // HTTP status code (401, 403, 404, etc.)
    console.error(err.code);     // stable string code (see table below)
    console.error(err.message);  // human-readable (never includes API key)

    // Convenience booleans:
    err.isAuthError;      // true for 401/403
    err.isAborted;        // true for abort
    err.isTimeout;        // true for wait() timeout
    err.isNetworkError;   // true for connection failures
  }
}

Error Code Reference

| HTTP Status | code | When it happens | |-------------|--------|-----------------| | 401 | unauthorized | Missing, invalid, or revoked API key | | 403 | forbidden | Key lacks the required scope for this endpoint | | 404 | not_found | Resource doesn't exist in your workspace (agent, workflow, task) | | 409 | conflict | Conflict (e.g. duplicate resource) | | 400 | validation_error | Malformed request body, missing required fields, invalid params | | 500 | internal_error | Server-side error (AI provider failure, DB issue, etc.) | | — | network | No HTTP response received (connection refused, DNS failure, timeout) | | — | aborted | Caller aborted via AbortSignal | | — | timeout | wait() exceeded its timeoutMs budget |

Common Error Scenarios

// 1. Expired/revoked key
try {
  await ql.workspace.get();
} catch (e) {
  if (e instanceof QLError && e.code === "unauthorized") {
    console.log("API key is invalid or revoked. Generate a new one in your dashboard.");
  }
}

// 2. Key doesn't have chat scope
try {
  await ql.agents.chat({ agentId: "finance", messages: [...] });
} catch (e) {
  if (e instanceof QLError && e.code === "forbidden") {
    console.log("This key needs the 'chat' scope. Create a new key with broader scopes.");
  }
}

// 3. AI provider error (transient)
try {
  await ql.agents.chat({ agentId: "governing", messages: [...] });
} catch (e) {
  if (e instanceof QLError && e.status === 500) {
    console.log("AI service temporarily unavailable. Retry later.");
  }
}

TypeScript

Full type declarations are included. All public types are exported:

import type {
  QLClient,
  QLClientOptions,
  QLError,
  // Request types
  ChatInput,
  RunCapabilityInput,
  TaskCreateInput,
  WaitOptions,
  HistoryParams,
  WorkflowRunInput,
  // Response types
  Workspace,
  AgentSummary,
  AgentDetail,
  CapabilityInfo,
  CapabilityResult,
  ChatResult,
  ChatStreamEvent,
  HistoryPage,
  MessageItem,
  WorkflowRun,
  Task,
  TaskCreated,
  // Constants
  API_ERROR_CODES,
} from "ql-agent-sdk";

Using in Next.js App Router

The SDK is safe to use server-side in Route Handlers or Server Components:

// app/api/agent/route.ts
import { QLClient, QLError } from "ql-agent-sdk";

export const runtime = "nodejs"; // uses native fetch + Web Streams

const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });

export async function POST(req: Request) {
  const { message } = await req.json();

  try {
    const reply = await ql.agents.chat({
      agentId: "governing",
      messages: [{ role: "user", content: message }],
    });
    return Response.json({ text: reply.content });
  } catch (err) {
    if (err instanceof QLError) {
      return Response.json({ error: err.message }, { status: err.status ?? 500 });
    }
    throw err;
  }
}

For streaming in a Route Handler:

export async function POST(req: Request) {
  const { message } = await req.json();
  const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });

  const stream = new ReadableStream({
    async start(controller) {
      try {
        for await (const ev of ql.agents.chatStream({
          agentId: "governing",
          messages: [{ role: "user", content: message }],
        })) {
          if (ev.type === "delta" && ev.content) {
            controller.enqueue(new TextEncoder().encode(ev.content));
          }
        }
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, { headers: { "Content-Type": "text/plain" } });
}

Agent Reference

The platform includes 8 specialized agents. Each has 20 capabilities in its domain:

| ID | Domain | Examples | |----|--------|----------| | governing | Orchestration | Coordinates other agents, complex multi-step reasoning | | finance | Finance | Invoices, P&L, budgeting, cash flow, tax estimates | | marketing | Marketing | Social media content, ad copy, SEO, campaign planning | | inventory | Supply Chain | Stock levels, reorder points, supplier management | | customer | Customer Success | Support drafts, onboarding sequences, churn analysis | | developer | Engineering | Code generation, API docs, bug triage, test writing | | operations | Operations | SOPs, process mapping, KPI dashboards, compliance | | research | Research | Market analysis, competitor intel, literature reviews |

Discover available capabilities at runtime:

const detail = await ql.agents.get("finance");
console.log(detail.capabilities.map((c) => c.id));

Environment Variables

| Variable | Purpose | |----------|---------| | QL_API_KEY | Your API key (convention — the SDK reads it from the apiKey option, not env directly) | | QL_BASE_URL | Override the base URL (useful for staging/self-hosted) |


Troubleshooting

| Symptom | Cause | Fix | |---------|-------|-----| | QLError: unauthorized | Key is invalid, revoked, or expired | Generate a new key in your dashboard | | QLError: forbidden | Key scope doesn't cover the endpoint | Create a key with the required scope or * | | QLError: not_found (agent) | Typo in agent slug | Use ql.agents.list() to see valid IDs | | QLError: network | Can't reach the API | Check your network / base URL / firewall | | Task stuck in "running" | Server restarted mid-execution | Wait for stale recovery (≤5 min) or poll again later | | Stream produces no output | Model returned empty response | Check reasoning field or try a different agent |


License

MIT