@sema-agent/core
Version:
Stateless, task-oriented AI agent core
146 lines (145 loc) • 12.6 kB
TypeScript
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 fourth also
* rides the resume face as a `CheckpointError` code) 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;