import type { AssistantMessage } from "../../internal/llm.js"; import type { TaskResult, TaskSpec } from "../types.js"; /** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */ export interface Stats { turns: number; tokens: number; /** design/97 CORE-8 (②): total tool calls executed (one per `tool_start`). Spread into `TaskResult.stats`. */ toolCalls: number; promptTokens: number; cachedTokens: number; cacheWriteTokens: number; cacheWriteTokensLong: number; outputTokens: number; cacheHitRate?: number; totalInputTokens?: number; costUsd: number; costMicroUsd: number; nested?: { tokens: number; turns: number; tasks: number; costUsd?: number; costMicroUsd?: number; }; /** Post-task memory-consolidation usage (design/41); set after the result is assembled. Kept out of * the budget gate (`overBudget` reads `costMicroUsd`/`tokens`, never this). */ memory?: { tokens: number; costMicroUsd: number; applied: number; }; /** design/91: human-review burden (design/89 §2.4 C2). Wall-clock approval-gate wait + count, bucketed by * `gate.kind`, accumulated across resume legs. Set by the run loop from the per-task human-review * accumulator (synchronous `resolveAsk` timing + durable resume latency). **NOT an LLM cost** — never * folded into `costMicroUsd`/`costBreakdown`/the budget gate (same budget-excluded treatment as `memory`). * Flows straight through to `TaskResult.stats.humanReview` via the public-stats spread; absent ⇒ no gate. */ humanReview?: { count: number; totalWaitMs: number; gates: Array<{ kind: string; waitMs: number; decision?: string; toolName?: string; toolArg?: string; }>; }; /** TB telemetry B2 (service [397]): deadline-mechanism engagement counters (design/128 nudges + * design/130 P1 cap shrinks / P2 finalize / P2b callCutoffs=brain-side walltime stream cuts + * toolClamps=foreground shell timeouts clamped to the soft tool deadline). Set by the run loop * only when ≥1 engaged; flows straight through to `TaskResult.stats.mechanisms` via the * public-stats spread. */ mechanisms?: { finalizeInjected?: true; nudgesSent?: number; capShrinks?: number; callCutoffs?: number; toolClamps?: number; finalVerifyInjected?: true; finalVerifyInjections?: number; attachmentsInjected?: number; repetitionCuts?: number; repetitionSpared?: number; repetitionEvents?: Array<{ turn: number; action: "cut" | "spared"; rule: "char-run" | "unit-loop"; period: number; reps: number; segment: string; }>; }; /** design/80 D-E-core (Part B): within-task compaction LLM cost (micro-USD), accumulated by * `recordCompactionUsage` as a SEPARATE running total because that cost IS folded into `costMicroUsd` * (so it can't be re-derived from `costMicroUsd` alone). The {@link costBreakdown.compactionMicroUsd} * source. Run-loop internal — not a public `TaskResult.stats` field. */ compactionMicroUsd?: number; /** * design/80 D-E-core (Part B): a thin FINANCE TAXONOMY of the LLM-derived costs the engine actually * prices, decomposing the task's spend into report categories (no new persisted structure — derived from * the cost sources that already exist). CORE = LLM-token-derived ONLY; the SERVICE adds the infra axes it * owns (tool-call / sandbox-walltime / egress — it has the k8s cost data) and composes them with this. * Each category is ≥ 0 and the parts reconcile to the independently-summed LLM total * (`llmRoot + nestedSubagent + compaction` — `memory` is a separate, budget-excluded line, see its doc). * Filled in assembleResult for `llmRoot`/`nestedSubagent`/`compaction`; the `memoryConsolidation` line is * filled LATER (runtask, alongside `stats.memory`, AFTER the async consolidation pass) — it is 0 here. */ costBreakdown?: { /** Root-agent LLM cost (micro-USD): `costMicroUsd` MINUS the sub-categories already folded into it * (compaction). NOT minus nested — nested subagent cost is tracked separately and is NEVER folded into * `costMicroUsd` (it lands only in `stats.nested`), so subtracting it would under-report the root. */ llmRootMicroUsd: number; /** Delegated sub-agent (nested) LLM cost (micro-USD) = `stats.nested?.costMicroUsd ?? 0`. */ nestedSubagentMicroUsd: number; /** Post-task memory-consolidation LLM cost (micro-USD) = `stats.memory?.costMicroUsd ?? 0`. Filled in * runtask AFTER the (async) consolidation pass sets `stats.memory`; 0 at assembleResult time. */ memoryConsolidationMicroUsd: number; /** Within-task compaction LLM cost (micro-USD) = the running `compactionMicroUsd` accumulator (the * category Framing-4 missed). This IS part of `costMicroUsd`, so it is subtracted from `llmRoot`. */ compactionMicroUsd: number; }; } /** * The terminal-state flags the run loop hands {@link assembleResult} — the typed contract between the * loop and result assembly (council design/34 ⑤: exported so it's grep-able and tests can construct it). */ export interface ResultFlags { threw: unknown; /** design/99 MF-25: the EFFECTIVE (resolved) model id that served the task — echoed on `TaskResult.model`. */ model?: string; abortedForTimeout: boolean; abortedForTurns: boolean; /** audit A-1: the abort signal fired DURING the run (user interrupt / timeout / max-turns / budget), * captured before the run loop's unconditional release-abort. Since the loop now exits CLEANLY on an * abort (no synthetic stopReason:"aborted" assistant is appended), `final` is the last REAL assistant * message — this flag is what keeps an interrupted run from being mistaken for a completed one. */ abortedLive?: boolean; budgetHit?: "exceeded" | "precall"; blockedReason?: string; conflict?: boolean; outputInvalid?: boolean; /** design/72 §2.2 (B): a re-suspend was refused because the task already suspended `maxSuspends` times * (a resume/restart loop). It aborted the run (no `threw`) but must read as `failed`/`suspend.loop`, * NOT `suspended` — slotted ABOVE `suspendRef` and the aborted branches (this suspend never minted a * checkpoint, so `suspendRef` is unset anyway; the ordering makes the intent explicit). */ suspendLoop?: boolean; /** design/45: the task deliberately suspended at a durable tool gate (capture + abort fired). When set, * the result is `status:"suspended"` carrying this token/gate — a clean pause, NOT a failure, so it is * slotted ABOVE the `!final`/aborted branches (slot 8.5: it beats them but yields to a real * budget/threw failure that happened to coincide). The abort it used sets `final.stopReason="aborted"` * but does not set `threw`, so it never reaches the `flags.threw` branch. */ suspendRef?: { token: import("../checkpoint-store.js").CheckpointToken; gate: import("../checkpoint-store.js").CheckpointGate; }; /** design/76 §2.5 (dry-run / shadow) + design/80 D-B (plan-gate): the task deliberately paused at a * REVIEW-PAUSE gate — `{kind:"needs_review"}` (a profile's dry-run interception produced a predicted * state-diff a human/judge must REVIEW) OR `{kind:"plan_review"}` (a profile's plan-gate produced a * proposed PLAN a human must approve/edit/reject). When set, the result is `status:"needs_review"` + * errorCode `"review.pending"`, carrying this token/gate (keyed off ref-PRESENCE, not `gate.kind`, so it * serves both review kinds uniformly). It is the DUAL of `suspendRef` but for DIFFERENT semantics (a human * REVIEW, not a pre-action tool-call APPROVAL), so it has its OWN flag and its OWN assemble slot (8.6, * between `suspendRef` 8.5 and the aborted branches 9) — **never both set at once** (the commit-side * discriminant in prepare-task branches on `gate.kind` and sets exactly one). Disjoint from `suspendRef` * so this branch is reachable: keying status off `suspendRef` alone (reusing it) would report `"suspended"` * and make this dead code (v4 MAJOR-A). Like `suspendRef`, it used an abort to stop the loop (no `threw`), * so it never hits the failure branches above. */ reviewRef?: { token: import("../checkpoint-store.js").CheckpointToken; gate: import("../checkpoint-store.js").CheckpointGate; }; } /** * Extract a machine-readable code from a thrown error (e.g. `SessionError.code`, a Node error code). * Walks the `cause` chain because the harness wraps lower-level errors (e.g. a `SessionError("conflict")` * append failure surfaces as an `AgentHarnessError("unknown")` whose cause is the real one) — so we * prefer the first specific code over a generic `"unknown"` wrapper. */ export declare function errorCodeOf(err: unknown): string | undefined; /** * Build the final {@link TaskResult} from the run's terminal state. **The errorCode priority chain is a * public contract** (callers `switch` on these strings) — first match wins, in this exact order * (council design/34 red-line #3, do not reorder): * 1. `outputInvalid` → `"output.invalid"` * 2. degenerate → `"output.degenerate"` + `salvagedOutput` (design/39; wins over budget/timeout) * 2.5 walltime cutoff → status `"timeout"` + `"limit.timeout"` + `salvagedOutput` (design/130 P2b: * the brain cut the final turn at the soft deadline and the bounded write-out recovery was * exhausted — semantically a walltime timeout WITH salvage, not an opaque provider failure) * 3. `budgetHit` → `"budget.precall"` | `"budget.exceeded"` * 3.5 `suspendLoop` → `"suspend.loop"` (design/72 §2.2 B — the re-suspend cap; deterministic root * cause, above `threw` because it owns the abort) * 4. `threw` + timeout → `"limit.timeout"` * 5. `threw` + turns → `"limit.max_turns"` * 6. `threw` + conflict→ `"conflict"` * 7. `threw` (other) → `errorCodeOf(threw)` * 8. `blockedReason` → status `"blocked"` (no errorCode) * 8.5 `suspendRef` → status `"suspended"` + checkpointToken/gate (design/45 B4 — a deliberate * durable pause beats the abort it used, but yields to a real budget/threw failure above) * 8.6 `reviewRef` → status `"needs_review"` + `"review.pending"` + checkpointToken/gate (design/76 §2.5 * — a dry-run post-prediction REVIEW pause; the DUAL of 8.5, between it and the aborts, same red-line: * a `suspendRef` at 8.5 still wins if both were somehow set, proving slot order — but the commit-side * discriminant guarantees only one is ever set) * 9. `abortedLive` | stopReason aborted → `"limit.timeout"` | `"limit.max_turns"` | undefined * (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a * normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`) * 10. no `final` → status `"failed"` * 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag) * 12. else → `"completed"` */ export declare function assembleResult(spec: TaskSpec, sessionId: string, final: AssistantMessage | undefined, stats: Stats, flags: ResultFlags): TaskResult; //# sourceMappingURL=assemble-result.d.ts.map