Coverage for src/lilbee/providers/fleet/contract.py: 100%
35 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Decides whether a running engine can serve a lilbee's configuration.
3The contract is the per-role models, the engine build pin and the settings each
4launch bakes in. Planner-derived values (ctx, slots) are accepted from the running
5engine, never recomputed: they vary legitimately with GPU occupancy at plan time.
6The one exception is ``chat_ctx_covers``: a live chat window smaller than what this process needs
7cannot be adopted, since no prompt fit can grow it. ``vision_slots_cover``
8applies the same rule to the vision role's slot ceiling.
9"""
11from __future__ import annotations
13from types import MappingProxyType
14from typing import TYPE_CHECKING
16from lilbee.providers.fleet.launch import InstanceLaunch
17from lilbee.providers.roles import WorkerRole
19if TYPE_CHECKING:
20 from collections.abc import Iterable, Mapping
22 from lilbee.providers.fleet.swap_manager import SwapState
24NO_SETTINGS: Mapping[WorkerRole, str] = MappingProxyType({})
27def decoded_launches(state: SwapState) -> list[InstanceLaunch] | None:
28 """The engine's recorded launches, or ``None`` when the contract is undecodable.
30 The single decode site. Callers previously re-decoded launches bare, relying on
31 a preceding contract_matches call to have proven decodability, so reordering or
32 dropping that guard turned a non-match into an unhandled exception in the bind
33 ladder. Returning ``None`` makes "undecodable" a value every caller must handle.
34 """
35 try:
36 return [InstanceLaunch.from_state(item) for item in state.launches]
37 except (KeyError, TypeError, ValueError):
38 return None
41def chat_ctx_covers(launches: Iterable[InstanceLaunch], demanded_ctx: int) -> bool:
42 """Whether the engine's per-slot chat window serves *demanded_ctx* tokens.
44 ``launches`` are the running engine's recorded launches. A zero demand, no
45 chat launch, or a record without a positive chat ctx never refuses: derived
46 values are adopted from the running engine. A window below the demand still
47 covers when the record's ``built_ctx_target`` reaches the demand: the same
48 planner aimed at least as high and achieved this window, so replacing the
49 engine would rebuild the same window in a loop. A refusal sends the ladder
50 to its replace-or-overflow decision.
51 """
52 chat = [launch for launch in launches if launch.role is WorkerRole.CHAT and launch.ctx > 0]
53 live = min((launch.ctx for launch in chat), default=0)
54 if demanded_ctx <= 0 or live <= 0 or demanded_ctx <= live:
55 return True
56 built_target = min((launch.built_ctx_target for launch in chat), default=0)
57 return built_target > 0 and demanded_ctx <= built_target
60def vision_slots_cover(launches: Iterable[InstanceLaunch], demanded_slots: int) -> bool:
61 """Whether the engine's vision slot ceiling serves *demanded_slots* at once.
63 A fitted count below the demand still covers when the builder's
64 ``built_slots_target`` reached it: the same ceiling produced this fit, so a
65 replacement would fit the same count in a loop.
66 """
67 vision = [
68 launch for launch in launches if launch.role is WorkerRole.VISION and launch.slots > 0
69 ]
70 live = min((launch.slots for launch in vision), default=0)
71 if demanded_slots <= 0 or live <= 0 or demanded_slots <= live:
72 return True
73 built_target = min((launch.built_slots_target for launch in vision), default=0)
74 return built_target > 0 and demanded_slots <= built_target
77def contract_matches(
78 state: SwapState,
79 wanted: Iterable[tuple[WorkerRole, str]],
80 pin: str,
81 settings: Mapping[WorkerRole, str],
82) -> bool:
83 """Whether the engine behind *state* serves every wanted (role, model) pair.
85 The engine may serve more roles than asked; the engine build pin must
86 equal *pin*, and an empty or undecodable served contract never matches.
87 *settings* holds what a plan here gives each role: a launch with other
88 settings never matches, and a role absent from it never refuses.
89 """
90 if state.engine_pin != pin:
91 return False
92 launches = decoded_launches(state)
93 if not launches:
94 return False
95 if any(settings.get(launch.role, launch.settings) != launch.settings for launch in launches):
96 return False
97 served = {(launch.role, launch.model) for launch in launches}
98 return all(pair in served for pair in wanted)