UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

106 lines (105 loc) • 7.57 kB
/** * The MODEL seat: how a task names a model (`ModelRef`), the roles a deployment binds models to * (`ModelRole` / `RoleSpec` / `ModelRoles`), and the provider seat a ref resolves to (`Brain`). Layer 0 * vocabulary beside `terminal-cause.ts` and `gate-outcome.ts` — no import above this layer, so any * module may read the words. `types.ts` re-exports every name below, so no consumer's import changes. */ import type { ThinkingLevel } from "../internal/harness.js"; import type { CompleteSimpleFn, Model, StreamFn } from "../internal/llm.js"; /** A model reference: either a fully-formed Model, or a `"provider/id"` ref resolved via a catalog. */ export type ModelRef = string | Model; /** * Logical model roles, so a deployment declares its models once and every subsystem asks for its * role instead of repeating a model name. Resolved against `RunnerDeps.roles` / `TaskSpec.roles` * with a fallback chain (e.g. `summarize → default`, `classifier → summarize → default`). A key or a * `modelRole` value outside this vocabulary is refused by name (`config.unknown_role`), never ignored. * - `default` — the main task model when `TaskSpec.model` is omitted. * - `summarize` — between-turn compaction/summarization. Absent = the MAIN task model, which is the * correct default baseline: the summary request FORKS the main conversation's already-paid * prompt-cache prefix (design/169-A CC form — same system prompt, same message prefix, one * appended instruction), so the marginal cost is a cache read plus one summary's output, not a * full-price re-prefill. Configuring a separate cheap model here is an explicit deployment * tradeoff (quality for price): an independent model cannot share the main prefix, so it pays the * full independent-request serialization every boundary AND summarizes with a weaker model. * - `subagent` — delegated sub-runs (`createSubagentTool` without an explicit model). * - `verifier` — the verification gate's verifier leg (falls back to `default`). * - `classifier` — the auto-mode permission classifier (a roster-cheap tier; falls back * `summarize → default`, tier default `flash` on tier-expanded catalogs). * - `consolidate` — the memory-consolidation distillation driver (design/376). Falls back to * `summarize` and then REFUSES LOUDLY — deliberately never to `default`: one consolidation run * is a whole-library read (~10⁵ prompt tokens), so silently escalating to the most expensive * main-task model is the bad-value-silence family, not a convenience. The divergence from the * classifier family's `→ default` tail is calibrated on call volume: a classifier resolves one * short classification, this role resolves a full-library batch. An explicit * `roles.consolidate = <main model>` is a legal deployment choice; the summarize hop is kept * because a deployment that declared a cheap summarize tier has already made the cost choice * this role wants (note the summarize DEFAULT rationale — prompt-cache forking — does not * apply here: consolidation is an offline independent request with no shared prefix). */ export declare const MODEL_ROLES: readonly ["default", "summarize", "subagent", "verifier", "classifier", "consolidate"]; /** The role vocabulary, derived from {@link MODEL_ROLES} — one source; a consumer that needs the members at * runtime (a settings schema, a parity pin, a doctor line) reads the tuple instead of transcribing it. */ export type ModelRole = (typeof MODEL_ROLES)[number]; /** * A role's model: a `ModelRef`, or a config object that pins a `model`, **selects** one by * capability/price (`select`, design/11 Layer 2), and/or sets a default `thinking` level. */ export type RoleSpec = ModelRef | { model?: ModelRef; select?: import("./select-model.js").ModelCriteria; thinking?: ThinkingLevel; /** * Optional preset system prompt for this role (e.g. {@link CODE_AGENT_PROMPT} for a dev role). * Used only when the task does not supply its own `TaskSpec.systemPrompt`. Lets coding roles run * a coding prompt while other roles keep the neutral default — without touching the global default. */ systemPrompt?: string; }; /** Map of role → model. `TaskSpec.roles` overrides `RunnerDeps.roles` per task/scenario. */ export type ModelRoles = Partial<Record<ModelRole, RoleSpec>>; /** The "external brain": a streaming completion function plus a non-streaming one (auto-derived if omitted). */ export interface Brain { /** Streaming completion. Must follow the AssistantMessageEvent stream contract (never throw post-invocation). * * NOTE: the return type is a union — `Stream | Promise<Stream>`. Third-party direct callers must * normalize first (`const s = await brain.stream(...)`) before iterating or reading `.result()`, * otherwise TS reports TS2339 on the union. The Runner already handles this internally. */ stream: StreamFn; /** * Optional non-streaming completion used by compaction/summarization. * If omitted, it is derived from `stream` by consuming the stream to its final result. * * This is exactly the runtime's `completeSimple` type: the engine forwards the value you supply * as-is, with no adapting wrapper in between. * * CONTRACT BEYOND WHAT THE TYPE STATES — every field named here is OPTIONAL on * {@link AssistantMessage}, so a message rebuilt from a field whitelist still typechecks while * silently disabling recovery: * - return the AssistantMessage with ALL fields intact, in particular `errorKind` (the compaction * summary path keys its budget-escalation recovery on it) and `errorMessage`; * - honor `options.signal` — the summary call runs under a soft deadline that aborts through it; * - honor `options.maxTokens` — that is the escalating budget the recovery re-issues the call with. */ complete?: CompleteSimpleFn; /** * Optional key↔URL pairing judge (see `src/brain/route-adjudicator.ts` for the law). Answers, for a * model this brain would serve, whether the credential the request would carry belongs to the URL * it would target — WITHOUT sending anything. `perModelAuth` is the caller's already-resolved * per-model auth (the `getApiKeyAndHeaders` result), so a resolution seat can pre-flight the exact * request it is about to make. First-party brains implement it against their own config; the * decorators (routing/failover/degrading/circuit-breaker) re-dispatch it the way their `stream` * would. `undefined` = this brain cannot judge (a custom brain without the face) — callers must * treat that as "no judgment", never as OK or as broken. A refusal is an `ok: false` verdict, never a * throw (the first-party faces include the per-call key handed beside its lane's own key header, * `route.credential_answer_conflict`); a throw from this face is a defect — an advisory pre-flight * abstains on it, and a failover chain's face lets it escape. * * The same law runs again inside the first-party brains' request build (single source, called * twice): a broken pairing that skips the pre-flight still refuses loudly at the request instead * of sending a credential to a host it is not paired with. */ adjudicateRoute?: (model: Model, perModelAuth?: { apiKey?: string; headers?: Record<string, string>; }) => import("../internal/llm.js").RouteAdjudication | undefined; }