import { type TaskInput, type RunnableConfig, type AgentType, type ExecutionRegistry, type ResolvedAgent } from '@zhixuan92/multi-model-agent-core'; import type { EnvelopeBus } from '@zhixuan92/multi-model-agent-core/events/envelope-bus'; import type { CallerContext } from './caller-context.js'; import type { ExecutionStore } from './execution-store.js'; import type { InitiativeLinker } from './initiative-linker.js'; import type { InitiativeRecordRuntime } from './initiative-record-runtime.js'; import type { ProjectRegistry } from './project-registry.js'; import type { ExecutionEntry } from '@zhixuan92/multi-model-agent-core'; interface ExecutionRuntimeDeps { /** Narrowed at daemon start by `assertRunnable`: the runtime cannot * resolve a tier without one, so it takes a config that provably has them. */ config: RunnableConfig; bus: EnvelopeBus; executionRegistry: ExecutionRegistry; projectRegistry: ProjectRegistry; /** Durable execution records — admission is persisted before the handle is * returned; terminal transitions mirror the in-memory registry. */ store: ExecutionStore; /** Outbox consumer for a linked Execution's terminal result (SPEC-003 Task I-5). Invoked * after every terminal store write below — optional so every existing unlinked-execution * call site (and every test that never admits a linkage) keeps working unchanged; an * execution with no `ExecutionLinkage` never produces an outbox row to replay anyway. */ initiativeLinker?: InitiativeLinker; /** Shared Initiative Record application service (SPEC-003 Task I-6) — resolves the linked * Initiative/Task and performs the pre-admission Task transition for a request carrying * `input.initiative`. Optional so every existing unlinked-only `ExecutionRuntime` * construction (most unit tests, which never set `input.initiative`) keeps working * unchanged; a linked request against a runtime with none wired is rejected as an invalid * linked request before any handle or session is created — see `admitLinkedTask` below. */ initiativeRuntime?: InitiativeRecordRuntime; /** Injectable agent resolver — tests substitute mock providers; production * uses the config-driven resolveAgent (same pattern as PipelineInput's * runAcceptanceCommand). */ resolveAgentFn?: (tier: AgentType, config: RunnableConfig) => ResolvedAgent; } export type SubmitError = { kind: 'agent_not_configured'; message: string; } | { kind: 'skill_load_failed'; message: string; } | { kind: 'project_reservation'; code: string; message: string; } /** The in-memory registry and durable execution store could not both admit the execution. * In particular, a linked Task has NOT been transitioned when this is returned. */ | { kind: 'execution_admission'; code: 'execution_admission_failed'; message: string; } /** SPEC-003 Task I-6 linked-admission rejection. `code` is one of the three typed Initiative * errors the Contract names: unknown Initiative / malformed membership / absent * authorization all surface as `invalid_request`; a Task outside `open | claimed` surfaces * as `invalid_task_transition`; a claimed Task with a mismatched `authorized_by` surfaces * as `task_claim_conflict`. Every one of these returns before any execution handle, * provider session, Task mutation, or outbox row exists. */ | { kind: 'linked_admission'; code: 'invalid_request' | 'invalid_task_transition' | 'task_claim_conflict'; message: string; } /** SPEC-005 Task I-4: an explicit, syntactically-valid request `method` does not name a * registered Method. Returned AFTER linked-Task validation succeeds (a bad linked Task * wins first) and BEFORE skill loading, prompt construction, pipeline start, * `ExecutionRegistry.register()`, `ExecutionStore.admit()`, or outbox writing — the same * no-durable-write-on-the-wrong-side guarantee `linked_admission` already holds. A * malformed `method` (wrong shape) never reaches here — `taskInputSchema` rejects it as * `invalid_request` at the wire boundary, before `submit()` runs at all. */ | { kind: 'unknown_method'; message: string; }; type SubmitResult = { ok: true; executionId: string; } | { ok: false; error: SubmitError; }; type CancelResult = { outcome: 'not_found'; } | { outcome: 'terminal'; entry: ExecutionEntry; } | { outcome: 'requested'; entry: ExecutionEntry; }; export declare class ExecutionRuntime { private readonly deps; /** Live abort channels, keyed by executionId. An entry exists from admission until * the execution's finally block — cancel() fires the scope's signal, the * provider guards terminate the worker process group, and the terminal CAS * decides between cancelled and a completed/failed that won the race. */ private readonly liveScopes; /** Detach the running-headline producer. Held so tests can subscribe/unsubscribe cleanly. */ private readonly detachHeadlines; constructor(deps: ExecutionRuntimeDeps); /** Release the bus subscription. */ close(): void; /** Best-effort outbox replay after a terminal store write (SPEC-003 Task I-5). Swallows any * throw — `InitiativeLinker.replayOutbox()` already catches per-row failures internally, so * reaching a throw here means something unexpected (e.g. a closed store); an execution's own * terminal result must never be lost or a task destabilized over a downstream Initiative * write. */ private replayLinkage; /** * Linked-admission READ-ONLY validation phase (SPEC-003 B6 round-2 defect A). Resolves the * selected Initiative and — when `linkage.task_uuid` is present — the named Task: checks Task * membership (the Task must belong to the resolved Initiative), checks Task state (`open | * claimed` only — deliberately narrower than the store's own FR-9 transition matrix, which * would separately permit e.g. `blocked -> in_progress`: only `open` and `claimed` are valid * LINKED-ADMISSION starting states), and checks claim ownership for a claimed Task. Issues NO * writes — every call here is a read (`initiative_get` / `initiative_task_get`). * * Runs BEFORE `executionRegistry.register` / `store.admit` in `submit()` below, so every * rejection here (unknown Initiative, unknown/foreign Task, invalid status, claim conflict) * returns before any execution handle, provider session, or outbox row exists (AC-1.9, * AC-1.10). A prior version of this method ran its Task-transition WRITE inline with these * checks, downstream of admission — every rejection was still detected correctly, but by then * `register`/`admit` had already run, so every rejected linked admission durably created a * FAILED execution row nothing ever needed. * * `task_uuid` is OPTIONAL (frozen interface, AC-1.1). When omitted this is Initiative-only * linkage: only the Initiative selector above is validated, and this returns `{ ok: true, task: * null }` — there is no Task to check membership/state/ownership against. */ private validateLinkedTask; /** * Method resolution + guidance load (SPEC-005 Task I-4, AC-1.7/AC-1.8). Runs AFTER * `validateLinkedTask` succeeds (a bad linked Task wins first — see the Errors contract) and * BEFORE skill loading, prompt construction, pipeline start, `ExecutionRegistry.register()`, * `ExecutionStore.admit()`, or outbox writing. Precedence: a non-null explicit request * `method` wins; otherwise the linked Task's stored `method`; otherwise `null`. `null` * resolves trivially — no store lookup, no guidance load, no prompt injection — which is * what keeps a Method-less generic prompt byte-identical to the pre-Method baseline. * * A resolved, non-null identifier must be a REGISTERED Method (`UnknownMethodError` maps to * `unknown_method`) with a committed guidance asset (a resolver failure maps to * `skill_load_failed` — the contract's "fails cleanly ... never falls back to a route * asset"). `taskInputSchema` already rejected a malformed `method` shape at the wire * boundary, so the only failure reachable here for a syntactically valid identifier is * "not registered" or "no committed guidance." */ private resolveMethod; /** * Linked-admission MUTATION phase (SPEC-003 B6 round-2 defect A). Performs the Task's * `open|claimed -> in_progress` transition with `execution_ref: executionId` and system * provenance — the ONE write `validateLinkedTask` above deliberately does not make. Called from * `submit()` only after `executionRegistry.register` / `store.admit` (with linkage already * durably attached to the pending row — SPEC-003 B6 round-2 defect B, Option 1) have both * succeeded, so a failure here fails the already-real execution handle as a unit rather than * leaving anything on the Task side to compensate (the transition itself never landed). * * `task` is the exact value `validateLinkedTask` resolved moments earlier in the same * `submit()` call — re-reading it here would reopen the staleness window * `expected_revision`/claim-ownership checking exists to catch, for a gap this method exists * specifically to keep as small as one durable write. */ private commitLinkedTask; /** * Request cooperative cancellation. 202-semantics: 'requested' means the * abort channel fired, not that the work already stopped — the task stays * `pending` (with cancellationRequestedAt set) until the runner confirms * termination, then transitions to `cancelled` unless completion won the * race. Idempotent; terminal tasks report 'terminal' with their final entry. */ cancel(executionId: string): CancelResult; /** * Synchronous admission: resolve tiers/agents/skills, reserve the project, * register the task, and schedule the async execution. Returns the executionId the * adapter surfaces to the caller; the execution result arrives via polling. */ submit(input: TaskInput, caller: CallerContext): Promise; private execute; } export {}; //# sourceMappingURL=execution-runtime.d.ts.map