feat(runner): unify plugin execution across agents and event processors

This commit is contained in:
RockChinQ
2026-09-10 18:04:38 +08:00
parent 8903a40c41
commit f24a7c9bb2
223 changed files with 4091 additions and 3068 deletions
+8 -8
View File
@@ -2,35 +2,35 @@
from __future__ import annotations
from .runner.descriptor import AgentRunnerDescriptor
from .runner.descriptor import RunnerDescriptor
from .runner.id import parse_runner_id, format_runner_id, RunnerIdParts, is_plugin_runner_id
from .runner.errors import (
AgentRunnerError,
RunnerError,
RunnerNotFoundError,
RunnerNotAuthorizedError,
RunnerProtocolError,
RunnerExecutionError,
)
from .runner.registry import AgentRunnerRegistry
from .runner.context_builder import AgentRunContextBuilder
from .runner.registry import RunnerRegistry
from .runner.context_builder import RunnerContextBuilder
from .runner.resource_builder import AgentResourceBuilder
from .runner.result_normalizer import AgentResultNormalizer
from .runner.orchestrator import AgentRunOrchestrator
from .runner.config_resolver import RunnerConfigResolver
__all__ = [
'AgentRunnerDescriptor',
'RunnerDescriptor',
'parse_runner_id',
'format_runner_id',
'is_plugin_runner_id',
'RunnerIdParts',
'AgentRunnerError',
'RunnerError',
'RunnerNotFoundError',
'RunnerNotAuthorizedError',
'RunnerProtocolError',
'RunnerExecutionError',
'AgentRunnerRegistry',
'AgentRunContextBuilder',
'RunnerRegistry',
'RunnerContextBuilder',
'AgentResourceBuilder',
'AgentResultNormalizer',
'AgentRunOrchestrator',
+10 -10
View File
@@ -2,22 +2,22 @@
from __future__ import annotations
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .id import parse_runner_id, format_runner_id, RunnerIdParts
from .errors import (
AgentRunnerError,
RunnerError,
RunnerNotFoundError,
RunnerNotAuthorizedError,
RunnerProtocolError,
RunnerExecutionError,
)
from .registry import AgentRunnerRegistry
from .context_builder import AgentRunContextBuilder
from .registry import RunnerRegistry
from .context_builder import RunnerContextBuilder
from .resource_builder import AgentResourceBuilder
from .result_normalizer import AgentResultNormalizer
from .orchestrator import AgentRunOrchestrator
from .config_resolver import RunnerConfigResolver
from .default_config import AgentRunnerDefaultConfigService
from .default_config import RunnerDefaultConfigService
from .binding_resolver import AgentBindingResolver, AgentBindingResolutionError
from .session_registry import (
AgentRunSessionRegistry,
@@ -35,22 +35,22 @@ from .events import (
)
__all__ = [
'AgentRunnerDescriptor',
'RunnerDescriptor',
'parse_runner_id',
'format_runner_id',
'RunnerIdParts',
'AgentRunnerError',
'RunnerError',
'RunnerNotFoundError',
'RunnerNotAuthorizedError',
'RunnerProtocolError',
'RunnerExecutionError',
'AgentRunnerRegistry',
'AgentRunContextBuilder',
'RunnerRegistry',
'RunnerContextBuilder',
'AgentResourceBuilder',
'AgentResultNormalizer',
'AgentRunOrchestrator',
'RunnerConfigResolver',
'AgentRunnerDefaultConfigService',
'RunnerDefaultConfigService',
'AgentBindingResolver',
'AgentBindingResolutionError',
'AgentRunSessionRegistry',
@@ -1,4 +1,4 @@
"""Resolve the current AgentRunner configuration shape."""
"""Resolve the current Runner configuration shape."""
from __future__ import annotations
@@ -12,7 +12,7 @@ HOST_SECURITY_BOOLEAN_FIELDS = (
class RunnerConfigResolver:
"""Configuration helpers for the current AgentRunner shape.
"""Configuration helpers for the current Runner shape.
Responsibilities:
- Resolve runner ID from ai.runner.id
@@ -131,20 +131,20 @@ class RunnerConfigResolver:
return config
@staticmethod
def resolve_agent_runner_id(config: dict[str, typing.Any]) -> str | None:
def resolve_agent_id(config: dict[str, typing.Any]) -> str | None:
"""Resolve a runner ID from a validated persisted Agent config."""
runner = config.get('runner', {})
runner_id = runner.get('id') if isinstance(runner, dict) else None
return runner_id if isinstance(runner_id, str) and runner_id else None
@classmethod
def resolve_agent_runner_config(
def resolve_agent_config(
cls,
config: typing.Any,
) -> tuple[dict[str, typing.Any], str | None, dict[str, typing.Any]]:
"""Validate an Agent config and return its selected runner configuration."""
validated = cls.validate_agent_config(config)
runner_id = cls.resolve_agent_runner_id(validated)
runner_id = cls.resolve_agent_id(validated)
runner_configs = typing.cast(dict[str, typing.Any], validated['runner_config'])
runner_config = runner_configs.get(runner_id, {}) if runner_id else {}
return validated, runner_id, typing.cast(dict[str, typing.Any], runner_config)
+14 -15
View File
@@ -1,9 +1,10 @@
"""Helpers for interpreting AgentRunner DynamicForm configuration."""
"""Helpers for interpreting Runner DynamicForm configuration."""
from __future__ import annotations
import typing
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
FORM_ITEM_TYPE_ALIASES = {
@@ -24,7 +25,7 @@ def normalize_schema_item_type(item_type: typing.Any) -> typing.Any:
def iter_schema_items(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
field_types: set[str],
) -> typing.Iterator[dict[str, typing.Any]]:
"""Yield descriptor config schema items whose type is in field_types."""
@@ -37,22 +38,22 @@ def iter_schema_items(
yield item
def uses_host_models(descriptor: AgentRunnerDescriptor | None) -> bool:
def uses_host_models(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether LangBot should resolve model resources for this runner."""
return any(True for _ in iter_schema_items(descriptor, LLM_MODEL_SELECTOR_TYPES))
def uses_host_tools(descriptor: AgentRunnerDescriptor | None) -> bool:
def uses_host_tools(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether LangBot should expose tool resources to this runner."""
return descriptor is not None and descriptor.supports_tool_calling()
def uses_host_knowledge_bases(descriptor: AgentRunnerDescriptor | None) -> bool:
def uses_host_knowledge_bases(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether LangBot should expose knowledge-base resources to this runner."""
return descriptor is not None and descriptor.supports_knowledge_retrieval()
def supports_skill_authoring(descriptor: AgentRunnerDescriptor | None) -> bool:
def supports_skill_authoring(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether the runner wants Host skill-authoring tools."""
if descriptor is None:
return False
@@ -60,7 +61,7 @@ def supports_skill_authoring(descriptor: AgentRunnerDescriptor | None) -> bool:
def extract_prompt_config(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
runner_config: dict[str, typing.Any],
default_prompt: list[dict[str, typing.Any]],
) -> list[dict[str, typing.Any]]:
@@ -78,7 +79,7 @@ def extract_prompt_config(
def extract_model_selection(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
runner_config: dict[str, typing.Any],
) -> tuple[str, list[str]]:
"""Extract primary/fallback LLM selections from schema-defined fields."""
@@ -110,7 +111,7 @@ def extract_model_selection(
def extract_knowledge_base_uuids(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
runner_config: dict[str, typing.Any],
) -> list[str]:
"""Extract configured knowledge-base UUIDs from schema-defined fields."""
@@ -124,15 +125,13 @@ def extract_knowledge_base_uuids(
continue
value = runner_config.get(field_name, item.get('default', []))
if isinstance(value, list):
kb_uuids.extend(
kb_uuid for kb_uuid in value if isinstance(kb_uuid, str) and kb_uuid not in NONE_SENTINELS
)
kb_uuids.extend(kb_uuid for kb_uuid in value if isinstance(kb_uuid, str) and kb_uuid not in NONE_SENTINELS)
return list(dict.fromkeys(kb_uuids))
def iter_config_model_refs(
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> typing.Iterator[tuple[str, str]]:
"""Yield model references declared by schema-defined model selector fields."""
@@ -167,7 +166,7 @@ def iter_config_model_refs(
def set_empty_llm_model_selection(
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
model_uuid: str,
) -> bool:
+13 -13
View File
@@ -1,4 +1,4 @@
"""Agent run context builder for provisioning AgentRunContext envelopes."""
"""Agent run context builder for provisioning RunnerContext envelopes."""
from __future__ import annotations
@@ -7,7 +7,7 @@ import time
import typing
from ...core import app
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .persistent_state_store import get_persistent_state_store
from .host_models import AgentEventEnvelope, AgentBinding
@@ -125,10 +125,10 @@ class AgentRuntimeContext(typing.TypedDict):
metadata: dict[str, typing.Any]
class AgentRunContextPayload(typing.TypedDict):
"""AgentRunContext payload passed to an agent runner.
class RunnerContextPayload(typing.TypedDict):
"""RunnerContext payload passed to an agent runner.
Protocol v1 structure - matches SDK AgentRunContext.
Protocol v1 structure - matches SDK RunnerContext.
Note: The 'config' field contains the current Agent/runner config
from ai.runner_config[runner_id] while the current Query entry remains
@@ -152,8 +152,8 @@ class AgentRunContextPayload(typing.TypedDict):
metadata: dict[str, typing.Any] # Additional metadata
class AgentRunContextBuilder:
"""Builder for provisioning AgentRunContext.
class RunnerContextBuilder:
"""Builder for provisioning RunnerContext.
Responsibilities:
- Generate new run_id (UUID, not query id)
@@ -222,10 +222,10 @@ class AgentRunContextBuilder:
self,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
resources: AgentResources,
) -> AgentRunContextPayload:
"""Build AgentRunContext from event-first envelope.
) -> RunnerContextPayload:
"""Build RunnerContext from event-first envelope.
This is the main entry point for Protocol v1.
Does NOT inline full history by default.
@@ -237,7 +237,7 @@ class AgentRunContextBuilder:
resources: Built resources
Returns:
AgentRunContextPayload for the runner
RunnerContextPayload for the runner
"""
# Generate new run_id
run_id = str(uuid.uuid4())
@@ -351,7 +351,7 @@ class AgentRunContextBuilder:
}
# Build full context - Protocol v1 structure
context: AgentRunContextPayload = {
context: RunnerContextPayload = {
'run_id': run_id,
'trigger': trigger,
'conversation': conversation,
@@ -397,7 +397,7 @@ class AgentRunContextBuilder:
async def _build_context_access(
self,
event: AgentEventEnvelope,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
binding: AgentBinding | None = None,
) -> dict[str, typing.Any]:
"""Build ContextAccess with actual values from stores.
@@ -1,4 +1,4 @@
"""Default AgentRunner binding configuration helpers."""
"""Default Runner binding configuration helpers."""
from __future__ import annotations
@@ -11,8 +11,8 @@ from . import config_schema
from .config_resolver import RunnerConfigResolver
class AgentRunnerDefaultConfigService:
"""Apply AgentRunner schema-defined defaults to host binding config."""
class RunnerDefaultConfigService:
"""Apply Runner schema-defined defaults to host binding config."""
ap: app.Application
@@ -20,7 +20,7 @@ class AgentRunnerDefaultConfigService:
self.ap = ap
async def _get_runner_descriptor(self, context: TenantContext, runner_id: str):
registry = getattr(self.ap, 'agent_runner_registry', None)
registry = getattr(self.ap, 'runner_registry', None)
if registry is None:
return None
try:
@@ -28,7 +28,7 @@ class AgentRunnerDefaultConfigService:
except Exception as e:
logger = getattr(self.ap, 'logger', None)
if logger:
logger.warning(f'Failed to load AgentRunner descriptor while setting default model: {e}')
logger.warning(f'Failed to load Runner descriptor while setting default model: {e}')
return None
async def auto_set_default_pipeline_llm_model(
@@ -39,8 +39,7 @@ class AgentRunnerDefaultConfigService:
"""Set model_uuid into the default pipeline runner config when the selector is empty."""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
persistence_pipeline.LegacyPipeline.workspace_uuid
== require_workspace_uuid(context),
persistence_pipeline.LegacyPipeline.workspace_uuid == require_workspace_uuid(context),
persistence_pipeline.LegacyPipeline.is_default == True,
)
)
+9 -8
View File
@@ -5,13 +5,13 @@ from __future__ import annotations
import typing
import pydantic
from langbot_plugin.api.entities.builtin.agent_runner.manifest import (
AgentRunnerCapabilities,
AgentRunnerPermissions,
from langbot_plugin.api.entities.builtin.runner.manifest import (
RunnerCapabilities,
RunnerPermissions,
)
class AgentRunnerDescriptor(pydantic.BaseModel):
class RunnerDescriptor(pydantic.BaseModel):
"""Descriptor for an agent runner.
Represents the discovered metadata for a runner, including
@@ -37,7 +37,7 @@ class AgentRunnerDescriptor(pydantic.BaseModel):
"""Plugin name from manifest"""
runner_name: str
"""AgentRunner component name from manifest"""
"""Runner component name from manifest"""
plugin_version: str | None = None
"""Optional plugin version"""
@@ -45,16 +45,17 @@ class AgentRunnerDescriptor(pydantic.BaseModel):
config_schema: list[dict[str, typing.Any]] = pydantic.Field(default_factory=list)
"""Configuration schema using DynamicForm format"""
capabilities: AgentRunnerCapabilities = pydantic.Field(default_factory=AgentRunnerCapabilities)
capabilities: RunnerCapabilities = pydantic.Field(default_factory=RunnerCapabilities)
"""Runner capabilities: streaming, tool_calling, knowledge_retrieval, etc."""
permissions: AgentRunnerPermissions = pydantic.Field(default_factory=AgentRunnerPermissions)
permissions: RunnerPermissions = pydantic.Field(default_factory=RunnerPermissions)
"""Requested LangBot resource permissions."""
raw_manifest: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
"""Original manifest for reference"""
component_kind: typing.Literal['AgentRunner', 'EventProcessor'] = 'AgentRunner'
component_kind: typing.Literal['Runner'] = 'Runner'
usages: list[typing.Literal['agent', 'event']] = pydantic.Field(default_factory=lambda: ['agent'])
supported_event_patterns: list[str] = pydantic.Field(default_factory=lambda: ['*'])
model_config = pydantic.ConfigDict(
+5 -5
View File
@@ -3,13 +3,13 @@
from __future__ import annotations
class AgentRunnerError(Exception):
class RunnerError(Exception):
"""Base error for agent runner operations."""
pass
class RunnerNotFoundError(AgentRunnerError):
class RunnerNotFoundError(RunnerError):
"""Runner not found in registry."""
def __init__(self, runner_id: str):
@@ -17,7 +17,7 @@ class RunnerNotFoundError(AgentRunnerError):
super().__init__(f'Agent runner not found: {runner_id}')
class RunnerNotAuthorizedError(AgentRunnerError):
class RunnerNotAuthorizedError(RunnerError):
"""Runner not authorized for this binding."""
def __init__(self, runner_id: str, bound_plugins: list[str] | None):
@@ -26,7 +26,7 @@ class RunnerNotAuthorizedError(AgentRunnerError):
super().__init__(f'Agent runner {runner_id} not authorized for bound_plugins={bound_plugins}')
class RunnerProtocolError(AgentRunnerError):
class RunnerProtocolError(RunnerError):
"""Runner protocol version mismatch or invalid manifest."""
def __init__(self, runner_id: str, message: str):
@@ -34,7 +34,7 @@ class RunnerProtocolError(AgentRunnerError):
super().__init__(f'Agent runner protocol error for {runner_id}: {message}')
class RunnerExecutionError(AgentRunnerError):
class RunnerExecutionError(RunnerError):
"""Runner execution failed."""
def __init__(
+1 -1
View File
@@ -1,4 +1,4 @@
"""Canonical AgentRunner event names reserved for future EBA integration."""
"""Canonical Runner event names reserved for future EBA integration."""
from __future__ import annotations
@@ -1,4 +1,4 @@
"""Host-only Query compatibility views for AgentRunner tool execution."""
"""Host-only Query compatibility views for Runner tool execution."""
from __future__ import annotations
+3 -3
View File
@@ -8,13 +8,13 @@ from __future__ import annotations
import typing
import pydantic
from langbot_plugin.api.entities.builtin.agent_runner.event import (
from langbot_plugin.api.entities.builtin.runner.event import (
ActorContext,
SubjectContext,
RawEventRef,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
class AgentEventEnvelope(pydantic.BaseModel):
+3 -3
View File
@@ -31,7 +31,7 @@ def parse_runner_id(runner_id: str) -> RunnerIdParts:
Raises:
ValueError: If runner_id format is invalid
"""
if runner_id.startswith(('plugin:', 'event_processor:')):
if runner_id.startswith('plugin:'):
source, value = runner_id.split(':', 1)
parts = value.split('/')
if len(parts) != 3:
@@ -71,7 +71,7 @@ def format_runner_id(
Returns:
Runner ID string
"""
if source in {'plugin', 'event_processor'}:
if source == 'plugin':
return f'{source}:{plugin_author}/{plugin_name}/{runner_name}'
else:
raise ValueError(f'Invalid runner source: {source}')
@@ -86,4 +86,4 @@ def is_plugin_runner_id(runner_id: str) -> bool:
Returns:
True if runner ID starts with 'plugin:'
"""
return runner_id.startswith(('plugin:', 'event_processor:'))
return runner_id.startswith('plugin:')
@@ -10,7 +10,7 @@ import pydantic
from langbot_plugin.api.entities.builtin.platform import message as platform_message
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .errors import RunnerProtocolError
from .host_models import AgentBinding, AgentEventEnvelope
from .interaction_store import InteractionStore
@@ -97,7 +97,7 @@ class InteractionManager:
result_dict: dict[str, typing.Any],
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
run_id: str,
adapter_context: dict[str, typing.Any] | None,
) -> bool:
@@ -230,7 +230,7 @@ class InteractionManager:
*,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
processor_id: str,
conversation_id: str | None,
) -> dict[str, typing.Any] | None:
@@ -390,7 +390,7 @@ class InteractionManager:
raise ValueError(f'interaction submission option is not present in the request: {field_id}')
@staticmethod
def _authorize(descriptor: AgentRunnerDescriptor, binding: AgentBinding) -> None:
def _authorize(descriptor: RunnerDescriptor, binding: AgentBinding) -> None:
supports_interactions = bool(getattr(descriptor.capabilities, 'interactions', False))
permissions = set(getattr(descriptor.permissions, 'interactions', []) or [])
if not supports_interactions or 'request' not in permissions:
+13 -24
View File
@@ -1,4 +1,4 @@
"""Plugin-runtime invocation for AgentRunner executions."""
"""Plugin-runtime invocation for Runner executions."""
from __future__ import annotations
@@ -10,13 +10,13 @@ import typing
from langbot_plugin.entities.io.errors import ActionCallTimeoutError
from ...core import app
from .context_builder import AgentRunContextPayload
from .descriptor import AgentRunnerDescriptor
from .context_builder import RunnerContextPayload
from .descriptor import RunnerDescriptor
from .errors import RunnerExecutionError
class AgentRunnerInvoker:
"""Invoke an AgentRunner through the plugin runtime.
class RunnerInvoker:
"""Invoke an Runner through the plugin runtime.
This keeps runtime transport, deadline enforcement, and transport error
mapping out of the orchestration state machine.
@@ -29,8 +29,8 @@ class AgentRunnerInvoker:
async def invoke(
self,
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
"""Invoke the runner and yield raw result dictionaries."""
if not self.ap.plugin_connector.is_enable_plugin:
@@ -41,18 +41,7 @@ class AgentRunnerInvoker:
)
try:
if descriptor.component_kind == 'EventProcessor':
context = {
**context,
'runtime': {
**context['runtime'],
'metadata': {
**context['runtime'].get('metadata', {}),
'component_kind': 'EventProcessor',
},
},
}
gen = self.ap.plugin_connector.run_agent(
gen = self.ap.plugin_connector.run_runner(
plugin_author=descriptor.plugin_author,
plugin_name=descriptor.plugin_name,
runner_name=descriptor.runner_name,
@@ -93,8 +82,8 @@ class AgentRunnerInvoker:
async def _next_with_deadline(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> dict[str, typing.Any]:
"""Read the next runner result while enforcing the run deadline."""
remaining = self._remaining_deadline_seconds(context)
@@ -116,7 +105,7 @@ class AgentRunnerInvoker:
def _remaining_deadline_seconds(
self,
context: AgentRunContextPayload,
context: RunnerContextPayload,
) -> float | None:
runtime = context.get('runtime') or {}
deadline_at = runtime.get('deadline_at')
@@ -127,14 +116,14 @@ class AgentRunnerInvoker:
except (TypeError, ValueError):
return None
def _is_deadline_exhausted(self, context: AgentRunContextPayload) -> bool:
def _is_deadline_exhausted(self, context: RunnerContextPayload) -> bool:
remaining = self._remaining_deadline_seconds(context)
return remaining is not None and remaining <= 0
async def _close_generator(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> None:
try:
await gen.aclose()
+24 -24
View File
@@ -15,8 +15,8 @@ from ...core import app
from ...api.http.context import ExecutionContext
from ...pipeline.pool import get_query_execution_context
from .binding_resolver import AgentBindingResolver
from .context_builder import AgentRunContextBuilder, AgentRunContextPayload
from .descriptor import AgentRunnerDescriptor
from .context_builder import RunnerContextBuilder, RunnerContextPayload
from .descriptor import RunnerDescriptor
from .execution_context import (
append_mcp_resource_context_to_event,
build_mcp_resource_context_addition,
@@ -26,10 +26,10 @@ from .execution_context import (
project_mcp_resource_config,
)
from .host_models import AgentBinding, AgentEventEnvelope
from .invoker import AgentRunnerInvoker
from .invoker import RunnerInvoker
from .interaction_manager import InteractionManager
from .query_bridge import QueryRunBridge
from .registry import AgentRunnerRegistry
from .registry import RunnerRegistry
from .resource_builder import AgentResourceBuilder
from .platform_tools import freeze_platform_context
from .result_normalizer import AgentResultNormalizer
@@ -43,7 +43,7 @@ ACTIVATED_SKILL_NAMES_STATE_KEY = 'host.activated_skills'
class AgentRunOrchestrator:
"""Coordinate one AgentRunner execution.
"""Coordinate one Runner execution.
The orchestrator keeps the run state machine readable and delegates
transport, Query bridging, and persistence side effects to narrower
@@ -51,13 +51,13 @@ class AgentRunOrchestrator:
"""
ap: app.Application
registry: AgentRunnerRegistry
context_builder: AgentRunContextBuilder
registry: RunnerRegistry
context_builder: RunnerContextBuilder
resource_builder: AgentResourceBuilder
result_normalizer: AgentResultNormalizer
binding_resolver: AgentBindingResolver
query_bridge: QueryRunBridge
invoker: AgentRunnerInvoker
invoker: RunnerInvoker
interaction_manager: InteractionManager
journal: AgentRunJournal
_session_registry: AgentRunSessionRegistry
@@ -65,16 +65,16 @@ class AgentRunOrchestrator:
def __init__(
self,
ap: app.Application,
registry: AgentRunnerRegistry,
registry: RunnerRegistry,
):
self.ap = ap
self.registry = registry
self.context_builder = AgentRunContextBuilder(ap)
self.context_builder = RunnerContextBuilder(ap)
self.resource_builder = AgentResourceBuilder(ap)
self.result_normalizer = AgentResultNormalizer(ap)
self.binding_resolver = AgentBindingResolver()
self.query_bridge = QueryRunBridge(self.binding_resolver)
self.invoker = AgentRunnerInvoker(ap)
self.invoker = RunnerInvoker(ap)
self.interaction_manager = InteractionManager(ap)
self.journal = AgentRunJournal(ap)
self._session_registry = get_session_registry()
@@ -86,7 +86,7 @@ class AgentRunOrchestrator:
bound_plugins: list[str] | None = None,
adapter_context: dict[str, typing.Any] | None = None,
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
"""Run an AgentRunner from an event-first envelope."""
"""Run an Runner from an event-first envelope."""
runner_id = binding.runner_id
execution_query = adapter_context.get('_query') if adapter_context else None
execution_context = adapter_context.get('_execution_context') if adapter_context else None
@@ -105,9 +105,9 @@ class AgentRunOrchestrator:
bound_plugins,
)
expected_kind = 'EventProcessor' if binding.processor_type == 'event_processor' else 'AgentRunner'
if descriptor.component_kind != expected_kind:
raise ValueError('Processor kind does not match the selected plugin component')
usage = 'event' if binding.processor_type == 'event_processor' else 'agent'
if usage not in descriptor.usages:
raise ValueError(f'The selected Runner does not support {usage} usage')
if execution_query is None:
execution_query = build_execution_query(event, [])
@@ -391,7 +391,7 @@ class AgentRunOrchestrator:
self,
query: pipeline_query.Query,
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
"""Run an AgentRunner from the current Pipeline Query entry point."""
"""Run an Runner from the current Pipeline Query entry point."""
plan = self.query_bridge.build_plan(query)
adapter_context = dict(plan.adapter_context)
adapter_context['_query'] = query
@@ -566,8 +566,8 @@ class AgentRunOrchestrator:
async def _invoke_runner(
self,
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
"""Compatibility delegate for older tests and internal callers."""
async for result in self.invoker.invoke(descriptor, context):
@@ -576,24 +576,24 @@ class AgentRunOrchestrator:
async def _next_with_deadline(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> dict[str, typing.Any]:
return await self.invoker._next_with_deadline(gen, descriptor, context)
def _remaining_deadline_seconds(
self,
context: AgentRunContextPayload,
context: RunnerContextPayload,
) -> float | None:
return self.invoker._remaining_deadline_seconds(context)
def _is_deadline_exhausted(self, context: AgentRunContextPayload) -> bool:
def _is_deadline_exhausted(self, context: RunnerContextPayload) -> bool:
return self.invoker._is_deadline_exhausted(context)
async def _close_generator(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> None:
await self.invoker._close_generator(gen, descriptor)
@@ -602,7 +602,7 @@ class AgentRunOrchestrator:
result_dict: dict[str, typing.Any],
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> None:
await self.journal.handle_state_updated_event(result_dict, event, binding, descriptor)
@@ -1,7 +1,8 @@
"""Persistent state store for AgentRunner protocol state.
"""Persistent state store for Runner protocol state.
This module provides a database-backed state store for event-first Protocol v1.
"""
from __future__ import annotations
import typing
@@ -16,7 +17,7 @@ from sqlalchemy.dialects.postgresql import insert as postgresql_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.exc import IntegrityError
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .host_models import AgentEventEnvelope, AgentBinding
from .state_scope import (
VALID_STATE_SCOPES,
@@ -24,7 +25,7 @@ from .state_scope import (
get_binding_identity,
normalize_state_key,
)
from ...entity.persistence.agent_runner_state import AgentRunnerState
from ...entity.persistence.runner_state import RunnerState
# Maximum value_json size (256KB)
@@ -32,7 +33,7 @@ MAX_VALUE_JSON_BYTES = 256 * 1024
class PersistentStateStore:
"""Database-backed state store for AgentRunner protocol state.
"""Database-backed state store for Runner protocol state.
IMPORTANT: This is HOST-OWNED protocol state, NOT plugin instance state.
@@ -55,7 +56,7 @@ class PersistentStateStore:
scope: str,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> str | None:
"""Get scope key for given scope."""
return build_state_scope_key(scope, event, binding, descriptor)
@@ -104,7 +105,7 @@ class PersistentStateStore:
dialect_name = self._db_engine.dialect.name
if dialect_name == 'sqlite':
stmt = sqlite_insert(AgentRunnerState).values(**values)
stmt = sqlite_insert(RunnerState).values(**values)
await conn.execute(
stmt.on_conflict_do_update(
index_elements=constraint_columns,
@@ -114,7 +115,7 @@ class PersistentStateStore:
return
if dialect_name == 'postgresql':
stmt = postgresql_insert(AgentRunnerState).values(**values)
stmt = postgresql_insert(RunnerState).values(**values)
await conn.execute(
stmt.on_conflict_do_update(
index_elements=constraint_columns,
@@ -124,12 +125,12 @@ class PersistentStateStore:
return
try:
await conn.execute(sqlalchemy.insert(AgentRunnerState).values(**values))
await conn.execute(sqlalchemy.insert(RunnerState).values(**values))
except IntegrityError:
await conn.execute(
update(AgentRunnerState)
.where(AgentRunnerState.scope_key == values['scope_key'])
.where(AgentRunnerState.state_key == values['state_key'])
update(RunnerState)
.where(RunnerState.scope_key == values['scope_key'])
.where(RunnerState.state_key == values['state_key'])
.values(**update_values)
)
@@ -139,7 +140,7 @@ class PersistentStateStore:
self,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, dict[str, typing.Any]]:
"""Build state snapshot for all scopes from event and binding.
@@ -174,8 +175,7 @@ class PersistentStateStore:
# Query all state entries for this scope_key
result = await conn.execute(
select(AgentRunnerState.state_key, AgentRunnerState.value_json)
.where(AgentRunnerState.scope_key == scope_key)
select(RunnerState.state_key, RunnerState.value_json).where(RunnerState.scope_key == scope_key)
)
rows = result.fetchall()
@@ -199,7 +199,7 @@ class PersistentStateStore:
self,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
scope: str,
key: str,
value: typing.Any,
@@ -280,9 +280,9 @@ class PersistentStateStore:
async with self._db_engine.connect() as conn:
result = await conn.execute(
select(AgentRunnerState.value_json)
.where(AgentRunnerState.scope_key == scope_key)
.where(AgentRunnerState.state_key == state_key)
select(RunnerState.value_json)
.where(RunnerState.scope_key == scope_key)
.where(RunnerState.state_key == state_key)
)
row = result.first()
@@ -358,9 +358,7 @@ class PersistentStateStore:
async with self._db_engine.begin() as conn:
result = await conn.execute(
delete(AgentRunnerState)
.where(AgentRunnerState.scope_key == scope_key)
.where(AgentRunnerState.state_key == state_key)
delete(RunnerState).where(RunnerState.scope_key == scope_key).where(RunnerState.state_key == state_key)
)
return (result.rowcount or 0) > 0
@@ -379,17 +377,15 @@ class PersistentStateStore:
async with self._db_engine.connect() as conn:
query = (
select(AgentRunnerState.state_key)
.where(AgentRunnerState.scope_key == scope_key)
.order_by(AgentRunnerState.state_key)
select(RunnerState.state_key)
.where(RunnerState.scope_key == scope_key)
.order_by(RunnerState.state_key)
.limit(limit + 1) # Fetch one extra to check has_more
)
if prefix:
prefix = normalize_state_key(prefix)
query = query.where(
AgentRunnerState.state_key.like(f'{prefix}%')
)
query = query.where(RunnerState.state_key.like(f'{prefix}%'))
result = await conn.execute(query)
rows = result.fetchall()
@@ -402,7 +398,7 @@ class PersistentStateStore:
async def clear_all(self) -> None:
"""Clear all state entries (for testing)."""
async with self._db_engine.begin() as conn:
await conn.execute(delete(AgentRunnerState))
await conn.execute(delete(RunnerState))
# Global singleton persistent state store
@@ -423,7 +419,7 @@ def get_persistent_state_store(db_engine: AsyncEngine | None = None) -> Persiste
with _persistent_state_store_lock:
if _persistent_state_store is None:
if db_engine is None:
raise RuntimeError("db_engine required for first call to get_persistent_state_store")
raise RuntimeError('db_engine required for first call to get_persistent_state_store')
_persistent_state_store = PersistentStateStore(db_engine)
return _persistent_state_store
@@ -1,4 +1,4 @@
"""Run-scoped platform and event action tools exposed to AgentRunners."""
"""Run-scoped platform and event action tools exposed to Runners."""
from __future__ import annotations
+1 -1
View File
@@ -1,4 +1,4 @@
"""Pipeline Query bridge for AgentRunner execution."""
"""Pipeline Query bridge for Runner execution."""
from __future__ import annotations
@@ -12,15 +12,15 @@ import typing
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
from langbot_plugin.api.entities.builtin.platform import message as platform_message
from langbot_plugin.api.entities.builtin.agent_runner.event import (
from langbot_plugin.api.entities.builtin.runner.event import (
AgentEventContext,
ConversationContext,
ActorContext,
SubjectContext,
RawEventRef,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
from .host_models import (
AgentConfig,
+26 -26
View File
@@ -5,23 +5,23 @@ from __future__ import annotations
import typing
import asyncio
from langbot_plugin.api.entities.builtin.agent_runner.manifest import (
AgentRunnerManifest,
from langbot_plugin.api.entities.builtin.runner.manifest import (
RunnerManifest,
)
from ...core import app
from ...api.http.context import ExecutionContext
from ...api.http.service.tenant import TenantContext
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .id import parse_runner_id, format_runner_id
from .errors import RunnerNotFoundError, RunnerNotAuthorizedError
class AgentRunnerRegistry:
class RunnerRegistry:
"""Registry for discovering and managing agent runners.
Responsibilities:
- Discover runners from plugin runtime via LIST_AGENT_RUNNERS
- Discover runners from plugin runtime via LIST_RUNNERS
- Validate runner manifests (kind, metadata, spec)
- Cache discovered runners for performance
- Filter runners by bound plugins
@@ -30,7 +30,7 @@ class AgentRunnerRegistry:
ap: app.Application
_cache: dict[tuple[str, str, int], dict[str, AgentRunnerDescriptor]]
_cache: dict[tuple[str, str, int], dict[str, RunnerDescriptor]]
"""Runner descriptors keyed by immutable Workspace execution scope."""
_cache_lock: asyncio.Lock
@@ -52,7 +52,7 @@ class AgentRunnerRegistry:
async def _resolve_context(self, context: TenantContext) -> ExecutionContext:
return await self.ap.plugin_connector.require_workspace_context(context)
async def _discover_runners(self) -> dict[str, AgentRunnerDescriptor]:
async def _discover_runners(self) -> dict[str, RunnerDescriptor]:
"""Discover runners from plugin runtime.
Always discovers ALL runners (no bound_plugins filter).
@@ -64,11 +64,11 @@ class AgentRunnerRegistry:
if not self.ap.plugin_connector.is_enable_plugin:
return {}
runners: dict[str, AgentRunnerDescriptor] = {}
runners: dict[str, RunnerDescriptor] = {}
try:
# Always list all runners (bound_plugins=None)
plugin_runners = await self.ap.plugin_connector.list_agent_runners(None)
plugin_runners = await self.ap.plugin_connector.list_runners(None)
for runner_data in plugin_runners:
try:
@@ -90,16 +90,16 @@ class AgentRunnerRegistry:
return runners
def _validate_and_build_descriptor(self, runner_data: dict[str, typing.Any]) -> AgentRunnerDescriptor | None:
def _validate_and_build_descriptor(self, runner_data: dict[str, typing.Any]) -> RunnerDescriptor | None:
"""Validate runner manifest and build descriptor.
Args:
runner_data: Raw runner data from plugin runtime with fields:
- plugin_author, plugin_name, runner_name
- manifest (typed AgentRunnerManifest)
- manifest (typed RunnerManifest)
Returns:
AgentRunnerDescriptor if valid, None if invalid
RunnerDescriptor if valid, None if invalid
"""
plugin_author = runner_data.get('plugin_author', '')
plugin_name = runner_data.get('plugin_name', '')
@@ -110,18 +110,19 @@ class AgentRunnerRegistry:
manifest = runner_data.get('manifest', {})
runner_id = format_runner_id(
source='event_processor' if manifest.get('component_kind') == 'EventProcessor' else 'plugin',
source='plugin',
plugin_author=plugin_author,
plugin_name=plugin_name,
runner_name=runner_name,
)
typed_manifest = AgentRunnerManifest.model_validate(manifest)
typed_manifest = RunnerManifest.model_validate(manifest)
config_schema = [item.model_dump(mode='json') for item in typed_manifest.config_schema]
return AgentRunnerDescriptor(
return RunnerDescriptor(
id=runner_id,
component_kind=typed_manifest.component_kind,
usages=typed_manifest.usages,
supported_event_patterns=typed_manifest.supported_event_patterns,
source='plugin',
label=typed_manifest.label,
@@ -152,8 +153,8 @@ class AgentRunnerRegistry:
context: TenantContext,
bound_plugins: list[str] | None = None,
use_cache: bool = True,
component_kind: str = 'AgentRunner',
) -> list[AgentRunnerDescriptor]:
usage: typing.Literal['agent', 'event'] | None = 'agent',
) -> list[RunnerDescriptor]:
"""List available runners.
Args:
@@ -173,7 +174,7 @@ class AgentRunnerRegistry:
return [
r
for r in self._filter_runners_by_bound_plugins(cached, bound_plugins)
if r.component_kind == component_kind
if usage is None or usage in r.usages
]
# Discover fresh (always full list)
@@ -187,14 +188,14 @@ class AgentRunnerRegistry:
return [
r
for r in self._filter_runners_by_bound_plugins(runners, bound_plugins)
if r.component_kind == component_kind
if usage is None or usage in r.usages
]
def _filter_runners_by_bound_plugins(
self,
runners: dict[str, AgentRunnerDescriptor],
runners: dict[str, RunnerDescriptor],
bound_plugins: list[str] | None,
) -> list[AgentRunnerDescriptor]:
) -> list[RunnerDescriptor]:
"""Filter runners by bound plugins.
Args:
@@ -222,7 +223,7 @@ class AgentRunnerRegistry:
context: TenantContext,
runner_id: str,
bound_plugins: list[str] | None = None,
) -> AgentRunnerDescriptor:
) -> RunnerDescriptor:
"""Get a specific runner descriptor.
Args:
@@ -230,7 +231,7 @@ class AgentRunnerRegistry:
bound_plugins: Optional bound plugins filter
Returns:
AgentRunnerDescriptor
RunnerDescriptor
Raises:
RunnerNotFoundError: If runner not found
@@ -242,8 +243,7 @@ class AgentRunnerRegistry:
except ValueError as e:
raise RunnerNotFoundError(runner_id) from e
component_kind = 'EventProcessor' if runner_id.startswith('event_processor:') else 'AgentRunner'
runners = await self.list_runners(context, bound_plugins=None, component_kind=component_kind)
runners = await self.list_runners(context, bound_plugins=None, usage=None)
descriptor = next((item for item in runners if item.id == runner_id), None)
if descriptor is None:
# The runtime launches installed plugins asynchronously, so an
@@ -252,7 +252,7 @@ class AgentRunnerRegistry:
context,
bound_plugins=None,
use_cache=False,
component_kind=component_kind,
usage=None,
)
descriptor = next((item for item in runners if item.id == runner_id), None)
if descriptor is None:
@@ -6,7 +6,7 @@ import typing
from ...core import app
from ...api.http.context import ExecutionContext
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .context_builder import (
AgentResources,
ModelResource,
@@ -54,7 +54,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> AgentResources:
"""Build AgentResources from event and binding.
@@ -127,7 +127,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
manifest_perms: typing.Any,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> list[ModelResource]:
"""Build models list from binding."""
@@ -174,7 +174,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
manifest_perms: typing.Any,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> list[ToolResource]:
"""Build tools list from binding."""
@@ -274,7 +274,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
manifest_perms: typing.Any,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> list[KnowledgeBaseResource]:
"""Build knowledge bases list from binding."""
@@ -321,7 +321,7 @@ class AgentResourceBuilder:
self,
execution_context: ExecutionContext,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> list[SkillResource]:
"""Build pipeline-visible skill resource facts.
@@ -372,7 +372,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
models: list[ModelResource],
seen_model_ids: set[str],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
include_llm: bool,
include_rerank: bool,
@@ -1,4 +1,4 @@
"""Project AgentRunner configuration into Host resource policy."""
"""Project Runner configuration into Host resource policy."""
from __future__ import annotations
@@ -1,11 +1,11 @@
"""Agent result normalizer for converting AgentRunResult to Pipeline messages."""
"""Agent result normalizer for converting RunnerResult to Pipeline messages."""
from __future__ import annotations
import typing
import pydantic
from langbot_plugin.api.entities.builtin.agent_runner.result import (
from langbot_plugin.api.entities.builtin.runner.result import (
ActionRequestedPayload,
MessageCompletedPayload,
MessageDeltaPayload,
@@ -19,7 +19,7 @@ from langbot_plugin.api.entities.builtin.agent_runner.result import (
from langbot_plugin.api.entities.builtin.provider import message as provider_message
from ...core import app
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .errors import RunnerExecutionError, RunnerProtocolError
@@ -40,7 +40,7 @@ STRICT_RESULT_PAYLOADS: dict[str, type[pydantic.BaseModel]] = {
class AgentResultNormalizer:
"""Normalizer for converting AgentRunResult to Pipeline messages.
"""Normalizer for converting RunnerResult to Pipeline messages.
Responsibilities:
- Accept only supported result types (message.delta, message.completed, etc.)
@@ -71,9 +71,9 @@ class AgentResultNormalizer:
async def normalize(
self,
result_dict: dict[str, typing.Any],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> provider_message.Message | provider_message.MessageChunk | None:
"""Normalize AgentRunResult to Message or MessageChunk.
"""Normalize RunnerResult to Message or MessageChunk.
Args:
result_dict: Raw result dict from plugin runtime
@@ -186,7 +186,7 @@ class AgentResultNormalizer:
self,
result_type: str,
data: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> bool:
"""Validate typed payloads that affect Host state or delivery.
@@ -210,7 +210,7 @@ class AgentResultNormalizer:
def _normalize_message_delta(
self,
data: dict[str, typing.Any],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> provider_message.MessageChunk:
"""Normalize message.delta to MessageChunk."""
chunk_data = data.get('chunk', {})
@@ -226,7 +226,7 @@ class AgentResultNormalizer:
def _normalize_message_completed(
self,
data: dict[str, typing.Any],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> provider_message.Message:
"""Normalize message.completed to Message."""
message_data = data.get('message', {})
+5 -5
View File
@@ -1,11 +1,11 @@
"""Run-side effects for AgentRunner executions."""
"""Run-side effects for Runner executions."""
from __future__ import annotations
import typing
from ...core import app
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .errors import RunnerProtocolError
from .host_models import AgentBinding, AgentEventEnvelope
from .persistent_state_store import PersistentStateStore, get_persistent_state_store
@@ -73,7 +73,7 @@ class AgentRunJournal:
*,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
context: dict[str, typing.Any],
authorization: dict[str, typing.Any],
) -> dict[str, typing.Any]:
@@ -112,7 +112,7 @@ class AgentRunJournal:
source: str = 'runner',
metadata: dict[str, typing.Any] | None = None,
) -> dict[str, typing.Any]:
"""Persist one AgentRunResult in the run ledger."""
"""Persist one RunnerResult in the run ledger."""
usage = result_dict.get('usage')
if hasattr(usage, 'model_dump'):
usage = usage.model_dump(mode='json')
@@ -153,7 +153,7 @@ class AgentRunJournal:
result_dict: dict[str, typing.Any],
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
run_id: str | None = None,
) -> None:
"""Handle state.updated result in event-first mode."""
@@ -61,7 +61,7 @@ class AgentRunSession(typing.TypedDict):
Stored in AgentRunSessionRegistry for proxy action permission validation.
Fields:
run_id: Unique run identifier (UUID from AgentRunContext)
run_id: Unique run identifier (UUID from RunnerContext)
runner_id: Runner descriptor ID (plugin:author/name/runner)
query_id: Host entry query ID, only present for query-based adapters
execution_query: Host-only Query used by providers and tool loaders
@@ -84,10 +84,10 @@ class AgentRunSession(typing.TypedDict):
class AgentRunSessionRegistry:
"""Registry for active agent run sessions.
Host-owned registry for tracking active AgentRunner executions.
Host-owned registry for tracking active Runner executions.
Used by proxy actions in handler.py to validate resource access.
Key: run_id (UUID from AgentRunContext)
Key: run_id (UUID from RunnerContext)
Value: AgentRunSession with authorized resources
Thread-safe via asyncio.Lock.
@@ -130,7 +130,7 @@ class AgentRunSessionRegistry:
bot_id: Bot UUID for history/event access
workspace_id: Workspace ID for history/event access
thread_id: Thread ID for history/event access
available_apis: Run-scoped pull APIs exposed in AgentRunContext
available_apis: Run-scoped pull APIs exposed in RunnerContext
state_policy: State policy from binding (enable_state, state_scopes)
state_context: Context for state API (scope_keys, binding_identity, etc.)
execution_query: Host-only Query used for provider and tool execution
+31 -21
View File
@@ -1,11 +1,12 @@
"""State scope key helpers for AgentRunner host-owned state."""
"""State scope key helpers for Runner host-owned state."""
from __future__ import annotations
import hashlib
import json
import typing
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .host_models import AgentBinding, AgentEventEnvelope
@@ -47,7 +48,7 @@ def _scope_hash(scope: str, parts: dict[str, typing.Any]) -> str:
def _base_scope_parts(
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, typing.Any]:
return {
'runner_id': descriptor.id,
@@ -61,7 +62,7 @@ def build_state_scope_key(
scope: str,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> str | None:
"""Build the storage key for one state scope.
@@ -72,29 +73,38 @@ def build_state_scope_key(
if scope == 'conversation':
if not event.conversation_id:
return None
return _scope_hash(scope, {
**base_parts,
'conversation_id': event.conversation_id,
'thread_id': event.thread_id,
})
return _scope_hash(
scope,
{
**base_parts,
'conversation_id': event.conversation_id,
'thread_id': event.thread_id,
},
)
if scope == 'actor':
if not event.actor or not event.actor.actor_id:
return None
return _scope_hash(scope, {
**base_parts,
'actor_type': event.actor.actor_type or 'user',
'actor_id': event.actor.actor_id,
})
return _scope_hash(
scope,
{
**base_parts,
'actor_type': event.actor.actor_type or 'user',
'actor_id': event.actor.actor_id,
},
)
if scope == 'subject':
if not event.subject or not event.subject.subject_id:
return None
return _scope_hash(scope, {
**base_parts,
'subject_type': event.subject.subject_type or 'unknown',
'subject_id': event.subject.subject_id,
})
return _scope_hash(
scope,
{
**base_parts,
'subject_type': event.subject.subject_type or 'unknown',
'subject_id': event.subject.subject_id,
},
)
if scope == 'runner':
return _scope_hash(scope, base_parts)
@@ -105,7 +115,7 @@ def build_state_scope_key(
def build_state_scope_keys(
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, str]:
"""Build all available scope keys for an event/binding pair."""
scope_keys: dict[str, str] = {}
@@ -119,7 +129,7 @@ def build_state_scope_keys(
def build_state_context(
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, typing.Any]:
"""Build the State API context stored in the run session."""
return {
@@ -1,4 +1,5 @@
"""Transcript store for writing and querying conversation history."""
from __future__ import annotations
import json
@@ -46,9 +47,7 @@ class TranscriptStore:
def __init__(self, engine: AsyncEngine):
self.engine = engine
self._session_factory = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
self._session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def append_transcript(
self,
@@ -62,7 +61,7 @@ class TranscriptStore:
content_json: dict[str, typing.Any] | None = None,
attachment_refs: list[dict[str, typing.Any]] | None = None,
thread_id: str | None = None,
item_type: str = "message",
item_type: str = 'message',
run_id: str | None = None,
runner_id: str | None = None,
metadata: dict[str, typing.Any] | None = None,
@@ -93,7 +92,7 @@ class TranscriptStore:
# Truncate content if too long
if content and len(content) > self.MAX_CONTENT_LENGTH:
content = content[:self.MAX_CONTENT_LENGTH - 3] + "..."
content = content[: self.MAX_CONTENT_LENGTH - 3] + '...'
async with self._session_factory() as session:
item = Transcript(
@@ -127,7 +126,7 @@ class TranscriptStore:
before_seq: int | None = None,
after_seq: int | None = None,
limit: int = 50,
direction: str = "backward",
direction: str = 'backward',
include_attachments: bool = False,
bot_id: str | None = None,
workspace_id: str | None = None,
@@ -154,15 +153,13 @@ class TranscriptStore:
limit = min(limit, self.HARD_LIMIT)
async with self._session_factory() as session:
query = sqlalchemy.select(Transcript).where(
Transcript.conversation_id == conversation_id
)
query = sqlalchemy.select(Transcript).where(Transcript.conversation_id == conversation_id)
query = self._apply_scope_filters(query, bot_id, workspace_id, thread_id, strict_thread)
if direction == "backward" and before_seq is not None:
if direction == 'backward' and before_seq is not None:
query = query.where(Transcript.seq < before_seq)
query = query.order_by(Transcript.seq.desc())
elif direction == "forward" and after_seq is not None:
elif direction == 'forward' and after_seq is not None:
query = query.where(Transcript.seq > after_seq)
query = query.order_by(Transcript.seq.asc())
else:
@@ -181,7 +178,7 @@ class TranscriptStore:
next_seq = None
prev_seq = None
if direction == "backward":
if direction == 'backward':
# Items are in descending order
if items:
next_seq = items[-1].get('seq') if has_more else None
@@ -225,7 +222,7 @@ class TranscriptStore:
async with self._session_factory() as session:
query = sqlalchemy.select(Transcript).where(
Transcript.conversation_id == conversation_id,
Transcript.content.ilike(f"%{query_text}%"),
Transcript.content.ilike(f'%{query_text}%'),
)
query = self._apply_scope_filters(query, bot_id, workspace_id, thread_id, strict_thread)
@@ -278,14 +275,14 @@ class TranscriptStore:
) -> list[provider_message.Message]:
"""Project Transcript rows into the legacy provider Message view.
AgentRunner history is canonical in Transcript. This view exists for
Runner history is canonical in Transcript. This view exists for
legacy Pipeline readers such as PromptPreProcessing that still expect
query.messages.
"""
items, _, _, _ = await self.page_transcript(
conversation_id=conversation_id,
limit=limit,
direction="backward",
direction='backward',
bot_id=bot_id,
workspace_id=workspace_id,
thread_id=thread_id,
@@ -353,9 +350,7 @@ class TranscriptStore:
) -> int:
"""Delete Transcript rows created before the supplied timestamp."""
async with self._session_factory() as session:
result = await session.execute(
sqlalchemy.delete(Transcript).where(Transcript.created_at < before)
)
result = await session.execute(sqlalchemy.delete(Transcript).where(Transcript.created_at < before))
await session.commit()
return result.rowcount or 0
@@ -363,8 +358,9 @@ class TranscriptStore:
"""Fallback next sequence number for stores that cannot expose autoincrement IDs."""
async with self._session_factory() as session:
result = await session.execute(
sqlalchemy.select(sqlalchemy.func.max(Transcript.seq))
.where(Transcript.conversation_id == conversation_id)
sqlalchemy.select(sqlalchemy.func.max(Transcript.seq)).where(
Transcript.conversation_id == conversation_id
)
)
max_seq = result.scalar()
return (max_seq or 0) + 1
@@ -9,7 +9,7 @@ import json
import quart
from .....agent.runner.errors import (
AgentRunnerError,
RunnerError,
RunnerExecutionError,
RunnerNotAuthorizedError,
RunnerNotFoundError,
@@ -39,7 +39,7 @@ def debug_stream_response(service, context, agent_uuid: str, payload: dict) -> q
code, message = 'runner_protocol_error', 'The Agent runner returned an invalid response'
elif isinstance(exc, ValueError):
code, message = 'invalid_request', str(exc)
elif isinstance(exc, AgentRunnerError):
elif isinstance(exc, RunnerError):
code, message = 'runner_error', 'The Agent runner could not complete this test'
else:
code, message = 'runner_error', 'The Agent debug execution failed'
@@ -3,7 +3,7 @@ from __future__ import annotations
import quart
from .....agent.runner.errors import (
AgentRunnerError,
RunnerError,
RunnerExecutionError,
RunnerNotAuthorizedError,
RunnerNotFoundError,
@@ -144,7 +144,7 @@ class AgentsRouterGroup(group.RouterGroup):
'runner_protocol_error',
'The Agent runner returned an invalid response',
)
except AgentRunnerError:
except RunnerError:
return self.http_status(
502,
'runner_error',
+25 -20
View File
@@ -8,13 +8,13 @@ import uuid
import typing
import sqlalchemy
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.agent_runner.event import (
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.runner.event import (
ActorContext,
RawEventRef,
SubjectContext,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
from ....core import app
from ....agent.runner.config_resolver import RunnerConfigResolver
@@ -69,13 +69,13 @@ class AgentService:
except Exception as exc:
self.ap.logger.warning(f'Failed to load Agent Host tool catalog: {exc}')
event_processors = []
registry = getattr(self.ap, 'agent_runner_registry', None)
registry = getattr(self.ap, 'runner_registry', None)
if registry is not None:
event_processors = [
item.model_dump(mode='json')
for item in await registry.list_runners(
context,
component_kind='EventProcessor',
usage='event',
use_cache=False,
)
]
@@ -168,7 +168,7 @@ class AgentService:
config = agent.get('config')
if not isinstance(config, dict):
raise ValueError('Agent configuration is invalid')
_, runner_id, runner_config = RunnerConfigResolver.resolve_agent_runner_config(config)
_, runner_id, runner_config = RunnerConfigResolver.resolve_agent_config(config)
if not runner_id:
raise ValueError('Agent has no configured runner')
@@ -405,9 +405,8 @@ class AgentService:
config, runner_id, patterns = await self._prepare_event_processor(context, agent_data)
else:
config = agent_data['config'] if 'config' in agent_data else await self._get_default_agent_config(context)
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(config)
if (runner_id or '').startswith('event_processor:'):
raise ValueError('EventProcessor components require an event processor instance')
config, runner_id, _ = RunnerConfigResolver.resolve_agent_config(config)
await self._validate_runner_for_agent(context, runner_id)
patterns = agent_data.get('supported_event_patterns', AGENT_DEFAULT_EVENT_PATTERNS)
new_uuid = str(uuid.uuid4())
values = {
@@ -444,13 +443,13 @@ class AgentService:
config, runner_id, patterns = await self._prepare_event_processor(context, agent_data, existing_agent)
update_data.update(config=config, component_ref=runner_id, supported_event_patterns=patterns)
if 'config' in update_data:
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(update_data['config'])
config, runner_id, _ = RunnerConfigResolver.resolve_agent_config(update_data['config'])
update_data['config'] = config
else:
_, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(existing_agent.config)
_, runner_id, _ = RunnerConfigResolver.resolve_agent_config(existing_agent.config)
update_data['component_ref'] = runner_id
if existing_agent.kind == AGENT_KIND_AGENT and (runner_id or '').startswith('event_processor:'):
raise ValueError('EventProcessor components require an event processor instance')
if existing_agent.kind == AGENT_KIND_AGENT:
await self._validate_runner_for_agent(context, runner_id)
result = await self.ap.persistence_mgr.execute_async(
scope_statement(
sqlalchemy.update(persistence_agent.Agent)
@@ -482,6 +481,12 @@ class AgentService:
raise ValueError(f'Agent {agent_uuid} not found')
await self.ap.pipeline_service.delete_pipeline(context, agent_uuid)
async def _validate_runner_for_agent(self, context, runner_id):
if runner_id:
descriptor = await self.ap.runner_registry.get(context, runner_id)
if 'agent' not in descriptor.usages:
raise ValueError('The selected Runner does not support agent usage')
async def _prepare_event_processor(self, context, data, existing=None):
"""Resolve an installed component and keep its capability declaration authoritative."""
config = copy.deepcopy(data.get('config', existing.config if existing is not None else {}))
@@ -491,17 +496,17 @@ class AgentService:
if component_ref is None and not config and not data.get('parameters'):
# An unconfigured instance cannot subscribe to or execute any events.
return {}, None, []
if not isinstance(component_ref, str) or not component_ref.startswith('event_processor:'):
raise ValueError('Select an installed EventProcessor component')
if not isinstance(component_ref, str) or not component_ref.startswith('plugin:'):
raise ValueError('Select an installed Runner component')
try:
descriptor = await self.ap.agent_runner_registry.get(context, component_ref)
descriptor = await self.ap.runner_registry.get(context, component_ref)
except Exception as exc:
from ....agent.runner.errors import RunnerNotFoundError
if isinstance(exc, RunnerNotFoundError):
raise ValueError('EventProcessor component is unavailable') from exc
raise ValueError('Runner component is unavailable') from exc
raise
if descriptor.component_kind != 'EventProcessor' or not descriptor.supported_event_patterns:
if 'event' not in descriptor.usages or not descriptor.supported_event_patterns:
raise ValueError('The component does not declare supported events')
config['runner'] = {'id': component_ref}
parameters = data.get('parameters')
@@ -583,9 +588,9 @@ class AgentService:
async def _get_default_agent_config(self, context: TenantContext) -> dict[str, typing.Any]:
runners = []
if getattr(self.ap, 'agent_runner_registry', None) is not None:
if getattr(self.ap, 'runner_registry', None) is not None:
try:
runners = await self.ap.agent_runner_registry.list_runners(context, bound_plugins=None)
runners = await self.ap.runner_registry.list_runners(context, bound_plugins=None)
except Exception as e:
if getattr(self.ap, 'logger', None):
self.ap.logger.warning(f'Failed to load plugin agent runners for default agent config: {e}')
+16 -49
View File
@@ -42,30 +42,22 @@ class PipelineService:
def _get_default_values_from_schema(
config_schema: list[dict[str, typing.Any]],
) -> dict[str, typing.Any]:
return {
item['name']: item['default']
for item in config_schema
if item.get('name') and 'default' in item
}
return {item['name']: item['default'] for item in config_schema if item.get('name') and 'default' in item}
async def get_default_pipeline_config(self, context: TenantContext) -> dict[str, typing.Any]:
from ....utils import paths as path_utils
template_path = path_utils.get_resource_path(
'templates/default-pipeline-config.json'
)
template_path = path_utils.get_resource_path('templates/default-pipeline-config.json')
with open(template_path, 'r', encoding='utf-8') as f:
config = json.load(f)
registry = getattr(self.ap, 'agent_runner_registry', None)
registry = getattr(self.ap, 'runner_registry', None)
if registry is None:
return config
try:
runners = await registry.list_runners(context, bound_plugins=None)
except Exception as exc:
self.ap.logger.warning(
f'Failed to load AgentRunner defaults for pipeline config: {exc}'
)
self.ap.logger.warning(f'Failed to load Runner defaults for pipeline config: {exc}')
return config
if not runners:
return config
@@ -75,11 +67,7 @@ class PipelineService:
runner_config = ai_config.setdefault('runner', {})
runner_config['id'] = selected.id
runner_config.setdefault('expire-time', 0)
ai_config['runner_config'] = {
selected.id: self._get_default_values_from_schema(
selected.config_schema
)
}
ai_config['runner_config'] = {selected.id: self._get_default_values_from_schema(selected.config_schema)}
return config
async def get_pipeline_metadata(self, context: TenantContext) -> list[dict]:
@@ -88,11 +76,7 @@ class PipelineService:
ai_metadata = copy.deepcopy(self.ap.pipeline_config_meta_ai)
runner_stage = next(
(
stage
for stage in ai_metadata.get('stages', [])
if stage.get('name') == 'runner'
),
(stage for stage in ai_metadata.get('stages', []) if stage.get('name') == 'runner'),
None,
)
if runner_stage:
@@ -100,25 +84,18 @@ class PipelineService:
if config_item.get('name') != 'id':
continue
try:
runner_options, runner_stages = (
await self.ap.agent_runner_registry.get_runner_metadata_for_pipeline(context)
runner_options, runner_stages = await self.ap.runner_registry.get_runner_metadata_for_pipeline(
context
)
config_item['options'] = runner_options
if runner_options and 'default' not in config_item:
config_item['default'] = runner_options[0]['name']
existing = {
stage.get('name')
for stage in ai_metadata.get('stages', [])
}
existing = {stage.get('name') for stage in ai_metadata.get('stages', [])}
ai_metadata.setdefault('stages', []).extend(
stage
for stage in runner_stages
if stage.get('name') not in existing
stage for stage in runner_stages if stage.get('name') not in existing
)
except Exception as exc:
self.ap.logger.warning(
f'Failed to load AgentRunner pipeline metadata: {exc}'
)
self.ap.logger.warning(f'Failed to load Runner pipeline metadata: {exc}')
return [
self.ap.pipeline_config_meta_trigger,
self.ap.pipeline_config_meta_safety,
@@ -187,9 +164,7 @@ class PipelineService:
async def create_pipeline(self, context: TenantContext, pipeline_data: dict, default: bool = False) -> str:
workspace_uuid = require_workspace_uuid(context)
if 'extensions_preferences' in pipeline_data:
self._validate_extension_preferences(
pipeline_data['extensions_preferences']
)
self._validate_extension_preferences(pipeline_data['extensions_preferences'])
if 'config' in pipeline_data:
RunnerConfigResolver.validate_pipeline_config(pipeline_data['config'])
# Check limitation
@@ -250,9 +225,7 @@ class PipelineService:
)
RunnerConfigResolver.validate_pipeline_config(pipeline_data['config'])
if 'extensions_preferences' in pipeline_data:
self._validate_extension_preferences(
pipeline_data['extensions_preferences']
)
self._validate_extension_preferences(pipeline_data['extensions_preferences'])
result = await self.ap.persistence_mgr.execute_async(
scope_statement(
@@ -333,9 +306,7 @@ class PipelineService:
'stages': original_pipeline.stages.copy() if original_pipeline.stages else default_stage_order.copy(),
'config': original_pipeline.config.copy() if original_pipeline.config else {},
'is_default': False,
'extensions_preferences': normalize_extension_preferences(
original_pipeline.extensions_preferences
),
'extensions_preferences': normalize_extension_preferences(original_pipeline.extensions_preferences),
}
# Insert the new pipeline
@@ -377,9 +348,7 @@ class PipelineService:
if bound_mcp_resources is not None:
extension_updates['mcp_resources'] = bound_mcp_resources
if mcp_resource_agent_read_enabled is not None:
extension_updates['mcp_resource_agent_read_enabled'] = (
mcp_resource_agent_read_enabled
)
extension_updates['mcp_resource_agent_read_enabled'] = mcp_resource_agent_read_enabled
self._validate_extension_preferences(
extension_updates,
context='Pipeline extension',
@@ -406,9 +375,7 @@ class PipelineService:
raise WorkspaceNotFoundError(f'Pipeline {pipeline_uuid} not found')
# Update extensions_preferences
extensions_preferences = normalize_extension_preferences(
pipeline.extensions_preferences
)
extensions_preferences = normalize_extension_preferences(pipeline.extensions_preferences)
extensions_preferences['enable_all_plugins'] = enable_all_plugins
extensions_preferences['enable_all_mcp_servers'] = enable_all_mcp_servers
extensions_preferences['enable_all_skills'] = enable_all_skills
+3 -1
View File
@@ -209,7 +209,9 @@ class LangBotMCPServer:
await ap.agent_service.delete_agent(context, processor_uuid)
return _dump({'ok': True})
@mcp.tool(description='Get processor kinds and installed EventProcessor components with configuration schemas.')
@mcp.tool(
description='Get processor kinds and installed event-capable Runner components with configuration schemas.'
)
async def get_processor_metadata() -> str:
context = _authorized(Permission.RESOURCE_VIEW)
return _dump(await ap.agent_service.get_agent_metadata(context))
+1 -1
View File
@@ -2073,7 +2073,7 @@ class BoxService:
"""Return LLM system-prompt guidance for the exec tool.
All execution-specific prompt text is kept here so that callers
(e.g. LocalAgentRunner) stay free of box domain knowledge.
(e.g. LocalRunner) stay free of box domain knowledge.
``query`` is the current turn's pipeline query. When provided,
the guidance ALWAYS advertises the per-query outbox path so the agent
+3 -3
View File
@@ -60,7 +60,7 @@ from ..cloud import model_catalog as cloud_model_catalog_module
from ..api.http.context import ExecutionContext, PrincipalContext, PrincipalType
if TYPE_CHECKING:
from ..agent.runner import AgentRunnerRegistry, AgentRunOrchestrator, AgentRunnerDefaultConfigService
from ..agent.runner import RunnerRegistry, AgentRunOrchestrator, RunnerDefaultConfigService
class Application:
@@ -203,9 +203,9 @@ class Application:
maintenance_service: maintenance_service.MaintenanceService = None
# Agent runner subsystem
agent_runner_registry: AgentRunnerRegistry = None
runner_registry: RunnerRegistry = None
agent_runner_default_config_service: AgentRunnerDefaultConfigService = None
runner_default_config_service: RunnerDefaultConfigService = None
agent_run_orchestrator: AgentRunOrchestrator = None
+6 -6
View File
@@ -38,7 +38,7 @@ from ...vector import mgr as vectordb_mgr
from .. import taskmgr
from ...telemetry import telemetry as telemetry_module
from ...survey import manager as survey_module
from ...agent.runner import AgentRunnerRegistry, AgentRunOrchestrator, AgentRunnerDefaultConfigService
from ...agent.runner import RunnerRegistry, AgentRunOrchestrator, RunnerDefaultConfigService
from ...workspace import service as workspace_service_module
from ...workspace import collaboration as workspace_collaboration_module
from ...workspace import invitation_delivery as invitation_delivery_module
@@ -312,13 +312,13 @@ class BuildAppStage(stage.BootingStage):
workspace_service_inst.release_startup_execution_bindings()
# Initialize agent runner subsystem
agent_runner_registry_inst = AgentRunnerRegistry(ap)
ap.agent_runner_registry = agent_runner_registry_inst
runner_registry_inst = RunnerRegistry(ap)
ap.runner_registry = runner_registry_inst
agent_runner_default_config_service_inst = AgentRunnerDefaultConfigService(ap)
ap.agent_runner_default_config_service = agent_runner_default_config_service_inst
runner_default_config_service_inst = RunnerDefaultConfigService(ap)
ap.runner_default_config_service = runner_default_config_service_inst
agent_run_orchestrator_inst = AgentRunOrchestrator(ap, agent_runner_registry_inst)
agent_run_orchestrator_inst = AgentRunOrchestrator(ap, runner_registry_inst)
ap.agent_run_orchestrator = agent_run_orchestrator_inst
ctrl = controller.Controller(ap)
@@ -18,7 +18,7 @@ class AgentRun(Base):
"""Auto-increment ID for pagination."""
run_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, unique=True, index=True)
"""Unique AgentRunner run identifier."""
"""Unique Runner run identifier."""
event_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
"""Input event that triggered this run."""
@@ -1,4 +1,5 @@
"""EventLog persistence entity for storing auditable event facts."""
from __future__ import annotations
import sqlalchemy
@@ -8,7 +9,7 @@ from .base import Base
class EventLog(Base):
"""EventLog stores auditable event records for AgentRunner.
"""EventLog stores auditable event records for Runner.
This is the fact source for events - messages, tool calls, system events, etc.
Large payloads are stored separately; this table stores references and
@@ -1,4 +1,5 @@
"""Agent runner state persistence entity for host-owned state."""
from __future__ import annotations
import sqlalchemy
@@ -7,8 +8,8 @@ import datetime
from .base import Base
class AgentRunnerState(Base):
"""AgentRunnerState stores host-owned state for AgentRunner protocol.
class RunnerState(Base):
"""RunnerState stores host-owned state for Runner protocol.
State is:
- Host-owned: Managed by LangBot, not by plugin instances
@@ -21,10 +22,10 @@ class AgentRunnerState(Base):
- subject: runner_id + binding_id + subject_type + subject_id
- runner: runner_id + binding_id
This table is the production store for AgentRunner state.
This table is the production store for Runner state.
"""
__tablename__ = 'agent_runner_state'
__tablename__ = 'runner_state'
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
"""Auto-increment ID for sequencing."""
@@ -77,12 +78,14 @@ class AgentRunnerState(Base):
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow)
"""When this state entry was created."""
updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
)
"""When this state entry was last updated."""
# Unique constraint: scope_key + state_key
__table_args__ = (
sqlalchemy.UniqueConstraint('scope_key', 'state_key', name='uq_agent_runner_state_scope_key_state_key'),
sqlalchemy.Index('ix_agent_runner_state_runner_binding', 'runner_id', 'binding_identity'),
sqlalchemy.Index('ix_agent_runner_state_scope_key_lookup', 'scope_key'),
sqlalchemy.UniqueConstraint('scope_key', 'state_key', name='uq_runner_state_scope_key_state_key'),
sqlalchemy.Index('ix_runner_state_runner_binding', 'runner_id', 'binding_identity'),
sqlalchemy.Index('ix_runner_state_scope_key_lookup', 'scope_key'),
)
+1 -1
View File
@@ -19,7 +19,7 @@ from langbot.pkg.entity.persistence import (
agent, # noqa: F401
agent_interaction, # noqa: F401
agent_run, # noqa: F401
agent_runner_state, # noqa: F401
runner_state, # noqa: F401
apikey, # noqa: F401
bot, # noqa: F401
bstorage, # noqa: F401
@@ -1,4 +1,4 @@
"""Migrate official AgentRunner IDs to their marketplace identities.
"""Migrate official Runner IDs to their marketplace identities.
Revision ID: 0015_official_runner_ids
Revises: 0014_interaction_delivery
@@ -21,7 +21,7 @@ depends_on = None
_RUNNER_ID_RENAMES = {
'plugin:langbot/acp-agent-runner/default': 'plugin:langbot-team/ACPAgentRunner/default',
'plugin:langbot/acp-agent-runner/default': 'plugin:langbot-team/ACPRunner/default',
'plugin:langbot/claude-code-agent/default': 'plugin:langbot-team/ClaudeCodeAgent/default',
'plugin:langbot/codex-agent/default': 'plugin:langbot-team/CodexAgent/default',
'plugin:langbot/coze-agent/default': 'plugin:langbot-team/CozeAgent/default',
@@ -186,17 +186,16 @@ def _rewrite_runner_state(renames: dict[str, str]) -> None:
scope_key = _state_scope_key(row, runner_id, binding_identity) or row['scope_key']
collision = bind.execute(
sa.text(
'SELECT id FROM agent_runner_state '
'WHERE scope_key = :scope_key AND state_key = :state_key AND id != :id'
'SELECT id FROM runner_state WHERE scope_key = :scope_key AND state_key = :state_key AND id != :id'
),
{'scope_key': scope_key, 'state_key': row['state_key'], 'id': row['id']},
).scalar_one_or_none()
if collision is not None:
bind.execute(sa.text('DELETE FROM agent_runner_state WHERE id = :id'), {'id': row['id']})
bind.execute(sa.text('DELETE FROM runner_state WHERE id = :id'), {'id': row['id']})
continue
bind.execute(
sa.text(
'UPDATE agent_runner_state '
'UPDATE runner_state '
'SET runner_id = :runner_id, binding_identity = :binding_identity, scope_key = :scope_key '
'WHERE id = :id'
),
@@ -1,4 +1,4 @@
"""Merge AgentRunner and Workspace heads and scope Agents to a Workspace.
"""Merge Runner and Workspace heads and scope Agents to a Workspace.
Revision ID: 0016_agent_workspace
Revises: 0015_official_runner_ids, 0015_cloud_core_collab
@@ -27,10 +27,7 @@ def _inspector(conn: sa.Connection) -> sa.Inspector:
def _columns(conn: sa.Connection) -> dict[str, dict]:
return {
column['name']: column
for column in _inspector(conn).get_columns(_TABLE)
}
return {column['name']: column for column in _inspector(conn).get_columns(_TABLE)}
def _default_workspace_uuid(conn: sa.Connection) -> str | None:
@@ -59,8 +56,7 @@ def _default_workspace_uuid(conn: sa.Connection) -> str | None:
def _foreign_key_exists(conn: sa.Connection) -> bool:
return any(
tuple(foreign_key.get('constrained_columns') or ())
== ('workspace_uuid',)
tuple(foreign_key.get('constrained_columns') or ()) == ('workspace_uuid',)
and foreign_key.get('referred_table') == 'workspaces'
and tuple(foreign_key.get('referred_columns') or ()) == ('uuid',)
for foreign_key in _inspector(conn).get_foreign_keys(_TABLE)
@@ -75,10 +71,7 @@ def _enable_postgres_rls(conn: sa.Connection) -> None:
op.execute(sa.text(f'DROP POLICY IF EXISTS {policy} ON {table}'))
op.execute(sa.text(f'ALTER TABLE {table} ENABLE ROW LEVEL SECURITY'))
op.execute(sa.text(f'ALTER TABLE {table} FORCE ROW LEVEL SECURITY'))
expression = (
"workspace_uuid::text = "
f"NULLIF(current_setting('{_TENANT_SETTING}', true), '')"
)
expression = f"workspace_uuid::text = NULLIF(current_setting('{_TENANT_SETTING}', true), '')"
op.execute(
sa.text(
f'CREATE POLICY {policy} ON {table} AS PERMISSIVE FOR ALL TO PUBLIC '
@@ -104,22 +97,12 @@ def upgrade() -> None:
_TABLE,
sa.column('workspace_uuid', sa.String(36)),
)
null_count = conn.scalar(
sa.select(sa.func.count())
.select_from(agents)
.where(agents.c.workspace_uuid.is_(None))
)
null_count = conn.scalar(sa.select(sa.func.count()).select_from(agents).where(agents.c.workspace_uuid.is_(None)))
if null_count:
workspace_uuid = _default_workspace_uuid(conn)
if workspace_uuid is None:
raise RuntimeError(
'Cannot backfill Agents: the instance has no unique local Workspace'
)
conn.execute(
agents.update()
.where(agents.c.workspace_uuid.is_(None))
.values(workspace_uuid=workspace_uuid)
)
raise RuntimeError('Cannot backfill Agents: the instance has no unique local Workspace')
conn.execute(agents.update().where(agents.c.workspace_uuid.is_(None)).values(workspace_uuid=workspace_uuid))
columns = _columns(conn)
needs_contract = columns['workspace_uuid']['nullable'] or not _foreign_key_exists(conn)
@@ -140,9 +123,7 @@ def upgrade() -> None:
ondelete='CASCADE',
)
index_names = {
index['name'] for index in _inspector(conn).get_indexes(_TABLE)
}
index_names = {index['name'] for index in _inspector(conn).get_indexes(_TABLE)}
if 'ix_agents_workspace_name' not in index_names:
op.create_index(
'ix_agents_workspace_name',
@@ -172,9 +153,7 @@ def downgrade() -> None:
op.execute(sa.text(f'ALTER TABLE {table} NO FORCE ROW LEVEL SECURITY'))
op.execute(sa.text(f'ALTER TABLE {table} DISABLE ROW LEVEL SECURITY'))
index_names = {
index['name'] for index in _inspector(conn).get_indexes(_TABLE)
}
index_names = {index['name'] for index in _inspector(conn).get_indexes(_TABLE)}
for index_name in ('ix_agents_workspace_updated', 'ix_agents_workspace_name'):
if index_name in index_names:
op.drop_index(index_name, table_name=_TABLE)
@@ -184,8 +163,7 @@ def downgrade() -> None:
(
foreign_key
for foreign_key in foreign_keys
if tuple(foreign_key.get('constrained_columns') or ())
== ('workspace_uuid',)
if tuple(foreign_key.get('constrained_columns') or ()) == ('workspace_uuid',)
and foreign_key.get('referred_table') == 'workspaces'
),
None,
@@ -1,4 +1,4 @@
"""merge AgentRunner and OSS Workspace migration heads
"""merge Runner and OSS Workspace migration heads
Revision ID: 0018_merge_workspace_heads
Revises: 0017_local_owner_repair, 0017_oss_workspace_identity
@@ -1,4 +1,4 @@
"""merge AgentRunner and Cloud Workspace migration heads
"""merge Runner and Cloud Workspace migration heads
Revision ID: 0020_merge_agent_cloud_heads
Revises: 0018_merge_workspace_heads, 0019_single_workspace_owner
@@ -1,4 +1,4 @@
"""merge AgentRunner and model reasoning migration heads
"""merge Runner and model reasoning migration heads
Revision ID: 0022_merge_agent_reasoning_heads
Revises: 0020_merge_agent_cloud_heads, 0021_merge_reasoning_config
@@ -0,0 +1,41 @@
"""Rename the persisted Runner state table without discarding development data.
Revision ID: 0024_unify_runner_state
Revises: 0023_drop_agent_enabled
"""
from alembic import op
import sqlalchemy as sa
revision = '0024_unify_runner_state'
down_revision = '0023_drop_agent_enabled'
branch_labels = None
depends_on = None
def _rename(source, target):
tables = sa.inspect(op.get_bind()).get_table_names()
if source in tables:
if target in tables:
metadata = sa.MetaData()
source_table = sa.Table(source, metadata, autoload_with=op.get_bind())
target_table = sa.Table(target, metadata, autoload_with=op.get_bind())
target_count = op.get_bind().scalar(sa.select(sa.func.count()).select_from(target_table))
source_count = op.get_bind().scalar(sa.select(sa.func.count()).select_from(source_table))
if not target_count:
# Startup may already have created the empty current model table.
op.drop_table(target)
elif not source_count:
op.drop_table(source)
return
else:
raise RuntimeError(f'Both {source} and {target} contain state; reconcile before migrating')
op.rename_table(source, target)
def upgrade():
_rename('agent_runner_state', 'runner_state')
def downgrade():
_rename('runner_state', 'agent_runner_state')
@@ -1,10 +1,11 @@
# Alembic script.py.mako — template for auto-generated revisions
"""add agent_runner_state table for host-owned persistent state
"""add runner_state table for host-owned persistent state
Revision ID: 6dfd3dd7f0c7
Revises: 58846a8d7a81
Create Date: 2026-05-23 19:49:08.529110
"""
from alembic import op
import sqlalchemy as sa
@@ -41,26 +42,27 @@ def _drop_index_if_exists(table_name: str, index_name: str) -> None:
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
if not _table_exists('agent_runner_state'):
op.create_table('agent_runner_state',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('runner_id', sa.String(length=255), nullable=False),
sa.Column('binding_identity', sa.String(length=255), nullable=False),
sa.Column('scope', sa.String(length=50), nullable=False),
sa.Column('scope_key', sa.String(length=512), nullable=False),
sa.Column('state_key', sa.String(length=255), nullable=False),
sa.Column('value_json', sa.Text(), nullable=True),
sa.Column('bot_id', sa.String(length=255), nullable=True),
sa.Column('workspace_id', sa.String(length=255), nullable=True),
sa.Column('conversation_id', sa.String(length=255), nullable=True),
sa.Column('thread_id', sa.String(length=255), nullable=True),
sa.Column('actor_type', sa.String(length=50), nullable=True),
sa.Column('actor_id', sa.String(length=255), nullable=True),
sa.Column('subject_type', sa.String(length=50), nullable=True),
sa.Column('subject_id', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('scope_key', 'state_key', name='uq_agent_runner_state_scope_key_state_key')
op.create_table(
'agent_runner_state',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('runner_id', sa.String(length=255), nullable=False),
sa.Column('binding_identity', sa.String(length=255), nullable=False),
sa.Column('scope', sa.String(length=50), nullable=False),
sa.Column('scope_key', sa.String(length=512), nullable=False),
sa.Column('state_key', sa.String(length=255), nullable=False),
sa.Column('value_json', sa.Text(), nullable=True),
sa.Column('bot_id', sa.String(length=255), nullable=True),
sa.Column('workspace_id', sa.String(length=255), nullable=True),
sa.Column('conversation_id', sa.String(length=255), nullable=True),
sa.Column('thread_id', sa.String(length=255), nullable=True),
sa.Column('actor_type', sa.String(length=50), nullable=True),
sa.Column('actor_id', sa.String(length=255), nullable=True),
sa.Column('subject_type', sa.String(length=50), nullable=True),
sa.Column('subject_id', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('scope_key', 'state_key', name='uq_agent_runner_state_scope_key_state_key'),
)
_create_index_if_missing('agent_runner_state', 'ix_agent_runner_state_actor_id', ['actor_id'])
_create_index_if_missing('agent_runner_state', 'ix_agent_runner_state_binding_identity', ['binding_identity'])
+1 -1
View File
@@ -42,7 +42,7 @@ class Controller:
self,
query: pipeline_query.Query,
) -> bool:
"""Offer follow-up input to an active AgentRunner before it queues."""
"""Offer follow-up input to an active Runner before it queues."""
try:
pipeline_uuid = query.pipeline_uuid
+9 -9
View File
@@ -11,7 +11,7 @@ import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
import langbot_plugin.api.entities.builtin.platform.events as platform_events
from ...pipeline.pool import get_query_execution_context
from ...agent.runner.descriptor import AgentRunnerDescriptor
from ...agent.runner.descriptor import RunnerDescriptor
from ...agent.runner.config_resolver import RunnerConfigResolver
from ...agent.runner import config_schema
from ...agent.runner.resource_policy import ResourcePolicyProjector
@@ -44,11 +44,11 @@ class PreProcessor(stage.PipelineStage):
query: pipeline_query.Query,
runner_id: str | None,
bound_plugins: list[str] | None,
) -> AgentRunnerDescriptor | None:
) -> RunnerDescriptor | None:
if not runner_id:
return None
registry = getattr(self.ap, 'agent_runner_registry', None)
registry = getattr(self.ap, 'runner_registry', None)
if registry is None:
return None
@@ -59,7 +59,7 @@ class PreProcessor(stage.PipelineStage):
bound_plugins,
)
except Exception as e:
self.ap.logger.debug(f'Unable to load AgentRunner descriptor for {runner_id}: {e}')
self.ap.logger.debug(f'Unable to load Runner descriptor for {runner_id}: {e}')
return None
async def _resolve_llm_model(
@@ -110,7 +110,7 @@ class PreProcessor(stage.PipelineStage):
ToolManager.bind_query_tool_sources(query, catalog)
return ToolManager.tools_from_catalog(catalog)
def _runner_accepts_multimodal_input(self, descriptor: AgentRunnerDescriptor | None) -> bool:
def _runner_accepts_multimodal_input(self, descriptor: RunnerDescriptor | None) -> bool:
if descriptor is None:
return True
return descriptor.capabilities.multimodal_input
@@ -123,7 +123,7 @@ class PreProcessor(stage.PipelineStage):
def _should_keep_image_inputs(
self,
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
uses_host_models: bool,
llm_model: typing.Any | None,
) -> bool:
@@ -146,7 +146,7 @@ class PreProcessor(stage.PipelineStage):
return True
return hasattr(type(persistence_mgr), 'get_db_engine')
async def _load_agent_runner_history_messages(
async def _load_runner_history_messages(
self,
runner_id: str | None,
conversation_uuid: str | None,
@@ -181,7 +181,7 @@ class PreProcessor(stage.PipelineStage):
bot_id: str | None = None,
workspace_id: str | None = None,
) -> list[provider_message.Message]:
transcript_messages = await self._load_agent_runner_history_messages(
transcript_messages = await self._load_runner_history_messages(
runner_id,
getattr(conversation, 'uuid', None),
bot_id=bot_id,
@@ -388,7 +388,7 @@ class PreProcessor(stage.PipelineStage):
query.user_message = provider_message.Message(role='user', content=content_list)
# Extract configured KB UUIDs into query variables so PromptPreProcessing
# plugins can still adjust the authorized retrieval set before run_agent.
# plugins can still adjust the authorized retrieval set before run_runner.
query.variables['_knowledge_base_uuids'] = config_schema.extract_knowledge_base_uuids(
descriptor,
runner_config,
@@ -138,7 +138,7 @@ class ChatMessageHandler(handler.MessageHandler):
has_result = False
# Use AgentRunOrchestrator to run the agent
# This replaces direct runner lookup and PluginAgentRunnerWrapper
# This replaces direct runner lookup and PluginRunnerWrapper
async for result in self.ap.agent_run_orchestrator.run_from_query(query):
has_result = True
self._check_response_size(result)
@@ -173,12 +173,8 @@ class ChatMessageHandler(handler.MessageHandler):
if is_stream:
chunk_count += 1
if chunk_count > self._response_limit(
'max_stream_chunks', 100_000
):
raise RuntimeError(
'Provider stream exceeds the configured event limit'
)
if chunk_count > self._response_limit('max_stream_chunks', 100_000):
raise RuntimeError('Provider stream exceeds the configured event limit')
# Only log every 10th chunk to reduce excessive logging during streaming.
# First chunk uses INFO level to confirm connection establishment.
if chunk_count == 1:
@@ -209,7 +205,7 @@ class ChatMessageHandler(handler.MessageHandler):
)
# Keep a conversation object available for downstream legacy
# readers, but do not mirror AgentRunner history into
# readers, but do not mirror Runner history into
# conversation.messages. TranscriptStore is the canonical
# history source for this path.
await self._ensure_conversation_for_history(query)
@@ -383,7 +379,7 @@ class ChatMessageHandler(handler.MessageHandler):
if not runner_id:
return None
registry = getattr(self.ap, 'agent_runner_registry', None)
registry = getattr(self.ap, 'runner_registry', None)
if registry is None:
return None
@@ -394,5 +390,5 @@ class ChatMessageHandler(handler.MessageHandler):
bound_plugins,
)
except Exception as e:
self.ap.logger.debug(f'Unable to load AgentRunner descriptor for {runner_id}: {e}')
self.ap.logger.debug(f'Unable to load Runner descriptor for {runner_id}: {e}')
return None
+5 -5
View File
@@ -43,13 +43,13 @@ import langbot_plugin.api.entities.builtin.platform.events as platform_events
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
from langbot_plugin.api.entities.builtin.agent_runner.event import (
from langbot_plugin.api.entities.builtin.runner.event import (
ActorContext,
SubjectContext,
RawEventRef,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
class RuntimeBot:
@@ -802,7 +802,7 @@ class RuntimeBot:
if config is None:
return None
_, runner_id, runner_config = RunnerConfigResolver.resolve_agent_runner_config(config)
_, runner_id, runner_config = RunnerConfigResolver.resolve_agent_config(config)
if not runner_id:
return None
@@ -846,7 +846,7 @@ class RuntimeBot:
return
# Legacy listeners run inside Pipeline stages. EBA handlers require an
# explicitly created and routed EventProcessor instance.
# explicitly created and routed plugin processor instance.
await self._dispatch_eba_event_to_processor(event, adapter)
async def _dispatch_eba_event_to_processor(
@@ -453,7 +453,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
# 更新历史记录中的对应消息
message_list[existing_index] = message_data
# Keep the index for the lifetime of the history entry. AgentRunner can
# Keep the index for the lifetime of the history entry. Runner can
# emit a final delta followed by message.completed/run.completed; all
# events with the same Host response id must update one UI message.
+17 -17
View File
@@ -26,7 +26,7 @@ class _RuntimeActionName:
AGENT_RUN_ADMIN_PERMISSION = 'agent_run:admin'
RUNTIME_ADMIN_PERMISSION = 'runtime:admin'
AGENT_RUNNER_ADMIN_PERMISSION = 'agent_runner:admin'
RUNNER_ADMIN_PERMISSION = 'runner:admin'
LEDGER_ONLY_SIDE_EFFECTING_RESULT_TYPES = {
'message.delta',
'message.completed',
@@ -50,15 +50,15 @@ def _normalize_permission_set(value: Any) -> set[str]:
return set()
def _iter_agent_runner_admin_plugin_configs(ap: app.Application) -> list[dict[str, Any]]:
def _iter_runner_admin_plugin_configs(ap: app.Application) -> list[dict[str, Any]]:
instance_config = getattr(ap, 'instance_config', None)
config_data = getattr(instance_config, 'data', {}) if instance_config is not None else {}
if not isinstance(config_data, dict):
return []
agent_runner_config = config_data.get('agent_runner', {})
if not isinstance(agent_runner_config, dict):
runner_config = config_data.get('runner', {})
if not isinstance(runner_config, dict):
return []
raw_admin_plugins = agent_runner_config.get('admin_plugins', [])
raw_admin_plugins = runner_config.get('admin_plugins', [])
if isinstance(raw_admin_plugins, dict):
items: list[dict[str, Any]] = []
for identity, entry in raw_admin_plugins.items():
@@ -74,12 +74,12 @@ def _iter_agent_runner_admin_plugin_configs(ap: app.Application) -> list[dict[st
return []
def _agent_runner_admin_permissions(ap: app.Application, plugin_identity: str | None) -> set[str]:
def _runner_admin_permissions(ap: app.Application, plugin_identity: str | None) -> set[str]:
if not isinstance(plugin_identity, str) or not plugin_identity.strip():
return set()
normalized_identity = plugin_identity.strip()
permissions: set[str] = set()
for entry in _iter_agent_runner_admin_plugin_configs(ap):
for entry in _iter_runner_admin_plugin_configs(ap):
if entry.get('enabled', True) is False:
continue
identity = entry.get('identity') or entry.get('plugin_identity') or entry.get('plugin') or entry.get('id')
@@ -90,19 +90,19 @@ def _agent_runner_admin_permissions(ap: app.Application, plugin_identity: str |
return permissions
def _has_agent_runner_admin_permission(
def _has_runner_admin_permission(
ap: app.Application,
plugin_identity: str | None,
permission: str,
) -> bool:
permissions = _agent_runner_admin_permissions(ap, plugin_identity)
permissions = _runner_admin_permissions(ap, plugin_identity)
if not permissions:
return False
domain = permission.split(':', 1)[0]
return bool(
permission in permissions
or f'{domain}:*' in permissions
or AGENT_RUNNER_ADMIN_PERMISSION in permissions
or RUNNER_ADMIN_PERMISSION in permissions
or '*' in permissions
)
@@ -251,11 +251,11 @@ async def _validate_agent_run_session(
allow_persistent_authorization: bool = False,
admin_permission: str | None = None,
) -> Union[tuple[None, handler.ActionResponse], tuple[Any, None]]:
"""Validate an AgentRunner pull API run session and run-scoped API access."""
"""Validate an Runner pull API run session and run-scoped API access."""
if (
not run_id
and admin_permission
and _has_agent_runner_admin_permission(
and _has_runner_admin_permission(
ap,
caller_plugin_identity,
admin_permission,
@@ -294,7 +294,7 @@ async def _validate_agent_run_session(
if api_capability:
available_apis = _get_run_authorization(session).get('available_apis', {})
has_admin_permission = bool(admin_permission) and _has_agent_runner_admin_permission(
has_admin_permission = bool(admin_permission) and _has_runner_admin_permission(
ap,
caller_plugin_identity,
admin_permission,
@@ -428,7 +428,7 @@ def _project_event_record_for_api(event: dict[str, Any]) -> dict[str, Any]:
def _project_runner_descriptor_for_api(descriptor: Any) -> dict[str, Any]:
"""Project an AgentRunnerDescriptor-like object onto a JSON dict."""
"""Project an RunnerDescriptor-like object onto a JSON dict."""
if isinstance(descriptor, dict):
return dict(descriptor)
if hasattr(descriptor, 'model_dump'):
@@ -449,7 +449,7 @@ def _project_runner_descriptor_for_api(descriptor: Any) -> dict[str, Any]:
}
async def _record_agent_runner_admin_action(
async def _record_runner_admin_action(
ap: app.Application,
store: Any,
*,
@@ -460,7 +460,7 @@ async def _record_agent_runner_admin_action(
target_runtime_id: str | None = None,
detail: dict[str, Any] | None = None,
) -> None:
"""Record a small audit trail for privileged AgentRunner operations."""
"""Record a small audit trail for privileged Runner operations."""
audit_data: dict[str, Any] = {
'action': action,
'caller_plugin_identity': caller_plugin_identity,
@@ -485,4 +485,4 @@ async def _record_agent_runner_admin_action(
metadata={'permission': permission},
)
except Exception as exc:
ap.logger.warning(f'Failed to record AgentRunner admin audit event: {exc}', exc_info=True)
ap.logger.warning(f'Failed to record Runner admin audit event: {exc}', exc_info=True)
@@ -71,7 +71,7 @@ def register(h):
await store.append_event(
event_id=None,
event_type='steering.injected',
source='agent_runner',
source='runner',
bot_id=conversation.get('bot_id') if isinstance(conversation, dict) else None,
workspace_id=conversation.get('workspace_id') if isinstance(conversation, dict) else None,
conversation_id=conversation.get('conversation_id') if isinstance(conversation, dict) else None,
+15 -15
View File
@@ -1094,8 +1094,8 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
async def initialize_plugins(self):
pass
async def _refresh_agent_runner_registry(self) -> None:
registry = getattr(self.ap, 'agent_runner_registry', None)
async def _refresh_runner_registry(self) -> None:
registry = getattr(self.ap, 'runner_registry', None)
if registry is None:
return
try:
@@ -1814,7 +1814,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
if task_context is not None:
task_context.set_current_action('refreshing plugin components')
task_context.metadata['progress_percent'] = 95
await self._refresh_agent_runner_registry()
await self._refresh_runner_registry()
if task_context is not None:
operation = task_context.metadata.get('operation')
task_context.set_current_action('plugin updated' if operation == 'upgrade' else 'plugin installed')
@@ -1903,7 +1903,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
self._workspace_installations.pop(binding.workspace_uuid, None)
if task_context is not None:
task_context.set_current_action('plugin removed')
await self._refresh_agent_runner_registry()
await self._refresh_runner_registry()
return {}
async def list_plugins(self, component_kinds: list[str] | None = None) -> list[dict[str, Any]]:
@@ -2245,9 +2245,9 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
async for ret in gen:
yield command_context.CommandReturn.model_validate(ret)
# AgentRunner methods
async def list_agent_runners(self, bound_plugins: list[str] | None = None) -> list[dict[str, Any]]:
"""List all available AgentRunner components.
# Runner methods
async def list_runners(self, bound_plugins: list[str] | None = None) -> list[dict[str, Any]]:
"""List all available Runner components.
Returns list of dicts with plugin_author, plugin_name, runner_name, manifest, etc.
"""
@@ -2260,26 +2260,26 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
runners: list[dict[str, Any]] = []
for binding in await self._operation_bindings(include_plugins=bound_plugins):
with runtime_handler.installation_scope(binding):
runners.extend(await runtime_handler.list_agent_runners(include_plugins=bound_plugins))
runners.extend(await runtime_handler.list_runners(include_plugins=bound_plugins))
return runners
async def run_agent(
async def run_runner(
self,
plugin_author: str,
plugin_name: str,
runner_name: str,
context: dict[str, Any],
) -> typing.AsyncGenerator[dict[str, Any], None]:
"""Run an AgentRunner from a plugin.
"""Run an Runner from a plugin.
Args:
plugin_author: Plugin author
plugin_name: Plugin name
runner_name: AgentRunner component name
context: AgentRunContext as dict
runner_name: Runner component name
context: RunnerContext as dict
Yields:
AgentRunResult dicts
RunnerResult dicts
"""
if not self.is_enable_plugin:
# Return a protocol-level failure result.
@@ -2297,7 +2297,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
'metadata', {}
).get('workspace_id')
if not isinstance(workspace_id, str) or not workspace_id.strip():
raise ValueError('AgentRunner execution requires a Workspace')
raise ValueError('Runner execution requires a Workspace')
execution_context = await self._current_execution_context()
if workspace_id.strip() != execution_context.workspace_uuid:
raise WorkspaceNotFoundError('Plugin resource not found')
@@ -2309,7 +2309,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
)
runtime_handler = self._runtime_handler()
with runtime_handler.installation_scope(binding):
async for ret in runtime_handler.run_agent(
async for ret in runtime_handler.run_runner(
plugin_author,
plugin_name,
runner_name,
+37 -37
View File
@@ -56,7 +56,7 @@ from ..agent.runner.platform_tools import execute_platform_tool, get_platform_to
from ..pipeline.pool import get_query_execution_context
from . import agent_pull_actions, agent_runner_actions, agent_state_actions
from . import agent_pull_actions, runner_actions, agent_state_actions
from .agent_run_support import (
_validate_agent_run_session,
)
@@ -177,7 +177,7 @@ async def _get_pipeline_knowledge_base_uuids(ap: app.Application, query: Any) ->
return []
runner_config = RunnerConfigResolver.resolve_runner_config(pipeline_config, runner_id)
registry = getattr(ap, 'agent_runner_registry', None)
registry = getattr(ap, 'runner_registry', None)
if registry is None:
return []
@@ -189,7 +189,7 @@ async def _get_pipeline_knowledge_base_uuids(ap: app.Application, query: Any) ->
bound_plugins,
)
except Exception as e:
ap.logger.warning(f'Failed to load AgentRunner descriptor for knowledge-base scope: {e}')
ap.logger.warning(f'Failed to load Runner descriptor for knowledge-base scope: {e}')
return []
return config_schema.extract_knowledge_base_uuids(descriptor, runner_config)
@@ -1275,7 +1275,7 @@ class RuntimeConnectionHandler(handler.Handler):
async def count_tokens(data: dict[str, Any]) -> handler.ActionResponse:
"""Count model input tokens.
For AgentRunner calls: requires run_id and validates model_uuid against session.resources.models.
For Runner calls: requires run_id and validates model_uuid against session.resources.models.
For regular plugin calls: no run_id, unrestricted access (backward compatibility).
"""
action_context, _ = await self._require_plugin_action_context()
@@ -1338,7 +1338,7 @@ class RuntimeConnectionHandler(handler.Handler):
async def invoke_llm(data: dict[str, Any]) -> handler.ActionResponse:
"""Invoke llm
For AgentRunner calls: requires run_id and validates model_uuid against session.resources.models.
For Runner calls: requires run_id and validates model_uuid against session.resources.models.
For regular plugin calls: no run_id, unrestricted access (backward compatibility).
"""
action_context, _ = await self._require_plugin_action_context()
@@ -1346,11 +1346,11 @@ class RuntimeConnectionHandler(handler.Handler):
messages = data['messages']
funcs = data.get('funcs', [])
extra_args = data.get('extra_args', {})
run_id = data.get('run_id') # Optional: present for AgentRunner calls
run_id = data.get('run_id') # Optional: present for Runner calls
caller_plugin_identity = data.get('caller_plugin_identity') # Optional: for cross-plugin validation
session = None
# Permission validation for AgentRunner calls
# Permission validation for Runner calls
if run_id:
session, error = await _validate_run_authorization(
run_id, 'model', llm_model_uuid, self.ap, caller_plugin_identity, operation='invoke'
@@ -1431,7 +1431,7 @@ class RuntimeConnectionHandler(handler.Handler):
async def invoke_llm_stream(data: dict[str, Any]):
"""Invoke llm with streaming response
For AgentRunner calls: requires run_id and validates model_uuid against session.resources.models.
For Runner calls: requires run_id and validates model_uuid against session.resources.models.
For regular plugin calls: no run_id, unrestricted access (backward compatibility).
"""
action_context, _ = await self._require_plugin_action_context()
@@ -1439,11 +1439,11 @@ class RuntimeConnectionHandler(handler.Handler):
messages = data['messages']
funcs = data.get('funcs', [])
extra_args = data.get('extra_args', {})
run_id = data.get('run_id') # Optional: present for AgentRunner calls
run_id = data.get('run_id') # Optional: present for Runner calls
caller_plugin_identity = data.get('caller_plugin_identity') # Optional: for cross-plugin validation
session = None
# Permission validation for AgentRunner calls
# Permission validation for Runner calls
if run_id:
session, error = await _validate_run_authorization(
run_id, 'model', llm_model_uuid, self.ap, caller_plugin_identity, operation='stream'
@@ -1565,27 +1565,27 @@ class RuntimeConnectionHandler(handler.Handler):
async def call_tool(data: dict[str, Any]) -> handler.ActionResponse:
"""Call a tool
For AgentRunner calls: requires run_id and validates tool_name against session.resources.tools.
For Runner calls: requires run_id and validates tool_name against session.resources.tools.
For regular plugin calls: no run_id, unrestricted access (backward compatibility).
"""
tool_name = data['tool_name']
run_id = data.get('run_id') # Optional: present for AgentRunner calls
run_id = data.get('run_id') # Optional: present for Runner calls
caller_plugin_identity = data.get('caller_plugin_identity') # Optional: for cross-plugin validation
session = None
source_ref = None
is_agent_runner_call = bool(run_id)
is_runner_call = bool(run_id)
action_context = self._require_runtime_action_context()
if is_agent_runner_call:
if is_runner_call:
if 'parameters' not in data:
return handler.ActionResponse.error(
message='parameters is required for AgentRunner tool calls',
message='parameters is required for Runner tool calls',
)
parameters = data.get('parameters') or {}
else:
parameters = data.get('tool_parameters') or {}
# Permission validation for AgentRunner calls
# Permission validation for Runner calls
if run_id:
session, error = await _validate_run_authorization(
run_id, 'tool', tool_name, self.ap, caller_plugin_identity, operation='call'
@@ -1627,7 +1627,7 @@ class RuntimeConnectionHandler(handler.Handler):
result = await self.ap.tool_mgr.execute_func_call(
**execute_kwargs,
)
if is_agent_runner_call:
if is_runner_call:
return handler.ActionResponse.success(data={'result': result})
return handler.ActionResponse.success(data={'tool_response': result})
except Exception as e:
@@ -1640,19 +1640,19 @@ class RuntimeConnectionHandler(handler.Handler):
async def get_tool_detail(data: dict[str, Any]) -> handler.ActionResponse:
"""Get tool detail for LLM function calling.
For AgentRunner calls: requires run_id and validates tool_name against session.resources.tools.
For Runner calls: requires run_id and validates tool_name against session.resources.tools.
For regular plugin calls: no run_id, unrestricted access (backward compatibility).
Returns tool manifest including name, description, and parameters schema.
"""
tool_name = data['tool_name']
run_id = data.get('run_id') # Optional: present for AgentRunner calls
run_id = data.get('run_id') # Optional: present for Runner calls
caller_plugin_identity = data.get('caller_plugin_identity') # Optional: for cross-plugin validation
session = None
source_ref = None
action_context = self._require_runtime_action_context()
# Permission validation for AgentRunner calls
# Permission validation for Runner calls
if run_id:
session, error = await _validate_run_authorization(
run_id, 'tool', tool_name, self.ap, caller_plugin_identity, operation='detail'
@@ -1693,10 +1693,10 @@ class RuntimeConnectionHandler(handler.Handler):
# ================= Binary Storage Handlers =================
# Permission validation:
# - For AgentRunner calls (with run_id): validates storage permission via session_registry
# - For Runner calls (with run_id): validates storage permission via session_registry
# - For regular plugin calls (no run_id): unrestricted access (backward compatibility)
# - Plugin storage: inherent isolation via owner = plugin identity (set by SDK runtime)
# - Workspace storage: requires ctx.resources.storage.workspace_storage for AgentRunner
# - Workspace storage: requires ctx.resources.storage.workspace_storage for Runner
@self.action(RuntimeToLangBotAction.SET_BINARY_STORAGE)
async def set_binary_storage(data: dict[str, Any]) -> handler.ActionResponse:
@@ -2329,10 +2329,10 @@ class RuntimeConnectionHandler(handler.Handler):
async def retrieve_knowledge(data: dict[str, Any]) -> handler.ActionResponse:
"""Retrieve documents from any knowledge base.
For AgentRunner calls: requires run_id and validates kb_id against session.resources.knowledge_bases.
For Runner calls: requires run_id and validates kb_id against session.resources.knowledge_bases.
For regular plugin calls: no run_id, unrestricted access (backward compatibility).
Note: SDK AgentRunAPIProxy.retrieve_knowledge calls this action with run_id.
Note: SDK RunnerAPIProxy.retrieve_knowledge calls this action with run_id.
"""
action_context, _ = await self._require_plugin_action_context()
execution_context = self._execution_context(action_context)
@@ -2340,10 +2340,10 @@ class RuntimeConnectionHandler(handler.Handler):
query_text = data['query_text']
top_k = data.get('top_k', 5)
filters = data.get('filters') or {}
run_id = data.get('run_id') # Optional: present for AgentRunner calls
run_id = data.get('run_id') # Optional: present for Runner calls
caller_plugin_identity = data.get('caller_plugin_identity') # Optional: for cross-plugin validation
# Permission validation for AgentRunner calls
# Permission validation for Runner calls
if run_id:
session, error = await _validate_run_authorization(
run_id, 'knowledge_base', kb_id, self.ap, caller_plugin_identity, operation='retrieve'
@@ -2410,11 +2410,11 @@ class RuntimeConnectionHandler(handler.Handler):
async def retrieve_knowledge_base(data: dict[str, Any]) -> handler.ActionResponse:
"""Retrieve documents from a knowledge base within the current run or query scope.
For AgentRunner calls: requires run_id and validates kb_id against session.resources.knowledge_bases.
For Runner calls: requires run_id and validates kb_id against session.resources.knowledge_bases.
For regular plugin calls: no run_id, validates against pipeline's configured knowledge bases.
Note: This action has dual validation paths:
- AgentRunner: uses session_registry for permission check
- Runner: uses session_registry for permission check
- Regular plugin: uses RunnerConfigResolver.resolve_runner_config for pipeline-level check
"""
action_context, _ = await self._require_plugin_action_context()
@@ -2424,12 +2424,12 @@ class RuntimeConnectionHandler(handler.Handler):
query_text = data['query_text']
top_k = data.get('top_k', 5)
filters = data.get('filters') or {}
run_id = data.get('run_id') # Optional: present for AgentRunner calls
run_id = data.get('run_id') # Optional: present for Runner calls
caller_plugin_identity = data.get('caller_plugin_identity') # Optional: for cross-plugin validation
session = None
query = None
# Permission validation for AgentRunner calls
# Permission validation for Runner calls
if run_id:
session, error = await _validate_run_authorization(
run_id, 'knowledge_base', kb_id, self.ap, caller_plugin_identity, operation='retrieve'
@@ -2535,7 +2535,7 @@ class RuntimeConnectionHandler(handler.Handler):
)
agent_pull_actions.register(self)
agent_runner_actions.register(self)
runner_actions.register(self)
agent_state_actions.register(self)
@self.action(CommonAction.PING)
@@ -2834,7 +2834,7 @@ class RuntimeConnectionHandler(handler.Handler):
return result['tools']
async def list_agent_runners(self, include_plugins: list[str] | None = None) -> list[dict[str, Any]]:
async def list_runners(self, include_plugins: list[str] | None = None) -> list[dict[str, Any]]:
"""List agent runners from plugin runtime.
Returns list of dicts with:
@@ -2844,7 +2844,7 @@ class RuntimeConnectionHandler(handler.Handler):
- manifest
"""
result = await self.call_action(
LangBotToRuntimeAction.LIST_AGENT_RUNNERS,
LangBotToRuntimeAction.LIST_RUNNERS,
{
'include_plugins': include_plugins,
},
@@ -2853,20 +2853,20 @@ class RuntimeConnectionHandler(handler.Handler):
return result['runners']
async def run_agent(
async def run_runner(
self,
plugin_author: str,
plugin_name: str,
runner_name: str,
context: dict[str, Any],
) -> typing.AsyncGenerator[dict[str, Any], None]:
"""Run an AgentRunner component.
"""Run an Runner component.
Yields AgentRunResult dicts.
Yields RunnerResult dicts.
"""
timeout = self._get_runner_action_timeout(context)
gen = self.call_action_generator(
LangBotToRuntimeAction.RUN_AGENT,
LangBotToRuntimeAction.RUN_RUNNER,
{
'plugin_author': plugin_author,
'plugin_name': plugin_name,
@@ -16,7 +16,7 @@ from .agent_run_support import (
AGENT_RUN_ADMIN_PERMISSION,
RUNTIME_ADMIN_PERMISSION,
_plugin_runtime_action,
_has_agent_runner_admin_permission,
_has_runner_admin_permission,
_deadline_seconds_from_payload,
_get_run_authorization,
_authorize_target_run,
@@ -27,7 +27,7 @@ from .agent_run_support import (
_run_scope_filters,
_run_ledger_scope_filters,
_project_runner_descriptor_for_api,
_record_agent_runner_admin_action,
_record_runner_admin_action,
)
@@ -38,7 +38,7 @@ def register(h):
run_id = data.get('run_id')
target_run_id = data.get('target_run_id') or run_id
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -74,7 +74,7 @@ def register(h):
if auth_error:
return auth_error
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_get',
@@ -96,7 +96,7 @@ def register(h):
before_cursor = data.get('before_cursor')
limit = data.get('limit', 50)
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -159,7 +159,7 @@ def register(h):
**scope_filters,
)
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_list',
@@ -185,10 +185,10 @@ def register(h):
@h.action(_plugin_runtime_action('RUNNER_LIST', 'runner_list'))
async def runner_list(data: dict[str, Any]) -> handler.ActionResponse:
"""List Host-discovered AgentRunner descriptors."""
"""List Host-discovered Runner descriptors."""
run_id = data.get('run_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -213,7 +213,7 @@ def register(h):
if include_plugins is not None and not isinstance(include_plugins, list):
return handler.ActionResponse.error(message='include_plugins must be a list')
registry = getattr(h.ap, 'agent_runner_registry', None)
registry = getattr(h.ap, 'runner_registry', None)
if registry is None:
return handler.ActionResponse.success(data={'items': []})
@@ -231,7 +231,7 @@ def register(h):
)
items = [_project_runner_descriptor_for_api(item) for item in runners]
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
None,
action='runner_list',
@@ -257,7 +257,7 @@ def register(h):
limit = data.get('limit', 50)
direction = data.get('direction', 'forward')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -307,7 +307,7 @@ def register(h):
direction=str(direction or 'forward'),
)
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_events_page',
@@ -333,7 +333,7 @@ def register(h):
run_id = data.get('run_id')
target_run_id = data.get('target_run_id') or run_id
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -376,7 +376,7 @@ def register(h):
if not updated:
return handler.ActionResponse.error(message=f'Run {target_run_id} not found')
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_cancel',
@@ -397,7 +397,7 @@ def register(h):
target_run_id = data.get('target_run_id') or run_id
caller_plugin_identity = data.get('caller_plugin_identity')
result = data.get('result') if isinstance(data.get('result'), dict) else {}
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -479,7 +479,7 @@ def register(h):
metadata=metadata,
)
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_append_result',
@@ -500,7 +500,7 @@ def register(h):
target_run_id = data.get('target_run_id') or run_id
caller_plugin_identity = data.get('caller_plugin_identity')
status = data.get('status')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -558,7 +558,7 @@ def register(h):
if not updated:
return handler.ActionResponse.error(message=f'Run {target_run_id} not found')
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_finalize',
@@ -578,7 +578,7 @@ def register(h):
run_id = data.get('run_id')
runtime_id = data.get('runtime_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -617,7 +617,7 @@ def register(h):
heartbeat_deadline_seconds=_deadline_seconds_from_payload(data),
)
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='runtime_register',
@@ -637,7 +637,7 @@ def register(h):
run_id = data.get('run_id')
runtime_id = data.get('runtime_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -675,7 +675,7 @@ def register(h):
if runtime is None:
return handler.ActionResponse.error(message=f'Runtime {runtime_id} not found')
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='runtime_heartbeat',
@@ -694,7 +694,7 @@ def register(h):
"""List Host-owned runtime registry records."""
run_id = data.get('run_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -730,7 +730,7 @@ def register(h):
limit=data.get('limit', 50),
)
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='runtime_list',
@@ -759,7 +759,7 @@ def register(h):
"""Reconcile stale runtime heartbeats and expired claim leases."""
run_id = data.get('run_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -796,7 +796,7 @@ def register(h):
)
released_claims = await store.release_expired_claims()
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='runtime_reconcile',
@@ -824,7 +824,7 @@ def register(h):
"""Get run statistics within a time window (admin-only)."""
run_id = data.get('run_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -858,7 +858,7 @@ def register(h):
end_time=end_time,
runner_id=runner_id,
)
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_stats',
@@ -880,7 +880,7 @@ def register(h):
"""Get runtime registry statistics (admin-only)."""
run_id = data.get('run_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -906,7 +906,7 @@ def register(h):
try:
stats = await store.get_runtime_stats()
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='runtime_stats',
@@ -924,7 +924,7 @@ def register(h):
"""Get runner-aggregated statistics (admin-only)."""
run_id = data.get('run_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
AGENT_RUN_ADMIN_PERMISSION,
@@ -958,7 +958,7 @@ def register(h):
end_time=end_time,
limit=limit,
)
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='runner_stats',
@@ -981,7 +981,7 @@ def register(h):
run_id = data.get('run_id')
runtime_id = data.get('runtime_id')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -1035,7 +1035,7 @@ def register(h):
if run is None:
return handler.ActionResponse.error(message='No queued run available')
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_claim',
@@ -1061,7 +1061,7 @@ def register(h):
runtime_id = data.get('runtime_id')
claim_token = data.get('claim_token')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -1108,7 +1108,7 @@ def register(h):
if run is None:
return handler.ActionResponse.error(message=f'Run claim {target_run_id} not found')
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_renew_claim',
@@ -1131,7 +1131,7 @@ def register(h):
runtime_id = data.get('runtime_id')
claim_token = data.get('claim_token')
caller_plugin_identity = data.get('caller_plugin_identity')
is_admin = _has_agent_runner_admin_permission(
is_admin = _has_runner_admin_permission(
h.ap,
caller_plugin_identity,
RUNTIME_ADMIN_PERMISSION,
@@ -1184,7 +1184,7 @@ def register(h):
if run is None:
return handler.ActionResponse.error(message=f'Run claim {target_run_id} not found')
if is_admin:
await _record_agent_runner_admin_action(
await _record_runner_admin_action(
h.ap,
store,
action='run_release_claim',
+2 -2
View File
@@ -286,9 +286,9 @@ plugin:
binary_storage:
# Max bytes for a single plugin binary storage value
max_value_bytes: 10485760
agent_runner:
runner:
# Host-level admin permissions for trusted control plugins. These plugins
# can use existing plugin action handlers to inspect or manage AgentRunner
# can use existing plugin action handlers to inspect or manage Runner
# infrastructure across runner/plugin boundaries. Keep empty unless you
# fully trust the plugin identity.
#
@@ -17,7 +17,7 @@ stages:
zh_Hans: 运行器
type: select
required: true
# Options and default are dynamically populated from AgentRunnerRegistry
# Options and default are dynamically populated from RunnerRegistry
- name: expire-time
label:
en_US: Conversation expire time (seconds)
@@ -38,6 +38,6 @@ stages:
type: integer
required: true
default: 0
# Runner config stages are dynamically added from AgentRunnerRegistry
# Runner config stages are dynamically added from RunnerRegistry
# Each plugin runner's config schema is added as a separate stage
# The stage name matches the runner id for frontend matching