@qoniai/qoni 0.4.0

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 ADDED
@@ -0,0 +1,771 @@
1
+ # @qoniai/qoni
2
+
3
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-339933)](https://nodejs.org/)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-3178c6)](https://www.typescriptlang.org/)
5
+ [![Package manager](https://img.shields.io/badge/package%20manager-pnpm-f69220)](https://pnpm.io/)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
+
8
+ Unified Node.js SDK for Qoni Agent delegation, GenAuth user management, GUMem memory, WebAgent automation, web search, and monitoring.
9
+
10
+ `@qoniai/qoni` gives a trusted server one compact way to use Qoni AK/SK. For runtime product calls, your service verifies the GenAuth user, requests short-lived delegation with `delegateToken`, and then uses the returned token in GUMem, WebAgent, Do Anything, Web Search, and Track calls. For GenAuth user management, the SDK exchanges AK/SK for a standard GenAuth management token internally and calls GenAuth v3 users APIs with that token.
11
+
12
+ AK/SK credentials must stay on a trusted server. Do not ship them to browsers, mobile apps, public CLI config, or untrusted Agent runtimes.
13
+
14
+ English | [中文](./README.zh-CN.md)
15
+
16
+ ## Why Qoni
17
+
18
+ Agents that perform real work need more than a backend API key. They need a user boundary, explicit scopes, expiry, observable execution, and audit metadata that explains what happened later.
19
+
20
+ Qoni keeps that model small:
21
+
22
+ - One SDK entry: `new QoniAgentKit({ accessKey, secretKey })`.
23
+ - One discovery path: `host` can override the Qoni Console/SDK gateway for private or local deployments, and the SDK reads downstream runtime URLs from `/api/v3/eak/runtime-config`.
24
+ - One delegation entry: call `delegateToken`; silent mode returns `data.token`, while interactive mode returns an authorization URL and later completes on your server.
25
+ - One management path: call `genauth.users.*`, and the SDK exchanges AK/SK for a GenAuth management token before calling GenAuth v3 users APIs.
26
+ - Capability-first namespaces: `genauth`, `gumem`, `webSearch`, `doAnything`, and `track`.
27
+ - Readable scope strings for least-privilege authorization.
28
+ - Typed errors with `requestId`, `traceId`, `auditId`, and `retryable`.
29
+
30
+ The public package and API use the Qoni brand. The deployed server protocol has
31
+ not migrated yet, so internal signed routes remain under `/api/v3/eak/*`, the
32
+ delegation token claim remains `eak_delegation_token`, and existing `eak.*`
33
+ backend error codes are surfaced unchanged. Existing wire claims such as
34
+ `eak_tenant_id` also remain unchanged. Applications should use the public
35
+ Qoni API and must not construct these internal wire values themselves.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install @qoniai/qoni
41
+ # or
42
+ pnpm add @qoniai/qoni
43
+ # or
44
+ yarn add @qoniai/qoni
45
+ ```
46
+
47
+ Requirements:
48
+
49
+ - Node.js 18 or later.
50
+ - A server-side runtime with `fetch`.
51
+ - Qoni `accessKey` and `secretKey` created in Qoni Console.
52
+ - For silent runtime product calls, a real GenAuth user id from the userpool bound to the Qoni credential. For smoke tests, pass it as `QONI_USER_ID` or call `qoni.resolveAnyBoundUser()` to grab the first bound user; in application code, resolve it with `currentUser` or your existing server-side user session. Interactive delegation can start without `user` or `userId` because the Qoni Console/BFF resolves the GenAuth login during authorization.
53
+ - For `genauth.users.*` management calls, `QONI_USER_ID` is not required. The SDK uses AK/SK to request a GenAuth management token from Qoni.
54
+ - Optional `host` for private or local Qoni deployments. Leave it unset for hosted Qoni.
55
+
56
+ You do not pass `tenantId` during normal SDK initialization. The AK/SK is already bound to the tenant and application boundary in Qoni.
57
+
58
+ The package ships both ESM and CommonJS builds with TypeScript types, so either
59
+ import style works:
60
+
61
+ ```ts
62
+ import { QoniAgentKit } from "@qoniai/qoni"; // ESM / TypeScript
63
+ const { QoniAgentKit } = require("@qoniai/qoni"); // CommonJS
64
+ ```
65
+
66
+ ## AI Skill
67
+
68
+ Qoni can also be exposed to AI coding tools through a Skill package, so Codex, Claude Code, or internal Agents can follow the same authorization model.
69
+
70
+ Install the Qoni Skill:
71
+
72
+ ```bash
73
+ npx skills add https://github.com/QONIAI/qoni-sdk-node --skill qoni-sdk
74
+ ```
75
+
76
+ If you only want to install it for Claude Code, specify the Agent:
77
+
78
+ ```bash
79
+ npx skills add https://github.com/QONIAI/qoni-sdk-node --agent claude-code --skill qoni-sdk
80
+ ```
81
+
82
+ The npm package includes the `skills/` directory, but the recommended Skill install path is still the GitHub repository command above so agents receive the repository-local skill metadata.
83
+
84
+ ## Quick Start
85
+
86
+ Start with a server-side SDK instance. AK/SK stays on your trusted server for both management-plane calls and runtime product delegation.
87
+
88
+ ```ts
89
+ import fs from "node:fs";
90
+ import { QoniAgentKit } from "@qoniai/qoni";
91
+
92
+ const qoni = new QoniAgentKit({
93
+ accessKey: process.env.QONI_ACCESS_KEY!,
94
+ secretKey: process.env.QONI_SECRET_KEY!,
95
+ });
96
+ ```
97
+
98
+ ### GenAuth User Management
99
+
100
+ `genauth.users.*` is a management-plane capability. It does not need `QONI_USER_ID` or `delegateToken`; the SDK signs `POST /api/v3/eak/genauth/admin-token` with `{}` internally, receives a GenAuth management token plus `userPoolId`, and then calls GenAuth v3 users APIs.
101
+
102
+ ```ts
103
+ const users = await qoni.genauth.users.list({
104
+ page: 1,
105
+ limit: 20,
106
+ });
107
+
108
+ console.log("GenAuth users:", users.data);
109
+
110
+ const created = await qoni.genauth.users.create<{ userId: string }>({
111
+ username: `sdk-demo-${Date.now()}`,
112
+ password: process.env.GENAUTH_DEMO_USER_PASSWORD!,
113
+ });
114
+
115
+ const profile = await qoni.genauth.users.get<{ userId: string }>({
116
+ userId: created.data.userId,
117
+ });
118
+
119
+ await qoni.genauth.users.update({
120
+ userId: profile.data.userId,
121
+ nickname: "SDK demo user",
122
+ });
123
+
124
+ // Optional smoke-test cleanup:
125
+ // await qoni.genauth.users.deleteBatch({ userIds: [profile.data.userId] });
126
+ ```
127
+
128
+ ### Runtime Product Delegation
129
+
130
+ GUMem, WebAgent, Do Anything, Web Search, and Track act for an end user. Silent delegation calls need a real GenAuth user id from the userpool bound to the Qoni credential, then a `delegateToken` result.
131
+
132
+ ```ts
133
+ const userId = process.env.QONI_USER_ID!;
134
+ if (!userId) {
135
+ throw new Error("QONI_USER_ID must be a real GenAuth user id from the credential-bound userpool");
136
+ }
137
+
138
+ // `products` is per-product authorization sugar; `agent` defaults to "sdk".
139
+ const { token } = (
140
+ await qoni.delegateToken({
141
+ user: { id: userId },
142
+ products: ["doAnything"],
143
+ })
144
+ ).data;
145
+
146
+ const run = await qoni.doAnything.run({
147
+ token, // passed once — the handle holds it; handle methods never take a token
148
+ prompt: "Open https://en.wikipedia.org/wiki/Singapore and summarize the country's key facts.",
149
+ capture: { screenshots: true },
150
+ });
151
+
152
+ const result = await run.wait({
153
+ onScreenshot: (img, i) => fs.writeFileSync(`step-${i}.jpg`, img.bytes),
154
+ onInteraction: (i) => { // login / clarify / confirm / take-control
155
+ if (i.can("confirm_signed_in")) return i.confirmSignedIn();
156
+ if (i.can("confirm")) return i.confirm();
157
+ },
158
+ });
159
+ console.log(result.output);
160
+ ```
161
+
162
+ `run()` returns a `RunHandle` typed to the product: `qoni.deepResearch.run()` is a `RunHandle<DeepResearchEvent>`, so its events are narrowed to what Deep Research actually emits. `capture: { screenshots: true }` opts into per-step screenshots, decoded to bytes for you. `wait()` drives the run to its terminal state and settles the result (`result.raw` carries cost, step counts, token usage).
163
+
164
+ > **Best practice — match `event.type` against the `QoniEventTypes` constants.**
165
+ > Each product handle is typed to its own event set, so in TypeScript a case
166
+ > for another product's event is a compile error and autocomplete shows only
167
+ > the right ones. All the internal churn (supervisor planning, individual
168
+ > clicks/keystrokes, telemetry) folds into `QoniEventTypes.Progress`; once you
169
+ > match a `case`, `event.data` has a fixed shape — a bare value for simple
170
+ > events (`progress` → a string line, `resultsReady` → a number) and a small
171
+ > object for rich ones (`done` → `{ output, succeeded, terminalReason }`). The
172
+ > raw wire types are not exported.
173
+
174
+ ```ts
175
+ import { QoniEventTypes } from "@qoniai/qoni";
176
+
177
+ const run = await qoni.deepResearch.run({ token, prompt: "State of EU battery recycling, 2026 update" }); // RunHandle<DeepResearchEvent>
178
+
179
+ for await (const event of run.events()) {
180
+ switch (event.type) {
181
+ case QoniEventTypes.Progress: console.log(event.data); break; // data IS the line (string)
182
+ case QoniEventTypes.Phase: console.log(event.data); break; // data IS the phase name (string)
183
+ case QoniEventTypes.SectionReady: console.log(event.data); break; // data IS the section title (string)
184
+ case QoniEventTypes.Message: console.log(event.data.text); break; // rich → { text, role }
185
+ case QoniEventTypes.Interaction: { // rich → the typed Interaction object
186
+ const i = run.interactionHandle(event.data); // e.g. an outline-approval confirmation
187
+ if (i.can("confirm")) await i.confirm();
188
+ break;
189
+ }
190
+ case QoniEventTypes.Done: console.log(event.data.output); break; // rich → terminal
191
+ // case QoniEventTypes.ResultsReady — Web Search only → compile error here
192
+ }
193
+ // event.runId / event.at / event.isTerminal / event.raw (escape hatch)
194
+ }
195
+ // The iterator ends automatically at the terminal (QoniEventTypes.Done) event.
196
+ ```
197
+
198
+ > **JS note:** the per-product compile-time narrowing is a TypeScript feature.
199
+ > In plain JavaScript there's no type checking, so a wrong-product `case` just
200
+ > never fires (it's not an error). Use TypeScript for the compile-time guard.
201
+
202
+ Power users can match the exact internal wire type via `event.raw.event` (a
203
+ raw string like `"run.action.started"`) — e.g. to tell a specific browser
204
+ action apart inside `progress`. That's the escape hatch, not the everyday API.
205
+
206
+ ```ts
207
+ await run.cancel("user stopped the task"); // idempotent — safe on a finished run
208
+ const followUp = await qoni.doAnything.run({
209
+ token,
210
+ prompt: "Now open the Wikipedia page for Malaysia and compare its population with Singapore's.",
211
+ session: run.sessionRef, // reuse the same browser session
212
+ });
213
+ const reattached = await qoni.doAnything.attach(run.id, { token }); // re-attach by run id alone
214
+ ```
215
+
216
+ ## Authorization Model
217
+
218
+ ### Runtime Discovery
219
+
220
+ `host` points to the Qoni Console/SDK gateway, not directly to GenAuth, GUMem, or WebAgent. Hosted integrations normally leave `host` unset; private and local deployments can pass their own gateway.
221
+
222
+ The SDK signs a request to:
223
+
224
+ ```text
225
+ GET /api/v3/eak/runtime-config
226
+ ```
227
+
228
+ This path is an intentional compatibility boundary with the current server; it
229
+ is not the public SDK brand or JavaScript namespace.
230
+
231
+ The response provides runtime base URLs for Qoni, GenAuth, GUMem, and WebAgent. Application code should not configure those product URLs separately for normal usage.
232
+
233
+ ### Server-Side AK/SK
234
+
235
+ `accessKey` and `secretKey` identify the tenant and application boundary configured in Qoni Console. The SDK uses them to sign trusted server-side Qoni requests, including delegation and product-token exchange.
236
+
237
+ ### Delegation Token
238
+
239
+ In silent mode, `delegateToken` binds four things:
240
+
241
+ - the current user id
242
+ - the Agent id
243
+ - the requested scopes
244
+ - the token expiry and audit context
245
+
246
+ Silent product calls receive `token: delegation.data.token`. If that token is an Qoni delegation token, the SDK exchanges it internally for the correct GUMem or WebAgent product access token before calling the downstream service.
247
+
248
+ **Input shape.** `delegateToken` is discriminated by `mode`:
249
+
250
+ - **Silent** (`mode: "silent"`, the default) — acts for one end user, so a user
251
+ is **required**: pass `user: { id: <genauthUserId> }` (preferred) or the
252
+ deprecated top-level `userId`. Omitting it throws `QoniValidationError`
253
+ locally before any request. Authorize with the `products` sugar and/or
254
+ explicit `scopes`.
255
+ - **Interactive** (`mode: "interactive"`) — pass `redirectUri`, `state`,
256
+ `agent`, `scopes` and **no** user (Qoni Console resolves the login during
257
+ authorization). Returns `data.authorizationUrl`, not a token; complete it
258
+ server-side with `completeDelegateToken({ grantId, code, state })`.
259
+
260
+ `agent` is optional and defaults to `"sdk"`.
261
+
262
+ ```ts
263
+ const delegation = await qoni.delegateToken({
264
+ userId,
265
+ agent: "research-assistant",
266
+ scopes: ["gumem.memory:read"],
267
+ mode: "silent",
268
+ });
269
+
270
+ await qoni.gumem.recall({
271
+ token: delegation.data.token,
272
+ query: "research preferences",
273
+ });
274
+ ```
275
+
276
+ In interactive mode, your server still starts with `delegateToken`, but it does not receive a token:
277
+
278
+ ```ts
279
+ const grant = await qoni.delegateToken({
280
+ mode: "interactive",
281
+ redirectUri: "https://app.example.com/qoni/callback",
282
+ state: "business-csrf-state",
283
+ agent: "research-assistant",
284
+ scopes: ["gumem.memory:read", "webagent.do_anything:manage"],
285
+ });
286
+
287
+ redirectUserTo(grant.data.authorizationUrl);
288
+ ```
289
+
290
+ The user visits `authorizationUrl` in Qoni Console/BFF. If they are not logged in, Console/BFF sends them through GenAuth login, confirms authorization, completes the grant internally, then redirects back to your `redirectUri` with a one-time `code`, your `state`, and `grant_state`/`grantId`. The browser never receives a delegation token.
291
+
292
+ Your callback handler runs on the server and exchanges the callback fields with AK/SK:
293
+
294
+ ```ts
295
+ const completed = await qoni.completeDelegateToken({
296
+ grantId: String(req.query.grantId ?? req.query.grant_id),
297
+ code: String(req.query.code),
298
+ state: String(req.query.state),
299
+ });
300
+
301
+ await qoni.gumem.recall({
302
+ token: completed.data.token,
303
+ query: "research preferences",
304
+ });
305
+ ```
306
+
307
+ `completeDelegateToken` is the recommended server callback helper. `delegateAgent` and `completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
308
+
309
+ ## Choosing Scopes
310
+
311
+ Each WebAgent product exposes exactly two scopes: `read` (observe — status, events, artifacts) and `manage` (anything that changes execution state — run, cancel, respond, messages, monitor CRUD). The full set is the 8 scopes `webagent.{do_anything, web_search, deep_research, track}:{read, manage}`. The simplest authorization is the `products` sugar — each product name expands to its `read` + `manage` pair:
312
+
313
+ ```ts
314
+ await qoni.delegateToken({
315
+ user: { id: userId },
316
+ products: ["doAnything", "webSearch", "deepResearch", "track"],
317
+ scopes: ["gumem.memory:read"], // fine-grained scopes can be mixed in
318
+ });
319
+ ```
320
+
321
+ Scope strings are validated locally before any request: a scope missing its service prefix (e.g. `do_anything:manage` instead of `webagent.do_anything:manage`) throws `QoniValidationError` immediately with the correct form in the message.
322
+
323
+ Use explicit scope strings when the requested permission boundary should be visible in code.
324
+
325
+ | Scenario | Useful scopes | User-facing meaning |
326
+ | --- | --- | --- |
327
+ | Read user memory | `gumem.memory:read` | Agent can read relevant historical preferences. |
328
+ | Create a GUMem session and recall context | `gumem.memory:read`, `gumem.memory:write`, `gumem.message:write` | Agent can create a memory session, write messages, and recall context for the current user. |
329
+ | Write task results | `gumem.message:write`, `gumem.action:write` | Agent can write this task's confirmed result back to GUMem. |
330
+ | Search public web | `webagent.web_search:read`, `webagent.web_search:manage` | Agent can search public pages and read search results. |
331
+ | Run a bounded web task | `webagent.do_anything:read`, `webagent.do_anything:manage` | Agent can run a visible web task, read progress, stop it, and intervene when needed. |
332
+ | Create a monitor | `webagent.track:read`, `webagent.track:manage` | Agent can configure monitors, read events, run checks, and stop monitors. |
333
+
334
+ Silent mode is usually appropriate for low-risk actions such as reading current-user memory, writing a confirmed summary, or searching public web pages.
335
+
336
+ Interactive mode is recommended for higher-risk actions such as browser takeover, external site login, long-running monitoring, form submission, or access to sensitive artifacts and recordings.
337
+
338
+ The SDK also exports a GUMem-oriented bundle for the common session/write/recall path:
339
+
340
+ ```ts
341
+ QoniScopeBundles.GUMEM_SESSION_RECALL
342
+ ```
343
+
344
+ ## Capability Examples
345
+
346
+ ### GUMem
347
+
348
+ ```ts
349
+ await qoni.gumem.createSession({
350
+ token,
351
+ userId,
352
+ sessionId: "daily-assistant",
353
+ title: "Daily assistant memory",
354
+ });
355
+
356
+ // Your application has already collected and confirmed this preference with the user.
357
+ const confirmedPreference =
358
+ "Please keep daily planning suggestions concise and include only the next action.";
359
+
360
+ await qoni.gumem.addMessages({
361
+ token,
362
+ sessionId: "daily-assistant",
363
+ messages: [
364
+ {
365
+ role: "user",
366
+ content: confirmedPreference,
367
+ },
368
+ {
369
+ role: "assistant",
370
+ content: `Saved confirmed user preference: ${confirmedPreference}`,
371
+ },
372
+ ],
373
+ });
374
+
375
+ const context = await qoni.gumem.recall({
376
+ token,
377
+ sessionId: "daily-assistant",
378
+ query: "What confirmed preferences should I consider for this user's next task?",
379
+ details: true,
380
+ });
381
+
382
+ // Pass context.data into your own assistant response composer or task planner.
383
+ // The SDK example stops here because application orchestration is product-specific.
384
+ ```
385
+
386
+ ### Do Anything
387
+
388
+ ```ts
389
+ const run = await qoni.doAnything.run({
390
+ token,
391
+ prompt: "Open https://en.wikipedia.org/wiki/Solid-state_battery and summarize its key advantages over lithium-ion.",
392
+ });
393
+
394
+ const status = await run.status(); // refresh the run's current state
395
+ await run.cancel("User stopped the task"); // idempotent
396
+ ```
397
+
398
+ ### Web Search
399
+
400
+ ```ts
401
+ const search = await qoni.webSearch.run({
402
+ token,
403
+ prompt: "EU battery recycling regulations 2026",
404
+ maxResultsPerQuery: 5,
405
+ });
406
+
407
+ const result = await search.wait();
408
+ console.log(result.output); // search results
409
+ ```
410
+
411
+ ### Deep Research
412
+
413
+ ```ts
414
+ const research = await qoni.deepResearch.run({
415
+ token,
416
+ prompt: "State of EU battery recycling, 2026 update",
417
+ depth: "standard", // light | standard | deep — the main research knob
418
+ limits: { maxDurationMinutes: 120 }, // wall-clock cap; the run self-terminates past it
419
+ });
420
+
421
+ const report = await research.wait();
422
+ for (const artifact of report.artifacts) {
423
+ fs.writeFileSync(artifact.name ?? artifact.id, await artifact.content());
424
+ }
425
+
426
+ // Follow-up research that inherits the prior run's context:
427
+ const followUp = await qoni.deepResearch.run({
428
+ token,
429
+ prompt: "Now compare against the 2025 numbers.",
430
+ session: research.sessionRef,
431
+ });
432
+ ```
433
+
434
+ ### Track
435
+
436
+ ```ts
437
+ const monitor = await qoni.track.create({
438
+ token,
439
+ prompt: "Monitor the price of Bitcoin every minute.",
440
+ });
441
+
442
+ // A new monitor starts in `pending_clarification` while the backend aligns
443
+ // the intent into a concrete spec, then flips to `active` (usually a few
444
+ // seconds; it may emit an `interaction` first if the intent is ambiguous).
445
+ // runNow()/refine() require `active`, so wait for it before acting:
446
+ for await (const event of monitor.events()) {
447
+ if (event.type === "interaction") {
448
+ const i = monitor.interactionHandle(event.data); // e.g. a needs-reauth site_login
449
+ if (i.can("confirm")) await i.confirm();
450
+ continue;
451
+ }
452
+ const { status } = await monitor.get();
453
+ if (status === "active") break;
454
+ }
455
+
456
+ await monitor.runNow(); // trigger an immediate tick
457
+ const ticks = await monitor.runs({ limit: 10 }); // read-only tick run views
458
+
459
+ // Change the definition with refine() (any subset of: schedule / targetUrls /
460
+ // extractionSchema / triggerDsl / stopConditionDsl / notifyChannel). `schedule`
461
+ // is { kind: "cron", expr } or { kind: "interval", intervalSeconds }. The
462
+ // natural-language intent isn't refinable — create a new monitor to change it:
463
+ await monitor.refine({ schedule: { kind: "cron", expr: "0 9 * * *" } });
464
+ await monitor.pause(); // stop scheduled ticks without deleting
465
+ await monitor.resume();
466
+ await monitor.delete();
467
+
468
+ // Reconnect later from a stored id:
469
+ const sameMonitor = await qoni.track.attach(monitor.id, { token });
470
+ ```
471
+
472
+ ### GenAuth User Context and Management
473
+
474
+ ```ts
475
+ const client = new QoniAgentKit({
476
+ accessKey: process.env.QONI_ACCESS_KEY!,
477
+ secretKey: process.env.QONI_SECRET_KEY!,
478
+ });
479
+
480
+ const profile = await client.genauth.userInfo({
481
+ accessToken: process.env.GENAUTH_ACCESS_TOKEN!,
482
+ });
483
+
484
+ const users = await client.genauth.users.list({
485
+ page: 1,
486
+ limit: 20,
487
+ });
488
+
489
+ const created = await client.genauth.users.create({
490
+ username: "sdk-demo-user",
491
+ password: process.env.GENAUTH_DEMO_USER_PASSWORD!,
492
+ });
493
+ ```
494
+
495
+ `currentUser` and `genauth.userInfo` use a GenAuth access token because they read the logged-in user's identity. `genauth.users.*` is a management-plane capability, but application code still authenticates the SDK with Qoni AK/SK: the SDK signs `POST /api/v3/eak/genauth/admin-token` with an empty JSON body `{}`, receives a standard GenAuth management token plus `userPoolId`, then calls GenAuth v3 user management APIs with `Authorization: Bearer ...` and `x-authing-userpool-id`. It does not send `resource`, `actions`, `expiresIn`, or `QONI_USER_ID`. Runtime capability calls such as GUMem/WebAgent use `delegateToken` output instead.
496
+
497
+ ## API Surface
498
+
499
+ ### Client
500
+
501
+ ```ts
502
+ const qoni = new QoniAgentKit({
503
+ accessKey: process.env.QONI_ACCESS_KEY!,
504
+ secretKey: process.env.QONI_SECRET_KEY!,
505
+ timeoutMs: 30_000,
506
+ });
507
+ ```
508
+
509
+ | Option | Type | Required | Description |
510
+ | --- | --- | --- | --- |
511
+ | `accessKey` | `string` | Yes | Qoni access key from Qoni Console. |
512
+ | `secretKey` | `string` | Yes | Qoni secret key from Qoni Console. |
513
+ | `host` | `string` | No | Optional Qoni Console/SDK gateway override for private or local deployments. |
514
+ | `fetch` | `typeof fetch` | No | Custom transport implementation. |
515
+ | `timeoutMs` | `number` | No | Per-request timeout (connect + response headers). Defaults to `30000`. |
516
+
517
+ > **`timeoutMs` does not bound a long-running run.** It applies to individual
518
+ > one-shot HTTP requests (delegation, `status()`, a single POST). The live
519
+ > event stream (`events()` / `wait()`) is **not** cut off by it — once the
520
+ > stream's response headers arrive the per-request timer is cleared, so a
521
+ > Deep Research run that takes an hour streams fine under the default 30s
522
+ > client timeout. To bound how long you wait, pass `wait({ timeoutMs })` —
523
+ > with no value, `wait()` runs until the run reaches a terminal state. A
524
+ > `wait()` timeout does not stop the run; it keeps executing server-side and
525
+ > you can `attach()` to it again.
526
+
527
+ `Qoni` is also exported as a short alias:
528
+
529
+ ```ts
530
+ import { Qoni, QoniAgentKit } from "@qoniai/qoni";
531
+ ```
532
+
533
+ ### Namespaces
534
+
535
+ | Namespace | Methods |
536
+ | --- | --- |
537
+ | Delegation | `delegateToken`, `completeDelegateToken`, deprecated aliases `delegateAgent`, `completeDelegateAgent` |
538
+ | GenAuth | `userInfo`, `jwks`, `discovery`, `introspectDelegationToken`, `users.list`, `users.get`, `users.getBatch`, `users.create`, `users.createBatch`, `users.update`, `users.deleteBatch` |
539
+ | GUMem | `createSession`, `addMessages`, `recall`, `uploadResource`, `actions.record`, `actions.recall`, `actions.stream` |
540
+ | Do Anything | `run`, `attach` → `RunHandle` |
541
+ | Deep Research | `run`, `attach` → `RunHandle` |
542
+ | Web Search | `run`, `attach` → `RunHandle` |
543
+ | Track | `create`, `attach` → `MonitorHandle` |
544
+
545
+ The three run products share one handle shape:
546
+
547
+ ```ts
548
+ run.id
549
+ run.sessionRef // pass back to run({ session }) to reuse the session
550
+ await run.status() // refresh and return the current state
551
+ run.events({ onWireEvent?, onReconnect?, ...opts }) // semantic events + optional raw telemetry
552
+ await run.wait({ timeoutMs?, onEvent?, onWireEvent?, onReconnect?, onInteraction?, onScreenshot? })
553
+ run.interactionHandle(interaction) // typed HITL handle (see Interactions below)
554
+ await run.cancel(reason?) // idempotent
555
+ ```
556
+
557
+ `await run.wait(...)` settles to a `RunResult`:
558
+
559
+ ```ts
560
+ interface RunResult {
561
+ runId: string;
562
+ status: "succeeded" | "failed" | "canceled"; // delivery status of the run
563
+ output?: unknown; // final answer / payload (shape is task-specific)
564
+ artifacts: Artifact[]; // deliverables — deepResearch reports; [] for the others
565
+ isTaskSuccessful?: boolean; // did the task achieve its goal (distinct from `status`)
566
+ terminalReason?: string;
567
+ raw: JsonObject; // full wire envelope — cost, step counts, token usage
568
+ }
569
+ ```
570
+
571
+ Each `Artifact` exposes `id`, `name?`, `mime?`, `sizeBytes?`, `createdAt?`, and
572
+ `content(): Promise<Uint8Array>` (lazy byte download). Every other method returns the `QoniResponse<T>` envelope below; `T`
573
+ is the typed payload for that call (e.g. `genauth.users.list` → a paged user
574
+ list on `.data`). The exact `T` shapes live in the bundled `.d.ts` and follow
575
+ the backend contract.
576
+
577
+ `MonitorHandle` (Track) exposes `id`, `get()`, `pause()`, `resume()`, `refine(patch)`, `runNow()`, `events()`, `interactionHandle()`, `runs()`, `run(runId)`, and `delete()`. `runNow()` and `refine()` require the monitor to be `active` — a freshly created monitor is briefly `pending_clarification` first (see the Track example).
578
+
579
+ ### Interactions (HITL)
580
+
581
+ When a run needs the user — sign in to a site, answer a clarification, approve a
582
+ plan, take over the browser, or wait — it emits an `interaction` event whose
583
+ `event.data` is a typed `Interaction` (discriminated by `type`: `site_login` /
584
+ `clarification` / `confirmation` / `take_control` / `wait`). Wrap it in a handle
585
+ and call the action methods; each posts to the endpoint the backend declared in
586
+ the interaction's `actions[]`, so you never hard-code a route:
587
+
588
+ ```ts
589
+ for await (const event of run.events()) {
590
+ if (event.type === "interaction") {
591
+ const i = run.interactionHandle(event.data); // event.data is the Interaction
592
+ if (i.type === "clarification" && i.can("answer")) await i.answer("the blue button");
593
+ else if (i.can("confirm")) await i.confirm();
594
+ else if (i.can("confirm_signed_in")) await i.confirmSignedIn();
595
+ else if (i.can("release_control")) await i.releaseControl();
596
+ }
597
+ }
598
+ ```
599
+
600
+ Methods: `answer(text)`, `skip()`, `confirm()`, `reject()`, `openLogin()`,
601
+ `confirmSignedIn()`, `connectControl()`, `refreshControl()`, `releaseControl()`,
602
+ `retry()`, `switchProfile()`. `i.can(kind)` reports whether the interaction
603
+ currently offers that action; calling a method the backend did NOT declare
604
+ throws (so a "recovery" button only exists when it actually works).
605
+
606
+ The discriminant strings used above are also exported as constants so you can
607
+ avoid bare literals: `InteractionTypes` (`site_login` / `clarification` /
608
+ `confirmation` / `take_control` / `wait`), `InteractionStatuses`, and
609
+ `ActionKinds` (the `i.can(...)` action names). Authorization scope strings have
610
+ the same treatment — `QoniScopes` (every individual scope), `QoniProductScopes`
611
+ (the per-product `read`+`manage` pairs the `products` sugar expands to), and
612
+ `QoniScopeBundles` (curated bundles). Use them in place of hand-typed strings:
613
+
614
+ ```ts
615
+ import { InteractionTypes, ActionKinds, QoniScopes } from "@qoniai/qoni";
616
+
617
+ if (i.type === InteractionTypes.Clarification && i.can(ActionKinds.answer)) {
618
+ await i.answer("the blue button");
619
+ }
620
+ ```
621
+
622
+ ### Wire-Level Escape Hatch
623
+
624
+ The 1:1 low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnything.api.createSession`, `qoni.deepResearch.api.followUp`, `qoni.track.api.createMonitor`). These mirror the backend HTTP contract directly — snake_case fields, double-envelope events, single-ID `/{product}/runs/{run_id}` addressing for run operations — and their shapes may evolve with the API. They are not part of the frozen public contract.
625
+
626
+ `event.raw.event` is the **raw wire event type** — an internal transport name (`run.action.started`, `run.section_completed`, …) whose naming is historical and inconsistent, exposed as a plain string. This is an implementation detail, not the product API: developers match on the curated `event.type` above. The full wire-type catalog is intentionally NOT exported — reach for `event.raw.event` (a string) only when you need an exact wire match the curated layer folds into `progress` (e.g. telling a specific browser action apart).
627
+
628
+ Browser Use and Site Login scopes are reserved until their product runtime SDK methods are exported.
629
+
630
+ ### Response Shape
631
+
632
+ Most SDK methods return:
633
+
634
+ ```ts
635
+ type QoniResponse<T> = {
636
+ data: T;
637
+ meta: {
638
+ requestId?: string;
639
+ traceId?: string;
640
+ auditId?: string;
641
+ service?: "qoni" | "genauth" | "gumem" | "webagent";
642
+ };
643
+ };
644
+ ```
645
+
646
+ Handle event streams (`run.events()`, `monitor.events()`) yield a discriminated
647
+ `RunEvent` — once you match `event.type`, `event.data` is the fixed shape for
648
+ that type:
649
+
650
+ ```ts
651
+ // event.type → event.data:
652
+ // SIMPLE events — event.data IS the value (no field to remember):
653
+ // "progress" string — a human-readable line of what's happening
654
+ // "phase" string — the Deep Research phase name
655
+ // "sectionReady" string — the Deep Research section title
656
+ // "resultsReady" number — Web Search count of unique results
657
+ // "monitorCreated" string — Track monitor id
658
+ // "triggered" string — Track change summary
659
+ // "checkCompleted" boolean — Track: did the scheduled check find a change
660
+ // RICH events — event.data is a small object:
661
+ // "message" { text, role }
662
+ // "interaction" the typed Interaction object (HITL) — see Interactions
663
+ // "screenshot" { pageUrl?, step? } + the decoded image on event.image
664
+ // "done" { output, succeeded, terminalReason }
665
+ // Common to every event: runId, at (ISO 8601), isTerminal (true only for "done"),
666
+ // raw (the original wire event — escape hatch).
667
+ ```
668
+
669
+ Each product handle is typed to its own subset:
670
+
671
+ | Product | `event.type` set |
672
+ |---|---|
673
+ | Do Anything | core: `progress` / `message` / `interaction` / `screenshot` / `done` |
674
+ | Web Search | core + `resultsReady` |
675
+ | Deep Research | core + `phase` / `sectionReady` (the outline gate arrives as an `interaction`) |
676
+ | Track (`MonitorHandle`) | core + `monitorCreated` / `triggered` / `checkCompleted` |
677
+
678
+ Dropped connections reconnect automatically with `Last-Event-ID` catch-up; the iterator ends at the run's terminal (`done`) event.
679
+ For complete audit/observability capture, `onWireEvent` receives every original
680
+ SSE frame before semantic normalization or filtering; `onReconnect` receives
681
+ the retry attempt and last event id. Keep application behavior on the curated
682
+ `RunEvent` stream and use the wire callback only as evidence/diagnostics.
683
+
684
+ Wire-level streaming methods (`qoni.<product>.api.events`) return `AsyncIterable<QoniEvent<T>>`:
685
+
686
+ ```ts
687
+ type QoniEvent<T = unknown> = {
688
+ id?: string;
689
+ event?: string;
690
+ data: T;
691
+ };
692
+ ```
693
+
694
+ ## Error Handling
695
+
696
+ All SDK errors inherit from `QoniError` and expose `code`, `status`, `requestId`, `traceId`, `auditId`, `retryable`, and the original response `body` when available. `code` is typed as `QoniErrorCode` (the known SDK / backend codes plus `(string & {})`), so you get literal autocomplete and can `switch (err.code)` over the known set while still tolerating new backend codes.
697
+
698
+ All error classes are exported and can be matched with `instanceof`:
699
+
700
+ | Class | Fires when | Typical recovery |
701
+ | --- | --- | --- |
702
+ | `QoniValidationError` | Local input check failed before any request (bad scope string, silent delegation missing a user). | Fix the call — the message states the correct form. |
703
+ | `QoniAuthError` | AK/SK signing rejected by Qoni (bad/rotated credentials). | Check `accessKey` / `secretKey`. |
704
+ | `QoniPermissionDeniedError` | The delegation token lacks a required scope (403). | Re-`delegateToken` with the missing scope. |
705
+ | `QoniTokenExpiredError` | The delegation / product token expired. | Mint a fresh token and retry. |
706
+ | `QoniDelegationRequiredError` | A product call was made without `token`, or with a raw token the gateway won't accept directly. | Pass `delegation.data.token` to the product call. |
707
+ | `QoniRateLimitError` | Rate limited (429). | Back off and retry (`retryable === true`). |
708
+ | `QoniTimeoutError` | A request (or `wait({ timeoutMs })`) timed out. | Retry; the run keeps executing server-side — `attach()` to it. |
709
+ | `QoniUpstreamError` | A downstream service failed (5xx / upstream exchange failure). | Back off and retry when `retryable`. |
710
+ | `QoniError` | Base class / any other code. | Inspect `err.code`. |
711
+
712
+ The SDK does **not** auto-retry. `err.retryable` tells you whether a retry could
713
+ succeed; you own the backoff.
714
+
715
+ ```ts
716
+ import {
717
+ QoniDelegationRequiredError,
718
+ QoniPermissionDeniedError,
719
+ QoniRateLimitError,
720
+ QoniTimeoutError,
721
+ QoniTokenExpiredError,
722
+ QoniUpstreamError,
723
+ } from "@qoniai/qoni";
724
+
725
+ try {
726
+ await qoni.webSearch.run({ token, prompt: "Qoni SDK" });
727
+ } catch (error) {
728
+ if (
729
+ error instanceof QoniPermissionDeniedError ||
730
+ error instanceof QoniTokenExpiredError ||
731
+ error instanceof QoniDelegationRequiredError
732
+ ) {
733
+ // Re-mint a delegation token (add the missing scope / refresh expiry) and retry.
734
+ }
735
+
736
+ if (
737
+ error instanceof QoniRateLimitError ||
738
+ error instanceof QoniTimeoutError ||
739
+ error instanceof QoniUpstreamError
740
+ ) {
741
+ // Transient — back off and retry when `error.retryable` is true.
742
+ }
743
+
744
+ throw error;
745
+ }
746
+ ```
747
+
748
+ Common first-run errors:
749
+
750
+ | Error | Meaning | Next step |
751
+ | --- | --- | --- |
752
+ | `agent must be a string` | The online delegation API currently expects `agent` to be a string id. | Pass `agent: "memory-agent"` rather than an object. |
753
+ | `eak.delegation.user_not_bound` | The `userId` is not in the GenAuth userpool bound to this Qoni credential. | Use a real user id from `currentUser` or from the same bound userpool. |
754
+ | `eak.genauth.userpool_binding_missing` | The AK/SK is not bound to a GenAuth userpool, so `genauth.users.*` cannot obtain a management token. | Bind the Qoni credential to the target GenAuth userpool before calling user management APIs. |
755
+ | `eak.genauth.userpool_owner_missing` | The bound GenAuth userpool has no resolvable owner user for management-token signing. | Fix the GenAuth userpool owner data or binding. |
756
+ | `eak.token_exchange.upstream_failed` with `unauthorized_client` or `grant_type is not enabled` | Delegation succeeded, but the managed delegation app is missing token-exchange grant support or has drifted from the tenant binding. | Repair the managed delegation app binding before calling GUMem or WebAgent product capabilities. |
757
+ | `delegation.required` | A GUMem/WebAgent call was made without `token`. | Pass `delegation.data.token` to the product namespace call. |
758
+ | `direct delegation token deprecated` | Application code is calling a product service directly with an Qoni delegation token. | Call the product through the SDK so token exchange is handled internally. |
759
+
760
+ ## Security Notes
761
+
762
+ - Keep Qoni AK/SK on trusted servers only.
763
+ - Do not expose delegation tokens to untrusted clients.
764
+ - Request the smallest useful scope set for each Agent action.
765
+ - Prefer `interactive` mode for browser control, site login, long-running monitors, and sensitive artifacts.
766
+ - Store `auditId`, `requestId`, and `traceId` with product logs when later investigation matters.
767
+ - Rotate AK/SK through Qoni Console if a credential might have been exposed.
768
+
769
+ ## License
770
+
771
+ MIT