/** * Builder-side client for the Personal Server derivative question API. * * @remarks * A question is a standing prompt over the owner's source scopes. The * Personal Server answers it locally (the raw sources never leave the * machine except through its inference call) and writes the answer into the * derived scope as an ordinary derivative record, with lineage pointing at * the sources. The builder then reads the derived scope with its normal read * grant. Every source change re-runs the question, so a builder registers it * once and keeps reading a scope that stays up to date. * * One grant carries the whole pipeline, and it needs all three of: * * - a bare read entry for every source scope (the answer exposes them, so * the server refuses the registration otherwise: * `DERIVATIVE_SOURCE_NOT_GRANTED`), * - a bare read entry for the derived scope (to read the answer back), * - `write:` (the credential the question routes authorize * against). * * Authentication is the Write API's, with no new credential: the write * session bearer from {@link openWriteSession} plus a fresh, single-use * `X-Vana-Write-Signature` Web3Signed proof over every request, carrying the * grant id as a signed claim. These helpers own that: they open one session * per `{ signer, Personal Server, grant }`, reuse it across calls, sign a new * proof per request, and re-open the session once when a call comes back a * 401 the session is responsible for (the Personal Server keeps sessions in * memory and forgets them when it restarts; a 401 about the PROOF is * surfaced as it is, since a new session would not change it). * * Two rules govern the proof on these routes, and both are the server's * (`personal-server-ts` d91124d and later): * * - the signed `uri` claim covers the query string, not just the path, * because `?derivedScope=` is what the list route authorizes against; * - every call carries a fresh `nonce` claim, which becomes the server's * replay key. Without one the whole proof is the key, so two identical * polls signed inside the same second are refused as a replay. * * @category Protocol */ import { z } from "zod"; import { type PersonalServerWriteError } from "../errors.js"; import { type PersonalServerErrorBody } from "./personal-server-error-body.js"; import type { DataFileEnvelope } from "./data-file.js"; import { type WriteTransportRetryOptions } from "./write-request.js"; import { type ResolveWriteSignerOptions, type WriteSignerSource } from "./write-signer.js"; /** Path the question routes are mounted at. */ export declare const DERIVATIVE_QUESTIONS_PATH = "/v1/derivatives/questions"; /** The most source scopes one question may read. */ export declare const MAX_QUESTION_SOURCE_SCOPES = 16; /** The longest question text the Personal Server accepts. */ export declare const MAX_QUESTION_CHARS = 8000; /** The longest model id the Personal Server accepts. */ export declare const MAX_QUESTION_MODEL_CHARS = 128; /** How long {@link waitForQuestion} polls before giving up. */ export declare const DEFAULT_QUESTION_TIMEOUT_MS = 120000; /** How long {@link waitForQuestion} waits between polls. */ export declare const DEFAULT_QUESTION_POLL_INTERVAL_MS = 2000; /** Every state a question can be in. */ export declare const QUESTION_STATUSES: readonly ["pending", "ready", "failed", "stale"]; /** * `pending` (never computed) -> `ready` | `failed`; a source change or an * explicit recompute puts a computed question back to `stale`, which * settles as `ready` or `failed` again. */ /** * Every failure class a question can carry. A closed vocabulary, because the * status route serves it to a reader of the derived scope: it never carries a * scope name, the question or provider detail. * * - `inference_unavailable` — the provider or relay failed. The only * transient class: the Personal Server retries it on its own. * - `source_missing` — a source scope is deleted or holds no local data. * - `grant_invalid` — the registering builder's grant no longer covers what * the question reads. * - `internal` — anything else, including a permanent provider 4xx. */ export declare const DERIVATIVE_ERROR_CODES: readonly ["inference_unavailable", "source_missing", "grant_invalid", "internal"]; /** @see {@link DERIVATIVE_ERROR_CODES} */ export declare const DerivativeErrorCodeSchema: z.ZodEnum<{ internal: "internal"; inference_unavailable: "inference_unavailable"; source_missing: "source_missing"; grant_invalid: "grant_invalid"; }>; /** @see {@link DERIVATIVE_ERROR_CODES} */ export type DerivativeErrorCode = z.infer; export declare const QuestionStatusSchema: z.ZodEnum<{ pending: "pending"; ready: "ready"; failed: "failed"; stale: "stale"; }>; /** @see {@link QuestionStatusSchema} */ export type QuestionStatus = z.infer; /** Who registered the question: the owner, or a builder under a grant. */ export declare const QuestionRegisteredBySchema: z.ZodUnion; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"builder">; builder: z.ZodString; grantId: z.ZodString; }, z.core.$strip>]>; /** @see {@link QuestionRegisteredBySchema} */ export type QuestionRegisteredBy = z.infer; /** * A question registration as the Personal Server reports it (the answer of * register, get and list). */ export declare const DerivativeQuestionSchema: z.ZodObject<{ questionId: z.ZodString; derivedScope: z.ZodString; sourceScopes: z.ZodArray; question: z.ZodString; model: z.ZodPipe>, z.ZodTransform>; registeredBy: z.ZodUnion; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"builder">; builder: z.ZodString; grantId: z.ZodString; }, z.core.$strip>]>; status: z.ZodEnum<{ pending: "pending"; ready: "ready"; failed: "failed"; stale: "stale"; }>; error: z.ZodPipe>, z.ZodTransform>; errorCode: z.ZodPipe>>, z.ZodTransform<"internal" | "inference_unavailable" | "source_missing" | "grant_invalid" | null, "internal" | "inference_unavailable" | "source_missing" | "grant_invalid" | null | undefined>>; createdAt: z.ZodString; updatedAt: z.ZodPipe>, z.ZodTransform>; lastComputedAt: z.ZodPipe>, z.ZodTransform>; derivedVersion: z.ZodPipe>, z.ZodTransform>; derivedCollectedAt: z.ZodPipe>, z.ZodTransform>; }, z.core.$strip>; /** @see {@link DerivativeQuestionSchema} */ export type DerivativeQuestion = z.infer; /** * The 202 answer of a recompute request: the same registration view every * other question route answers, so a client needs one schema. * * @remarks * Older Personal Servers answered only * `{ questionId, derivedScope, status }` here. The full view is a superset * of those three fields, so code reading them is unaffected, but the answer * of a server before `personal-server-ts` d91124d no longer parses. */ export declare const QuestionRecomputeResultSchema: z.ZodObject<{ questionId: z.ZodString; derivedScope: z.ZodString; sourceScopes: z.ZodArray; question: z.ZodString; model: z.ZodPipe>, z.ZodTransform>; registeredBy: z.ZodUnion; }, z.core.$strip>, z.ZodObject<{ kind: z.ZodLiteral<"builder">; builder: z.ZodString; grantId: z.ZodString; }, z.core.$strip>]>; status: z.ZodEnum<{ pending: "pending"; ready: "ready"; failed: "failed"; stale: "stale"; }>; error: z.ZodPipe>, z.ZodTransform>; errorCode: z.ZodPipe>>, z.ZodTransform<"internal" | "inference_unavailable" | "source_missing" | "grant_invalid" | null, "internal" | "inference_unavailable" | "source_missing" | "grant_invalid" | null | undefined>>; createdAt: z.ZodString; updatedAt: z.ZodPipe>, z.ZodTransform>; lastComputedAt: z.ZodPipe>, z.ZodTransform>; derivedVersion: z.ZodPipe>, z.ZodTransform>; derivedCollectedAt: z.ZodPipe>, z.ZodTransform>; }, z.core.$strip>; /** @see {@link QuestionRecomputeResultSchema} */ export type QuestionRecomputeResult = DerivativeQuestion; /** The answer of a delete request. */ export declare const QuestionDeleteResultSchema: z.ZodObject<{ questionId: z.ZodString; deleted: z.ZodLiteral; }, z.core.$strip>; /** @see {@link QuestionDeleteResultSchema} */ export type QuestionDeleteResult = z.infer; /** * Connection, credential and transport shared by every question call. * * @remarks * The write session is opened on demand and reused for every later call * made with the same `signer` object, Personal Server, audience, grant and * `fetch`; a 401 re-opens it once and replays the call. */ export interface DerivativeQuestionAuthParams extends ResolveWriteSignerOptions { /** Personal Server origin, e.g. `https://ps.example.com`. */ personalServerUrl: string; /** Builder key: a viem `LocalAccount`, `WalletClient`, or `{ signMessage }`. */ signer: WriteSignerSource; /** * The grant the call runs under. It must carry `write:`, a * bare read entry for the derived scope, and a bare read entry for every * source scope. */ grantId: string; /** Web3Signed audience; defaults to `personalServerUrl`. */ audience?: string; /** `fetch` to use; defaults to `globalThis.fetch`. */ fetch?: typeof fetch; /** Extra request headers. */ headers?: HeadersInit; retry?: WriteTransportRetryOptions; /** Aborts the request (and, for {@link waitForQuestion}, the polling). */ signal?: AbortSignal; } export interface RegisterQuestionParams extends DerivativeQuestionAuthParams { /** * The scope the answer is written into. Must not share its first * dot-segment with any source scope, so put derivatives in the app's own * namespace. */ derivedScope: string; /** * The scopes the question reads: 1 to * {@link MAX_QUESTION_SOURCE_SCOPES} distinct scopes, none of them the * derived scope. They do not have to hold data yet: the question computes * once they do. */ sourceScopes: readonly string[]; /** The prompt, 1 to {@link MAX_QUESTION_CHARS} characters. */ question: string; /** Model id override; omitted = the Personal Server's default model. */ model?: string; } export interface GetQuestionParams extends DerivativeQuestionAuthParams { questionId: string; } export interface ListQuestionsParams extends DerivativeQuestionAuthParams { /** * The derived scope to list. A builder must name one (it may only see its * own questions on a scope it may write); the unfiltered list is the * owner's. */ derivedScope: string; } export interface RecomputeQuestionParams extends DerivativeQuestionAuthParams { questionId: string; } export interface DeleteQuestionParams extends DerivativeQuestionAuthParams { questionId: string; } export interface WaitForQuestionParams extends DerivativeQuestionAuthParams { questionId: string; /** Give up after this long (default {@link DEFAULT_QUESTION_TIMEOUT_MS}). */ timeoutMs?: number; /** Wait between polls (default {@link DEFAULT_QUESTION_POLL_INTERVAL_MS}). */ pollIntervalMs?: number; } export interface AskPersonalServerParams extends RegisterQuestionParams { timeoutMs?: number; pollIntervalMs?: number; } /** {@link askPersonalServer}'s answer. */ export interface AskPersonalServerResult { /** The settled registration (`status` is `ready`). */ registration: DerivativeQuestion; /** The derived record the Personal Server wrote and the builder just read. */ record: DataFileEnvelope; } /** * Map a non-2xx question answer onto the SDK's typed errors. `body` is the * already-read error body, for the caller that had to peek at it (a response * body can only be read once). */ declare function questionErrorFromResponse(response: Response, body?: PersonalServerErrorBody): Promise; /** * The typed error for a non-2xx answer of any `/v1/derivatives` route. * * @remarks * Shared with the reader-facing status client so both surfaces map the same * `errorCode` to the same error class. Not part of the package's public API. * * @internal */ export declare const personalServerErrorFromQuestionResponse: typeof questionErrorFromResponse; /** * Register a standing question over the owner's source scopes. * * @remarks * Sends `POST /v1/derivatives/questions`. The registration comes back * `pending` and the first compute is scheduled immediately; poll it with * {@link waitForQuestion}, then read `derivedScope`. * * @example * ```typescript * const registered = await registerQuestion({ * personalServerUrl: "https://ps.example.com", * signer, * grantId, * derivedScope: "coach.weekly", * sourceScopes: ["oura.sleep", "chatgpt.conversations"], * question: "How did my sleep relate to my mood this week?", * }); * ``` * @returns The registration, `status: "pending"`. * @throws {WriteRequestError} Before sending: a missing grant, a bad scope * list, an over-long question, a derived scope under a source's namespace. * @throws {DerivativeSourceNotGrantedError} 403: a source scope is not * read-granted to the builder (`details.scopes`). * @throws {DerivativeCycleError} 409: the question would make the derived * scope a transitive source of itself. * @throws {DerivativeQuestionInvalidError} 400 from the server. * @throws {DerivativeComputeUnavailableError} 503: no compute layer. * @throws {WriteForbiddenError} 403: the grant does not authorize writing * the derived scope. */ export declare function registerQuestion(params: RegisterQuestionParams): Promise; /** * Read one question's current state. * * @remarks * Sends `GET /v1/derivatives/questions/:id`. A builder only sees questions * it registered itself; anything else is a 404. * * @returns The registration, including `status`, `lastComputedAt`, * `derivedVersion` and (when it failed) `error`. * @throws {DerivativeQuestionNotFoundError} 404: unknown id, or not this * builder's question. */ export declare function getQuestion(params: GetQuestionParams): Promise; /** * List the questions this builder registered on a derived scope. * * @remarks * Sends `GET /v1/derivatives/questions?derivedScope=...`. The scope is * required for a builder: it is what the call is authorized against, which * is exactly why the signed proof commits to the query string as well as the * path. The target is built once and used for both, so the signature and the * request can never name different scopes. * * @returns The registrations, newest state included. * @throws {DerivativeDerivedScopeRequiredError} 400 * `DERIVATIVE_DERIVED_SCOPE_REQUIRED` when the server saw no * `?derivedScope=` (the SDK refuses an empty one before sending). */ export declare function listQuestions(params: ListQuestionsParams): Promise; /** * Ask the Personal Server to recompute a question now. * * @remarks * Sends `POST /v1/derivatives/questions/:id/recompute`, which answers 202 * and schedules the compute immediately instead of after the usual quiet * period. Use it to retry a `failed` question; a source change recomputes on * its own. * * @returns The full registration view, with the status the question was put * into (`pending` when it had never computed, else `stale`). Servers * before `personal-server-ts` d91124d answered only * `{ questionId, derivedScope, status }` here, which no longer parses. */ export declare function recomputeQuestion(params: RecomputeQuestionParams): Promise; /** * Delete a question registration. * * @remarks * Sends `DELETE /v1/derivatives/questions/:id`. The question stops * recomputing; the derived records it already wrote are left alone (delete * those through the data-point deletion path). * * @returns `{ questionId, deleted: true }`. */ export declare function deleteQuestion(params: DeleteQuestionParams): Promise; /** * Poll a question until it settles. * * @remarks * Calls {@link getQuestion} every `pollIntervalMs` until `status` is `ready` * or `failed` and returns that state; a `failed` question is returned, not * thrown, so the caller can read `error` and decide whether to * {@link recomputeQuestion}. All polls share the one write session and each * signs its own proof. * * @example * ```typescript * const settled = await waitForQuestion({ * personalServerUrl, * signer, * grantId, * questionId: registered.questionId, * timeoutMs: 60_000, * }); * if (settled.status === "ready") { * // read derivedScope * } * ``` * @returns The settled registration (`ready` or `failed`). * @throws {DerivativeQuestionTimeoutError} The question had not settled * within `timeoutMs`; it keeps computing on the server. * @throws Whatever {@link getQuestion} throws, and the `signal`'s abort * reason when the caller aborts. */ export declare function waitForQuestion(params: WaitForQuestionParams): Promise; /** * Register a question, wait for it, and read the answer: the whole builder * loop in one call. * * @remarks * {@link registerQuestion} + {@link waitForQuestion} + * {@link readPersonalServerData} on the derived scope, which is why the * grant needs a bare read entry for `derivedScope` on top of * `write:` and the source reads. The read is the plain * Web3Signed one; when the grant is priced, settle the 402 yourself with the * escrow-aware read from `@opendatalabs/vana-sdk/server` and use * {@link registerQuestion} and {@link waitForQuestion} directly. * * A question registered this way keeps recomputing after the call returns: * every later change to a source scope refreshes the derived record, and the * builder can read it again without registering anything. * * @example * ```typescript * const { registration, record } = await askPersonalServer({ * personalServerUrl: "https://ps.example.com", * signer, * grantId, * derivedScope: "coach.weekly", * sourceScopes: ["oura.sleep"], * question: "How did my sleep trend this week?", * }); * console.log(record.data.answer, registration.questionId); * ``` * @returns The settled registration and the derived record. * @throws {DerivativeQuestionFailedError} The question settled as `failed` * (`details.error` is the server's reason). * @throws Everything {@link registerQuestion}, {@link waitForQuestion} and * the read path throw. */ export declare function askPersonalServer(params: AskPersonalServerParams): Promise; export {};