import type { Id } from "./_generated/dataModel.js"; /** * MCP Tasks (`io.modelcontextprotocol/tasks`) storage and lifecycle. * * The component owns the durable task rows and every legal status * transition; the host's HTTP handler owns the wire protocol * (`tools/call` task augmentation, `tasks/get`, `tasks/update`) and all * authorization. Owner-facing functions therefore take the caller's * resolved `ownerSubject` and answer a mismatch exactly like an unknown * id, so a task is never observable across callers. Trusted functions * (including `completeTask`, `failTask`, `requireTaskInput`, * `getTaskInternal`, `cancelPendingTasksForOwner`, `pruneTasks`) skip the * owner check: * only the host app can reach component functions, and the host's own * workflow is the intended caller. * * Functions are public `mutation` / `query` / `action` (not `internal*`) * for the same reason as `registry.*` and `dispatch.*`: component-internal * references resolve through `anyApi`, which cannot see internal markers. * The component boundary already prevents external callers. */ /** Retention defaults (documented in docs/tasks.md). */ export declare const TASK_DEFAULT_TTL_MS: number; export declare const TASK_MIN_TTL_MS: number; export declare const TASK_MAX_TTL_MS: number; /** Permitted serialized sizes (documented in docs/tasks.md). */ export declare const TASK_MAX_ARGS_BYTES: number; export declare const TASK_MAX_INPUT_BYTES: number; /** * Cap on a tool's result value. The row stores exactly that value, so * this is the whole story: nothing inflates it between the check and the * write, and the per-document budget stays what `docs/tasks.md` says * (256 + 64 + 64 + 64 + 8 KiB, comfortably inside Convex's 1 MiB). * * The `CallToolResult` a client polls is DERIVED from the value at read * time (see `descriptor`), never stored. Storing the envelope instead * would keep the value twice over, so a legal 256 KiB value could * serialize past a megabyte: the tool would commit its writes and the * client would then be told the call failed, for a result the same tool * returns fine inline. * * With compact serialization the derived form is bounded at about 3x this * (see `callToolResult`), i.e. ~768 KiB. That is comfortably inside the * function return limit (16 MiB); the 1 MiB figure is the DOCUMENT limit, * which the derived envelope never touches because it is never stored. `completeTask` still measures the derived form once, at write * time, so a value that somehow escapes that reasoning fails loudly then * rather than making the task unreadable on every poll afterwards. */ export declare const TASK_MAX_RESULT_BYTES: number; /** * Ceiling on the DERIVED envelope, checked once at completion. Not a * storage bound: the row holds only the value. * * Nearly unreachable by size, since compact serialization bounds the * envelope at 3x the value. Reachable by DEPTH, though: `callToolResult` * nests the value one level further under `structuredContent`, so a value * sitting exactly at `TASK_MAX_STRINGIFY_DEPTH` passes the value cap and * fails here. The message says "does not fit a readable CallToolResult", * which covers both, and failing loudly at completion is the point: * anything that escapes the reasoning above must not become a task that * is unreadable on every poll instead. */ export declare const TASK_MAX_DERIVED_RESULT_BYTES: number; /** * Per-owner cap on live (non-terminal) tasks, mirroring * `sessions.SUBSCRIPTION_CAP`. This is a CONCURRENCY bound: it stops a * caller from holding unbounded simultaneous work (and unbounded pending * scheduler jobs), but it does not bound total volume: terminal tasks * do not count, so a caller looping short tasks stays under the cap * while retained rows accumulate. Retention (`ttlMs` + `pruneTasks`) is * the only bound on that; size it accordingly, and rate-limit upstream * if a caller can loop faster than you want to store. */ export declare const TASK_OWNER_ACTIVE_CAP = 256; /** * Serialized-size cap on the stored caller identity snapshot. A fat * claims object would otherwise multiply per-row storage past the args * budget, since `args` is capped but `caller` was not. */ export declare const TASK_MAX_CALLER_BYTES: number; /** * Max structural nesting `stableStringify` will descend before it * rejects the value. A client-controlled deeply nested `args` (tens of * thousands of `[`) that survives `JSON.parse` would otherwise overflow * the stack inside the mutation, before any byte cap could reject it. */ export declare const TASK_MAX_STRINGIFY_DEPTH = 100; type TaskRow = { _id: Id<"tasks">; taskId: string; ownerSubject: string; toolName: string; toolKind: "query" | "mutation" | "action"; args: unknown; caller?: { subject: string; claims?: unknown; }; status: "working" | "input_required" | "completed" | "failed" | "cancelled"; result?: unknown; /** Shape flags for the result, recorded by whoever completed the task. */ resultIsError?: boolean; resultStructured?: boolean; error?: { code: number; message: string; }; inputRequests?: unknown; inputResponses?: unknown; inputRound?: number; idempotencyKey: string; executor: "component" | "host"; startedAt?: number; scope?: string; mrtrApproved?: boolean; createdAt: number; updatedAt: number; expiresAt: number; }; /** * Create one task row for a task-augmented modern `tools/call`. The host * has already authorized the call and resolved the owner; the component * only enforces storage invariants (unique id, size caps, TTL clamp) and * the per-owner live-task cap, which refuses a well-formed request with * `limit_exceeded`. * * When `executor` is `"component"`, execution is scheduled immediately * via the Convex scheduler; scheduled work is durable across restarts. * When `"host"`, the host starts its own durable execution (typically a * `@convex-dev/workflow` run) and finalizes via `completeTask` / * `failTask` / `requireTaskInput`. */ export declare const createTask: import("convex/server").RegisteredMutation<"public", { scope?: string | undefined; caller?: { claims?: any; subject: string; } | undefined; mrtrApproved?: boolean | undefined; ttlMs?: number | undefined; taskId: string; toolName: string; args: any; ownerSubject: string; executor: "component" | "host"; idempotencyKey: string; toolKind: "query" | "mutation" | "action"; }, Promise<{ created: false; reason: "duplicate_id"; task?: undefined; } | { startPending?: true | undefined; created: true; task: { error?: { code: number; message: string; } | undefined; result?: Record | undefined; inputRound?: number | undefined; inputRequests?: {} | null | undefined; taskId: string; toolName: string; status: "completed" | "cancelled" | "working" | "input_required" | "failed"; createdAt: number; updatedAt: number; expiresAt: number; }; reused: true; reason?: undefined; } | { created: false; reason: "args_too_large"; task?: undefined; } | { created: false; reason: "caller_too_large"; task?: undefined; } | { created: false; reason: "limit_exceeded"; task?: undefined; } | { created: true; task: { error?: { code: number; message: string; } | undefined; result?: Record | undefined; inputRound?: number | undefined; inputRequests?: {} | null | undefined; taskId: string; toolName: string; status: "completed" | "cancelled" | "working" | "input_required" | "failed"; createdAt: number; updatedAt: number; expiresAt: number; }; reason?: undefined; }>>; /** * Owner-bound poll for `tasks/get`. Returns `null` for unknown ids, * foreign owners, and expired rows alike: all three are answered * identically on the wire so existence never leaks across callers. */ export declare const getTaskForOwner: import("convex/server").RegisteredQuery<"public", { scope?: string | undefined; taskId: string; ownerSubject: string; }, Promise<{ error?: { code: number; message: string; } | undefined; result?: Record | undefined; inputRound?: number | undefined; inputRequests?: {} | null | undefined; taskId: string; toolName: string; status: "completed" | "cancelled" | "working" | "input_required" | "failed"; createdAt: number; updatedAt: number; expiresAt: number; } | null>>; /** * Trusted full-row read for the host's executor / workflow code. Unlike * `getTaskForOwner` it returns execution data (`args`, `caller`, * `idempotencyKey`) and skips owner binding; never expose it to clients. */ export declare const getTaskInternal: import("convex/server").RegisteredQuery<"public", { taskId: string; }, Promise>; /** * Owner-initiated cancellation (`tasks/update` with `action: "cancel"`). * Cancelling an already-cancelled task is idempotent * (`"already_cancelled"`, no new audit row); cancelling a completed or * failed task is a `"conflict"` because the outcome already exists and * must stay observable. */ export declare const cancelTaskForOwner: import("convex/server").RegisteredMutation<"public", { scope?: string | undefined; taskId: string; ownerSubject: string; }, Promise<{ outcome: "not_found"; task?: undefined; executor?: undefined; status?: undefined; } | { outcome: "already_cancelled"; task: { error?: { code: number; message: string; } | undefined; result?: Record | undefined; inputRound?: number | undefined; inputRequests?: {} | null | undefined; taskId: string; toolName: string; status: "completed" | "cancelled" | "working" | "input_required" | "failed"; createdAt: number; updatedAt: number; expiresAt: number; }; executor: "component" | "host"; status?: undefined; } | { outcome: "conflict"; status: "completed" | "working" | "input_required" | "failed"; task?: undefined; executor?: undefined; } | { outcome: "cancelled"; task: { error?: { code: number; message: string; } | undefined; result?: Record | undefined; inputRound?: number | undefined; inputRequests?: {} | null | undefined; taskId: string; toolName: string; status: "completed" | "cancelled" | "working" | "input_required" | "failed"; createdAt: number; updatedAt: number; expiresAt: number; }; executor: "component" | "host"; status?: undefined; }>>; /** * Owner submission of MRTR-shaped `inputResponses` for an * `input_required` task (`tasks/update`). Idempotent: re-sending the * responses that were already accepted returns `"duplicate"` without a * state change or audit row. Every response whose `action` is `"cancel"` * cancels the task instead of resuming it. */ export declare const submitInputResponsesForOwner: import("convex/server").RegisteredMutation<"public", { scope?: string | undefined; inputRound?: number | undefined; taskId: string; ownerSubject: string; inputResponses: any; }, Promise<{ outcome: "not_found"; expectedRound?: undefined; task?: undefined; executor?: undefined; status?: undefined; } | { outcome: "mismatch"; expectedRound?: undefined; task?: undefined; executor?: undefined; status?: undefined; } | { outcome: "stale_round"; expectedRound: number; task?: undefined; executor?: undefined; status?: undefined; } | { outcome: "duplicate"; task: { error?: { code: number; message: string; } | undefined; result?: Record | undefined; inputRound?: number | undefined; inputRequests?: {} | null | undefined; taskId: string; toolName: string; status: "completed" | "cancelled" | "working" | "input_required" | "failed"; createdAt: number; updatedAt: number; expiresAt: number; }; executor: "component" | "host"; expectedRound?: undefined; status?: undefined; } | { outcome: "conflict"; status: "completed" | "cancelled" | "working" | "failed"; expectedRound?: undefined; task?: undefined; executor?: undefined; } | { outcome: "too_large"; expectedRound?: undefined; task?: undefined; executor?: undefined; status?: undefined; } | { outcome: "cancelled" | "accepted"; task: { error?: { code: number; message: string; } | undefined; result?: Record | undefined; inputRound?: number | undefined; inputRequests?: {} | null | undefined; taskId: string; toolName: string; status: "completed" | "cancelled" | "working" | "input_required" | "failed"; createdAt: number; updatedAt: number; expiresAt: number; }; executor: "component" | "host"; expectedRound?: undefined; status?: undefined; }>>; /** * Record that the host's `tasks.execute` returned for this task, so a * replayed request can tell "execution started" from "a row was left * behind by a start that failed". Idempotent and best-effort: it only * ever sets the marker, never clears it, and a missing row is not an * error (the task may have been cancelled or pruned meanwhile). */ export declare const markTaskStarted: import("convex/server").RegisteredMutation<"public", { taskId: string; }, Promise>; /** * Trusted completion, called by the built-in executor or by the host's * workflow. Only non-terminal tasks can complete; a cancel that raced * ahead wins (`"conflict"`), so a cancelled task never flips back to a * success. An oversized result fails the task instead of storing a row * the client could never fetch within limits: reported as * `"result_too_large"`, NOT `"finalized"`, because the caller's work * succeeded while the client will be served an error. */ export declare const completeTask: import("convex/server").RegisteredMutation<"public", { isError?: boolean | undefined; taskId: string; result: any; }, Promise<"conflict" | "not_found" | "finalized" | "result_too_large">>; /** * Trusted failure, the error counterpart of `completeTask`. `error` * reaches the polling client verbatim, so callers sanitize it first; * `auditErrorMessage` (defaulting to the wire message) is what lands in * the audit row and may carry the full exception text. */ export declare const failTask: import("convex/server").RegisteredMutation<"public", { auditErrorMessage?: string | undefined; error: { message: string; code: number; }; taskId: string; }, Promise<"conflict" | "not_found" | "finalized">>; /** * Trusted transition to `input_required`, called by the host's workflow * when it needs MRTR-shaped input before continuing. Prior responses are * cleared and `inputRound` is bumped so the next `tasks/update` answers * THIS request: the round is the anti-replay mechanism that makes the * client's echo mandatory, so do not "simplify" the bump away. Resumption is host-owned: the gateway surfaces accepted responses * through the `onInputResponses` handler option. */ export declare const requireTaskInput: import("convex/server").RegisteredMutation<"public", { taskId: string; inputRequests: any; }, Promise<"conflict" | "not_found" | "updated" | "invalid_requests" | "too_large" | "unsupported_executor">>; /** * Built-in executor: runs the registered tool function once and * finalizes the task. Scheduled by `createTask` when the host did not * configure its own executor; Convex scheduled functions are durable, so * a deploy or restart between creation and execution does not lose the * task. The invocation itself goes through `dispatch.runTool`, so a * task-run tool is identical to a synchronous one in identity injection * and error sanitization, and (importantly) produces the same * `entryType: "tool"` audit row. (Redaction is moot here: `taskSupport` * is incompatible with `metadata.auditArgs`, so a task-run tool always * audits its arguments verbatim.) The task lifecycle * rows (`entryType: "task"`) are bookkeeping *around* that call, not a * replacement for it. A cancellation that lands before execution wins: * the task is left untouched. */ export declare const executeScheduledTask: import("convex/server").RegisteredAction<"public", { taskId: string; }, Promise>; /** * Drop expired task rows. Bounded per call like every other prune in * this component; hosts drain from a cron via `gateway.pruneTasks`, * looping until the return value is `0`. * * Non-terminal rows are pruned too, deliberately: the TTL is the task's * execution deadline, not just its retention window. An expired `working` * or `input_required` row is already unobservable to its owner (every * owner-facing function answers `not_found` past `expiresAt`) and its * trusted finalizers refuse to write to it, so keeping the row would only * accumulate storage. A long-running tool therefore needs a `ttlMs` that * covers its worst-case runtime; a host executor that outlives the TTL * finds its `completeTask` / `failTask` answered `not_found`. Pruning a * non-terminal row writes the `fail` audit row it never got, so the * lifecycle trail (not pruned here) always shows how a task ended. */ export declare const pruneTasks: import("convex/server").RegisteredMutation<"public", {}, Promise>; /** * Cancel every live (non-terminal) task owned by `ownerSubject`, for the * revocation case: an operator learns a subject's access was revoked and * wants its pending tasks stopped before they execute with the (still * valid until TTL) stored identity snapshot. Bounded per call via the * `by_ownerSubject` index; the host re-invokes with * `cursorCreationTime = cursor` until `cursor` is `null`. `cancelled` may * legitimately be `0` for a page whose rows were all terminal while later * pages still hold live tasks, which is why the cursor, not the count, * terminates the loop. Returns what was cancelled this batch. Terminal tasks are left as-is * so their outcome stays observable. The host still cancels any durable * execution (workflow run) itself. * * `scope` behaves DIFFERENTLY here than on the owner-facing functions, on * purpose. There, an omitted scope means "unscoped rows only", because a * mount must not reach another mount's tasks. Revocation is about the * SUBJECT, not the mount: omitting `scope` cancels every task that * subject owns across all mounts, which is what an operator processing a * revocation wants. Pass a `scope` only to narrow the sweep to one mount. */ export declare const cancelPendingTasksForOwner: import("convex/server").RegisteredMutation<"public", { cursorCreationTime?: number | undefined; scope?: string | undefined; ownerSubject: string; }, Promise<{ cancelled: number; taskIds: string[]; scanned: number; outOfScope: number; cursor: number | null; }>>; export {}; //# sourceMappingURL=tasks.d.ts.map