@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/CHANGELOG.md +164 -2
- package/README.md +250 -109
- package/README.zh-CN.md +194 -78
- package/dist/index.cjs +14166 -519
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25619 -371
- package/dist/index.d.ts +25619 -371
- package/dist/index.js +14164 -519
- package/dist/index.js.map +1 -1
- package/docs/authing-sdk-LICENSE.txt +21 -0
- package/docs/genauth-communication.md +1410 -0
- package/docs/genauth-management-api.md +597 -0
- package/docs/genauth-management.md +285 -0
- package/package.json +14 -5
- package/skills/qoni-sdk/SKILL.md +156 -23
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://pnpm.io/)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
Unified Node.js SDK for Qoni Agent delegation, GenAuth
|
|
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) => { //
|
|
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
|
|
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:
|
|
322
|
+
grantId: pending.grantId,
|
|
299
323
|
code: String(req.query.code),
|
|
300
|
-
state: String(req.query.
|
|
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
|
|
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.
|
|
333
|
-
|
|
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: "
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
await monitor.
|
|
507
|
-
|
|
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[]; //
|
|
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)
|
|
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
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
`
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
`
|
|
663
|
-
|
|
664
|
-
`
|
|
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 {
|
|
742
|
+
import type { InteractionHandle } from "@qoniai/qoni";
|
|
670
743
|
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
`
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
`
|
|
702
|
-
`
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
`
|
|
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 (
|
|
711
|
-
await
|
|
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
|
-
|
|
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 `
|
|
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 —
|
|
751
|
-
// "triggered" string —
|
|
752
|
-
// "checkCompleted" boolean —
|
|
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`) |
|
|
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.
|