/** * Construction options for `QodoClient` and friends. * * Strict — no `any` at the public surface. Optional fields that may grow over * time (e.g. `headers`, `transport`) carry an index-signature-free interface so * adding a key is a SemVer-additive change rather than a silent extension. */ import type { OTelAPI } from '../tracing/types.js'; import type { MessageId, SessionId } from '../qar/ids.js'; import type { WSTransportFactory } from './transport.js'; import type { SkillsConfig, SkillSpecifier } from '../skills/types.js'; /** * Auth strategy passed to the client. * * `'env'` reads `QODO_API_KEY` (and falls back to `~/.qodo/auth.key`) via the * existing `AuthProvider`; explicit `{ apiKey }` uses the literal value. * * No environment selector beyond this — 2.0 talks to QAR only. The endpoint * itself is `baseUrl`. */ export type ClientAuth = 'env' | { readonly apiKey: string; }; /** Connection-level retry / backoff hints. */ export interface ClientReconnectOptions { /** * Hard cap on reconnect attempts before the iterator throws. Default `6`. * `0` disables reconnect (the first transport drop is terminal). */ readonly maxAttempts?: number; /** * Initial backoff delay in milliseconds, applied from the *second* * attempt. The first reconnect attempt always fires immediately (0ms) so * a transient blip — the common case — recovers in ~1 RTT rather than * paying a full backoff. Default `1000`. */ readonly initialBackoffMs?: number; /** Backoff multiplier applied each attempt. Default `2`. */ readonly backoffMultiplier?: number; /** * Upper bound on a single backoff delay, in milliseconds. Caps the * exponential growth so a deep attempt budget doesn't produce * multi-minute waits. Default `30000`. */ readonly maxBackoffMs?: number; } /** * WebSocket keepalive tuning. * * The default (`ws`) transport sends a protocol-level ping every * `pingIntervalMs` and proactively tears the socket down — triggering the * normal reconnect loop — if no pong returns within `pongTimeoutMs`. This * serves two ends: half-open sockets (LB drop, NAT timeout, vanished pod * with no close frame) are detected in seconds instead of waiting minutes * for the OS TCP timeout, and the periodic ping keeps the flow non-idle so * an idle-timeout load balancer never culls a quiet-but-live connection * (e.g. during a long tool call or a HITL wait). * * Keepalive is **enabled by default**. Set `enabled: false` to turn it off. * A BYO transport that can't send a protocol ping (e.g. a browser WHATWG * `WebSocket` adapter) may ignore these values. */ export interface ClientKeepaliveOptions { /** * Disable keepalive entirely. Default `true` (keepalive on). Setting * `false` reverts to the prior behavior — drops are only detected on a * close frame or the OS TCP timeout. */ readonly enabled?: boolean; /** Interval between pings, in milliseconds. Default `20000`. */ readonly pingIntervalMs?: number; /** * How long to wait for a pong before declaring the socket dead and * reconnecting, in milliseconds. Should be well under `pingIntervalMs`. * Default `10000`. */ readonly pongTimeoutMs?: number; } /** * App-level backpressure tuning. QAR emits `flow.pause` when its * per-session in-flight envelope counter crosses the high watermark; the SDK * queues subsequent `task.start` / `task.continue` until `flow.resume` * arrives. `tool.response` and `task.cancel` ignore the pause — the server is * either awaiting them (tool answer) or the user wants out (cancel). */ export interface ClientBackpressureOptions { /** * Soft cap on the number of envelopes the SDK will hold in its paused queue. * Past this, public-API calls reject with `QodoBackpressureError`. Default * `100`. Healthy traffic typically queues 0–2 envelopes; reaching the cap * means the consumer is driving far above QAR's in-flight ceiling. */ readonly maxPausedQueueSize?: number; } /** * Construction options for `QodoClient`. * * Required: `baseUrl` (the QAR deployment), `auth`. Everything else is optional. */ export interface ClientOptions { /** * Base URL of the QAR deployment, e.g. `https://qar.qodo.ai`. The client * derives the WS endpoint (`wss:///v2/qar/ws/connect`) and HTTP routes * (`/v1/info`, `/v1/auth/me`, …) from this. */ readonly baseUrl: string; /** Managed-tools HTTP contract. Defaults to v1; v2 requires an explicitly capable runtime. */ readonly managedToolsApiVersion?: 1 | 2; /** How to obtain the bearer token. */ readonly auth: ClientAuth; /** * Optional OTel API surface (typically `import * as otel from '@opentelemetry/api'`). * * If provided, the client emits `qar.client.*` spans and propagates W3C * Trace Context on every outbound envelope. If omitted, the client * no-ops cleanly — no warnings, no bundled fallback TracerProvider. */ readonly otel?: OTelAPI; /** Reconnect / backoff hints. */ readonly reconnect?: ClientReconnectOptions; /** * WebSocket keepalive tuning. Keepalive is on by default (ping every 20s, * 10s pong timeout); pass `{ enabled: false }` to disable or tune the * intervals. See {@link ClientKeepaliveOptions}. */ readonly keepalive?: ClientKeepaliveOptions; /** App-level backpressure tuning. */ readonly backpressure?: ClientBackpressureOptions; /** * Extra HTTP headers attached to non-WS calls (`/v1/info`, `/v1/auth/me`, …). * The bearer header is set automatically from `auth`. */ readonly headers?: { readonly [name: string]: string; }; /** * Identifies the calling client so the backend can attribute a call's origin * (e.g. `cli` vs a direct SDK consumer). Sent as the `Qodo-Client-Type` header * on every HTTP request. Default `'sdk'`; the CLI sets `'cli'`. A product/ * version suffix is fine (`cli/1.2.3`) as long as the leading token is the * stable origin. An explicit `headers['Qodo-Client-Type']` wins if both given. */ readonly clientType?: string; /** * Version of the calling client, sent as the `Qodo-Client-Version` header so * the backend can stamp it on analytics (e.g. `cli_version` on tool_run * events). Defaults to this SDK's own version; the CLI overrides with its * version. An explicit `headers['Qodo-Client-Version']` wins if both given. */ readonly clientVersion?: string; /** * Override the WebSocket transport factory. * * Default: `defaultTransportFactory`, which opens a real WS connection via the * `ws` package. Tests inject a fake factory to avoid the network. Useful for * consumers running in environments where `ws` doesn't fit (e.g. browsers * with the WHATWG WebSocket — they'd write a thin adapter here). */ readonly transport?: WSTransportFactory; /** * Skills foundation config. When provided, the client builds a * `SkillsManager` at construction; `tasks.startWithAgent` and * `tasks.startWithGraph` auto-inject a slim `` block * into inline-agent instructions before the envelope hits the wire. * * Omit to disable; pass `{ sources: [] }` to opt in to the manager * surface but ship no skills. The block lands inside the consumer's * existing instructions text — a first-class `available_skills` wire * field is a follow-up. */ readonly skills?: SkillsConfig; /** * Opt in to sending the bearer token over an insecure (`http://` / `ws://`) * transport. * * `connect()` always emits the `Authorization: Bearer ` header on the * upgrade. Over `ws://` that header is plaintext on the wire — a passive * observer can capture the token. The default rejects insecure URLs in * `connect()` (before the upgrade actually happens) so the failure surfaces * with a typed `QodoClientOptionsError` and the bearer is never sent. * * Set to `true` for **local development only** (e.g. `ws://localhost:8000` * against a dev QAR). Production consumers must use `wss://`. * * Default: `false`. */ readonly allowInsecureTransport?: boolean; } /** Per-task options forwarded to `tasks.start` / `tasks.continue`. */ export interface TaskOptions { /** * Abort signal for cooperative cancellation. The iterator stops yielding and * an in-flight task is cancelled via `task.cancel`. */ readonly signal?: AbortSignal; /** * Caller-pinned skills for this run. Each entry is a bare name, * vendor/name, or vendor/name@version specifier; the SkillsManager * resolves and marks them `[loaded]` in the slim index. Unresolved * names throw `SkillNotFoundError` synchronously. `undefined` falls * back to `SkillsConfig.activate`; `[]` is "explicitly pinned nothing" * (overrides constructor default). * * Records the pinned set in the slim index; runtime body injection * (the L1 caller-pinned activation path) is provided by the * `qodo-skills` MCP server. */ readonly skills?: readonly SkillSpecifier[]; /** * "Current file" context for the `paths:` filter. Consumers without a * current-file concept omit this; the renderer treats `paths:` as * no-op for those skills. */ readonly skillsCurrentFile?: string; /** * Optional caller-supplied work-unit identity for cross-pod session * serialization on the deterministic-key path. * * When OMITTED (default), QAR mints a fresh `uuidv7()` and derives * `session_id` from that — every `task.start` call produces a unique * session_id and no two pods coordinate. This is the **DX default** — * the right shape for one-shot scripts, CLI tools, single-process * consumers, and any caller that doesn't need cross-pod coordination. * * When PRESENT, QAR derives `session_id = uuidv5(QAR_NS_V1, * tenant_id + ":" + idempotencyKey)`. Two pods receiving the same * `(tenant_id, idempotencyKey)` derive the same `session_id` and * serialize on the same lease key (`qar.session_runs`) — the basis * for Slack-thread-per-pod, PR-per-pod, customer-issue-per-pod * scale-out topologies. Encode your app-level fine scope INTO the * string: * * - Slack threads: `${teamId}:${channelId}:${threadTs}` (or * `${teamId}:dm:${userId}` for DM fallback) * - GitHub PRs: `${owner}/${repo}:pr:${prNumber}` * - Customer issues: `${customerId}:${issueId}` * * **Validation** (mirrors QAR's `Field(min_length=1, max_length=512, * pattern=r"^[^\x00]+$")`): 1–512 **Unicode code points** (NOT bytes * — counted with `[...str].length`), no NUL byte. Invalid values * throw {@link QodoIdempotencyKeyValidationError} synchronously, * before the wire write. * * **Anti-patterns**: * - `idempotencyKey = Date.now()` — looks deterministic, collides * on rapid-fire calls within clock resolution. * - `idempotencyKey = randomUUID()` / fresh random per call — works * correctly (every call gets a unique session_id) but defeats * the purpose; omit the field and let the server mint instead. * * **Lost-ack retry contract** (mandatory): the omitted-key path is * **non-idempotent** across `task.start` retries. If the client * issues `task.start` without `idempotencyKey`, doesn't receive the * `task.started` ack (transport drop, timeout), and naively retries, * the server mints a NEW uuidv7 and a SECOND fresh session starts. * The SDK MUST NOT silently retry an uncertain `task.start` without * a sticky key. Consumers requiring lost-ack idempotent retry MUST * pass a deterministic key (domain-derived or a sticky per-attempt * UUIDv7). A Phase-2 `attemptKey()` helper that codifies the * sticky-per-attempt pattern is tracked separately. */ readonly idempotencyKey?: string; /** * Explicit cold-address `session_id`. * * Meaningful only for `tasks.continue` (and via {@link ResubscribeOptions} * for `tasks.resubscribe`). On `tasks.start` this field is IGNORED at * both the wire and span layers: * - The wire envelope strips `session_id` (admission CREATES the * session); a caller-supplied value cannot reach the server. * - SDK-side span attribution (`subscribeAndSend`) explicitly gates * this field on `outbound.kind !== 'task.start'` so a stale caller * value can't pollute the `task.start` span's `qar.session_id` * attribute before the server-derived id arrives via the * `task.started` ack. * * Pass the value the SDK previously exposed on `await stream.sessionId` * (or persisted alongside the `taskId` from a prior admission) when * reusing the `taskId` across processes. * * When OMITTED on a continue call, the SDK looks up the per-Task * session captured from the in-process `task.started` ack. If neither * a captured session nor an explicit override is available, the SDK * throws {@link QodoColdAddressError} synchronously BEFORE the wire * write — the doctrinal correct shape for cross-process recovery, * replacing a zero-UUID sentinel that would otherwise send a rejected * frame to the wire. */ readonly sessionId?: SessionId; } /** * Options for `tasks.cancel`. The payload arg already carries the cancel * reason; this options bag is for control fields (abort signal, * cold-address `sessionId`). */ export interface TaskCancelOptions { /** Abort signal for cooperative cancellation of the cancel itself. */ readonly signal?: AbortSignal; /** * Explicit cold-address `session_id` for cancelling a task across * processes. Same semantics as the `sessionId` field on {@link TaskOptions}: * required when the `taskId` was hydrated from durable storage rather * than observed from an in-process `task.started`. */ readonly sessionId?: SessionId; } /** * Options for `tools.respond`. Today carries only the cold-address * `sessionId` override; an explicit options bag is the right shape for * future control fields (e.g. per-call abort, span overrides) without * breaking the existing overload signature. */ export interface ToolRespondOptions { /** * Explicit cold-address `session_id` for responding to a `tool_call_id` * whose originating `tool.request` was never observed in this process * (cross-process HITL flow: a long-running approval lands on a * different SDK instance than the one that emitted the request). */ readonly sessionId?: SessionId; } /** * Options for `tasks.resubscribe`. Mirrors `TaskResubscribePayload` so consumers * can hand the same shape they'd hand the wire. */ export interface ResubscribeOptions extends TaskOptions { /** * The most recent `message_id` the client has already observed. QAR replays * from immediately after this point (server-side per-session ring buffer, * cap 1000 envelopes per the design doc). * * Maps to `since_message_id` on the wire when the client encodes the * envelope. Branded `MessageId` so consumers can't accidentally pass a * `TaskId` / `SessionId` here. */ readonly sinceMessageId?: MessageId; } //# sourceMappingURL=options.d.ts.map