Skip to content

Qoni SDK ​

This page covers installation, all unified SDK method entry points, run and interaction data, and application functions that present requests and submit user responses.

Available SDK ​

The public unified Qoni SDK is the Node.js and TypeScript package @qoniai/qoni. Its source is available in QoniAI/qoni-sdk-node.

ItemCurrent status
npm package@qoniai/qoni
GitHub repositoryQoniAI/qoni-sdk-node
Current public version0.9.0
Example version0.9.0, matching the npm release; bundled with the download
RuntimeNode.js 18 or later with server-side fetch
Module formatsESM, CommonJS, and TypeScript declarations
LicenseMIT

Server-side credentials

Keep the Qoni accessKey and secretKey on a trusted server. Do not bundle them into browsers, mobile apps, public CLI configuration, or untrusted Agent runtimes.

Published package and pending design

The stable examples on this page use npm 0.9.0. Personal Agent, Enterprise Agent, and hosted sign-in retain the Owner's future Quickstart design. Their real Agent identity field agentId, scopes: ['*'], Agent creation/binding, and fill_form cannot be copied into the published package. 0.9.0 has no handle.submit(); agent (compatible alias agentKey) is an audit label and does not establish Agent identity or user binding. The SDK rejects * locally; list the required scopes explicitly. The automatic run({ memory }) design is pending and returned 422 in the 2026-10-08 gray acceptance test; current integrations must call GUMem's atomic methods separately and verify the results. See Track for its backend contract and unpublished repair candidate (Unreleased, targeting 0.10.0, a breaking change).

Capability surface ​

The unified SDK exposes these capabilities from Qoni:

NamespacePurpose
genauthDelegate to Agents, read the current user, and manage GenAuth users
gumemCreate sessions, add messages, recall Memory, upload resources, and record Actions
doAnythingStart or reattach to general Web Agent tasks
webSearchSearch the public web and read results
deepResearchRun long-form research and download artifacts
track0.9.0 exposes legacy monitor entries incompatible with the current /track/tracks backend; see Track

Runtime product calls use short-lived delegation tokens. The SDK handles runtime discovery and downstream product-token exchange. Application code should not construct internal /api/v3/eak/* routes or internal claims.

Install ​

The downloadable examples and this page use the published 0.9.0 release, including the compatible genauth.delegateAgent() entry and interactions narrowed by type. Use the commands below to install it in an existing application. Download the examples and run npm ci in examples/qoni to install the bundled SDK of the same version.

bash
npm install @qoniai/qoni@0.9.0

You can also use pnpm or Yarn:

bash
pnpm add @qoniai/qoni@0.9.0
# or
yarn add @qoniai/qoni@0.9.0

Initialize the client ​

Get an AccessKey ​

  1. Open the Qoni Console workspace list and select the workspace that will use the SDK.
  2. Select AccessKey in the sidebar, then Create AccessKey, and configure the permissions needed for your calls.
  3. In the Save AccessKey dialog, select Copy credentials. Store the AccessKey ID and AccessKey Secret as the server-side environment variables QONI_ACCESS_KEY and QONI_SECRET_KEY.

The AccessKey Secret is shown only when the key is created. You can view an existing key's ID in the list, but you cannot retrieve its Secret again. Create a new AccessKey if you did not save the Secret.

ts
import { Qoni, QoniScopes } from "@qoniai/qoni";

// Initialize on the server; AK/SK access the services bound to this workspace.
const qoni = new Qoni({
  accessKey: process.env.QONI_ACCESS_KEY!,
  secretKey: process.env.QONI_SECRET_KEY!,
});

When host is omitted, the SDK uses the hosted gateway https://dashboard.qoni.ai. Private or local deployments can pass host; it must point to the Qoni Console or SDK gateway, not directly to GenAuth, Web Agent, or GUMem.

Qoni accepts the following options:

OptionRequiredDescription
accessKeyYesAccess key created in the Qoni Console
secretKeyYesSecret key created in the Qoni Console
hostNoQoni gateway address for private deployments; overrides runtime discovery
fetchNoCustom fetch implementation for proxies or test environments
timeoutMsNoPer-request timeout, default 30000; event-stream waiting is not limited by it
sseMaxRetriesNoEvent-stream reconnect attempt limit, default 5; set 0 to disable

Obtain a delegation token ​

Web Agent and GUMem products act for an end user. Silent delegation requires a real GenAuth user ID from the user pool bound to the Qoni credential:

ts
// Delegate this research task for the current user from the bound GenAuth pool.
const { token } = await qoni.genauth.delegateAgent({
  userId: process.env.QONI_USER_ID!,
  agentKey: "research-assistant", // Agent audit label for this delegation
  scopes: [
    QoniScopes.WEB_SEARCH_READ, // Read search status and results
    QoniScopes.WEB_SEARCH_MANAGE, // Start or cancel searches
    QoniScopes.GUMEM_MEMORY_READ, // Recall user context
    QoniScopes.GUMEM_MEMORY_WRITE, // Write confirmed demo preferences
  ],
  expiresIn: "15m", // Converted to the server's integer 900 seconds
});

Prefer mode: "interactive" for higher-risk operations such as site login, browser takeover, long-running monitoring, or sensitive artifacts. Complete interactive authorization on the server with completeDelegateToken({ grantId, code, state }); the browser never receives the delegation token.

Fine-grained permissions are declared with scopes in the <namespace>.<resource>:<verb> format, for example webagent.web_search:read. Each Web Agent product has exactly two verbs, read and manage; products: ["webSearch"] requests the whole product as a shorthand. Site sign-in has two more scopes: webagent.site_login:request (open a controlled login browser) and webagent.site_login:confirm (save the login to the browser profile), exported as QoniScopes.SITE_LOGIN_REQUEST / QoniScopes.SITE_LOGIN_CONFIRM. They are not part of any products shorthand: request them explicitly in scopes, and the AccessKey policy must allow them. The SDK exports the QoniScopes constants and QoniScopeBundles presets (such as GUMEM_SESSION_RECALL) so you avoid hand-writing scope strings; a malformed scope throws QoniValidationError locally. For SDK constants and service availability boundaries, see Requestable scopes.

The silent delegation response does not contain grantedScopes. When you need to record the effective permission boundary, use authoritative introspection:

ts
// Declare the fields this page reads; the SDK types introspection data as unknown by default.
const { data: tokenInfo } = await qoni.genauth.introspectDelegationToken<{
  active: boolean;
  scope?: string[];
}>({ token });
if (!tokenInfo.active) throw new Error("Delegation token is inactive");
const effectiveScopes = tokenInfo.scope ?? []; // Record effective scopes with the audit data

In an interactive response, state is a compatibility alias of the server-generated grantState; it is not an echo of the caller input state. Verify that the authorization URL's grant_id query value equals the response grantId, then save grantId and your business state on the server keyed by grantState. After consent, the GenAuth callback to redirectUri carries code, your business state, and grant_state (equal to grantState), but no grantId: look the record up by grant_state, check the business state, then call completeDelegateToken({ grantId, code, state }) with the saved grantId and the callback's grant_state as state.

Requestable scopes ​

A scope passed to delegateToken or genauth.delegateAgent must clear two checks:

  1. The AccessKey policy: a delegation can request only scopes the current AccessKey allows; anything else returns eak.delegation.scope_not_allowed. The permission policies you select when creating or editing the AccessKey in Console define that allowlist.
  2. The product API check: GUMem and Web Agent endpoints admit requests by scope. The "Allows" column below states what each scope actually permits and names the SDK methods that use it.

The tables list SDK scope constants and their intended contracts; a constant does not prove that a deployment has registered or implemented it. User Profile, payment, and Agent inbox capabilities remain pending design; legacy verbs from earlier versions are covered after the tables. The SDK constants live on QoniScopes, for example QoniScopes.GUMEM_MEMORY_READ.

GUMem

ScopeSDK constantAllowsConsole policy
gumem.memory:readGUMEM_MEMORY_READRead session context and the message stream; also satisfies gumem.action:read and gumem.profile:read. SDK method: gumem.recallGUMem Memory
gumem.memory:writeGUMEM_MEMORY_WRITEWrite Memory; also satisfies gumem.session:create, gumem.message:write, gumem.resource:write, and gumem.action:write. SDK methods: gumem.createSession, gumem.addMessages, gumem.uploadResourceGUMem Memory
gumem.session:createGUMEM_SESSION_CREATECreate a sessionGUMem Session
gumem.message:writeGUMEM_MESSAGE_WRITEWrite messages to a sessionGUMem Session
gumem.resource:writeGUMEM_RESOURCE_WRITEUpload resource filesGUMem Admin
gumem.action:writeGUMEM_ACTION_WRITERecord user Actions. SDK method: gumem.actions.recordGUMem Actions
gumem.action:readGUMEM_ACTION_READQuery user Actions and their progress, facts, summaries, and topics; subscribe to the Action stream. SDK methods: gumem.actions.recall, gumem.actions.streamGUMem Actions
gumem.profile:readGUMEM_PROFILE_READRead and recall the user profileGUMem Admin
gumem.admin:manageGUMEM_ADMIN_MANAGECall the GUMem console's project management APIsGUMem Admin
gumem.memory:deleteGUMEM_MEMORY_DELETEReserved; no endpoint checks it yet, so requesting it adds no permissionGUMem Memory
gumem.search:runGUMEM_SEARCH_RUNReserved; no endpoint checks it yet, so requesting it adds no permissionGUMem Search

Web Agent

ScopeSDK constantAllowsConsole policy
webagent.do_anything:readDO_ANYTHING_READRead DoAnything run status, events, artifacts, and recordingsWebAgent Do Anything
webagent.do_anything:manageDO_ANYTHING_MANAGEStart and cancel runs, respond to interactions, send messages; includes readWebAgent Do Anything
webagent.web_search:readWEB_SEARCH_READRead search status and resultsWebAgent Web Search
webagent.web_search:manageWEB_SEARCH_MANAGEStart and cancel searches; includes readWebAgent Web Search
webagent.deep_research:readDEEP_RESEARCH_READRead research status, events, and report artifactsWebAgent Deep Research
webagent.deep_research:manageDEEP_RESEARCH_MANAGEStart and cancel research, respond to interactions; includes readWebAgent Deep Research
webagent.track:readTRACK_READRead monitors and their executionsWebAgent Track
webagent.track:manageTRACK_MANAGECreate, update, pause, resume, run now, and delete monitors; includes readWebAgent Track

Among the Web Agent SDK methods, reads (status, events, results, artifacts) use read; every operation that changes execution state uses manage. Each product's read + manage pair can also be requested with the products shorthand: doAnything, webSearch, deepResearch, track.

User Profile and Agent inbox (pending design)

0.9.0 exports these constants, but all five scope requests were rejected with eak.delegation.scope_not_allowed in the 2026-10-08 gray acceptance test. The table retains their design intent; it does not claim completion in the published package or deployment.

ScopeSDK constantAllowsConsole policy
user.profile:readUSER_PROFILE_READRead the name, phone and shipping address in the user's Profile so DoAnything can fill in checkoutGenAuth User Profile
user.profile:writeUSER_PROFILE_WRITESave the fields the user fills in during a fill-in (fill_form) interaction that carry a profileField to their Profile. Design method: handle.submit (absent from 0.9.0)GenAuth User Profile
user.payment:useUSER_PAYMENT_USEAfter the user allows the payment in a confirmation (confirmation) interaction, the Web Agent runtime fetches the card from the Profile and fills it in the controlled browser; the card number is never returned to your app or the modelGenAuth Payment
agent.mail:readAGENT_MAIL_READRead mail in the Agent's own inbox, such as verification codes and order confirmationsGenAuth Agent Mail
agent.mail:sendAGENT_MAIL_SENDSend mail from the Agent's own inboxGenAuth Agent Mail

These scopes serve consumer scenarios; see Personal Agent for the full flow. In the future design, * requests every scope the AccessKey policy allows. 0.9.0 rejects * locally, so current callers must list scopes explicitly. Silent mode has no consent page; interactive authorization has a user confirmation step.

When you request scopes:

  • Broader scopes include narrower ones: a Web Agent manage scope includes the same product's read; GUMem gumem.memory:write and gumem.memory:read cover the narrower scopes noted in the table. Request only what the task needs.
  • The SDK exchanges a product token per method: before each GUMem or Web Agent call, the SDK exchanges the delegation for a product token with only the scopes that method needs. If the delegation lacks one, the exchange returns eak.token_exchange.scope_not_delegated. With the SDK, request the scope listed for the SDK method you call. For example, delegating only gumem.session:create still fails gumem.createSession; delegate gumem.memory:write instead.
  • Site sign-in: webagent.site_login:request (QoniScopes.SITE_LOGIN_REQUEST) and webagent.site_login:confirm (QoniScopes.SITE_LOGIN_CONFIRM) let openLogin() open a controlled login browser and save the login after confirmation. No products shorthand includes them. Availability depends on deployment registration and the AccessKey policy. The 2026-10-08 gray test issued these scopes, but exchanging them for a Web Agent product token returned 400. Issuance alone does not prove the sign-in flow works.
  • Legacy verbs: Console's WebAgent policies also list scopes ending in :run, :stop, and :control. These are verbs from earlier versions. Web Agent endpoints check only read and manage, so requesting them adds no permission; do not use them in new code.

SDK method index ​

This page follows the declarations for the bundled @qoniai/qoni 0.9.0 build. Ordinary requests return QoniResponse<T>: data holds the business result and meta holds request, trace, and audit identifiers. The compatible genauth.delegateAgent() entry returns business data directly. Task creation returns a handle for continued reading. Application interaction functions are explained below.

Client and GenAuth ​

MethodInput and purposeReturn
qoni.genauth.delegateAgent(input)Same authorization endpoint as delegateToken; accepts userId, agentKey, explicit scopes, and expiresIn: '15m'Direct grant.token/grantId/auditId in silent mode, or the authorization request in interactive mode
qoni.delegateToken(input)mode, agent, products/scopes, expiresIn, optional idempotencyKey; silent mode uses user, interactive mode uses redirectUri/stateDelegation result or authorization request in data
qoni.delegateAgent(input)Existing root alias of delegateToken; distinct from genauth.delegateAgentRetains QoniResponse, with token at response.data.token
qoni.completeDelegateToken(input)Server-saved grantId (the callback does not carry it), callback code, and callback grant_state passed as statedata.token, grantId, auditId, and other delegation fields
qoni.currentUser({ accessToken })Read the signed-in user; equivalent to genauth.userInfoQoniResponse<T>
qoni.resolveAnyBoundUser()Pick a bound user for demos; production apps identify the actual current userUser ID string
qoni.genauth.userInfo({ accessToken })Read identity with the user's GenAuth Access TokenQoniResponse<T>
qoni.genauth.discovery() / jwks()Read OIDC discovery configuration / verification keysQoniResponse<T>
qoni.genauth.introspectDelegationToken({ token })Read delegation validity and effective scopesQoniResponse<T>
qoni.genauth.users.list(input?)page, limit, options; list bound-pool usersQoniResponse<T>
qoni.genauth.users.get({ userId }) / getBatch({ userIds })Read one / multiple usersQoniResponse<T>
qoni.genauth.users.create(input) / createBatch(input)User fields / batch users or list; fields follow the GenAuth user APIQoniResponse<T>
qoni.genauth.users.update({ userId, ...fields })Update the specified user's fieldsQoniResponse<T>
qoni.genauth.users.deleteBatch({ userIds })Delete selected users after the application confirms the scopeQoniResponse<T>

users.* exchanges AK/SK for management access to the bound user pool by default, with optional adminToken/userPoolId overrides. User fields and most GenAuth results are generic data; declare T for the fields your application reads and validate results.

For the values the delegation methods accept in scopes and what each scope allows, see Requestable scopes.

GUMem ​

These methods use a delegated token. Effective Session, Memory, and Action permissions follow the service contract.

MethodMain inputPurpose and return
qoni.gumem.createSession(input)token; optional userId, sessionId, title, metadataCreate a Session; QoniResponse<T>
qoni.gumem.addMessages(input)token, sessionId, messages; optional userId, syncWrite user-confirmed messages; QoniResponse<T>
qoni.gumem.recall(input)token; optional sessionId, query, details, recallConfig, metadataFiltersRecall context; QoniResponse<T>
qoni.gumem.uploadResource(input)token, file (Blob/File); optional user, Session, filename, and content typeUpload a resource; QoniResponse<T>
qoni.gumem.actions.record(input)token and business Action fieldsRecord an Action; QoniResponse<T>
qoni.gumem.actions.recall(input)token and query fieldsQuery Actions; QoniResponse<T>
qoni.gumem.actions.stream(input)token and query fieldsRead the Action stream endpoint result; QoniResponse<T>, not AsyncIterable

Cross-session recall follows the Memory record's scope. User preferences extracted as scope=user can enter user context across Sessions; facts classified as scope=session (such as a task's project name or budget) do not enter another Session's user context by default. Verify message writes, extraction, scope classification, and recall separately; not every session fact automatically becomes long-term user memory.

Web Agent product entry points ​

MethodMain inputReturn and limits
qoni.doAnything.run(input)token, task prompt; optional capture, limits, session, profileId, browserProxy, keepAlive, allowedActions, skillsRunHandle<DoAnythingEvent>
qoni.doAnything.attach(runId, options)Existing run ID, token; optional session/captureReattach without creating a new task
qoni.doAnything.artifacts({ token, runId })Delegation token and run ID; optional signalArtifact[] without replaying events
qoni.webSearch.run(input)token, prompt (string or string array); optional maxResultsPerQuery, siteWhitelist, siteBlacklist, captureRunHandle<WebSearchEvent>; no session/limits support
qoni.webSearch.attach(runId, options)Existing run ID, token; optional captureReattach to the search
qoni.deepResearch.run(input)token, prompt; optional depth, outputFormat, targetAudience, domainWhitelist, domainBlacklist, session, limits, captureRunHandle<DeepResearchEvent>
qoni.deepResearch.attach(runId, options)Existing run ID, token; optional captureReattach to the research
qoni.track.create(input)token, monitoring intent prompt, and supported monitor-definition fieldsThe 0.9.0 legacy route is unusable with the current backend; the repair candidate returns MonitorHandle
qoni.track.attach(monitorId, { token })Existing monitor ID and delegation tokenReattach to the monitor

capture supports screenshots/videoFrames; limits declares maxDurationMinutes; session uses { sessionId }. DoAnything browserProxy accepts proxyId, mode (off/all/scoped), and region; research depth is light/standard/deep. Declared or forwarded options do not guarantee that a deployment enables the capability. DoAnything rejects platform options such as model and outputSchema.

Run, monitor, and artifact handles ​

MemberUsage
run.id / run.sessionRefRetain the run ID / optional Session reference for queries or follow-up runs
run.status()Read RunStatus: id/status/sessionId/output/raw
run.events(options?)AsyncIterable; accepts lastEventId, signal, onWireEvent, onReconnect
run.wait(options?)Return terminal RunResult; accepts timeoutMs/signal, onEvent/onWireEvent/onReconnect, onScreenshot/onInteraction
run.interactionHandle(request)Wrap event request data in a handle bound to this run
run.cancel(reason?)Request cancellation and return status; distinct from stopping client-side waiting
monitor.id / monitor.get()Retain the monitor ID / read its definition
monitor.pause() / resume()Pause / resume monitoring
monitor.refine(patch)The repair candidate changes only title/schedule, preserving the original intent; legacy DSL is unsupported
monitor.runNow()The repair candidate returns trackId/sessionId/runId, meaning accepted; first_look or another in-flight check causes 409 task_in_progress
monitor.events({ lastEventId?, signal? })No current Track SSE endpoint; the repair candidate throws QoniUnsupportedError locally on iteration, with zero HTTP
monitor.interactionHandle(request)No current Track question/intervention endpoint; the repair candidate throws QoniUnsupportedError locally
monitor.runs({ limit?, offset? }) / run(runId)The repair candidate reads the most recent 50 checks; limit/offset paginate locally within this window; an exact run ID outside it fails
monitor.delete()The repair candidate soft-deletes and stops future scheduling; it does not cancel a running task
artifact.content()Download bytes (Uint8Array)
artifact.refreshDownloadUrl?.()Refresh a temporary URL when the implementation provides this method

RunResult includes runId/status/output/artifacts/terminalReason/isTaskSuccessful/raw; validate business output. Artifact metadata is id/name/mime/sizeBytes/createdAt/downloadUrl/expiresIn, all optional except id. Timing out or aborting a client read does not cancel server execution: use attach() to continue reading or cancel() to request cancellation.

Monitor rows describe a breaking Unreleased candidate targeting 0.10.0, not available npm 0.9.x functionality. Track creation starts first_look immediately; successful checks use done, without mapping to RunResult.status value succeeded. See Track for the contract.

event.type values include core progress/message/interaction/screenshot/browserLiveUrlChanged/done, search resultsReady, research phase/sectionReady, and legacy monitor type constants monitorCreated/triggered/checkCompleted (current Track has no event stream to subscribe to). Constant keys use PascalCase: for example, QoniEventTypes.Done has the value done. Product handles have different event types; no product emits every type. RunImage contains bytes/mime and optional pageUrl/step. Original frames are available at event.raw; subscribe to all wire frames with onWireEvent.

Low-level and compatibility entry points ​

Prefer typed methods above. These are actual exposed low-level interfaces; api bodies and results follow the backend and are outside the stable semantic contract.

Entry pointMethods
qoni.doAnything.apicreateSession, createRun, getRun, events, intervene, cancel, readArtifacts, listArtifacts, artifactDownloadUrl, readRecording
qoni.webSearch.apirun, get, events, cancel
qoni.deepResearch.apirun, get, events, followUp, cancel, feedback, listArtifacts, getArtifact
qoni.track.apicreateMonitor, getMonitor, runNow, events, intervene, listRuns, getRun, updateMonitor, deleteMonitor
qoni.unstableRequest(input) / request(input)Gateway request with method/path and optional token/query/body/headers; both use the same implementation
Signing exportsbuildStringToSign(method, path, headers, params), buildSignature(secretKey, stringToSign), buildAuthorization(accessKey, secretKey, stringToSign)

doAnything.api.readArtifacts is the legacy structured snapshot interface. The current backend has no implementation; 0.9.0 requests the old route and returns 404. The repair candidate marks it deprecated and throws QoniUnsupportedError locally, with zero HTTP. For files use doAnything.artifacts({ token, runId }) / artifact.content(); for browser screenshots use capture: { screenshots: true } and onScreenshot. Neither implements the legacy structured snapshot.

qoni.qoni.delegateToken/completeDelegateToken are also callable; prefer the client methods. delegateAgent/completeDelegateAgent and old credential/token fields are compatibility entries discussed in migration notes below. QoniScopes exports scope constants, QoniProductScopes maps products to scopes, and QoniScopeBundles supplies presets; exported input, result, event, and error types follow these APIs. Advanced deployments can override individual discovery URLs with genauthHost/gumemHost/webAgentHost; ordinary integrations use host.

Interaction requests and business functions ​

When a task needs the user, the Agent pauses and raises an interaction, then waits for the application to send back the user's decision. Interactions are typed by what the user has to do, not by business: draw one screen per type and the application handles interactions from any task. This section explains each type's data, the SDK methods it offers, and how one application wires them into its own UI. The business functions used by the Quickstart script in the downloadable examples (quickstart.ts) are application code in the downloadable interaction-demo.ts, not exports from @qoniai/qoni.

TypeWhat the user doesSDK methods
site_loginSigns in to a site in the controlled browseropenLogin(), confirmSignedIn()
take_controlOperates the browser by hand for anything other than signing in, such as a CAPTCHAconnectControl(), refreshControl(), releaseControl()
ask_userAnswers a question shaped by answerType and optionsanswer(), skip()
confirmationAllows or rejects an actionconfirm(), reject()
waitNothing to decide: the system is waiting for a rate limit or an external conditionretry()

Keep three things in mind. For the future design (including fill_form, absent from the published package), see Personal Agent: run DoAnything and handle interactions.

  • One interaction calls back several times: creation, status changes, and reconnect replays all call onInteraction again. Act only on status pending and dedupe by request.id; update one UI card per request.id.
  • Interactions arrive one at a time: the task pauses at an interaction; the Agent continues, and the next interaction appears, only after the current one is handled.
  • Methods follow the actions the backend offers: do not call a method when handle.can(kind) returns false; doing so throws QoniValidationError.

Handle and request data ​

onInteraction receives an SDK InteractionHandle, named handle here. request = handle.interaction contains data. handle.id/type/status/actions are convenient field accessors; read title/prompt/payload from request. Response methods are already bound to the run and declared action endpoint, so applications do not construct URLs. The second callback argument is event (RunEvent); use event.runId to associate the task. Quickstart uses only the first argument.

request fieldApplication usage
idStable request ID for updates and replay
typeChoose a sign-in, question, approval, or other business view
statuspending, active, resolved, expired, or canceled
title / prompt?Card title / optional explanation
createdAt / resolvedAt? / expiresAt?Creation, resolution, and expiration times
evidence?Optional { artifactId } evidence reference
payloadType-specific business data below
actionsCurrently offered actions, each with kind/label/method/endpoint and optional inputSchema

A question request with demo values, showing only the main fields:

jsonc
{
  "id": "demo-question-1", // Retained across updates to this request
  "type": "ask_user", // The Agent needs the user to answer a question
  "status": "pending", // Waiting for the user's response
  "title": "Please clarify the scope",
  "payload": {
    "question": "Only support issues, or all customer questions?",
    "answerType": "single_choice", // Text box, number, yes/no, single or multiple choice
    "options": [
      { "value": "support", "label": "Support issues only" },
      { "value": "all", "label": "All customer questions" }
    ]
  },
  "actions": [
    { "kind": "answer", "label": "Submit answer" } // method, endpoint, etc. omitted
  ]
}

Data and responses for the five request types ​

typepayloadWhat the application does
site_loginsites: [{ siteId, displayName, loginUrl }], optional monitorIdFinish controlled sign-in, then call handle.confirmSignedIn() to request a recheck; when openLogin() opened the login UI, it saves that login first
ask_userquestion, answerType, optional options: [{ value, label }]Send the actual answer through handle.answer(value), typed by answerType; use skip() only when offered
confirmationsummaryApprove with handle.confirm() or reject with handle.reject()
take_controlliveUrl, optional surface/reasonOpen the browser already handed to the user, then call handle.releaseControl() when finished
waitwaitKind (rate_limit/external/sleep), optional until/retryableShow waiting status; call handle.retry() only when offered and chosen by the user

answerType is text, number, boolean, single_choice, or multiple_choice; answer() takes a string, a number, a boolean, one option value, or an array of option values respectively, and throws QoniValidationError before sending a value that does not fit. The wire still calls this type clarification; since SDK 0.9.0 it is presented as ask_user, and a question with choices is single_choice. Interaction is a union over type, so switch (request.type) narrows payload in each branch. A request keeps its type for its whole lifecycle; when the backend settles it with an empty shell, the SDK keeps the original payload and only changes status. retryable: true is a data hint; actions still decides whether to enable a retry button.

Example application: five business functions ​

DemoRequestCard describes application display data and button callbacks. Logs show a summary; connect these data to your own card or dialog renderer. demoScreen is local, single-task storage. All five functions return synchronously without waiting for user input: wait() awaits its callbacks before consuming the next event.

ts
// Application demo, not SDK API. Rendering never submits a user decision.
import { QoniValidationError, type ActionKind, type AskUserAnswer, type Interaction, type InteractionHandle,
  type InteractionPayloadByType, type InteractionType, type OpenLoginResult } from '@qoniai/qoni'

type DemoPayload = Interaction['payload'] | undefined
type Replies = Partial<Record<ActionKind, (input?: AskUserAnswer) => Promise<void>>>
ts
export interface DemoRequestCard {
  id: string                     // Stable request ID
  type: string                   // Original business type
  status: string                 // Latest server status
  title: string
  prompt?: string | null
  content: Record<string, unknown> // Application display data
  disabled: boolean              // Disable after close or submission
  buttons: {
    kind: ActionKind
    label: string                // Backend button label
    onUserClick(input?: AskUserAnswer): Promise<void> // Triggered by the user
  }[]
}

// Local, single-task demo screen. Scope production storage by user and run.
export const demoScreen = new Map<string, DemoRequestCard>()

1. Request site sign-in ​

Display sites and let the user sign in through a controlled login UI, then click the signed-in button. confirmSignedIn() requests a recheck; it does not prove login success. The controlled login UI can be the deployment's flow, or the app can open it with openLogin({ siteId }) (below).

ts
export function requestUserLogin(payload: DemoPayload, handle: InteractionHandle): void {
  // Each site has siteId, displayName and loginUrl. Integrate the deployment's controlled login UI.
  renderRequest(handle, 'Sign in to the requested sites', () => ({
    sites: payloadFor(payload, handle, 'site_login').sites,
  }), {
    // Click after signing in. The SDK saves the login openLogin() opened, then asks the Agent to recheck.
    confirm_signed_in: () => handle.confirmSignedIn(),
  })
  // Register the opener when open_login is offered; openSiteLogin() calls it only after a user click.
  // A closed or locked request invalidates pending and current logins; same-ID updates are the same request.
  const card = demoScreen.get(handle.id)
  if (card && !card.disabled && handle.can('open_login')) {
    loginOpeners.set(handle.id, (siteId, profileId) => handle.openLogin({ siteId, profileId }))
  } else {
    loginOpeners.delete(handle.id)
    invalidateLogins(handle.id)
  }
}

Open the login browser from the app. When the backend offers open_login, handle.openLogin({ siteId }) starts a controlled login browser for the site the user chose and returns liveUrl and related fields. Show liveUrl only to the current user and never log it. After the user signs in, call confirmSignedIn(): since SDK 0.9.0 it first saves this login to the browser profile (later tasks reuse it), then asks the Agent to recheck and continue. Sign-in takes these two calls:

SituationWhat confirmSignedIn() does
The user is signed inSaves the login; the Agent rechecks and continues
The user did not actually sign inStill asks the Agent to recheck; the Agent raises a new site_login, which the app shows as a new card under a new request ID
The backend no longer holds the session (HTTP 409 no_pending_login, for example because it already saved the login)Same: asks the Agent to recheck
The backend could not save it yet (probeResult: "release_failed"; the session is kept)Throws a retryable QoniError coded site_login.save_failed and does not notify the Agent; calling it again saves again
Any other save error (for example a missing scope or a network failure)Throws a QoniError and does not notify the Agent

openLogin() and saving the login need QoniScopes.SITE_LOGIN_REQUEST and QoniScopes.SITE_LOGIN_CONFIRM respectively. Request both explicitly in scopes when delegating, and allow them in the AccessKey policy; without either one the SDK's token exchange is rejected. However many times it is called, one login session is saved once and the Agent is asked to recheck once. Handles from the same RunHandle share the login session, so confirmSignedIn() on the handle of a later callback also saves this login; a RunHandle from a new attach() does not. confirm() on the openLogin() result is deprecated and shares the same save with confirmSignedIn().

The two application functions below back the UI's open-login and finished-sign-in buttons. Finishing calls confirmSignedIn() through the same card's signed-in button, so the card's submission lock still applies. If the request has already ended when it is called, or the login session is not the latest one opened for this request, finishSiteLogin() rejects without saving the login or notifying the Agent. It also rejects while another login browser for the request is still opening; finish in the new browser once it is shown.

ts
// The app can open a controlled login browser for the chosen site; after sign-in, confirmSignedIn() saves it and asks the Agent to recheck.
const loginOpeners = new Map<string, (siteId: string, profileId?: string) => Promise<OpenLoginResult>>()
// Each request's current login: replaced only when a newer open succeeds; a failed open keeps it; finish it once.
const currentLogins = new Map<string, { attempt: number; login: OpenLoginResult }>()
// Attempt counter: a late older result never overrides a newer successful one.
const loginAttempts = new Map<string, number>()
// Bumped when the request ends or locks: every open in flight is void.
const loginEpochs = new Map<string, number>()
// Login browsers still opening: finishing meanwhile could save a login the SDK has already replaced.
const loginsOpening = new Map<string, number>()

function invalidateLogins(requestId: string): void {
  loginEpochs.set(requestId, (loginEpochs.get(requestId) ?? 0) + 1)
  currentLogins.delete(requestId)
}

function signedInButton(requestId: string) {
  // Only the current, actionable card can finish sign-in; closed or submitted cards have no button.
  const card = demoScreen.get(requestId)
  return card && !card.disabled ? card.buttons.find(button => button.kind === 'confirm_signed_in') : undefined
}

export async function openSiteLogin(requestId: string, siteId: string, profileId?: string): Promise<OpenLoginResult> {
  // siteId must come from the current card. Show the returned liveUrl only to this user; never log it.
  const card = demoScreen.get(requestId)
  const open = loginOpeners.get(requestId)
  const sites = (card?.content.sites ?? []) as { siteId: string }[]
  if (!card || card.disabled || !open) throw new Error('This request cannot open a login browser')
  if (!sites.some(site => site.siteId === siteId)) throw new Error('Choose a site listed in this request')
  const epoch = loginEpochs.get(requestId) ?? 0
  const attempt = (loginAttempts.get(requestId) ?? 0) + 1
  loginAttempts.set(requestId, attempt)
  loginsOpening.set(requestId, (loginsOpening.get(requestId) ?? 0) + 1)
  let login: OpenLoginResult
  try {
    login = await open(siteId, profileId)
  } finally {
    loginsOpening.set(requestId, (loginsOpening.get(requestId) ?? 1) - 1)
  }
  // The request ended while opening: this session does not become current.
  if ((loginEpochs.get(requestId) ?? 0) !== epoch || !signedInButton(requestId)) {
    throw new Error('This sign-in request is no longer actionable')
  }
  // A newer open already succeeded: this late result does not override it. If the newer one failed, this becomes current.
  const current = currentLogins.get(requestId)
  if (current && current.attempt > attempt) throw new Error('A newer login browser was opened')
  currentLogins.set(requestId, { attempt, login })
  return login
}

export async function finishSiteLogin(requestId: string, login: OpenLoginResult): Promise<void> {
  // Call after the user signs in at liveUrl. Only the request's latest, still-actionable login session is accepted.
  const button = signedInButton(requestId)
  if (currentLogins.get(requestId)?.login !== login || !button) throw new Error('This sign-in request is no longer actionable')
  // The user opened another login browser that is still opening: finish in the newest one once it is shown.
  if ((loginsOpening.get(requestId) ?? 0) > 0) throw new Error('Another login browser is still opening for this request')
  currentLogins.delete(requestId)
  // The "signed in" button calls confirmSignedIn(): the SDK saves this login, then the Agent rechecks and asks again if still signed out.
  await button.onUserClick()
}

2. Ask the user a question ​

Display question in a dialog and render it by answerType as a text box, number input, yes/no switch, single choice, or multiple choice; show each option's label and submit its value. Pass the user's answer to onUserClick(answer), which calls handle.answer(answer). The demo never substitutes a canned answer such as “only support issues.”

ts
export function askForTaskDetails(payload: DemoPayload, handle: InteractionHandle): void {
  // question asks for missing task details; answerType picks the control: text, number, yes/no, single or multiple choice.
  renderRequest(handle, 'Clarify the task scope or requirements', () => {
    const question = payloadFor(payload, handle, 'ask_user')
    // Options are { value, label }: show label, submit value
    return { ...question, choices: (question.options ?? []).map(option => option.value) }
  }, {
    // Submit the user's answer; its type follows answerType. The demo never generates an answer on their behalf.
    answer: value => handle.answer(value!),
    skip: () => handle.skip(), // Show only when offered
  })
}

3. Request approval ​

Display summary. Only the user's approve or reject click invokes confirm() or reject(); rendering the card does not approve the plan.

ts
export function requestUserApproval(payload: DemoPayload, handle: InteractionHandle): void {
  // summary describes the proposed operation for the user to review.
  renderRequest(handle, 'Review and approve or reject the plan', () => ({
    summary: payloadFor(payload, handle, 'confirmation').summary,
  }), {
    confirm: () => handle.confirm(), // User approves
    reject: () => handle.reject(),   // User rejects
  })
}

4. Offer browser control ​

Display liveUrl and the manual-step reason. The user clicks finished after completing the operation. The demo keeps the browser URL in card data rather than logs because it may carry access to the current browser.

ts
export function offerBrowserControl(payload: DemoPayload, handle: InteractionHandle): void {
  // liveUrl opens the browser already handed to the user; reason explains the manual step.
  renderRequest(handle, 'Complete a manual step in the browser', () => ({
    ...payloadFor(payload, handle, 'take_control'),
  }), {
    // Click when finished to hand control back. The browser URL is not written to demo logs.
    release_control: () => handle.releaseControl(),
  })
}

5. Show waiting status ​

Explain rate limiting, external conditions, or sleep, plus the optional expected recovery time. Without an offered retry action, the card is informational. When retry is offered, the user decides whether to invoke it.

ts
export function showWaitingStatus(payload: DemoPayload, handle: InteractionHandle): void {
  // waitKind is rate_limit, external or sleep; until is an optional expected end time.
  renderRequest(handle, 'Show why the task is waiting', () => ({
    ...payloadFor(payload, handle, 'wait'),
  }), {
    // Show retry only when offered and invoke it only after the user's choice.
    retry: () => handle.retry(),
  })
}

Connect user actions to SDK methods ​

buttons[].onUserClick is an application callback that invokes an actual SDK method. Pass submitted question text to the answer button callback, or invoke the selected approval button after the user's decision. Display functions never invoke these callbacks themselves.

For a question form, connect the application function submitTaskAnswer(requestId, answer): the request ID comes from the displayed request, and the answer comes from the user's control. It retrieves the latest card button, which invokes the actual handle.answer(answer). This is also application code, not an SDK method.

ts
export async function submitTaskAnswer(requestId: string, answer: AskUserAnswer): Promise<void> {
  // Application form handler: find the current card instead of retaining an old event's button.
  const button = demoScreen.get(requestId)?.buttons.find(button => button.kind === 'answer')
  if (!button) throw new Error('This request is not accepting an answer')
  // answer comes from the user's control. The callback validates it, then calls handle.answer(answer).
  return button.onUserClick(answer)
}

In a Web application, retain callbacks and SDK handles on a trusted server. The frontend displays required data and submits the request ID, action, and input to the application's own business endpoint. The server verifies the current user and run ownership, retrieves the latest request, and responds. Do not expose AK/SK or the SDK client to the browser.

The shared implementation has four responsibilities:

  1. Replace a card by id and rebuild buttons from current actions; stale buttons cannot submit.
  2. Display pending/active requests. Terminal updates clear buttons and retain original content; terminal-only replay creates no card. Closure is final, so later nonterminal replay is ignored.
  3. Validate user input and the offered action, then lock the request against double clicks and replayed submissions.
  4. Propagate submission errors without automatic retries or re-enabling old buttons. The one exception is a local SDK check that fails before sending (QoniValidationError without an HTTP status, such as text for a number question): nothing was sent, so the demo unlocks the card for the user to correct and resubmit. An error with a status came from the server after the request was sent, so the card stays locked. This local demo submits each request only once and does not automatically reopen buttons on later nonterminal events; failure is never treated as success.

One-response demo limit: SDK interaction data has no request revision number or typed method to query one interaction’s current state; updates arrive through interaction events. This demo retains its submission lock, so it cannot reopen sign-in or retry buttons when the same ID is offered again. Use Console or the deployment UI for renewed requests or uncertain submission outcomes. Production apps design recovery from server events and their own idempotency strategy. This limit belongs to the demo, not to SDK methods such as confirmSignedIn().

Shared card updates, payload narrowing, and response implementation
ts
// One-response local demo: a submitted ID stays locked. This is not an SDK restriction.
const submitted = new Set<string>()
const finished = new Set<string>()

function closeFinishedRequest(handle: InteractionHandle): boolean {
  // Closure is final. Old pending/active replay cannot revive even a terminal-only request.
  if (finished.has(handle.id)) return true
  if (handle.status === 'pending' || handle.status === 'active') return false
  finished.add(handle.id)
  const card = demoScreen.get(handle.id)
  // Terminal updates only change status; keep the original content (run handles fold them; hand-built handles may not).
  if (card) { card.status = handle.status; card.disabled = true; card.buttons = [] }
  console.log({ id: handle.id, status: handle.status, actions: [] })
  return true
}

function renderRequest(handle: InteractionHandle, scenario: string,
  content: () => Record<string, unknown>, replies: Replies): void {
  // Handle closure before reading payload; return immediately so the SDK can keep receiving events.
  if (closeFinishedRequest(handle)) return
  const card: DemoRequestCard = {
    id: handle.id, type: handle.type, status: handle.status,
    title: handle.interaction.title, prompt: handle.interaction.prompt,
    content: content(), disabled: submitted.has(handle.id), buttons: [],
  }
  if (!card.disabled) {
    card.buttons = handle.actions.filter(action => replies[action.kind]).map(action => ({
      kind: action.kind, label: action.label,
      onUserClick: input => submitUserChoice(card, handle, action.kind, input, replies[action.kind]!),
    }))
  }
  // Replace the card for this ID and rebuild buttons from the latest offered actions.
  demoScreen.set(handle.id, card)
  console.log({ scenario, id: card.id, type: card.type, status: card.status,
    title: card.title, actions: card.buttons.map(({ kind, label }) => ({ kind, label })) })
}

async function submitUserChoice(card: DemoRequestCard, handle: InteractionHandle,
  kind: ActionKind, input: AskUserAnswer | undefined, send: (input?: AskUserAnswer) => Promise<void>): Promise<void> {
  // Reject stale cards, closed requests and double clicks; the SDK also checks offered actions.
  if (demoScreen.get(card.id) !== card || card.disabled || submitted.has(card.id) || !handle.can(kind)) {
    throw new Error('This request card is no longer actionable')
  }
  // A login browser for this request is still opening: wait and finish in it (both the card button and finishSiteLogin()).
  if (kind === 'confirm_signed_in' && (loginsOpening.get(card.id) ?? 0) > 0) {
    throw new Error('Another login browser is still opening for this request')
  }
  if (kind === 'answer') {
    // Choices accept option values only (an empty string or an empty selection is legal); free text must not be blank.
    const choices = card.content.choices as string[]
    if (choices.length) {
      const picked = Array.isArray(input) ? input : [input]
      if (picked.some(value => typeof value !== 'string' || !choices.includes(value))) throw new Error('Choose an offered answer')
    } else if (input === undefined || (typeof input === 'string' && !input.trim())) {
      throw new Error('Enter an answer first')
    }
  }
  submitted.add(card.id)
  card.disabled = true
  try {
    await send(input)
  } catch (error) {
    // A local SDK check failed before sending (QoniValidationError without an HTTP status, say text for a number):
    // nothing was sent, so unlock the card. An error with a status came from the server after sending; keep the lock.
    // Other errors may mean an uncertain POST outcome; do not retry or re-enable old buttons automatically.
    if (error instanceof QoniValidationError && error.status === undefined) { submitted.delete(card.id); card.disabled = false }
    throw error
  }
}

function payloadFor<T extends InteractionType>(payload: DemoPayload, handle: InteractionHandle,
  type: T): InteractionPayloadByType[T] {
  if (handle.type !== type || !payload || payload !== handle.interaction.payload) {
    throw new Error(`Expected a ${type} request with its SDK payload`)
  }
  // switch (request.type) narrows payload directly; these functions are dispatched by type, so check type and identity, then return it.
  return payload as InteractionPayloadByType[T]
}

Types outside the five abstract types, such as the backend's secure_input, have no typed payload. The demo directs them to Console or the deployment's specialized UI. Never treat credential requests as questions or collect passwords through answer().

ts
export function showUnsupportedRequest(_payload: DemoPayload, handle: InteractionHandle): void {
  // Types outside the five (such as the current backend's secure_input): do not read, answer or collect credentials.
  if (closeFinishedRequest(handle)) return
  const previous = demoScreen.get(handle.id)
  if (previous) { previous.status = handle.status; previous.disabled = true; previous.buttons = [] }
  console.log({ id: handle.id, type: handle.type, status: handle.status,
    nextStep: 'Use Qoni Console or the deployment-specific interaction UI' })
}

Complete InteractionHandle method table ​

handle.can(kind) checks whether this request offers an action; invoking an unoffered method throws QoniValidationError. The SDK does not replace user consent, replay handling, or application user authentication.

Methodactions[].kindPurpose and limits
can(kind)—Check whether the current request offers an action
answer(value)answerSubmit the answer to a question, typed by answerType and checked locally before sending; not passwords or new credential-reference objects
skip()skipSubmit the user's choice to skip
confirm() / reject()confirm / rejectSubmit approval / rejection
openLogin({ siteId?, profileId? })open_loginOpens a controlled login browser for a site in payload.sites and returns an OpenLoginResult (profileId/siteId/sessionId/liveUrl/raw). siteId may be omitted when one site is listed; pass profileId to re-login an existing profile. Needs SITE_LOGIN_REQUEST
confirmSignedIn()confirm_signed_inSaves the login openLogin() opened (needs SITE_LOGIN_CONFIRM), then asks the Agent to recheck login state; without openLogin() it only asks the Agent
login.confirm({ displayLabel? })— (method on the openLogin() result)Deprecated: confirmSignedIn() saves automatically. Saves the login and ends this login session; returns a ConfirmLoginResult (profileId/active/state/probeResult/raw). Needs SITE_LOGIN_CONFIRM
connectControl()connect_controlTakes over the browser and returns a ControlSession (liveUrl/expiresAt?/status?/raw): liveUrl is the signed control URL for the user; show it only to that user. Call only if offered; the current takeover flow typically does not offer it
refreshControl()refresh_controlGets a new control URL when the current one expires, also as a ControlSession; later events also carry the new browser URL
releaseControl()release_controlHand browser control back after the user finishes
retry()retryRequest a retry after the user's choice
switchProfile()switch_profileExported, but the current backend neither offers nor implements it; not wired in the demo

ActionKinds, InteractionTypes, and InteractionStatuses are exported constants. Controlled login and takeover also require deployment permissions. Merge updates by id; terminal notifications may change type or omit the original payload, so retain earlier business content.

Download all examples, including interaction-demo.ts, tests, and runtime dependencies. Place interaction-demo.ts beside the Quickstart script. The examples run with tsx, so imports use the .js suffix. Page code and application functions are strictly typechecked against installed declarations. Method names and low-level API membership are checked automatically; table parameter descriptions are manually matched to the published declarations. Button and login-opening tests use the real InteractionHandle with simulated business input and simulated login-service responses; these tests do not constitute acceptance of private-site sign-in.

Complete runnable example ​

This program calls GenAuth delegation and introspection, Web Search, GUMem write and recall, and DoAnything. It also uses attach() to read the same search run again. Public sources and an isolated demonstration user keep sample preferences separate from business users.

Download the complete examples, or run from the documentation repository:

bash
cd examples/qoni
npm ci
npm run sdk

The downloadable scripts require Node.js 20.11 or later; the SDK itself supports Node.js 18+.

Set server-side QONI_ACCESS_KEY and QONI_SECRET_KEY. Creating an isolated demonstration user requires GenAuth user-management access. Input handling, parsing, file output, and interaction handling are implemented in the package. npm run typecheck checks every source against the installed SDK declarations.

ts
import { QoniScopes } from '@qoniai/qoni'
import { cleanupDemoUser, cliOptions, createContext, delegate, isolateDemoUser, object, parseJsonOutput, readWithRetry, save, saveArtifacts, searchHits, settled, settleRun, withCleanup } from './runtime.js'

// Create a client from server credentials; context belongs to this sample app.
const context = await createContext('sdk', { ...cliOptions(), skipUserResolution: true })
await withCleanup(context, async register => {
  // Keep Memory writes under an isolated demo user and delete that user on exit.
  register('isolated demonstration user', () => cleanupDemoUser(context))
  await isolateDemoUser(context)
  const qoni = context.qoni
  // One delegation covers search, browser tasks, and the demo user's Memory.
  const grant = await delegate(context, 'sdk-demonstration', ['webSearch','doAnything'],
    [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_MEMORY_WRITE, QoniScopes.GUMEM_MESSAGE_WRITE])
  const token = grant.token
  // Query effective scopes for audit; readWithRetry is the sample's read retry helper.
  const { data: tokenInfo } = await readWithRetry(context, 'delegation introspection', () => qoni.genauth.introspectDelegationToken({ token }))
  if (object(tokenInfo).active !== true) throw new Error('The delegation token is not active')

  // Find official Firefox sources, with at most three results per query.
  const search = await qoni.webSearch.run({
    token,
    prompt: 'Mozilla Firefox official features',
    maxResultsPerQuery: 3,
  })
  save(context, 'search-ref.json', { runId: search.id, auditId: grant.auditId })
  register('Web Search run', () => search.cancel('SDK demonstration cleanup'))
  // wait() reads the final result; searchHits() validates the actual results[].
  const result = await settleRun(context, search)
  save(context, 'search-result.json', result)
  settled(result)
  const hits = searchHits(result.output)
  // Reattach by the original run ID to inspect the same search after a restart.
  const attached = await qoni.webSearch.attach(search.id, { token })
  if ((await readWithRetry(context, 'attached run status', () => attached.status())).status !== 'succeeded') throw new Error('The reattached run has not succeeded')

  // Create a session for the demo user; writes and recall share its sessionId.
  const sessionId = `sdk-demonstration-${Date.now()}`
  await qoni.gumem.createSession({
    token,
    userId: context.userId,
    sessionId,
    title: 'SDK demonstration',
  })
  // Write a confirmed demo preference; sync: true requests synchronous processing.
  await qoni.gumem.addMessages({ token, userId: context.userId, sessionId, sync: true,
    messages: [{ role: 'user', content: 'For this demonstration, my confirmed preference is concise explanations.' }] })
  // Recall task-relevant preferences and verify the new concise preference is found.
  const { data: memory } = await readWithRetry(context, 'GUMem recall', () => qoni.gumem.recall({ token, sessionId,
    query: 'What confirmed explanation preference should be used?', details: true }))
  save(context, 'memory.json', { sessionId, memory })
  if (!JSON.stringify(memory).includes('concise')) throw new Error('Recall did not include the demo preference just written')

  // Ask the Agent to read the actual search results and return one sourced fact.
  const run = await qoni.doAnything.run({ token,
    prompt: `Read the actual official sources below and provide one concise, supported Firefox feature with its URL. Return only a JSON object with feature and sourceUrl. Sources: ${JSON.stringify(hits)}`,
    capture: { screenshots: true } })
  save(context, 'run-ref.json', { runId: run.id, session: run.sessionRef })
  register('DoAnything run', () => run.cancel('SDK demonstration cleanup'))
  // Read the settled result; saveArtifacts() downloads files with Artifact.content().
  const summary = await settleRun(context, run)
  save(context, 'result.json', summary)
  settled(summary)
  await saveArtifacts(context, summary)
  // feature and sourceUrl are app-defined fields parsed and validated here.
  const feature = object(parseJsonOutput(summary.output))
  if (typeof feature.feature !== 'string' || !feature.feature || typeof feature.sourceUrl !== 'string' || !/^https:\/\//.test(feature.sourceUrl)) {
    throw new Error('The SDK example did not return a product fact with a source URL')
  }
  // Link search, browser-task, and delegation audit IDs for source traceability.
  const report = { passed: true, dataset: 'public-demo', runId: run.id, searchRunId: search.id,
    status: summary.status, hits, memorySessionId: sessionId, output: feature,
    auditId: grant.auditId, scopes: object(tokenInfo).scope }
  save(context, 'report.json', report)
  console.log(JSON.stringify(report, null, 2))
})

Outputs and run handles ​

Web Search RunResult.output contains search results[] with fields such as title, url, and snippet; it is not a JSON generator for arbitrary prompts. Pass these results into a subsequent DoAnything task for analysis or drafting.

Current DoAnything output is commonly wrapped as { answer: string }. The application parses JSON from answer and validates required fields and sources. The full response is saved in result.json; invalid output fails instead of producing a fabricated success.

run.wait() returns RunResult with runId, status, output, artifacts, terminalReason, isTaskSuccessful, and raw. Each Artifact exposes content() to download real bytes. downloadUrl is a temporary signed URL and should not be retained or exposed in logs.

See quickstart.ts in the downloadable examples for event and callback consumption: run npm run quickstart in examples/qoni, adding -- --events to use the event stream. onScreenshot receives an image and screenshot index starting at 0; onInteraction receives an SDK handle. Request fields, business functions, and user responses are documented in Interaction requests and business functions.

Local verification and evidence ​

bash
npm run verify

The default command runs Quickstart, the SDK program, and 23 browser scenarios, for 25 items. Add --include-track to include the two legacy Track scenarios, for 27 items in total. They use official 0.9.0 and cannot pass against the current /track/tracks backend; they are not repair-candidate examples. It creates an isolated test user and writes a result matrix. Request statuses, run/monitor IDs, scopes, and business outputs come from real services. Missing data, unsuccessful terminal states, unresolved interactions, or invalid output yield a nonzero exit code. Demonstration execution does not claim access to actual private business accounts.

The verifier also requests every source URL cited in the output. It requests only public HTTPS addresses and checks each redirect hop. A missing or unreachable source fails the sample; an anti-bot refusal (401, 403, or 429) is recorded in the matrix as unverified rather than treated as a missing source. Idempotent reads such as introspection, run status, and Memory recall retry at most twice when the SDK marks an error retryable; creating runs, delegating, writing Memory, and responding to interactions are never retried. Since SDK 0.7.0, wait() retries the same kind of error up to twice when it reads the run detail and artifact list after the run ends; if it still fails, the examples call wait() again without callbacks to read the same run's terminal state; the run is not restarted and callbacks do not fire twice.

Source requests check HTTP reachability, not the returned page content; a challenge page with HTTP 200 can pass. Business facts still need review.

passed-unverified means SDK execution and output structure passed while at least one source could not be independently rechecked. The summary lists these scenarios separately; it does not establish factual accuracy.

The inline code blocks on this page are checked verbatim by the documentation repository's page-code verifier. It assembles the page's code in order, type-checks it strictly and runs it with a real AccessKey.

bash
# Run from the documentation repository root (after npm ci in examples/qoni)
npm run qoni:verify-doc-snippets -- --live

By default, --live runs this page's SDK programs to their verification endpoints.

Error handling ​

All SDK errors extend QoniError and carry code, status, requestId, traceId, auditId, and a retryable flag. Regular HTTP requests are not retried automatically (since 0.7.0, wait() retries its post-run detail and artifact reads up to twice); your application decides the retry policy for errors with retryable: true. The events() SSE stream reconnects according to sseMaxRetries.

Error classTriggered when
QoniValidationErrorLocal input validation fails, for example a malformed scope or silent delegation without user
QoniUnsupportedErrorAdded only in the Unreleased repair candidate: unsupported legacy Track or snapshot operations throw unsupported.operation locally; absent from the original 0.9.0 package
QoniAuthErrorThe accessKey / secretKey signature is rejected
QoniPermissionDeniedErrorThe delegation token lacks a required scope (HTTP 403)
QoniTokenExpiredErrorThe delegation token has expired
QoniDelegationRequiredErrorA product call is missing token, or the gateway rejects the token
QoniRateLimitErrorRate limited (HTTP 429)
QoniTimeoutErrorA request or wait() times out; the run continues server-side and can be reattached with attach()
QoniUpstreamErrorA downstream product service fails

Original Quickstart delegation style ​

Since 0.8.0, qoni.genauth.delegateAgent() retains the original delegation style. Tasks still use qoni.doAnything.run({ token: grant.token, prompt }). This compatibility applies to the current @qoniai/qoni package; it does not restore old package names or nonexistent server routes.

Input or resultCurrent behavior
userId / user: { id }Required in silent mode. Existing behavior is retained: a nonempty top-level userId takes precedence, otherwise user.id is used. Supply only one form in application code
agentKey / agentSame Agent audit label. Both must agree; this is not an independent Agent identity credential
expiresIn: '15m' / 900Converted to integer seconds before signing; integer s/m/h/d durations or numeric seconds strings, between 60 and 86400 seconds
scopesExplicit scopes supported by the server. products remains available and is unioned with scopes, not used to narrow them
grant.tokengenauth.delegateAgent() returns the grant directly. Root delegateToken() retains response.data.token and meta
Legacy mailbox.*, web.search, web.extract / deniedScopesNot implemented by this delegation service; rejected rather than broadened to product permissions

read/manage constrain product APIs, not mailbox actions. "Draft only" is a task instruction; preventing email sending or deletion requires actual application and server enforcement.

Migrating from versions before 0.4.0 ​

0.4.0 (2026-08-18) is a breaking rename release; the server-side wire contract is unchanged:

  • The package was renamed from @eazo/anima to @qoniai/qoni, the recommended primary constructor was consolidated as Qoni, and environment variables moved to the QONI_* prefix. The public 0.4.0 package still includes the old long-form constructor export as a compatibility alias; new code should use only Qoni.
  • Root qoni.delegateAgent / qoni.completeDelegateAgent are deprecated in favor of delegateToken / completeDelegateToken; the exchange must include the server-saved grantId (the browser callback does not carry it) and pass the callback's grant_state as state — the old { code, state } form is no longer supported.
  • Since 0.8.0, both top-level userId and user: { id } are supported; the constructor option accessKeyId is now accessKey.
  • Gateway routes remain under /api/v3/eak/*, and token claims and eak.* error codes are unchanged; applications should not construct these internal values themselves.

Publication status of other SDKs ​

As of August 19, 2026, the QoniAI GitHub organization has one public SDK repository: qoni-sdk-node. No unified Qoni SDK for Python, Java, Go, PHP, or C# is currently visible in that organization.

Product-specific GUMem, Web Agent, or GenAuth SDK references elsewhere in these docs are product integration material or historical SDKs. They do not prove that a corresponding unified SDK has been published by the QoniAI organization. Before selecting a production dependency, verify its package registry entry, source repository, and released version instead of inferring publication from an example package name.

Check the installation ​

Confirm that npm resolves the current package version:

bash
npm view @qoniai/qoni version

Then verify that the application reads QONI_ACCESS_KEY and QONI_SECRET_KEY only on the server and uses a real GenAuth user ID for silent delegation.

Next step ​