/**
* 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