@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qoniai/qoni",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Unified Node.js SDK for Qoni Agent delegation, GUMem memory, and WebAgent automation.",
5
5
  "homepage": "https://github.com/QONIAI/qoni-sdk-node#readme",
6
6
  "bugs": {
@@ -46,7 +46,7 @@
46
46
  "gen:event-types": "node scripts/gen-event-types.mjs",
47
47
  "typecheck": "tsc --noEmit",
48
48
  "test": "pnpm build && pnpm check:brand && pnpm test:mock && pnpm test:package",
49
- "test:mock": "vitest run --coverage=false tests/qoni.test.ts tests/semantic-surface.test.ts tests/open-source-readiness.test.ts tests/complete-coverage.test.ts tests/event-catalog.test.ts tests/interactions.test.ts tests/contract-surface.test.ts tests/sse-abort.test.ts tests/do-anything-evidence.test.ts tests/query-matrix.test.ts tests/live-evidence.test.ts tests/site-login-settle-track.test.ts",
49
+ "test:mock": "vitest run --coverage=false tests/qoni.test.ts tests/semantic-surface.test.ts tests/open-source-readiness.test.ts tests/complete-coverage.test.ts tests/event-catalog.test.ts tests/interactions.test.ts tests/contract-surface.test.ts tests/sse-abort.test.ts tests/do-anything-evidence.test.ts tests/query-matrix.test.ts tests/live-evidence.test.ts tests/site-login-settle-track.test.ts tests/abstract-interactions.test.ts",
50
50
  "test:e2e": "QONI_LIVE_E2E=1 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=120000 tests/live-e2e/*.test.ts",
51
51
  "test:e2e:do-anything": "QONI_LIVE_E2E=1 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=120000 tests/live-e2e/do-anything.test.ts",
52
52
  "test:e2e:do-anything:release": "QONI_LIVE_E2E=1 QONI_LIVE_ALLOW_ENV_CONSTRAINTS=0 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=120000 tests/live-e2e/do-anything.test.ts",
@@ -66,12 +66,12 @@ const run = await qoni.doAnything.run({
66
66
  });
67
67
  const result = await run.wait({
68
68
  onScreenshot: (img, i) => saveImage(`step-${i}.jpg`, img.bytes),
69
- onInputRequest: (req) => openForUser(req.liveUrl),
69
+ onInteraction: (handle) => handleInteraction(handle), // see "Do Anything Interactions"
70
70
  });
71
71
  console.log(result.output);
72
72
  ```
73
73
 
74
- For explicit authorization, the Qoni Console/BFF owns GenAuth login and grant completion before it redirects back to the business `redirectUri`. The redirect only carries one-time callback fields such as `code`, `state`, and `grant_state`/`grantId`; never design an API that exposes a delegation token to browser code.
74
+ For explicit authorization, the Qoni Console/BFF owns GenAuth login and grant completion before it redirects back to the business `redirectUri`. The redirect only carries one-time callback fields: `code`, `grant_state`, and your business `state` when you supplied one. It does not carry `grantId`; load the `grantId` you saved by `grant_state`. Never design an API that exposes a delegation token to browser code.
75
75
 
76
76
  ```ts
77
77
  const grant = await qoni.delegateToken({
@@ -82,12 +82,17 @@ const grant = await qoni.delegateToken({
82
82
  scopes: [QoniScopes.GUMEM_MEMORY_READ],
83
83
  });
84
84
 
85
+ // The callback carries code, state (your business state) and grant_state, but no grantId.
86
+ await savePendingGrant(grant.data.grantState, { grantId: grant.data.grantId, businessState: "business-csrf-state" });
85
87
  redirectUserTo(grant.data.authorizationUrl);
86
88
 
89
+ // Callback handler: look up by grant_state, check the business state, then exchange.
90
+ const pending = await loadPendingGrant(callback.grant_state);
91
+ if (!pending || callback.state !== pending.businessState) throw new Error("Invalid delegation callback");
87
92
  const completed = await qoni.completeDelegateToken({
88
- grantId,
89
- code,
90
- state,
93
+ grantId: pending.grantId,
94
+ code: callback.code,
95
+ state: callback.grant_state,
91
96
  });
92
97
  ```
93
98
 
@@ -148,7 +153,7 @@ const run = await qoni.doAnything.run({
148
153
  run.id;
149
154
  run.sessionRef; // pass back to run({ session })
150
155
  await run.status(); // refresh current state
151
- await run.respond(requestId, response?); // HITL answer; omit response to skip
156
+ run.interactionHandle(event.data); // wrap an interaction event; answer via its methods
152
157
  await run.cancel(reason?); // idempotent — safe on a finished run
153
158
 
154
159
  const result = await run.wait({
@@ -156,7 +161,7 @@ const result = await run.wait({
156
161
  onEvent: (e) => console.log(e.type),
157
162
  onWireEvent: (e) => appendRawEvidence(e), // every SSE frame before semantic filtering
158
163
  onReconnect: (info) => logReconnect(info.attempt, info.lastEventId),
159
- onInputRequest: (req) => openForUser(req.liveUrl),
164
+ onInteraction: (handle) => handleInteraction(handle), // typed InteractionHandle
160
165
  onScreenshot: (img, i) => saveImage(`step-${i}.jpg`, img.bytes),
161
166
  });
162
167
  // result: { runId, status: "succeeded"|"failed"|"canceled", output?, artifacts,
@@ -210,18 +215,38 @@ for await (const event of run.events()) {
210
215
 
211
216
  `qoni.doAnything.api.events({ token, sessionId, runId, signal?, lastEventId?, onlyTopLevel? })` yields raw `{ id?, event, data }` wire events: the type is a raw string on `event.event` (e.g. `"run.status_changed"`), the payload is `event.data.data` (snake_case), and `event.data.task_id` identifies the run. `run.status_changed` payloads carry the new status in `.to` (NOT `.status`). Terminal detection is `event.event === "run.completed"`. Wire names are internal and may evolve — prefer the high-level `run.events()` + `event.type` instead.
212
217
 
213
- ## Do Anything Login Wall (human handoff)
218
+ ## Do Anything Interactions (human handoff)
214
219
 
215
- A web task often hits a page only the end user can log in to. The agent parks and emits an **`inputRequest`** event. The live-browser URL arrives on the request payload (`event.data.liveUrl`; live-url changes also stream as `progress` events with `data.liveUrl`). The client's ONLY job is to surface that URL so the human can sign in — login detection is the BACKEND's job; it re-probes and resumes the run automatically.
220
+ When a run needs the user it pauses and emits one `interaction` event per request (`event.data` is a typed `Interaction`; `wait({ onInteraction })` hands you an `InteractionHandle`). Interactions are typed by what the user must do, never by business: `site_login`, `take_control`, `ask_user`, `confirmation`, plus the system `wait`. `switch (handle.interaction.type)` narrows `payload` per branch.
216
221
 
217
- - Open `liveUrl` ONCE in a real browser when you see `inputRequest`. Do NOT poll, do NOT call `respond`, do NOT wait on the window closing.
218
- - Keep iterating (or stay inside `wait()`); the run progresses past the wall and reaches its terminal event on its own once the user is signed in.
219
- - The first login is unavoidable; later runs reuse persisted login state and never reach this wall.
222
+ - Act only on `status === "pending"` and dedupe by `interaction.id`: creation, status changes and reconnect replays all call back again with the same `id` and `type`. Updates with real content replace the card (an `active` take-over carries a new `liveUrl`); only empty settling shells keep the original `payload`.
223
+ - Interactions arrive one at a time; the run resumes once the current one is handled.
224
+ - `site_login`: `openLogin()` → show `liveUrl` → after the user signs in, `confirmSignedIn()` (it saves the login once, then the agent rechecks). Do NOT call the deprecated `login.confirm()` first. A `QoniError` coded `site_login.save_failed` or `site_login.session_replaced` is retryable: call `confirmSignedIn()` again. Delegate `webagent.site_login:request` and `webagent.site_login:confirm`.
225
+ - `ask_user`: `answer(value)` with the type `payload.answerType` names (`text` string, `number`, `boolean`, `single_choice` option value, `multiple_choice` array of option values), or `skip()`. The wire's `clarification` arrives as `ask_user`; never switch on `"clarification"`.
226
+ - `take_control`: `connectControl()` only when `handle.can("connect_control")`; it resolves to `{ liveUrl, expiresAt }` (show that URL, else `payload.liveUrl`) → `releaseControl()`. `refreshControl()` returns a fresh URL.
227
+ - `confirmation`: `confirm()` or `reject()` after showing `payload.summary`.
228
+ - Only call methods `handle.can(kind)` reports; an undeclared action throws.
220
229
 
221
230
  ```ts
222
231
  const result = await run.wait({
223
- onInputRequest: (req) => {
224
- if (req.liveUrl) openBrowserForUser(String(req.liveUrl)); // pop once; the backend resumes by itself
232
+ onInteraction: async (handle) => {
233
+ const request = handle.interaction;
234
+ if (request.status !== "pending" || handled.has(request.id)) return;
235
+ handled.add(request.id);
236
+ switch (request.type) {
237
+ case "site_login": {
238
+ const login = await handle.openLogin();
239
+ await showToUser(login.liveUrl); // resolves when the user says "I'm signed in"
240
+ await handle.confirmSignedIn();
241
+ break;
242
+ }
243
+ case "ask_user":
244
+ await handle.answer(await askUser(request.payload));
245
+ break;
246
+ case "confirmation":
247
+ await handle.confirm();
248
+ break;
249
+ }
225
250
  },
226
251
  });
227
252
  ```
@@ -243,12 +268,9 @@ pending → running → (running ⇄ awaiting_input)* → succeeded | failed | c
243
268
  `terminal_reason` / `is_task_successful`).
244
269
 
245
270
  **Client action is event-driven, not status-driven.** The only thing that needs
246
- you is the `inputRequest` event (surface `liveUrl` so a human can log in /
247
- confirm — see Login Wall). Everything else, including login walls, the backend
248
- re-probes and resumes on its own. A run flapping `running ⇄ awaiting_input`
249
- (e.g. `waiting_reason=confirmation_required` during internal pacing) is
250
- expected — do NOT poll or call `respond`. Only call `respond` to answer an
251
- actual `inputRequest`.
271
+ you is an `interaction` event (see Do Anything Interactions). A run flapping
272
+ `running ⇄ awaiting_input` (e.g. `waiting_reason=confirmation_required` during
273
+ internal pacing) is expected — do NOT poll; act only on pending interactions.
252
274
 
253
275
  ### Parent & sub-tasks
254
276