@qoniai/qoni 0.7.0 → 0.9.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/CHANGELOG.md +126 -3
- package/README.md +146 -71
- package/README.zh-CN.md +98 -44
- package/dist/index.cjs +395 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -44
- package/dist/index.d.ts +176 -44
- package/dist/index.js +395 -61
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/qoni-sdk/SKILL.md +42 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,128 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-10-07
|
|
4
|
+
|
|
5
|
+
DoAnything interactions are now typed by what the user has to do, never by
|
|
6
|
+
business: `site_login`, `take_control`, `ask_user`, `confirmation`, plus the
|
|
7
|
+
system `wait`.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `Interaction` is a union distributed over `type`: `switch (interaction.type)`
|
|
12
|
+
narrows `payload` in each branch, including `handle.interaction` inside
|
|
13
|
+
`onInteraction`. `Interaction<"site_login">` still picks one member.
|
|
14
|
+
`TypedInteraction` names the union. Because it is now a type alias, not an
|
|
15
|
+
interface, `interface X extends Interaction` no longer compiles (TS2312):
|
|
16
|
+
use `type X = Interaction & { ... }`. Code that builds an `Interaction` from a
|
|
17
|
+
separate `type` and `payload` must narrow them together or cast the object.
|
|
18
|
+
- The wire's `clarification` is presented as `ask_user`, with payload
|
|
19
|
+
`{ question, answerType, options? }`. `answerType` is `text`, `number`,
|
|
20
|
+
`boolean`, `single_choice` or `multiple_choice`, and options are
|
|
21
|
+
`{ value, label }`. A clarification with choices becomes `single_choice`; one
|
|
22
|
+
without choices is `text`. A native `ask_user` keeps the `answer_type` it
|
|
23
|
+
sends, and every string option value is kept, including `""`.
|
|
24
|
+
Migration: compare `type` with `"ask_user"` instead of `"clarification"`
|
|
25
|
+
(TypeScript no longer compiles the old comparison); read
|
|
26
|
+
`payload.answerType` / `payload.options` instead of `payload.inputType`
|
|
27
|
+
(removed) or the answer action's `inputSchema` enum.
|
|
28
|
+
`InteractionTypes.Clarification` is deprecated and now equals
|
|
29
|
+
`InteractionTypes.AskUser`: it still matches interactions the SDK hands out,
|
|
30
|
+
but not a raw wire `type` or a `"clarification"` string you stored.
|
|
31
|
+
`ClarificationPayload` is a deprecated alias of `AskUserPayload`, which
|
|
32
|
+
requires `answerType`.
|
|
33
|
+
- `InteractionHandle.answer(value)` accepts the structured value its
|
|
34
|
+
`answerType` names (string, number, boolean, one option value, or an array of
|
|
35
|
+
option values) and throws `QoniValidationError` before sending a value that
|
|
36
|
+
does not fit.
|
|
37
|
+
- `InteractionHandle.confirmSignedIn()` saves the login opened by `openLogin()`
|
|
38
|
+
on the same interaction (`POST /profiles/{profileId}/confirm`, scope
|
|
39
|
+
`webagent.site_login:confirm`), then tells the agent to recheck. Sign-in is
|
|
40
|
+
`openLogin()` → `confirmSignedIn()`:
|
|
41
|
+
- It saves once and rechecks once per login session, however many handles
|
|
42
|
+
or concurrent calls ask; the deprecated `OpenLoginResult.confirm()` shares
|
|
43
|
+
the same save.
|
|
44
|
+
- A user who is not signed in yet, or 409 `no_pending_login`, still gets a
|
|
45
|
+
recheck: the agent raises a new `site_login` when needed.
|
|
46
|
+
- When the backend keeps the session but could not save it
|
|
47
|
+
(`probeResult: "release_failed"`), it throws a retryable `QoniError` coded
|
|
48
|
+
`site_login.save_failed` and does not tell the agent to go on; calling it
|
|
49
|
+
again saves again. Any other save failure is thrown as well.
|
|
50
|
+
- When `openLogin()` calls overlap, the latest call that succeeds owns the
|
|
51
|
+
session, whichever response arrives last.
|
|
52
|
+
- Reopening that returns the same backend session shares its save; when a
|
|
53
|
+
newer backend session replaces it, `confirm()` on the old result throws
|
|
54
|
+
`QoniValidationError` instead of saving. If the login is reopened while a
|
|
55
|
+
`confirmSignedIn()` is still saving, that call throws a retryable
|
|
56
|
+
`QoniError` coded `site_login.session_replaced` without telling the agent
|
|
57
|
+
to go on; call it again to save the newer login.
|
|
58
|
+
- Handles from the same `RunHandle` or `MonitorHandle` share the login
|
|
59
|
+
session, so a handle from a later callback saves it too. A RunHandle from
|
|
60
|
+
another `attach()` or process does not.
|
|
61
|
+
- `connectControl()` and `refreshControl()` resolve to a `ControlSession`
|
|
62
|
+
(`{ liveUrl, expiresAt?, status?, raw }`, the signed URL of the browser the
|
|
63
|
+
user now controls) instead of `void`. `await handle.connectControl()` is
|
|
64
|
+
unaffected; code that returns the call where `Promise<void>` is expected,
|
|
65
|
+
such as `const cb: () => Promise<void> = () => handle.connectControl()`, no
|
|
66
|
+
longer compiles (TS2322) — wrap it: `async () => { await handle.connectControl(); }`.
|
|
67
|
+
- `run.events()`, `wait({ onInteraction })` and `monitor.events()` keep an
|
|
68
|
+
interaction's type and content when the backend settles it with an empty
|
|
69
|
+
shell (a resolve labelled `clarification` with an empty question, or a
|
|
70
|
+
`site_login` with one empty site): only `status`, `resolvedAt` and `actions`
|
|
71
|
+
change. A settled update with content of its own, such as an expired
|
|
72
|
+
take-over with its reason, replaces the card as before. Run and monitor
|
|
73
|
+
handles keep about 256 interactions, forgetting settled cards first, and
|
|
74
|
+
logins once they are saved or their interaction ended; a pending card or a
|
|
75
|
+
login still in progress is never dropped.
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- `QoniScopes.USER_PROFILE_READ` (`user.profile:read`), `USER_PROFILE_WRITE`
|
|
80
|
+
(`user.profile:write`), `USER_PAYMENT_USE` (`user.payment:use`),
|
|
81
|
+
`AGENT_MAIL_READ` (`agent.mail:read`) and `AGENT_MAIL_SEND`
|
|
82
|
+
(`agent.mail:send`). They belong to no product bundle: request them
|
|
83
|
+
explicitly, and allow them in the AccessKey policy. The constants only name
|
|
84
|
+
the scopes; requesting them does not by itself enable Profile, payment or
|
|
85
|
+
mailbox access in a run.
|
|
86
|
+
- Exported types `AskUserPayload`, `AskUserOption`, `AskUserAnswer`,
|
|
87
|
+
`AskUserAnswerType`, `InteractionBase`, `TypedInteraction` and
|
|
88
|
+
`ControlSession`.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- A scope written without its service prefix names every matching scope:
|
|
93
|
+
`profile:read` suggests both `gumem.profile:read` and `user.profile:read`.
|
|
94
|
+
|
|
95
|
+
### Deprecated
|
|
96
|
+
|
|
97
|
+
- `user` / `userId` on interactive delegation input: the authorization page
|
|
98
|
+
identifies the signed-in user, and the delegation service ignores them.
|
|
99
|
+
- `DelegateTokenResponse.grantedScopes`: the delegation service does not return
|
|
100
|
+
it. Read `qoni.genauth.introspectDelegationToken({ token }).data.scope`.
|
|
101
|
+
- `OpenLoginResult.confirm()`: `confirmSignedIn()` saves the login itself.
|
|
102
|
+
|
|
103
|
+
## 0.8.0 - 2026-10-04
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
|
|
107
|
+
- `qoni.genauth.delegateAgent(input)` returns the grant directly (`grant.token`),
|
|
108
|
+
or an interactive authorization request, through the existing signed endpoint.
|
|
109
|
+
- `agentKey` is an alias for `agent`. Existing root and `qoni.qoni` entries keep
|
|
110
|
+
their `QoniResponse` envelopes. Conflicting Agent labels are rejected;
|
|
111
|
+
the existing top-level `userId` precedence is unchanged.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
|
|
115
|
+
- Expiry strings such as `"15m"` are converted to integer seconds; valid
|
|
116
|
+
durations are 60–86400 seconds, matching the GenAuth DTO.
|
|
117
|
+
- Unsupported `deniedScopes` is rejected rather than forwarding an unenforced
|
|
118
|
+
deny policy. Legacy `mailbox.*`, `web.search`, and `web.extract` scopes are rejected without mapping to product scopes.
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- Expiry validation now happens locally before signing. Invalid formats and
|
|
123
|
+
values outside 60–86400 seconds throw `QoniValidationError`; numeric seconds
|
|
124
|
+
strings are sent as integers.
|
|
125
|
+
|
|
3
126
|
## 0.7.0 - 2026-10-02
|
|
4
127
|
|
|
5
128
|
### Changed
|
|
@@ -127,7 +250,7 @@ compatible fixes.
|
|
|
127
250
|
- Optional abort signals on product run-detail and artifact-list wire reads;
|
|
128
251
|
existing calls without a signal remain compatible.
|
|
129
252
|
|
|
130
|
-
##
|
|
253
|
+
## Earlier changes
|
|
131
254
|
|
|
132
255
|
### Breaking
|
|
133
256
|
|
|
@@ -136,8 +259,8 @@ compatible fixes.
|
|
|
136
259
|
|
|
137
260
|
### Changed
|
|
138
261
|
|
|
139
|
-
- The default Qoni host is now `https://dashboard.qoni.ai
|
|
140
|
-
|
|
262
|
+
- The default Qoni host is now `https://dashboard.qoni.ai`.
|
|
263
|
+
Callers that never passed an explicit `host`
|
|
141
264
|
reach a different control plane after upgrading. Note that the new default has
|
|
142
265
|
no `/dashboard` path segment — pass `host` explicitly for private or local
|
|
143
266
|
deployments.
|
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ const run = await qoni.doAnything.run({
|
|
|
151
151
|
|
|
152
152
|
const result = await run.wait({
|
|
153
153
|
onScreenshot: (img, i) => fs.writeFileSync(`step-${i}.jpg`, img.bytes),
|
|
154
|
-
onInteraction: (i) => { //
|
|
154
|
+
onInteraction: (i) => { // site_login / take_control / ask_user / confirmation
|
|
155
155
|
if (i.can("confirm_signed_in")) return i.confirmSignedIn();
|
|
156
156
|
if (i.can("confirm")) return i.confirm();
|
|
157
157
|
},
|
|
@@ -251,7 +251,7 @@ Silent product calls receive `token: delegation.data.token`. If that token is an
|
|
|
251
251
|
|
|
252
252
|
- **Silent** (`mode: "silent"`, the default) — acts for one end user, so a user
|
|
253
253
|
is **required**: pass `user: { id: <genauthUserId> }` (preferred) or the
|
|
254
|
-
|
|
254
|
+
top-level `userId`. Omitting it throws `QoniValidationError`
|
|
255
255
|
locally before any request. Authorize with the `products` sugar and/or
|
|
256
256
|
explicit `scopes`.
|
|
257
257
|
- **Interactive** (`mode: "interactive"`) — pass `redirectUri`, `state`,
|
|
@@ -286,18 +286,27 @@ const grant = await qoni.delegateToken({
|
|
|
286
286
|
scopes: ["gumem.memory:read", "webagent.do_anything:manage"],
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
+
// The callback will not carry grantId: save it with your business state, keyed by grantState.
|
|
290
|
+
await savePendingGrant(grant.data.grantState, {
|
|
291
|
+
grantId: grant.data.grantId,
|
|
292
|
+
businessState: "business-csrf-state",
|
|
293
|
+
});
|
|
289
294
|
redirectUserTo(grant.data.authorizationUrl);
|
|
290
295
|
```
|
|
291
296
|
|
|
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
|
|
297
|
+
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
298
|
|
|
294
299
|
Your callback handler runs on the server and exchanges the callback fields with AK/SK:
|
|
295
300
|
|
|
296
301
|
```ts
|
|
302
|
+
const pending = await loadPendingGrant(String(req.query.grant_state));
|
|
303
|
+
// Your business state comes back unchanged; reject forged or mixed-up callbacks.
|
|
304
|
+
if (!pending || req.query.state !== pending.businessState) throw new Error("Invalid delegation callback");
|
|
305
|
+
|
|
297
306
|
const completed = await qoni.completeDelegateToken({
|
|
298
|
-
grantId:
|
|
307
|
+
grantId: pending.grantId,
|
|
299
308
|
code: String(req.query.code),
|
|
300
|
-
state: String(req.query.
|
|
309
|
+
state: String(req.query.grant_state), // grant_state, not your business state
|
|
301
310
|
});
|
|
302
311
|
|
|
303
312
|
await qoni.gumem.recall({
|
|
@@ -306,7 +315,35 @@ await qoni.gumem.recall({
|
|
|
306
315
|
});
|
|
307
316
|
```
|
|
308
317
|
|
|
309
|
-
`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.
|
|
318
|
+
`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.
|
|
319
|
+
|
|
320
|
+
## Original delegation style (0.8.0+)
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const grant = await qoni.genauth.delegateAgent({
|
|
324
|
+
userId: process.env.QONI_USER_ID!, // The current user's GenAuth ID
|
|
325
|
+
agentKey: "email-assistant", // Agent audit label; alias for agent
|
|
326
|
+
scopes: [
|
|
327
|
+
"webagent.do_anything:read", // Read status, events, and artifacts
|
|
328
|
+
"webagent.do_anything:manage", // Start/cancel tasks and respond to interactions
|
|
329
|
+
],
|
|
330
|
+
expiresIn: "15m", // Converted to 900 seconds before signing
|
|
331
|
+
});
|
|
332
|
+
console.log(grant.grantId, grant.auditId);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
This entry returns the grant directly; root `delegateToken` still returns
|
|
336
|
+
`QoniResponse`, with the token in `response.data.token`. Interactive mode returns
|
|
337
|
+
the authorization request directly; use `completeDelegateToken()` in the server
|
|
338
|
+
callback after consent. `agentKey` aliases `agent`; both labels must agree when
|
|
339
|
+
supplied together. Top-level `userId` retains precedence over `user.id`.
|
|
340
|
+
Expiry accepts integer seconds or whole-number
|
|
341
|
+
`s/m/h/d` durations, between 60 and 86400 seconds. These additions are available
|
|
342
|
+
in the published `@qoniai/qoni@0.8.0` release.
|
|
343
|
+
|
|
344
|
+
Legacy `mailbox.*`, `web.search`, and `web.extract` scopes, and `deniedScopes`, are unsupported and rejected.
|
|
345
|
+
They are never expanded into broader product permissions. Product scopes do not
|
|
346
|
+
prohibit email sending or deletion; a prompt is not an enforced deny policy.
|
|
310
347
|
|
|
311
348
|
## Choosing Scopes
|
|
312
349
|
|
|
@@ -320,7 +357,9 @@ await qoni.delegateToken({
|
|
|
320
357
|
});
|
|
321
358
|
```
|
|
322
359
|
|
|
323
|
-
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.
|
|
360
|
+
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`).
|
|
361
|
+
|
|
362
|
+
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.
|
|
324
363
|
|
|
325
364
|
Use explicit scope strings when the requested permission boundary should be visible in code.
|
|
326
365
|
|
|
@@ -332,6 +371,10 @@ Use explicit scope strings when the requested permission boundary should be visi
|
|
|
332
371
|
| Search public web | `webagent.web_search:read`, `webagent.web_search:manage` | Agent can search public pages and read search results. |
|
|
333
372
|
| 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. |
|
|
334
373
|
| Create a monitor | `webagent.track:read`, `webagent.track:manage` | Agent can configure monitors, read events, run checks, and stop monitors. |
|
|
374
|
+
| 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. |
|
|
375
|
+
| 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. |
|
|
376
|
+
| Pay with the user's saved card | `user.payment:use` | After the user approves a payment, the saved card is filled in for them. |
|
|
377
|
+
| 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. |
|
|
335
378
|
|
|
336
379
|
Silent mode is usually appropriate for low-risk actions such as reading current-user memory, writing a confirmed summary, or searching public web pages.
|
|
337
380
|
|
|
@@ -606,81 +649,113 @@ the backend contract.
|
|
|
606
649
|
|
|
607
650
|
### Interactions (HITL)
|
|
608
651
|
|
|
609
|
-
When a run needs the user
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
`clarification` / `confirmation` / `take_control` / `wait`). Wrap it in a handle
|
|
613
|
-
and call the action methods; each posts to the endpoint the backend declared in
|
|
614
|
-
the interaction's `actions[]`, so you never hard-code a route:
|
|
652
|
+
When a run needs the user, it pauses and emits an `interaction` event whose
|
|
653
|
+
`event.data` is a typed `Interaction`. Interactions are typed by what the user
|
|
654
|
+
has to do, never by business, so one handler covers every task:
|
|
615
655
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
else if (i.can("release_control")) await i.releaseControl();
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
Methods: `answer(text)`, `skip()`, `confirm()`, `reject()`, `openLogin({ siteId?, profileId? })`,
|
|
629
|
-
`confirmSignedIn()`, `connectControl()`, `refreshControl()`, `releaseControl()`,
|
|
630
|
-
`retry()`, `switchProfile()`. `i.can(kind)` reports whether the interaction
|
|
631
|
-
currently offers that action; calling a method the backend did NOT declare
|
|
632
|
-
throws (so a "recovery" button only exists when it actually works).
|
|
656
|
+
| `type` | The user… | Methods |
|
|
657
|
+
| --- | --- | --- |
|
|
658
|
+
| `site_login` | signs in to a site in the live browser | `openLogin()`, `confirmSignedIn()` |
|
|
659
|
+
| `take_control` | operates the browser by hand (captcha, 2FA, …) | `connectControl()`, `refreshControl()`, `releaseControl()` |
|
|
660
|
+
| `ask_user` | answers a question shaped by `answerType` / `options` | `answer()`, `skip()` |
|
|
661
|
+
| `confirmation` | allows or rejects an action | `confirm()`, `reject()` |
|
|
662
|
+
| `wait` | nothing — the system is waiting (rate limit, external event) | `retry()` |
|
|
633
663
|
|
|
634
|
-
`
|
|
635
|
-
|
|
636
|
-
`
|
|
637
|
-
listed. Show the returned `liveUrl` to the user; after they sign in, persist the
|
|
638
|
-
login with `login.confirm()`, then tell the agent with `confirmSignedIn()`:
|
|
664
|
+
`Interaction` is a union distributed over `type`, so `switch (request.type)`
|
|
665
|
+
narrows `request.payload` in each branch. Each method posts to the endpoint the
|
|
666
|
+
backend declared in the interaction's `actions[]`, so you never hard-code a route:
|
|
639
667
|
|
|
640
668
|
```ts
|
|
641
|
-
import {
|
|
669
|
+
import type { InteractionHandle } from "@qoniai/qoni";
|
|
642
670
|
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
671
|
+
const handled = new Set<string>();
|
|
672
|
+
|
|
673
|
+
async function handleInteraction(handle: InteractionHandle) {
|
|
674
|
+
const request = handle.interaction;
|
|
675
|
+
// Creation, status updates and replays all call back: act once per pending id.
|
|
676
|
+
if (request.status !== "pending" || handled.has(request.id)) return;
|
|
677
|
+
handled.add(request.id);
|
|
678
|
+
|
|
679
|
+
switch (request.type) {
|
|
680
|
+
case "site_login": {
|
|
681
|
+
const login = await handle.openLogin(); // { liveUrl, ... } of a live login browser
|
|
682
|
+
await showToUser(login.liveUrl); // resolves when the user clicks "I'm signed in"
|
|
683
|
+
await handle.confirmSignedIn(); // saves the login, then the agent rechecks and continues
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "take_control": {
|
|
687
|
+
// connectControl() resolves to the signed control URL, when the backend offers it
|
|
688
|
+
const control = handle.can("connect_control") ? await handle.connectControl() : undefined;
|
|
689
|
+
await showToUser(control?.liveUrl ?? request.payload.liveUrl);
|
|
690
|
+
await handle.releaseControl(); // hand the browser back
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
case "ask_user": // payload: { question, answerType, options? }
|
|
694
|
+
await handle.answer(await askUser(request.payload));
|
|
695
|
+
break;
|
|
696
|
+
case "confirmation": // payload: { summary }
|
|
697
|
+
if (await approve(request.payload.summary)) await handle.confirm();
|
|
698
|
+
else await handle.reject();
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
659
701
|
}
|
|
660
|
-
```
|
|
661
702
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
703
|
+
const result = await run.wait({ onInteraction: handleInteraction });
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Interactions arrive one at a time: the run waits until the current one is
|
|
707
|
+
handled. The same interaction calls back again on every status change and on
|
|
708
|
+
reconnect replays, always with the same `id` and `type` — key your UI cards by
|
|
709
|
+
`request.id`. An update with content of its own replaces the card (a take-over
|
|
710
|
+
that became `active` carries its new `liveUrl`); when the backend settles an
|
|
711
|
+
interaction with an empty shell, the card keeps its original `payload`. A
|
|
712
|
+
`take_control` that arrives already `active` was started by the user, for
|
|
713
|
+
example from Console; show it if your UI offers control there. Methods: `answer(value)`, `skip()`, `confirm()`,
|
|
714
|
+
`reject()`, `openLogin({ siteId?, profileId? })`, `confirmSignedIn()`,
|
|
715
|
+
`connectControl()`, `refreshControl()`, `releaseControl()`, `retry()`,
|
|
716
|
+
`switchProfile()`. `connectControl()` and `refreshControl()` resolve to
|
|
717
|
+
`{ liveUrl, expiresAt?, status?, raw }`: the signed URL of the browser the user
|
|
718
|
+
now controls. `handle.can(kind)` reports whether the interaction currently
|
|
719
|
+
offers that action; calling a method the backend did NOT declare throws (so a
|
|
720
|
+
"recovery" button only exists when it actually works).
|
|
721
|
+
|
|
722
|
+
**`ask_user` answers are structured.** `answer()` takes the value matching
|
|
723
|
+
`payload.answerType` — `text` → string, `number` → number, `boolean` → boolean,
|
|
724
|
+
`single_choice` → one `options[].value`, `multiple_choice` → an array of option
|
|
725
|
+
values — and throws `QoniValidationError` before sending anything that does not
|
|
726
|
+
fit. The wire still calls this type `clarification`; the SDK presents it as
|
|
727
|
+
`ask_user` (choices arrive as `{ value, label }` options).
|
|
728
|
+
|
|
729
|
+
**Sign-in takes two calls.** `openLogin()` opens a live login browser for the
|
|
730
|
+
`site_login` interaction (pass `siteId` from `payload.sites[].siteId`, or omit it
|
|
731
|
+
when exactly one site is listed). Once the user is signed in, `confirmSignedIn()`
|
|
732
|
+
saves the login so later tasks reuse it, then tells the agent to recheck the site
|
|
733
|
+
and continue; if the user turns out not to be signed in, the agent raises a new
|
|
734
|
+
`site_login`. It saves once and rechecks once per login session, however many
|
|
735
|
+
times it is called. If the backend could not save the login yet, it throws a
|
|
736
|
+
retryable `QoniError` coded `site_login.save_failed` without telling the agent to
|
|
737
|
+
go on; call it again. The same holds for `site_login.session_replaced`, thrown
|
|
738
|
+
when the login was reopened while it was being saved. Handles from the same `RunHandle` (or `MonitorHandle`)
|
|
739
|
+
share the login session, so `confirmSignedIn()` also works on the handle of a
|
|
740
|
+
later callback; a RunHandle from a new `attach()` does not. Delegate
|
|
668
741
|
`QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, and allow
|
|
669
|
-
both in the AccessKey policy.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
`
|
|
674
|
-
`
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
`
|
|
742
|
+
both in the AccessKey policy. `openLogin()`'s result still offers `confirm()` for
|
|
743
|
+
callers that save the login themselves; it is deprecated.
|
|
744
|
+
|
|
745
|
+
The discriminant strings are also exported as constants so you can avoid bare
|
|
746
|
+
literals: `InteractionTypes` (`SiteLogin` / `TakeControl` / `AskUser` /
|
|
747
|
+
`Confirmation` / `Wait`; the deprecated `Clarification` now equals `AskUser`),
|
|
748
|
+
`InteractionStatuses`, and `ActionKinds` (the `handle.can(...)` action names).
|
|
749
|
+
Authorization scope strings have the same treatment — `QoniScopes` (every
|
|
750
|
+
individual scope), `QoniProductScopes` (the per-product `read`+`manage` pairs the
|
|
751
|
+
`products` sugar expands to), and `QoniScopeBundles` (curated bundles). Use them in
|
|
752
|
+
place of hand-typed strings:
|
|
678
753
|
|
|
679
754
|
```ts
|
|
680
755
|
import { InteractionTypes, ActionKinds, QoniScopes } from "@qoniai/qoni";
|
|
681
756
|
|
|
682
|
-
if (
|
|
683
|
-
await
|
|
757
|
+
if (handle.type === InteractionTypes.AskUser && handle.can(ActionKinds.answer)) {
|
|
758
|
+
await handle.answer("the blue button");
|
|
684
759
|
}
|
|
685
760
|
```
|
|
686
761
|
|
|
@@ -690,7 +765,7 @@ The 1:1 low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnythi
|
|
|
690
765
|
|
|
691
766
|
`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).
|
|
692
767
|
|
|
693
|
-
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 `
|
|
768
|
+
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.
|
|
694
769
|
|
|
695
770
|
### Response Shape
|
|
696
771
|
|