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

This commit is contained in:
RockChinQ
2026-09-10 18:04:38 +08:00
parent 8903a40c41
commit f24a7c9bb2
223 changed files with 4090 additions and 3067 deletions
+8 -8
View File
@@ -2,35 +2,35 @@
from __future__ import annotations
from .runner.descriptor import AgentRunnerDescriptor
from .runner.descriptor import RunnerDescriptor
from .runner.id import parse_runner_id, format_runner_id, RunnerIdParts, is_plugin_runner_id
from .runner.errors import (
AgentRunnerError,
RunnerError,
RunnerNotFoundError,
RunnerNotAuthorizedError,
RunnerProtocolError,
RunnerExecutionError,
)
from .runner.registry import AgentRunnerRegistry
from .runner.context_builder import AgentRunContextBuilder
from .runner.registry import RunnerRegistry
from .runner.context_builder import RunnerContextBuilder
from .runner.resource_builder import AgentResourceBuilder
from .runner.result_normalizer import AgentResultNormalizer
from .runner.orchestrator import AgentRunOrchestrator
from .runner.config_resolver import RunnerConfigResolver
__all__ = [
'AgentRunnerDescriptor',
'RunnerDescriptor',
'parse_runner_id',
'format_runner_id',
'is_plugin_runner_id',
'RunnerIdParts',
'AgentRunnerError',
'RunnerError',
'RunnerNotFoundError',
'RunnerNotAuthorizedError',
'RunnerProtocolError',
'RunnerExecutionError',
'AgentRunnerRegistry',
'AgentRunContextBuilder',
'RunnerRegistry',
'RunnerContextBuilder',
'AgentResourceBuilder',
'AgentResultNormalizer',
'AgentRunOrchestrator',
+10 -10
View File
@@ -2,22 +2,22 @@
from __future__ import annotations
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .id import parse_runner_id, format_runner_id, RunnerIdParts
from .errors import (
AgentRunnerError,
RunnerError,
RunnerNotFoundError,
RunnerNotAuthorizedError,
RunnerProtocolError,
RunnerExecutionError,
)
from .registry import AgentRunnerRegistry
from .context_builder import AgentRunContextBuilder
from .registry import RunnerRegistry
from .context_builder import RunnerContextBuilder
from .resource_builder import AgentResourceBuilder
from .result_normalizer import AgentResultNormalizer
from .orchestrator import AgentRunOrchestrator
from .config_resolver import RunnerConfigResolver
from .default_config import AgentRunnerDefaultConfigService
from .default_config import RunnerDefaultConfigService
from .binding_resolver import AgentBindingResolver, AgentBindingResolutionError
from .session_registry import (
AgentRunSessionRegistry,
@@ -35,22 +35,22 @@ from .events import (
)
__all__ = [
'AgentRunnerDescriptor',
'RunnerDescriptor',
'parse_runner_id',
'format_runner_id',
'RunnerIdParts',
'AgentRunnerError',
'RunnerError',
'RunnerNotFoundError',
'RunnerNotAuthorizedError',
'RunnerProtocolError',
'RunnerExecutionError',
'AgentRunnerRegistry',
'AgentRunContextBuilder',
'RunnerRegistry',
'RunnerContextBuilder',
'AgentResourceBuilder',
'AgentResultNormalizer',
'AgentRunOrchestrator',
'RunnerConfigResolver',
'AgentRunnerDefaultConfigService',
'RunnerDefaultConfigService',
'AgentBindingResolver',
'AgentBindingResolutionError',
'AgentRunSessionRegistry',
@@ -1,4 +1,4 @@
"""Resolve the current AgentRunner configuration shape."""
"""Resolve the current Runner configuration shape."""
from __future__ import annotations
@@ -12,7 +12,7 @@ HOST_SECURITY_BOOLEAN_FIELDS = (
class RunnerConfigResolver:
"""Configuration helpers for the current AgentRunner shape.
"""Configuration helpers for the current Runner shape.
Responsibilities:
- Resolve runner ID from ai.runner.id
@@ -131,20 +131,20 @@ class RunnerConfigResolver:
return config
@staticmethod
def resolve_agent_runner_id(config: dict[str, typing.Any]) -> str | None:
def resolve_agent_id(config: dict[str, typing.Any]) -> str | None:
"""Resolve a runner ID from a validated persisted Agent config."""
runner = config.get('runner', {})
runner_id = runner.get('id') if isinstance(runner, dict) else None
return runner_id if isinstance(runner_id, str) and runner_id else None
@classmethod
def resolve_agent_runner_config(
def resolve_agent_config(
cls,
config: typing.Any,
) -> tuple[dict[str, typing.Any], str | None, dict[str, typing.Any]]:
"""Validate an Agent config and return its selected runner configuration."""
validated = cls.validate_agent_config(config)
runner_id = cls.resolve_agent_runner_id(validated)
runner_id = cls.resolve_agent_id(validated)
runner_configs = typing.cast(dict[str, typing.Any], validated['runner_config'])
runner_config = runner_configs.get(runner_id, {}) if runner_id else {}
return validated, runner_id, typing.cast(dict[str, typing.Any], runner_config)
+14 -15
View File
@@ -1,9 +1,10 @@
"""Helpers for interpreting AgentRunner DynamicForm configuration."""
"""Helpers for interpreting Runner DynamicForm configuration."""
from __future__ import annotations
import typing
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
FORM_ITEM_TYPE_ALIASES = {
@@ -24,7 +25,7 @@ def normalize_schema_item_type(item_type: typing.Any) -> typing.Any:
def iter_schema_items(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
field_types: set[str],
) -> typing.Iterator[dict[str, typing.Any]]:
"""Yield descriptor config schema items whose type is in field_types."""
@@ -37,22 +38,22 @@ def iter_schema_items(
yield item
def uses_host_models(descriptor: AgentRunnerDescriptor | None) -> bool:
def uses_host_models(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether LangBot should resolve model resources for this runner."""
return any(True for _ in iter_schema_items(descriptor, LLM_MODEL_SELECTOR_TYPES))
def uses_host_tools(descriptor: AgentRunnerDescriptor | None) -> bool:
def uses_host_tools(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether LangBot should expose tool resources to this runner."""
return descriptor is not None and descriptor.supports_tool_calling()
def uses_host_knowledge_bases(descriptor: AgentRunnerDescriptor | None) -> bool:
def uses_host_knowledge_bases(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether LangBot should expose knowledge-base resources to this runner."""
return descriptor is not None and descriptor.supports_knowledge_retrieval()
def supports_skill_authoring(descriptor: AgentRunnerDescriptor | None) -> bool:
def supports_skill_authoring(descriptor: RunnerDescriptor | None) -> bool:
"""Return whether the runner wants Host skill-authoring tools."""
if descriptor is None:
return False
@@ -60,7 +61,7 @@ def supports_skill_authoring(descriptor: AgentRunnerDescriptor | None) -> bool:
def extract_prompt_config(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
runner_config: dict[str, typing.Any],
default_prompt: list[dict[str, typing.Any]],
) -> list[dict[str, typing.Any]]:
@@ -78,7 +79,7 @@ def extract_prompt_config(
def extract_model_selection(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
runner_config: dict[str, typing.Any],
) -> tuple[str, list[str]]:
"""Extract primary/fallback LLM selections from schema-defined fields."""
@@ -110,7 +111,7 @@ def extract_model_selection(
def extract_knowledge_base_uuids(
descriptor: AgentRunnerDescriptor | None,
descriptor: RunnerDescriptor | None,
runner_config: dict[str, typing.Any],
) -> list[str]:
"""Extract configured knowledge-base UUIDs from schema-defined fields."""
@@ -124,15 +125,13 @@ def extract_knowledge_base_uuids(
continue
value = runner_config.get(field_name, item.get('default', []))
if isinstance(value, list):
kb_uuids.extend(
kb_uuid for kb_uuid in value if isinstance(kb_uuid, str) and kb_uuid not in NONE_SENTINELS
)
kb_uuids.extend(kb_uuid for kb_uuid in value if isinstance(kb_uuid, str) and kb_uuid not in NONE_SENTINELS)
return list(dict.fromkeys(kb_uuids))
def iter_config_model_refs(
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> typing.Iterator[tuple[str, str]]:
"""Yield model references declared by schema-defined model selector fields."""
@@ -167,7 +166,7 @@ def iter_config_model_refs(
def set_empty_llm_model_selection(
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
model_uuid: str,
) -> bool:
+13 -13
View File
@@ -1,4 +1,4 @@
"""Agent run context builder for provisioning AgentRunContext envelopes."""
"""Agent run context builder for provisioning RunnerContext envelopes."""
from __future__ import annotations
@@ -7,7 +7,7 @@ import time
import typing
from ...core import app
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .persistent_state_store import get_persistent_state_store
from .host_models import AgentEventEnvelope, AgentBinding
@@ -125,10 +125,10 @@ class AgentRuntimeContext(typing.TypedDict):
metadata: dict[str, typing.Any]
class AgentRunContextPayload(typing.TypedDict):
"""AgentRunContext payload passed to an agent runner.
class RunnerContextPayload(typing.TypedDict):
"""RunnerContext payload passed to an agent runner.
Protocol v1 structure - matches SDK AgentRunContext.
Protocol v1 structure - matches SDK RunnerContext.
Note: The 'config' field contains the current Agent/runner config
from ai.runner_config[runner_id] while the current Query entry remains
@@ -152,8 +152,8 @@ class AgentRunContextPayload(typing.TypedDict):
metadata: dict[str, typing.Any] # Additional metadata
class AgentRunContextBuilder:
"""Builder for provisioning AgentRunContext.
class RunnerContextBuilder:
"""Builder for provisioning RunnerContext.
Responsibilities:
- Generate new run_id (UUID, not query id)
@@ -222,10 +222,10 @@ class AgentRunContextBuilder:
self,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
resources: AgentResources,
) -> AgentRunContextPayload:
"""Build AgentRunContext from event-first envelope.
) -> RunnerContextPayload:
"""Build RunnerContext from event-first envelope.
This is the main entry point for Protocol v1.
Does NOT inline full history by default.
@@ -237,7 +237,7 @@ class AgentRunContextBuilder:
resources: Built resources
Returns:
AgentRunContextPayload for the runner
RunnerContextPayload for the runner
"""
# Generate new run_id
run_id = str(uuid.uuid4())
@@ -351,7 +351,7 @@ class AgentRunContextBuilder:
}
# Build full context - Protocol v1 structure
context: AgentRunContextPayload = {
context: RunnerContextPayload = {
'run_id': run_id,
'trigger': trigger,
'conversation': conversation,
@@ -397,7 +397,7 @@ class AgentRunContextBuilder:
async def _build_context_access(
self,
event: AgentEventEnvelope,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
binding: AgentBinding | None = None,
) -> dict[str, typing.Any]:
"""Build ContextAccess with actual values from stores.
@@ -1,4 +1,4 @@
"""Default AgentRunner binding configuration helpers."""
"""Default Runner binding configuration helpers."""
from __future__ import annotations
@@ -11,8 +11,8 @@ from . import config_schema
from .config_resolver import RunnerConfigResolver
class AgentRunnerDefaultConfigService:
"""Apply AgentRunner schema-defined defaults to host binding config."""
class RunnerDefaultConfigService:
"""Apply Runner schema-defined defaults to host binding config."""
ap: app.Application
@@ -20,7 +20,7 @@ class AgentRunnerDefaultConfigService:
self.ap = ap
async def _get_runner_descriptor(self, context: TenantContext, runner_id: str):
registry = getattr(self.ap, 'agent_runner_registry', None)
registry = getattr(self.ap, 'runner_registry', None)
if registry is None:
return None
try:
@@ -28,7 +28,7 @@ class AgentRunnerDefaultConfigService:
except Exception as e:
logger = getattr(self.ap, 'logger', None)
if logger:
logger.warning(f'Failed to load AgentRunner descriptor while setting default model: {e}')
logger.warning(f'Failed to load Runner descriptor while setting default model: {e}')
return None
async def auto_set_default_pipeline_llm_model(
@@ -39,8 +39,7 @@ class AgentRunnerDefaultConfigService:
"""Set model_uuid into the default pipeline runner config when the selector is empty."""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
persistence_pipeline.LegacyPipeline.workspace_uuid
== require_workspace_uuid(context),
persistence_pipeline.LegacyPipeline.workspace_uuid == require_workspace_uuid(context),
persistence_pipeline.LegacyPipeline.is_default == True,
)
)
+9 -8
View File
@@ -5,13 +5,13 @@ from __future__ import annotations
import typing
import pydantic
from langbot_plugin.api.entities.builtin.agent_runner.manifest import (
AgentRunnerCapabilities,
AgentRunnerPermissions,
from langbot_plugin.api.entities.builtin.runner.manifest import (
RunnerCapabilities,
RunnerPermissions,
)
class AgentRunnerDescriptor(pydantic.BaseModel):
class RunnerDescriptor(pydantic.BaseModel):
"""Descriptor for an agent runner.
Represents the discovered metadata for a runner, including
@@ -37,7 +37,7 @@ class AgentRunnerDescriptor(pydantic.BaseModel):
"""Plugin name from manifest"""
runner_name: str
"""AgentRunner component name from manifest"""
"""Runner component name from manifest"""
plugin_version: str | None = None
"""Optional plugin version"""
@@ -45,16 +45,17 @@ class AgentRunnerDescriptor(pydantic.BaseModel):
config_schema: list[dict[str, typing.Any]] = pydantic.Field(default_factory=list)
"""Configuration schema using DynamicForm format"""
capabilities: AgentRunnerCapabilities = pydantic.Field(default_factory=AgentRunnerCapabilities)
capabilities: RunnerCapabilities = pydantic.Field(default_factory=RunnerCapabilities)
"""Runner capabilities: streaming, tool_calling, knowledge_retrieval, etc."""
permissions: AgentRunnerPermissions = pydantic.Field(default_factory=AgentRunnerPermissions)
permissions: RunnerPermissions = pydantic.Field(default_factory=RunnerPermissions)
"""Requested LangBot resource permissions."""
raw_manifest: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
"""Original manifest for reference"""
component_kind: typing.Literal['AgentRunner', 'EventProcessor'] = 'AgentRunner'
component_kind: typing.Literal['Runner'] = 'Runner'
usages: list[typing.Literal['agent', 'event']] = pydantic.Field(default_factory=lambda: ['agent'])
supported_event_patterns: list[str] = pydantic.Field(default_factory=lambda: ['*'])
model_config = pydantic.ConfigDict(
+5 -5
View File
@@ -3,13 +3,13 @@
from __future__ import annotations
class AgentRunnerError(Exception):
class RunnerError(Exception):
"""Base error for agent runner operations."""
pass
class RunnerNotFoundError(AgentRunnerError):
class RunnerNotFoundError(RunnerError):
"""Runner not found in registry."""
def __init__(self, runner_id: str):
@@ -17,7 +17,7 @@ class RunnerNotFoundError(AgentRunnerError):
super().__init__(f'Agent runner not found: {runner_id}')
class RunnerNotAuthorizedError(AgentRunnerError):
class RunnerNotAuthorizedError(RunnerError):
"""Runner not authorized for this binding."""
def __init__(self, runner_id: str, bound_plugins: list[str] | None):
@@ -26,7 +26,7 @@ class RunnerNotAuthorizedError(AgentRunnerError):
super().__init__(f'Agent runner {runner_id} not authorized for bound_plugins={bound_plugins}')
class RunnerProtocolError(AgentRunnerError):
class RunnerProtocolError(RunnerError):
"""Runner protocol version mismatch or invalid manifest."""
def __init__(self, runner_id: str, message: str):
@@ -34,7 +34,7 @@ class RunnerProtocolError(AgentRunnerError):
super().__init__(f'Agent runner protocol error for {runner_id}: {message}')
class RunnerExecutionError(AgentRunnerError):
class RunnerExecutionError(RunnerError):
"""Runner execution failed."""
def __init__(
+1 -1
View File
@@ -1,4 +1,4 @@
"""Canonical AgentRunner event names reserved for future EBA integration."""
"""Canonical Runner event names reserved for future EBA integration."""
from __future__ import annotations
@@ -1,4 +1,4 @@
"""Host-only Query compatibility views for AgentRunner tool execution."""
"""Host-only Query compatibility views for Runner tool execution."""
from __future__ import annotations
+3 -3
View File
@@ -8,13 +8,13 @@ from __future__ import annotations
import typing
import pydantic
from langbot_plugin.api.entities.builtin.agent_runner.event import (
from langbot_plugin.api.entities.builtin.runner.event import (
ActorContext,
SubjectContext,
RawEventRef,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
class AgentEventEnvelope(pydantic.BaseModel):
+3 -3
View File
@@ -31,7 +31,7 @@ def parse_runner_id(runner_id: str) -> RunnerIdParts:
Raises:
ValueError: If runner_id format is invalid
"""
if runner_id.startswith(('plugin:', 'event_processor:')):
if runner_id.startswith('plugin:'):
source, value = runner_id.split(':', 1)
parts = value.split('/')
if len(parts) != 3:
@@ -71,7 +71,7 @@ def format_runner_id(
Returns:
Runner ID string
"""
if source in {'plugin', 'event_processor'}:
if source == 'plugin':
return f'{source}:{plugin_author}/{plugin_name}/{runner_name}'
else:
raise ValueError(f'Invalid runner source: {source}')
@@ -86,4 +86,4 @@ def is_plugin_runner_id(runner_id: str) -> bool:
Returns:
True if runner ID starts with 'plugin:'
"""
return runner_id.startswith(('plugin:', 'event_processor:'))
return runner_id.startswith('plugin:')
@@ -10,7 +10,7 @@ import pydantic
from langbot_plugin.api.entities.builtin.platform import message as platform_message
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .errors import RunnerProtocolError
from .host_models import AgentBinding, AgentEventEnvelope
from .interaction_store import InteractionStore
@@ -97,7 +97,7 @@ class InteractionManager:
result_dict: dict[str, typing.Any],
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
run_id: str,
adapter_context: dict[str, typing.Any] | None,
) -> bool:
@@ -230,7 +230,7 @@ class InteractionManager:
*,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
processor_id: str,
conversation_id: str | None,
) -> dict[str, typing.Any] | None:
@@ -390,7 +390,7 @@ class InteractionManager:
raise ValueError(f'interaction submission option is not present in the request: {field_id}')
@staticmethod
def _authorize(descriptor: AgentRunnerDescriptor, binding: AgentBinding) -> None:
def _authorize(descriptor: RunnerDescriptor, binding: AgentBinding) -> None:
supports_interactions = bool(getattr(descriptor.capabilities, 'interactions', False))
permissions = set(getattr(descriptor.permissions, 'interactions', []) or [])
if not supports_interactions or 'request' not in permissions:
+13 -24
View File
@@ -1,4 +1,4 @@
"""Plugin-runtime invocation for AgentRunner executions."""
"""Plugin-runtime invocation for Runner executions."""
from __future__ import annotations
@@ -10,13 +10,13 @@ import typing
from langbot_plugin.entities.io.errors import ActionCallTimeoutError
from ...core import app
from .context_builder import AgentRunContextPayload
from .descriptor import AgentRunnerDescriptor
from .context_builder import RunnerContextPayload
from .descriptor import RunnerDescriptor
from .errors import RunnerExecutionError
class AgentRunnerInvoker:
"""Invoke an AgentRunner through the plugin runtime.
class RunnerInvoker:
"""Invoke an Runner through the plugin runtime.
This keeps runtime transport, deadline enforcement, and transport error
mapping out of the orchestration state machine.
@@ -29,8 +29,8 @@ class AgentRunnerInvoker:
async def invoke(
self,
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
"""Invoke the runner and yield raw result dictionaries."""
if not self.ap.plugin_connector.is_enable_plugin:
@@ -41,18 +41,7 @@ class AgentRunnerInvoker:
)
try:
if descriptor.component_kind == 'EventProcessor':
context = {
**context,
'runtime': {
**context['runtime'],
'metadata': {
**context['runtime'].get('metadata', {}),
'component_kind': 'EventProcessor',
},
},
}
gen = self.ap.plugin_connector.run_agent(
gen = self.ap.plugin_connector.run_runner(
plugin_author=descriptor.plugin_author,
plugin_name=descriptor.plugin_name,
runner_name=descriptor.runner_name,
@@ -93,8 +82,8 @@ class AgentRunnerInvoker:
async def _next_with_deadline(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> dict[str, typing.Any]:
"""Read the next runner result while enforcing the run deadline."""
remaining = self._remaining_deadline_seconds(context)
@@ -116,7 +105,7 @@ class AgentRunnerInvoker:
def _remaining_deadline_seconds(
self,
context: AgentRunContextPayload,
context: RunnerContextPayload,
) -> float | None:
runtime = context.get('runtime') or {}
deadline_at = runtime.get('deadline_at')
@@ -127,14 +116,14 @@ class AgentRunnerInvoker:
except (TypeError, ValueError):
return None
def _is_deadline_exhausted(self, context: AgentRunContextPayload) -> bool:
def _is_deadline_exhausted(self, context: RunnerContextPayload) -> bool:
remaining = self._remaining_deadline_seconds(context)
return remaining is not None and remaining <= 0
async def _close_generator(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> None:
try:
await gen.aclose()
+24 -24
View File
@@ -15,8 +15,8 @@ from ...core import app
from ...api.http.context import ExecutionContext
from ...pipeline.pool import get_query_execution_context
from .binding_resolver import AgentBindingResolver
from .context_builder import AgentRunContextBuilder, AgentRunContextPayload
from .descriptor import AgentRunnerDescriptor
from .context_builder import RunnerContextBuilder, RunnerContextPayload
from .descriptor import RunnerDescriptor
from .execution_context import (
append_mcp_resource_context_to_event,
build_mcp_resource_context_addition,
@@ -26,10 +26,10 @@ from .execution_context import (
project_mcp_resource_config,
)
from .host_models import AgentBinding, AgentEventEnvelope
from .invoker import AgentRunnerInvoker
from .invoker import RunnerInvoker
from .interaction_manager import InteractionManager
from .query_bridge import QueryRunBridge
from .registry import AgentRunnerRegistry
from .registry import RunnerRegistry
from .resource_builder import AgentResourceBuilder
from .platform_tools import freeze_platform_context
from .result_normalizer import AgentResultNormalizer
@@ -43,7 +43,7 @@ ACTIVATED_SKILL_NAMES_STATE_KEY = 'host.activated_skills'
class AgentRunOrchestrator:
"""Coordinate one AgentRunner execution.
"""Coordinate one Runner execution.
The orchestrator keeps the run state machine readable and delegates
transport, Query bridging, and persistence side effects to narrower
@@ -51,13 +51,13 @@ class AgentRunOrchestrator:
"""
ap: app.Application
registry: AgentRunnerRegistry
context_builder: AgentRunContextBuilder
registry: RunnerRegistry
context_builder: RunnerContextBuilder
resource_builder: AgentResourceBuilder
result_normalizer: AgentResultNormalizer
binding_resolver: AgentBindingResolver
query_bridge: QueryRunBridge
invoker: AgentRunnerInvoker
invoker: RunnerInvoker
interaction_manager: InteractionManager
journal: AgentRunJournal
_session_registry: AgentRunSessionRegistry
@@ -65,16 +65,16 @@ class AgentRunOrchestrator:
def __init__(
self,
ap: app.Application,
registry: AgentRunnerRegistry,
registry: RunnerRegistry,
):
self.ap = ap
self.registry = registry
self.context_builder = AgentRunContextBuilder(ap)
self.context_builder = RunnerContextBuilder(ap)
self.resource_builder = AgentResourceBuilder(ap)
self.result_normalizer = AgentResultNormalizer(ap)
self.binding_resolver = AgentBindingResolver()
self.query_bridge = QueryRunBridge(self.binding_resolver)
self.invoker = AgentRunnerInvoker(ap)
self.invoker = RunnerInvoker(ap)
self.interaction_manager = InteractionManager(ap)
self.journal = AgentRunJournal(ap)
self._session_registry = get_session_registry()
@@ -86,7 +86,7 @@ class AgentRunOrchestrator:
bound_plugins: list[str] | None = None,
adapter_context: dict[str, typing.Any] | None = None,
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
"""Run an AgentRunner from an event-first envelope."""
"""Run an Runner from an event-first envelope."""
runner_id = binding.runner_id
execution_query = adapter_context.get('_query') if adapter_context else None
execution_context = adapter_context.get('_execution_context') if adapter_context else None
@@ -105,9 +105,9 @@ class AgentRunOrchestrator:
bound_plugins,
)
expected_kind = 'EventProcessor' if binding.processor_type == 'event_processor' else 'AgentRunner'
if descriptor.component_kind != expected_kind:
raise ValueError('Processor kind does not match the selected plugin component')
usage = 'event' if binding.processor_type == 'event_processor' else 'agent'
if usage not in descriptor.usages:
raise ValueError(f'The selected Runner does not support {usage} usage')
if execution_query is None:
execution_query = build_execution_query(event, [])
@@ -391,7 +391,7 @@ class AgentRunOrchestrator:
self,
query: pipeline_query.Query,
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
"""Run an AgentRunner from the current Pipeline Query entry point."""
"""Run an Runner from the current Pipeline Query entry point."""
plan = self.query_bridge.build_plan(query)
adapter_context = dict(plan.adapter_context)
adapter_context['_query'] = query
@@ -566,8 +566,8 @@ class AgentRunOrchestrator:
async def _invoke_runner(
self,
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
"""Compatibility delegate for older tests and internal callers."""
async for result in self.invoker.invoke(descriptor, context):
@@ -576,24 +576,24 @@ class AgentRunOrchestrator:
async def _next_with_deadline(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
context: AgentRunContextPayload,
descriptor: RunnerDescriptor,
context: RunnerContextPayload,
) -> dict[str, typing.Any]:
return await self.invoker._next_with_deadline(gen, descriptor, context)
def _remaining_deadline_seconds(
self,
context: AgentRunContextPayload,
context: RunnerContextPayload,
) -> float | None:
return self.invoker._remaining_deadline_seconds(context)
def _is_deadline_exhausted(self, context: AgentRunContextPayload) -> bool:
def _is_deadline_exhausted(self, context: RunnerContextPayload) -> bool:
return self.invoker._is_deadline_exhausted(context)
async def _close_generator(
self,
gen: typing.AsyncGenerator[dict[str, typing.Any], None],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> None:
await self.invoker._close_generator(gen, descriptor)
@@ -602,7 +602,7 @@ class AgentRunOrchestrator:
result_dict: dict[str, typing.Any],
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> None:
await self.journal.handle_state_updated_event(result_dict, event, binding, descriptor)
@@ -1,7 +1,8 @@
"""Persistent state store for AgentRunner protocol state.
"""Persistent state store for Runner protocol state.
This module provides a database-backed state store for event-first Protocol v1.
"""
from __future__ import annotations
import typing
@@ -16,7 +17,7 @@ from sqlalchemy.dialects.postgresql import insert as postgresql_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.exc import IntegrityError
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .host_models import AgentEventEnvelope, AgentBinding
from .state_scope import (
VALID_STATE_SCOPES,
@@ -24,7 +25,7 @@ from .state_scope import (
get_binding_identity,
normalize_state_key,
)
from ...entity.persistence.agent_runner_state import AgentRunnerState
from ...entity.persistence.runner_state import RunnerState
# Maximum value_json size (256KB)
@@ -32,7 +33,7 @@ MAX_VALUE_JSON_BYTES = 256 * 1024
class PersistentStateStore:
"""Database-backed state store for AgentRunner protocol state.
"""Database-backed state store for Runner protocol state.
IMPORTANT: This is HOST-OWNED protocol state, NOT plugin instance state.
@@ -55,7 +56,7 @@ class PersistentStateStore:
scope: str,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> str | None:
"""Get scope key for given scope."""
return build_state_scope_key(scope, event, binding, descriptor)
@@ -104,7 +105,7 @@ class PersistentStateStore:
dialect_name = self._db_engine.dialect.name
if dialect_name == 'sqlite':
stmt = sqlite_insert(AgentRunnerState).values(**values)
stmt = sqlite_insert(RunnerState).values(**values)
await conn.execute(
stmt.on_conflict_do_update(
index_elements=constraint_columns,
@@ -114,7 +115,7 @@ class PersistentStateStore:
return
if dialect_name == 'postgresql':
stmt = postgresql_insert(AgentRunnerState).values(**values)
stmt = postgresql_insert(RunnerState).values(**values)
await conn.execute(
stmt.on_conflict_do_update(
index_elements=constraint_columns,
@@ -124,12 +125,12 @@ class PersistentStateStore:
return
try:
await conn.execute(sqlalchemy.insert(AgentRunnerState).values(**values))
await conn.execute(sqlalchemy.insert(RunnerState).values(**values))
except IntegrityError:
await conn.execute(
update(AgentRunnerState)
.where(AgentRunnerState.scope_key == values['scope_key'])
.where(AgentRunnerState.state_key == values['state_key'])
update(RunnerState)
.where(RunnerState.scope_key == values['scope_key'])
.where(RunnerState.state_key == values['state_key'])
.values(**update_values)
)
@@ -139,7 +140,7 @@ class PersistentStateStore:
self,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, dict[str, typing.Any]]:
"""Build state snapshot for all scopes from event and binding.
@@ -174,8 +175,7 @@ class PersistentStateStore:
# Query all state entries for this scope_key
result = await conn.execute(
select(AgentRunnerState.state_key, AgentRunnerState.value_json)
.where(AgentRunnerState.scope_key == scope_key)
select(RunnerState.state_key, RunnerState.value_json).where(RunnerState.scope_key == scope_key)
)
rows = result.fetchall()
@@ -199,7 +199,7 @@ class PersistentStateStore:
self,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
scope: str,
key: str,
value: typing.Any,
@@ -280,9 +280,9 @@ class PersistentStateStore:
async with self._db_engine.connect() as conn:
result = await conn.execute(
select(AgentRunnerState.value_json)
.where(AgentRunnerState.scope_key == scope_key)
.where(AgentRunnerState.state_key == state_key)
select(RunnerState.value_json)
.where(RunnerState.scope_key == scope_key)
.where(RunnerState.state_key == state_key)
)
row = result.first()
@@ -358,9 +358,7 @@ class PersistentStateStore:
async with self._db_engine.begin() as conn:
result = await conn.execute(
delete(AgentRunnerState)
.where(AgentRunnerState.scope_key == scope_key)
.where(AgentRunnerState.state_key == state_key)
delete(RunnerState).where(RunnerState.scope_key == scope_key).where(RunnerState.state_key == state_key)
)
return (result.rowcount or 0) > 0
@@ -379,17 +377,15 @@ class PersistentStateStore:
async with self._db_engine.connect() as conn:
query = (
select(AgentRunnerState.state_key)
.where(AgentRunnerState.scope_key == scope_key)
.order_by(AgentRunnerState.state_key)
select(RunnerState.state_key)
.where(RunnerState.scope_key == scope_key)
.order_by(RunnerState.state_key)
.limit(limit + 1) # Fetch one extra to check has_more
)
if prefix:
prefix = normalize_state_key(prefix)
query = query.where(
AgentRunnerState.state_key.like(f'{prefix}%')
)
query = query.where(RunnerState.state_key.like(f'{prefix}%'))
result = await conn.execute(query)
rows = result.fetchall()
@@ -402,7 +398,7 @@ class PersistentStateStore:
async def clear_all(self) -> None:
"""Clear all state entries (for testing)."""
async with self._db_engine.begin() as conn:
await conn.execute(delete(AgentRunnerState))
await conn.execute(delete(RunnerState))
# Global singleton persistent state store
@@ -423,7 +419,7 @@ def get_persistent_state_store(db_engine: AsyncEngine | None = None) -> Persiste
with _persistent_state_store_lock:
if _persistent_state_store is None:
if db_engine is None:
raise RuntimeError("db_engine required for first call to get_persistent_state_store")
raise RuntimeError('db_engine required for first call to get_persistent_state_store')
_persistent_state_store = PersistentStateStore(db_engine)
return _persistent_state_store
@@ -1,4 +1,4 @@
"""Run-scoped platform and event action tools exposed to AgentRunners."""
"""Run-scoped platform and event action tools exposed to Runners."""
from __future__ import annotations
+1 -1
View File
@@ -1,4 +1,4 @@
"""Pipeline Query bridge for AgentRunner execution."""
"""Pipeline Query bridge for Runner execution."""
from __future__ import annotations
@@ -12,15 +12,15 @@ import typing
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
from langbot_plugin.api.entities.builtin.platform import message as platform_message
from langbot_plugin.api.entities.builtin.agent_runner.event import (
from langbot_plugin.api.entities.builtin.runner.event import (
AgentEventContext,
ConversationContext,
ActorContext,
SubjectContext,
RawEventRef,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
from .host_models import (
AgentConfig,
+26 -26
View File
@@ -5,23 +5,23 @@ from __future__ import annotations
import typing
import asyncio
from langbot_plugin.api.entities.builtin.agent_runner.manifest import (
AgentRunnerManifest,
from langbot_plugin.api.entities.builtin.runner.manifest import (
RunnerManifest,
)
from ...core import app
from ...api.http.context import ExecutionContext
from ...api.http.service.tenant import TenantContext
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .id import parse_runner_id, format_runner_id
from .errors import RunnerNotFoundError, RunnerNotAuthorizedError
class AgentRunnerRegistry:
class RunnerRegistry:
"""Registry for discovering and managing agent runners.
Responsibilities:
- Discover runners from plugin runtime via LIST_AGENT_RUNNERS
- Discover runners from plugin runtime via LIST_RUNNERS
- Validate runner manifests (kind, metadata, spec)
- Cache discovered runners for performance
- Filter runners by bound plugins
@@ -30,7 +30,7 @@ class AgentRunnerRegistry:
ap: app.Application
_cache: dict[tuple[str, str, int], dict[str, AgentRunnerDescriptor]]
_cache: dict[tuple[str, str, int], dict[str, RunnerDescriptor]]
"""Runner descriptors keyed by immutable Workspace execution scope."""
_cache_lock: asyncio.Lock
@@ -52,7 +52,7 @@ class AgentRunnerRegistry:
async def _resolve_context(self, context: TenantContext) -> ExecutionContext:
return await self.ap.plugin_connector.require_workspace_context(context)
async def _discover_runners(self) -> dict[str, AgentRunnerDescriptor]:
async def _discover_runners(self) -> dict[str, RunnerDescriptor]:
"""Discover runners from plugin runtime.
Always discovers ALL runners (no bound_plugins filter).
@@ -64,11 +64,11 @@ class AgentRunnerRegistry:
if not self.ap.plugin_connector.is_enable_plugin:
return {}
runners: dict[str, AgentRunnerDescriptor] = {}
runners: dict[str, RunnerDescriptor] = {}
try:
# Always list all runners (bound_plugins=None)
plugin_runners = await self.ap.plugin_connector.list_agent_runners(None)
plugin_runners = await self.ap.plugin_connector.list_runners(None)
for runner_data in plugin_runners:
try:
@@ -90,16 +90,16 @@ class AgentRunnerRegistry:
return runners
def _validate_and_build_descriptor(self, runner_data: dict[str, typing.Any]) -> AgentRunnerDescriptor | None:
def _validate_and_build_descriptor(self, runner_data: dict[str, typing.Any]) -> RunnerDescriptor | None:
"""Validate runner manifest and build descriptor.
Args:
runner_data: Raw runner data from plugin runtime with fields:
- plugin_author, plugin_name, runner_name
- manifest (typed AgentRunnerManifest)
- manifest (typed RunnerManifest)
Returns:
AgentRunnerDescriptor if valid, None if invalid
RunnerDescriptor if valid, None if invalid
"""
plugin_author = runner_data.get('plugin_author', '')
plugin_name = runner_data.get('plugin_name', '')
@@ -110,18 +110,19 @@ class AgentRunnerRegistry:
manifest = runner_data.get('manifest', {})
runner_id = format_runner_id(
source='event_processor' if manifest.get('component_kind') == 'EventProcessor' else 'plugin',
source='plugin',
plugin_author=plugin_author,
plugin_name=plugin_name,
runner_name=runner_name,
)
typed_manifest = AgentRunnerManifest.model_validate(manifest)
typed_manifest = RunnerManifest.model_validate(manifest)
config_schema = [item.model_dump(mode='json') for item in typed_manifest.config_schema]
return AgentRunnerDescriptor(
return RunnerDescriptor(
id=runner_id,
component_kind=typed_manifest.component_kind,
usages=typed_manifest.usages,
supported_event_patterns=typed_manifest.supported_event_patterns,
source='plugin',
label=typed_manifest.label,
@@ -152,8 +153,8 @@ class AgentRunnerRegistry:
context: TenantContext,
bound_plugins: list[str] | None = None,
use_cache: bool = True,
component_kind: str = 'AgentRunner',
) -> list[AgentRunnerDescriptor]:
usage: typing.Literal['agent', 'event'] | None = 'agent',
) -> list[RunnerDescriptor]:
"""List available runners.
Args:
@@ -173,7 +174,7 @@ class AgentRunnerRegistry:
return [
r
for r in self._filter_runners_by_bound_plugins(cached, bound_plugins)
if r.component_kind == component_kind
if usage is None or usage in r.usages
]
# Discover fresh (always full list)
@@ -187,14 +188,14 @@ class AgentRunnerRegistry:
return [
r
for r in self._filter_runners_by_bound_plugins(runners, bound_plugins)
if r.component_kind == component_kind
if usage is None or usage in r.usages
]
def _filter_runners_by_bound_plugins(
self,
runners: dict[str, AgentRunnerDescriptor],
runners: dict[str, RunnerDescriptor],
bound_plugins: list[str] | None,
) -> list[AgentRunnerDescriptor]:
) -> list[RunnerDescriptor]:
"""Filter runners by bound plugins.
Args:
@@ -222,7 +223,7 @@ class AgentRunnerRegistry:
context: TenantContext,
runner_id: str,
bound_plugins: list[str] | None = None,
) -> AgentRunnerDescriptor:
) -> RunnerDescriptor:
"""Get a specific runner descriptor.
Args:
@@ -230,7 +231,7 @@ class AgentRunnerRegistry:
bound_plugins: Optional bound plugins filter
Returns:
AgentRunnerDescriptor
RunnerDescriptor
Raises:
RunnerNotFoundError: If runner not found
@@ -242,8 +243,7 @@ class AgentRunnerRegistry:
except ValueError as e:
raise RunnerNotFoundError(runner_id) from e
component_kind = 'EventProcessor' if runner_id.startswith('event_processor:') else 'AgentRunner'
runners = await self.list_runners(context, bound_plugins=None, component_kind=component_kind)
runners = await self.list_runners(context, bound_plugins=None, usage=None)
descriptor = next((item for item in runners if item.id == runner_id), None)
if descriptor is None:
# The runtime launches installed plugins asynchronously, so an
@@ -252,7 +252,7 @@ class AgentRunnerRegistry:
context,
bound_plugins=None,
use_cache=False,
component_kind=component_kind,
usage=None,
)
descriptor = next((item for item in runners if item.id == runner_id), None)
if descriptor is None:
@@ -6,7 +6,7 @@ import typing
from ...core import app
from ...api.http.context import ExecutionContext
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .context_builder import (
AgentResources,
ModelResource,
@@ -54,7 +54,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> AgentResources:
"""Build AgentResources from event and binding.
@@ -127,7 +127,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
manifest_perms: typing.Any,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> list[ModelResource]:
"""Build models list from binding."""
@@ -174,7 +174,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
manifest_perms: typing.Any,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> list[ToolResource]:
"""Build tools list from binding."""
@@ -274,7 +274,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
manifest_perms: typing.Any,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
) -> list[KnowledgeBaseResource]:
"""Build knowledge bases list from binding."""
@@ -321,7 +321,7 @@ class AgentResourceBuilder:
self,
execution_context: ExecutionContext,
resource_policy: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> list[SkillResource]:
"""Build pipeline-visible skill resource facts.
@@ -372,7 +372,7 @@ class AgentResourceBuilder:
execution_context: ExecutionContext,
models: list[ModelResource],
seen_model_ids: set[str],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
runner_config: dict[str, typing.Any],
include_llm: bool,
include_rerank: bool,
@@ -1,4 +1,4 @@
"""Project AgentRunner configuration into Host resource policy."""
"""Project Runner configuration into Host resource policy."""
from __future__ import annotations
@@ -1,11 +1,11 @@
"""Agent result normalizer for converting AgentRunResult to Pipeline messages."""
"""Agent result normalizer for converting RunnerResult to Pipeline messages."""
from __future__ import annotations
import typing
import pydantic
from langbot_plugin.api.entities.builtin.agent_runner.result import (
from langbot_plugin.api.entities.builtin.runner.result import (
ActionRequestedPayload,
MessageCompletedPayload,
MessageDeltaPayload,
@@ -19,7 +19,7 @@ from langbot_plugin.api.entities.builtin.agent_runner.result import (
from langbot_plugin.api.entities.builtin.provider import message as provider_message
from ...core import app
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .errors import RunnerExecutionError, RunnerProtocolError
@@ -40,7 +40,7 @@ STRICT_RESULT_PAYLOADS: dict[str, type[pydantic.BaseModel]] = {
class AgentResultNormalizer:
"""Normalizer for converting AgentRunResult to Pipeline messages.
"""Normalizer for converting RunnerResult to Pipeline messages.
Responsibilities:
- Accept only supported result types (message.delta, message.completed, etc.)
@@ -71,9 +71,9 @@ class AgentResultNormalizer:
async def normalize(
self,
result_dict: dict[str, typing.Any],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> provider_message.Message | provider_message.MessageChunk | None:
"""Normalize AgentRunResult to Message or MessageChunk.
"""Normalize RunnerResult to Message or MessageChunk.
Args:
result_dict: Raw result dict from plugin runtime
@@ -186,7 +186,7 @@ class AgentResultNormalizer:
self,
result_type: str,
data: typing.Any,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> bool:
"""Validate typed payloads that affect Host state or delivery.
@@ -210,7 +210,7 @@ class AgentResultNormalizer:
def _normalize_message_delta(
self,
data: dict[str, typing.Any],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> provider_message.MessageChunk:
"""Normalize message.delta to MessageChunk."""
chunk_data = data.get('chunk', {})
@@ -226,7 +226,7 @@ class AgentResultNormalizer:
def _normalize_message_completed(
self,
data: dict[str, typing.Any],
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> provider_message.Message:
"""Normalize message.completed to Message."""
message_data = data.get('message', {})
+5 -5
View File
@@ -1,11 +1,11 @@
"""Run-side effects for AgentRunner executions."""
"""Run-side effects for Runner executions."""
from __future__ import annotations
import typing
from ...core import app
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .errors import RunnerProtocolError
from .host_models import AgentBinding, AgentEventEnvelope
from .persistent_state_store import PersistentStateStore, get_persistent_state_store
@@ -73,7 +73,7 @@ class AgentRunJournal:
*,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
context: dict[str, typing.Any],
authorization: dict[str, typing.Any],
) -> dict[str, typing.Any]:
@@ -112,7 +112,7 @@ class AgentRunJournal:
source: str = 'runner',
metadata: dict[str, typing.Any] | None = None,
) -> dict[str, typing.Any]:
"""Persist one AgentRunResult in the run ledger."""
"""Persist one RunnerResult in the run ledger."""
usage = result_dict.get('usage')
if hasattr(usage, 'model_dump'):
usage = usage.model_dump(mode='json')
@@ -153,7 +153,7 @@ class AgentRunJournal:
result_dict: dict[str, typing.Any],
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
run_id: str | None = None,
) -> None:
"""Handle state.updated result in event-first mode."""
@@ -61,7 +61,7 @@ class AgentRunSession(typing.TypedDict):
Stored in AgentRunSessionRegistry for proxy action permission validation.
Fields:
run_id: Unique run identifier (UUID from AgentRunContext)
run_id: Unique run identifier (UUID from RunnerContext)
runner_id: Runner descriptor ID (plugin:author/name/runner)
query_id: Host entry query ID, only present for query-based adapters
execution_query: Host-only Query used by providers and tool loaders
@@ -84,10 +84,10 @@ class AgentRunSession(typing.TypedDict):
class AgentRunSessionRegistry:
"""Registry for active agent run sessions.
Host-owned registry for tracking active AgentRunner executions.
Host-owned registry for tracking active Runner executions.
Used by proxy actions in handler.py to validate resource access.
Key: run_id (UUID from AgentRunContext)
Key: run_id (UUID from RunnerContext)
Value: AgentRunSession with authorized resources
Thread-safe via asyncio.Lock.
@@ -130,7 +130,7 @@ class AgentRunSessionRegistry:
bot_id: Bot UUID for history/event access
workspace_id: Workspace ID for history/event access
thread_id: Thread ID for history/event access
available_apis: Run-scoped pull APIs exposed in AgentRunContext
available_apis: Run-scoped pull APIs exposed in RunnerContext
state_policy: State policy from binding (enable_state, state_scopes)
state_context: Context for state API (scope_keys, binding_identity, etc.)
execution_query: Host-only Query used for provider and tool execution
+31 -21
View File
@@ -1,11 +1,12 @@
"""State scope key helpers for AgentRunner host-owned state."""
"""State scope key helpers for Runner host-owned state."""
from __future__ import annotations
import hashlib
import json
import typing
from .descriptor import AgentRunnerDescriptor
from .descriptor import RunnerDescriptor
from .host_models import AgentBinding, AgentEventEnvelope
@@ -47,7 +48,7 @@ def _scope_hash(scope: str, parts: dict[str, typing.Any]) -> str:
def _base_scope_parts(
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, typing.Any]:
return {
'runner_id': descriptor.id,
@@ -61,7 +62,7 @@ def build_state_scope_key(
scope: str,
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> str | None:
"""Build the storage key for one state scope.
@@ -72,29 +73,38 @@ def build_state_scope_key(
if scope == 'conversation':
if not event.conversation_id:
return None
return _scope_hash(scope, {
**base_parts,
'conversation_id': event.conversation_id,
'thread_id': event.thread_id,
})
return _scope_hash(
scope,
{
**base_parts,
'conversation_id': event.conversation_id,
'thread_id': event.thread_id,
},
)
if scope == 'actor':
if not event.actor or not event.actor.actor_id:
return None
return _scope_hash(scope, {
**base_parts,
'actor_type': event.actor.actor_type or 'user',
'actor_id': event.actor.actor_id,
})
return _scope_hash(
scope,
{
**base_parts,
'actor_type': event.actor.actor_type or 'user',
'actor_id': event.actor.actor_id,
},
)
if scope == 'subject':
if not event.subject or not event.subject.subject_id:
return None
return _scope_hash(scope, {
**base_parts,
'subject_type': event.subject.subject_type or 'unknown',
'subject_id': event.subject.subject_id,
})
return _scope_hash(
scope,
{
**base_parts,
'subject_type': event.subject.subject_type or 'unknown',
'subject_id': event.subject.subject_id,
},
)
if scope == 'runner':
return _scope_hash(scope, base_parts)
@@ -105,7 +115,7 @@ def build_state_scope_key(
def build_state_scope_keys(
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, str]:
"""Build all available scope keys for an event/binding pair."""
scope_keys: dict[str, str] = {}
@@ -119,7 +129,7 @@ def build_state_scope_keys(
def build_state_context(
event: AgentEventEnvelope,
binding: AgentBinding,
descriptor: AgentRunnerDescriptor,
descriptor: RunnerDescriptor,
) -> dict[str, typing.Any]:
"""Build the State API context stored in the run session."""
return {
@@ -1,4 +1,5 @@
"""Transcript store for writing and querying conversation history."""
from __future__ import annotations
import json
@@ -46,9 +47,7 @@ class TranscriptStore:
def __init__(self, engine: AsyncEngine):
self.engine = engine
self._session_factory = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
self._session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def append_transcript(
self,
@@ -62,7 +61,7 @@ class TranscriptStore:
content_json: dict[str, typing.Any] | None = None,
attachment_refs: list[dict[str, typing.Any]] | None = None,
thread_id: str | None = None,
item_type: str = "message",
item_type: str = 'message',
run_id: str | None = None,
runner_id: str | None = None,
metadata: dict[str, typing.Any] | None = None,
@@ -93,7 +92,7 @@ class TranscriptStore:
# Truncate content if too long
if content and len(content) > self.MAX_CONTENT_LENGTH:
content = content[:self.MAX_CONTENT_LENGTH - 3] + "..."
content = content[: self.MAX_CONTENT_LENGTH - 3] + '...'
async with self._session_factory() as session:
item = Transcript(
@@ -127,7 +126,7 @@ class TranscriptStore:
before_seq: int | None = None,
after_seq: int | None = None,
limit: int = 50,
direction: str = "backward",
direction: str = 'backward',
include_attachments: bool = False,
bot_id: str | None = None,
workspace_id: str | None = None,
@@ -154,15 +153,13 @@ class TranscriptStore:
limit = min(limit, self.HARD_LIMIT)
async with self._session_factory() as session:
query = sqlalchemy.select(Transcript).where(
Transcript.conversation_id == conversation_id
)
query = sqlalchemy.select(Transcript).where(Transcript.conversation_id == conversation_id)
query = self._apply_scope_filters(query, bot_id, workspace_id, thread_id, strict_thread)
if direction == "backward" and before_seq is not None:
if direction == 'backward' and before_seq is not None:
query = query.where(Transcript.seq < before_seq)
query = query.order_by(Transcript.seq.desc())
elif direction == "forward" and after_seq is not None:
elif direction == 'forward' and after_seq is not None:
query = query.where(Transcript.seq > after_seq)
query = query.order_by(Transcript.seq.asc())
else:
@@ -181,7 +178,7 @@ class TranscriptStore:
next_seq = None
prev_seq = None
if direction == "backward":
if direction == 'backward':
# Items are in descending order
if items:
next_seq = items[-1].get('seq') if has_more else None
@@ -225,7 +222,7 @@ class TranscriptStore:
async with self._session_factory() as session:
query = sqlalchemy.select(Transcript).where(
Transcript.conversation_id == conversation_id,
Transcript.content.ilike(f"%{query_text}%"),
Transcript.content.ilike(f'%{query_text}%'),
)
query = self._apply_scope_filters(query, bot_id, workspace_id, thread_id, strict_thread)
@@ -278,14 +275,14 @@ class TranscriptStore:
) -> list[provider_message.Message]:
"""Project Transcript rows into the legacy provider Message view.
AgentRunner history is canonical in Transcript. This view exists for
Runner history is canonical in Transcript. This view exists for
legacy Pipeline readers such as PromptPreProcessing that still expect
query.messages.
"""
items, _, _, _ = await self.page_transcript(
conversation_id=conversation_id,
limit=limit,
direction="backward",
direction='backward',
bot_id=bot_id,
workspace_id=workspace_id,
thread_id=thread_id,
@@ -353,9 +350,7 @@ class TranscriptStore:
) -> int:
"""Delete Transcript rows created before the supplied timestamp."""
async with self._session_factory() as session:
result = await session.execute(
sqlalchemy.delete(Transcript).where(Transcript.created_at < before)
)
result = await session.execute(sqlalchemy.delete(Transcript).where(Transcript.created_at < before))
await session.commit()
return result.rowcount or 0
@@ -363,8 +358,9 @@ class TranscriptStore:
"""Fallback next sequence number for stores that cannot expose autoincrement IDs."""
async with self._session_factory() as session:
result = await session.execute(
sqlalchemy.select(sqlalchemy.func.max(Transcript.seq))
.where(Transcript.conversation_id == conversation_id)
sqlalchemy.select(sqlalchemy.func.max(Transcript.seq)).where(
Transcript.conversation_id == conversation_id
)
)
max_seq = result.scalar()
return (max_seq or 0) + 1