mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(runner): unify plugin execution across agents and event processors
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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', {})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-8
@@ -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'),
|
||||
)
|
||||
@@ -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')
|
||||
+23
-21
@@ -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'])
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+38
-38
@@ -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',
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user