import * as ManagerSdk from "@norskvideo/norsk-manager-sdk"; import { ProductTemplateRef, JobId, NodeId, Role } from "@norskvideo/norsk-manager-sdk"; /** @public */ export type BundleId = string; /** @public */ export type ServiceId = string; /** * Compute capacity in vCPUs (fractional). A node's `totalCapacity` is its * vCPU count — cluster workers advertise their cpuCount, cloud machine * types declare their shape's vCPUs (prefilled from the cloud's own * description) — and a job's `requiredCapacity` is the vCPUs it consumes * against the same scale, so cloud and cluster nodes score on one scale. * * The deliberate simplification: a vCPU is a vCPU. Family throughput * differences (a c7g core ≠ an m5 core for encode) are accepted; an * operator derates a shape by declaring fewer vCPUs than the cloud * reports, and a per-machine-type derate factor can follow if that ever * needs to be systematic — without changing the unit. * * @public */ export type Capacity = number; /** * Integer count of cpuset slots (logical CPUs), used both for nodes * (`totalCores`) and for jobs that need explicit cpuset pinning * (`requiredCores`). Distinct from `Capacity`: capacity is the fractional * vCPU budget a job consumes; cores are the integer slots it pins. * * @public */ export type CoreCount = number; /** * Per-device GPU pool. Each physical GPU on a node appears as its own * `GpuResource` with a capacity score. Jobs request fractional capacity * via `requiredGpuCapacity`; for whole-GPU exclusive use, request the * `norsk.io/nvidia-gpu` capability instead (see §3.3). * * @public */ export interface GpuResource { index: number; model?: string; totalCapacity: number; /** * Like the node-level reserved fields, derived by AutoManager from the * jobs it placed on this device (`PlacementResult.gpuIndex`) when the * wire reports nothing. */ reservedCapacity: number; /** True if a whole-GPU exclusive job has taken this device (a claim that * fills it: reservedCapacity >= totalCapacity). */ exclusivelyReserved: boolean; } /** * A capability advertised by a node — physical hardware (NICs, GPUs, * capture cards), installed software (drivers, licenses), or capabilities * provided dynamically by a NodeService. Stringly-typed namespaced names * follow the K8s extended-resource pattern; see `KnownCapabilities` for * the standard names shipped with Norsk. * * @public */ export interface Capability { /** Namespaced name, e.g. "norsk.io/st2110-nic", "acme/license-key". */ name: string; /** Integer count, exclusive in v1. */ count: number; /** Optional metadata used for matching and runtime service-discovery. */ attributes?: Record; } /** * A capability a job requires. Matches against a node's advertised * capabilities by name and (optionally) by attribute equality. * * @public */ export interface CapabilityRequirement { name: string; count: number; /** * Each entry must be present on the matched capability with the given * value. Useful for "GPU model = RTX 4090" or "license tier = enterprise". */ attributeMatches?: Record; } /** * Whether a bundle's FIRST placement may take a hot spare. * * The reserve exists so that work which asked for a spare can recover onto * one immediately, and a launch is normally scheduled with time in hand — so * the default leaves spares alone. The exception is the emergency launch, * where something has to be on air now and taking a warm node is the right * trade. * * - `avoidSpare` (default) — an unconsumed spare is penalised, as at recovery. * - `allowSpare` — the penalty is lifted and the spare competes as an * ordinary candidate. Deliberately NOT a preference: biasing toward spares * would be `prefer`, and calling it that while merely un-penalising is the * sort of misleading name the `OnRuntimeFailure` rename set out to remove. * * @public */ export type OnLaunch = "avoidSpare" | "allowSpare"; /** * What to do with a runtime that has failed, or whose node was lost. * Three different behaviours, not a ranking. * * - `preferSpare` — re-place, biasing scoring toward standby nodes. * - `avoidSpare` — re-place, penalising unconsumed standby nodes so a * routine failure does not consume a reserve held for critical work. * - `drop` — do not re-place; the runtime is terminal. * * @public */ export type OnRuntimeFailure = "preferSpare" | "avoidSpare" | "drop"; /** * Resource and policy requirements for a single job. Bundles of jobs * compose these per-job specs together; placement filters and scoring * use them as inputs. * * @public */ export interface JobRequirements { requiredCapacity: Capacity; /** Omit (or 0) to share whatever cores are available — no cpuset pin. */ requiredCores?: CoreCount; /** GPU reference units. Omit if the job doesn't need a GPU. */ requiredGpuCapacity?: number; /** Optional model match (e.g. "RTX 4090") for the chosen GPU. */ requiredGpuModel?: string; /** Counted/exclusive resources — hardware and software capabilities. */ requiredCapabilities: CapabilityRequirement[]; /** * Fixed host sockets this job binds on whatever node it lands on — * canonical `PORT/proto` tokens (e.g. "1935/tcp", "5001/udp"). A host * socket is exclusive, so placement refuses a node where another job * already claimed one of these. Declared ports only: listener ports * buried in workflow content are invisible here, and the node-side bind * failure (surfaced as a job rejection) remains the authoritative check. */ requiredHostPorts?: string[]; onRuntimeFailure: OnRuntimeFailure; /** * Per-job override of the Manager's restart-rate ceiling: how many times * this runtime may be re-placed within a trailing hour before the Manager * gives up on it. Absent ⇒ the configured default. * * A RATE, not a lifetime count. The check is over a trailing hour, so a * runtime that fails twice a day forever never trips it — the ceiling * exists to stop a hopeless job spinning, not to retire an unlucky one. */ maxRestartsPerHour?: number; /** * Is losing this runtime fatal to its production? Absent ⇒ true. * * A production is a set of cooperating runtimes, so by default the loss of * any one ends it. Marking a runtime non-essential says the production can * carry on without it — a monitor, a recorder — which is otherwise only * expressible by splitting it into its own production, and that is not * available when the runtimes must share a node. * * Governs BOTH routes to "ultimately lost": exhausting the redeploy ceiling, * and an `onRuntimeFailure` of `drop`. */ essential?: boolean; } /** * A bundle is a logical workload unit. It runs a primary copy plus — when * a `resiliencePolicy` is set — one backup (replica index 0 = primary, * 1 = backup). The copy count is derived, not operator-set (see * `replicaCount`). Each copy's jobs are placed under the bundle's * intra-replica rules; the primary is placed against the bundle's single * `pool` (cost cascade lives in the pool's ordered tiers), the backup from * the resilience policy's pool subject to its anti-affinity flags. * * @public */ export interface Bundle { bundleId: BundleId; /** The pool this bundle's primary jobs are placed into. */ pool: string; jobs: BundleJobSpec[]; intraReplicaPlacement?: IntraReplicaRules; /** * When set, the bundle runs a primary + one backup; absent ⇒ active-only * (primary only — AutoManager still restarts a failed primary). */ resiliencePolicy?: ResiliencePolicy; /** * Lifecycle state. `"live"` for newly created bundles; transitions to * `"deleting"` when an operator issues DeleteBundle and the daemon * starts the stop-then-delete saga. The entry leaves the daemon's * BundleStore once all jobs have confirmed stop. Optional on read so * older callers can ignore the field; conversions default to `"live"`. */ state?: BundleState; /** * Whether this bundle's FIRST placement may take a hot spare. Bundle-level * rather than per-job: "get this on air now" is a decision about the * production, not one of its runtimes. Absent ⇒ `"avoidSpare"`. * * Distinct from `JobRequirements.onRuntimeFailure`, which answers the same * question at RECOVERY time — two different moments, two different * controls. See `OnLaunch`. */ onLaunch?: OnLaunch; /** * Phase G — this bundle's placements may take an EXCLUSIVELY reserved hot * spare, one set aside for other templates. The operator's explicit * "yes, take it": Steve's case is two spares provisioned for `xyz` and an * `abc` that has to be on air now. * * Bundle-level for the same reason as `onLaunch` — it is a decision about * the production, not about one of its runtimes. Distinct from * `onLaunch`, which answers whether to take an UNRESERVED spare: a * reservation says who a node is for, `onLaunch` says whether to spend * reserve at all. * * Rides the wire as a reserved per-job tag (`__overrideReservation`), * the same trick `stopDateTime` uses — Manager has no field for it. */ overrideReservation?: boolean; } /** * `failed` = every replica terminally failed (restart ceiling / policy * drop); the derived jobs are kept for inspection and the bundle stays * failed until deleted or re-placed. A bundle with any live replica stays * `live`. * @public */ export type BundleState = "live" | "deleting" | "failed"; /** * When a bundle member's runtime is started. Wire form is the free string * `BundleJobSpec.start_mode`; the daemon reads `"manual"` as manual and * anything else (including the empty string of a bundle that predates the * field) as auto, and echoes `"auto"` / `"manual"`. * * - `auto` — placed when the bundle is created (the default). * - `manual` — the derived job exists but stays dormant until * `startBundleJob` activates it. The daemon marks such a job with * `tags.startMode = "manual"` and drops the tag on activation, which is * why placement keys on the JOB's tag rather than this spec field: the * spec still says `manual` after the job has been activated. * * @public */ export type StartMode = "auto" | "manual"; /** @public */ export interface BundleJobSpec { /** Unique within the bundle. */ jobName: string; requirements: JobRequirements; /** When this member's runtime starts; absent means `auto`. See `StartMode`. */ startMode?: StartMode; /** jobNames in the same replica that MUST land on the same node. */ coLocateWith?: string[]; /** jobNames in the same replica that MUST land on different nodes. */ separateFrom?: string[]; /** * Exclusive node allocation: this job only places onto a node hosting * no other jobs, and no other job ever places onto its node. Same-replica * peers named in `coLocateWith` are exempt from each other — an exclusive * co-located group shares one node that nothing else may touch. */ exclusive?: boolean; /** * Single-use node: when this job (and any co-located siblings) ends, * the node is terminated immediately instead of lingering for reuse — * protection against one event's data leaking onto a later event's * runtime. Requires `exclusive`; only provisioned cloud nodes are * terminated (registered cluster nodes never are). */ singleUse?: boolean; /** * Parameter values that differ on the backup replica. The primary * (replicaIndex 0) gets the launch parameters verbatim; the daemon * merges this map over them (an override wins) when expanding backup * replicas (index >= 1). Ignored when the bundle has no * resiliencePolicy — no backup is expanded. */ backupParameterOverrides?: Record; /** * What this bundle member launches. Either a product-template tar (the new, * preferred path; opaque to Manager, materialised by the worker's * runner) or a legacy services/volumes Job template (existing * pre-product-template flow). * * Other per-replica differences flow through `Job.tags` projected into * the product-template's `.env` by the worker; parameter values that * differ per replica use `backupParameterOverrides` above. See * `.ai/steve/tasks/39-blueprint-jobs.md`. */ launch?: LaunchSpec; } /** @public */ export type LaunchSpec = { kind: "productTemplate"; ref: ProductTemplateRef; /** Resolved env-var overrides for the compose stack. Same for every * replica unless the spec's `backupParameterOverrides` overrides an * entry for the backup. */ parameters?: Record; /** * When this job should start. Required by Manager-daemon for every * stored job; if omitted, AutoManager stamps `new Date()` (immediate) * at conversion time. Surfaced explicitly so launch UIs can offer a * "now / pick a future time" picker without needing a separate field. */ startDateTime?: Date; /** * Optional auto-stop. When set and in the future, AutoManager * schedules a per-bundle timer that fires `deleteBundle(bundleId)` * at this instant. There's no daemon-side concept of stop time — * this is wire-encoded as a reserved `__stopDateTime` tag on the * proto Job (in `tags`), so it persists across reconnects via * the daemon's tag store. Use `setBundleStopTime` to change after * launch; the timer reschedules. */ stopDateTime?: Date; /** * Free-form tags forwarded to PbJob.tags. Daemon preserves them * verbatim; worker reads via cluster events. Names starting with * `__` are reserved for SDK use (e.g. `__stopDateTime`) and are * stripped on both inbound and outbound conversion — callers * who try to set them get a quiet no-op. */ tags?: Record; /** * Operator launch config for the product-template compose stack, as * the JSON encoding of norsk-ctl's ProductTemplateLaunchConfig. * Opaque to Manager; the worker validates and fuses it (the single * merge implementation) with the template tier below. */ launchConfigJson?: string; /** Template-author custom launch defaults — the fusion tier between * the manifest's defaults and operator intent. */ templateLaunchDefaultsJson?: string; /** * Extra security groups every derived job needs on its node — a * node's SG set is base ∪ extras(bound jobs); the Manager reconciles * membership on bind/unbind (worker-security-groups.md). */ extraSecurityGroups?: string[]; } | { kind: "legacy"; /** Job template Manager stamps per replica (legacy services/volumes path). */ job: ManagerSdk.Job; }; /** @public */ export interface IntraReplicaRules { /** * When true, every job of a replica that declares neither `coLocateWith` * nor `separateFrom` is co-located with every other such job — as a hard * rule, exactly as an explicit `coLocateWith` among them would be (the * group's first mover also sizes its node for the whole group). A job * with an explicit rule is outside the default group. False (the wire * default) ⇒ no implicit co-location. */ sameNodeDefault: boolean; sameAz: "hard" | "soft" | "off"; } /** * Anti-affinity strength for a resilience-policy flag. v1 is best-effort * always: `"forbid"` means "keep the backup out of this failure domain if * at all possible" — but if no domain-respecting node is free the engine * places it anyway and surfaces the degradation. `"allow"` = no constraint. * * @public */ export type AntiAffinity = "allow" | "forbid"; /** * When present on a bundle, the bundle runs a primary + one backup. The * backup is placed from `pool` (omitted ⇒ the bundle's own pool) subject to * the anti-affinity flags, which keep it in a different failure domain * (node / AZ / cloud) from the primary. * * @public */ export interface ResiliencePolicy { /** Backup capacity pool. Omitted ⇒ use the bundle's own `pool`. */ pool?: string; sameNode: AntiAffinity; sameAz: AntiAffinity; sameCloud: AntiAffinity; } /** * Derived copy count: primary (1) plus one backup when a resilience policy * is present. Replaces the old operator-set `replicas` field. * * @public */ export declare function replicaCount(bundle: Bundle): number; /** * What a node advertises through `nodeInventoryUpdated` events. Workers * are the source of truth — placement reads, never writes, this view. * * @public */ export interface NodeInventory { totalCapacity: Capacity; /** * Fraction (0, 1] of totalCapacity that's safe to allocate — the wire * contract, enforced at the boundary (`fromPbNodeInventory`): a producer * that sends anything else (an absolute, 0, negative, NaN) is read as 1, * so a node can never advertise more than its declared capacity. */ utilizationCap: number; totalCores: CoreCount; gpus: GpuResource[]; capabilities: Capability[]; reservedCapacity: Capacity; reservedCores: CoreCount; reservedCapabilities: Capability[]; /** * Host sockets (canonical `PORT/proto` tokens, e.g. "5001/udp") already * claimed by jobs on this node. Like the other reserved fields this is * derived by AutoManager from what it placed when the wire doesn't supply * it. Optional: absent means "none known", which older producers imply. */ reservedHostPorts?: string[]; reachable: boolean; lastSeen: Date; /** Set true by an operator drain; placement skips this node. */ cordoned: boolean; assignedJobs: NodeAssignedJob[]; /** * Phase G — the container images this node's engine holds, as the worker * advertises them. This is where WARMTH is observed: placement scores a * candidate by how much of the job's template image set is already here. * Absent/empty means "nothing advertised", which scores cold. */ heldImages?: HeldNodeImage[]; } /** * Phase G — one image a node holds. `imageRef` is the canonical * `repository@sha256:` identity; `tags` are the `repository:tag` * names it currently answers to on that node. * * @public */ export interface HeldNodeImage { imageRef: string; tags: string[]; } /** @public */ export interface NodeAssignedJob { jobId: JobId; /** CPU core indices, if pinned. */ coreSet?: number[]; /** Index of the GPU bound to this job, if any. */ gpuIndex?: number; } /** * A long-running job that consumes capabilities and provides new ones. * * The motivating example is an MXL bridge that takes exclusive * possession of an ST2110 NIC (a `consumes` capability) and * advertises N consumer slots (a `provides` capability) for other * jobs on the same node to use. * * AutoManager's NodeService reconciler places services on every * eligible node (`lifecycle: "eager"`, the only lifecycle). A non-empty * `nodeServices` needs `experimental.nodeServices: true` — see * `AutoSettings.experimental`. * * The actual capability advertisement is worker-side work * (`provides[]` is added to the node's inventory by the worker once * the service reaches `ready`). Without that, AutoManager places * the service but no consumer can see its capabilities. See * §"Worker-side work" in the design doc. * * @public */ export interface NodeService { serviceId: ServiceId; /** * SDK-shape runtime Job template. AutoManager stamps in the * generated jobId and adds `tags["nodeService"] = serviceId` and * `tags["auto-manager"] = "true"`; everything else flows through * verbatim. The worker recognises NodeService instances by the * `nodeService` tag. */ jobSpec: ManagerSdk.Job; /** Capabilities the service requires from the host node. */ consumes: CapabilityRequirement[]; /** Capabilities the service advertises while running and ready. */ provides: Capability[]; /** Pool names the reconciler considers when placing this service. */ pools: string[]; /** * `eager`: place on every node in `pools` that satisfies `consumes`. * The only lifecycle. (`lazy` — place only when an unplaceable consumer * would benefit — was declared here for years and never implemented; * settings validation still refuses it by name so a yaml that carries * it gets a sentence rather than a type error.) */ lifecycle: "eager"; } export type { JobId, NodeId, Role }; //# sourceMappingURL=types.d.ts.map