fix(plugin): persist v4 runtime state and tolerate runner registration gaps (#2774)

Persist the v4 plugin runtime state so recreating the runtime container no
longer discards installed plugins' artifacts, dependency environments and
installation-private data, and stop a transient Runner registry miss during
steering from dropping the message.

- docker-compose: bind-mount ./data/plugin-runtime for the plugin runtime
  alongside ./data/plugins.
- kubernetes: add the langbot-plugin-runtime-state claim (5Gi) mounted at
  /app/data/plugin-runtime, kept separate from the plugin data claim so
  runtime state does not land inside the plugin directory.
- chat handler: treat RunnerNotFoundError while claiming steering as
  "steering unavailable" and continue with a normal run; every other error
  keeps propagating. The runner error imports move to module scope, which
  the new early reference requires.
- tests: cover the steering fallback, the claimed-steering interrupt, and
  the lasting-gap path that still reports runner-not-found.

The steering fallback buys one extra uncached registry lookup while a
runtime reconnects; it does not recover a gap that outlasts the message.
This commit is contained in:
Martin
2026-10-27 20:31:49 +08:00
committed by GitHub
parent 6990607c5c
commit 12debe2747
4 changed files with 220 additions and 10 deletions
@@ -15,6 +15,11 @@ from ... import plugin_diagnostics
import langbot_plugin.api.entities.events as events
from ....agent.runner.config_resolver import RunnerConfigResolver
from ....agent.runner import config_schema
from ....agent.runner.errors import (
RunnerExecutionError,
RunnerNotFoundError,
RunnerNotAuthorizedError,
)
from ....utils import runner as runner_utils
import langbot_plugin.api.entities.builtin.provider.session as provider_session
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
@@ -119,9 +124,24 @@ class ChatMessageHandler(handler.MessageHandler):
'try_claim_steering_from_query',
None,
)
if try_claim_steering and await try_claim_steering(query):
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
return
if try_claim_steering:
try:
claimed = await try_claim_steering(query)
except RunnerNotFoundError as steering_error:
# Steering is an optimization for an already-running
# agent. A runtime reconnect can temporarily remove the
# selected Runner from the registry; that must not
# prevent the message from starting a normal run.
self.ap.logger.warning(
'Skipping steering for query %s because Runner %s is not registered',
query.query_id,
steering_error.runner_id,
)
claimed = False
if claimed:
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
return
try:
is_stream = await query.adapter.is_stream_output_supported()
@@ -228,13 +248,6 @@ class ChatMessageHandler(handler.MessageHandler):
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
except Exception as e:
# Import orchestrator errors for specific handling
from ....agent.runner.errors import (
RunnerNotFoundError,
RunnerNotAuthorizedError,
RunnerExecutionError,
)
error_info = f'{traceback.format_exc()}'
self.ap.logger.error(f'Conversation({query.query_id}) Request Failed: {error_info}')