/** * `TaskClient` — task lifecycle surface, mirroring QAR's `task.*` envelope kinds. */ import type { TaskEvent } from '../qar/index.js'; import type { SessionId, TaskId } from '../qar/ids.js'; import type { TaskInput, TaskStartByIdPayload, SessionState, TaskCancelPayload, TaskContinuePayload } from '../qar/payloads.js'; import type { FunctionToolDef, InlineAgentSpec, InlineGraphSpec } from '../qar/specs.js'; import type { TaskOptions, ResubscribeOptions, TaskCancelOptions } from './options.js'; import { Connection } from './connection.js'; import type { SpecsClient } from './SpecsClient.js'; import type { SpanRecorder } from '../observability/spans.js'; import type { TransportMetrics } from '../observability/transportMetrics.js'; import type { QarRegistryClient, RegisteredAgentRef } from './QarRegistryClient.js'; import { type SkillsManager } from '../skills/manager.js'; /** * Returned by `TaskClient.cancel`. Mirrors the `task.done` envelope payload. * * `status` carries the server-reported terminal value verbatim so callers can * tell whether the cancel actually applied (`'canceled'`), the server reported * failure (`'failed'`), or the task completed before the cancel was processed * (`'completed'` — race outcome). Don't collapse — collapsing loses the race * signal that the docs say `cancel()` returns. */ export interface TaskCancelResult { readonly task_id: TaskId; readonly status: 'canceled' | 'failed' | 'completed'; readonly error?: string; } /** * The shape returned by `tasks.start` / `tasks.startWithAgent` / * `tasks.startWithGraph` — an `AsyncIterable` augmented with * the synchronous outbound `taskId` and two Promises resolved on the * server's `task.started` admission ack. * * `taskId` is synchronous and equals the **first outbound** * `task.start.message_id`. Useful for the pre-admission cancel race: a * caller that wants to abandon the start before the ack arrives can * immediately invoke `client.tasks.cancel(stream.taskId)`. * * `admittedTaskId` is the **canonical post-admission task id** carried * on `task.started.payload.task_id`. On the omitted-key DX-default * path it equals `taskId` (no retry path possible — server can't mint * a different id). On the opt-in deterministic-key path with * `admission_in_progress` retries it **may differ** — the SDK rotates * `message_id` per retry, while the server's canonical task_id is * whatever the winning attempt's `task.started` reports. Use * `await stream.admittedTaskId` for cancel / continue / resubscribe * operations after admission completes. * * `sessionId` is the server-derived `session_id` carried on the same * envelope. Await it before any cross-pod operation that needs the * address (Bulletin forwarding, durable logging for replay recovery). * * **Rejection contract**: both Promises always settle — resolve on * admission, reject on `admission_stalled` / * `QodoAdmissionTimeoutError` / transport failure / pre-ack terminal. * Crucially, the SDK's internal admission driver advances the retry * loop **independent of whether the consumer iterates the stream**, * so `await stream.sessionId` BEFORE the first `for await` cannot * hang. */ export type TaskStartIterable = AsyncIterable & { readonly taskId: TaskId; readonly admittedTaskId: Promise; readonly sessionId: Promise; /** * Full admission ack including the idempotency discriminator. * Resolves with `{taskId, sessionId, isNew, state?, previousTaskId?, * previousState?}` when the server's `task.started` envelope arrives. * Settles atomically with `admittedTaskId` and `sessionId` (same * envelope, three views of the same data). * * `isNew === true` on the fresh-admission path (default — older QAR * builds without idempotent admission also report `true` because the * SDK defaults the absent field). `false` indicates QAR returned the * existing task for an already-dispatched session. * * `previousTaskId` + `previousState` are populated ONLY on terminal * re-dispatch (a NEW task with full `session_messages` history * inherited from a prior `completed | cancelled | failed` task). */ readonly admissionResult: Promise; }; /** * Resolved view of the `task.started` admission ack — what * `TaskStartIterable.admissionResult` carries. * * `taskId` + `sessionId` are concrete by the time this resolves (vs the * separate `admittedTaskId` / `sessionId` Promises which expose the same * fields). `isNew` discriminates fresh dispatch (`true`) from idempotent * existing-session return (`false`); `state` is present when * `isNew === false`. `previousTaskId` + `previousState` are populated * only on terminal re-dispatch. */ export interface TaskAdmissionResult { readonly taskId: TaskId; readonly sessionId: SessionId; readonly isNew: boolean; readonly state?: SessionState; readonly previousTaskId?: TaskId; readonly previousState?: SessionState; } /** * Resolved handle returned by `tasks.continueOrStart`. Composes the * admission ack with an `AsyncIterable` so consumers can * `for await` events directly off the handle: * * - `isNew === true` — the iterable yields the freshly-started task's * event stream (`task.delta` → `task.done`). * - `isNew === false && options.input !== undefined` — the iterable * yields events from the auto-emitted `tasks.continue(taskId, {input})` * dispatch the helper issued internally. * - `isNew === false && options.input === undefined` — the iterable * completes immediately (no events; consumer wanted a lookup, not a * dispatch). * * `previousTaskId` + `previousState` are populated only on terminal * re-dispatch. */ export interface TaskHandle extends AsyncIterable { readonly taskId: TaskId; readonly sessionId: SessionId; readonly isNew: boolean; readonly previousTaskId?: TaskId; readonly previousState?: SessionState; } /** * Resolved by `tasks.forceResume(idempotencyKey)`. Carries the recovered * task's id and the post-recovery session state (typically * `'auto-paused'` after QAR cancelled in-flight tool/HITL work; reports * the actual state on passthrough — e.g. already auto-paused or * terminal). */ export interface ForceResumeResult { readonly taskId: TaskId; readonly state: SessionState; } /** * `tasks.start` input — wire-shape `TaskStartByIdPayload` with `agent_id` * widened to accept either a bare string or a `RegisteredAgentRef` minted by * `client.qar.registerAgent`. * * The SDK never serializes a locally-registered agent's body to QAR — it * auto-fills the wire `agent_id` from `ref.name` and keeps the body in the * in-process registry for local dispatch. */ export type TaskStartInput = Omit & { readonly agent_id: string | RegisteredAgentRef; }; /** * `tasks.startWithGraph` input shape — inline-graph dispatch. * * Carries the declarative `graph` topology QAR's runtime will execute. The * `input` field is the same typed `TaskInput` shape as the by-id * variant — `user_query`, `params`, `metadata`. The `skill` field is * preserved from the wire envelope so a graph can be dispatched as the body * of a named QAR skill (Studio-generated workflows lean on this). */ export interface TaskStartGraphInput { readonly graph: InlineGraphSpec; readonly input?: TaskInput; readonly skill?: string | null; } /** * `tasks.startWithAgent` input shape — inline-AgentSpec dispatch. * * Carries the data-only safe subset (`apiVersion`, `instructions`, * `model`, optional `tools` / `skills` / `spawnable_agents` / etc.). The * `input` field mirrors the by-id variant — `user_query`, `params`, * `metadata`. The `skill` field passes through so a registered Qodo skill * can dispatch with the consumer-defined agent as its body. */ export interface TaskStartAgentInput { readonly agent: InlineAgentSpec; readonly input?: TaskInput; readonly skill?: string | null; } /** Per-call options for `tasks.startWithAgent`. */ export interface StartWithAgentOptions extends TaskOptions { /** * Opt-in pre-flight: run `client.specs.validate(agent)` (HTTP) against * QAR *before* opening the WebSocket task. Adds one round-trip but * catches the rules the SDK can't enforce locally — catalog tool * registration, skill version pinning, spawnable `agent_id` resolution * against the live registry. * * Default `false` — the SDK's pre-wire `collectInlineAgentSpecPreWireIssues` * covers the structural cases (forbidden `custom_code`, missing * required fields, unknown top-level keys). Set `true` in CI / * pre-deploy gates where the round-trip is affordable. */ readonly preflight?: boolean; } /** Per-call options for `tasks.startWithGraph`. */ export interface StartWithGraphOptions extends TaskOptions { /** * Opt-in pre-flight: run `client.specs.validate(graph)` (HTTP) against QAR * *before* opening the WebSocket task. Adds one round-trip but catches * rules the SDK can't enforce locally (model registry probes, AgentSpec * composition against the live agent table). * * Default `false` — the SDK's local validator covers the most common * mistakes. Set `true` in CI / pre-deploy gates where the round-trip is * affordable. */ readonly preflight?: boolean; /** * Override the graph-depth cap the SDK enforces client-side. Default * `DEFAULT_INLINE_GRAPH_MAX_DEPTH` (4) — matches the operator default. * Operators running a higher-limit deployment can raise this; the * server-side validator still enforces its own cap on dispatch. */ readonly maxDepth?: number; } export declare class TaskClient { private readonly resolveConnection; private readonly spanRecorder; private readonly registry; private readonly resolveSpecs?; /** * Transport-metric store passed through to each constructed * `TaskSubscription` so the connection's reconnect/replay path can * increment `replay_envelopes_received_total` / * `replay_anchor_missing_total` (the latter defensive — no released QAR * emits that code) against absorbed envelopes. Optional * for back-compat with consumers wiring `TaskClient` directly in * tests; production wires from `QodoClient`. */ private readonly metrics?; /** * Skills foundation manager. When provided, the `startWithAgent` and * `startWithGraph` paths await the manager's `discover()` (idempotent) * and append the rendered slim index to the spec's `instructions` * before the wire write. Undefined when the consumer didn't pass * `ClientOptions.skills` — those paths stay verbatim. */ private readonly skills?; /** * `defineFunctionTool` auto-bind hook. Called at every * `startWithAgent` / `startWithGraph` entry point so handlers * attached to `FunctionToolDef` entries (via the helper's * symbol-keyed handler bag) get installed on the `ToolClient` * before the wire `task.start` lands. Optional — when undefined * (e.g. `TaskClient` constructed in isolation in tests), the bind * step is skipped and consumers fall back to manual * `client.tools.onRequest(...)` registration. */ private readonly bindFunctionToolDefs?; /** * @param resolveConnection Returns the live `Connection` or throws if the * client isn't connected yet. * @param spanRecorder Recorder for `qar.client.task.*` spans. No-op * when OTel isn't configured. * @param registry Local agent + MCP registries. `tasks.start` reads * this to resolve `RegisteredAgentRef` → wire * `agent_id`. * @param resolveSpecs Lazily-resolved `SpecsClient` used by the * opt-in `preflight: true` path on * `startWithGraph()`. Lazy because `QodoClient` * constructs `tasks` before `specs`; defer * dereference until the caller actually asks * for pre-flight validation. */ constructor(resolveConnection: () => Connection, spanRecorder: SpanRecorder, registry: QarRegistryClient, resolveSpecs?: (() => SpecsClient) | undefined, /** * Transport-metric store passed through to each constructed * `TaskSubscription` so the connection's reconnect/replay path can * increment `replay_envelopes_received_total` / * `replay_anchor_missing_total` (the latter defensive — no released QAR * emits that code) against absorbed envelopes. Optional * for back-compat with consumers wiring `TaskClient` directly in * tests; production wires from `QodoClient`. */ metrics?: TransportMetrics | undefined, /** * Skills foundation manager. When provided, the `startWithAgent` and * `startWithGraph` paths await the manager's `discover()` (idempotent) * and append the rendered slim index to the spec's `instructions` * before the wire write. Undefined when the consumer didn't pass * `ClientOptions.skills` — those paths stay verbatim. */ skills?: SkillsManager | undefined, /** * `defineFunctionTool` auto-bind hook. Called at every * `startWithAgent` / `startWithGraph` entry point so handlers * attached to `FunctionToolDef` entries (via the helper's * symbol-keyed handler bag) get installed on the `ToolClient` * before the wire `task.start` lands. Optional — when undefined * (e.g. `TaskClient` constructed in isolation in tests), the bind * step is skipped and consumers fall back to manual * `client.tools.onRequest(...)` registration. */ bindFunctionToolDefs?: ((tools: readonly FunctionToolDef[]) => void) | undefined); /** * Send `task.start` and yield the resulting `TaskEvent` stream until `task.done`. * * The iterator is the canonical streaming primitive — consumers `for await` and * `switch (event.kind)` over the QAR discriminator. Breaking the iterator * before `task.done` arrives sends a best-effort `task.cancel` so the runtime * can free its resources promptly. * * `payload.agent_id` accepts either a wire-shape `string` or a * `RegisteredAgentRef` from `client.qar.registerAgent`. Refs are resolved * to `ref.name` for the outgoing envelope; the local agent body never * crosses the wire. */ start(payload: TaskStartInput, opts?: TaskOptions): TaskStartIterable; /** * Send `task.start { agent, input }` and yield the resulting `TaskEvent` * stream until `task.done` — inline-AgentSpec dispatch. * * The spec is validated client-side (the SDK-enforceable rule subset, * see `collectInlineAgentSpecPreWireIssues`) BEFORE the envelope hits * the wire. A `QodoInlineAgentValidationError` is thrown when any * forbidden-key or missing-required-field rule fails — the error's * `issues` array carries every violation with a `rule_id` + * JSON-Pointer `path` so consumers can route both client-side and * server-side rejections through the same branch logic. * * Opt into `{ preflight: true }` to additionally call * `client.specs.validate(agent)` (HTTP) before opening the task — that * catches the rules the SDK can't enforce locally (catalog tool * registration, skill version pinning, spawnable `agent_id` * resolution). Default `false`. * * Iterator + early-termination semantics match `start()` — breaking the * iterator before `task.done` arrives sends best-effort `task.cancel`. * Inline-agent spans carry `qar.agent.kind = "inline"` and * `qar.agent_id = "inline:"`. * * Server-side rejection: when QAR's `_handle_task_start` emits an * `error { code: 'agent_spec_rejected' }` envelope, the iterator * rejects with `QodoAgentSpecRejectedError` instead of yielding the * envelope as a `kind: 'error'` event — consumers don't need to * defensively narrow the error case out of `TaskEvent.kind`. */ startWithAgent(payload: TaskStartAgentInput, opts?: StartWithAgentOptions): TaskStartIterable; /** * Eagerly run `client.specs.validate(agent)` and, on success, dispatch * the `task.start` envelope via `subscribeAndSend`. Returns a * `Promise` that resolves once the wire write has * happened — kicked off in the public method body so the dispatch is * in flight by the time the consumer can read `stream.taskId`. * * `preflight: true` still means "validate before the wire write" per * the documented contract; we just no longer wait for iteration to * start the validate. * * Server-side rule failures throw `QodoAgentSpecRejectedError` so * consumers route SDK + server rejections through one branch. */ private runAgentPreflightAndDispatch; /** * Wrap the underlying subscription so a server-side `error { code: ... }` * envelope arriving on the iterator surfaces as a rejected typed error * instead of a yielded `kind: 'error'` event. Covers the documented * typed-error catalog: * * - `agent_spec_rejected` → `QodoAgentSpecRejectedError` * - the cancel / resume / HITL / graph code family → the matching * `QodoServerError` subclass * * Other error envelopes (`envelope_parse_error`, future codes the SDK * hasn't catalogued) pass through unchanged as `kind: 'error'` iterator events so * existing consumer code reading them keeps working. Callers that * want to convert an arbitrary error envelope into a typed throw * can wrap it themselves with `QodoUnknownServerError` (exposed via * the public surface) or `errorFromServerErrorEnvelope` (internal, * stable across the typed catalog). * * Non-error envelopes pass through unchanged. * * **Why this isn't an `async function*`.** Native async generators don't * propagate `.return()` to the inner iterator until the generator's * body has entered (one `.next()` call has fired). The SDK's * iterator-early-termination contract is that breaking the iterator * BEFORE the first envelope arrives still fires a best-effort * `task.cancel` — that relies on `.return()` reaching `sub.return()`. * The explicit iterator object below forwards every protocol call * straight to the underlying subscription's iterator so the * cancel-on-break path stays intact. */ private static wrapServerErrorsIterator; /** * Send `task.start { graph, input }` and yield the resulting `TaskEvent` * stream until `task.done` — inline-graph-spec dispatch. * * The graph is validated client-side (the SDK-enforceable rule subset, * see `validateInlineGraphSpec`) BEFORE the envelope hits the wire. A * `QodoInlineGraphValidationError` is thrown when any rule fails — the * error's `failures` array carries every violation with a `rule_id` + * JSON-Pointer `path` so consumers can route both client-side and * server-side rejections through the same branch logic. * * Opt into `{ preflight: true }` to additionally call * `client.specs.validate(graph)` (HTTP) before opening the task — that * catches the rules the SDK can't enforce locally (model registry probes, * AgentSpec composition against the live agent table). Default `false`. * * Iterator + early-termination semantics match `start()` — breaking the * iterator before `task.done` arrives sends best-effort `task.cancel`. * Inline-graph spans carry `qar.graph.entry`, `qar.graph.depth`, and * `qar.agent.kind = "inline"` attrs in addition to the standard * `qar.client.task.start` set. */ startWithGraph(payload: TaskStartGraphInput, opts?: StartWithGraphOptions): TaskStartIterable; /** * Eagerly run `client.specs.validate(graph)` and, on success, dispatch * the `task.start` envelope via `subscribeAndSend`. Returns a * `Promise` that resolves once the wire write has * happened. Validation rejection surfaces as * `QodoInlineGraphValidationError` so consumers don't need a second * `instanceof` branch for `SpecValidateResult` failures. */ private runGraphPreflightAndDispatch; /** * Compute the longest-path depth (edge count) from `entry` for the * `qar.graph.depth` span attr. Returns 0 when the graph has no edges * (single-node graphs). * * Delegates to the validator's shared `longestPathFrom` so the two call * sites can't drift on cycle handling. */ private static computeGraphDepth; /** * Return the documented `qar.agent.kind` value for a graph: `"inline"` * when any node carries an inline `AgentSpec` (or a nested `GraphSpec`), * `"agent_id"` when every node is a server-static reference. The * `qar.agent.kind` attribute is documented with the closed value set * `"inline" | "agent_id"` — keep this in sync with * `src/observability/attributes.ts`. */ private static computeAgentKind; /** * Send `task.continue` for an existing `task_id` and yield the resulting `TaskEvent` * stream until `task.done`. Same iterator semantics as `start`. * * Payload shape mirrors QAR's `TaskContinuePayload` minus `task_id` — the id is * already supplied as the first argument. `task.continue` carries no * `agent_id`/`skill` because those are bound at `task.start` time. */ continue(taskId: TaskId, payload: Omit, opts?: TaskOptions): AsyncIterable; /** * Cold-start helper. Unifies `task.start` and `task.continue` behind * a single call so consumers with deterministic `idempotencyKey` * derivation (Slack bots, CLI tools, IDE plugins, web apps without * server affinity) don't have to track in-memory * `(idempotencyKey → taskId)` mappings across process restarts. * * Behavior, gated on QAR's `task.started` admission response: * * - **No session yet** (`isNew === true`, no `previousTaskId`) — * fresh dispatch. The returned handle iterates the newly-started * task's event stream. * - **Existing session, `state === 'auto-paused'`** (`isNew === false`) — * QAR returned the existing `task_id`. The helper auto-emits a * `tasks.continue(taskId, { input })` to deliver the caller's * input; the returned handle iterates the continue subscription's * events. * - **Existing session, `state === 'running'`** (`isNew === false`) — * QAR returned the existing `task_id`. Same auto-continue path; * QAR queues the input until the next auto-pause boundary. * - **Terminal re-dispatch** (`isNew === true` + `previousTaskId` + * `previousState`) — QAR created a NEW task on the same * `session_id`, inheriting the full `session_messages` history * (B-raw). The handle iterates the new task's stream; consumer * can read `previousTaskId` + `previousState` to surface * "this is a new task after a prior failure" UX. * * **Busy-session recovery (`session_already_running`).** When the * derived session is still running on another connection / pod, QAR * rejects admission with `session_already_running` (lease contention). * The helper does NOT bounce the follow-up: it re-dispatches * `task.start` with the same `idempotencyKey` a few times with short * backoff (≈700ms total), riding the idempotent-admission path so the * turn is delivered once the holding run frees — either as a fresh * re-dispatch (the run went terminal) or via the auto-continue above * (the run settled non-terminal). If the session is *still* busy after * the bounded window, the returned Promise rejects with * {@link QodoSessionAlreadyRunningError} (branch on `.code` / * `instanceof`) so the caller can surface an honest "still working" * reply rather than hang. (Delivering a brand-new turn to a session * that stays busy is a server-side capability, not something the client * can force.) * * **The bounded retry is plain-graph-only.** It applies when the * dispatch takes the synchronous fast path. A graph that needs the * eager-async dispatch branch — one carrying MCP projection * (`mcpTools` / `mcpToolOverrides` on any inline-agent node) or when * skills discovery hasn't settled yet, or with `preflight: true` — has * no admission-retry driver attached, so `session_already_running` * rejects with {@link QodoSessionAlreadyRunningError} on the FIRST * attempt (no ≈700ms recovery window). The error is still typed and * catchable on `.code`, and the call never hangs; consumers on * MCP/skills graphs should expect the typed busy error immediately and * apply their own retry/backoff if they want the recovery window. * * `graphSpec` is required for the fresh-dispatch branch. On * already-dispatched sessions QAR ignores it (the existing task's * spec is the source of truth). The helper throws * {@link RequiredGraphSpecError} synchronously if `graphSpec` is * `undefined` — consumers who don't have a graphSpec on hand should * call {@link continue} or {@link forceResume} directly. * * Paired with QAR-side idempotent admission. Older QAR builds without * idempotent admission either reject `session_already_dispatched` (the * case this helper fixes) or — for fresh sessions — behave identically * (the helper defaults `isNew` to `true` when the field is absent in * the ack). */ continueOrStart(idempotencyKey: string, graphSpec: InlineGraphSpec | undefined, options?: { readonly input?: TaskInput; }): Promise; /** * Explicit stuck-session recovery. Sends `task.forceResume` and * awaits the matching `task.force_resumed` ack. * * Use case: a prior consumer process died mid-tool-call or mid-HITL; * the session is stuck `running` with outstanding `tool_call_id` / * `hitl_id` that will never resolve. `forceResume` triggers QAR to: * * 1. Cancel the in-flight tool call (mirrors the system-cancel * primitives used by `task.cancel` and the A2A bridge). * 2. Synth-resolve pending HITLs. * 3. Append a `{role: 'system', content: 'process restart — outstanding * tool call cancelled'}` marker to `session_messages` (audit trail). * 4. Transition the session to `'auto-paused'`. * * If the session is already `auto-paused` or terminal, QAR * passthroughs and reports the actual state. * * Returns `{taskId, state}` — the consumer can then call * `tasks.continue(taskId, { input })` with their next turn's input. */ forceResume(idempotencyKey: string): Promise; /** * Send `task.cancel` and await the matching `task.done { status: "canceled" }`. * * The returned promise resolves once the server confirms the cancel via * `task.done`. If the server reports failure (`task.done { status: "failed" }`) * during cancellation, `status: 'failed'` surfaces here so callers can * distinguish — they didn't ask for it, but it's still a terminal state. * * Cross-process recovery: pass `opts.sessionId` to cancel a task whose * `task_id` was hydrated from durable storage (i.e. the SDK never * observed the originating `task.started` in this process). Without the * override the SDK throws {@link QodoColdAddressError} before the wire * write. * * `opts.signal` semantic: **"signal aborts wait-for-task.done"**. If the * signal fires before `task.done` arrives, `cancel` throws * {@link QodoCancelAbortedError} with the task_id and the abort reason. * The wire `task.cancel` was already sent; the underlying task's * terminal ack still routes through its `tasks.start` / * `tasks.continue` subscription. The signal does NOT cause a second * `task.cancel` (that would be redundant — the consumer's intent is * "stop waiting", not "cancel twice"). Pre-aborted signals throw * synchronously before any wire write. */ cancel(taskId: TaskId, payload?: Omit, opts?: TaskCancelOptions): Promise; /** * Send `task.resubscribe { task_id, since_message_id? }` and yield replayed * `TaskEvent`s from the server-held ring buffer (per-session cap of 1000 * envelopes). No client outbox — durability is server-side. * * Use this when you have a `task_id` from a prior connection (e.g. across a * process restart) and want to catch up from a known anchor. For automatic * reconnect-on-drop within a single `connect()` lifetime, the SDK does this * for you transparently — see `qar.client.reconnect*` events on the * canonical iterator. * * There is no anchor-miss error. A missing or trimmed-out `sinceMessageId` * replays the *entire surviving buffer* instead of failing, silently — so * treat replayed events as at-least-once and dedupe on the envelope's * `message_id`. * * Rotation doesn't lose the terminal (trimming is oldest-first; * `task.done` is the newest append). The replay key's TTL does, and that * failure is a stall rather than an error: the stream is empty, so no * terminal ever arrives. Bound your own wait when resubscribing after a * long gap. See `docs/sdk2/reconnect-replay.md` §When the replay anchor is * stale. */ resubscribe(taskId: TaskId, opts?: ResubscribeOptions): AsyncIterable; /** * Pre-allocate a `MessageId`, register a `TaskSubscription` against it, then * send the outbound envelope using the same id. Subscribing before sending * eliminates the race where the server's first reply could land before the * subscription is in place. * * Pre-aborted signals short-circuit BEFORE the send so we never start * server-side work the consumer has already given up on. * * `preallocatedRootMessageId` lets the caller mint the root id at the * public-API boundary so it can derive `task_id` synchronously for * `task.start` envelopes (the wire protocol codifies * `task_id == task.start.message_id`). When omitted, we allocate fresh — * the `task.continue` / `task.cancel` / `task.resubscribe` call sites * already carry the task_id as an explicit arg. */ private subscribeAndSend; /** * Retry wrapper for `admission_in_progress` errors. * * When a deterministic-key admission collides with an in-flight admission * on the same `(tenant_id, idempotency_key)`, QAR emits * `error { code: 'admission_in_progress', retry_after_ms? }` instead of * starting work. The SDK MUST retry with the same payload (server * derives the same `session_id`) after the hinted delay — or, if absent, * after exponential backoff with jitter — capped at * `PENDING_ADMISSION_TIMEOUT` (5 min). The wrapper only fires on the * deterministic-key path; the DX-default omitted-key path never sees * `admission_in_progress` (each call mints a fresh `uuidv7` server-side * — no collisions possible). * * Returns an `AsyncIterable` that wraps the underlying * subscription. The wrapper: * * 1. Calls `buildAttempt(rootMessageId)` to register a fresh * `TaskSubscription` and emit the wire `task.start`. * 2. Reads the first event. * 3. If `error { code: 'admission_in_progress' }`: capture the * scoped `session_id` and `retry_after_ms` hint, sleep, retry * with a fresh `rootMessageId`. Lifecycle bookkeeping * (subscription close, span end) flows through the underlying * subscription's terminal handling on the error envelope. * 4. If `admission_stalled` typed throw arrives via the * wrap-server-errors layer: propagate (terminal, non-retryable). * 5. Otherwise: yield the first event and pass through every * subsequent event. * * After {@link PENDING_ADMISSION_TIMEOUT_MS} elapsed wall-clock time * the wrapper throws {@link QodoAdmissionTimeoutError} carrying the * scoped `session_id` so the caller can `task.resubscribe` if a * recovery path exists. */ private static wrapWithAdmissionRetry; /** * Wire an `AbortSignal` to the subscription. On abort, send `task.cancel` * (if we know the task id) and end the iterator. Returns a cleanup * function the subscription's `onClose` invokes so the listener doesn't * outlive the task (matters when a single AbortController is reused * across many short-lived tasks). */ private bindAbort; /** * Inject the rendered active-skills block + slim index into an * `InlineAgentSpec` synchronously. Validates caller-pinned skills * against the cached snapshot first; unresolved names throw * `SkillNotFoundError`. Returns the original spec when the catalog * is empty or when no `SkillsManager` is configured. * * When the caller pinned any skills, the SDK resolves the `requires:` * chain (with the depth/breadth/token caps applied), renders the * bodies into an `` block, and prepends that block to * the spec's `instructions` *before* the slim-index block. Trust * violations, cap breaches, cycles, or unresolved deps all throw * `QodoSkillError` so no partial body lands on the wire. */ private applyAgentSkillsInjection; /** * Inject the active-skills block + slim index into every inline-agent * node of an `InlineGraphSpec`. Mirrors `applyAgentSkillsInjection` * for the graph case. */ private applyGraphSkillsInjection; /** * MCP projection at the wire boundary. When the consumer set * `mcpTools` (or `mcpToolOverrides`), this: * * 1. Awaits the MCP pool's catalog-settle window so cold-start * `tools/list` round-trips can complete before projection. * 2. Runs `projectMcpTools` against the live pool snapshot. * 3. Partitions the diagnostics: * - `unknownMcps` and `unknownMcpTools` are policed by the * selector's `onMissing` policy. `'drop'` (default) soft-drops * the unresolved entries and emits one * `qodo.sdk.mcp.tool_dropped` event per drop; `'reject'` (opt-in) * raises `QodoUnknownMcpError` / `QodoUnknownMcpToolError` so a * typo surfaces at deploy time. * - `unreachableMcps` is policed per-MCP via the registry's * `unavailability` setting. `'fail'` raises * `QodoMcpUnavailableError`; `'warn'` omits the MCP's tools * and emits a console warning. * 4. Returns a fresh `InlineAgentSpec` with `tools[]` replaced by * the projected surface and `mcpTools` / `mcpToolOverrides` * stripped (SDK-only fields — QAR rejects them at the wire * validator via `D10-R5` if they slipped through). * * When the spec carries neither `mcpTools` nor `mcpToolOverrides`, the * helper returns the input verbatim — projection is opt-in, the v1 * `tools: FunctionToolDef[]` ergonomic continues to work. */ private projectMcpToolsForWire; /** * MCP projection over an `InlineGraphSpec`. Walks every node whose * `spec` is an inline `InlineAgentSpec` and applies * {@link projectMcpToolsForWire} to it; static refs and nested * subgraphs are recursed into. Returns a fresh `InlineGraphSpec` with * the same shape but every per-node `tools[]` replaced by the * projected surface. Diagnostic errors propagate from the first * node that fails — earlier nodes have already projected * successfully, but the wire write hasn't started so the early * partial work is invisible. */ private projectMcpToolsForGraphWire; /** * Resolve the caller-pinned roots into a rendered active-skills block * + telemetry events. Pure — relies on the already-loaded snapshot * (callers await `manager.discover()` before getting here). Returns * an empty result when no pins are effective. */ private composeActiveSkills; /** * Surface a caller-pin overflow as a loud failure. The renderer drops * pinned skills that would push the block past its hard cap * (`2 × charBudget`, floored at `charBudget + 4096`). Silently * truncating the wire payload would make the model behave as if the * caller never pinned those skills — throw instead so the caller * fixes the pin list or raises the budget. */ private throwIfPinsOmitted; /** * Resolve per-call pinned skills against the constructor-level * `SkillsConfig.activate` default: * * - `opts.skills === undefined` → fall back to the constructor's * `activate` (or `undefined` if none). * - `opts.skills === []` → explicit empty, no fallback. * - `opts.skills === [...]` → per-call wins outright. */ private resolveEffectivePinned; private buildRenderOptions; /** * Validate caller-pinned specifiers against the manager's already-loaded * snapshot. Throws `SkillNotFoundError` on a miss and * `SkillAmbiguousPinError` on a bare-name pin that matches multiple * vendors. Assumes the snapshot is present (the public entry-points * await `discover()` before calling this helper). */ private validatePinnedSync; /** Forward renderer telemetry events to the manager's configured sink. */ private dispatchSkillsEvents; } //# sourceMappingURL=TaskClient.d.ts.map