UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

146 lines (145 loc) • 12.6 kB
import type { TSchema } from "typebox"; /** * Truncate a long error to head + "[N characters truncated]" + tail (design/64 §11.2, CC parity). * * Anchored on CC 2.1.220 `A7e` (the current authority; the previous anchor was CC 88's `toolErrors.ts`, * which cut at raw code-unit indices): head and tail are cut on a code-POINT boundary — an astral character * straddling either cut is dropped whole rather than emitted as a lone surrogate — and the reported count is * `len − head − tail`, i.e. derived from what was actually KEPT, so a rollback is not under-reported. * * DELIBERATE DIVERGENCE from the sibling truncators in this repo (mcp.ts `truncateMcpErrorText`, * engine/compaction `truncateForSummary`, untrusted-egress `boundedString`), which iterate the kept length * down so the marker fits INSIDE the budget (RB-152): here the marker is NOT budgeted — a barely-over input * comes back ~36 chars LONGER than the budget. That is CC-verbatim behaviour (`A7e` keeps 5000+marker+5000), * it is pinned as a contract in test/core-toolgate-coverage.test.ts, and changing it would be a divergence * FROM the anchor, not a fix. Recorded as an intentional divergence in the cc-parity divergence registry * (internal assets repo, `cc-parity/`), not as an open defect. */ export declare function truncateError(s: string): string; /** * RB-198 F4: shared clip primitive originally grown inside task-registry's `clipTaskOutput` (CC `FHl` * shape, pretty.js:458012-458016) and extracted here so a SECOND caller (the foreground Bash tool) gets * the SAME already-hardened arithmetic instead of a parallel reimplementation — RB-85 (pointer-fit at the * boundary), RB-112 (pointer-only fallback bound), and RB-122 (surrogate-pair-safe split) were each a real * bug found by a red probe against THIS algorithm; a second copy would start over with none of them fixed. * TAIL-keep behind a `[Truncated. Full output: <path>]` pointer when a full-output file is available (the * tail is the part a long-running command's caller most needs — errors/summaries land at the end); without * a file, legacy head+tail middle-omission (there is no path to point at, and the head often carries * status a bare tail-keep would drop). */ /** The smallest output budget the clip primitives are given: below it a clip cannot carry its own * truncation banner plus any content, so the operator knobs that feed {@link clipWithFilePointer} * (`BASH_MAX_OUTPUT_LENGTH`, `TASK_MAX_OUTPUT_LENGTH`) are raised to it, loudly, never honored as a * budget of one character. */ export declare const OUTPUT_BUDGET_MIN_CHARS = 512; export declare function clipWithFilePointer(s: string, limit: number, fullOutputPath?: string): string; /** * Format a thrown tool error into model-facing text (design/64 §11.1/§11.2): a shell/exec error becomes * `Exit code N` + stderr + stdout; a plain Error becomes its message (+ any captured streams); anything * else is stringified. The result is truncated to {@link MAX_ERROR_CHARS}. An AbortError is reported as an * interruption. */ export declare function formatToolError(error: unknown): string; /** * A typed, read-side fold of a `TaskResult.errorCode` into the failure CLASS it belongs to (design/80 * D-G data contract; the WorkerReport "errorClass" small-slice). A terminal `errorCode` (1.37+) is a * **dotted namespace** (`limits.max_tokens_exceeded` / `limits.max_cost_exceeded` / `output.invalid` …) * so a consumer can prefix-match a whole CLASS — but every aggregator/consumer hand-rolls * `errorCode.startsWith("budget.")` — a prefix convention no declaration site records, which silently * mis-classifies any code that does NOT follow the convention and drifts as new prefixes are added. * This is the single shared folder. * * - `budget` — the cost/token budget gate fired (`limits.max_tokens_exceeded` / `limits.max_cost_exceeded`). * - `limit` — a hard run limit fired (`limits.max_turns_exceeded` / `limits.max_walltime_exceeded`). * - `output` — the structured/answer output was rejected (`output.invalid` / `output.degenerate`). * - `suspend` — the suspend-loop cap fired (`suspend.loop`); a TERMINAL failure, distinct from a * resumable `suspended` status (which carries no errorCode). * - `review` — a review-pause terminal (`review.pending`). * - `unexpected` — a nested orchestrator mapped an unexpected durable pause to failed * (`unexpected.suspended` / `unexpected.needs_review`, design/45 §11 Q6 / design/80 D-B). * - `oracle` — a verifier/oracle invariant breach (`oracle.unprotected`). * - `resume` — a post-CAS resume/restore infrastructure failure (`resume.env_failed` / * `resume.tool_unavailable`, design/45/49); a RETRYABLE basis (the work is still pending, * the checkpoint is reopenable) — distinct from a logical failure, so consumers can retry. * - `config` — a task-spec / assembly rejection raised in `prepare()` (`config.egress_requires_write_effect` * / `config.reserved_tool_name`); a deterministic configuration error (re-running the same * spec re-fails). ALSO the deployment governance-plane terminals that share the SAME * registry (governance-codes.ts `GOVERNANCE_CODES` mixes them with `config.*` in one * table): `memory.admission_required` / `memory.admission_denied` (org-memory admission, * genuinely `prepare()`-raised, same shape as the config.* codes above) plus the two * EXTERNAL/platform-cause terminals `usage.window_exhausted` (a deployment usage-governance * window is full — mid-run or at entry, design/164 件五) and `env.lifetime_expired` (the * execution environment's platform lifetime ran out — an engine-detected MID-RUN stop at a * clean turn boundary, design/164 件四; NOT a `prepare()`-time rejection like its * `config.env_lifetime_invalid` config-time sibling). The two platform-cause codes are * DEFERRED-retryable (a usage window frees, a fresh environment can be provisioned) — * genuinely NOT "never retryable, malformed request" in the literal sense of the codes * above; they are grouped into this class as the SAME deployment-governance DOMAIN (per * governance-codes.ts's own table membership and the calling convention's explicit * direction), not as a retry verdict. This class NEVER carries the retry answer for * ANY of its members — a caller that needs it reads the PER-CODE source of truth: * `TaskResult.retryAfterMs` (set ONLY for `usage.window_exhausted`, per its own * in-presence condition) for that one code, or the errorCode's own doc for every other * member (`env.lifetime_expired`'s doc says "retrying needs a NEW environment, not a * smaller budget" — there is no `retryAfterMs` for it, by design: an expiring * environment has no return time to give). falsification review (rounds 2 and 4) * twice suggested a distinct governance/platform class or leaving these `unknown` * instead — rejected both times: (a) explicit calling-convention direction; (b) * `WorkerErrorClass` is a small CLOSED public exported union — adding a member is an * API-surface/release-tier decision outside a mechanical fix batch's mandate, and * `unknown` would UNDO the very audit finding this folder exists to fix (these ARE real * terminal codes, not foreign ones); (c) pre-existing precedent (`config.compliance_required`, * NOT part of this diff) already folds a transient/retryable governance code into `config`. * `memory.admission_required`/`_denied`/`env.lifetime_expired` are EXACT codes, not broad * `memory.`/`env.` prefixes — falsification review (A-005.3 follow-up) caught an * earlier draft folding EVERY `memory.*`/`env.*` code here, which misclassified the * memory-engine's non-governance incident codes (`memory.secret_detected` et al., * explicitly listed as NON-governance in `NON_GOVERNANCE_MEMORY_CODES` — a content-safety * tool-call refusal, not a task-spec rejection) as configuration failures. `usage.*` STAYS * a broad prefix: governance-codes.ts's own `governanceRetryClass` already treats every * `usage.*` code (registered or not) as governance by prefix — a deliberate legacy-family * rule this folder mirrors rather than re-deciding (questioned whether that * mirroring is appropriate across the two taxonomies; kept as-is per the calling * convention's explicit direction — see the function's SCOPE note for the boundary this * folder does and does not draw). * - `conflict` — a cross-instance optimistic-lock loss (flat `"conflict"`, unchanged 1.36 code). * - `brain` — a model/transport failure lifted from the brain (flat `auth` / `rate_limit` / * `invalid_request` / `server` / `network` / `http`; see brain/errors.ts). * - `aborted` — a step was aborted (flat `"aborted"`). * - `unknown` — no code, or a code outside every known class (the safe catch-all). */ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "review" | "unexpected" | "oracle" | "resume" | "config" | "conflict" | "brain" | "aborted" | "unknown"; /** * Fold a {@link "../core/types.js".TaskResult}.`errorCode` into its typed {@link WorkerErrorClass} (design/80 * D-G). PURE and DERIVED — it reads an existing code, never produces or changes one; an unknown / missing * code folds to `"unknown"` (never throws). Replaces scattered `errorCode.startsWith("budget.")` consumer * code with one tested folder so the class taxonomy lives in exactly one place. * * SCOPE: this folds a TERMINAL `TaskResult.errorCode` only — i.e. every OUR dotted prefix that the runner can * assemble into a failed task outcome (`limits.`/`output.`/`suspend.`/`review.`/`unexpected.`/ * `oracle.`/`resume.`/`config.`/`usage.`) as a broad PREFIX, plus `memory.admission_required` / * `memory.admission_denied` / `env.lifetime_expired` as EXACT codes (their sibling `memory.*`/`env.*` strings * are NOT all terminal-errorCode-shaped — see {@link EXACT_CODE_CLASS}'s comment), plus the flat 1.36 codes. * The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see * {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a * task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/ * `steering.duplicate_input_id`/`steering.blocked_by_hook`, rejected to the `steer()` caller — the third also * rides the parked queue's `setPendingSteer` face and the fourth the resume face, as `CheckpointError` codes) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither * reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means * "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add"). */ export declare function errorClassOf(errorCode: string | undefined): WorkerErrorClass; /** * Convert a TypeBox/JSON-Schema error path (`/todos/0/activeForm`) into a readable accessor * (`todos[0].activeForm`), CC `formatValidationPath` parity. Numeric segments become `[i]`, named * segments become `.name`. Empty path → "" (the root object). */ export declare function formatValidationPath(path: string): string; /** * Turn a schema-validation failure into a human-readable message (design/64 §11.3 / §16.4). Reports the * parameter NAME + the expected type only — never an absolute path, stack trace, or filesystem context, so * a validation message can't leak internals (§16.4). Examples: * - "The required parameter `todos` is missing." * - "The parameter `questions[0].options` is invalid: Expected array." */ export declare function formatZodValidationError(schema: TSchema, value: unknown): string;