mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-21 17:57:14 +00:00
feat(agent-runner): add programmatic run create action
This commit is contained in:
@@ -220,6 +220,7 @@ class AgentRunContextBuilder:
|
|||||||
binding: AgentBinding,
|
binding: AgentBinding,
|
||||||
descriptor: AgentRunnerDescriptor,
|
descriptor: AgentRunnerDescriptor,
|
||||||
resources: AgentResources,
|
resources: AgentResources,
|
||||||
|
run_id: str | None = None,
|
||||||
) -> AgentRunContextPayload:
|
) -> AgentRunContextPayload:
|
||||||
"""Build AgentRunContext from event-first envelope.
|
"""Build AgentRunContext from event-first envelope.
|
||||||
|
|
||||||
@@ -235,8 +236,8 @@ class AgentRunContextBuilder:
|
|||||||
Returns:
|
Returns:
|
||||||
AgentRunContextPayload for the runner
|
AgentRunContextPayload for the runner
|
||||||
"""
|
"""
|
||||||
# Generate new run_id
|
# Generate new run_id unless an API caller already reserved one.
|
||||||
run_id = str(uuid.uuid4())
|
run_id = run_id or str(uuid.uuid4())
|
||||||
|
|
||||||
# Build trigger from event
|
# Build trigger from event
|
||||||
trigger: AgentTrigger = {
|
trigger: AgentTrigger = {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class AgentRunOrchestrator:
|
|||||||
binding: AgentBinding,
|
binding: AgentBinding,
|
||||||
bound_plugins: list[str] | None = None,
|
bound_plugins: list[str] | None = None,
|
||||||
adapter_context: dict[str, typing.Any] | None = None,
|
adapter_context: dict[str, typing.Any] | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
|
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
|
||||||
"""Run an AgentRunner from an event-first envelope."""
|
"""Run an AgentRunner from an event-first envelope."""
|
||||||
runner_id = binding.runner_id
|
runner_id = binding.runner_id
|
||||||
@@ -84,6 +85,7 @@ class AgentRunOrchestrator:
|
|||||||
binding=binding,
|
binding=binding,
|
||||||
descriptor=descriptor,
|
descriptor=descriptor,
|
||||||
resources=resources,
|
resources=resources,
|
||||||
|
run_id=run_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
session_query_id = None
|
session_query_id = None
|
||||||
|
|||||||
@@ -3,12 +3,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
from langbot_plugin.runtime.io import handler
|
from langbot_plugin.runtime.io import handler
|
||||||
|
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||||
|
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
|
||||||
|
|
||||||
|
|
||||||
|
from ..agent.runner.host_models import (
|
||||||
|
AgentBinding,
|
||||||
|
AgentEventEnvelope,
|
||||||
|
BindingScope,
|
||||||
|
DeliveryPolicy,
|
||||||
|
ResourcePolicy,
|
||||||
|
StatePolicy,
|
||||||
|
)
|
||||||
from ..agent.runner.run_ledger_store import TERMINAL_STATUSES
|
from ..agent.runner.run_ledger_store import TERMINAL_STATUSES
|
||||||
|
|
||||||
from .agent_run_support import (
|
from .agent_run_support import (
|
||||||
@@ -30,7 +42,320 @@ from .agent_run_support import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_payload(value: Any, *, field_name: str) -> tuple[dict[str, Any], handler.ActionResponse | None]:
|
||||||
|
if value is None:
|
||||||
|
return {}, None
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return {}, handler.ActionResponse.error(message=f'{field_name} must be an object')
|
||||||
|
return dict(value), None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_run_create_event(data: dict[str, Any], *, run_id: str) -> tuple[AgentEventEnvelope | None, handler.ActionResponse | None]:
|
||||||
|
event_payload, error = _dict_payload(data.get('event'), field_name='event')
|
||||||
|
if error:
|
||||||
|
return None, error
|
||||||
|
|
||||||
|
input_payload = event_payload.get('input', data.get('input'))
|
||||||
|
if input_payload is None:
|
||||||
|
input_payload = {
|
||||||
|
'text': data.get('text'),
|
||||||
|
'contents': [],
|
||||||
|
'attachments': [],
|
||||||
|
}
|
||||||
|
if not isinstance(input_payload, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='input must be an object')
|
||||||
|
|
||||||
|
delivery_payload = event_payload.get('delivery', data.get('delivery'))
|
||||||
|
if delivery_payload is None:
|
||||||
|
delivery_payload = {
|
||||||
|
'surface': 'api',
|
||||||
|
'reply_target': None,
|
||||||
|
'supports_streaming': False,
|
||||||
|
'supports_edit': False,
|
||||||
|
'supports_reaction': False,
|
||||||
|
'platform_capabilities': {},
|
||||||
|
}
|
||||||
|
if not isinstance(delivery_payload, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='delivery must be an object')
|
||||||
|
|
||||||
|
event_data = event_payload.get('data', data.get('data'))
|
||||||
|
if event_data is None:
|
||||||
|
event_data = {}
|
||||||
|
if not isinstance(event_data, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='event data must be an object')
|
||||||
|
|
||||||
|
event_type = str(event_payload.get('event_type') or data.get('event_type') or 'api.invoked')
|
||||||
|
source = str(event_payload.get('source') or data.get('source') or 'api')
|
||||||
|
event_id = str(event_payload.get('event_id') or data.get('event_id') or f'{source}:{run_id}')
|
||||||
|
event_time = event_payload.get('event_time', data.get('event_time'))
|
||||||
|
if event_time is None:
|
||||||
|
event_time = int(time.time())
|
||||||
|
|
||||||
|
try:
|
||||||
|
envelope = AgentEventEnvelope(
|
||||||
|
event_id=event_id,
|
||||||
|
event_type=event_type,
|
||||||
|
event_time=int(event_time) if isinstance(event_time, (int, float, str)) else None,
|
||||||
|
source=source,
|
||||||
|
source_event_type=event_payload.get('source_event_type') or data.get('source_event_type') or event_type,
|
||||||
|
bot_id=event_payload.get('bot_id', data.get('bot_id')),
|
||||||
|
workspace_id=event_payload.get('workspace_id', data.get('workspace_id')),
|
||||||
|
conversation_id=event_payload.get('conversation_id', data.get('conversation_id')),
|
||||||
|
thread_id=event_payload.get('thread_id', data.get('thread_id')),
|
||||||
|
actor=event_payload.get('actor', data.get('actor')),
|
||||||
|
subject=event_payload.get('subject', data.get('subject')),
|
||||||
|
input=AgentInput.model_validate(input_payload),
|
||||||
|
delivery=DeliveryContext.model_validate(delivery_payload),
|
||||||
|
raw_ref=event_payload.get('raw_ref', data.get('raw_ref')),
|
||||||
|
data=event_data,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return None, handler.ActionResponse.error(message=f'invalid event payload: {exc}')
|
||||||
|
|
||||||
|
return envelope, None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_run_create_binding(
|
||||||
|
data: dict[str, Any],
|
||||||
|
*,
|
||||||
|
event: AgentEventEnvelope,
|
||||||
|
run_id: str,
|
||||||
|
) -> tuple[AgentBinding | None, handler.ActionResponse | None]:
|
||||||
|
binding_payload, error = _dict_payload(data.get('binding'), field_name='binding')
|
||||||
|
if error:
|
||||||
|
return None, error
|
||||||
|
|
||||||
|
runner_id = binding_payload.get('runner_id') or data.get('runner_id')
|
||||||
|
if not runner_id:
|
||||||
|
return None, handler.ActionResponse.error(message='runner_id is required')
|
||||||
|
|
||||||
|
scope_payload = binding_payload.get('scope')
|
||||||
|
if scope_payload is None:
|
||||||
|
agent_id = binding_payload.get('agent_id') or data.get('agent_id')
|
||||||
|
workspace_id = event.workspace_id
|
||||||
|
bot_id = event.bot_id
|
||||||
|
if agent_id:
|
||||||
|
scope_payload = {'scope_type': 'agent', 'scope_id': agent_id}
|
||||||
|
elif bot_id:
|
||||||
|
scope_payload = {'scope_type': 'bot', 'scope_id': bot_id}
|
||||||
|
elif workspace_id:
|
||||||
|
scope_payload = {'scope_type': 'workspace', 'scope_id': workspace_id}
|
||||||
|
else:
|
||||||
|
scope_payload = {'scope_type': 'global', 'scope_id': None}
|
||||||
|
if not isinstance(scope_payload, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='binding.scope must be an object')
|
||||||
|
|
||||||
|
runner_config_payload = binding_payload.get('runner_config', data.get('runner_config'))
|
||||||
|
if runner_config_payload is None:
|
||||||
|
runner_config_payload = {}
|
||||||
|
if not isinstance(runner_config_payload, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='runner_config must be an object')
|
||||||
|
|
||||||
|
resource_policy_payload = binding_payload.get('resource_policy', data.get('resource_policy'))
|
||||||
|
if resource_policy_payload is None:
|
||||||
|
resource_policy_payload = {}
|
||||||
|
if not isinstance(resource_policy_payload, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='resource_policy must be an object')
|
||||||
|
|
||||||
|
state_policy_payload = binding_payload.get('state_policy', data.get('state_policy'))
|
||||||
|
if state_policy_payload is None:
|
||||||
|
state_policy_payload = {}
|
||||||
|
if not isinstance(state_policy_payload, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='state_policy must be an object')
|
||||||
|
|
||||||
|
delivery_policy_payload = binding_payload.get('delivery_policy', data.get('delivery_policy'))
|
||||||
|
if delivery_policy_payload is None:
|
||||||
|
delivery_policy_payload = {
|
||||||
|
'enable_streaming': bool(event.delivery.supports_streaming),
|
||||||
|
'enable_reply': False,
|
||||||
|
}
|
||||||
|
if not isinstance(delivery_policy_payload, dict):
|
||||||
|
return None, handler.ActionResponse.error(message='delivery_policy must be an object')
|
||||||
|
|
||||||
|
try:
|
||||||
|
binding = AgentBinding(
|
||||||
|
binding_id=str(binding_payload.get('binding_id') or data.get('binding_id') or f'api:{runner_id}:{run_id}'),
|
||||||
|
scope=BindingScope.model_validate(scope_payload),
|
||||||
|
event_types=list(binding_payload.get('event_types') or data.get('event_types') or [event.event_type]),
|
||||||
|
runner_id=str(runner_id),
|
||||||
|
runner_config=runner_config_payload,
|
||||||
|
resource_policy=ResourcePolicy.model_validate(resource_policy_payload),
|
||||||
|
state_policy=StatePolicy.model_validate(state_policy_payload),
|
||||||
|
delivery_policy=DeliveryPolicy.model_validate(delivery_policy_payload),
|
||||||
|
enabled=bool(binding_payload.get('enabled', data.get('enabled', True))),
|
||||||
|
agent_id=binding_payload.get('agent_id') or data.get('agent_id'),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return None, handler.ActionResponse.error(message=f'invalid binding payload: {exc}')
|
||||||
|
|
||||||
|
if event.event_type not in binding.event_types:
|
||||||
|
return None, handler.ActionResponse.error(
|
||||||
|
message=f'binding.event_types must include event type {event.event_type}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return binding, None
|
||||||
|
|
||||||
|
|
||||||
|
async def _consume_programmatic_run(
|
||||||
|
h,
|
||||||
|
*,
|
||||||
|
run_id: str,
|
||||||
|
event: AgentEventEnvelope,
|
||||||
|
binding: AgentBinding,
|
||||||
|
bound_plugins: list[str] | None,
|
||||||
|
) -> None:
|
||||||
|
async for _result in h.ap.agent_run_orchestrator.run(
|
||||||
|
event,
|
||||||
|
binding,
|
||||||
|
bound_plugins=bound_plugins,
|
||||||
|
run_id=run_id,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def register(h):
|
def register(h):
|
||||||
|
@h.action(_plugin_runtime_action('RUN_CREATE', 'run_create'))
|
||||||
|
async def run_create(data: dict[str, Any]) -> handler.ActionResponse:
|
||||||
|
"""Create a programmatic AgentRunner run from an explicit event and binding."""
|
||||||
|
caller_plugin_identity = data.get('caller_plugin_identity')
|
||||||
|
if not _has_agent_runner_admin_permission(
|
||||||
|
h.ap,
|
||||||
|
caller_plugin_identity,
|
||||||
|
AGENT_RUN_ADMIN_PERMISSION,
|
||||||
|
):
|
||||||
|
return handler.ActionResponse.error(message='Run create access not authorized')
|
||||||
|
|
||||||
|
orchestrator = getattr(h.ap, 'agent_run_orchestrator', None)
|
||||||
|
if orchestrator is None:
|
||||||
|
return handler.ActionResponse.error(message='AgentRunOrchestrator is not available')
|
||||||
|
|
||||||
|
run_id = str(data.get('run_id') or uuid.uuid4())
|
||||||
|
event, event_error = _build_run_create_event(data, run_id=run_id)
|
||||||
|
if event_error:
|
||||||
|
return event_error
|
||||||
|
assert event is not None
|
||||||
|
|
||||||
|
binding, binding_error = _build_run_create_binding(data, event=event, run_id=run_id)
|
||||||
|
if binding_error:
|
||||||
|
return binding_error
|
||||||
|
assert binding is not None
|
||||||
|
|
||||||
|
include_plugins = data.get('include_plugins')
|
||||||
|
if include_plugins is not None and not isinstance(include_plugins, list):
|
||||||
|
return handler.ActionResponse.error(message='include_plugins must be a list')
|
||||||
|
bound_plugins = [str(item) for item in include_plugins] if include_plugins else None
|
||||||
|
|
||||||
|
registry = getattr(h.ap, 'agent_runner_registry', None)
|
||||||
|
if registry is not None:
|
||||||
|
try:
|
||||||
|
await registry.get(binding.runner_id, bound_plugins)
|
||||||
|
except Exception as exc:
|
||||||
|
return handler.ActionResponse.error(message=f'Runner {binding.runner_id} is not available: {exc}')
|
||||||
|
|
||||||
|
async def background_run(*, raise_errors: bool = False) -> None:
|
||||||
|
from ..agent.runner.run_ledger_store import RunLedgerStore
|
||||||
|
|
||||||
|
store = RunLedgerStore(h.ap.persistence_mgr.get_db_engine())
|
||||||
|
try:
|
||||||
|
await _consume_programmatic_run(
|
||||||
|
h,
|
||||||
|
run_id=run_id,
|
||||||
|
event=event,
|
||||||
|
binding=binding,
|
||||||
|
bound_plugins=bound_plugins,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
h.ap.logger.error(f'RUN_CREATE background run {run_id} failed: {exc}', exc_info=True)
|
||||||
|
if await store.get_run(run_id) is None:
|
||||||
|
await store.create_run(
|
||||||
|
run_id=run_id,
|
||||||
|
event_id=event.event_id,
|
||||||
|
binding_id=binding.binding_id,
|
||||||
|
runner_id=binding.runner_id,
|
||||||
|
conversation_id=event.conversation_id,
|
||||||
|
thread_id=event.thread_id,
|
||||||
|
workspace_id=event.workspace_id,
|
||||||
|
bot_id=event.bot_id,
|
||||||
|
agent_id=binding.agent_id,
|
||||||
|
authorization={
|
||||||
|
'runner_id': binding.runner_id,
|
||||||
|
'binding_id': binding.binding_id,
|
||||||
|
'plugin_identity': None,
|
||||||
|
'resources': {},
|
||||||
|
'available_apis': {},
|
||||||
|
'conversation_id': event.conversation_id,
|
||||||
|
'bot_id': event.bot_id,
|
||||||
|
'workspace_id': event.workspace_id,
|
||||||
|
'thread_id': event.thread_id,
|
||||||
|
},
|
||||||
|
metadata={
|
||||||
|
'event_type': event.event_type,
|
||||||
|
'source': event.source,
|
||||||
|
'run_create_error': True,
|
||||||
|
},
|
||||||
|
status='running',
|
||||||
|
)
|
||||||
|
await store.finalize_run(
|
||||||
|
run_id=run_id,
|
||||||
|
status='failed',
|
||||||
|
status_reason=str(exc),
|
||||||
|
metadata={'run_create_error': True},
|
||||||
|
)
|
||||||
|
if raise_errors:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if data.get('wait_for_completion'):
|
||||||
|
try:
|
||||||
|
await background_run(raise_errors=True)
|
||||||
|
except Exception as exc:
|
||||||
|
return handler.ActionResponse.error(message=f'Run create error: {exc}')
|
||||||
|
from ..agent.runner.run_ledger_store import RunLedgerStore
|
||||||
|
|
||||||
|
store = RunLedgerStore(h.ap.persistence_mgr.get_db_engine())
|
||||||
|
run = await store.get_run(run_id)
|
||||||
|
if run is not None:
|
||||||
|
await _record_agent_runner_admin_action(
|
||||||
|
h.ap,
|
||||||
|
store,
|
||||||
|
action='run_create',
|
||||||
|
caller_plugin_identity=caller_plugin_identity,
|
||||||
|
permission=AGENT_RUN_ADMIN_PERMISSION,
|
||||||
|
durable_run_id=run_id,
|
||||||
|
detail={'runner_id': binding.runner_id, 'event_type': event.event_type},
|
||||||
|
)
|
||||||
|
return handler.ActionResponse.success(data=run)
|
||||||
|
else:
|
||||||
|
asyncio.create_task(background_run())
|
||||||
|
|
||||||
|
await _record_agent_runner_admin_action(
|
||||||
|
h.ap,
|
||||||
|
None,
|
||||||
|
action='run_create',
|
||||||
|
caller_plugin_identity=caller_plugin_identity,
|
||||||
|
permission=AGENT_RUN_ADMIN_PERMISSION,
|
||||||
|
durable_run_id=run_id,
|
||||||
|
detail={'runner_id': binding.runner_id, 'event_type': event.event_type},
|
||||||
|
)
|
||||||
|
return handler.ActionResponse.success(
|
||||||
|
data={
|
||||||
|
'run_id': run_id,
|
||||||
|
'event_id': event.event_id,
|
||||||
|
'agent_id': binding.agent_id,
|
||||||
|
'binding_id': binding.binding_id,
|
||||||
|
'runner_id': binding.runner_id,
|
||||||
|
'conversation_id': event.conversation_id,
|
||||||
|
'thread_id': event.thread_id,
|
||||||
|
'workspace_id': event.workspace_id,
|
||||||
|
'bot_id': event.bot_id,
|
||||||
|
'status': 'created',
|
||||||
|
'metadata': {
|
||||||
|
'event_type': event.event_type,
|
||||||
|
'source': event.source,
|
||||||
|
'accepted': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@h.action(_plugin_runtime_action('RUN_GET', 'run_get'))
|
@h.action(_plugin_runtime_action('RUN_GET', 'run_get'))
|
||||||
async def run_get(data: dict[str, Any]) -> handler.ActionResponse:
|
async def run_get(data: dict[str, Any]) -> handler.ActionResponse:
|
||||||
"""Get one Host-owned run record visible to the current run."""
|
"""Get one Host-owned run record visible to the current run."""
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ class FakeConnection:
|
|||||||
|
|
||||||
|
|
||||||
class FakeApplication:
|
class FakeApplication:
|
||||||
def __init__(self, db_engine, admin_plugins=None, runner_registry=None):
|
def __init__(self, db_engine, admin_plugins=None, runner_registry=None, orchestrator=None):
|
||||||
self.logger = MagicMock()
|
self.logger = MagicMock()
|
||||||
self.persistence_mgr = MagicMock()
|
self.persistence_mgr = MagicMock()
|
||||||
self.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
self.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
||||||
self.agent_runner_registry = runner_registry
|
self.agent_runner_registry = runner_registry
|
||||||
|
self.agent_run_orchestrator = orchestrator
|
||||||
self.instance_config = SimpleNamespace(
|
self.instance_config = SimpleNamespace(
|
||||||
data={
|
data={
|
||||||
'agent_runner': {
|
'agent_runner': {
|
||||||
@@ -72,16 +73,66 @@ class FakeRunnerRegistry:
|
|||||||
self.runners = runners
|
self.runners = runners
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|
||||||
|
async def get(self, runner_id, bound_plugins=None):
|
||||||
|
self.calls.append({'runner_id': runner_id, 'bound_plugins': bound_plugins})
|
||||||
|
if isinstance(self.runners, dict):
|
||||||
|
if runner_id not in self.runners:
|
||||||
|
raise KeyError(runner_id)
|
||||||
|
return self.runners[runner_id]
|
||||||
|
for runner in self.runners:
|
||||||
|
if getattr(runner, 'id', None) == runner_id:
|
||||||
|
return runner
|
||||||
|
raise KeyError(runner_id)
|
||||||
|
|
||||||
async def list_runners(self, *, bound_plugins=None, use_cache=True):
|
async def list_runners(self, *, bound_plugins=None, use_cache=True):
|
||||||
self.calls.append({'bound_plugins': bound_plugins, 'use_cache': use_cache})
|
self.calls.append({'bound_plugins': bound_plugins, 'use_cache': use_cache})
|
||||||
return self.runners
|
return self.runners
|
||||||
|
|
||||||
|
|
||||||
def _handler(db_engine, admin_plugins=None, runner_registry=None):
|
class FakeProgrammaticOrchestrator:
|
||||||
|
def __init__(self, db_engine):
|
||||||
|
self.db_engine = db_engine
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
async def run(self, event, binding, bound_plugins=None, adapter_context=None, run_id=None):
|
||||||
|
self.calls.append(
|
||||||
|
{
|
||||||
|
'event': event,
|
||||||
|
'binding': binding,
|
||||||
|
'bound_plugins': bound_plugins,
|
||||||
|
'adapter_context': adapter_context,
|
||||||
|
'run_id': run_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
store = RunLedgerStore(self.db_engine)
|
||||||
|
await store.create_run(
|
||||||
|
run_id=run_id,
|
||||||
|
event_id=event.event_id,
|
||||||
|
binding_id=binding.binding_id,
|
||||||
|
runner_id=binding.runner_id,
|
||||||
|
conversation_id=event.conversation_id,
|
||||||
|
thread_id=event.thread_id,
|
||||||
|
workspace_id=event.workspace_id,
|
||||||
|
bot_id=event.bot_id,
|
||||||
|
authorization={'available_apis': {}},
|
||||||
|
metadata={'event_type': event.event_type, 'source': event.source},
|
||||||
|
status='running',
|
||||||
|
)
|
||||||
|
await store.finalize_run(run_id=run_id, status='completed', status_reason='done')
|
||||||
|
if False:
|
||||||
|
yield None
|
||||||
|
|
||||||
|
|
||||||
|
def _handler(db_engine, admin_plugins=None, runner_registry=None, orchestrator=None):
|
||||||
async def fake_disconnect():
|
async def fake_disconnect():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
fake_app = FakeApplication(db_engine, admin_plugins=admin_plugins, runner_registry=runner_registry)
|
fake_app = FakeApplication(
|
||||||
|
db_engine,
|
||||||
|
admin_plugins=admin_plugins,
|
||||||
|
runner_registry=runner_registry,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
)
|
||||||
return RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app)
|
return RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app)
|
||||||
|
|
||||||
|
|
||||||
@@ -1260,6 +1311,64 @@ async def test_runtime_register_heartbeat_and_list_actions(session_registry, db_
|
|||||||
assert [item['runtime_id'] for item in page.data['items']] == ['runtime_1']
|
assert [item['runtime_id'] for item in page.data['items']] == ['runtime_1']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_admin_run_create_starts_programmatic_run(db_engine):
|
||||||
|
orchestrator = FakeProgrammaticOrchestrator(db_engine)
|
||||||
|
runner_id = 'plugin:test/runner/default'
|
||||||
|
registry = FakeRunnerRegistry({runner_id: SimpleNamespace(id=runner_id)})
|
||||||
|
handler = _handler(
|
||||||
|
db_engine,
|
||||||
|
admin_plugins=[{'identity': 'admin/plugin', 'permissions': ['agent_run:admin']}],
|
||||||
|
runner_registry=registry,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
)
|
||||||
|
run_create = handler.actions[PluginToRuntimeAction.RUN_CREATE.value]
|
||||||
|
|
||||||
|
result = await run_create(
|
||||||
|
{
|
||||||
|
'caller_plugin_identity': 'admin/plugin',
|
||||||
|
'run_id': 'run_programmatic',
|
||||||
|
'runner_id': runner_id,
|
||||||
|
'input': {'text': 'work on issue 1', 'contents': [], 'attachments': []},
|
||||||
|
'conversation_id': 'conv_issue_board',
|
||||||
|
'event_type': 'api.invoked',
|
||||||
|
'wait_for_completion': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.code == 0
|
||||||
|
assert result.data['run_id'] == 'run_programmatic'
|
||||||
|
assert result.data['runner_id'] == runner_id
|
||||||
|
assert result.data['status'] == 'completed'
|
||||||
|
assert len(orchestrator.calls) == 1
|
||||||
|
call = orchestrator.calls[0]
|
||||||
|
assert call['run_id'] == 'run_programmatic'
|
||||||
|
assert call['event'].event_type == 'api.invoked'
|
||||||
|
assert call['event'].source == 'api'
|
||||||
|
assert call['event'].conversation_id == 'conv_issue_board'
|
||||||
|
assert call['binding'].runner_id == runner_id
|
||||||
|
assert call['binding'].delivery_policy.enable_reply is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_create_requires_admin_permission(db_engine):
|
||||||
|
orchestrator = FakeProgrammaticOrchestrator(db_engine)
|
||||||
|
handler = _handler(db_engine, orchestrator=orchestrator)
|
||||||
|
run_create = handler.actions[PluginToRuntimeAction.RUN_CREATE.value]
|
||||||
|
|
||||||
|
result = await run_create(
|
||||||
|
{
|
||||||
|
'caller_plugin_identity': 'regular/plugin',
|
||||||
|
'runner_id': 'plugin:test/runner/default',
|
||||||
|
'input': {'text': 'work'},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.code != 0
|
||||||
|
assert 'not authorized' in result.message
|
||||||
|
assert orchestrator.calls == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_run_claim_renew_and_release_actions(session_registry, db_engine):
|
async def test_run_claim_renew_and_release_actions(session_registry, db_engine):
|
||||||
await _register_session(
|
await _register_session(
|
||||||
|
|||||||
Reference in New Issue
Block a user