@qoniai/qoni 0.8.0 → 0.9.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
@@ -5,7 +5,7 @@
5
5
  [![Package manager](https://img.shields.io/badge/package%20manager-pnpm-f69220)](https://pnpm.io/)
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
7
 
8
- Unified Node.js SDK for Qoni Agent delegation, GenAuth user management, GUMem memory, WebAgent automation, web search, and monitoring.
8
+ Unified Node.js SDK for Qoni Agent delegation, GenAuth management, GUMem memory, WebAgent automation, web search, and monitoring.
9
9
 
10
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
11
 
@@ -125,6 +125,21 @@ await qoni.genauth.users.update({
125
125
  // await qoni.genauth.users.deleteBatch({ userIds: [profile.data.userId] });
126
126
  ```
127
127
 
128
+ ### GenAuth Management
129
+
130
+ `qoni.genauth` exposes **591 typed management methods across 35 namespaces**, aligned with [Authing's official Node SDK 4.0.2](https://github.com/Authing/authing-node-sdk). This includes all 496 official JSON management methods plus GenAuth extensions and compatibility names, including positions, public accounts, invitations, metadata, approvals and risk lists. Calls share the Qoni AK/SK management-token exchange and support cancellation. Deployment features and permissions still determine endpoint availability.
131
+
132
+ ```ts
133
+ await qoni.genauth.groups.create({ code: "engineering", name: "Engineering" });
134
+ await qoni.genauth.groups.addMembers({ code: "engineering", userIds: ["USER_ID"] });
135
+ const roles = await qoni.genauth.management.listRoles({ namespace: "project", page: 1, limit: 20 });
136
+ for await (const user of qoni.genauth.users.iterate({ limit: 50 })) {
137
+ console.log(user.userId);
138
+ }
139
+ ```
140
+
141
+ [Integration guide](./docs/genauth-management.md) · [Complete API catalog](./docs/genauth-management-api.md). Roles belong to an existing permission namespace. GenAuth employment and departure rules still apply when deleting users.
142
+
128
143
  ### Runtime Product Delegation
129
144
 
130
145
  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.
@@ -151,7 +166,7 @@ const run = await qoni.doAnything.run({
151
166
 
152
167
  const result = await run.wait({
153
168
  onScreenshot: (img, i) => fs.writeFileSync(`step-${i}.jpg`, img.bytes),
154
- onInteraction: (i) => { // login / clarify / confirm / take-control
169
+ onInteraction: (i) => { // site_login / take_control / ask_user / confirmation
155
170
  if (i.can("confirm_signed_in")) return i.confirmSignedIn();
156
171
  if (i.can("confirm")) return i.confirm();
157
172
  },
@@ -286,18 +301,27 @@ const grant = await qoni.delegateToken({
286
301
  scopes: ["gumem.memory:read", "webagent.do_anything:manage"],
287
302
  });
288
303
 
304
+ // The callback will not carry grantId: save it with your business state, keyed by grantState.
305
+ await savePendingGrant(grant.data.grantState, {
306
+ grantId: grant.data.grantId,
307
+ businessState: "business-csrf-state",
308
+ });
289
309
  redirectUserTo(grant.data.authorizationUrl);
290
310
  ```
291
311
 
292
- 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.
312
+ 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`. The callback does **not** carry `grantId`, so look up the record you saved by `grant_state`. The browser never receives a delegation token.
293
313
 
294
314
  Your callback handler runs on the server and exchanges the callback fields with AK/SK:
295
315
 
296
316
  ```ts
317
+ const pending = await loadPendingGrant(String(req.query.grant_state));
318
+ // Your business state comes back unchanged; reject forged or mixed-up callbacks.
319
+ if (!pending || req.query.state !== pending.businessState) throw new Error("Invalid delegation callback");
320
+
297
321
  const completed = await qoni.completeDelegateToken({
298
- grantId: String(req.query.grantId ?? req.query.grant_id),
322
+ grantId: pending.grantId,
299
323
  code: String(req.query.code),
300
- state: String(req.query.state),
324
+ state: String(req.query.grant_state), // grant_state, not your business state
301
325
  });
302
326
 
303
327
  await qoni.gumem.recall({
@@ -308,7 +332,7 @@ await qoni.gumem.recall({
308
332
 
309
333
  `completeDelegateToken` is the recommended server callback helper. Root `qoni.delegateAgent` and `qoni.completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
310
334
 
311
- ## Original delegation style (0.8.0, unreleased)
335
+ ## Original delegation style (0.8.0+)
312
336
 
313
337
  ```ts
314
338
  const grant = await qoni.genauth.delegateAgent({
@@ -329,8 +353,8 @@ the authorization request directly; use `completeDelegateToken()` in the server
329
353
  callback after consent. `agentKey` aliases `agent`; both labels must agree when
330
354
  supplied together. Top-level `userId` retains precedence over `user.id`.
331
355
  Expiry accepts integer seconds or whole-number
332
- `s/m/h/d` durations, between 60 and 86400 seconds. Until published, use the 0.8.0
333
- source build for these additions.
356
+ `s/m/h/d` durations, between 60 and 86400 seconds. These additions are available
357
+ in the published `@qoniai/qoni@0.8.0` release.
334
358
 
335
359
  Legacy `mailbox.*`, `web.search`, and `web.extract` scopes, and `deniedScopes`, are unsupported and rejected.
336
360
  They are never expanded into broader product permissions. Product scopes do not
@@ -348,7 +372,9 @@ await qoni.delegateToken({
348
372
  });
349
373
  ```
350
374
 
351
- 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.
375
+ 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 — every candidate when the short form is ambiguous (`profile:read` → `gumem.profile:read` or `user.profile:read`).
376
+
377
+ The site-login, user Profile, payment and Agent mailbox scopes (`QoniScopes.SITE_LOGIN_*`, `USER_PROFILE_READ`, `USER_PROFILE_WRITE`, `USER_PAYMENT_USE`, `AGENT_MAIL_READ`, `AGENT_MAIL_SEND`) belong to no product: request them explicitly in `scopes`, and allow them in the AccessKey policy. The user Profile, payment and Agent mailbox constants only name the scopes; requesting them does not by itself enable Profile, payment or mailbox access in a run.
352
378
 
353
379
  Use explicit scope strings when the requested permission boundary should be visible in code.
354
380
 
@@ -360,6 +386,10 @@ Use explicit scope strings when the requested permission boundary should be visi
360
386
  | Search public web | `webagent.web_search:read`, `webagent.web_search:manage` | Agent can search public pages and read search results. |
361
387
  | 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. |
362
388
  | Create a monitor | `webagent.track:read`, `webagent.track:manage` | Agent can configure monitors, read events, run checks, and stop monitors. |
389
+ | Sign in to sites during a run | `webagent.site_login:request`, `webagent.site_login:confirm` | The user signs in through a live browser; the login is saved for later tasks. |
390
+ | Use the user's Profile | `user.profile:read`, `user.profile:write` | Agent can read the user's saved details (name, phone, shipping address, card brand and last 4) and save new ones the user provides. |
391
+ | Pay with the user's saved card | `user.payment:use` | After the user approves a payment, the saved card is filled in for them. |
392
+ | Use the Agent's own mailbox | `agent.mail:read`, `agent.mail:send` | Agent can read mail sent to its own address (such as verification codes) and send mail from it. |
363
393
 
364
394
  Silent mode is usually appropriate for low-risk actions such as reading current-user memory, writing a confirmed summary, or searching public web pages.
365
395
 
@@ -471,46 +501,104 @@ const followUp = await qoni.deepResearch.run({
471
501
  });
472
502
  ```
473
503
 
504
+ `doAnything.api.readArtifacts()` and the exported `DoAnythingSnapshot`,
505
+ `SnapshotImage`, and `SnapshotAction` types are deprecated. There has never been
506
+ a backend endpoint/source for that structured snapshot contract. The method
507
+ raises local `QoniUnsupportedError` (`code: "unsupported.operation"`) with zero
508
+ HTTP. Use `qoni.doAnything.artifacts({ token, runId })` / `api.listArtifacts()` for files, including
509
+ `artifact.content()` or `api.artifactDownloadUrl()` for downloads. Use
510
+ `capture: { screenshots: true }` and screenshot events for images. A text file
511
+ artifact is not a snapshot; the real file APIs are unchanged.
512
+
474
513
  ### Track
475
514
 
515
+ Track uses `/track/tracks`. `prompt` maps to `instructions`; include watched URLs
516
+ in that text. An explicit schedule is required: an integer interval of
517
+ 600–2592000 seconds, or a daily time with a time zone. A daily check starts
518
+ 0–30 minutes before the target time, rather than exactly at that time.
519
+
476
520
  ```ts
521
+ import type { MonitorSchedule } from "@qoniai/qoni";
522
+
477
523
  const monitor = await qoni.track.create({
478
524
  token,
479
- prompt: "Monitor the price of Bitcoin every minute.",
480
- });
525
+ prompt: "Watch https://example.com/policy and report changes.",
526
+ title: "Policy changes",
527
+ schedule: { kind: "interval", intervalSeconds: 3600 },
528
+ }); // create immediately starts a first_look check
529
+ const initial = await monitor.get();
530
+ const firstRunId = initial.lastTaskId; // capture this original ID once
531
+ if (typeof firstRunId !== "string" || !firstRunId) throw new Error("Missing initial lastTaskId");
532
+ const sameMonitor = await qoni.track.attach(monitor.id, { token });
481
533
 
482
- // A new monitor starts in `pending_clarification` while the backend aligns
483
- // the intent into a concrete spec, then flips to `active` (usually a few
484
- // seconds; it may emit an `interaction` first if the intent is ambiguous).
485
- // runNow()/refine() require `active`, so wait for it before acting:
486
- for await (const event of monitor.events()) {
487
- if (event.type === "interaction") {
488
- const i = monitor.interactionHandle(event.data); // e.g. an intent clarification
489
- if (i.can("confirm")) await i.confirm();
490
- continue;
491
- }
492
- const { status } = await monitor.get();
493
- if (status === "active") break;
534
+ async function waitForCheck(runId: string) {
535
+ const deadline = Date.now() + 300_000;
536
+ do {
537
+ const check = await monitor.run(runId); // exact ID in the recent 50 checks
538
+ if (["done", "failed", "expired", "canceled"].includes(String(check.status))) {
539
+ if (check.status !== "done") throw new Error(`Check ${runId} ${String(check.status)}: ${String(check.failure ?? "")}`);
540
+ return check;
541
+ }
542
+ await new Promise(resolve => setTimeout(resolve, Math.min(1000, Math.max(0, deadline - Date.now()))));
543
+ } while (Date.now() < deadline);
544
+ throw new Error(`Check ${runId} timed out`);
494
545
  }
495
546
 
496
- await monitor.runNow(); // trigger an immediate tick
497
- const ticks = await monitor.runs({ limit: 10 }); // read-only tick run views
498
-
499
- // Change the definition with refine() (any subset of: schedule / targetUrls /
500
- // extractionSchema / triggerDsl / stopConditionDsl / notifyChannel). `schedule`
501
- // is { kind: "cron", expr } or { kind: "interval", intervalSeconds } (same for
502
- // track.create()). Extraction field names and extracted values keep their
503
- // original keys in get()/run() reads. The
504
- // natural-language intent isn't refinable — create a new monitor to change it:
505
- await monitor.refine({ schedule: { kind: "cron", expr: "0 9 * * *" } });
506
- await monitor.pause(); // stop scheduled ticks without deleting
507
- await monitor.resume();
508
- await monitor.delete();
509
-
510
- // Reconnect later from a stored id:
511
- const sameMonitor = await qoni.track.attach(monitor.id, { token });
547
+ try {
548
+ await monitor.pause(); // pause future scheduling; the first look continues
549
+ await waitForCheck(firstRunId);
550
+ await monitor.refine({ schedule: initial.schedule as MonitorSchedule }); // our known, validated input roundtrips
551
+ await monitor.refine({ schedule: { kind: "daily", at: "09:00", tz: "Asia/Singapore" } });
552
+ const task = await monitor.runNow(); // one explicit manual check while scheduling is paused
553
+ const check = await waitForCheck(task.runId); // 202 acceptance is not completion
554
+ const checks = await monitor.runs({ limit: 10, offset: 0 });
555
+ await monitor.resume();
556
+ } finally {
557
+ await monitor.delete(); // soft-delete; any in-flight check continues
558
+ }
512
559
  ```
513
560
 
561
+ Create immediately starts a `first_look` check. Capture its initial `lastTaskId`
562
+ and wait for that same check before one explicit `runNow()`. A concurrent run
563
+ can still cause 409; the SDK surfaces it and never retries. Pause stops future
564
+ scheduled checks and keeps any started check running.
565
+
566
+ Track check `status` is `running`, `done`, `failed`, `expired`, or `canceled`;
567
+ `done` is the successful terminal value. It is kept verbatim and differs from
568
+ one-shot `RunResult.status: "succeeded"`. Known wire schedule shapes are read
569
+ without revalidating server values against SDK write bounds or Intl time zones;
570
+ unknown/incomplete shapes remain verbatim. Writing a schedule stays strict, so
571
+ validate/choose supported input rather than casting every server value back.
572
+
573
+ `runs()` retrieves the backend's newest 50 checks and applies `limit`/`offset`
574
+ locally. `run(id)` searches that same bounded window; a missing ID raises
575
+ `QoniError` with local `code: "not_found"` (no HTTP status) and explains the range. A missing Track itself is the backend 404 `track_not_found`. Checks expose the
576
+ actual status, outcome, headline, answer and failure; no extraction, diffs or
577
+ snapshot IDs are synthesized. `runNow()` can return the backend's 409
578
+ `task_in_progress` while an earlier task is working. It returns a task reference
579
+ on 202; read checks to observe completion.
580
+
581
+ | Retained surface | Current behavior |
582
+ | --- | --- |
583
+ | `track.create`, `track.attach`, `MonitorHandle.get` | Create/read the real Track resource; get converts known wire schedules; unknown shapes stay verbatim. |
584
+ | `MonitorHandle.pause`, `resume` | PATCH `status: paused` / `active`; pause keeps an in-flight check running. |
585
+ | `MonitorHandle.refine` | PATCH title/schedule only; changing prompt/instructions/intent requires a new track. |
586
+ | `MonitorHandle.runNow` | POST `run_now`; returns the actual task reference. |
587
+ | `MonitorHandle.runs`, `run` | Read recent checks with Track read scope only; no DoAnything fallback. |
588
+ | `MonitorHandle.delete` | DELETE 204; soft deletion stops scheduling and does not cancel running work. |
589
+ | `track.api.createMonitor`, `getMonitor` | Historical names retained; use real `instructions`, `title`, and wire schedule `{ every_seconds: N }` or `{ daily_at: "HH:MM", tz }`. |
590
+ | `track.api.updateMonitor` | PATCH real title/instructions/schedule/status fields; wire API permits instructions, while high-level refine does not. |
591
+ | `track.api.runNow`, `deleteMonitor` | Real `run_now` / DELETE; raw task reference / empty 204. |
592
+ | `track.api.listRuns`, `getRun` | Raw checks, local limit/offset, exact lookup within recent 50. |
593
+ | `MonitorHandle.events`, `interactionHandle`, `track.api.events`, `intervene` | Deprecated; no backend endpoint. Local `QoniUnsupportedError`, zero HTTP or token exchange. |
594
+ | `targetUrls`, `extractionSchema`, `triggerDsl`, `stopConditionDsl`, `notifyChannel` (including snake_case aliases), legacy `url` | No backend equivalent; rejected locally with `QoniUnsupportedError`. Put URLs in the prompt explicitly. |
595
+ | Cron schedule | Deprecated and rejected locally with a daily alternative. Arbitrary cron is never silently converted. |
596
+
597
+ A missing schedule, malformed input, invalid interval/time/zone, or invalid
598
+ pagination raises `QoniValidationError` before HTTP. `MonitorSchedule` retains
599
+ the deprecated cron branch; optional undefined fields retain omission behavior, while specified unsupported fields fail; historical event and option types remain exported
600
+ for source compatibility. Unsupported methods are not successful capability tests.
601
+
514
602
  ### GenAuth User Context and Management
515
603
 
516
604
  ```ts
@@ -617,7 +705,7 @@ interface RunResult {
617
705
  runId: string;
618
706
  status: "succeeded" | "failed" | "canceled"; // delivery status of the run
619
707
  output?: unknown; // final answer / payload (shape is task-specific)
620
- artifacts: Artifact[]; // deliverables — deepResearch reports; [] for the others
708
+ artifacts: Artifact[]; // persisted DoAnything files / deepResearch reports
621
709
  isTaskSuccessful?: boolean; // did the task achieve its goal (distinct from `status`)
622
710
  terminalReason?: string;
623
711
  raw: JsonObject; // full wire envelope — cost, step counts, token usage
@@ -630,95 +718,127 @@ is the typed payload for that call (e.g. `genauth.users.list` → a paged user
630
718
  list on `.data`). The exact `T` shapes live in the bundled `.d.ts` and follow
631
719
  the backend contract.
632
720
 
633
- `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).
721
+ `MonitorHandle` (Track) provides `id`, `get()`, `pause()`, `resume()`, `refine(patch)`, `runNow()`, `runs()`, `run(runId)`, and `delete()`. Its deprecated `events()` and `interactionHandle()` methods fail locally; see Track above for the complete mapping.
634
722
 
635
723
  ### Interactions (HITL)
636
724
 
637
- When a run needs the user — sign in to a site, answer a clarification, approve a
638
- plan, take over the browser, or wait — it emits an `interaction` event whose
639
- `event.data` is a typed `Interaction` (discriminated by `type`: `site_login` /
640
- `clarification` / `confirmation` / `take_control` / `wait`). Wrap it in a handle
641
- and call the action methods; each posts to the endpoint the backend declared in
642
- the interaction's `actions[]`, so you never hard-code a route:
643
-
644
- ```ts
645
- for await (const event of run.events()) {
646
- if (event.type === "interaction") {
647
- const i = run.interactionHandle(event.data); // event.data is the Interaction
648
- if (i.type === "clarification" && i.can("answer")) await i.answer("the blue button");
649
- else if (i.can("confirm")) await i.confirm();
650
- else if (i.can("confirm_signed_in")) await i.confirmSignedIn();
651
- else if (i.can("release_control")) await i.releaseControl();
652
- }
653
- }
654
- ```
725
+ When a run needs the user, it pauses and emits an `interaction` event whose
726
+ `event.data` is a typed `Interaction`. Interactions are typed by what the user
727
+ has to do, never by business, so one handler covers every task:
655
728
 
656
- Methods: `answer(text)`, `skip()`, `confirm()`, `reject()`, `openLogin({ siteId?, profileId? })`,
657
- `confirmSignedIn()`, `connectControl()`, `refreshControl()`, `releaseControl()`,
658
- `retry()`, `switchProfile()`. `i.can(kind)` reports whether the interaction
659
- currently offers that action; calling a method the backend did NOT declare
660
- throws (so a "recovery" button only exists when it actually works).
729
+ | `type` | The user… | Methods |
730
+ | --- | --- | --- |
731
+ | `site_login` | signs in to a site in the live browser | `openLogin()`, `confirmSignedIn()` |
732
+ | `take_control` | operates the browser by hand (captcha, 2FA, …) | `connectControl()`, `refreshControl()`, `releaseControl()` |
733
+ | `ask_user` | answers a question shaped by `answerType` / `options` | `answer()`, `skip()` |
734
+ | `confirmation` | allows or rejects an action | `confirm()`, `reject()` |
735
+ | `wait` | nothing — the system is waiting (rate limit, external event) | `retry()` |
661
736
 
662
- `openLogin()` starts an interactive login session for a `site_login`
663
- interaction of a run. Pass `siteId` (one of
664
- `i.interaction.payload.sites[].siteId`), or omit it when exactly one site is
665
- listed. Show the returned `liveUrl` to the user; after they sign in, persist the
666
- login with `login.confirm()`, then tell the agent with `confirmSignedIn()`:
737
+ `Interaction` is a union distributed over `type`, so `switch (request.type)`
738
+ narrows `request.payload` in each branch. Each method posts to the endpoint the
739
+ backend declared in the interaction's `actions[]`, so you never hard-code a route:
667
740
 
668
741
  ```ts
669
- import { QoniError } from "@qoniai/qoni";
742
+ import type { InteractionHandle } from "@qoniai/qoni";
670
743
 
671
- const login = await i.openLogin(); // { profileId, siteId, sessionId, liveUrl }
672
- showToUser(login.liveUrl); // the user signs in there
673
- await waitForUserToFinish();
674
- let result;
675
- try {
676
- result = await login.confirm(); // ends the session; saves the login if found
677
- } catch (error) {
678
- // 409: no pending login for this profile (session already ended, or profile gone).
679
- if (!(error instanceof QoniError && error.code === "no_pending_login")) throw error;
680
- }
681
- if (!result || result.active || result.probeResult === "no_session") {
682
- await i.confirmSignedIn(); // the agent rechecks; it asks again if still signed out
683
- } else {
684
- // The user was not signed in, and confirm() closed that session's browser.
685
- const retry = await i.openLogin({ siteId: login.siteId, profileId: login.profileId });
686
- showToUser(retry.liveUrl); // a new session for the same profile
744
+ const handled = new Set<string>();
745
+
746
+ async function handleInteraction(handle: InteractionHandle) {
747
+ const request = handle.interaction;
748
+ // Creation, status updates and replays all call back: act once per pending id.
749
+ if (request.status !== "pending" || handled.has(request.id)) return;
750
+ handled.add(request.id);
751
+
752
+ switch (request.type) {
753
+ case "site_login": {
754
+ const login = await handle.openLogin(); // { liveUrl, ... } of a live login browser
755
+ await showToUser(login.liveUrl); // resolves when the user clicks "I'm signed in"
756
+ await handle.confirmSignedIn(); // saves the login, then the agent rechecks and continues
757
+ break;
758
+ }
759
+ case "take_control": {
760
+ // connectControl() resolves to the signed control URL, when the backend offers it
761
+ const control = handle.can("connect_control") ? await handle.connectControl() : undefined;
762
+ await showToUser(control?.liveUrl ?? request.payload.liveUrl);
763
+ await handle.releaseControl(); // hand the browser back
764
+ break;
765
+ }
766
+ case "ask_user": // payload: { question, answerType, options? }
767
+ await handle.answer(await askUser(request.payload));
768
+ break;
769
+ case "confirmation": // payload: { summary }
770
+ if (await approve(request.payload.summary)) await handle.confirm();
771
+ else await handle.reject();
772
+ break;
773
+ }
687
774
  }
775
+
776
+ const result = await run.wait({ onInteraction: handleInteraction });
688
777
  ```
689
778
 
690
- `probeResult: "no_session"` means the backend no longer holds the login session —
691
- it may already have saved the login on its own once it detected the sign-in, or
692
- it restarted. Some browser providers instead reject `confirm()` with a
693
- `QoniError` coded `no_pending_login` (HTTP 409), which the backend also uses when
694
- the profile no longer exists for this user; the example handles it the same way,
695
- because the agent rechecks and asks again if the site is still signed out. Delegate
779
+ Interactions arrive one at a time: the run waits until the current one is
780
+ handled. The same interaction calls back again on every status change and on
781
+ reconnect replays, always with the same `id` and `type` — key your UI cards by
782
+ `request.id`. An update with content of its own replaces the card (a take-over
783
+ that became `active` carries its new `liveUrl`); when the backend settles an
784
+ interaction with an empty shell, the card keeps its original `payload`. A
785
+ `take_control` that arrives already `active` was started by the user, for
786
+ example from Console; show it if your UI offers control there. Methods: `answer(value)`, `skip()`, `confirm()`,
787
+ `reject()`, `openLogin({ siteId?, profileId? })`, `confirmSignedIn()`,
788
+ `connectControl()`, `refreshControl()`, `releaseControl()`, `retry()`,
789
+ `switchProfile()`. `connectControl()` and `refreshControl()` resolve to
790
+ `{ liveUrl, expiresAt?, status?, raw }`: the signed URL of the browser the user
791
+ now controls. `handle.can(kind)` reports whether the interaction currently
792
+ offers that action; calling a method the backend did NOT declare throws (so a
793
+ "recovery" button only exists when it actually works).
794
+
795
+ **`ask_user` answers are structured.** `answer()` takes the value matching
796
+ `payload.answerType` — `text` → string, `number` → number, `boolean` → boolean,
797
+ `single_choice` → one `options[].value`, `multiple_choice` → an array of option
798
+ values — and throws `QoniValidationError` before sending anything that does not
799
+ fit. The wire still calls this type `clarification`; the SDK presents it as
800
+ `ask_user` (choices arrive as `{ value, label }` options).
801
+
802
+ **Sign-in takes two calls.** `openLogin()` opens a live login browser for the
803
+ `site_login` interaction (pass `siteId` from `payload.sites[].siteId`, or omit it
804
+ when exactly one site is listed). Once the user is signed in, `confirmSignedIn()`
805
+ saves the login so later tasks reuse it, then tells the agent to recheck the site
806
+ and continue; if the user turns out not to be signed in, the agent raises a new
807
+ `site_login`. It saves once and rechecks once per login session, however many
808
+ times it is called. If the backend could not save the login yet, it throws a
809
+ retryable `QoniError` coded `site_login.save_failed` without telling the agent to
810
+ go on; call it again. The same holds for `site_login.session_replaced`, thrown
811
+ when the login was reopened while it was being saved. Handles from the same `RunHandle` (or `MonitorHandle`)
812
+ share the login session, so `confirmSignedIn()` also works on the handle of a
813
+ later callback; a RunHandle from a new `attach()` does not. Delegate
696
814
  `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, and allow
697
- both in the AccessKey policy.
698
-
699
- The discriminant strings used above are also exported as constants so you can
700
- avoid bare literals: `InteractionTypes` (`site_login` / `clarification` /
701
- `confirmation` / `take_control` / `wait`), `InteractionStatuses`, and
702
- `ActionKinds` (the `i.can(...)` action names). Authorization scope strings have
703
- the same treatment — `QoniScopes` (every individual scope), `QoniProductScopes`
704
- (the per-product `read`+`manage` pairs the `products` sugar expands to), and
705
- `QoniScopeBundles` (curated bundles). Use them in place of hand-typed strings:
815
+ both in the AccessKey policy. `openLogin()`'s result still offers `confirm()` for
816
+ callers that save the login themselves; it is deprecated.
817
+
818
+ The discriminant strings are also exported as constants so you can avoid bare
819
+ literals: `InteractionTypes` (`SiteLogin` / `TakeControl` / `AskUser` /
820
+ `Confirmation` / `Wait`; the deprecated `Clarification` now equals `AskUser`),
821
+ `InteractionStatuses`, and `ActionKinds` (the `handle.can(...)` action names).
822
+ Authorization scope strings have the same treatment — `QoniScopes` (every
823
+ individual scope), `QoniProductScopes` (the per-product `read`+`manage` pairs the
824
+ `products` sugar expands to), and `QoniScopeBundles` (curated bundles). Use them in
825
+ place of hand-typed strings:
706
826
 
707
827
  ```ts
708
828
  import { InteractionTypes, ActionKinds, QoniScopes } from "@qoniai/qoni";
709
829
 
710
- if (i.type === InteractionTypes.Clarification && i.can(ActionKinds.answer)) {
711
- await i.answer("the blue button");
830
+ if (handle.type === InteractionTypes.AskUser && handle.can(ActionKinds.answer)) {
831
+ await handle.answer("the blue button");
712
832
  }
713
833
  ```
714
834
 
715
835
  ### Wire-Level Escape Hatch
716
836
 
717
- 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.
837
+ Supported 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.
718
838
 
719
839
  `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).
720
840
 
721
- Browser Use scopes are reserved until their product runtime SDK methods are exported. Site Login scopes (`QoniScopes.SITE_LOGIN_REQUEST`, `QoniScopes.SITE_LOGIN_CONFIRM`) back `InteractionHandle.openLogin()` and `confirm()` on run interactions.
841
+ Browser Use scopes are reserved until their product runtime SDK methods are exported. Site Login scopes (`QoniScopes.SITE_LOGIN_REQUEST`, `QoniScopes.SITE_LOGIN_CONFIRM`) back `InteractionHandle.openLogin()` and `confirmSignedIn()` on run interactions.
722
842
 
723
843
  ### Response Shape
724
844
 
@@ -747,9 +867,9 @@ that type:
747
867
  // "phase" string — the Deep Research phase name
748
868
  // "sectionReady" string — the Deep Research section title
749
869
  // "resultsReady" number — Web Search count of unique results
750
- // "monitorCreated" string — Track monitor id
751
- // "triggered" string — Track change summary
752
- // "checkCompleted" boolean — Track: did the scheduled check find a change
870
+ // "monitorCreated" string — deprecated legacy monitor wire type
871
+ // "triggered" string — deprecated legacy monitor wire type
872
+ // "checkCompleted" boolean — deprecated legacy monitor wire type
753
873
  // RICH events — event.data is a small object:
754
874
  // "message" { text, role }
755
875
  // "interaction" the typed Interaction object (HITL) — see Interactions
@@ -767,7 +887,7 @@ Each product handle is typed to its own subset:
767
887
  | Do Anything | core: `progress` / `message` / `interaction` / `screenshot` / `browserLiveUrlChanged` / `done` |
768
888
  | Web Search | core + `resultsReady` |
769
889
  | Deep Research | core + `phase` / `sectionReady` (the outline gate arrives as an `interaction`) |
770
- | Track (`MonitorHandle`) | core + `monitorCreated` / `triggered` / `checkCompleted` |
890
+ | Track (`MonitorHandle`) | No SSE endpoint; historical MonitorEvent types are deprecated. Read checks with runs()/run(). |
771
891
 
772
892
  For a live browser preview, listen for `browserLiveUrlChanged` and show
773
893
  `event.data.liveUrl` in your viewer. A run can switch browser tabs after its
@@ -799,6 +919,7 @@ All error classes are exported and can be matched with `instanceof`:
799
919
 
800
920
  | Class | Fires when | Typical recovery |
801
921
  | --- | --- | --- |
922
+ | `QoniUnsupportedError` | Retained surface has no backend operation (code: `unsupported.operation`); zero HTTP. | Use the alternative named in the error. |
802
923
  | `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. |
803
924
  | `QoniAuthError` | AK/SK signing rejected by Qoni (bad/rotated credentials). | Check `accessKey` / `secretKey`. |
804
925
  | `QoniPermissionDeniedError` | The AccessKey or delegation token lacks a required scope (403). | Check the AccessKey permission policy and request the missing scope with `delegateToken`. |
@@ -889,3 +1010,23 @@ const artifacts = await client.doAnything.artifacts({ token, runId: run.id });
889
1010
  ```
890
1011
 
891
1012
  Wire methods: `client.doAnything.api.listArtifacts(...)` and `artifactDownloadUrl(...)`.
1013
+
1014
+ ## Agent Mail and Agent Phone
1015
+
1016
+ The local GenAuth communication extension provides `qoni.genauth.agentMail` and `qoni.genauth.agentPhone`, including runtime and administrator APIs. Runtime requires native GenAuth credentials via `genauthCommunicationCredentials`; administration reuses Qoni token exchange. See [the communication guide](docs/genauth-communication.md) and [method index](docs/genauth-communication-api.md).
1017
+
1018
+ ## Local Agent integration extension
1019
+
1020
+ The integration branch adds `qoni.genauth.auth.createLoginUrl` / `exchangeLoginCode` and
1021
+ `qoni.genauth.agents.create` / `get` / `bind` / `bindUser`, alongside the existing
1022
+ management, Agent Mail and Agent Phone APIs. These additions are not an npm release.
1023
+ The hosted-login callback must validate the App's stored `state` before exchanging
1024
+ the one-use code. Personal mailbox delegation requires a formal IAM `agentId`; an
1025
+ audit label does not prove mailbox ownership. A Personal creation response may still
1026
+ be pending; an unknown result must be reconciled using its original provisioning
1027
+ reference or idempotency key, without starting another creation.
1028
+
1029
+ The Personal Quickstart email creation flow is separate from the unified Agent
1030
+ management model, where identity, email and phone are optional capabilities and an
1031
+ Agent may have multiple email addresses and phone numbers. Personal binding does
1032
+ not issue credentials or enable IAM-token access to the native Mail/Phone APIs.