UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

345 lines (344 loc) • 22.9 kB
import type { ExecutionEnv } from "../../internal/harness-types.js"; import type { AssistantMessage, Message, Model, Usage } from "../../internal/llm.js"; import type { CwdRef } from "../../tools/fs/fs-shared.js"; import { type PermissionResult, type ToolCallRequest } from "../tool-policy.js"; import type { HookInvocationIdentity, PermissionDeniedPayload } from "../hooks.js"; import type { PermissionMode } from "../ask-origin.js"; import type { RunnerDeps } from "../runner-deps.js"; import type { TaskSpec } from "../task-spec.js"; import { type ModBus, type ModMountedHandler, type ModDispatchOutcome, type ModEventName, type ModEventPayload, type ModRegistration } from "./kernel/index.js"; import { type SourcedOverlay } from "../../prompt-assembly/overlay.js"; import type { BandId } from "../../prompt-assembly/catalog/schema.js"; import type { ModParticipant } from "./kernel/dispatch.js"; import { type EngineDollar, type ModelCompleteRequest, type ModelCompleteResult, type ModSeatView } from "./dollar.js"; /** * What the engine's core answers at the bottom of every classic chain: the engine itself has nothing to add. * Frozen and empty, so it reads as "no opinion" in every classic station's answer form, and a handler that * builds on it (`{ ...(await next(e)), additionalContext }`) builds a well-formed answer. */ export declare const ENGINE_CORE_ANSWER: Readonly<Record<string, never>>; /** The classic chains' bottom. */ export declare function engineCore(): Readonly<Record<string, never>>; /** The leg's bus seat, as the stations read it. */ export type LegModBus = Pick<ModBus, "hasHandlers" | "dispatch">; /** * The id a delegated leg's events carry as `agentId` (the leg's task id — the key its `task_progress` frames * and its parent's fleet row use). A top-level leg carries none. */ export declare function agentIdOf(identity: HookInvocationIdentity | undefined): string | undefined; /** * The stop event a leg raises when its run would otherwise end: `classic.SubagentStop` on a delegated child's leg — the * delegation fact, which a child resumed from its checkpoint keeps — else `classic.Stop`. Not the leg kind: a resumed * leg's kind reads `"resume"` whoever it runs as. */ export declare function stopEventOf(identity: Pick<HookInvocationIdentity, "isDelegatedChild"> | undefined): "classic.Stop" | "classic.SubagentStop"; /** * The args as a judging handler receives them: the gate pass's own immutable value ({@link ownArgs} — the value itself when * the gate already took it, which is every call the gate raises an event over), so the bytes a handler judges are the bytes * that run — an in-place edit throws instead of moving what executes, or what a handler further down judges (a rewrite is * an answer's `updatedInput`). A graph the copy refuses is handed as is (the gate refuses such args at its entry). */ export declare function judgedInputOf(args: unknown): unknown; /** One link of a leg's chain as a constraint entry names it: a registration the leg's admission mounted, with the handlers it hung. */ export interface ModChainLink { readonly registration: ModRegistration<EngineDollar>; readonly handlers: readonly ModMountedHandler[]; } /** * A bus's chain identity: the registrations its admission MOUNTED (a refused one judges nothing there), in run order, each * with the handlers it actually hung (pattern, matcher, observe, `.catch`, in hang order). Two legs that offered the same * registrations but admitted — or hung — differently are different chains: an inherited entry collapses into a child's phase * 1 only when the child mounted the very same. A handler's code is not compared (each leg's `register` builds its own * closures); what it was hung on is. Every engine admission records it once ({@link chainOfBus}). */ export declare function admittedChainOf(bus: Pick<ModBus, "records" | "mounted">, spec: Pick<TaskSpec, "mods">, deps: Pick<RunnerDeps, "mods">): readonly ModChainLink[]; /** The chain identity a bus's admission recorded, or `undefined` for a bus no engine admission mounted (its chain cannot be * read, so it is the same chain as none). */ export declare function chainOfBus(bus: object): readonly ModChainLink[] | undefined; /** Whether two chains are the same links: the same registration objects, in the same order, having hung the same handlers. */ export declare function sameModChain(a: readonly ModChainLink[], b: readonly ModChainLink[]): boolean; /** * Whether the leg's chain JUDGES a tool call: a judging handler on either event a call is judged on — `classic.PreToolUse` * (the gate's phase 1) or `tool.check` (the gate's verdict read). Such a chain is what a leg hands its children as a * screening constraint, and what a child's own gate stands in for when it mounted the same. */ export declare function chainJudges(bus: LegModBus): boolean; /** Record what the constraint mint minted `policy` over. */ export declare function recordScreeningChain(policy: object, chain: LegModBus): void; /** The chain a screening constraint was minted over, or `undefined` when the mint did not make this object. */ export declare function screeningChainOf(policy: object): LegModBus | undefined; /** How an inherited installation's question is settled: under the context its installation froze (its approver, its durable * mandates, its classifier — the settlement its `classic.PreToolUse` ask takes in the fold), never at this leg's approver. An * allow carrying `updatedInput` is a person's approval of an edit; an ask is one the settlement floated (the call is marked for * the park). */ export type InstallationAskSettlement = (ask: Extract<PermissionResult, { action: "ask"; }>, call: ToolCallRequest, signal: AbortSignal | undefined) => Promise<PermissionResult>; /** An inherited installation as a leg's gate raises `tool.check` on it: the bus that mounted that chain, which of its handlers * judge here, and how its question is settled. */ export interface InheritedToolCheck { readonly bus: LegModBus; readonly judges: (p: ModParticipant) => boolean; readonly settle: InstallationAskSettlement; } /** An inherited chain entry as its installation identity reads it: the policy (whose mint names the bus, whose admission names * the chain), the approver it froze, its two durable mandates, the environment it was installed against, its classifier. */ export interface InstallationEntry { readonly policy: object; readonly onAsk?: unknown; readonly durableMandate?: boolean; readonly contentMandate?: boolean; readonly hookEnv?: unknown; readonly autoMode?: { readonly decider: unknown; }; } /** A leg's own installation context: its chain identity, its frozen approver, its two durable mandates, the environment its * phase 1 runs in, its classifier instance. */ export interface LegInstallation { readonly chain: readonly ModChainLink[]; readonly onAsk: unknown; readonly durableMandate: boolean; readonly contentMandate: boolean; readonly hookEnv: unknown; readonly decider: unknown; } /** * Whether an inherited entry IS a leg's installation — the ONE installation identity. The chain is read off the entry's own * bus (the one its policy was minted over, as that bus's admission recorded it), never off a list the entry carries: an entry * whose policy the mint did not make, or whose bus no engine admission mounted, is no leg's installation. The rest is the * context its questions are settled in: the same approver ({@link askApproverIdentity}), the same two durable mandates, the * same environment, the same classifier instance. Readers: chain assembly (a leg already on the chain is not appended again) * and {@link standsInFor} (the fold's pass-through and the `tool.check` raise). */ export declare function sameInstallation(pc: InstallationEntry, leg: LegInstallation): boolean; /** * Whether a leg's own gate stands in for an inherited entry: the leg's chain judges, and the entry is the leg's installation * settled with no durable mandate — its phase 1 then judges the call with the same chain, and its questions go to the same * approver under no mandate, so the entry adds nothing on either judging event. The fold's pass-through for the entry's * `classic.PreToolUse` and the skip of its `tool.check` raise are this one test (an entry it does not hold for is judged under * its own context on both: a second judgment, never a lost one). */ export declare function standsInFor(pc: InstallationEntry, leg: Omit<LegInstallation, "durableMandate" | "contentMandate">, legChainJudges: boolean): boolean; /** An inherited screening installation as the carrier check reads it: the bus its policy was minted over (absent when the mint * did not make the policy) and the chain the entry declares (its `mods`; absent when it declares none). The declaration is read * for one thing — a guard the leg cannot raise ({@link unraisableGuardsOf}); identity is read off the bus ({@link sameInstallation}). */ export interface ScreeningInstallation { readonly bus: LegModBus | undefined; readonly chain: readonly ModChainLink[] | undefined; } /** The screening installations among a chain's entries, in the chain's order: every entry whose policy the mint made or that * declares a chain. */ export declare function screeningInstallationsOf(entries: ReadonlyArray<{ readonly policy: object; readonly mods?: readonly ModChainLink[]; }>): readonly ScreeningInstallation[]; /** Why an inherited chain cannot hold its guards, or `undefined`: an entry that names a judging `tool.check` handler beside a * policy the mint did not make has no bus to raise that guard on. Read where the chain enters a leg — before a resume's claim * and at the leg's prepare — so it is refused, never run without the guard. */ export declare function unraisableGuardsOf(installations: readonly ScreeningInstallation[]): string | undefined; /** * The inherited installations a leg's gate raises `tool.check` on after its own bus — the rule's one home. Every entry whose * policy the mint made is one installation, raised whole in the chain's order (the order its `classic.PreToolUse` folds in), * except one the leg's own gate stands in for (`standsIn` — the fold's pass-through test, {@link standsInFor}). Raised whole * because a handler's answer depends on the chain it sits in (an outer handler may answer without calling `next`, or rule on * what an inner one answered), so a registration the leg also mounts in a chain of its own judges again inside the ancestor's * chain, as the PreToolUse constraint judges it (a second judgment, never a lost one). Each carries its entry's settlement of a * question (`settleOf`): an installation's `ask` goes where its PreToolUse ask goes. Observe handlers never take part (a * constraint carries no observer). An entry that cannot raise its guard is refused (`config.mods_invalid`, {@link unraisableGuardsOf}). */ export declare function inheritedToolChecks<E extends InstallationEntry & { readonly mods?: readonly ModChainLink[]; }>(entries: readonly E[], standsIn: (pc: E) => boolean, settleOf: (pc: E) => InstallationAskSettlement): readonly InheritedToolCheck[]; /** The members every classic `e` of a leg carries: the identity envelope, the delegated leg's `agentId`, the leg's observed mode. */ export declare function classicEventBase(identity: HookInvocationIdentity | undefined, permissionMode?: PermissionMode): ModEventPayload; /** * Raise an observation event (a skip-on-failure event whose answer no station reads): no handler, no dispatch. * Its failures are the bus's to report; a rejected dispatch (an engine fault, never a handler's) is swallowed — * an observation must never change what it observes. */ export declare function observeClassic(bus: LegModBus, event: ModEventName, e: ModEventPayload, signal: AbortSignal | undefined): Promise<void>; /** * Raise a classic event whose answer the station reads: `undefined` when no handler hangs on it (the station * runs as if the bus were not there), else the dispatch outcome. A rejection is an engine fault, never a * handler's (handlers fail into the outcome); a decision station fails closed on it, an observation swallows it. */ export declare function dispatchClassic(bus: LegModBus, event: ModEventName, e: ModEventPayload, options: { readonly signal?: AbortSignal; readonly checkAnswer?: (answer: unknown) => string | undefined; }): Promise<ModDispatchOutcome | undefined>; /** The answer form of a classic event whose answer is an object of optional string members (`block`, `additionalContext`, …). */ export declare function stringMembersAnswer(event: string, members: readonly string[]): (answer: unknown) => string | undefined; /** The string members of an answer the station reads (a member of any other type reads as absent — the answer check has already refused it). */ export declare function stringMembersOf<M extends string>(answer: unknown, members: readonly M[]): { [K in M]?: string; }; /** The clause a fail-closed station puts in its sentence for the handler that failed it: `crashed (extension "x" threw boom)`. */ export declare function failedHandlerClause(f: Extract<ModDispatchOutcome, { kind: "failed"; }>): string; /** * The PermissionDenied station: the leg's `classic.PermissionDenied` handlers, then a host's own raw observer (a host * driving the tool gate itself) as one more observe handler of the same event on a bus of its own — so every observer * of a deny runs under that station's columns (the handler's budget, skipped on a failure, a short grace past an * abort) and none can hold the deny it reports. The raw observer's failure goes to `onHostObserverFailure`. */ export declare function observePermissionDenied(input: { readonly legBus: LegModBus | undefined; readonly hostObserver: ((payload: PermissionDeniedPayload) => void | Promise<void>) | undefined; readonly payload: PermissionDeniedPayload; readonly signal: AbortSignal | undefined; readonly onHostObserverFailure: (err: Error) => void; }): Promise<void>; /** What a `classic.UserPromptSubmit` screen came to, as the four entrances read it. */ export type PromptScreen = { readonly kind: "answered"; readonly block?: string; readonly additionalContext?: string; } | Extract<ModDispatchOutcome, { kind: "failed"; }> | { readonly kind: "aborted"; }; /** * Raise `classic.UserPromptSubmit` (undefined when no handler hangs on it). A decision event: a handler that throws, * runs past its budget or breaks the site's contract ends the screen `failed`, and every entrance fails closed on it; * a cancelled screen ends `aborted` at once. A dispatch the engine itself could not run is `failed` too. */ export declare function screenUserPrompt(bus: LegModBus, e: ModEventPayload, signal: AbortSignal | undefined): Promise<PromptScreen | undefined>; /** The three words a `tool.check` verdict speaks. */ export declare const TOOL_CHECK_DECISIONS: readonly ["allow", "ask", "deny"]; export type ToolCheckDecision = (typeof TOOL_CHECK_DECISIONS)[number]; /** * The gate's standing verdict as the bottom of the `tool.check` chain answers it — what a handler reads from `next(e)`. * Never `deny` (a standing refusal raises nothing). On an ask: its classes, and `mandated` — true when an `allow` * answer cannot clear it at this point of the gate. `ceiling` mirrors `e.ceiling`. Read-only metadata: the gate reads * the answer's `decision` and `reason` and nothing else of it. */ export interface ToolCheckProjection { readonly decision: "allow" | "ask"; readonly reason?: string; readonly classes?: readonly string[]; readonly mandated?: boolean; readonly ceiling?: "ask"; } /** What one `tool.check` raise came to: the chain's verdict (`by` = the registration whose answer the chain returned; * absent when no handler spoke and the gate's own verdict came back up), a handler's failure, or a cancel. */ export type ToolCheckOutcome = { readonly kind: "answered"; readonly decision: ToolCheckDecision; readonly reason?: string; readonly by?: string; } | Extract<ModDispatchOutcome, { kind: "failed"; }> | { readonly kind: "aborted"; }; /** * Raise `tool.check` over the gate's standing verdict (undefined when no handler hangs on it — nothing is raised). A * refusing event: a handler that throws, runs past its budget or answers out of the site's form ends the raise * `failed`, a cancel ends it `aborted`, and the gate refuses the call on either. A dispatch the engine itself could not * run is `failed` too. `judges` = which handlers take part (absent ⇒ every handler on the bus: the leg's own raise; an * inherited installation's raise passes its {@link InheritedToolCheck.judges}). */ export declare function raiseToolCheck(bus: LegModBus, e: ModEventPayload, projection: ToolCheckProjection, signal: AbortSignal | undefined, judges?: (p: ModParticipant) => boolean): Promise<ToolCheckOutcome | undefined>; /** What a `prompt.compose` raise came to: each source class's overlays in application order, and the operator lines * for every handler whose answer was skipped. */ export interface PromptComposeOutcome { readonly deploy: readonly SourcedOverlay[]; readonly task: readonly SourcedOverlay[]; readonly warnings: readonly string[]; } /** * Raise `prompt.compose` — the collection point of the deploy and task source classes' overlays — over the leg's * bus (nothing is raised when no handler hangs on it). A collecting event: a handler's contribution is what its * answer adds after what its `next(e)` returned, and what `next(e)` returned must stand unchanged, in order, at the * head of its answer — a handler that answers without its `next(e)` (or before it returned), rewrites or removes a * lower contribution, or adds an overlay the band table does not grant its seat's class (or one that carries locked * text, or one that is not data), is skipped, and what is below it stands (runs in its place, when it never called * `next`). The engine records, at each handler's boundary, the overlays as it validated them and who contributed * each (the registration and its seat: the deployment seat is the deploy class, the task seat the task class); what * applies is that record, never the answer read again. Each class applies its contributions in chain order, so a * later registration's overlay on the same target wins. * * `lowerAdds` — the entries the classes below deploy already added (the only entries a deploy `drop` may name * besides a built-in section). */ export declare function raisePromptCompose(bus: LegModBus, e: ModEventPayload, options: { readonly lowerAdds: ReadonlyMap<string, BandId>; readonly signal?: AbortSignal; }): Promise<PromptComposeOutcome>; /** The seats behind the engine facade, bound once the leg has them. Every member is read at its call. */ export interface LegFacadeSeats { readonly exec: ModSeatView["env"]["exec"]; complete(request: ModelCompleteRequest, signal: AbortSignal): Promise<ModelCompleteResult>; readonly sessionId: string; /** The run's account of side model calls ({@link ModSeatView.book}). */ book: ModSeatView["book"]; cwd(): string; root(): string; model(): string; messages(): readonly Message[]; now(): number; tools(): readonly string[]; agents(): readonly string[]; } /** The leg's admitted bus, its chain identity (the one its admission recorded, {@link chainOfBus}) and the binding of the * facade its `$` reaches. */ export interface LegMods { readonly bus: ModBus; readonly chain: readonly ModChainLink[]; /** Bind the engine facade once the leg is prepared. Until then a `$` call throws, naming the method. */ bindFacade(seats: LegFacadeSeats): void; } /** * Admit the leg's bus: both seats (`RunnerDeps.mods`, `TaskSpec.mods`), screened and put to `plugin.register`. * Called once per leg, at that leg's own prepare (a delegated child and a resumed leg each admit their own); * each refused registration becomes one `mods.registration_refused` notice. Throws `config.mods_invalid` for a * registration the kernel cannot take. */ export declare function admitLegMods(input: { readonly spec: Pick<TaskSpec, "mods">; readonly deps: Pick<RunnerDeps, "mods" | "onError" | "onNotice">; readonly sessionId: string; }): Promise<LegMods>; /** What a leg hands {@link legFacadeSeats}: the seats it already has. */ export interface LegFacadeInput { readonly deps: Pick<RunnerDeps, "brain" | "models" | "now">; readonly spec: Pick<TaskSpec, "getApiKeyAndHeaders">; readonly sessionId: string; readonly model: Model; readonly executionEnv: Pick<ExecutionEnv, "exec">; readonly taskRoot: string; readonly cwdRef: CwdRef | undefined; /** The leg's latest model request as recorded at the provider seam (the session's messages as the model last saw them). */ readonly lastBrainContext: () => { readonly messages: readonly Message[]; } | undefined; readonly activeTools: ReadonlySet<string>; readonly agentListing: { readonly entries: ReadonlyArray<{ readonly name: string; }>; } | undefined; /** The run's one account of side model calls (the same entry the classifier's side calls book on), handed the model a * `$.model.complete` call resolved to, the usage its reply reported (absent ⇒ none reported) and the reply's * diagnostics when it carried any (a degraded brain's fallback is priced by them). */ readonly book: (model: Model, reply: { readonly usage: Usage | undefined; readonly diagnostics?: AssistantMessage["diagnostics"]; }) => void; } /** The engine facade's seats over a prepared leg — each one a seat the leg already has (no new capability). */ export declare function legFacadeSeats(input: LegFacadeInput): LegFacadeSeats; /** * A chain admitted outside any leg — what a host rebuilds to re-supply an inherited screening entry on a fresh Runner * (a cross-process resume hands back `createPreToolUseConstraintPolicy(chain, env)` over the same registrations). The * same admission a leg runs (both seats, `plugin.register`); no leg exists, so its `$` facade is never bound and a `$` * call throws. Handler failures go to `onError` (absent ⇒ unreported). Its chain identity is recorded as a leg's is * ({@link chainOfBus}), so an entry minted over it is read by what it mounted — whatever list the entry carries. */ export declare function admitModChain(seats: { readonly deployment?: readonly ModRegistration<EngineDollar>[]; readonly task?: readonly ModRegistration<EngineDollar>[]; }, onError?: (err: Error) => void): Promise<ModBus>;