ql-agent-sdk 0.0.0-stage → 1.0.1

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,3 +1,849 @@
1
- # Temporary Holding Version
1
+ # ql-agent-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+
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
+ ---
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install ql-agent-sdk
17
+ # or
18
+ pnpm add ql-agent-sdk
19
+ # or
20
+ yarn add ql-agent-sdk
21
+ ```
22
+
23
+ Requires **Node.js 18+** (native `fetch`).
24
+
25
+ ---
26
+
27
+ ## Quick Start
28
+
29
+ ```ts
30
+ import { QLClient } from "ql-agent-sdk";
31
+
32
+ const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
33
+
34
+ // Discover available agents
35
+ const { agents } = await ql.agents.list();
36
+ console.log(agents.map((a) => a.id));
37
+ // → ["governing", "finance", "marketing", "inventory", "customer", "developer", "operations", "research"]
38
+
39
+ // Chat with an agent (non-streaming)
40
+ const reply = await ql.agents.chat({
41
+ agentId: "governing",
42
+ messages: [{ role: "user", content: "Summarize this quarter's revenue." }],
43
+ });
44
+ console.log(reply.content);
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Configuration
50
+
51
+ ```ts
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
56
+ });
57
+ ```
58
+
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.
105
+
106
+ ---
107
+
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:**
128
+ ```ts
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)
134
+ ```
135
+
136
+ ---
137
+
138
+ ### Agents
139
+
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
249
+ const { result } = await ql.agents.runCapability("finance", {
250
+ capId: "invoice",
251
+ params: { data: "Revenue | 12000\nCosts | 4000" },
252
+ });
253
+
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
+ }
263
+ ```
264
+
265
+ ---
266
+
267
+ ### Chat
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:**
334
+ ```ts
335
+ import { QLClient } from "ql-agent-sdk";
336
+
337
+ const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
338
+ const controller = new AbortController();
339
+
340
+ // Optional: auto-abort after 30s
341
+ const timer = setTimeout(() => controller.abort(), 30_000);
342
+
343
+ let full = "";
344
+ try {
345
+ for await (const ev of ql.agents.chatStream(
346
+ { agentId: "governing", messages: [{ role: "user", content: "Explain quantum computing" }] },
347
+ { signal: controller.signal },
348
+ )) {
349
+ if (ev.type === "delta") {
350
+ process.stdout.write(ev.content ?? ""); // print incrementally
351
+ full += ev.content ?? "";
352
+ } else if (ev.type === "done") {
353
+ full = ev.content; // final text (replaces accumulated)
354
+ }
355
+ }
356
+ } catch (err) {
357
+ if ((err as any)?.code === "aborted") console.log("\n[stopped by user]");
358
+ } finally {
359
+ clearTimeout(timer);
360
+ }
361
+ ```
362
+
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
+ ```
417
+
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"` |
426
+
427
+ **Walking all pages:**
428
+ ```ts
429
+ const allMessages = [];
430
+ let before: string | undefined;
431
+
432
+ do {
433
+ const page = await ql.agents.history("finance", { limit: 50, before });
434
+ allMessages.push(...page.items);
435
+ before = page.nextBefore ?? undefined; // null when exhausted
436
+ } while (before);
437
+
438
+ console.log(`${allMessages.length} total messages`);
439
+ ```
440
+
441
+ ---
442
+
443
+ ### Workflows
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
+ }
461
+ ```
462
+
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:**
495
+ ```ts
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 |
588
+
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
593
+
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",
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
+ }
630
+ ```
631
+
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.
633
+
634
+ ---
635
+
636
+ ## Error Handling
637
+
638
+ All API failures throw a typed **`QLError`**:
639
+
640
+ ```ts
641
+ import { QLClient, QLError } from "ql-agent-sdk";
642
+
643
+ try {
644
+ await ql.agents.chat({ agentId: "finance", messages: [...] });
645
+ } catch (err) {
646
+ if (err instanceof QLError) {
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
656
+ }
657
+ }
658
+ ```
659
+
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 |
673
+
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
+ ---
706
+
707
+ ## TypeScript
708
+
709
+ Full type declarations are included. All public types are exported:
710
+
711
+ ```ts
712
+ import type {
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,
738
+ } from "ql-agent-sdk";
739
+ ```
740
+
741
+ ---
742
+
743
+ ## Using in Next.js App Router
744
+
745
+ The SDK is safe to use **server-side** in Route Handlers or Server Components:
746
+
747
+ ```ts
748
+ // app/api/agent/route.ts
749
+ import { QLClient, QLError } from "ql-agent-sdk";
750
+
751
+ export const runtime = "nodejs"; // uses native fetch + Web Streams
752
+
753
+ const ql = new QLClient({ apiKey: process.env.QL_API_KEY! });
754
+
755
+ export async function POST(req: Request) {
756
+ const { message } = await req.json();
757
+
758
+ try {
759
+ const reply = await ql.agents.chat({
760
+ agentId: "governing",
761
+ messages: [{ role: "user", content: message }],
762
+ });
763
+ return Response.json({ text: reply.content });
764
+ } catch (err) {
765
+ if (err instanceof QLError) {
766
+ return Response.json({ error: err.message }, { status: err.status ?? 500 });
767
+ }
768
+ throw err;
769
+ }
770
+ }
771
+ ```
772
+
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
+ ```
799
+
800
+ ---
801
+
802
+ ## Agent Reference
803
+
804
+ The platform includes 8 specialized agents. Each has 20 capabilities in its domain:
805
+
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 |
816
+
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));
821
+ ```
822
+
823
+ ---
824
+
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