ql-agent-sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # ql-agent-sdk
2
2
 
3
- TypeScript SDK for the **Quanton Labs** agent platform API (v1). Integrates your application with the 8 specialized agents, capabilities, workflows, and background tasks. Zero runtime dependencies — built on native `fetch` and Web Streams.
3
+ TypeScript SDK for the **Quanton Labs** agent platform API (v1). Integrate your application with 8 specialized AI agents, capabilities, workflows, and background tasks.
4
4
 
5
- > Base URL defaults to `https://app.quantonlabs.com`. Override with `baseUrl` or the `QL_BASE_URL` env var.
5
+ - **Zero runtime dependencies** — built on native `fetch` and Web Streams
6
+ - **Full TypeScript** — all request/response types exported
7
+ - **ESM + CJS** — works in Node, Bun, Deno, Next.js, and edge runtimes
8
+
9
+ > Base URL defaults to `https://app.quantonlabs.com`. Override with `baseUrl` option or the `QL_BASE_URL` environment variable.
10
+
11
+ ---
6
12
 
7
13
  ## Install
8
14
 
@@ -10,20 +16,25 @@ TypeScript SDK for the **Quanton Labs** agent platform API (v1). Integrates your
10
16
  npm install ql-agent-sdk
11
17
  # or
12
18
  pnpm add ql-agent-sdk
19
+ # or
20
+ yarn add ql-agent-sdk
13
21
  ```
14
22
 
15
- Requires **Node.js 18+** (native `fetch`). Works in Node, Bun, Deno, and browser/edge runtimes that provide `fetch` and Web Streams.
23
+ Requires **Node.js 18+** (native `fetch`).
16
24
 
17
- ## Quick start
25
+ ---
26
+
27
+ ## Quick Start
18
28
 
19
29
  ```ts
20
30
  import { QLClient } from "ql-agent-sdk";
21
31
 
22
32
  const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
23
33
 
24
- // Discover agents
34
+ // Discover available agents
25
35
  const { agents } = await ql.agents.list();
26
36
  console.log(agents.map((a) => a.id));
37
+ // → ["governing", "finance", "marketing", "inventory", "customer", "developer", "operations", "research"]
27
38
 
28
39
  // Chat with an agent (non-streaming)
29
40
  const reply = await ql.agents.chat({
@@ -33,163 +44,705 @@ const reply = await ql.agents.chat({
33
44
  console.log(reply.content);
34
45
  ```
35
46
 
47
+ ---
48
+
36
49
  ## Configuration
37
50
 
38
51
  ```ts
39
- new QLClient({
40
- apiKey, // required — your Bearer API key (never logged/exposed)
41
- baseUrl, // optional — defaults to QL_BASE_URL or https://app.quantonlabs.com
42
- fetch: customFetch, // optional — override for tests/proxies/custom runtimes
52
+ const ql = new QLClient({
53
+ apiKey, // required — your Bearer API key (ql_live_...)
54
+ baseUrl, // optional — defaults to QL_BASE_URL or https://app.quantonlabs.com
55
+ fetch: customFetch, // optional — override for tests/proxies/custom runtimes
43
56
  });
44
57
  ```
45
58
 
46
- `apiKey` is held in a private field and is never returned by the client, included in errors, or logged.
59
+ The `apiKey` is held in a private field. It is **never** returned by the client, included in error messages, or logged.
60
+
61
+ ---
62
+
63
+ ## Authentication
64
+
65
+ All API calls use **Bearer token authentication** with your API key:
66
+
67
+ ```
68
+ Authorization: Bearer ql_live_your_key_here
69
+ ```
70
+
71
+ Create keys in your Quanton Labs dashboard → **API Keys** page. Each key is scoped to a single workspace.
72
+
73
+ ### Scope Requirements
74
+
75
+ | Scope | Grants access to |
76
+ |-------|-----------------|
77
+ | `workspace` | `ql.workspace.get()` |
78
+ | `agents` | `ql.agents.list()`, `ql.agents.get()` |
79
+ | `capabilities` | `ql.agents.capabilities()`, `ql.agents.runCapability()` |
80
+ | `chat` | `ql.agents.chat()`, `ql.agents.chatStream()` |
81
+ | `messages` | `ql.agents.history()` |
82
+ | `workflows` | `ql.workflows.run()`, `ql.workflows.getRun()` |
83
+ | `tasks` | `ql.tasks.create()`, `ql.tasks.get()` |
84
+ | `*` | All of the above (default for new keys) |
85
+
86
+ ---
87
+
88
+ ## API Reference
89
+
90
+ ### Response Envelope
91
+
92
+ All non-streaming endpoints return a consistent envelope:
93
+
94
+ **Success:**
95
+ ```json
96
+ { "ok": true, "data": { ... } }
97
+ ```
98
+
99
+ **Error:**
100
+ ```json
101
+ { "ok": false, "error": { "code": "not_found", "message": "Agent not found: foo" } }
102
+ ```
103
+
104
+ The SDK unwraps `data` for you and throws a typed `QLError` on errors. You never see the envelope directly.
47
105
 
48
- ## API reference
106
+ ---
49
107
 
50
108
  ### Workspace
109
+
110
+ #### `ql.workspace.get()` → `Promise<Workspace>`
111
+
112
+ **Scope:** `workspace`
113
+
114
+ Returns the workspace bound to your API key.
115
+
116
+ **Response (200):**
117
+ ```json
118
+ {
119
+ "_id": "6adf926f6f87c9ab86f32c0e",
120
+ "name": "Robin",
121
+ "plan": "standard",
122
+ "createdAt": "2026-10-26T17:48:31.038Z",
123
+ "updatedAt": "2026-10-27T11:56:44.074Z"
124
+ }
125
+ ```
126
+
127
+ **TypeScript:**
51
128
  ```ts
52
- const ws = await ql.workspace.get(); // -> { _id, name, plan, createdAt, updatedAt }
129
+ const ws = await ql.workspace.get();
130
+ ws._id; // string
131
+ ws.name; // string
132
+ ws.plan; // "standard" | "premium" | "enterprise" | undefined
133
+ ws.createdAt; // string (ISO 8601)
53
134
  ```
54
135
 
136
+ ---
137
+
55
138
  ### Agents
56
- ```ts
57
- const { agents } = await ql.agents.list(); // AgentListResult
58
- const detail = await ql.agents.get("finance"); // AgentDetail (agent + capabilities)
59
- const caps = await ql.agents.capabilities("finance"); // CapabilitiesResult
60
139
 
61
- // Run a capability ("feature"). The result is a discriminated union:
140
+ #### `ql.agents.list()` → `Promise<AgentListResult>`
141
+
142
+ **Scope:** `agents`
143
+
144
+ Lists all 8 available agents with their capability counts.
145
+
146
+ **Response (200):**
147
+ ```json
148
+ {
149
+ "agents": [
150
+ {
151
+ "id": "governing",
152
+ "name": "Governing Agent",
153
+ "domain": "orchestration",
154
+ "description": "Coordinates all other agents...",
155
+ "icon": "brain",
156
+ "tags": ["coordination", "reasoning"],
157
+ "capabilityCount": 20
158
+ }
159
+ ]
160
+ }
161
+ ```
162
+
163
+ #### `ql.agents.get(agentId)` → `Promise<AgentDetail>`
164
+
165
+ **Scope:** `agents`
166
+
167
+ Returns an agent's details and full capability list.
168
+
169
+ **Parameters:**
170
+ - `agentId: string` — agent slug (e.g. `"finance"`, `"governing"`) or ObjectId
171
+
172
+ **Response (200):**
173
+ ```json
174
+ {
175
+ "agent": {
176
+ "id": "finance",
177
+ "name": "Finance Agent",
178
+ "domain": "finance",
179
+ "description": "Handles financial analysis...",
180
+ "icon": "dollar-sign",
181
+ "tags": ["accounting", "budgeting"]
182
+ },
183
+ "capabilities": [
184
+ {
185
+ "id": "invoice",
186
+ "name": "Invoice Generator",
187
+ "description": "Generate formatted invoices from data",
188
+ "category": "documents",
189
+ "params": [
190
+ { "key": "data", "label": "Data", "kind": "textarea", "required": true }
191
+ ],
192
+ "templates": [
193
+ { "label": "Monthly Report", "values": { "data": "Revenue | 12000\nCosts | 4000" } }
194
+ ]
195
+ }
196
+ ]
197
+ }
198
+ ```
199
+
200
+ **Errors:**
201
+ - `404 not_found` — agent slug doesn't exist
202
+
203
+ #### `ql.agents.capabilities(agentId)` → `Promise<CapabilitiesResult>`
204
+
205
+ **Scope:** `capabilities`
206
+
207
+ Returns just the capability list for an agent (lighter than `get`).
208
+
209
+ **Response (200):**
210
+ ```json
211
+ {
212
+ "agentId": "finance",
213
+ "capabilities": [ ... ]
214
+ }
215
+ ```
216
+
217
+ #### `ql.agents.runCapability(agentId, input)` → `Promise<RunCapabilityResult>`
218
+
219
+ **Scope:** `capabilities`
220
+
221
+ Executes a specific capability (feature) on an agent.
222
+
223
+ **Parameters:**
224
+ - `agentId: string` — agent slug
225
+ - `input.capId: string` — capability ID (from the capabilities list)
226
+ - `input.params?: Record<string, unknown>` — capability-specific parameters
227
+
228
+ **Response (200):**
229
+ ```json
230
+ {
231
+ "result": {
232
+ "type": "html",
233
+ "html": "<div class=\"invoice\">...</div>"
234
+ }
235
+ }
236
+ ```
237
+
238
+ The `result` is a **discriminated union** — check `result.type`:
239
+
240
+ | `type` | Fields | When |
241
+ |--------|--------|------|
242
+ | `"text"` | `content: string` | Plain text output |
243
+ | `"json"` | `data: unknown` | Structured JSON |
244
+ | `"html"` | `html: string` | HTML content (renderable) |
245
+ | `"file"` | `url, filename, mime` | Generated file (image, PDF, etc.) |
246
+
247
+ **Example:**
248
+ ```ts
62
249
  const { result } = await ql.agents.runCapability("finance", {
63
250
  capId: "invoice",
64
251
  params: { data: "Revenue | 12000\nCosts | 4000" },
65
252
  });
66
- if (result.type === "html") render(result.html);
67
- else if (result.type === "json") use(result.data);
68
- else if (result.type === "text") print(result.content);
69
- // result.type === "file" -> { url, filename, mime }
70
253
 
71
- // Chat (non-streaming)
72
- const out = await ql.agents.chat({ agentId: "marketing", messages: [...] }); // ChatResult
254
+ if (result.type === "html") {
255
+ document.body.innerHTML = result.html;
256
+ } else if (result.type === "json") {
257
+ console.log(result.data);
258
+ } else if (result.type === "text") {
259
+ console.log(result.content);
260
+ } else if (result.type === "file") {
261
+ window.open(result.url); // download
262
+ }
73
263
  ```
74
264
 
75
- ### Streaming chat (SSE)
76
- `agents.chatStream` returns an **async iterable** of events. Stop it with an `AbortController`.
265
+ ---
266
+
267
+ ### Chat
77
268
 
269
+ #### `ql.agents.chat(input)` → `Promise<ChatResult>`
270
+
271
+ **Scope:** `chat`
272
+
273
+ Non-streaming chat with an agent. Waits for the full response.
274
+
275
+ **Parameters:**
276
+ - `agentId: string` — agent slug
277
+ - `messages: ChatMessage[]` — conversation history (newest last)
278
+ - `userId?: string` — optional internal user ID (defaults to workspace)
279
+ - `response_format?: object` — optional OpenRouter format hint
280
+
281
+ **Response (200):**
282
+ ```json
283
+ {
284
+ "content": "Based on your data, Q3 revenue was $120K...",
285
+ "toolCalls": 2,
286
+ "iterations": 3,
287
+ "duration": "4.21s"
288
+ }
289
+ ```
290
+
291
+ | Field | Type | Description |
292
+ |-------|------|-------------|
293
+ | `content` | `string` | The agent's full text response |
294
+ | `toolCalls` | `number` | How many tool calls were made during reasoning |
295
+ | `iterations` | `number` | Tool loop iterations |
296
+ | `duration` | `string` | Human-readable execution time |
297
+
298
+ **Example:**
299
+ ```ts
300
+ const reply = await ql.agents.chat({
301
+ agentId: "finance",
302
+ messages: [
303
+ { role: "user", content: "What was our profit margin last month?" },
304
+ ],
305
+ });
306
+ console.log(reply.content);
307
+ ```
308
+
309
+ ---
310
+
311
+ #### `ql.agents.chatStream(input, opts?)` → `AsyncIterable<ChatStreamEvent>`
312
+
313
+ **Scope:** `chat`
314
+
315
+ Streaming chat — yields incremental events as the agent responds.
316
+
317
+ **Parameters:**
318
+ - Same as `chat()` above
319
+ - `opts.signal?: AbortSignal` — abort the stream at any time
320
+
321
+ **Yielded events:**
322
+ ```ts
323
+ type ChatStreamEvent =
324
+ | { type: "delta"; content?: string; reasoning?: string } // incremental piece
325
+ | { type: "done"; content: string; reasoning?: string }; // final accumulated text
326
+ ```
327
+
328
+ | Event | When | Fields |
329
+ |-------|------|--------|
330
+ | `delta` | For each token/chunk from the model | `content` (text delta), `reasoning` (thinking delta, if model supports it) |
331
+ | `done` | Once at the end | `content` (full accumulated text), `reasoning` (full reasoning, if any) |
332
+
333
+ **Example:**
78
334
  ```ts
79
335
  import { QLClient } from "ql-agent-sdk";
80
336
 
81
337
  const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
82
338
  const controller = new AbortController();
83
339
 
84
- // e.g. abort after 30s or on user action
340
+ // Optional: auto-abort after 30s
85
341
  const timer = setTimeout(() => controller.abort(), 30_000);
86
342
 
87
343
  let full = "";
88
344
  try {
89
345
  for await (const ev of ql.agents.chatStream(
90
- { agentId: "governing", messages: [{ role: "user", content: "Hello" }] },
346
+ { agentId: "governing", messages: [{ role: "user", content: "Explain quantum computing" }] },
91
347
  { signal: controller.signal },
92
348
  )) {
93
349
  if (ev.type === "delta") {
94
- process.stdout.write(ev.content ?? ""); // incremental text
350
+ process.stdout.write(ev.content ?? ""); // print incrementally
95
351
  full += ev.content ?? "";
96
352
  } else if (ev.type === "done") {
97
- full = ev.content; // final accumulated text
353
+ full = ev.content; // final text (replaces accumulated)
98
354
  }
99
355
  }
100
356
  } catch (err) {
101
- if ((err as any)?.code === "aborted") console.log("stopped by user");
357
+ if ((err as any)?.code === "aborted") console.log("\n[stopped by user]");
102
358
  } finally {
103
359
  clearTimeout(timer);
104
360
  }
105
361
  ```
106
362
 
107
- The stream is robust to chunk boundaries, CRLF/LF endings, multiple events per chunk, a final event without a trailing newline, `[DONE]`, and malformed JSON (skipped).
363
+ **SSE wire format** (if you ever need to parse it manually):
364
+ ```
365
+ data: {"id":"gen-...","choices":[{"delta":{"content":"Hel"}}]}
366
+
367
+ data: {"id":"gen-...","choices":[{"delta":{"content":"lo"}}]}
368
+
369
+ data: [DONE]
370
+
371
+ ```
372
+ Events are separated by blank lines. `data: [DONE]` signals the end.
373
+
374
+ ---
375
+
376
+ ### History / Messages
377
+
378
+ #### `ql.agents.history(agentId, opts?)` → `Promise<HistoryPage>`
379
+
380
+ **Scope:** `messages`
381
+
382
+ Returns paginated conversation history for an agent (newest first).
383
+
384
+ **Parameters:**
385
+ - `agentId: string` — agent slug
386
+ - `opts.limit?: number` — page size, 1–100 (default 20)
387
+ - `opts.before?: string` — cursor from previous page's `nextBefore`
388
+ - `opts.userId?: string` — optional filter by user
389
+
390
+ **Response (200):**
391
+ ```json
392
+ {
393
+ "agentId": "finance",
394
+ "limit": 20,
395
+ "nextBefore": "6ae0d96274f1cf895ef06e0c",
396
+ "items": [
397
+ {
398
+ "id": "6ae0d963...",
399
+ "role": "user",
400
+ "content": "What was our profit margin?",
401
+ "toolCalls": [],
402
+ "status": "completed",
403
+ "timestamp": "2026-10-27T14:32:00.000Z"
404
+ },
405
+ {
406
+ "id": "6ae0d962...",
407
+ "role": "assistant",
408
+ "content": "Based on your records...",
409
+ "reasoning": "Let me check the financial data...",
410
+ "toolCalls": [{ "name": "db_query", ... }],
411
+ "status": "completed",
412
+ "timestamp": "2026-10-27T14:32:05.000Z"
413
+ }
414
+ ]
415
+ }
416
+ ```
108
417
 
109
- ### History / pagination
110
- `agents.history` returns newest-first pages. Walk them with the `nextBefore` cursor until it is `null`.
418
+ | Field | Description |
419
+ |-------|-------------|
420
+ | `nextBefore` | Cursor for next page. `null` when no more pages. |
421
+ | `items[].role` | `"user"` or `"assistant"` |
422
+ | `items[].content` | Message text (may be null for tool-only messages) |
423
+ | `items[].reasoning` | Model's thinking (if supported by the model) |
424
+ | `items[].toolCalls` | Array of tool invocations made during this message |
425
+ | `items[].status` | `"completed"`, `"processing"`, or `"failed"` |
111
426
 
427
+ **Walking all pages:**
112
428
  ```ts
113
- const ids: string[] = [];
429
+ const allMessages = [];
114
430
  let before: string | undefined;
431
+
115
432
  do {
116
433
  const page = await ql.agents.history("finance", { limit: 50, before });
117
- for (const m of page.items) ids.push(m.id);
118
- before = page.nextBefore ?? undefined; // null when exhausted
434
+ allMessages.push(...page.items);
435
+ before = page.nextBefore ?? undefined; // null when exhausted
119
436
  } while (before);
437
+
438
+ console.log(`${allMessages.length} total messages`);
120
439
  ```
121
440
 
441
+ ---
442
+
122
443
  ### Workflows
123
- ```ts
124
- const start = await ql.workflows.run("6adf929d6f87c9ab86f32c19", { input: { a: 1 } }); // 202 -> { runId, status: "running" }
125
- const run = await ql.workflows.getRun(start.runId); // poll -> WorkflowRun (status/output/error/durationMs/...)
444
+
445
+ #### `ql.workflows.run(workflowId, input?)` → `Promise<WorkflowRunStart>`
446
+
447
+ **Scope:** `workflows`
448
+
449
+ Triggers a workflow execution. Returns immediately with a run ID (HTTP 202).
450
+
451
+ **Parameters:**
452
+ - `workflowId: string` — the workflow's ObjectId
453
+ - `input?: { input?: Record<string, unknown> }` — optional input payload for the workflow
454
+
455
+ **Response (202):**
456
+ ```json
457
+ {
458
+ "runId": "6ae101c8...",
459
+ "status": "running"
460
+ }
126
461
  ```
127
462
 
128
- ### Tasks (background)
463
+ **Errors:**
464
+ - `400 validation_error` — invalid workflow ID format
465
+ - `404 not_found` — workflow doesn't exist in this workspace
466
+
467
+ #### `ql.workflows.getRun(runId)` → `Promise<WorkflowRun>`
468
+
469
+ **Scope:** `workflows`
470
+
471
+ Polls a workflow run's status.
472
+
473
+ **Response (200):**
474
+ ```json
475
+ {
476
+ "runId": "6ae101c8...",
477
+ "status": "completed",
478
+ "output": { "result": "..." },
479
+ "error": null,
480
+ "durationMs": 4523,
481
+ "startedAt": "2026-10-27T14:32:00.000Z",
482
+ "finishedAt": "2026-10-27T14:32:04.523Z"
483
+ }
484
+ ```
485
+
486
+ | `status` value | Meaning |
487
+ |----------------|---------|
488
+ | `"queued"` | Waiting to start |
489
+ | `"running"` | Currently executing |
490
+ | `"completed"` | Finished successfully — check `output` |
491
+ | `"failed"` | Execution error — check `error` |
492
+ | `"cancelled"` | Was cancelled |
493
+
494
+ **Polling example:**
129
495
  ```ts
130
- const created = await ql.tasks.create({ agentId: "finance", title: "Monthly report", priority: "high" }); // 201 -> { taskId, status }
131
- const handle = ql.tasks.handle(created.taskId);
496
+ const start = await ql.workflows.run("6adf929d6f87c9ab86f32c19", { input: { a: 1 } });
497
+
498
+ // Poll until done
499
+ let run;
500
+ do {
501
+ await new Promise((r) => setTimeout(r, 2000));
502
+ run = await ql.workflows.getRun(start.runId);
503
+ } while (run.status === "running" || run.status === "queued");
504
+
505
+ if (run.status === "completed") console.log(run.output);
506
+ else if (run.status === "failed") console.error(run.error);
507
+ ```
508
+
509
+ ---
510
+
511
+ ### Tasks (Background)
512
+
513
+ #### `ql.tasks.create(input)` → `Promise<TaskCreated>`
514
+
515
+ **Scope:** `tasks`
516
+
517
+ Creates a background task. The task **starts executing immediately** on the server — you don't need to call another endpoint.
518
+
519
+ **Parameters:**
520
+ - `agentId: string` — which agent handles this task
521
+ - `title: string` — human-readable name
522
+ - `description?: string` — optional longer description
523
+ - `input?: unknown` — payload for the agent (string, or `{ message }`, or `{ prompt }`)
524
+ - `priority?: "low" | "medium" | "high" | "urgent"` — scheduling priority
525
+
526
+ **Response (201):**
527
+ ```json
528
+ {
529
+ "taskId": "6ae101b7...",
530
+ "status": "queued"
531
+ }
532
+ ```
533
+
534
+ **Errors:**
535
+ - `400 validation_error` — missing `agentId` or `title`
536
+ - `404 not_found` — unknown agent slug
537
+
538
+ #### `ql.tasks.get(taskId)` → `Promise<Task>`
539
+
540
+ **Scope:** `tasks`
541
+
542
+ Single poll of a task's current state.
543
+
544
+ **Response (200):**
545
+ ```json
546
+ {
547
+ "taskId": "6ae101b7...",
548
+ "status": "completed",
549
+ "title": "Monthly Report",
550
+ "progress": {
551
+ "current": 100,
552
+ "total": 100,
553
+ "message": "Completed",
554
+ "updatedAt": "2026-10-27T19:53:23.117Z"
555
+ },
556
+ "result": "Here is your monthly report...",
557
+ "output": "Here is your monthly report...",
558
+ "error": null,
559
+ "createdAt": "2026-10-27T19:53:16.756Z",
560
+ "completedAt": "2026-10-27T19:53:23.117Z"
561
+ }
562
+ ```
563
+
564
+ | `status` value | Meaning | Check field |
565
+ |----------------|---------|-------------|
566
+ | `"queued"` | Waiting to start | — |
567
+ | `"running"` | In progress | `progress.message` for current step |
568
+ | `"retrying"` | Failed, will retry automatically | `error` for what went wrong |
569
+ | `"completed"` | Done successfully | `result` / `output` for the answer |
570
+ | `"failed"` | Permanently failed (max retries) | `error` for the reason |
571
+ | `"cancelled"` | Was cancelled | — |
572
+
573
+ #### `ql.tasks.handle(taskId)` → `TaskHandle`
574
+
575
+ Returns a handle with `poll()` and `wait()` methods.
576
+
577
+ **`handle.poll(signal?)`** — single fetch, returns current `Task` snapshot.
578
+
579
+ **`handle.wait(opts?)`** — polls until terminal status with backoff.
580
+
581
+ | Option | Default | Description |
582
+ |--------|---------|-------------|
583
+ | `intervalMs` | 2000 | Initial delay between polls (ms) |
584
+ | `maxIntervalMs` | 15000 | Backoff cap (grows 1.5× per poll) |
585
+ | `timeoutMs` | undefined (wait forever) | Throw after this many ms total |
586
+ | `signal` | undefined | AbortSignal for cancellation |
587
+ | `onTick` | undefined | Callback after each poll with latest snapshot |
132
588
 
133
- // One-shot poll
134
- const snapshot = await handle.poll();
589
+ **Terminal states returned by `wait()`:**
590
+ - `task.status === "completed"` → check `task.result`
591
+ - `task.status === "failed"` → check `task.error`
592
+ - `task.status === "cancelled"` → task was cancelled server-side
135
593
 
136
- // Wait until terminal (completed/failed/cancelled) with backoff + timeout + cancellation
137
- const done = await handle.wait({
138
- intervalMs: 2000, // initial delay
139
- maxIntervalMs: 15000, // backoff cap
140
- timeoutMs: 5 * 60_000, // throw QLError(code="timeout") if exceeded
141
- signal: controller.signal, // throw QLError(code="aborted") if aborted
594
+ **Non-terminal exits (throws QLError):**
595
+ - `err.code === "timeout"` → exceeded `timeoutMs`
596
+ - `err.code === "aborted"` → `signal` was aborted
597
+
598
+ **Example:**
599
+ ```ts
600
+ const created = await ql.tasks.create({
601
+ agentId: "finance",
602
+ title: "Quarterly Report",
603
+ input: { message: "Generate a Q3 financial summary from our database." },
604
+ priority: "high",
142
605
  });
606
+
607
+ const handle = ql.tasks.handle(created.taskId);
608
+
609
+ try {
610
+ const done = await handle.wait({
611
+ intervalMs: 2000,
612
+ maxIntervalMs: 10000,
613
+ timeoutMs: 5 * 60_000, // 5 minute timeout
614
+ onTick: (task) => {
615
+ if (task.status === "running" && task.progress?.message) {
616
+ process.stdout.write(`\r${task.progress.message}`);
617
+ }
618
+ },
619
+ });
620
+
621
+ if (done.status === "completed") {
622
+ console.log("\n✓ Done:", done.result);
623
+ } else if (done.status === "failed") {
624
+ console.error("✗ Failed:", done.error);
625
+ }
626
+ } catch (err) {
627
+ if ((err as any).code === "timeout") console.error("Timed out after 5 min");
628
+ if ((err as any).code === "aborted") console.log("Cancelled by user");
629
+ }
143
630
  ```
144
631
 
145
- > **Task execution limitation:** `tasks.create` only **enqueues** a task. It may remain in the `queued` state until an existing worker/execution mechanism on the backend processes it — creating a task via this SDK does **not** trigger execution. `wait()` simply polls until the task reaches a terminal status (or you time out / abort).
632
+ > **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.
146
633
 
147
- ## Errors
634
+ ---
148
635
 
149
- All failures throw a typed `QLError`:
636
+ ## Error Handling
637
+
638
+ All API failures throw a typed **`QLError`**:
150
639
 
151
640
  ```ts
152
641
  import { QLClient, QLError } from "ql-agent-sdk";
153
642
 
154
643
  try {
155
- await ql.agents.chat({ ... });
644
+ await ql.agents.chat({ agentId: "finance", messages: [...] });
156
645
  } catch (err) {
157
646
  if (err instanceof QLError) {
158
- err.status; // HTTP status (401/403/404/409/400/500) when from an HTTP response
159
- err.code; // stable code: unauthorized | forbidden | not_found | conflict |
160
- // validation_error | internal_error | network | aborted | timeout
161
- err.message; // human-readable (never includes the API key)
162
- err.isAuthError / err.isAborted / err.isTimeout / err.isNetworkError
647
+ console.error(err.status); // HTTP status code (401, 403, 404, etc.)
648
+ console.error(err.code); // stable string code (see table below)
649
+ console.error(err.message); // human-readable (never includes API key)
650
+
651
+ // Convenience booleans:
652
+ err.isAuthError; // true for 401/403
653
+ err.isAborted; // true for abort
654
+ err.isTimeout; // true for wait() timeout
655
+ err.isNetworkError; // true for connection failures
163
656
  }
164
657
  }
165
658
  ```
166
659
 
167
- | HTTP status | `code` | Meaning |
168
- |---|---|---|
169
- | 401 | `unauthorized` | missing/invalid/revoked API key |
170
- | 403 | `forbidden` | key lacks the required scope |
171
- | 404 | `not_found` | resource not found in this workspace |
172
- | 409 | `conflict` | conflict (e.g. duplicate) |
173
- | 400 | `validation_error` | bad request / invalid params |
174
- | 5xx | `internal_error` | server error |
660
+ ### Error Code Reference
661
+
662
+ | HTTP Status | `code` | When it happens |
663
+ |-------------|--------|-----------------|
664
+ | 401 | `unauthorized` | Missing, invalid, or revoked API key |
665
+ | 403 | `forbidden` | Key lacks the required scope for this endpoint |
666
+ | 404 | `not_found` | Resource doesn't exist in your workspace (agent, workflow, task) |
667
+ | 409 | `conflict` | Conflict (e.g. duplicate resource) |
668
+ | 400 | `validation_error` | Malformed request body, missing required fields, invalid params |
669
+ | 500 | `internal_error` | Server-side error (AI provider failure, DB issue, etc.) |
670
+ | — | `network` | No HTTP response received (connection refused, DNS failure, timeout) |
671
+ | — | `aborted` | Caller aborted via `AbortSignal` |
672
+ | — | `timeout` | `wait()` exceeded its `timeoutMs` budget |
175
673
 
176
- Transport-level failures map to `network` (no HTTP response), `aborted` (caller aborted), and `timeout` (`wait()` exceeded its budget).
674
+ ### Common Error Scenarios
675
+
676
+ ```ts
677
+ // 1. Expired/revoked key
678
+ try {
679
+ await ql.workspace.get();
680
+ } catch (e) {
681
+ if (e instanceof QLError && e.code === "unauthorized") {
682
+ console.log("API key is invalid or revoked. Generate a new one in your dashboard.");
683
+ }
684
+ }
685
+
686
+ // 2. Key doesn't have chat scope
687
+ try {
688
+ await ql.agents.chat({ agentId: "finance", messages: [...] });
689
+ } catch (e) {
690
+ if (e instanceof QLError && e.code === "forbidden") {
691
+ console.log("This key needs the 'chat' scope. Create a new key with broader scopes.");
692
+ }
693
+ }
694
+
695
+ // 3. AI provider error (transient)
696
+ try {
697
+ await ql.agents.chat({ agentId: "governing", messages: [...] });
698
+ } catch (e) {
699
+ if (e instanceof QLError && e.status === 500) {
700
+ console.log("AI service temporarily unavailable. Retry later.");
701
+ }
702
+ }
703
+ ```
704
+
705
+ ---
177
706
 
178
707
  ## TypeScript
179
708
 
180
- The package ships full type declarations (`dist/index.d.ts` / `.d.cts`). All request/response types are exported:
709
+ Full type declarations are included. All public types are exported:
181
710
 
182
711
  ```ts
183
712
  import type {
184
- QLClientOptions, ChatInput, ChatResult, ChatStreamEvent,
185
- AgentSummary, CapabilityInfo, CapabilityResult, HistoryPage,
186
- WorkflowRun, Task, TaskCreateInput, WaitOptions, QLError,
713
+ QLClient,
714
+ QLClientOptions,
715
+ QLError,
716
+ // Request types
717
+ ChatInput,
718
+ RunCapabilityInput,
719
+ TaskCreateInput,
720
+ WaitOptions,
721
+ HistoryParams,
722
+ WorkflowRunInput,
723
+ // Response types
724
+ Workspace,
725
+ AgentSummary,
726
+ AgentDetail,
727
+ CapabilityInfo,
728
+ CapabilityResult,
729
+ ChatResult,
730
+ ChatStreamEvent,
731
+ HistoryPage,
732
+ MessageItem,
733
+ WorkflowRun,
734
+ Task,
735
+ TaskCreated,
736
+ // Constants
737
+ API_ERROR_CODES,
187
738
  } from "ql-agent-sdk";
188
739
  ```
189
740
 
190
- ## Using it in a Next.js App Router (server-side)
741
+ ---
191
742
 
192
- In a **Route Handler** (`app/api/.../route.ts`) or a server Component — the SDK is safe to use on the server:
743
+ ## Using in Next.js App Router
744
+
745
+ The SDK is safe to use **server-side** in Route Handlers or Server Components:
193
746
 
194
747
  ```ts
195
748
  // app/api/agent/route.ts
@@ -200,48 +753,97 @@ export const runtime = "nodejs"; // uses native fetch + Web Streams
200
753
  const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
201
754
 
202
755
  export async function POST(req: Request) {
756
+ const { message } = await req.json();
757
+
203
758
  try {
204
- const { prompt } = await req.json();
205
- const res = await ql.agents.chat({
759
+ const reply = await ql.agents.chat({
206
760
  agentId: "governing",
207
- messages: [{ role: "user", content: prompt }],
761
+ messages: [{ role: "user", content: message }],
208
762
  });
209
- return Response.json({ ok: true, content: res.content });
763
+ return Response.json({ text: reply.content });
210
764
  } catch (err) {
211
765
  if (err instanceof QLError) {
212
- return Response.json({ ok: false, code: err.code, message: err.message }, { status: err.status ?? 500 });
766
+ return Response.json({ error: err.message }, { status: err.status ?? 500 });
213
767
  }
214
768
  throw err;
215
769
  }
216
770
  }
217
771
  ```
218
772
 
219
- For **streaming** to the browser from a Route Handler, forward the SDK's events as an SSE `ReadableStream` (see the streaming example above for consuming them).
773
+ For **streaming** in a Route Handler:
774
+ ```ts
775
+ export async function POST(req: Request) {
776
+ const { message } = await req.json();
777
+ const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
778
+
779
+ const stream = new ReadableStream({
780
+ async start(controller) {
781
+ try {
782
+ for await (const ev of ql.agents.chatStream({
783
+ agentId: "governing",
784
+ messages: [{ role: "user", content: message }],
785
+ })) {
786
+ if (ev.type === "delta" && ev.content) {
787
+ controller.enqueue(new TextEncoder().encode(ev.content));
788
+ }
789
+ }
790
+ } finally {
791
+ controller.close();
792
+ }
793
+ },
794
+ });
795
+
796
+ return new Response(stream, { headers: { "Content-Type": "text/plain" } });
797
+ }
798
+ ```
220
799
 
221
- ## Development
800
+ ---
222
801
 
223
- ```bash
224
- npm install
225
- npm run typecheck # tsc --noEmit
226
- npm run lint # eslint (typescript-eslint)
227
- npm test # vitest (unit: fetch, errors, SSE, polling, timeout, cancellation, pagination)
228
- npm run build # tsup -> dist (ESM + CJS + .d.ts)
229
- ```
802
+ ## Agent Reference
230
803
 
231
- ## Publishing
804
+ The platform includes 8 specialized agents. Each has 20 capabilities in its domain:
232
805
 
233
- This package is **not** published. To publish when ready (requires an npm account/org):
806
+ | ID | Domain | Examples |
807
+ |----|--------|----------|
808
+ | `governing` | Orchestration | Coordinates other agents, complex multi-step reasoning |
809
+ | `finance` | Finance | Invoices, P&L, budgeting, cash flow, tax estimates |
810
+ | `marketing` | Marketing | Social media content, ad copy, SEO, campaign planning |
811
+ | `inventory` | Supply Chain | Stock levels, reorder points, supplier management |
812
+ | `customer` | Customer Success | Support drafts, onboarding sequences, churn analysis |
813
+ | `developer` | Engineering | Code generation, API docs, bug triage, test writing |
814
+ | `operations` | Operations | SOPs, process mapping, KPI dashboards, compliance |
815
+ | `research` | Research | Market analysis, competitor intel, literature reviews |
234
816
 
235
- ```bash
236
- npm login
237
- # from packages/sdk:
238
- npm run build
239
- npm publish --access public # or --access restricted for a scoped/owned package
817
+ Discover available capabilities at runtime:
818
+ ```ts
819
+ const detail = await ql.agents.get("finance");
820
+ console.log(detail.capabilities.map((c) => c.id));
240
821
  ```
241
822
 
242
- To install from a local tarball instead of publishing:
823
+ ---
243
824
 
244
- ```bash
245
- cd packages/sdk && npm pack # -> ql-agent-sdk-0.1.0.tgz
246
- npm install /absolute/path/to/ql-agent-sdk-0.1.0.tgz
247
- ```
825
+ ## Environment Variables
826
+
827
+ | Variable | Purpose |
828
+ |----------|---------|
829
+ | `QL_API_KEY` | Your API key (convention — the SDK reads it from the `apiKey` option, not env directly) |
830
+ | `QL_BASE_URL` | Override the base URL (useful for staging/self-hosted) |
831
+
832
+ ---
833
+
834
+ ## Troubleshooting
835
+
836
+ | Symptom | Cause | Fix |
837
+ |---------|-------|-----|
838
+ | `QLError: unauthorized` | Key is invalid, revoked, or expired | Generate a new key in your dashboard |
839
+ | `QLError: forbidden` | Key scope doesn't cover the endpoint | Create a key with the required scope or `*` |
840
+ | `QLError: not_found` (agent) | Typo in agent slug | Use `ql.agents.list()` to see valid IDs |
841
+ | `QLError: network` | Can't reach the API | Check your network / base URL / firewall |
842
+ | Task stuck in `"running"` | Server restarted mid-execution | Wait for stale recovery (≤5 min) or poll again later |
843
+ | Stream produces no output | Model returned empty response | Check `reasoning` field or try a different agent |
844
+
845
+ ---
846
+
847
+ ## License
848
+
849
+ MIT