fix(models): synchronize canonical runtime selection
This commit is contained in:
@@ -533,9 +533,15 @@ class AgentLoop:
|
||||
self.subagents.max_iterations = self.max_iterations
|
||||
|
||||
def invalidate_runtime_config(self) -> None:
|
||||
"""Invalidate runtime config and notify clients to refresh its catalog."""
|
||||
"""Invalidate runtime config for lazy refresh at the next admission."""
|
||||
self.runtime_resolver.invalidate()
|
||||
self._publish_runtime_selection(self.runtime_resolver.runtime)
|
||||
|
||||
def refresh_runtime_config(self) -> LLMRuntime:
|
||||
"""Refresh runtime config now and publish the canonical selection."""
|
||||
self.runtime_resolver.invalidate()
|
||||
runtime = self.runtime_resolver.admit()
|
||||
self._publish_runtime_selection(runtime)
|
||||
return runtime
|
||||
|
||||
def runtime_for_session(
|
||||
self,
|
||||
@@ -1787,7 +1793,7 @@ class AgentLoop:
|
||||
session.provider_state = None
|
||||
self.sessions.save(session)
|
||||
ctx.input_persisted_early = True
|
||||
ctx.delivery.record_runtime(runtime)
|
||||
await ctx.delivery.runtime_admitted(runtime)
|
||||
|
||||
ctx.request_context = self._request_context_for_turn(ctx)
|
||||
if ctx.kind is TurnKind.USER:
|
||||
|
||||
@@ -189,7 +189,15 @@ class TurnDelivery:
|
||||
started_at=started_at,
|
||||
)
|
||||
|
||||
def record_runtime(self, runtime: LLMRuntime) -> None:
|
||||
async def runtime_admitted(self, runtime: LLMRuntime) -> None:
|
||||
"""Record the immutable runtime and expose it at the lifecycle seam."""
|
||||
if self.route.publish_lifecycle:
|
||||
await self.runtime_event_publisher.turn_runtime_admitted(
|
||||
self.delivery_message,
|
||||
self.session_key,
|
||||
runtime,
|
||||
)
|
||||
return
|
||||
self.runtime_event_publisher.record_turn_runtime(self.session_key, runtime)
|
||||
|
||||
def record_latency(self, latency_ms: int | None) -> None:
|
||||
|
||||
@@ -84,9 +84,10 @@ class RuntimeModelUpdatedEvent(OutboundEvent):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TurnModelUpdatedEvent(OutboundEvent):
|
||||
"""The fallback model currently handling one chat turn."""
|
||||
"""The canonical preset and concrete model handling one chat turn."""
|
||||
|
||||
model: str
|
||||
model_preset: str | None = None
|
||||
|
||||
|
||||
def outbound_message_for_event(
|
||||
|
||||
@@ -40,6 +40,14 @@ class SessionTurnStarted:
|
||||
context: RuntimeEventContext
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TurnRuntimeAdmitted:
|
||||
"""The immutable model runtime selected for one admitted turn."""
|
||||
|
||||
context: RuntimeEventContext
|
||||
runtime: LLMRuntime
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TurnRunStatusChanged:
|
||||
"""Visible run status changed for a turn."""
|
||||
@@ -85,6 +93,7 @@ class RuntimeModelChanged:
|
||||
|
||||
RuntimeEvent = (
|
||||
SessionTurnStarted
|
||||
| TurnRuntimeAdmitted
|
||||
| SessionTurnPersisted
|
||||
| TurnRunStatusChanged
|
||||
| TurnCompleted
|
||||
@@ -93,6 +102,7 @@ RuntimeEvent = (
|
||||
)
|
||||
RuntimeEventType = (
|
||||
type[SessionTurnStarted]
|
||||
| type[TurnRuntimeAdmitted]
|
||||
| type[SessionTurnPersisted]
|
||||
| type[TurnRunStatusChanged]
|
||||
| type[TurnCompleted]
|
||||
@@ -204,6 +214,26 @@ class RuntimeEventPublisher:
|
||||
)
|
||||
)
|
||||
|
||||
async def turn_runtime_admitted(
|
||||
self,
|
||||
msg: InboundMessage,
|
||||
session_key: str,
|
||||
runtime: LLMRuntime,
|
||||
) -> None:
|
||||
"""Record and publish the runtime selected for one turn."""
|
||||
self.record_turn_runtime(session_key, runtime)
|
||||
await self.bus.publish(
|
||||
TurnRuntimeAdmitted(
|
||||
context=self._context(
|
||||
channel=msg.channel,
|
||||
chat_id=msg.chat_id,
|
||||
session_key=session_key,
|
||||
metadata=msg.metadata,
|
||||
),
|
||||
runtime=runtime,
|
||||
)
|
||||
)
|
||||
|
||||
async def run_status_changed(
|
||||
self,
|
||||
msg: InboundMessage,
|
||||
|
||||
@@ -1407,6 +1407,7 @@ class WebSocketChannel(BaseChannel):
|
||||
await self.send_turn_model_updated(
|
||||
msg.chat_id,
|
||||
model_name=event.model,
|
||||
model_preset=event.model_preset,
|
||||
)
|
||||
return
|
||||
if isinstance(event, GoalStateSyncEvent):
|
||||
@@ -1774,6 +1775,7 @@ class WebSocketChannel(BaseChannel):
|
||||
chat_id: str,
|
||||
*,
|
||||
model_name: Any,
|
||||
model_preset: Any = None,
|
||||
) -> None:
|
||||
"""Notify one chat's subscribers which model is handling its current request."""
|
||||
conns = list(self._subs.get(chat_id, ()))
|
||||
@@ -1788,6 +1790,8 @@ class WebSocketChannel(BaseChannel):
|
||||
"chat_id": chat_id,
|
||||
"model_name": model_name.strip(),
|
||||
}
|
||||
if isinstance(model_preset, str) and model_preset.strip():
|
||||
body["model_preset"] = model_preset.strip()
|
||||
raw = json.dumps(body, ensure_ascii=False)
|
||||
for connection in conns:
|
||||
await self._safe_send_to(connection, raw, label=" turn_model_updated ")
|
||||
|
||||
@@ -1642,7 +1642,10 @@ async def test_send_scopes_turn_model_updates_to_the_subscribed_chat() -> None:
|
||||
channel="websocket",
|
||||
chat_id="chat-1",
|
||||
content="",
|
||||
event=TurnModelUpdatedEvent(model="deepseek/deepseek-chat"),
|
||||
event=TurnModelUpdatedEvent(
|
||||
model="deepseek/deepseek-chat",
|
||||
model_preset="Deep Research",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1651,6 +1654,7 @@ async def test_send_scopes_turn_model_updates_to_the_subscribed_chat() -> None:
|
||||
"event": "turn_model_updated",
|
||||
"chat_id": "chat-1",
|
||||
"model_name": "deepseek/deepseek-chat",
|
||||
"model_preset": "Deep Research",
|
||||
}
|
||||
chat_two.send.assert_not_awaited()
|
||||
|
||||
|
||||
@@ -658,7 +658,7 @@ def _run_gateway(
|
||||
return agent.model.strip() or None
|
||||
|
||||
def _webui_refresh_runtime_config() -> None:
|
||||
agent.invalidate_runtime_config()
|
||||
agent.refresh_runtime_config()
|
||||
|
||||
def _webui_skill_state_action(disabled_skills: set[str]) -> None:
|
||||
config.agents.defaults.disabled_skills = sorted(disabled_skills)
|
||||
|
||||
@@ -33,6 +33,7 @@ from nanobot.bus.runtime_events import (
|
||||
SessionTurnStarted,
|
||||
TurnCompleted,
|
||||
TurnRunStatusChanged,
|
||||
TurnRuntimeAdmitted,
|
||||
)
|
||||
from nanobot.providers.base import LLMProvider
|
||||
from nanobot.providers.fallback_provider import FallbackModelObserver
|
||||
@@ -459,7 +460,14 @@ def build_webui_fallback_model_observer(bus: MessageBus) -> FallbackModelObserve
|
||||
outbound_message_for_event(
|
||||
channel=context.channel,
|
||||
chat_id=chat_id,
|
||||
event=TurnModelUpdatedEvent(model=model),
|
||||
event=TurnModelUpdatedEvent(
|
||||
model=model,
|
||||
model_preset=(
|
||||
context.runtime.model_preset
|
||||
if context.runtime is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
metadata=context.metadata,
|
||||
)
|
||||
)
|
||||
@@ -486,6 +494,10 @@ class WebuiTurnCoordinator:
|
||||
self._handle_run_status_changed,
|
||||
TurnRunStatusChanged,
|
||||
),
|
||||
runtime_events.subscribe(
|
||||
self._handle_turn_runtime_admitted,
|
||||
TurnRuntimeAdmitted,
|
||||
),
|
||||
runtime_events.subscribe(
|
||||
self._handle_turn_completed_event,
|
||||
TurnCompleted,
|
||||
@@ -537,6 +549,21 @@ class WebuiTurnCoordinator:
|
||||
started_at=event.started_at,
|
||||
)
|
||||
|
||||
async def _handle_turn_runtime_admitted(self, event: TurnRuntimeAdmitted) -> None:
|
||||
if not self._is_websocket_event(event.context):
|
||||
return
|
||||
await self.bus.publish_outbound(
|
||||
outbound_message_for_event(
|
||||
channel=event.context.channel,
|
||||
chat_id=event.context.chat_id,
|
||||
event=TurnModelUpdatedEvent(
|
||||
model=event.runtime.model,
|
||||
model_preset=event.runtime.model_preset,
|
||||
),
|
||||
metadata=event.context.metadata,
|
||||
)
|
||||
)
|
||||
|
||||
async def _handle_turn_completed_event(self, event: TurnCompleted) -> None:
|
||||
if not self._is_websocket_event(event.context):
|
||||
return
|
||||
|
||||
@@ -1646,6 +1646,11 @@ class ModelSettingsHandler:
|
||||
self.settings = settings
|
||||
self.logger = logger
|
||||
|
||||
def _refresh_runtime_config(self) -> None:
|
||||
"""Make a successful model-settings mutation visible to live clients now."""
|
||||
if self.settings.refresh_runtime_config is not None:
|
||||
self.settings.refresh_runtime_config()
|
||||
|
||||
async def handle(
|
||||
self,
|
||||
action: str,
|
||||
@@ -1655,6 +1660,7 @@ class ModelSettingsHandler:
|
||||
try:
|
||||
if action == "agent-update":
|
||||
payload = self.settings.mutate(operations.update_agent, request.query)
|
||||
self._refresh_runtime_config()
|
||||
return SettingsRouteResult.success(
|
||||
payload,
|
||||
decorate_restart=True,
|
||||
@@ -1667,8 +1673,7 @@ class ModelSettingsHandler:
|
||||
request.query,
|
||||
rename_model_preset=self.settings.rename_model_preset,
|
||||
)
|
||||
if self.settings.refresh_runtime_config is not None:
|
||||
self.settings.refresh_runtime_config()
|
||||
self._refresh_runtime_config()
|
||||
return SettingsRouteResult.success(payload, decorate_restart=True)
|
||||
|
||||
mutation = {
|
||||
@@ -1680,6 +1685,7 @@ class ModelSettingsHandler:
|
||||
}.get(action)
|
||||
if mutation is not None:
|
||||
payload = self.settings.mutate(mutation, request.query)
|
||||
self._refresh_runtime_config()
|
||||
return SettingsRouteResult.success(payload, decorate_restart=True)
|
||||
|
||||
if action == "provider-update":
|
||||
@@ -1690,6 +1696,7 @@ class ModelSettingsHandler:
|
||||
payload, image_restart_cleared = await operations.apply_image_runtime_change(
|
||||
payload
|
||||
)
|
||||
self._refresh_runtime_config()
|
||||
return SettingsRouteResult.success(
|
||||
payload,
|
||||
decorate_restart=True,
|
||||
|
||||
Reference in New Issue
Block a user