import { z } from 'zod'; /** * nexus-agents/core - Price-basis vocabulary (#4406) * * The kind of rate a recorded dollar figure rests on, defined ONCE for both * consumers: the pricing chain (`core/trace-pricing.ts`, which turns a model id * into a cost) and the persisted decision-cost records * (`observability/decision-cost.ts`, which have to transport the basis into * JSONL and an MCP `outputSchema`). * * WHY A SEPARATE LEAF MODULE. `core/trace-pricing` pulls in the model registry, * and a runtime edge from `observability/decision-cost` to it closes a cycle * back through weather-report → decision-cost-store → decision-cost, which left * the zod schema `undefined` at evaluation time. The first fix was a type-only * import plus a hand-written `['list', 'unknown'] as const satisfies readonly * PriceBasis[]` mirror in decision-cost.ts. That mirror was a data-loss hazard: * `satisfies` rejects a member being RENAMED or DROPPED but happily accepts the * union GAINING one, so an added member would compile, fail validation at the * persistence boundary, and make `JsonlStore` reject the ENTIRE decision record * — `append` returning `persisted: false`, and on read the line skipped with * only an aggregate debug count. Governance and billing data lost wholesale * over one unrecognised field value. * * This module has no imports beyond zod, so both sides can import it at runtime * without a cycle, and the duplication is gone rather than merely guarded: the * schema is the single definition and the TypeScript union is DERIVED from it, * so a new member cannot be added to one and not the other. * * @module core/price-basis * (Source: Issue #4406) */ /** * The vocabulary itself. Members: * * - `'list'` — a price WAS resolved from the registry chain. Read it as "an * assumed published rate", not a guaranteed vendor list rate: see * {@link PriceBasis} for the cases where the resolved number is something * else and is still reported this way. * - `'unknown'` — no price was resolved for this model. Read it as "the chain * produced nothing", not "no price exists in the world": the generated * catalog loader (`config/models-generated-loader.ts`) deliberately discards * a published $0/$0 rate unless the id ends `:free`, so a genuinely free * model can land here. * * Deriving the type from the schema (rather than the reverse) is what keeps the * runtime validator and the compile-time union from drifting. */ declare const PriceBasisSchema: z.ZodEnum<{ unknown: "unknown"; list: "list"; }>; /** * Where a price came from, so a consumer can caveat it honestly (#4406). * * `'list'` is an ASSUMPTION about the pricing chain, not a verified property of * the number. The chain's tiers are mostly vendors' advertised public rates, * but at least three paths put something else behind the same label: * * 1. The operator manifest overlay (`config/manifest-overlay.ts`) is the * HIGHEST-precedence tier and carries `pricing` in its passthrough keys — * it exists specifically to override pricing. An operator's negotiated rate * entered there is reported `'list'`, and {@link priceBasisCaveat} then * warns the reader their contract may differ over the contract rate. * 2. The normalized/fuzzy identity tier (`config/model-registry.ts` * `mergeMatchedWithDerived`) grants a decorated gateway id the pricing of a * DIFFERENT canonical entry it matched. That rate is a real vendor rate for * some other model, not necessarily for the id being priced. * 3. In the reverse direction, `'unknown'` is not a claim that no price * exists — see the loader caveat on {@link PriceBasisSchema}. * * There is deliberately no `'contract'` member. The gap is NOT that an operator * has no way to state a negotiated rate — the manifest overlay above is exactly * that mechanism — it is that the mechanism carries no LABEL distinguishing a * negotiated rate from a published one, so nothing downstream could populate * `'contract'` truthfully. Adding the label, and the member, is tracked * separately; until then `'list'` over-claims in the conservative direction * (it warns about an estimate over a number that may be exact) and every basis * a consumer sees should be read as "the best rate the chain knew about". */ type PriceBasis = z.infer; /** * nexus-agents/config - Model Capabilities Type Definitions * * Zod schemas and TypeScript interfaces for the model capabilities matrix. * Defines output/input modalities, tool support, and special features * for each supported AI model. * * @module config/model-capabilities-types * (Source: Issue #683, Epic #682) */ /** Output modalities a model can produce. */ declare const OUTPUT_MODALITIES: readonly ["text", "image_png", "image_jpeg", "audio_pcm", "audio_wav", "audio_mp3", "video_mp4", "svg", "structured_json", "code"]; type OutputModality = (typeof OUTPUT_MODALITIES)[number]; /** Input modalities a model can accept. */ declare const INPUT_MODALITIES: readonly ["text", "image", "audio", "video", "pdf", "code"]; type InputModality = (typeof INPUT_MODALITIES)[number]; /** Tool capabilities a model supports. */ declare const TOOL_CAPABILITIES: readonly ["mcp", "function_calling", "computer_use", "code_execution_sandbox", "web_search", "file_operations", "structured_output", "apply_patch"]; type ToolCapability = (typeof TOOL_CAPABILITIES)[number]; /** Special features beyond standard text generation. */ declare const SPECIAL_FEATURES: readonly ["extended_thinking", "deep_research", "streaming", "grounding", "citations", "image_editing", "voice_cloning", "live_api", "context_caching"]; type SpecialFeature = (typeof SPECIAL_FEATURES)[number]; /** CLI tool names supported by the routing system. */ declare const CLI_NAMES: readonly ["claude", "gemini", "codex", "opencode"]; type CliNameLiteral = (typeof CLI_NAMES)[number]; /** * Canonical model id enum used for narrow `ModelId` typing and as the * argument to `z.enum(MODEL_IDS)` in `ModelCapabilitySchema`. * * This is a hand-maintained narrow tuple rather than a derived view * from `ModelRegistry` because the literal-union type is load-bearing: * — the Zod schema (`ModelCapabilitySchema.id`) needs a * closed enum at compile time. * — Many function signatures across `src/` accept `ModelId` and rely * on the narrowed type for exhaustiveness. * * The runtime invariant — `MODEL_IDS` matches the in-tree registry * entries — is asserted by `model-ids-invariant.test.ts`. If you add * or remove a model in `DEFAULT_MODEL_CAPABILITIES.models`, you must * also update this list (and the test will tell you when they drift). * * Slice E of #2546 will collapse `model-capabilities.ts` itself; at * that point `MODEL_IDS` either moves to `model-config-helpers.ts` or * its consumers are loosened to `string`. This narrow type stays * until then. */ declare const MODEL_IDS: readonly ["claude-fable-5", "claude-opus", "claude-sonnet", "claude-haiku", "gemini-3-pro", "gemini-pro", "gemini-3.5-flash", "gemini-3-flash", "gemini-flash", "gpt-5.5", "codex-5.3", "codex-5.2", "codex-5.1-mini", "opencode-default", "opencode-custom-opus", "opencode-custom-sonnet", "openrouter-nemotron-super", "openrouter-qwen-coder"]; type ModelId = (typeof MODEL_IDS)[number]; /** Quality scores for model capability routing (0-10 scale). */ declare const QualityScoresSchema: z.ZodObject<{ reasoning: z.ZodNumber; codeGeneration: z.ZodNumber; speed: z.ZodNumber; cost: z.ZodNumber; }, z.core.$strip>; type QualityScores = z.infer; /** Pricing information (USD per 1M tokens). */ declare const PricingSchema: z.ZodObject<{ inputPer1M: z.ZodNumber; outputPer1M: z.ZodNumber; cacheReadPer1M: z.ZodOptional; cacheWritePer1M: z.ZodOptional; }, z.core.$strip>; type Pricing = z.infer; /** * nexus-agents/cli-adapters - Core Type Definitions * * Core CLI types: CliName, CliTransport, CliResponse, CliError, etc. * * (Source: cli-project_plan.md v2.1.0) * (Source: docs/research/cli-integration-architecture.md) */ /** * Supported CLI names. * Derived from canonical source: config/model-capabilities-types.ts CliNameLiteral */ type CliName = CliNameLiteral; /** * API-vendor identifiers an `AdapterSelection{source:'api'}` reports (#3422). * Distinct from the four CLI slots: a direct vendor API and the same vendor's * CLI binary have different latency/failure profiles, so they must NOT share a * routing/bandit arm (would pollute the learned model). */ type ApiVendor = 'anthropic' | 'openai' | 'google' | 'custom-openai'; /** Prefixed routing arm id for a direct-API adapter, e.g. `api:anthropic` (#3422). */ type ApiArmId = `api:${ApiVendor}`; /** * A LinUCB/routing arm id — either a canonical CLI slot or a distinct API arm * (#3317 step 1 / #3422). Confined to the router/bandit/outcome surface so the * exhaustive `Record` maps elsewhere stay narrow and untouched. */ type RoutingArmId = CliName | ApiArmId; /** * Routing arm id for a dynamically registered API endpoint (#4392 increment * 1): `api:` plus a validated endpoint identity — one arm per GATEWAY, so two * operator-named endpoints are distinct arms. Deliberately NOT a member of * {@link RoutingArmId} or of the persisted `OutcomeCli` union (#6290 panel): * an endpoint arm can be registered and observed (breaker, capacity, * telemetry key) but cannot enter an outcome record until #6291 widens the * published ids in 9.0. The type alone admits any `api:` string; the runtime * shape is enforced by {@link isEndpointArmId}, and a cast from an * unvalidated string is exactly what that validator exists to refuse. */ type EndpointArmId = `api:${string}`; /** * Every arm the circuit-breaker registry and the adapter registry can hold * (#4392 increment 1): a published {@link RoutingArmId} or a dynamically * registered {@link EndpointArmId}. This is the parameter type of the * arm-typed registry siblings (`getArmBreaker`, `getAdapterForArm`, …) and of * the `armId` fields on circuit events and errors. It is NOT the routing / * bandit / outcome arm type — that stays {@link RoutingArmId} until #6291. */ type ObservedArmId = RoutingArmId | EndpointArmId; /** * Transport type for CLI communication. * - 'mcp': Uses Model Context Protocol (most stable) * - 'subprocess': Spawns CLI process with JSON output */ type CliTransport = 'mcp' | 'subprocess'; /** * Token usage information from CLI response — ONE call's usage as the CLI * parsers emit it. * * This is deliberately a separate type from the adapter response contract's * `TokenUsage` in `core/types/model.ts` (#4440): here `totalTokens` is * optional because not every CLI prints one, there it is required. The two * cross only in the two adapter bridges, and every crossing goes through * `token-usage-bridge.ts` so no field is narrowed silently. */ interface TokenUsage { /** Input tokens consumed */ readonly inputTokens: number; /** Output tokens generated */ readonly outputTokens: number; /** Cached input tokens READ from an existing cache (if applicable). */ readonly cachedInputTokens?: number; /** * Input tokens spent WRITING the cache, when the vendor reports them * separately (#4435). Kept distinct from {@link cachedInputTokens} because * they bill at opposite ends: cache writes are ~1.25x the uncached input * rate, cache reads ~0.1x. Collapsing them would make correct pricing * impossible. */ readonly cacheCreationInputTokens?: number; /** Total tokens (input + output) */ readonly totalTokens?: number; /** * Whether `inputTokens` is a measurement (#4835); `false` means it is a * placeholder `0` and `totalTokens` a lower bound. Absent means measured. * * No CLI parser sets this. It exists so the model→CLI bridge * (`toCliTokenUsage`, #4440) can carry a direct-API adapter's flag instead of * dropping it, which turned a placeholder count into a measured zero. */ readonly inputTokensMeasured?: boolean; } /** * Unified CLI response format. * Normalized across all CLI output formats. */ interface CliResponse { /** The response text */ readonly text: string; /** Token usage statistics */ readonly usage?: TokenUsage; /** Session ID for resumption */ readonly sessionId?: string; /** Cost in USD (if available) */ readonly costUsd?: number; /** Model used for generation */ readonly model?: string; /** * The model the caller asked for, when the adapter answered with a * different one from the same CLI family (#6120). Present only on a * substituted response — the claude adapter sets it after an * out-of-credits envelope for the requested model made it retry the next * registry alias — so a record consumer (#6115) can say which model * actually voted. Absent means the requested model answered. */ readonly fallbackFrom?: string; /** Duration in milliseconds */ readonly durationMs?: number; /** Raw response (for debugging) */ readonly raw?: unknown; /** * Stderr the transport captured during a SUCCESSFUL call, when non-empty * (#6094). On the subprocess path this is the CLI's stderr; on the codex MCP * path it is what `codex mcp-server` wrote to its piped stderr while the tool * call was in flight. A sandbox failure inside the CLI's tool loop surfaces * here while `text` still carries a parsed answer. Absent on a clean run. */ readonly stderr?: string; } /** * Error codes for CLI operations. */ type CliErrorCode = 'NOT_FOUND' | 'NOT_AUTHENTICATED' | 'RATE_LIMITED' | 'TIMEOUT' | 'PARSE_ERROR' | 'CONNECTION_ERROR' | 'EXECUTION_ERROR' | 'UNSUPPORTED_VERSION' | 'BUDGET_EXCEEDED' | 'UNKNOWN'; /** * CLI execution error. */ interface CliError { /** Error code */ readonly code: CliErrorCode; /** Human-readable message */ readonly message: string; /** CLI that produced the error */ readonly cli: CliName; /** Underlying error (if any) */ readonly cause?: Error; /** Whether the error is retryable */ readonly retryable: boolean; /** * How long the provider asked us to wait before retrying, in milliseconds * (#4373). Present only when the CLI's own message stated one — parsed by * `parseRetryAfterMs`. The retry loop prefers this over its computed * exponential backoff, since a provider that names its window knows better * than our guess. */ readonly retryAfterMs?: number; } /** * Version compatibility status. */ type VersionStatus = 'supported' | 'outdated' | 'breaking' | 'unsupported'; /** * Health check status for a CLI. */ interface HealthStatus { /** Whether the CLI is healthy */ readonly healthy: boolean; /** CLI version */ readonly version: string; /** Version compatibility status */ readonly versionStatus: VersionStatus; /** Optional message (e.g., upgrade recommendation) */ readonly message?: string; /** * Whether the underlying CLI could be reached at all (#5060). * * `healthCheck` catches its own failures and returns rather than throwing, so * a `healthy: false` result covers two very different states: the binary ran * and reported an unsupported version, or the binary could not be run at all * (`spawn ENOENT`). Both arrive as `versionStatus: 'unsupported'`, and a * consumer reading only `healthy` told users to authenticate a CLI they had * not installed. * * Absent means the producer predates the distinction — unknown, not * unreachable. Consumers should treat `reachable !== false` as "present". */ readonly reachable?: boolean; /** * When the evidence behind {@link reachable} was actually gathered (#5864). * * `BaseCliAdapter` caches the version string forever — no TTL, no reset, not * even on `dispose()` — so every `healthCheck` after the first for a given * instance returns without spawning anything. `reachable: true` then restates * a past observation as a present one, and `lastChecked` is stamped `now`, * which dates a replay as if it were a fresh probe. * * Compare the two: equal (to the probe) means this check ran the binary; * earlier means `reachable` rests on a cached reading and the binary may have * gone away since. Absent means the producer does not probe a binary at all * (an in-process adapter) or predates the distinction — unknown, not stale. */ readonly versionProbedAt?: Date; /** Last successful health check */ readonly lastChecked: Date; } /** * Capacity status for rate limiting. */ interface CapacityStatus { /** Remaining tokens in current window */ readonly remainingTokens: number; /** Remaining requests in current window */ readonly remainingRequests: number; /** When the rate limit resets */ readonly resetTime: Date; /** Current utilization percentage (0-100) */ readonly utilizationPercent: number; /** * Whether this process's rolling rate window is used up (#4456). * * Local arithmetic only: this process's own spend over the last * {@link RATE_LIMIT_WINDOW_MS}, measured against a per-CLI constant the * source calls a conservative estimate. It self-clears within the window, * and an ordinary burst (a 7-voter panel, a subagent fan-out) sets it while * plenty of provider quota remains. * * This is a throttling hint, NOT evidence that the account is out of * capacity. Do not exclude a candidate on it — see {@link quotaExhausted}. */ readonly rateLimited: boolean; /** * @deprecated Since #4456 — renamed to {@link rateLimited}, which says what * it actually measures. The name `exhausted` promised an account/plan * capacity signal while reporting a 60-second local rate heuristic, so every * reader inherited a claim the value could not support. Identical value; * scheduled for removal in the next major. */ readonly exhausted: boolean; /** * Whether a PROVIDER asserted that durable quota is gone (#4456). * * Set only from provider-asserted evidence — a rate-limit error whose * `retry-after` exceeds the local window, which is the provider itself * saying the wait is longer than a per-minute throttle. Never inferred from * local counting. * * `false` means "no provider has asserted exhaustion to THIS process". It is * NOT a measurement that quota remains: a weekly quota burned by another * process is invisible here. Read it with {@link observed}; absence of * evidence must not be presented as capacity. */ readonly quotaExhausted: boolean; /** * When the provider said the quota window clears, from `retry-after`. * * Present only alongside `quotaExhausted: true`. Absent means the provider * asserted exhaustion without a horizon, which is a weaker signal, not a * shorter one. */ readonly quotaResetAt?: Date; /** * Whether this process has observed any usage of the adapter (#4374). * * When false, every other field is a *default*, not a measurement: a tracker * that has never recorded a request reports the full token limit remaining and * 0% utilization, which is indistinguishable from a genuinely idle adapter. * Consumers must not present an unobserved reading as health. * * Note the narrower guarantee even when true: the tracker sees only THIS * process's spend. It has no visibility into a provider-side weekly quota * consumed elsewhere, so `remainingTokens` is a local upper bound on what is * left, never an authoritative one. */ readonly observed: boolean; } /** * decision-cost — per-DECISION cost aggregation over a governed panel's voters. * * Source: Issue #3855 (epic #3854 child, M4). * * A governed decision — a `consensus_vote` or `pr_review` run — fans out to N * voter calls; each voter is one LLM call. Per-CALL usage telemetry already * exists ({@link module:learning/usage-log} — token + cost per model call, * PR #2479/#2480 era). This module is the AGGREGATION layer that rolls those * per-call numbers UP into a single per-decision answer: "what did this * governed decision cost?". * * It is a PURE rollup (no I/O, no clock, no env reads at the math layer): * {@link rollupDecisionCost} takes the per-voter cost inputs + the billing mode * and returns a {@link DecisionCostSummary} — total tokens, total USD, a * per-voter breakdown, and a per-model breakdown. Persistence + the live * `process.env` billing-mode read live in the callers (the store + the tools), * mirroring the usage-log split between `computeCostUSD` (pure) and * `recordUsageEvent` (I/O). * * Design decisions that the fixture tests pin (#3855 acceptance criteria): * * - **Missing cost is UNMEASURED, not zero.** A voter with no computable cost * — no usage report at all (a CLI-subscription adapter, an error vote that * never reached the model) or a model with no pricing anywhere in the * registry chain (#4165) — is counted in `unmeasuredVoters` and contributes * 0 to the COST totals (reported tokens still count toward consumption) — * but the summary records that the total is a floor, not an exact figure * (`measured` < `voterCount`). Treating unmeasured as a true $0 would * silently understate spend; this keeps the honesty. * - **Plan mode records 0-cost but keeps tokens.** Under `NEXUS_BILLING_MODE=plan` * the spend is pre-covered by a subscription, so cost is recorded as $0 while * token counts are preserved (so the operator can still see consumption and * a later `api`-mode reprice is possible). This mirrors how plan mode zeroes * cost in routing/scoring without dropping the token signal. * * @module observability/decision-cost */ /** Billing mode in effect for a decision. Mirrors `NEXUS_BILLING_MODE`. */ type DecisionBillingMode = 'plan' | 'api'; /** Per-voter line in the decision rollup. */ interface VoterCostBreakdown { readonly role: string; readonly model: string; /** * The CLI the panel assigned this seat (#6115), when the caller knew it. * Omitted otherwise — absent is not a claim, the same rule as the cache * counters (#4439). */ readonly assignedCli?: string | undefined; /** Uncached input tokens. See {@link cachedInputTokens} for the rest. */ readonly inputTokens: number; readonly outputTokens: number; /** * Uncached input + output. * * Deliberately EXCLUDES the cache figures (#4435). Redefining this field to * include them is a semantics change for every existing consumer and every * record already written, so it is tracked separately rather than slipped in * — read it alongside `cachedInputTokens` for true consumption. */ readonly totalTokens: number; /** * Input tokens read from an existing prompt cache, when reported. Omitted * when the adapter said nothing — absent is not zero (#4439). */ readonly cachedInputTokens?: number | undefined; /** Input tokens spent writing the cache, when reported. */ readonly cacheCreationInputTokens?: number | undefined; /** Effective cost after billing-mode application (0 in plan mode). */ readonly costUsd: number; /** * True when no cost was computable for this voter — no usage report at all, * or a token-reporting call on an unpriced model (#4165). Its cost zero is a * placeholder, not a measured $0. */ readonly unmeasured: boolean; /** * What kind of rate `costUsd` rests on, when the caller stated one (#4406). * Omitted when it did not — absent is not a claim, the same discipline the * cache counters follow (#4439). * * Also omitted in `plan` mode, where `costUsd` was forced to 0: that zero * rests on no price, so attributing it to one would be a fiction. Same rule * as {@link DecisionCostSummary.priceBasis}, applied at the row level. */ readonly priceBasis?: PriceBasis | undefined; } /** Per-model rollup line within a single decision. */ interface ModelCostBreakdown { readonly model: string; readonly voterCount: number; readonly inputTokens: number; readonly outputTokens: number; readonly totalTokens: number; readonly costUsd: number; } /** * The per-decision cost rollup. Totals are a FLOOR when `unmeasuredVoters > 0` * — read alongside `measuredVoters` / `voterCount` for the confidence. */ interface DecisionCostSummary { /** Billing mode applied to produce `totalCostUsd` (and per-line `costUsd`). */ readonly billingMode: DecisionBillingMode; /** Total voters folded into this decision. */ readonly voterCount: number; /** Voters with a computable cost AND at least one reported token count. */ readonly measuredVoters: number; /** * Voters whose cost could not be computed — an unpriced model (#4165) — or * which reported no token counts at all (#4430). Counted, not zeroed-as-fact. */ readonly unmeasuredVoters: number; readonly totalInputTokens: number; readonly totalOutputTokens: number; readonly totalTokens: number; /** * Total cost in USD. 0 under `plan` mode by construction. A FLOOR when * `unmeasuredVoters > 0` (unmeasured voters contribute 0, not their unknown * real cost). */ readonly totalCostUsd: number; /** * What kind of rate `totalCostUsd` rests on (#4406) — `'list'` if ANY voter * that contributed a price used a list rate, `'unknown'` if every voter that * stated a basis had no price at all. * * OMITTED when no voter stated a basis (nothing was claimed) and in `plan` * mode, where the recorded $0 is pre-covered by a subscription and rests on * no price at all — `billingMode` already explains that figure, and labelling * it `'list'` would credit a rate that produced nothing. */ readonly priceBasis?: PriceBasis | undefined; /** Per-voter breakdown, in input order. */ readonly perVoter: readonly VoterCostBreakdown[]; /** Per-model breakdown, sorted by total cost desc then total tokens desc. */ readonly perModel: readonly ModelCostBreakdown[]; } /** * The one async-dispatch input every async-capable MCP tool composes (#4968). * * Ten tools dispatch through `runAsJob`, and before this module they did not * agree on what the switch was called: `mode` on three (`consensus_vote`, * `run_workflow`, `orchestrate`), `dispatch` on seven. Because the advertised * schemas strip unknown keys, the WRONG spelling was silently ignored — a * caller sending `consensus_vote { dispatch: 'async' }` got a 97-second * synchronous run and no jobId. Panel decision (quick panel, 3 of 3): `dispatch` * is canonical everywhere, `mode` is accepted as a deprecated alias on the three * that had it (removal is next-major, #6225), and the wrong key is rejected * instead of dropped. * * Where the wrong-key rejection has to live. The MCP SDK builds * `z.object(inputSchema)` from the ADVERTISED shape and hands the handler the * parsed, already-stripped object (`validateToolInput` in * `@modelcontextprotocol/sdk/server/mcp.js`). So neither a `.refine()` on the * tool's internal schema nor a `z.preprocess` in the handler can see a key the * SDK removed — either would be a check that cannot fire on the real path * while passing every unit test that calls the handler directly. The only * schema element that sees the raw value of `mode` is a `mode` entry in the * advertised shape itself; {@link REJECTED_MODE_KEY} is that entry. It is the * "declare the forbidden key with a never-type carrying the message" form the * panel's architect seat named. * * @module mcp/tools/async-dispatch-input */ /** The enum both field builders return. */ type DispatchEnum = z.ZodEnum<{ readonly sync: 'sync'; readonly async: 'async'; }>; /** The type of {@link REJECTED_MODE_KEY}: absent is fine, any value is an error. */ type RejectedModeKey = z.ZodOptional; /** * nexus-agents/consensus - Core Type Definitions * * Core type definitions and Zod schemas for the consensus engine. * Supports multiple voting strategies for multi-agent decisions. */ /** * Consensus algorithm types. * - simple_majority: >50% of votes required * - supermajority: >=67% of votes required * - unanimous: 100% approval required * - proof_of_learning: weighted voting based on agent performance * - opinion_wise: higher-order voting with correlation awareness (Issue #333) * - higher_order: alias for opinion_wise (Issue #514) */ declare const ConsensusAlgorithmSchema: z.ZodEnum<{ simple_majority: "simple_majority"; supermajority: "supermajority"; unanimous: "unanimous"; proof_of_learning: "proof_of_learning"; opinion_wise: "opinion_wise"; higher_order: "higher_order"; }>; type ConsensusAlgorithm = z.infer; /** * Vote decision options. */ declare const VoteDecisionSchema: z.ZodEnum<{ approve: "approve"; reject: "reject"; abstain: "abstain"; }>; type VoteDecision = z.infer; /** * Proposal status in the lifecycle. */ declare const ProposalStatusSchema: z.ZodEnum<{ timeout: "timeout"; closed: "closed"; rejected: "rejected"; pending: "pending"; voting: "voting"; approved: "approved"; }>; type ProposalStatus = z.infer; /** * Structured rejection feedback categories (Issue #1213). * Enables reject→refine→re-vote feedback loops by classifying rejection reasons. */ declare const RejectionCategorySchema: z.ZodEnum<{ YAGNI: "YAGNI"; DRY_VIOLATION: "DRY_VIOLATION"; OVER_ENGINEERING: "OVER_ENGINEERING"; SCOPE_CREEP: "SCOPE_CREEP"; SECURITY_RISK: "SECURITY_RISK"; MISALIGNED: "MISALIGNED"; INSUFFICIENT_EVIDENCE: "INSUFFICIENT_EVIDENCE"; }>; type RejectionCategory = z.infer; /** * All valid rejection category values, for runtime reference. */ declare const REJECTION_CATEGORIES: ("YAGNI" | "DRY_VIOLATION" | "OVER_ENGINEERING" | "SCOPE_CREEP" | "SECURITY_RISK" | "MISALIGNED" | "INSUFFICIENT_EVIDENCE")[]; /** * A vote cast by an agent. */ declare const VoteSchema: z.ZodObject<{ decision: z.ZodEnum<{ approve: "approve"; reject: "reject"; abstain: "abstain"; }>; reasoning: z.ZodString; confidence: z.ZodNumber; conditions: z.ZodOptional>; rejectionCategories: z.ZodOptional>>; findings: z.ZodOptional>; gate: z.ZodObject<{ reread_cited_line: z.ZodDefault>; traced_call_path: z.ZodDefault>; named_assertion: z.ZodDefault; ruled_out_language_non_issue: z.ZodDefault>; }, z.core.$strip>; claim: z.ZodString; }, z.core.$strip>>>; selectedOption: z.ZodOptional; timestamp: z.ZodOptional; }, z.core.$strip>; type Vote = z.infer; /** * A proposal submitted for consensus. */ declare const ProposalSchema: z.ZodObject<{ id: z.ZodOptional; title: z.ZodString; description: z.ZodString; algorithm: z.ZodEnum<{ simple_majority: "simple_majority"; supermajority: "supermajority"; unanimous: "unanimous"; proof_of_learning: "proof_of_learning"; opinion_wise: "opinion_wise"; higher_order: "higher_order"; }>; timeout: z.ZodOptional; requiredVoters: z.ZodOptional>; metadata: z.ZodOptional>; createdAt: z.ZodOptional; }, z.core.$strip>; type Proposal = z.infer; /** * Unique identifier for a proposal. */ type ProposalId = string; /** * Vote counts summary. */ interface VoteCounts { approve: number; reject: number; abstain: number; total: number; } /** * Weighted vote counts for proof-of-learning. */ /** * What a weighted tally's weights were actually derived from (#5117). * * `proof_of_learning` reported `"X% weighted approval"` and a populated * `weightedCounts` for tallies in which every weight was structurally `1.0` — * because the performance map feeding them has never had a writer * (`updateAgentPerformance` has no non-test caller). A reader was invited to * believe voter track record influenced the outcome. It did not, and could not. * * Deliberately NOT derived by checking whether any weight differs from `1.0`. * A voter with a perfect record legitimately weighs `1.0`, so numeric equality * cannot tell "measured, and they were reliable" from "never measured" — that * test would be its own can't-distinguish defect. The basis is derived from * PROVENANCE: whether a performance record existed for each voter. * * `partial` is a real state, not a rounding of the other two. Some voters * having history while others do not must not be reported as fully * performance-weighted. */ type WeightBasis = 'performance' | 'partial' | 'unweighted'; interface WeightedVoteCounts { approve: number; reject: number; abstain: number; totalWeight: number; } /** * Result of a consensus decision. */ interface ConsensusResult { proposalId: ProposalId; proposal: Proposal; outcome: ProposalStatus; votes: Map; voteCounts: VoteCounts; weightedCounts?: WeightedVoteCounts | undefined; /** * What the weights in `weightedCounts` were derived from (#5117). * * Carried on the RESULT, not just computed inside the strategy, because the * result is what a reviewer reads. A tally reported as weighted when every * weight was structurally 1.0 invites the reader to believe voter track * record moved the number. Absent for strategies that do not weight at all. */ weightBasis?: WeightBasis | undefined; approvalPercentage: number; quorumReached: boolean; startedAt: string; closedAt: string; durationMs: number; } /** * Consensus result schema for validation. */ declare const ConsensusResultSchema: z.ZodObject<{ proposalId: z.ZodString; proposal: z.ZodObject<{ id: z.ZodOptional; title: z.ZodString; description: z.ZodString; algorithm: z.ZodEnum<{ simple_majority: "simple_majority"; supermajority: "supermajority"; unanimous: "unanimous"; proof_of_learning: "proof_of_learning"; opinion_wise: "opinion_wise"; higher_order: "higher_order"; }>; timeout: z.ZodOptional; requiredVoters: z.ZodOptional>; metadata: z.ZodOptional>; createdAt: z.ZodOptional; }, z.core.$strip>; outcome: z.ZodEnum<{ timeout: "timeout"; closed: "closed"; rejected: "rejected"; pending: "pending"; voting: "voting"; approved: "approved"; }>; votes: z.ZodMap; reasoning: z.ZodString; confidence: z.ZodNumber; conditions: z.ZodOptional>; rejectionCategories: z.ZodOptional>>; findings: z.ZodOptional>; gate: z.ZodObject<{ reread_cited_line: z.ZodDefault>; traced_call_path: z.ZodDefault>; named_assertion: z.ZodDefault; ruled_out_language_non_issue: z.ZodDefault>; }, z.core.$strip>; claim: z.ZodString; }, z.core.$strip>>>; selectedOption: z.ZodOptional; timestamp: z.ZodOptional; }, z.core.$strip>>; voteCounts: z.ZodObject<{ approve: z.ZodNumber; reject: z.ZodNumber; abstain: z.ZodNumber; total: z.ZodNumber; }, z.core.$strip>; weightedCounts: z.ZodOptional>; approvalPercentage: z.ZodNumber; quorumReached: z.ZodBoolean; startedAt: z.ZodISODateTime; closedAt: z.ZodISODateTime; durationMs: z.ZodNumber; }, z.core.$strip>; /** * Agent performance record for proof-of-learning. */ interface AgentPerformance { agentId: string; totalVotes: number; correctVotes: number; successRate: number; lastUpdated: string; } /** * Agent performance schema. */ declare const AgentPerformanceSchema: z.ZodObject<{ agentId: z.ZodString; totalVotes: z.ZodNumber; correctVotes: z.ZodNumber; successRate: z.ZodNumber; lastUpdated: z.ZodISODateTime; }, z.core.$strip>; /** * Proposal content caching configuration for determinism. (Issue #589) */ interface ProposalCacheConfig { /** Enable content-based caching for repeated proposals */ enabled: boolean; /** TTL in milliseconds (default: 1 hour) */ ttlMs: number; /** Maximum cached entries (default: 500) */ maxEntries: number; } /** * Incremental quorum configuration (Issue #1408). * When enabled, ambiguous votes trigger voter pool expansion. */ interface IncrementalQuorumConfig { /** Enable incremental quorum expansion. Default: false */ enabled: boolean; /** Maximum expansion rounds (5→7→9). Default: 2 */ maxExpansionRounds: number; /** Voters to add per expansion round. Default: 2 */ votersPerExpansion: number; /** Minimum average confidence to avoid expansion. Default: 0.6 */ confidenceThreshold: number; /** Ambiguity band: if approval rate is within this of threshold, expand. Default: 0.15 */ ambiguityBand: number; } /** * Callback to request additional voters for incremental quorum. * Returns the IDs of newly added voters. */ type VoterExpansionCallback = (proposalId: ProposalId, currentVoterCount: number, requestedCount: number) => Promise; /** * Consensus engine configuration. */ interface ConsensusEngineConfig { defaultTimeout: number; minVotersForQuorum: number; maxActiveProposals: number; enablePerformanceTracking: boolean; /** Maximum number of closed proposals to retain. Oldest are evicted when exceeded. (Issue #549) */ maxClosedProposals: number; /** Content-based proposal caching for determinism (Issue #589) */ proposalCache?: ProposalCacheConfig; /** Incremental quorum configuration (Issue #1408) */ incrementalQuorum?: IncrementalQuorumConfig; } /** * Consensus engine configuration schema. */ declare const ConsensusEngineConfigSchema: z.ZodObject<{ defaultTimeout: z.ZodDefault; minVotersForQuorum: z.ZodDefault; maxActiveProposals: z.ZodDefault; enablePerformanceTracking: z.ZodDefault; maxClosedProposals: z.ZodDefault; proposalCache: z.ZodOptional; ttlMs: z.ZodDefault; maxEntries: z.ZodDefault; }, z.core.$strip>>; }, z.core.$strip>; /** * Default configuration values. */ declare const DEFAULT_CONSENSUS_CONFIG: ConsensusEngineConfig; /** * Internal proposal state managed by the engine. */ interface ProposalState { proposal: Proposal; status: ProposalStatus; votes: Map; voteWeights: Map; startedAt: Date; timeoutId?: ReturnType; /** Number of incremental quorum expansions applied (Issue #1408). */ expansionRounds?: number; /** * True while a quorum expansion is awaiting its callback for this * proposal. Concurrent `vote()` calls check this to avoid double- * expanding across the `await` gap (Issue #2861). Per-proposal so * independent proposals never block each other. */ expansionInFlight?: boolean; } /** * Consensus metrics for monitoring. */ interface ConsensusMetrics { totalProposals: number; approvedProposals: number; rejectedProposals: number; timedOutProposals: number; averageDurationMs: number; averageVotesPerProposal: number; algorithmUsage: Record; } /** * Consensus metrics schema. */ declare const ConsensusMetricsSchema: z.ZodObject<{ totalProposals: z.ZodNumber; approvedProposals: z.ZodNumber; rejectedProposals: z.ZodNumber; timedOutProposals: z.ZodNumber; averageDurationMs: z.ZodNumber; averageVotesPerProposal: z.ZodNumber; algorithmUsage: z.ZodRecord, z.ZodNumber>; }, z.core.$strip>; /** * nexus-agents/cli - Voter role configuration * * Which seats sit on a consensus panel, what each is told to evaluate, and * which subset the quick panel runs. Extracted from `cli/vote-types.ts` and * `mcp/tools/consensus-vote.ts` (#6000 step 1): a single edit here changes who * decides a vote, so the configuration lives apart from the routine result * types it used to share a file with. `vote-types.ts` re-exports the role * type and descriptions, so the public API is unchanged. * * @module cli/voter-roles */ /** * Voter agent role definitions. * * `scope_steward` (#2185) was added 2026-04-25 to address a build-vs-buy * blind spot in the original 6-role panel: the panel approved a proposal * to build a USB-flasher CLI without flagging that Rufus already solves * the problem. The scope-steward role explicitly checks for existing tools * + biases toward "don't build." */ type VoterRole = 'architect' | 'security' | 'devex' | 'ai_ml' | 'pm' | 'catfish' | 'scope_steward'; /** * nexus-agents vote command types * * Type definitions for the consensus voting CLI command. * * (Source: Issue #212, Process Automation Epic #209) */ /** * #4135: how the `vote` command maps a `no_quorum` decision — a quorum void * (a missing/errored voice under the opt-in `absolute_quorum` error policy, or an * error-policy short-circuit), which is DISTINCT from a genuine rejection. * * - `fail` (default): exit 1, exactly as a rejection would — back-compat. * - `exit2`: exit with a distinct code 2 so scripts can tell a quorum void apart * from an approval (0) or a rejection (1). * - `retry`: re-run the vote ONCE (the plan is fine, a voice was missing); if it * still cannot reach quorum, fall back to `fail` (exit 1). */ type NoQuorumPolicy = 'fail' | 'exit2' | 'retry'; /** * Which evidence classified a seat as `unverifiable` (#6094). * * - `stderr`: the structured signal — the CLI transport captured a sandbox / * shell failure on stderr while serving the completion. * - `reasoning`: the fallback — the seat's own reasoning text said it could * not read the artifact: the `UNVERIFIABLE:` prefix, or an error string * with no recovery asserted (`UNVERIFIABLE_REASONING_RE`, #6104). */ type UnverifiableSignal = 'stderr' | 'reasoning'; /** * Why a seat answered somewhere other than where it was assigned (#6115). * * The adapter error class that triggered the fallover, named with the * predicates the adapters already classify by (`rate-limit-detector`, * `cli-error-envelope`, the subprocess timeout patterns, the #6094 sandbox * signal). `capacity` is a DURABLE cap (out of usage credits, a spend * ceiling); `rate-limit` a transient throttle. `unknown` is the named empty * case — the message matched no class — never a default standing in for one. */ type FallbackReason = 'rate-limit' | 'capacity' | 'auth' | 'timeout' | 'sandbox' | 'unknown'; /** * A seat that answered on a different CLI or model than assigned (#6115). * * Present only when it happened. Two producers: the #3587 cross-CLI fallover * (`fromCli` is the assigned CLI, the answer's `cli` is where it went) and * the #6120 in-family model substitution (`fromCli` equals the answer's * `cli`; `fromModel` is the alias the seat asked for). `fromModel` is absent * when the assigned adapter never detected a model — the placeholder is not * a model and is not disclosed as one. */ interface SeatFallback { readonly fromCli: string; readonly fromModel?: string | undefined; readonly reason: FallbackReason; } /** One attempt's timing on one CLI lane (#6103). */ interface SeatAttemptTiming { /** The CLI key the attempt was serialized on (`adapterCliKey`). */ readonly cli: string; /** Milliseconds between enqueueing on that CLI's lane and the attempt actually starting. */ readonly queuedMs: number; /** Milliseconds the attempt ran once started, until it settled (answer, error or deadline). */ readonly ranMs: number; /** True for the cross-CLI fallback attempt (#3587); false for the primary. */ readonly fallback: boolean; } /** * What a recovered seat was retried FROM (#6246). * * `retryErroredRoles` replaces an absent first-pass seat — errored, or * unverifiable — with the retry's result. Before this the first pass was * discarded at that point: the merged panel, the summary row and the ledger * entry said the seat was retried, but not what it recovered from. On the * #6241 ratification panel the catfish seat's first pass errored on two * response-parse failures and the retry came back unverifiable; nothing joined * the two, and #6244 read the parse errors as a misclassification. * * `source` is the first pass's `source` — only the two absent values, a * literal union so a record cannot claim a first pass that was never retried. * `error` is the first pass's `error` string, present only when it had one * (an unverifiable seat's cause lives in its reasoning and carries no * `error`), with control characters replaced and bounded by the #5373 record * clip; `errorTruncated` is that clip's marker, present only when it fired. */ interface RetriedFrom { readonly source: 'error' | 'unverifiable'; readonly error?: string | undefined; readonly errorTruncated?: true | undefined; } /** * Individual agent vote with metadata. */ interface AgentVoteResult { readonly role: VoterRole; readonly vote: Vote; readonly processingTimeMs: number; /** * Source of the vote: * - 'llm': Real LLM execution * - 'simulation': Fallback simulation (opt-in only) * - 'error': Error during execution (Issue #523) * - 'unverifiable': the seat answered but could not read the artifact * (#6094). A DISTINCT value, not a flag on `abstain`, so no aggregation * over `vote.decision` can fold it back into the abstain bucket. The * `vote.decision` is always `abstain` — an unverifiable seat never carries * approve/reject, whatever the model returned. */ readonly source: 'llm' | 'simulation' | 'error' | 'unverifiable'; /** * Present only when `source === 'unverifiable'`: which evidence classified * the seat (#6094). Lets an auditor tell a structured stderr signal from the * reasoning-text heuristic. */ readonly unverifiableSignal?: UnverifiableSignal | undefined; /** CLI that executed this vote (for adaptive routing feedback). */ readonly cli?: string | undefined; /** * Which named option this voter chose, when the proposal declared `options` * (#4452). Absent on an ordinary yes/no vote. * * The approve/reject/abstain tally cannot express option choice: on a * multi-option proposal every engaged voter returns `approve`, so a real 6-1 * split records as unanimous. This is what makes the split recoverable * without parsing free-text `reasoning`. */ readonly selectedOption?: string | undefined; /** * True when this vote came from the per-role retry of an errored seat * (#5578). A first-attempt vote never carries it. * * The panel launches once; a voter that errors is dropped, so under * `reduce_denominator` its seat silently leaves the denominator and under * `absolute_quorum` the whole vote voids and the caller replays all N * voters for a single failure. Retrying just the errored roles recovers the * seat for one extra call — and this flag is what makes the recovery * visible instead of indistinguishable from a clean first attempt. */ readonly retried?: boolean | undefined; /** * What this seat was retried from (#6246): the first pass's source and, when * it had one, its error string. Present only on a seat the per-role retry * REPLACED — never on a first-attempt result, and never on a seat whose retry * failed again (that seat keeps its first attempt, unmarked). Orthogonal to * {@link retried}: that flag says a recovery happened; this says what it * recovered from. */ readonly retriedFrom?: RetriedFrom | undefined; /** * Model id that executed this vote, when known (e.g. 'claude-sonnet'). Carried * so per-decision cost aggregation can attribute spend per model (#3855). Absent * for error/simulation votes that never reached a model. */ readonly model?: string | undefined; /** * Model assigned to this role before execution. Unlike `model`, this stays on * the primary assignment when router failover serves the vote elsewhere. */ readonly pinnedModel?: string | undefined; /** * The CLI the round-robin or `NEXUS_VOTER_MODEL_` pin chose for this * seat (#6115), as a bare name (`claude`, not `cli-claude`). Unlike * {@link pinnedModel} it is known BEFORE detection, so it survives the * `pending-detection` placeholder and says where a seat was meant to * answer. Compare with `cli` to see where it did. Absent on results built * outside the panel launcher (simulation, direct `executeAgentVote` calls). */ readonly assignedCli?: string | undefined; /** * The `api:` gateway arm this seat answered through, when it * answered through an in-process gateway adapter (#4392 increment 2, step * 4). Absent on a CLI-subprocess seat and on results built outside the * panel launcher. The decision-cost rollup keys on it: a gateway seat is * priced by the arm's `NEXUS_GATEWAY_COST` declaration — UNKNOWN when * undeclared — never by the model id's vendor list price, which is what a * `claude-*` id served by a gateway used to record. `cli` stays the * adapter's `providerId` (`openai`), which cannot say this. */ readonly gatewayArm?: EndpointArmId | undefined; /** * Present only when the seat answered on a different CLI or model than * assigned (#6115). Three consecutive 7-seat panels ran every seat on one * model because claude was out of credits and codex could not spawn, and * nothing in the result said so — a single-model panel is a weaker * independence claim than the assignment, and the tally read identically. */ readonly fallback?: SeatFallback | undefined; /** * Per-attempt timing of this seat (#6103): how long each attempt QUEUED * behind its CLI's serialized lane (#3348) and how long it RAN, in launch * order — the primary attempt first, then the cross-CLI fallback (#3587) * when one was made. Absent on a seat that never reached the launcher * (simulation, a direct `executeAgentVote`); an empty `attempts` list on a * seat the launcher refused before any attempt (cancelled). Recorded so the * panel wall-clock can be attributed to queueing versus model time before * a fallback lane is designed; never folded into the vote record. */ readonly timing?: { readonly attempts: readonly SeatAttemptTiming[]; } | undefined; /** * Input tokens the adapter reported for this voter's LLM call, when known * (#3910). Propagated from `CompletionResponse.usage` so per-decision cost * aggregation resolves from `unmeasured` to MEASURED. Absent for * error/simulation votes that never reached a model, or for adapters that do * not report usage (CLI subscriptions) — those stay honestly `unmeasured`. */ readonly inputTokens?: number | undefined; /** * Output tokens the adapter reported for this voter's LLM call, when known * (#3910). See {@link AgentVoteResult.inputTokens}. */ readonly outputTokens?: number | undefined; /** * Input tokens read from an existing prompt cache, when the adapter * reported them (#4435). Separate from {@link inputTokens} because cache * reads bill at roughly a tenth of the uncached rate. */ readonly cachedInputTokens?: number | undefined; /** * Input tokens spent writing the cache, when reported (#4435). Bills at * roughly 1.25x the uncached rate — the opposite end from a cache read. */ readonly cacheCreationInputTokens?: number | undefined; /** Error message if vote fell back to simulation or encountered an error */ readonly error?: string; } /** * Panel model diversity (#6115). * * `costSummary.perModel` showed the final model per seat but not the ASSIGNED * one, so "7 of 7 on gemini" was visible only to someone who knew the * round-robin. This module measures what the response and the summary line * disclose: how many distinct models actually answered, and how many seats * answered somewhere other than where they were assigned. * * Shared by the MCP response (`consensus-vote-types`) and the CLI / GitHub * renderings (`vote-summary-lines`) so the two cannot drift. * * @module cli/vote-diversity */ /** * Always present on the response, explicit zeros included: an absent key * would read as a healthy spread, which is exactly what the three * single-model panels looked like. */ interface PanelDiversity { /** Distinct models (by canonical identity, #4390) among the seats that answered. */ readonly distinctModels: number; /** Seats that answered on a CLI or model other than the one assigned. */ readonly fallbacks: number; } /** * nexus-agents/cli - Target project for the voter panel (#6110) * * `getVoterPrompts(project)` puts the project name into every voter's system * prompt, and until #6110 nothing ever passed one: a consuming repository got a * scope_steward judging its proposal against "the nexus-agents project". This * module answers "which project is the panel judging?" from three sources, in * order, and says which one answered: * * 1. `input` — the caller's explicit `project` (tool input or `--project`). * 2. `derived` — `owner/repo` parsed from the `origin` URL in the repo's * `.git/config`, else the nearest `package.json` `name`. * 3. `default` — `nexus-agents`, this repository's own name. * * Decided by a 7-voter panel (option C, 4 of 7; job * `job-consensus_vote-b97309d13ca77db9`) with two safeguards adopted from the * minority: the SOURCE is disclosed on the response so a forgotten input reads * as `default` next to the verdict instead of a silent mis-scope, and no * candidate — from any source — reaches a prompt unless it matches * {@link VOTER_PROJECT_PATTERN}. Derivation is a pure parse of files the * process can already read; it never spawns `git`. * * @module cli/voter-project */ /** Which of the three sources supplied the project name. */ type VoterProjectSource = 'input' | 'derived' | 'default'; /** The project the voter panel was told it is judging, and how that was decided. */ interface ResolvedVoterProject { readonly name: string; readonly source: VoterProjectSource; } /** * Available consensus voting strategies. * * - `simple_majority`: Standard majority voting (>50%) * - `supermajority`: Requires >=67% approval * - `unanimous`: Requires 100% approval * - `proof_of_learning`: Weighted by agent performance (Issue #103). NOTE: weights come from * recorded voter history, and nothing writes that history today (#5234), so in practice this * currently behaves as simple_majority. The outcome reports `weightBasis: 'unweighted'` when * that is the case (#5117) rather than claiming a weighting that did not happen. * - `higher_order`: Bayesian-optimal with correlation awareness (Issue #514) * - `opinion_wise`: Alias for higher_order (Issue #333) */ type VotingStrategy = 'simple_majority' | 'supermajority' | 'unanimous' | 'proof_of_learning' | 'higher_order' | 'opinion_wise'; /** * How error-source votes (timed-out or crashed voters) are counted toward * the threshold (#2630). * * - `reduce_denominator` (default for non-strict strategies): errors are * filtered out before the engine sees votes — denominator = non-error * votes. Best for operational decisions where you trust the responding * voters and infrastructure flake should not block the vote. * - `count_as_abstain`: error votes reach the engine as abstain. Behaves * conservatively — a timed-out voter effectively withholds approval * relative to the threshold. Use when you can't tell what the error * voter would have decided and want the math to reflect uncertainty. * - `fail_closed` (default for unanimous / higher_order): any error voids * the vote. Threshold math is not run. Use for security-critical or * breaking-change decisions where every voter must be heard. * - `absolute_quorum` (opt-in, #4132): an errored voter DEGRADES the panel * verdict to `no_quorum` instead of being silently dropped from the * denominator. Unlike `fail_closed` (which reports a rejection-flavored void), * `absolute_quorum` reports `no_quorum` — a recoverable "re-run the missing * voice" state that never manufactures `approved` NOR `rejected` from an * induced error. An approval requires ZERO errors, the contrarian (catfish) * present and non-error (unless quick-mode drops it), and an ABSOLUTE approval * count (`ceil(fraction * panelSize)` over the full requested panel — not just * a majority of the responders). A genuine reject (zero errors) still blocks. * The anti-DoS point: a voter you can knock offline can only ever force a * re-run, never flip the verdict. * * Regardless of policy, a hard floor applies: when errors exceed 50% of * total voters, the vote always fails. Catches "all CLIs are down" — a * 2-voter consensus is not a real consensus. */ declare const ErrorPolicySchema: z.ZodEnum<{ reduce_denominator: "reduce_denominator"; count_as_abstain: "count_as_abstain"; fail_closed: "fail_closed"; absolute_quorum: "absolute_quorum"; }>; type ErrorPolicy = z.infer; /** * Threshold values accepted by the `--threshold` CLI flag and the * \`threshold\` MCP input field (#2638 — single source of truth). * * Maps to consensus algorithms via: * `majority → simple_majority`, `supermajority → supermajority`, `unanimous → unanimous`. * * Used as the canonical Zod schema for CLI parsing * (`cli.ts:parseThreshold`), validation (`cli-commands-validators.ts:isValidThreshold`), * and the `ConsensusVoteInputSchema.threshold` field. */ declare const VoteThresholdSchema: z.ZodEnum<{ supermajority: "supermajority"; unanimous: "unanimous"; majority: "majority"; }>; type VoteThreshold = z.infer; declare const ConsensusVoteInputSchema: z.ZodObject<{ idempotencyKey: z.ZodOptional; ratifies: z.ZodOptional; ratifiesPr: z.ZodOptional>; dispatch: z.ZodOptional; mode: z.ZodOptional>; proposal: z.ZodString; options: z.ZodOptional>; project: z.ZodOptional; threshold: z.ZodOptional>; strategy: z.ZodOptional>; errorPolicy: z.ZodOptional>; quickMode: z.ZodDefault>; simulateVotes: z.ZodDefault>; }, z.core.$strip>; type ConsensusVoteInput = z.infer; interface AgentVoteSummary { role: string; decision: 'approve' | 'reject' | 'abstain'; confidence: number; reasoning: string; simulated: boolean; /** True when this vote was generated from an error (Issue #815). */ error: boolean; /** Model used for this agent's vote (Issue #817). */ modelUsed?: string; /** Structured rejection categories for reject→refine→re-vote loops (Issue #1213). */ rejectionCategories?: readonly string[]; /** * True when this seat was recovered by the per-role retry (#6050). * * Present only when true. A retried seat is weaker evidence than a first-pass * one — the model was unavailable or timed out — so a caller reading a panel * result should be able to see that "7 of 7 answered" and "6 answered, 1 * recovered" are different facts. */ retried?: boolean; /** Which declared option this voter chose (#4472). Absent when the proposal * declared none, or the voter's selection matched none of them. */ selectedOption?: string; /** * True when this seat could not read the artifact (#6094). Present only * when true. Its `decision` is always `abstain`; `voteCounts.unverifiable` * counts these seats separately so a blind seat is never read as a * considered abstention. */ unverifiable?: true; /** * Present only when this seat answered on a different CLI or model than it * was assigned (#6115): where it was meant to answer and the adapter error * class that moved it. `panelDiversity.fallbacks` counts these seats. */ fallback?: SeatFallback; /** * Present only when the per-role retry REPLACED this seat (#6246): the first * pass's source and, when it had one, its clipped error string. `retried` * says a recovery happened; this says what it recovered from. */ retriedFrom?: RetriedFrom; } /** * Canonical set of decision statuses a vote response can carry. Single source * of truth: the `consensus_vote` MCP `outputSchema` reuses * {@link VoteDecisionStatusSchema} so the advertised enum can never be narrower * than what {@link buildResponse} emits (all five are reachable — * `no_quorum` on an all-error/no-quorum panel, the rest via * {@link mapOutcomeToDecision}). A narrower schema made strict MCP clients * reject `timeout`/`pending` votes with a `-32602`-class error (#4032). */ declare const VoteDecisionStatusSchema: z.ZodEnum<{ timeout: "timeout"; rejected: "rejected"; pending: "pending"; approved: "approved"; no_quorum: "no_quorum"; }>; type VoteDecisionStatus = z.infer; /** * Outcome of the quick-mode contrarian check (#6111). * * In quick mode the contrarian is a separate `executeExpert` call, not a seat * in `votes`, so `voteCounts.error` cannot count it: an errored check under * `absolute_quorum` yields `no_quorum` with `error: 0`. This names that voice. * * - `ok` — the check ran and returned a verdict (escalating or not). * - `errored` — the check ran and the contrarian voice was NOT obtained. * - `skipped` — the check did not run: full-panel mode (catfish is a seat), * simulated votes, a non-approved quick verdict, a posterior-confidence * escalation that pre-empted it, or an error-policy short-circuit. */ declare const ContrarianCheckStatusSchema: z.ZodEnum<{ ok: "ok"; skipped: "skipped"; errored: "errored"; }>; type ContrarianCheckStatus = z.infer; /** * Higher-Order Voting metadata (Issue #514). * * ADVISORY, not the verdict (#4701). Read `appliedToDecision` before drawing * any conclusion from the rest of this object. */ interface HigherOrderMetadata { posteriorApproval: number; posteriorRejection: number; effectiveVoteCount: number; /** * Aggregation used for THIS correlation-aware run — not necessarily the one * that produced `decision`. See {@link HigherOrderMetadata.appliedToDecision}. */ method: 'ow' | 'isp' | 'simple'; usedCorrelationData: boolean; improvementOverBaseline: number; downweightedAgents: readonly string[]; reasoning: string; /** * Whether this correlation-aware result actually produced the response's * `decision` (#4701). * * Currently ALWAYS FALSE. The verdict comes from `ConsensusEngine.close()`, * which calls `HigherOrderVotingStrategy.calculateOutcome` — and that calls * `aggregateSimpleInternal`, a plain `approve / (approve + reject)` ratio * with no correlation input. This object is computed separately and consumed * only as metadata plus one escalation check. * * The field exists because the omission was actively misleading: `method` * can read `'ow'` while `downweightedAgents` is non-empty, from which any * reasonable reader concludes the correlation analysis decided the vote. It * did not — the "seven voters that are really one opinion" case is detected * here and then discarded. * * Making the decision genuinely correlation-aware changes governance * outcomes and is tracked separately; this field makes the current state * legible in the meantime, including in persisted vote records. */ appliedToDecision: boolean; } interface ConsensusVoteResponse { proposal: string; threshold?: VoteThreshold; strategy: VotingStrategy; decision: VoteDecisionStatus; approvalPercentage: number; /** * `unverifiable` (#6094) counts seats that answered without reading the * artifact. Those seats are ALSO inside `abstain` (their legacy decision); * the bucket is always present, explicit 0 included — an absent key would * read as health. */ voteCounts: { approve: number; reject: number; abstain: number; error: number; unverifiable: number; }; /** * #6111: the quick-mode contrarian check, reported beside the tally because * `voteCounts.error` counts seats and the check is not one. Always present; * `skipped` is the named empty case (see {@link ContrarianCheckStatus}). */ contrarianCheck: ContrarianCheckStatus; votes: AgentVoteSummary[]; durationMs: number; simulateVotes: boolean; /** * #6110: the project every voter was told it is judging, and how that name * was decided — `input` (the caller's `project`), `derived` (the working * directory's `origin` remote or `package.json`), or `default` * (`nexus-agents`). Always present: a consuming repository that forgot the * input sees `default` next to the verdict instead of a silent mis-scope. */ project: ResolvedVoterProject; higherOrderMetadata?: HigherOrderMetadata; /** * Set when an error policy short-circuited the vote (#2630/#3124). Explains a * `rejected` decision that may coexist with a high `approvalPercentage` — e.g. * `fail_closed: 1 voter(s) errored`. Absent on normally-tallied votes. */ policyReason?: string; /** * Set when the panel was DEGRADED (#3587): some voters errored, so the * decision rests on fewer than the requested number of voters. Surfaces a * silently-shrunk panel so the result isn't read as a full-strength consensus. * Absent when every requested voter returned a real vote. */ panelWarning?: string; /** * #6115: how many distinct models answered and how many seats answered * somewhere other than where they were assigned. Always present, explicit * zeros included — three consecutive 7-seat panels ran every seat on one * model after the claude and codex seats fell over, and the response read * identically to a three-model panel. `panelWarning` names a single-model * panel of 3+ seats. */ panelDiversity: PanelDiversity; /** * Per-decision cost rollup (#3855): per-voter / per-model token + USD totals * for this governed decision. Rides the existing response — no new MCP tool. * Totals are a floor when `costSummary.unmeasuredVoters > 0` (voters whose * adapter reported no usage are counted as unmeasured, not a measured $0). */ costSummary?: DecisionCostSummary; /** * #4472: declared-option outcome, present only when the proposal declared * `options`. Separate from `approvalPercentage` — that stays the * approve/reject figure — so a caller can tell WHICH bar failed. * * `unattributedApprovals` is load-bearing, not decoration: a share alone * cannot distinguish dissent from absence, since `4 pick X + 3 unparseable` * reads 57% exactly like a real 4/3 split. */ optionOutcome?: { tally: ReadonlyArray<{ option: string; count: number; }>; leadingOption?: string; leadingShare: number; approverCount: number; selectedCount: number; unattributedApprovals: number; thresholdMet: boolean; /** * #4529: why the gate vetoed, present only when it did. Carried here rather * than on `policyReason`, which means "an error policy voided this vote" — * a split is a decision, not a void, and conflating them let a retry policy * re-roll a panel that had already disagreed. */ vetoReason?: string; }; /** * #3991: whether the authentic vote record (#3897) was persisted at vote time. * Post-#3991 the runtime ledger routes through `nexusDataPath` under * `governance/`, so a writable `.nexus-agents/governance/` location almost * always exists and `true` is the normal case. `false` means the persist was * skipped (all votes simulated) or the write failed (data dir unwritable) — * see {@link voteRecordNote}. Surfaces to the MCP caller what was previously * only a server-side WARN. */ voteRecordPersisted: boolean; /** * #3991: present only when {@link voteRecordPersisted} is `false` — the * actionable reason the record was not written (e.g. the data dir is unwritable * → fix permissions or set `NEXUS_VOTE_RECORDS_PATH` to a writable path). */ voteRecordNote?: string; /** * #5130: the persisted record's `id`, present only when * {@link voteRecordPersisted} is `true`. The caller-commits script * (`scripts/append-ratification-record.ts --record-id`) is keyed on it, and * a job result carries it so `--job ` can find the record. Without * it nothing downstream of the vote could name the record it produced. */ voteRecordId?: string; } export { ProposalSchema as $, type AgentVoteResult as A, type VotingStrategy as B, type CliNameLiteral as C, type DispatchEnum as D, type EndpointArmId as E, type ErrorPolicy as F, AgentPerformanceSchema as G, type HealthStatus as H, type InputModality as I, type AgentVoteSummary as J, ConsensusAlgorithmSchema as K, ConsensusEngineConfigSchema as L, type ModelId as M, ConsensusMetricsSchema as N, type ObservedArmId as O, type Pricing as P, type QualityScores as Q, type RoutingArmId as R, type SpecialFeature as S, type TokenUsage as T, ConsensusResultSchema as U, type VersionStatus as V, type WeightedVoteCounts as W, type ConsensusVoteInput as X, ConsensusVoteInputSchema as Y, type ConsensusVoteResponse as Z, DEFAULT_CONSENSUS_CONFIG as _, type CliName as a, ProposalStatusSchema as a0, REJECTION_CATEGORIES as a1, type RejectionCategory as a2, RejectionCategorySchema as a3, type VoteDecisionStatus as a4, VoteSchema as a5, type VoteThreshold as a6, type NoQuorumPolicy as a7, type CliTransport as b, type CliResponse as c, type CliError as d, type CapacityStatus as e, type OutputModality as f, type ToolCapability as g, type ConsensusAlgorithm as h, type Vote as i, type VoteDecision as j, type ProposalStatus as k, type RejectedModeKey as l, type VoterRole as m, type ResolvedVoterProject as n, type DecisionCostSummary as o, type CliErrorCode as p, type VoteCounts as q, type WeightBasis as r, type AgentPerformance as s, type ProposalState as t, type ProposalId as u, type ConsensusEngineConfig as v, type ConsensusResult as w, type Proposal as x, type ConsensusMetrics as y, type VoterExpansionCallback as z };