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:
+34
-39
@@ -28,7 +28,7 @@ from tests.e2e.utils.process_manager import find_project_root
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
||||
LOCAL_AGENT_RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
LOCAL_RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
FAKE_PROVIDER_UUID = 'e2e-fake-provider'
|
||||
FAKE_MODEL_UUID = 'e2e-fake-local-agent-model'
|
||||
E2E_TOOL_NAME = 'e2e_lookup'
|
||||
@@ -112,11 +112,11 @@ def _event(
|
||||
text: str,
|
||||
thread_id: str = 'e2e-local-agent-thread',
|
||||
):
|
||||
"""Build an AgentRunner event envelope for Local Agent E2E probes."""
|
||||
"""Build an Runner event envelope for Local Agent E2E probes."""
|
||||
from langbot.pkg.agent.runner.host_models import AgentEventEnvelope
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import ActorContext, SubjectContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.event import ActorContext, SubjectContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
|
||||
return AgentEventEnvelope(
|
||||
event_id=event_id,
|
||||
@@ -163,7 +163,7 @@ def _binding(
|
||||
return AgentBinding(
|
||||
binding_id=binding_id,
|
||||
scope=BindingScope(scope_type='global'),
|
||||
runner_id=LOCAL_AGENT_RUNNER_ID,
|
||||
runner_id=LOCAL_RUNNER_ID,
|
||||
runner_config=config,
|
||||
resource_policy=ResourcePolicy(
|
||||
allowed_model_uuids=[FAKE_MODEL_UUID],
|
||||
@@ -205,7 +205,7 @@ class _FakeToolManager:
|
||||
del query
|
||||
self.calls.append({'name': name, 'parameters': dict(parameters)})
|
||||
return {
|
||||
'value': f"tool-result:{parameters.get('query')}",
|
||||
'value': f'tool-result:{parameters.get("query")}',
|
||||
'source': 'fake-tool-manager',
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ async def _inject_fake_llm_model(ap) -> Any:
|
||||
return fake_requester
|
||||
|
||||
|
||||
async def _run_agent(ap, event, binding) -> list[Any]:
|
||||
async def _run_runner(ap, event, binding) -> list[Any]:
|
||||
"""Execute through the trusted Workspace context used by the real Host."""
|
||||
execution_context = await ap.plugin_connector._current_execution_context()
|
||||
return [
|
||||
@@ -463,20 +463,20 @@ async def _boot_local_agent_app(tmpdir: Path):
|
||||
)
|
||||
|
||||
execution_context = await ap.plugin_connector._current_execution_context()
|
||||
runners = await ap.agent_runner_registry.list_runners(execution_context, use_cache=False)
|
||||
if not any(runner.id == LOCAL_AGENT_RUNNER_ID for runner in runners):
|
||||
runners = await ap.runner_registry.list_runners(execution_context, use_cache=False)
|
||||
if not any(runner.id == LOCAL_RUNNER_ID for runner in runners):
|
||||
await ap.plugin_connector.install_plugin(
|
||||
PluginInstallSource.LOCAL,
|
||||
{'plugin_file': (tmpdir / 'langbot-local-agent.zip').read_bytes()},
|
||||
)
|
||||
|
||||
for _ in range(60):
|
||||
runners = await ap.agent_runner_registry.list_runners(execution_context, use_cache=False)
|
||||
if any(runner.id == LOCAL_AGENT_RUNNER_ID for runner in runners):
|
||||
runners = await ap.runner_registry.list_runners(execution_context, use_cache=False)
|
||||
if any(runner.id == LOCAL_RUNNER_ID for runner in runners):
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
raise AssertionError(f'{LOCAL_AGENT_RUNNER_ID} was not discovered after installation')
|
||||
raise AssertionError(f'{LOCAL_RUNNER_ID} was not discovered after installation')
|
||||
|
||||
return ap, run_task
|
||||
|
||||
@@ -521,7 +521,7 @@ def _run_local_agent_probe(tmpdir: Path, probe):
|
||||
return asyncio.run(_run())
|
||||
|
||||
|
||||
def test_local_agent_runner_uses_host_fake_provider_and_persists_ledger(
|
||||
def test_local_runner_uses_host_fake_provider_and_persists_ledger(
|
||||
local_agent_e2e_tmpdir,
|
||||
local_agent_e2e_config_path,
|
||||
local_agent_runtime_process,
|
||||
@@ -536,7 +536,7 @@ def test_local_agent_runner_uses_host_fake_provider_and_persists_ledger(
|
||||
conversation_id='e2e-local-agent-conversation',
|
||||
text='Say pong through the fake provider.',
|
||||
)
|
||||
messages = await _run_agent(ap, event, _binding())
|
||||
messages = await _run_runner(ap, event, _binding())
|
||||
return messages, list(fake_requester._count_tokens_payloads)
|
||||
|
||||
messages, token_payloads = _run_local_agent_probe(local_agent_e2e_tmpdir, _run_probe)
|
||||
@@ -553,13 +553,13 @@ def test_local_agent_runner_uses_host_fake_provider_and_persists_ledger(
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
run_row = conn.execute(
|
||||
"SELECT run_id, status, runner_id, status_reason FROM agent_run WHERE event_id = ?",
|
||||
'SELECT run_id, status, runner_id, status_reason FROM agent_run WHERE event_id = ?',
|
||||
('e2e-local-agent-event-001',),
|
||||
).fetchone()
|
||||
assert run_row is not None
|
||||
run_id, status, runner_id, status_reason = run_row
|
||||
assert status == 'completed'
|
||||
assert runner_id == LOCAL_AGENT_RUNNER_ID
|
||||
assert runner_id == LOCAL_RUNNER_ID
|
||||
assert status_reason == 'stop'
|
||||
|
||||
event_rows = conn.execute(
|
||||
@@ -578,12 +578,12 @@ def test_local_agent_runner_uses_host_fake_provider_and_persists_ledger(
|
||||
assert transcript_rows[0][1] == 'Say pong through the fake provider.'
|
||||
assert transcript_rows[1][1] == 'Fake LLM response'
|
||||
assert transcript_rows[1][2] == run_id
|
||||
assert transcript_rows[1][3] == LOCAL_AGENT_RUNNER_ID
|
||||
assert transcript_rows[1][3] == LOCAL_RUNNER_ID
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_local_agent_runner_executes_authorized_tool_loop_through_host_action(
|
||||
def test_local_runner_executes_authorized_tool_loop_through_host_action(
|
||||
local_agent_e2e_tmpdir,
|
||||
local_agent_e2e_config_path,
|
||||
local_agent_runtime_process,
|
||||
@@ -613,7 +613,7 @@ def test_local_agent_runner_executes_authorized_tool_loop_through_host_action(
|
||||
'tool-execution-mode': 'serial',
|
||||
},
|
||||
)
|
||||
messages = await _run_agent(ap, event, binding)
|
||||
messages = await _run_runner(ap, event, binding)
|
||||
return messages, tool_mgr.calls, _invoke_payload_texts(fake_requester)
|
||||
|
||||
messages, tool_calls, invoke_payload_texts = _run_local_agent_probe(local_agent_e2e_tmpdir, _run_probe)
|
||||
@@ -627,7 +627,7 @@ def test_local_agent_runner_executes_authorized_tool_loop_through_host_action(
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
run_row = conn.execute(
|
||||
"SELECT run_id, status, status_reason FROM agent_run WHERE event_id = ?",
|
||||
'SELECT run_id, status, status_reason FROM agent_run WHERE event_id = ?',
|
||||
('e2e-local-agent-tool-event-001',),
|
||||
).fetchone()
|
||||
assert run_row is not None
|
||||
@@ -652,7 +652,7 @@ def test_local_agent_runner_executes_authorized_tool_loop_through_host_action(
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_local_agent_runner_retrieves_authorized_rag_context_through_host_action(
|
||||
def test_local_runner_retrieves_authorized_rag_context_through_host_action(
|
||||
local_agent_e2e_tmpdir,
|
||||
local_agent_e2e_config_path,
|
||||
local_agent_runtime_process,
|
||||
@@ -679,7 +679,7 @@ def test_local_agent_runner_retrieves_authorized_rag_context_through_host_action
|
||||
'retrieval-top-k': 1,
|
||||
},
|
||||
)
|
||||
messages = await _run_agent(ap, event, binding)
|
||||
messages = await _run_runner(ap, event, binding)
|
||||
return messages, fake_kb.retrieve_calls, _invoke_payload_texts(fake_requester)
|
||||
|
||||
messages, retrieve_calls, invoke_payload_texts = _run_local_agent_probe(local_agent_e2e_tmpdir, _run_probe)
|
||||
@@ -702,7 +702,7 @@ def test_local_agent_runner_retrieves_authorized_rag_context_through_host_action
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
run_row = conn.execute(
|
||||
"SELECT run_id, status FROM agent_run WHERE event_id = ?",
|
||||
'SELECT run_id, status FROM agent_run WHERE event_id = ?',
|
||||
('e2e-local-agent-rag-event-001',),
|
||||
).fetchone()
|
||||
assert run_row is not None
|
||||
@@ -720,7 +720,7 @@ def test_local_agent_runner_retrieves_authorized_rag_context_through_host_action
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_local_agent_runner_compacts_history_and_persists_checkpoint(
|
||||
def test_local_runner_compacts_history_and_persists_checkpoint(
|
||||
local_agent_e2e_tmpdir,
|
||||
local_agent_e2e_config_path,
|
||||
local_agent_runtime_process,
|
||||
@@ -745,8 +745,7 @@ def test_local_agent_runner_compacts_history_and_persists_checkpoint(
|
||||
conversation_id='e2e-local-agent-compaction-conversation',
|
||||
role='user' if index % 2 == 0 else 'assistant',
|
||||
content=(
|
||||
f'HIST_SENTINEL-{index} '
|
||||
'This is intentionally long deterministic history for compaction. ' * 10
|
||||
f'HIST_SENTINEL-{index} This is intentionally long deterministic history for compaction. ' * 10
|
||||
),
|
||||
thread_id='e2e-local-agent-thread',
|
||||
item_type='message',
|
||||
@@ -767,7 +766,7 @@ def test_local_agent_runner_compacts_history_and_persists_checkpoint(
|
||||
'context-history-fetch-limit': 20,
|
||||
},
|
||||
)
|
||||
messages = await _run_agent(ap, event, binding)
|
||||
messages = await _run_runner(ap, event, binding)
|
||||
return messages, _invoke_payload_texts(fake_requester), fake_requester._invoke_count
|
||||
|
||||
messages, invoke_payload_texts, invoke_count = _run_local_agent_probe(local_agent_e2e_tmpdir, _run_probe)
|
||||
@@ -782,7 +781,7 @@ def test_local_agent_runner_compacts_history_and_persists_checkpoint(
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
run_row = conn.execute(
|
||||
"SELECT run_id, status FROM agent_run WHERE event_id = ?",
|
||||
'SELECT run_id, status FROM agent_run WHERE event_id = ?',
|
||||
('e2e-local-agent-compaction-event-001',),
|
||||
).fetchone()
|
||||
assert run_row is not None
|
||||
@@ -799,7 +798,7 @@ def test_local_agent_runner_compacts_history_and_persists_checkpoint(
|
||||
assert event_types == ['message.completed', 'run.completed']
|
||||
|
||||
state_row = conn.execute(
|
||||
"SELECT value_json FROM agent_runner_state WHERE state_key = 'runner.compaction.checkpoint'"
|
||||
"SELECT value_json FROM runner_state WHERE state_key = 'runner.compaction.checkpoint'"
|
||||
).fetchone()
|
||||
assert state_row is not None
|
||||
checkpoint = json.loads(state_row[0])
|
||||
@@ -812,7 +811,7 @@ def test_local_agent_runner_compacts_history_and_persists_checkpoint(
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_local_agent_runner_combines_rag_compaction_and_multi_turn_tool_loop(
|
||||
def test_local_runner_combines_rag_compaction_and_multi_turn_tool_loop(
|
||||
local_agent_e2e_tmpdir,
|
||||
local_agent_e2e_config_path,
|
||||
local_agent_runtime_process,
|
||||
@@ -883,7 +882,7 @@ def test_local_agent_runner_combines_rag_compaction_and_multi_turn_tool_loop(
|
||||
'context-history-fetch-limit': 25,
|
||||
},
|
||||
)
|
||||
messages = await _run_agent(ap, event, binding)
|
||||
messages = await _run_runner(ap, event, binding)
|
||||
return (
|
||||
messages,
|
||||
tool_mgr.calls,
|
||||
@@ -926,7 +925,7 @@ def test_local_agent_runner_combines_rag_compaction_and_multi_turn_tool_loop(
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
run_row = conn.execute(
|
||||
"SELECT run_id, status, status_reason FROM agent_run WHERE event_id = ?",
|
||||
'SELECT run_id, status, status_reason FROM agent_run WHERE event_id = ?',
|
||||
('e2e-local-agent-combo-event-001',),
|
||||
).fetchone()
|
||||
assert run_row is not None
|
||||
@@ -952,15 +951,11 @@ def test_local_agent_runner_combines_rag_compaction_and_multi_turn_tool_loop(
|
||||
assert 'COMBO_FINAL' in event_rows[4][1]
|
||||
|
||||
state_rows = conn.execute(
|
||||
"SELECT value_json FROM agent_runner_state WHERE state_key = 'runner.compaction.checkpoint'"
|
||||
"SELECT value_json FROM runner_state WHERE state_key = 'runner.compaction.checkpoint'"
|
||||
).fetchall()
|
||||
checkpoints = [json.loads(row[0]) for row in state_rows]
|
||||
checkpoint = next(
|
||||
(
|
||||
item
|
||||
for item in checkpoints
|
||||
if item.get('conversation_id') == 'e2e-local-agent-combo-conversation'
|
||||
),
|
||||
(item for item in checkpoints if item.get('conversation_id') == 'e2e-local-agent-combo-conversation'),
|
||||
None,
|
||||
)
|
||||
assert checkpoint is not None
|
||||
+95
-104
@@ -1,7 +1,7 @@
|
||||
"""E2E tests for pluginized AgentRunner execution.
|
||||
"""E2E tests for pluginized Runner execution.
|
||||
|
||||
This module starts the real LangBot backend with the plugin system enabled and
|
||||
loads a deterministic AgentRunner plugin through the real SDK Plugin Runtime.
|
||||
loads a deterministic Runner plugin through the real SDK Plugin Runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,22 +29,22 @@ QA_RUNNER_ID = 'plugin:e2e/agent-runner-qa/default'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def agent_runner_e2e_port():
|
||||
"""Port for the AgentRunner plugin-runtime E2E process."""
|
||||
def runner_e2e_port():
|
||||
"""Port for the Runner plugin-runtime E2E process."""
|
||||
return 15310
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def agent_runner_e2e_tmpdir():
|
||||
"""Create temporary directory for AgentRunner E2E testing."""
|
||||
tmpdir = Path(tempfile.mkdtemp(prefix='langbot_agent_runner_e2e_'))
|
||||
def runner_e2e_tmpdir():
|
||||
"""Create temporary directory for Runner E2E testing."""
|
||||
tmpdir = Path(tempfile.mkdtemp(prefix='langbot_runner_e2e_'))
|
||||
yield tmpdir
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
def _write_qa_agent_runner_plugin(plugin_root: Path) -> None:
|
||||
"""Write a deterministic AgentRunner plugin used by this E2E."""
|
||||
runner_dir = plugin_root / 'components' / 'agent_runner'
|
||||
def _write_qa_runner_plugin(plugin_root: Path) -> None:
|
||||
"""Write a deterministic Runner plugin used by this E2E."""
|
||||
runner_dir = plugin_root / 'components' / 'runner'
|
||||
runner_dir.mkdir(parents=True, exist_ok=True)
|
||||
(plugin_root / 'assets').mkdir(parents=True, exist_ok=True)
|
||||
(plugin_root / 'assets' / 'icon.svg').write_text(
|
||||
@@ -61,27 +61,24 @@ def _write_qa_agent_runner_plugin(plugin_root: Path) -> None:
|
||||
name: agent-runner-qa
|
||||
version: 0.1.0
|
||||
label:
|
||||
en_US: AgentRunner QA
|
||||
zh_Hans: AgentRunner QA
|
||||
en_US: Runner QA
|
||||
zh_Hans: Runner QA
|
||||
description:
|
||||
en_US: Deterministic AgentRunner E2E probe.
|
||||
zh_Hans: 确定性的 AgentRunner E2E 探针。
|
||||
en_US: Deterministic Runner E2E probe.
|
||||
zh_Hans: 确定性的 Runner E2E 探针。
|
||||
icon: assets/icon.svg
|
||||
spec:
|
||||
version: 0.1.0
|
||||
config: []
|
||||
components:
|
||||
AgentRunner:
|
||||
Runner:
|
||||
fromDirs:
|
||||
- path: components/agent_runner/
|
||||
EventProcessor:
|
||||
fromDirs:
|
||||
- path: components/event_processor/
|
||||
- path: components/runner/
|
||||
pages: []
|
||||
execution:
|
||||
python:
|
||||
path: main.py
|
||||
attr: AgentRunnerQAPlugin
|
||||
attr: RunnerQAPlugin
|
||||
"""
|
||||
).strip()
|
||||
+ '\n',
|
||||
@@ -95,7 +92,7 @@ def _write_qa_agent_runner_plugin(plugin_root: Path) -> None:
|
||||
from langbot_plugin.api.definition.plugin import BasePlugin
|
||||
|
||||
|
||||
class AgentRunnerQAPlugin(BasePlugin):
|
||||
class RunnerQAPlugin(BasePlugin):
|
||||
async def initialize(self) -> None:
|
||||
pass
|
||||
"""
|
||||
@@ -107,7 +104,7 @@ def _write_qa_agent_runner_plugin(plugin_root: Path) -> None:
|
||||
textwrap.dedent(
|
||||
"""
|
||||
apiVersion: langbot/v1
|
||||
kind: AgentRunner
|
||||
kind: Runner
|
||||
metadata:
|
||||
name: default
|
||||
label:
|
||||
@@ -124,7 +121,7 @@ def _write_qa_agent_runner_plugin(plugin_root: Path) -> None:
|
||||
execution:
|
||||
python:
|
||||
path: default.py
|
||||
attr: DefaultAgentRunner
|
||||
attr: DefaultRunner
|
||||
"""
|
||||
).strip()
|
||||
+ '\n',
|
||||
@@ -137,42 +134,42 @@ def _write_qa_agent_runner_plugin(plugin_root: Path) -> None:
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from langbot_plugin.api.definition.components.agent_runner.runner import AgentRunner
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.context import AgentRunContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.result import AgentRunResult
|
||||
from langbot_plugin.api.definition.components.runner.runner import Runner
|
||||
from langbot_plugin.api.entities.builtin.runner.context import RunnerContext
|
||||
from langbot_plugin.api.entities.builtin.runner.result import RunnerResult
|
||||
from langbot_plugin.api.entities.builtin.provider.message import Message
|
||||
|
||||
|
||||
class DefaultAgentRunner(AgentRunner):
|
||||
async def run(self, ctx: AgentRunContext) -> AsyncGenerator[AgentRunResult, None]:
|
||||
class DefaultRunner(Runner):
|
||||
async def run(self, ctx: RunnerContext) -> AsyncGenerator[RunnerResult, None]:
|
||||
text = ctx.input.to_text()
|
||||
yield AgentRunResult.message_completed(
|
||||
yield RunnerResult.message_completed(
|
||||
ctx.run_id,
|
||||
Message(role='assistant', content=f'e2e echo: {text}'),
|
||||
)
|
||||
yield AgentRunResult.state_updated(
|
||||
yield RunnerResult.state_updated(
|
||||
ctx.run_id,
|
||||
'e2e.echo_count',
|
||||
{'count': 1},
|
||||
scope='conversation',
|
||||
)
|
||||
yield AgentRunResult.run_completed(ctx.run_id, finish_reason='stop')
|
||||
yield RunnerResult.run_completed(ctx.run_id, finish_reason='stop')
|
||||
"""
|
||||
).strip()
|
||||
+ '\n',
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
processor_dir = plugin_root / 'components' / 'event_processor'
|
||||
processor_dir.mkdir(parents=True)
|
||||
(processor_dir / 'default.yaml').write_text(
|
||||
processor_dir = runner_dir
|
||||
(processor_dir / 'welcome.yaml').write_text(
|
||||
textwrap.dedent("""
|
||||
apiVersion: langbot/v1
|
||||
kind: EventProcessor
|
||||
kind: Runner
|
||||
metadata:
|
||||
name: default
|
||||
name: welcome
|
||||
label: {en_US: Welcome processor, zh_Hans: Welcome processor}
|
||||
spec:
|
||||
usages: [event]
|
||||
events: [group.member_joined]
|
||||
config:
|
||||
- name: greeting
|
||||
@@ -184,20 +181,20 @@ def _write_qa_agent_runner_plugin(plugin_root: Path) -> None:
|
||||
permissions:
|
||||
tools: [detail, call]
|
||||
execution:
|
||||
python: {path: default.py, attr: WelcomeProcessor}
|
||||
python: {path: welcome.py, attr: WelcomeProcessor}
|
||||
""")
|
||||
)
|
||||
(processor_dir / 'default.py').write_text(
|
||||
(processor_dir / 'welcome.py').write_text(
|
||||
textwrap.dedent("""
|
||||
from langbot_plugin.api.definition.components.event_processor import EventProcessor, EventProcessorContext
|
||||
from langbot_plugin.api.definition.components.runner import Runner, RunnerContext
|
||||
from langbot_plugin.api.entities.builtin.platform.events import MemberJoinedEvent
|
||||
|
||||
class WelcomeProcessor(EventProcessor):
|
||||
class WelcomeProcessor(Runner):
|
||||
async def initialize(self):
|
||||
@self.handler(MemberJoinedEvent)
|
||||
async def handle(ctx: EventProcessorContext):
|
||||
await ctx.log('Handling ' + str(ctx.event.member.id))
|
||||
result = await ctx.reply(ctx.config['greeting'] + ', ' + (ctx.event.member.nickname or str(ctx.event.member.id)))
|
||||
async def handle(ctx: RunnerContext):
|
||||
await ctx.log('Handling ' + str(ctx.platform_event.member.id))
|
||||
result = await ctx.reply(ctx.config['greeting'] + ', ' + (ctx.platform_event.member.nickname or str(ctx.platform_event.member.id)))
|
||||
await ctx.log('Reply simulated: ' + str(result.get('mock')))
|
||||
""")
|
||||
)
|
||||
@@ -211,7 +208,7 @@ def _free_port() -> int:
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def agent_runner_runtime_ports():
|
||||
def runner_runtime_ports():
|
||||
"""Control/debug ports for the standalone plugin runtime."""
|
||||
control_port = _free_port()
|
||||
debug_port = _free_port()
|
||||
@@ -221,17 +218,17 @@ def agent_runner_runtime_ports():
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def agent_runner_e2e_config_path(agent_runner_e2e_tmpdir, agent_runner_e2e_port, agent_runner_runtime_ports):
|
||||
"""Create a plugin-enabled config and deterministic AgentRunner fixture."""
|
||||
config_path = create_minimal_config(agent_runner_e2e_tmpdir, port=agent_runner_e2e_port)
|
||||
create_test_directories(agent_runner_e2e_tmpdir)
|
||||
def runner_e2e_config_path(runner_e2e_tmpdir, runner_e2e_port, runner_runtime_ports):
|
||||
"""Create a plugin-enabled config and deterministic Runner fixture."""
|
||||
config_path = create_minimal_config(runner_e2e_tmpdir, port=runner_e2e_port)
|
||||
create_test_directories(runner_e2e_tmpdir)
|
||||
|
||||
import yaml
|
||||
|
||||
with open(config_path, encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
config['api']['global_api_key'] = 'e2e-agent-runner-key'
|
||||
runtime_control_port, _runtime_debug_port = agent_runner_runtime_ports
|
||||
runtime_control_port, _runtime_debug_port = runner_runtime_ports
|
||||
config['plugin']['enable'] = True
|
||||
config['plugin']['runtime_ws_url'] = f'ws://127.0.0.1:{runtime_control_port}/control/ws'
|
||||
config['plugin']['enable_marketplace'] = False
|
||||
@@ -240,10 +237,10 @@ def agent_runner_e2e_config_path(agent_runner_e2e_tmpdir, agent_runner_e2e_port,
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(config, f, default_flow_style=False)
|
||||
|
||||
plugin_source = agent_runner_e2e_tmpdir / 'agent-runner-qa-package'
|
||||
_write_qa_agent_runner_plugin(plugin_source)
|
||||
plugin_source = runner_e2e_tmpdir / 'agent-runner-qa-package'
|
||||
_write_qa_runner_plugin(plugin_source)
|
||||
shutil.make_archive(
|
||||
str(agent_runner_e2e_tmpdir / 'agent-runner-qa'),
|
||||
str(runner_e2e_tmpdir / 'agent-runner-qa'),
|
||||
'zip',
|
||||
root_dir=plugin_source,
|
||||
)
|
||||
@@ -251,11 +248,11 @@ def agent_runner_e2e_config_path(agent_runner_e2e_tmpdir, agent_runner_e2e_port,
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def agent_runner_runtime_process(agent_runner_e2e_tmpdir, agent_runner_runtime_ports):
|
||||
def runner_runtime_process(runner_e2e_tmpdir, runner_runtime_ports):
|
||||
"""Start the real SDK plugin runtime over WebSocket."""
|
||||
control_port, debug_port = agent_runner_runtime_ports
|
||||
stdout_path = agent_runner_e2e_tmpdir / 'plugin-runtime.stdout.log'
|
||||
stderr_path = agent_runner_e2e_tmpdir / 'plugin-runtime.stderr.log'
|
||||
control_port, debug_port = runner_runtime_ports
|
||||
stdout_path = runner_e2e_tmpdir / 'plugin-runtime.stdout.log'
|
||||
stderr_path = runner_e2e_tmpdir / 'plugin-runtime.stderr.log'
|
||||
stdout_file = open(stdout_path, 'wb')
|
||||
stderr_file = open(stderr_path, 'wb')
|
||||
proc = subprocess.Popen(
|
||||
@@ -269,7 +266,7 @@ def agent_runner_runtime_process(agent_runner_e2e_tmpdir, agent_runner_runtime_p
|
||||
'--ws-debug-port',
|
||||
str(debug_port),
|
||||
],
|
||||
cwd=agent_runner_e2e_tmpdir,
|
||||
cwd=runner_e2e_tmpdir,
|
||||
stdout=stdout_file,
|
||||
stderr=stderr_file,
|
||||
start_new_session=True,
|
||||
@@ -286,18 +283,18 @@ def agent_runner_runtime_process(agent_runner_e2e_tmpdir, agent_runner_runtime_p
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def agent_runner_langbot_process(
|
||||
agent_runner_e2e_config_path,
|
||||
agent_runner_e2e_port,
|
||||
agent_runner_e2e_tmpdir,
|
||||
agent_runner_runtime_process,
|
||||
def runner_langbot_process(
|
||||
runner_e2e_config_path,
|
||||
runner_e2e_port,
|
||||
runner_e2e_tmpdir,
|
||||
runner_runtime_process,
|
||||
):
|
||||
"""Start real LangBot with plugin runtime enabled."""
|
||||
project_root = find_project_root()
|
||||
proc = LangBotProcess(
|
||||
project_root=project_root,
|
||||
work_dir=agent_runner_e2e_tmpdir,
|
||||
port=agent_runner_e2e_port,
|
||||
work_dir=runner_e2e_tmpdir,
|
||||
port=runner_e2e_port,
|
||||
timeout=180,
|
||||
debug=True,
|
||||
cli_args=['--standalone-runtime'],
|
||||
@@ -306,7 +303,7 @@ def agent_runner_langbot_process(
|
||||
success = proc.start()
|
||||
if not success:
|
||||
stdout, stderr = proc.get_logs()
|
||||
pytest.fail(f'LangBot failed to start with AgentRunner plugin runtime:\nstdout: {stdout}\nstderr: {stderr}')
|
||||
pytest.fail(f'LangBot failed to start with Runner plugin runtime:\nstdout: {stdout}\nstderr: {stderr}')
|
||||
|
||||
yield proc
|
||||
|
||||
@@ -314,10 +311,10 @@ def agent_runner_langbot_process(
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_runner_client(agent_runner_e2e_port, agent_runner_langbot_process):
|
||||
"""HTTP client for the AgentRunner E2E backend."""
|
||||
def runner_client(runner_e2e_port, runner_langbot_process):
|
||||
"""HTTP client for the Runner E2E backend."""
|
||||
with httpx.Client(
|
||||
base_url=f'http://127.0.0.1:{agent_runner_e2e_port}',
|
||||
base_url=f'http://127.0.0.1:{runner_e2e_port}',
|
||||
timeout=90.0,
|
||||
trust_env=False,
|
||||
) as client:
|
||||
@@ -396,29 +393,25 @@ def _ensure_qa_plugin(client: httpx.Client, token: str, package_path: Path) -> N
|
||||
_install_qa_plugin(client, token, package_path)
|
||||
|
||||
|
||||
def test_plugin_runtime_discovers_agent_runner(
|
||||
agent_runner_client,
|
||||
agent_runner_langbot_process,
|
||||
agent_runner_e2e_tmpdir,
|
||||
def test_plugin_runtime_discovers_runner(
|
||||
runner_client,
|
||||
runner_langbot_process,
|
||||
runner_e2e_tmpdir,
|
||||
):
|
||||
"""Pipeline metadata should include the real runtime-discovered QA runner."""
|
||||
token = _init_and_auth(agent_runner_client)
|
||||
token = _init_and_auth(runner_client)
|
||||
_ensure_qa_plugin(
|
||||
agent_runner_client,
|
||||
runner_client,
|
||||
token,
|
||||
agent_runner_e2e_tmpdir / 'agent-runner-qa.zip',
|
||||
runner_e2e_tmpdir / 'agent-runner-qa.zip',
|
||||
)
|
||||
option_names = _wait_for_qa_runner(agent_runner_client, token)
|
||||
option_names = _wait_for_qa_runner(runner_client, token)
|
||||
if QA_RUNNER_ID in option_names:
|
||||
return
|
||||
|
||||
host_stdout, host_stderr = agent_runner_langbot_process.get_logs()
|
||||
runtime_stdout = (agent_runner_e2e_tmpdir / 'plugin-runtime.stdout.log').read_text(
|
||||
encoding='utf-8', errors='replace'
|
||||
)
|
||||
runtime_stderr = (agent_runner_e2e_tmpdir / 'plugin-runtime.stderr.log').read_text(
|
||||
encoding='utf-8', errors='replace'
|
||||
)
|
||||
host_stdout, host_stderr = runner_langbot_process.get_logs()
|
||||
runtime_stdout = (runner_e2e_tmpdir / 'plugin-runtime.stdout.log').read_text(encoding='utf-8', errors='replace')
|
||||
runtime_stderr = (runner_e2e_tmpdir / 'plugin-runtime.stderr.log').read_text(encoding='utf-8', errors='replace')
|
||||
assert QA_RUNNER_ID in option_names, (
|
||||
f'{QA_RUNNER_ID} was not discovered\n'
|
||||
f'Host stdout (tail):\n{host_stdout[-20_000:]}\nHost stderr (tail):\n{host_stderr[-20_000:]}\n'
|
||||
@@ -427,26 +420,26 @@ def test_plugin_runtime_discovers_agent_runner(
|
||||
)
|
||||
|
||||
|
||||
def test_host_orchestrator_runs_agent_runner_and_records_ledger(
|
||||
agent_runner_client,
|
||||
agent_runner_langbot_process,
|
||||
agent_runner_e2e_tmpdir,
|
||||
def test_host_orchestrator_runs_runner_and_records_ledger(
|
||||
runner_client,
|
||||
runner_langbot_process,
|
||||
runner_e2e_tmpdir,
|
||||
):
|
||||
"""Create/configure/debug an Agent through HTTP and persist Runner side effects."""
|
||||
del agent_runner_langbot_process
|
||||
token = _init_and_auth(agent_runner_client)
|
||||
del runner_langbot_process
|
||||
token = _init_and_auth(runner_client)
|
||||
_ensure_qa_plugin(
|
||||
agent_runner_client,
|
||||
runner_client,
|
||||
token,
|
||||
agent_runner_e2e_tmpdir / 'agent-runner-qa.zip',
|
||||
runner_e2e_tmpdir / 'agent-runner-qa.zip',
|
||||
)
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
create_response = agent_runner_client.post(
|
||||
create_response = runner_client.post(
|
||||
'/api/v1/agents',
|
||||
headers=headers,
|
||||
json={
|
||||
'kind': 'agent',
|
||||
'name': 'AgentRunner E2E Agent',
|
||||
'name': 'Runner E2E Agent',
|
||||
'description': 'Exercises the installed QA Runner.',
|
||||
'emoji': 'QA',
|
||||
'supported_event_patterns': ['message.*'],
|
||||
@@ -462,7 +455,7 @@ def test_host_orchestrator_runs_agent_runner_and_records_ledger(
|
||||
assert create_payload['code'] == 0, create_payload
|
||||
agent_uuid = create_payload['data']['uuid']
|
||||
|
||||
get_response = agent_runner_client.get(f'/api/v1/agents/{agent_uuid}', headers=headers)
|
||||
get_response = runner_client.get(f'/api/v1/agents/{agent_uuid}', headers=headers)
|
||||
assert get_response.status_code == 200, get_response.text
|
||||
stored_agent = get_response.json()['data']['agent']
|
||||
assert stored_agent['config']['allowed_platform_tools'] == [
|
||||
@@ -470,7 +463,7 @@ def test_host_orchestrator_runs_agent_runner_and_records_ledger(
|
||||
'platform_get_user_info',
|
||||
]
|
||||
|
||||
debug_response = agent_runner_client.post(
|
||||
debug_response = runner_client.post(
|
||||
f'/api/v1/agents/{agent_uuid}/debug',
|
||||
headers=headers,
|
||||
json={
|
||||
@@ -486,7 +479,7 @@ def test_host_orchestrator_runs_agent_runner_and_records_ledger(
|
||||
assert result['final_text'] == 'e2e echo: hello from orchestrator e2e'
|
||||
assert result['outputs'][0]['role'] == 'assistant'
|
||||
|
||||
db_path = agent_runner_e2e_tmpdir / 'data' / 'langbot.db'
|
||||
db_path = runner_e2e_tmpdir / 'data' / 'langbot.db'
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
run_row = conn.execute(
|
||||
@@ -504,9 +497,7 @@ def test_host_orchestrator_runs_agent_runner_and_records_ledger(
|
||||
}
|
||||
assert {'state.updated', 'message.completed', 'run.completed'}.issubset(event_types)
|
||||
|
||||
state_row = conn.execute(
|
||||
"SELECT value_json FROM agent_runner_state WHERE state_key = 'e2e.echo_count'"
|
||||
).fetchone()
|
||||
state_row = conn.execute("SELECT value_json FROM runner_state WHERE state_key = 'e2e.echo_count'").fetchone()
|
||||
assert state_row is not None
|
||||
assert '"count": 1' in state_row[0]
|
||||
finally:
|
||||
@@ -514,17 +505,17 @@ def test_host_orchestrator_runs_agent_runner_and_records_ledger(
|
||||
|
||||
|
||||
def test_event_processor_real_runtime_logs_actions_and_instance_isolation(
|
||||
agent_runner_client,
|
||||
agent_runner_e2e_tmpdir,
|
||||
runner_client,
|
||||
runner_e2e_tmpdir,
|
||||
):
|
||||
client = agent_runner_client
|
||||
client = runner_client
|
||||
token = _init_and_auth(client)
|
||||
_ensure_qa_plugin(client, token, agent_runner_e2e_tmpdir / 'agent-runner-qa.zip')
|
||||
_ensure_qa_plugin(client, token, runner_e2e_tmpdir / 'agent-runner-qa.zip')
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
metadata_response = client.get('/api/v1/agents/_/metadata', headers=headers).json()
|
||||
assert metadata_response['code'] == 0, metadata_response
|
||||
metadata = metadata_response['data']
|
||||
ref = 'event_processor:e2e/agent-runner-qa/default'
|
||||
ref = 'plugin:e2e/agent-runner-qa/welcome'
|
||||
assert any(item['id'] == ref for item in metadata['event_processors']), metadata
|
||||
assert ref not in _wait_for_qa_runner(client, token)
|
||||
created = []
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for current AgentRunner config resolution."""
|
||||
"""Tests for current Runner config resolution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for persisted AgentRunner config templates."""
|
||||
"""Tests for persisted Runner config templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
"""Tests for ContextAccess.state determination in AgentRunContextBuilder.
|
||||
"""Tests for ContextAccess.state determination in RunnerContextBuilder.
|
||||
|
||||
Tests focus on:
|
||||
- Event-first mode: state=True when enable_state=True and state_scopes non-empty
|
||||
- Event-first mode: state=False when enable_state=False
|
||||
- Legacy Query mode: state=False (no persistent state API)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from langbot.pkg.agent.runner.context_builder import AgentRunContextBuilder
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.context_builder import RunnerContextBuilder
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.host_models import AgentEventEnvelope, AgentBinding, BindingScope, StatePolicy
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import ActorContext
|
||||
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.event import ActorContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
|
||||
|
||||
class MockApplication:
|
||||
"""Mock Application for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = MagicMock()
|
||||
self.persistence_mgr = MagicMock()
|
||||
@@ -28,8 +30,8 @@ class MockApplication:
|
||||
|
||||
def make_descriptor(
|
||||
permissions: dict | None = None,
|
||||
) -> AgentRunnerDescriptor:
|
||||
return AgentRunnerDescriptor(
|
||||
) -> RunnerDescriptor:
|
||||
return RunnerDescriptor(
|
||||
id='plugin:test/runner/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -91,7 +93,7 @@ class TestContextAccessStateDetermination:
|
||||
),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call to _build_context_access
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -112,7 +114,7 @@ class TestContextAccessStateDetermination:
|
||||
),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -133,7 +135,7 @@ class TestContextAccessStateDetermination:
|
||||
),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -144,7 +146,7 @@ class TestContextAccessStateDetermination:
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_binding_sets_state_false(self, mock_app, mock_event, mock_descriptor):
|
||||
"""ContextAccess.state=False when no binding is provided."""
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call without binding
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding=None)
|
||||
@@ -180,7 +182,7 @@ class TestContextAccessStateDetermination:
|
||||
),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -201,7 +203,7 @@ class TestContextAccessStateDetermination:
|
||||
),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -277,7 +279,7 @@ class TestContextAccessOtherAPIs:
|
||||
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -301,7 +303,7 @@ class TestContextAccessOtherAPIs:
|
||||
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -324,7 +326,7 @@ class TestContextAccessOtherAPIs:
|
||||
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
# Real call
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
@@ -350,7 +352,7 @@ class TestContextAccessOtherAPIs:
|
||||
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
|
||||
)
|
||||
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
context_access = await builder._build_context_access(mock_event, mock_descriptor, binding)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test that LangBot context builder output validates against SDK AgentRunContext."""
|
||||
"""Test that LangBot context builder output validates against SDK RunnerContext."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,26 +7,26 @@ from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
# SDK imports for validation
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.context import AgentRunContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import AgentEventContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.context_access import ContextAccess
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.resources import AgentResources
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.runtime import AgentRuntimeContext
|
||||
from langbot_plugin.api.entities.builtin.runner.context import RunnerContext
|
||||
from langbot_plugin.api.entities.builtin.runner.event import AgentEventContext
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.context_access import ContextAccess
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.resources import AgentResources
|
||||
from langbot_plugin.api.entities.builtin.runner.runtime import AgentRuntimeContext
|
||||
|
||||
# LangBot imports
|
||||
from langbot.pkg.agent.runner.context_builder import (
|
||||
AgentRunContextBuilder,
|
||||
RunnerContextBuilder,
|
||||
AgentResources as BuilderResources,
|
||||
)
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.host_models import AgentEventEnvelope, AgentBinding, BindingScope
|
||||
from langbot.pkg.core import app
|
||||
|
||||
|
||||
class TestContextValidation:
|
||||
"""Test that context builder output validates against SDK AgentRunContext."""
|
||||
"""Test that context builder output validates against SDK RunnerContext."""
|
||||
|
||||
def _make_mock_app(self):
|
||||
"""Create a mock application."""
|
||||
@@ -40,9 +40,9 @@ class TestContextValidation:
|
||||
|
||||
def _make_event_envelope(self) -> AgentEventEnvelope:
|
||||
"""Create a test event envelope."""
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import ActorContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput as EventInput
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.event import ActorContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput as EventInput
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
|
||||
return AgentEventEnvelope(
|
||||
event_id='evt_1',
|
||||
@@ -90,7 +90,7 @@ class TestContextValidation:
|
||||
|
||||
def _make_descriptor(self):
|
||||
"""Create a mock runner descriptor."""
|
||||
return AgentRunnerDescriptor(
|
||||
return RunnerDescriptor(
|
||||
id='plugin:test/plugin/runner',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -106,9 +106,9 @@ class TestContextValidation:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_from_event_validates(self):
|
||||
"""Test that build_context_from_event output validates against SDK AgentRunContext."""
|
||||
"""Test that build_context_from_event output validates against SDK RunnerContext."""
|
||||
mock_app = self._make_mock_app()
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
event = self._make_event_envelope()
|
||||
binding = self._make_binding()
|
||||
@@ -136,9 +136,9 @@ class TestContextValidation:
|
||||
resources=resources,
|
||||
)
|
||||
|
||||
# Validate it can be parsed by SDK AgentRunContext
|
||||
# Validate it can be parsed by SDK RunnerContext
|
||||
# This will raise ValidationError if invalid
|
||||
validated = AgentRunContext.model_validate(context_dict)
|
||||
validated = RunnerContext.model_validate(context_dict)
|
||||
|
||||
# Verify required fields
|
||||
assert validated.run_id is not None
|
||||
@@ -179,13 +179,13 @@ class TestContextValidation:
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_preserves_interaction_protocol_fields(self):
|
||||
"""Validated submissions and delivery capabilities survive the final context projection."""
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.interaction import (
|
||||
from langbot_plugin.api.entities.builtin.runner.interaction import (
|
||||
InteractionDeliveryCapabilities,
|
||||
InteractionSubmission,
|
||||
)
|
||||
|
||||
mock_app = self._make_mock_app()
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
event = self._make_event_envelope()
|
||||
event.event_type = 'interaction.submitted'
|
||||
event.input.interaction = InteractionSubmission(
|
||||
@@ -211,7 +211,7 @@ class TestContextValidation:
|
||||
resources=self._make_resources(),
|
||||
)
|
||||
|
||||
validated = AgentRunContext.model_validate(context_dict)
|
||||
validated = RunnerContext.model_validate(context_dict)
|
||||
assert validated.input.interaction is not None
|
||||
assert validated.input.interaction.interaction_id == 'form-1'
|
||||
assert validated.input.interaction.values == {'comment': 'looks good'}
|
||||
@@ -228,7 +228,7 @@ class TestContextValidation:
|
||||
model_entity=SimpleNamespace(context_length=128000),
|
||||
)
|
||||
)
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
event = self._make_event_envelope()
|
||||
binding = self._make_binding()
|
||||
@@ -281,7 +281,7 @@ class TestContextValidation:
|
||||
model_entity=SimpleNamespace(context_length=None),
|
||||
)
|
||||
)
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
resources = self._make_resources()
|
||||
resources['models'] = [
|
||||
{
|
||||
@@ -304,12 +304,12 @@ class TestContextValidation:
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_preserves_subject_data_for_non_message_events(self):
|
||||
"""Non-message EBA events keep subject.data instead of relying on message text."""
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import ActorContext, SubjectContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput as EventInput
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.event import ActorContext, SubjectContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput as EventInput
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
|
||||
mock_app = self._make_mock_app()
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
event = AgentEventEnvelope(
|
||||
event_id='evt_recall_1',
|
||||
event_type='message.recalled',
|
||||
@@ -353,7 +353,7 @@ class TestContextValidation:
|
||||
resources=resources,
|
||||
)
|
||||
|
||||
validated = AgentRunContext.model_validate(context_dict)
|
||||
validated = RunnerContext.model_validate(context_dict)
|
||||
|
||||
assert validated.event.event_type == 'message.recalled'
|
||||
assert validated.input.text is None
|
||||
@@ -366,7 +366,7 @@ class TestContextValidation:
|
||||
async def test_build_context_from_event_has_no_legacy_top_level_fields(self):
|
||||
"""Test that build_context_from_event does NOT have top-level messages/prompt/params."""
|
||||
mock_app = self._make_mock_app()
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
event = self._make_event_envelope()
|
||||
binding = self._make_binding()
|
||||
@@ -409,7 +409,7 @@ class TestContextValidation:
|
||||
async def test_build_context_from_event_event_is_not_none(self):
|
||||
"""Test that event field is NOT None in Protocol v1."""
|
||||
mock_app = self._make_mock_app()
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
event = self._make_event_envelope()
|
||||
binding = self._make_binding()
|
||||
@@ -440,14 +440,14 @@ class TestContextValidation:
|
||||
assert context_dict.get('event') is not None, 'event is REQUIRED for Protocol v1'
|
||||
|
||||
# Validate
|
||||
validated = AgentRunContext.model_validate(context_dict)
|
||||
validated = RunnerContext.model_validate(context_dict)
|
||||
assert validated.event is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_from_event_delivery_is_not_none(self):
|
||||
"""Test that delivery field is NOT None in Protocol v1."""
|
||||
mock_app = self._make_mock_app()
|
||||
builder = AgentRunContextBuilder(mock_app)
|
||||
builder = RunnerContextBuilder(mock_app)
|
||||
|
||||
event = self._make_event_envelope()
|
||||
binding = self._make_binding()
|
||||
@@ -478,5 +478,5 @@ class TestContextValidation:
|
||||
assert context_dict.get('delivery') is not None, 'delivery is REQUIRED for Protocol v1'
|
||||
|
||||
# Validate
|
||||
validated = AgentRunContext.model_validate(context_dict)
|
||||
validated = RunnerContext.model_validate(context_dict)
|
||||
assert validated.delivery is not None
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Tests cover:
|
||||
1. Query -> AgentEventEnvelope conversion
|
||||
2. Current config -> AgentConfig projection and single-binding resolution
|
||||
3. AgentRunContext not inlining full history by default
|
||||
3. RunnerContext not inlining full history by default
|
||||
4. LangBot Host not defining context-window controls
|
||||
5. Event-first run() entry point
|
||||
"""
|
||||
@@ -14,14 +14,14 @@ import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
# Import SDK entities
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import (
|
||||
from langbot_plugin.api.entities.builtin.runner.event import (
|
||||
AgentEventContext,
|
||||
)
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.trigger import AgentTrigger
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.context import AgentRunContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.result import (
|
||||
AgentRunResult,
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.trigger import AgentTrigger
|
||||
from langbot_plugin.api.entities.builtin.runner.context import RunnerContext
|
||||
from langbot_plugin.api.entities.builtin.runner.result import (
|
||||
RunnerResult,
|
||||
)
|
||||
|
||||
# Import LangBot host models
|
||||
@@ -263,8 +263,8 @@ class TestQueryConfigToAgentConfig:
|
||||
AgentBindingResolver().resolve_one(event, [first, second])
|
||||
|
||||
|
||||
class TestAgentRunContextProtocolV1:
|
||||
"""Test AgentRunContext Protocol v1 behavior."""
|
||||
class TestRunnerContextProtocolV1:
|
||||
"""Test RunnerContext Protocol v1 behavior."""
|
||||
|
||||
def test_sdk_context_event_required(self):
|
||||
"""Test that event is required in Protocol v1 context."""
|
||||
@@ -275,11 +275,11 @@ class TestAgentRunContextProtocolV1:
|
||||
source='platform',
|
||||
)
|
||||
input = AgentInput(text='Hello')
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.resources import AgentResources
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.runtime import AgentRuntimeContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.resources import AgentResources
|
||||
from langbot_plugin.api.entities.builtin.runner.runtime import AgentRuntimeContext
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
|
||||
ctx = AgentRunContext(
|
||||
ctx = RunnerContext(
|
||||
run_id='run_1',
|
||||
trigger=trigger,
|
||||
event=event,
|
||||
@@ -293,7 +293,7 @@ class TestAgentRunContextProtocolV1:
|
||||
assert ctx.event.event_type == 'message.received'
|
||||
|
||||
def test_sdk_context_has_no_history_message_fields(self):
|
||||
"""AgentRunContext should not expose inline history message fields."""
|
||||
"""RunnerContext should not expose inline history message fields."""
|
||||
trigger = AgentTrigger(type='message.received')
|
||||
event = AgentEventContext(
|
||||
event_id='evt_1',
|
||||
@@ -301,11 +301,11 @@ class TestAgentRunContextProtocolV1:
|
||||
source='platform',
|
||||
)
|
||||
input = AgentInput(text='Hello')
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.resources import AgentResources
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.runtime import AgentRuntimeContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.resources import AgentResources
|
||||
from langbot_plugin.api.entities.builtin.runner.runtime import AgentRuntimeContext
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
|
||||
ctx = AgentRunContext(
|
||||
ctx = RunnerContext(
|
||||
run_id='run_1',
|
||||
trigger=trigger,
|
||||
event=event,
|
||||
@@ -315,8 +315,8 @@ class TestAgentRunContextProtocolV1:
|
||||
runtime=AgentRuntimeContext(),
|
||||
)
|
||||
|
||||
assert 'messages' not in AgentRunContext.model_fields
|
||||
assert 'bootstrap' not in AgentRunContext.model_fields
|
||||
assert 'messages' not in RunnerContext.model_fields
|
||||
assert 'bootstrap' not in RunnerContext.model_fields
|
||||
assert not hasattr(ctx, 'bootstrap')
|
||||
|
||||
|
||||
@@ -324,20 +324,20 @@ class TestHostManagedHistoryNotInProtocol:
|
||||
"""Test that Host-managed history payloads are not in Protocol v1."""
|
||||
|
||||
def test_messages_not_in_sdk_context_top_level(self):
|
||||
"""AgentRunContext should not expose top-level history messages."""
|
||||
ctx_fields = AgentRunContext.model_fields.keys()
|
||||
"""RunnerContext should not expose top-level history messages."""
|
||||
ctx_fields = RunnerContext.model_fields.keys()
|
||||
|
||||
assert 'messages' not in ctx_fields
|
||||
|
||||
|
||||
class TestSDKResultProtocolV1:
|
||||
"""Test SDK AgentRunResult for Protocol v1."""
|
||||
"""Test SDK RunnerResult for Protocol v1."""
|
||||
|
||||
def test_result_requires_run_id(self):
|
||||
"""Test result requires run_id for Protocol v1."""
|
||||
from langbot_plugin.api.entities.builtin.provider.message import Message
|
||||
|
||||
result = AgentRunResult.message_completed(
|
||||
result = RunnerResult.message_completed(
|
||||
run_id='run_1',
|
||||
message=Message(role='assistant', content='Hello'),
|
||||
)
|
||||
|
||||
@@ -17,11 +17,11 @@ from langbot.pkg.agent.runner.host_models import (
|
||||
from langbot.pkg.agent.runner.event_log_store import EventLogStore
|
||||
from langbot.pkg.agent.runner.transcript_store import TranscriptStore
|
||||
from langbot.pkg.agent.runner.session_registry import get_session_registry
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import (
|
||||
from langbot_plugin.api.entities.builtin.runner.event import (
|
||||
ActorContext,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def make_event_envelope(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Tests for Host-only AgentRunner tool execution context."""
|
||||
"""Tests for Host-only Runner tool execution context."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
|
||||
from langbot_plugin.api.entities.builtin.provider.message import ContentElement
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_prepare_box_scope_overwrites_untrusted_existing_scope():
|
||||
assert json.loads(variables['_host_box_scope'])['target_id'] == 'user-1'
|
||||
|
||||
|
||||
def test_project_mcp_resource_config_uses_independent_agent_runner_settings():
|
||||
def test_project_mcp_resource_config_uses_independent_runner_settings():
|
||||
query = pipeline_query.Query.model_construct(variables={})
|
||||
attachments = [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ Tests focus on:
|
||||
- RETRIEVE_KNOWLEDGE_BASE authorization
|
||||
|
||||
Authorization paths:
|
||||
1. AgentRunner calls: has run_id, validates against session_registry
|
||||
1. Runner calls: has run_id, validates against session_registry
|
||||
2. Regular plugin calls: no run_id, unscoped plugin action path
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
|
||||
from langbot.pkg.plugin.handler import _get_pipeline_knowledge_base_uuids
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
@@ -122,9 +122,9 @@ class MockApplication:
|
||||
self.persistence_mgr.execute_async = AsyncMock(return_value=MagicMock(first=lambda: None))
|
||||
|
||||
|
||||
class FakeAgentRunnerRegistry:
|
||||
class FakeRunnerRegistry:
|
||||
async def get(self, context, runner_id, bound_plugins=None):
|
||||
return AgentRunnerDescriptor(
|
||||
return RunnerDescriptor(
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -161,7 +161,7 @@ class TestPipelineKnowledgeBaseScope:
|
||||
@pytest.mark.asyncio
|
||||
async def test_uses_runner_schema_when_query_scope_not_preprocessed(self):
|
||||
app = MockApplication()
|
||||
app.agent_runner_registry = FakeAgentRunnerRegistry()
|
||||
app.runner_registry = FakeRunnerRegistry()
|
||||
query = MockQuery()
|
||||
query.variables = {}
|
||||
|
||||
@@ -458,15 +458,15 @@ class TestRetrieveKnowledgeBaseAuthorization:
|
||||
|
||||
|
||||
class TestAuthorizationPathDifferentiation:
|
||||
"""Tests that verify AgentRunner vs regular plugin call differentiation."""
|
||||
"""Tests that verify Runner vs regular plugin call differentiation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_runner_path_with_run_id(self):
|
||||
"""AgentRunner calls provide run_id and use session_registry."""
|
||||
async def test_runner_path_with_run_id(self):
|
||||
"""Runner calls provide run_id and use session_registry."""
|
||||
registry = AgentRunSessionRegistry()
|
||||
|
||||
# AgentRunner call has run_id
|
||||
run_id = 'run_agent_123'
|
||||
# Runner call has run_id
|
||||
run_id = 'run_runner_123'
|
||||
|
||||
# Register session with resources
|
||||
await registry.register(
|
||||
@@ -548,7 +548,7 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
|
||||
Fix: Now uses RunnerConfigResolver.resolve_runner_id first, then resolve_runner_config.
|
||||
"""
|
||||
|
||||
def test_retrieve_kb_fix_local_agent_runner(self):
|
||||
def test_retrieve_kb_fix_local_runner(self):
|
||||
"""Fix should work for local-agent runner."""
|
||||
from langbot.pkg.agent.runner.config_resolver import RunnerConfigResolver
|
||||
|
||||
@@ -830,8 +830,8 @@ class TestHandlerActionAuthorization:
|
||||
await registry.unregister(run_id)
|
||||
|
||||
|
||||
class TestSDKAgentRunAPIProxyFieldConsistency:
|
||||
"""Tests for SDK AgentRunAPIProxy field name consistency with Host handler.
|
||||
class TestSDKRunnerAPIProxyFieldConsistency:
|
||||
"""Tests for SDK RunnerAPIProxy field name consistency with Host handler.
|
||||
|
||||
These tests verify that SDK sends field names that match what Host handler reads.
|
||||
"""
|
||||
@@ -901,7 +901,7 @@ class TestSDKAgentRunAPIProxyFieldConsistency:
|
||||
class TestNoRunIdBackwardCompatPath:
|
||||
"""Tests for unscoped plugin action path when no run_id is provided.
|
||||
|
||||
Regular plugins (non-AgentRunner) don't have run_id and should
|
||||
Regular plugins (non-Runner) don't have run_id and should
|
||||
have unrestricted access to certain APIs.
|
||||
"""
|
||||
|
||||
@@ -1182,14 +1182,14 @@ class TestResourceTypeValidation:
|
||||
|
||||
|
||||
class TestBypassPrevention:
|
||||
"""Tests to ensure AgentRunAPIProxy cannot bypass authorization."""
|
||||
"""Tests to ensure RunnerAPIProxy cannot bypass authorization."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cannot_bypass_via_unrestricted_retrieve_knowledge(self):
|
||||
"""Cannot bypass KB authorization via unrestricted RETRIEVE_KNOWLEDGE action."""
|
||||
# AgentRunAPIProxy uses RETRIEVE_KNOWLEDGE_BASE (with run_id)
|
||||
# RunnerAPIProxy uses RETRIEVE_KNOWLEDGE_BASE (with run_id)
|
||||
# RETRIEVE_KNOWLEDGE is unrestricted and separate
|
||||
# AgentRunner should NOT use RETRIEVE_KNOWLEDGE to bypass authorization
|
||||
# Runner should NOT use RETRIEVE_KNOWLEDGE to bypass authorization
|
||||
|
||||
registry = AgentRunSessionRegistry()
|
||||
resources = make_resources(knowledge_bases=[{'kb_id': 'kb_001'}])
|
||||
@@ -1207,8 +1207,8 @@ class TestBypassPrevention:
|
||||
# kb_002 is not authorized
|
||||
assert registry.is_resource_allowed(session, 'knowledge_base', 'kb_002') is False
|
||||
|
||||
# If AgentRunner tried to use RETRIEVE_KNOWLEDGE (unrestricted),
|
||||
# it would bypass authorization - but AgentRunAPIProxy correctly uses
|
||||
# If Runner tried to use RETRIEVE_KNOWLEDGE (unrestricted),
|
||||
# it would bypass authorization - but RunnerAPIProxy correctly uses
|
||||
# RETRIE_KNOWLEDGE_BASE which requires authorization
|
||||
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
@@ -2006,7 +2006,7 @@ class TestCallerPluginIdentityValidation:
|
||||
class TestBackwardCompatStorageNoRunId:
|
||||
"""Tests for unscoped storage actions without run_id.
|
||||
|
||||
Regular plugins (non-AgentRunner) don't have run_id and should
|
||||
Regular plugins (non-Runner) don't have run_id and should
|
||||
have unrestricted access to storage APIs.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for AgentRunner history/event pull API authorization."""
|
||||
"""Tests for Runner history/event pull API authorization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
@@ -11,7 +12,7 @@ from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
|
||||
from langbot.pkg.entity.persistence import event_log as event_log_model
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
from langbot.pkg.plugin.handler import RuntimeConnectionHandler
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.page_results import (
|
||||
from langbot_plugin.api.entities.builtin.runner.page_results import (
|
||||
AgentEventRecord,
|
||||
EventPage,
|
||||
)
|
||||
@@ -92,10 +93,12 @@ async def test_history_page_requires_runtime_capability(session_registry, db_eng
|
||||
handler = _handler(db_engine, session_registry)
|
||||
history_page = handler.actions[PluginToRuntimeAction.HISTORY_PAGE.value]
|
||||
|
||||
result = await history_page({
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await history_page(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not authorized' in result.message.lower()
|
||||
@@ -107,11 +110,13 @@ async def test_history_page_rejects_cross_conversation(session_registry, db_engi
|
||||
handler = _handler(db_engine, session_registry)
|
||||
history_page = handler.actions[PluginToRuntimeAction.HISTORY_PAGE.value]
|
||||
|
||||
result = await history_page({
|
||||
'run_id': 'run_1',
|
||||
'conversation_id': 'conv_other',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await history_page(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'conversation_id': 'conv_other',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not accessible' in result.message.lower()
|
||||
@@ -123,12 +128,14 @@ async def test_history_search_rejects_filter_conversation_override(session_regis
|
||||
handler = _handler(db_engine, session_registry)
|
||||
history_search = handler.actions[PluginToRuntimeAction.HISTORY_SEARCH.value]
|
||||
|
||||
result = await history_search({
|
||||
'run_id': 'run_1',
|
||||
'query': 'hello',
|
||||
'filters': {'conversation_id': 'conv_other'},
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await history_search(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'query': 'hello',
|
||||
'filters': {'conversation_id': 'conv_other'},
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not accessible' in result.message.lower()
|
||||
@@ -140,10 +147,12 @@ async def test_event_page_requires_runtime_capability(session_registry, db_engin
|
||||
handler = _handler(db_engine, session_registry)
|
||||
event_page = handler.actions[PluginToRuntimeAction.EVENT_PAGE.value]
|
||||
|
||||
result = await event_page({
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await event_page(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not authorized' in result.message.lower()
|
||||
@@ -155,11 +164,13 @@ async def test_event_page_rejects_cross_conversation(session_registry, db_engine
|
||||
handler = _handler(db_engine, session_registry)
|
||||
event_page = handler.actions[PluginToRuntimeAction.EVENT_PAGE.value]
|
||||
|
||||
result = await event_page({
|
||||
'run_id': 'run_1',
|
||||
'conversation_id': 'conv_other',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await event_page(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'conversation_id': 'conv_other',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not accessible' in result.message.lower()
|
||||
@@ -184,11 +195,13 @@ async def test_event_get_returns_sdk_record_projection(session_registry, db_engi
|
||||
handler = _handler(db_engine, session_registry)
|
||||
event_get = handler.actions[PluginToRuntimeAction.EVENT_GET.value]
|
||||
|
||||
result = await event_get({
|
||||
'run_id': 'run_1',
|
||||
'event_id': event_id,
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await event_get(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'event_id': event_id,
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code == 0
|
||||
AgentEventRecord.model_validate(result.data)
|
||||
@@ -216,10 +229,12 @@ async def test_event_page_returns_sdk_page_projection(session_registry, db_engin
|
||||
handler = _handler(db_engine, session_registry)
|
||||
event_page = handler.actions[PluginToRuntimeAction.EVENT_PAGE.value]
|
||||
|
||||
result = await event_page({
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await event_page(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code == 0
|
||||
page = EventPage.model_validate(result.data)
|
||||
@@ -272,10 +287,12 @@ async def test_history_page_filters_run_scope_thread_and_bot(session_registry, d
|
||||
handler = _handler(db_engine, session_registry)
|
||||
history_page = handler.actions[PluginToRuntimeAction.HISTORY_PAGE.value]
|
||||
|
||||
result = await history_page({
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await history_page(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code == 0
|
||||
assert [item['content'] for item in result.data['items']] == ['visible']
|
||||
@@ -317,10 +334,12 @@ async def test_event_page_filters_run_scope_thread_and_bot(session_registry, db_
|
||||
handler = _handler(db_engine, session_registry)
|
||||
event_page = handler.actions[PluginToRuntimeAction.EVENT_PAGE.value]
|
||||
|
||||
result = await event_page({
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await event_page(
|
||||
{
|
||||
'run_id': 'run_1',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code == 0
|
||||
assert [item['event_id'] for item in result.data['items']] == ['evt_visible']
|
||||
|
||||
@@ -8,10 +8,10 @@ import time
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.event import ActorContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.interaction import (
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.event import ActorContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.interaction import (
|
||||
InteractionDeliveryCapabilities,
|
||||
InteractionSubmission,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from unittest.mock import AsyncMock
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.errors import RunnerExecutionError
|
||||
from langbot.pkg.agent.runner.orchestrator import AgentRunOrchestrator
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
@@ -84,7 +84,7 @@ class FakePluginConnector:
|
||||
self.contexts: list[dict] = []
|
||||
self.sessions_during_run: list[dict | None] = []
|
||||
|
||||
async def run_agent(self, plugin_author, plugin_name, runner_name, context):
|
||||
async def run_runner(self, plugin_author, plugin_name, runner_name, context):
|
||||
self.calls.append(
|
||||
{
|
||||
'plugin_author': plugin_author,
|
||||
@@ -105,7 +105,7 @@ class FakePluginConnector:
|
||||
|
||||
|
||||
class FakeRegistry:
|
||||
def __init__(self, descriptor: AgentRunnerDescriptor):
|
||||
def __init__(self, descriptor: RunnerDescriptor):
|
||||
self.descriptor = descriptor
|
||||
self.calls: list[dict] = []
|
||||
|
||||
@@ -162,8 +162,8 @@ class FakeConversation:
|
||||
create_time = datetime.datetime(2026, 5, 15, 12, 0, 0)
|
||||
|
||||
|
||||
def make_descriptor() -> AgentRunnerDescriptor:
|
||||
return AgentRunnerDescriptor(
|
||||
def make_descriptor() -> RunnerDescriptor:
|
||||
return RunnerDescriptor(
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
@@ -798,7 +798,7 @@ async def test_unconsumed_steering_audit_does_not_persist_pinned_context(clean_a
|
||||
self.started = asyncio.Event()
|
||||
self.release = asyncio.Event()
|
||||
|
||||
async def run_agent(self, plugin_author, plugin_name, runner_name, context):
|
||||
async def run_runner(self, plugin_author, plugin_name, runner_name, context):
|
||||
self.calls.append(
|
||||
{
|
||||
'plugin_author': plugin_author,
|
||||
@@ -998,8 +998,8 @@ class TestQueryEntrySessionQueryId:
|
||||
DeliveryPolicy,
|
||||
ResourcePolicy,
|
||||
)
|
||||
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
|
||||
|
||||
db_engine = clean_agent_state
|
||||
descriptor = make_descriptor()
|
||||
|
||||
@@ -2,7 +2,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from langbot_plugin.api.entities.builtin.agent_runner import (
|
||||
from langbot_plugin.api.entities.builtin.runner import (
|
||||
ActorContext,
|
||||
AgentInput,
|
||||
DeliveryContext,
|
||||
|
||||
@@ -6,8 +6,8 @@ from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.agent.runner.registry import AgentRunnerRegistry
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.registry import RunnerRegistry
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.errors import RunnerNotFoundError, RunnerNotAuthorizedError
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
|
||||
@@ -44,7 +44,7 @@ class FakeApplication:
|
||||
async def require_workspace_context(self, context):
|
||||
return context
|
||||
|
||||
async def list_agent_runners(self, bound_plugins=None):
|
||||
async def list_runners(self, bound_plugins=None):
|
||||
# Return sample runner data
|
||||
return [
|
||||
{
|
||||
@@ -90,7 +90,7 @@ class FakeApplication:
|
||||
'plugin_name': 'missing-name',
|
||||
'runner_name': 'default',
|
||||
'manifest': {
|
||||
'kind': 'AgentRunner',
|
||||
'kind': 'Runner',
|
||||
'metadata': {}, # No name
|
||||
'spec': {},
|
||||
},
|
||||
@@ -107,7 +107,7 @@ class TestRegistryDiscovery:
|
||||
async def test_discover_valid_runners(self):
|
||||
"""Discover valid runners from plugin runtime."""
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
runners = await registry.list_runners(TEST_CONTEXT, use_cache=False)
|
||||
|
||||
@@ -122,7 +122,7 @@ class TestRegistryDiscovery:
|
||||
async def test_discover_caches_results(self):
|
||||
"""Discovery should cache results."""
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
# First discovery
|
||||
runners1 = await registry.list_runners(TEST_CONTEXT, use_cache=True)
|
||||
@@ -138,7 +138,7 @@ class TestRegistryDiscovery:
|
||||
"""Discovery returns empty when plugin system disabled."""
|
||||
ap = FakeApplication()
|
||||
ap.plugin_connector.is_enable_plugin = False
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
runners = await registry.list_runners(TEST_CONTEXT, use_cache=False)
|
||||
|
||||
@@ -152,7 +152,7 @@ class TestRegistryDiscovery:
|
||||
so subsequent list_runners(bound_plugins=None) should return all runners.
|
||||
"""
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
# First: get with bound_plugins filter (should not pollute cache)
|
||||
descriptor = await registry.get(
|
||||
@@ -189,7 +189,7 @@ class TestRegistryGet:
|
||||
async def test_get_existing_runner(self):
|
||||
"""Get existing runner by ID."""
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
descriptor = await registry.get(
|
||||
TEST_CONTEXT,
|
||||
@@ -205,7 +205,7 @@ class TestRegistryGet:
|
||||
async def test_get_nonexistent_runner(self):
|
||||
"""Get nonexistent runner raises RunnerNotFoundError."""
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
with pytest.raises(RunnerNotFoundError) as exc_info:
|
||||
await registry.get(TEST_CONTEXT, 'plugin:notexist/unknown/default')
|
||||
@@ -216,10 +216,10 @@ class TestRegistryGet:
|
||||
async def test_get_refreshes_partial_startup_cache_on_miss(self):
|
||||
"""A runner initialized after early discovery should become available."""
|
||||
ap = FakeApplication()
|
||||
ap.plugin_connector.list_agent_runners = AsyncMock(
|
||||
side_effect=ap.plugin_connector.list_agent_runners,
|
||||
ap.plugin_connector.list_runners = AsyncMock(
|
||||
side_effect=ap.plugin_connector.list_runners,
|
||||
)
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
await registry.list_runners(TEST_CONTEXT)
|
||||
cache = registry._cache[('instance-test', 'workspace-test', 1)]
|
||||
@@ -231,13 +231,13 @@ class TestRegistryGet:
|
||||
)
|
||||
|
||||
assert descriptor.id == 'plugin:alice/my-agent/custom'
|
||||
assert ap.plugin_connector.list_agent_runners.await_count == 2
|
||||
assert ap.plugin_connector.list_runners.await_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_runner_with_bound_plugins_filter(self):
|
||||
"""Get runner with bound plugins authorization."""
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
# Authorized - langbot plugin in bound list
|
||||
descriptor = await registry.get(
|
||||
@@ -263,7 +263,7 @@ class TestRegistryMetadataForPipeline:
|
||||
async def test_get_metadata_options_and_stages(self):
|
||||
"""Get metadata options and stages for pipeline UI."""
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
options, stages = await registry.get_runner_metadata_for_pipeline(TEST_CONTEXT)
|
||||
|
||||
@@ -284,10 +284,10 @@ class TestRegistryMetadataForPipeline:
|
||||
async def test_metadata_refreshes_partial_startup_cache(self):
|
||||
"""Pipeline metadata should not preserve an early partial discovery."""
|
||||
ap = FakeApplication()
|
||||
ap.plugin_connector.list_agent_runners = AsyncMock(
|
||||
side_effect=ap.plugin_connector.list_agent_runners,
|
||||
ap.plugin_connector.list_runners = AsyncMock(
|
||||
side_effect=ap.plugin_connector.list_runners,
|
||||
)
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
registry = RunnerRegistry(ap)
|
||||
|
||||
await registry.list_runners(TEST_CONTEXT)
|
||||
cache = registry._cache[('instance-test', 'workspace-test', 1)]
|
||||
@@ -299,7 +299,7 @@ class TestRegistryMetadataForPipeline:
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
'plugin:alice/my-agent/custom',
|
||||
}
|
||||
assert ap.plugin_connector.list_agent_runners.await_count == 2
|
||||
assert ap.plugin_connector.list_runners.await_count == 2
|
||||
|
||||
|
||||
class TestDescriptorValidation:
|
||||
@@ -307,7 +307,7 @@ class TestDescriptorValidation:
|
||||
|
||||
def test_validate_runner_descriptor(self):
|
||||
"""Validate correctly built descriptor."""
|
||||
descriptor = AgentRunnerDescriptor(
|
||||
descriptor = RunnerDescriptor(
|
||||
id='plugin:test/my-runner/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -318,11 +318,11 @@ class TestDescriptorValidation:
|
||||
|
||||
assert descriptor.id == 'plugin:test/my-runner/default'
|
||||
assert descriptor.get_plugin_id() == 'test/my-runner'
|
||||
assert 'protocol_version' not in AgentRunnerDescriptor.model_fields
|
||||
assert 'protocol_version' not in RunnerDescriptor.model_fields
|
||||
|
||||
def test_descriptor_capabilities(self):
|
||||
"""Descriptor capability helper methods."""
|
||||
descriptor = AgentRunnerDescriptor(
|
||||
descriptor = RunnerDescriptor(
|
||||
id='plugin:test/my-runner/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -338,28 +338,30 @@ class TestDescriptorValidation:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_registry_separates_processor_kinds_with_same_plugin_component_name():
|
||||
async def test_registry_filters_usages_without_splitting_component_identity():
|
||||
ap = FakeApplication()
|
||||
entries = []
|
||||
for kind, prefix in [('AgentRunner', 'plugin'), ('EventProcessor', 'event_processor')]:
|
||||
for name, usages in [('agent', ['agent']), ('events', ['event']), ('both', ['agent', 'event'])]:
|
||||
entries.append(
|
||||
{
|
||||
'plugin_author': 'test',
|
||||
'plugin_name': 'both',
|
||||
'runner_name': 'default',
|
||||
'plugin_name': 'runners',
|
||||
'runner_name': name,
|
||||
'manifest': {
|
||||
'id': f'{prefix}:test/both/default',
|
||||
'name': 'default',
|
||||
'component_kind': kind,
|
||||
'label': {'en_US': kind},
|
||||
'id': f'plugin:test/runners/{name}',
|
||||
'name': name,
|
||||
'component_kind': 'Runner',
|
||||
'usages': usages,
|
||||
'label': {'en_US': name},
|
||||
'supported_event_patterns': ['group.member_joined'],
|
||||
},
|
||||
}
|
||||
)
|
||||
ap.plugin_connector.list_agent_runners = AsyncMock(return_value=entries)
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
ap.plugin_connector.list_runners = AsyncMock(return_value=entries)
|
||||
registry = RunnerRegistry(ap)
|
||||
agents = await registry.list_runners(TEST_CONTEXT)
|
||||
processors = await registry.list_runners(TEST_CONTEXT, component_kind='EventProcessor')
|
||||
assert [item.id for item in agents] == ['plugin:test/both/default']
|
||||
assert [item.id for item in processors] == ['event_processor:test/both/default']
|
||||
assert (await registry.get(TEST_CONTEXT, processors[0].id)).component_kind == 'EventProcessor'
|
||||
processors = await registry.list_runners(TEST_CONTEXT, usage='event')
|
||||
assert [item.runner_name for item in agents] == ['agent', 'both']
|
||||
assert [item.runner_name for item in processors] == ['events', 'both']
|
||||
assert agents[-1].id == processors[-1].id
|
||||
assert (await registry.get(TEST_CONTEXT, processors[0].id)).usages == ['event']
|
||||
|
||||
@@ -7,9 +7,9 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langbot_plugin.api.entities.builtin.platform import events, entities, message
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.context_access import ContextAPICapabilities
|
||||
from langbot_plugin.api.proxies.agent_run import AgentRunAPIProxy
|
||||
from langbot_plugin.api.proxies.agent_run.common import PermissionDeniedError
|
||||
from langbot_plugin.api.entities.builtin.runner.context_access import ContextAPICapabilities
|
||||
from langbot_plugin.api.proxies.runner import RunnerAPIProxy
|
||||
from langbot_plugin.api.proxies.runner.common import PermissionDeniedError
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
|
||||
from langbot.pkg.agent.runner.reply_stream import ReplyStreamRequest, ReplyStreamSession
|
||||
@@ -50,7 +50,7 @@ def request(key, operation='update', text='hello'):
|
||||
|
||||
|
||||
def proxy_for(session, *, allowed=True, advertised=True):
|
||||
if not hasattr(AgentRunAPIProxy, 'reply_stream'):
|
||||
if not hasattr(RunnerAPIProxy, 'reply_stream'):
|
||||
pytest.skip('SDK does not provide the optional streaming reply API')
|
||||
context = SimpleNamespace(
|
||||
run_id='run-1',
|
||||
@@ -73,7 +73,7 @@ def proxy_for(session, *, allowed=True, advertised=True):
|
||||
}
|
||||
|
||||
transport = SimpleNamespace(call_action=AsyncMock(side_effect=action))
|
||||
return AgentRunAPIProxy(context, transport), transport
|
||||
return RunnerAPIProxy(context, transport), transport
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -211,11 +211,11 @@ async def test_streams_are_isolated_by_run_and_bounded():
|
||||
|
||||
async def test_event_processor_uses_shared_sdk_api_and_emits_one_trace_for_the_stream():
|
||||
from unittest.mock import Mock
|
||||
from langbot_plugin.api.definition.components.event_processor import EventProcessor
|
||||
from langbot_plugin.api.definition.components.runner import Runner, RunnerContext
|
||||
|
||||
session, adapter, incoming = make_session()
|
||||
api, _ = proxy_for(session)
|
||||
processor = EventProcessor()
|
||||
processor = Runner()
|
||||
processor.get_run_api = Mock(return_value=api)
|
||||
|
||||
@processor.handler(events.MessageReceivedEvent)
|
||||
@@ -224,14 +224,23 @@ async def test_event_processor_uses_shared_sdk_api_and_emits_one_trace_for_the_s
|
||||
await stream.update('one')
|
||||
await stream.update('one two')
|
||||
|
||||
context = SimpleNamespace(
|
||||
run_id='run-1',
|
||||
config={},
|
||||
event=SimpleNamespace(
|
||||
data=incoming.model_dump(mode='json', exclude={'source_platform_object', 'legacy_event'})
|
||||
),
|
||||
context = RunnerContext.model_validate(
|
||||
{
|
||||
'run_id': 'run-1',
|
||||
'trigger': {'type': incoming.type},
|
||||
'event': {
|
||||
'event_id': 'one',
|
||||
'event_type': incoming.type,
|
||||
'source': 'test',
|
||||
'data': incoming.model_dump(mode='json', exclude={'source_platform_object', 'legacy_event'}),
|
||||
},
|
||||
'input': {},
|
||||
'delivery': {'surface': 'test'},
|
||||
'resources': {},
|
||||
'runtime': {},
|
||||
}
|
||||
)
|
||||
results = [result async for result in processor.run(context)]
|
||||
results = [result async for result in processor.invoke(context)]
|
||||
assert [r.type for r in results] == ['tool.call.started', 'tool.call.completed', 'run.completed']
|
||||
assert results[1].data['result']['text'] == 'one two'
|
||||
assert adapter.reply_message_chunk.await_count == 3
|
||||
|
||||
@@ -6,9 +6,9 @@ from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from langbot_plugin.api.entities.builtin.agent_runner import AgentInput, DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner import AgentInput, DeliveryContext
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.binding_resolver import AgentBindingResolver
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
from langbot.pkg.agent.runner.resource_builder import AgentResourceBuilder
|
||||
@@ -37,8 +37,8 @@ def make_descriptor(
|
||||
config_schema: list[dict] | None = None,
|
||||
capabilities: dict | None = None,
|
||||
permissions: dict | None = None,
|
||||
) -> AgentRunnerDescriptor:
|
||||
return AgentRunnerDescriptor(
|
||||
) -> RunnerDescriptor:
|
||||
return RunnerDescriptor(
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for generic AgentRunner resource-policy projection."""
|
||||
"""Tests for generic Runner resource-policy projection."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.agent.runner.result_normalizer import AgentResultNormalizer
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.errors import RunnerExecutionError, RunnerProtocolError
|
||||
|
||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
||||
@@ -36,7 +36,7 @@ class FakeApplication:
|
||||
|
||||
def make_descriptor():
|
||||
"""Create a test descriptor."""
|
||||
return AgentRunnerDescriptor(
|
||||
return RunnerDescriptor(
|
||||
id='plugin:langbot-team/LocalAgent/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent', 'zh_Hans': '内置 Agent'},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for AgentRunner run ledger pull API authorization."""
|
||||
"""Tests for Runner run ledger pull API authorization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,7 +17,7 @@ from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
|
||||
from langbot.pkg.entity.persistence import agent_run as agent_run_model
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
from langbot.pkg.plugin.handler import RuntimeConnectionHandler
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.run_ledger import (
|
||||
from langbot_plugin.api.entities.builtin.runner.run_ledger import (
|
||||
AgentRun,
|
||||
AgentRunEvent,
|
||||
RunEventPage,
|
||||
@@ -37,10 +37,10 @@ class FakeApplication:
|
||||
self.logger = MagicMock()
|
||||
self.persistence_mgr = MagicMock()
|
||||
self.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
||||
self.agent_runner_registry = runner_registry
|
||||
self.runner_registry = runner_registry
|
||||
self.instance_config = SimpleNamespace(
|
||||
data={
|
||||
'agent_runner': {
|
||||
'runner': {
|
||||
'admin_plugins': admin_plugins or [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ Authorization rules:
|
||||
- enable_state must be True
|
||||
- scope must be in state_scopes
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
@@ -34,11 +35,13 @@ from .conftest import bind_runtime_action_context, make_resources
|
||||
|
||||
class FakeConnection:
|
||||
"""Fake connection for testing."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FakeApplication:
|
||||
"""Fake Application for testing."""
|
||||
|
||||
def __init__(self, db_engine=None):
|
||||
self.logger = MagicMock()
|
||||
self.logger.debug = MagicMock()
|
||||
@@ -77,10 +80,10 @@ async def persistent_store(db_engine):
|
||||
store = PersistentStateStore(db_engine)
|
||||
|
||||
# Create the table
|
||||
from langbot.pkg.entity.persistence.agent_runner_state import AgentRunnerState
|
||||
from langbot.pkg.entity.persistence.runner_state import RunnerState
|
||||
|
||||
async with db_engine.begin() as conn:
|
||||
await conn.run_sync(AgentRunnerState.__table__.create, checkfirst=True)
|
||||
await conn.run_sync(RunnerState.__table__.create, checkfirst=True)
|
||||
|
||||
yield store
|
||||
reset_persistent_state_store()
|
||||
@@ -124,11 +127,13 @@ class TestStateAPIHandlerAuthorization:
|
||||
state_get_handler = handler.actions[PluginToRuntimeAction.STATE_GET.value]
|
||||
|
||||
# Call with non-existent run_id
|
||||
result = await state_get_handler({
|
||||
'run_id': 'nonexistent_run',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
})
|
||||
result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'nonexistent_run',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not found' in result.message.lower()
|
||||
@@ -164,11 +169,13 @@ class TestStateAPIHandlerAuthorization:
|
||||
state_get_handler = handler.actions[PluginToRuntimeAction.STATE_GET.value]
|
||||
|
||||
# Call without caller_plugin_identity
|
||||
result = await state_get_handler({
|
||||
'run_id': 'run_test_missing_identity',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
})
|
||||
result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_test_missing_identity',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code == 0
|
||||
assert result.data == {'value': None}
|
||||
@@ -176,7 +183,9 @@ class TestStateAPIHandlerAuthorization:
|
||||
await session_registry.unregister('run_test_missing_identity')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_get_caller_identity_mismatch_returns_error(self, session_registry, db_engine, persistent_store):
|
||||
async def test_state_get_caller_identity_mismatch_returns_error(
|
||||
self, session_registry, db_engine, persistent_store
|
||||
):
|
||||
"""STATE_GET: caller_plugin_identity mismatch returns error."""
|
||||
fake_app = FakeApplication(db_engine)
|
||||
fake_app.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
||||
@@ -200,12 +209,14 @@ class TestStateAPIHandlerAuthorization:
|
||||
state_get_handler = handler.actions[PluginToRuntimeAction.STATE_GET.value]
|
||||
|
||||
# Call with wrong caller_plugin_identity
|
||||
result = await state_get_handler({
|
||||
'run_id': 'run_test_mismatch',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'other/plugin',
|
||||
})
|
||||
result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_test_mismatch',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'other/plugin',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'does not match' in result.message.lower()
|
||||
@@ -236,12 +247,14 @@ class TestStateAPIHandlerAuthorization:
|
||||
handler = RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app)
|
||||
state_get_handler = handler.actions[PluginToRuntimeAction.STATE_GET.value]
|
||||
|
||||
result = await state_get_handler({
|
||||
'run_id': 'run_test_disabled',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_test_disabled',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'disabled' in result.message.lower()
|
||||
@@ -262,7 +275,10 @@ class TestStateAPIHandlerAuthorization:
|
||||
resources=make_resources(),
|
||||
available_apis={'state': True},
|
||||
state_policy={'enable_state': True, 'state_scopes': ['conversation']},
|
||||
state_context={'scope_keys': {'conversation': 'conv_key', 'actor': 'actor_key'}, 'binding_identity': 'binding_1'},
|
||||
state_context={
|
||||
'scope_keys': {'conversation': 'conv_key', 'actor': 'actor_key'},
|
||||
'binding_identity': 'binding_1',
|
||||
},
|
||||
)
|
||||
|
||||
async def fake_disconnect():
|
||||
@@ -273,12 +289,14 @@ class TestStateAPIHandlerAuthorization:
|
||||
state_get_handler = handler.actions[PluginToRuntimeAction.STATE_GET.value]
|
||||
|
||||
# Request 'actor' scope which is not in state_scopes
|
||||
result = await state_get_handler({
|
||||
'run_id': 'run_test_scope_disabled',
|
||||
'scope': 'actor',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_test_scope_disabled',
|
||||
'scope': 'actor',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not enabled' in result.message.lower() or 'scope' in result.message.lower()
|
||||
@@ -309,12 +327,14 @@ class TestStateAPIHandlerAuthorization:
|
||||
handler = RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app)
|
||||
state_get_handler = handler.actions[PluginToRuntimeAction.STATE_GET.value]
|
||||
|
||||
result = await state_get_handler({
|
||||
'run_id': 'run_test_no_scope_key',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_test_no_scope_key',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'not available' in result.message.lower()
|
||||
@@ -360,9 +380,11 @@ class TestStateAPIFullFlowWithRealDB:
|
||||
session = await session_registry.get('run_full_flow')
|
||||
assert session is not None
|
||||
state_ctx = session['authorization']['state_context']
|
||||
assert state_ctx is not None, f"state_context is None. Session keys: {list(session.keys())}"
|
||||
assert 'scope_keys' in state_ctx, f"scope_keys not in state_context: {state_ctx}"
|
||||
assert 'conversation' in state_ctx['scope_keys'], f"conversation not in scope_keys: {state_ctx['scope_keys']}"
|
||||
assert state_ctx is not None, f'state_context is None. Session keys: {list(session.keys())}'
|
||||
assert 'scope_keys' in state_ctx, f'scope_keys not in state_context: {state_ctx}'
|
||||
assert 'conversation' in state_ctx['scope_keys'], (
|
||||
f'conversation not in scope_keys: {state_ctx["scope_keys"]}'
|
||||
)
|
||||
|
||||
# Get handlers (actions dict is keyed by action value string)
|
||||
state_set_handler = handler.actions[PluginToRuntimeAction.STATE_SET.value]
|
||||
@@ -371,57 +393,67 @@ class TestStateAPIFullFlowWithRealDB:
|
||||
state_delete_handler = handler.actions[PluginToRuntimeAction.STATE_DELETE.value]
|
||||
|
||||
# 1. STATE_SET
|
||||
set_result = await state_set_handler({
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'value': {'data': 'test_value'},
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
set_result = await state_set_handler(
|
||||
{
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'value': {'data': 'test_value'},
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert set_result.code == 0
|
||||
assert set_result.data.get('success') is True
|
||||
|
||||
# 2. STATE_GET
|
||||
get_result = await state_get_handler({
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
get_result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert get_result.code == 0
|
||||
assert get_result.data.get('value') == {'data': 'test_value'}
|
||||
|
||||
# 3. STATE_LIST
|
||||
list_result = await state_list_handler({
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'prefix': 'external.',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
list_result = await state_list_handler(
|
||||
{
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'prefix': 'external.',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert list_result.code == 0
|
||||
keys = list_result.data.get('keys', [])
|
||||
assert 'external.test_key' in keys
|
||||
|
||||
# 4. STATE_DELETE
|
||||
delete_result = await state_delete_handler({
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
delete_result = await state_delete_handler(
|
||||
{
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert delete_result.code == 0
|
||||
|
||||
# 5. Verify deleted
|
||||
get_after_delete = await state_get_handler({
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
get_after_delete = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_full_flow',
|
||||
'scope': 'conversation',
|
||||
'key': 'external.test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert get_after_delete.code == 0
|
||||
assert get_after_delete.data.get('value') is None
|
||||
@@ -433,7 +465,9 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
|
||||
"""Tests verifying handlers read state_policy/state_context from authorization snapshot."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_handler_reads_state_policy_from_authorization(self, session_registry, db_engine, persistent_store):
|
||||
async def test_state_handler_reads_state_policy_from_authorization(
|
||||
self, session_registry, db_engine, persistent_store
|
||||
):
|
||||
"""Handler reads state_policy from session['authorization'], not resources."""
|
||||
fake_app = FakeApplication(db_engine)
|
||||
fake_app.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
||||
@@ -454,7 +488,7 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
|
||||
session = await session_registry.get('run_policy_top_level')
|
||||
assert session is not None
|
||||
resources = session['authorization']['resources']
|
||||
assert 'state_policy' not in resources, "resources should NOT contain state_policy"
|
||||
assert 'state_policy' not in resources, 'resources should NOT contain state_policy'
|
||||
|
||||
async def fake_disconnect():
|
||||
return True
|
||||
@@ -464,12 +498,14 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
|
||||
state_get_handler = handler.actions[PluginToRuntimeAction.STATE_GET.value]
|
||||
|
||||
# Should fail because enable_state=False in authorization.state_policy
|
||||
result = await state_get_handler({
|
||||
'run_id': 'run_policy_top_level',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await state_get_handler(
|
||||
{
|
||||
'run_id': 'run_policy_top_level',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'disabled' in result.message.lower()
|
||||
@@ -477,7 +513,9 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
|
||||
await session_registry.unregister('run_policy_top_level')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_handler_reads_state_context_from_authorization(self, session_registry, db_engine, persistent_store):
|
||||
async def test_state_handler_reads_state_context_from_authorization(
|
||||
self, session_registry, db_engine, persistent_store
|
||||
):
|
||||
"""Handler reads state_context from session['authorization'], not resources."""
|
||||
fake_app = FakeApplication(db_engine)
|
||||
fake_app.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
||||
@@ -498,7 +536,7 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
|
||||
session = await session_registry.get('run_context_top_level')
|
||||
assert session is not None
|
||||
resources = session['authorization']['resources']
|
||||
assert 'state_context' not in resources, "resources should NOT contain state_context"
|
||||
assert 'state_context' not in resources, 'resources should NOT contain state_context'
|
||||
|
||||
async def fake_disconnect():
|
||||
return True
|
||||
@@ -508,13 +546,15 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
|
||||
state_set_handler = handler.actions[PluginToRuntimeAction.STATE_SET.value]
|
||||
|
||||
# Should use scope_key from authorization.state_context.scope_keys.conversation
|
||||
result = await state_set_handler({
|
||||
'run_id': 'run_context_top_level',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'value': 'test_value',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
})
|
||||
result = await state_set_handler(
|
||||
{
|
||||
'run_id': 'run_context_top_level',
|
||||
'scope': 'conversation',
|
||||
'key': 'test_key',
|
||||
'value': 'test_value',
|
||||
'caller_plugin_identity': 'test/runner',
|
||||
}
|
||||
)
|
||||
|
||||
# Should succeed - scope_key was found in state_context
|
||||
assert result.code == 0
|
||||
@@ -546,10 +586,8 @@ class TestResourcesDoesNotContainStateMetadata:
|
||||
# Verify resources is nested under authorization and is clean.
|
||||
assert 'resources' not in session
|
||||
session_resources = session['authorization']['resources']
|
||||
assert 'state_policy' not in session_resources, \
|
||||
"authorization['resources'] should NOT contain state_policy"
|
||||
assert 'state_context' not in session_resources, \
|
||||
"authorization['resources'] should NOT contain state_context"
|
||||
assert 'state_policy' not in session_resources, "authorization['resources'] should NOT contain state_policy"
|
||||
assert 'state_context' not in session_resources, "authorization['resources'] should NOT contain state_context"
|
||||
|
||||
assert 'state_policy' in session['authorization']
|
||||
assert 'state_context' in session['authorization']
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for persistent AgentRunner state store."""
|
||||
"""Tests for persistent Runner state store."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -8,7 +9,7 @@ import tempfile
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.agent.runner.host_models import BindingScope, StatePolicy
|
||||
from langbot.pkg.agent.runner.persistent_state_store import PersistentStateStore
|
||||
from langbot.pkg.agent.runner.state_scope import (
|
||||
@@ -21,9 +22,9 @@ from langbot.pkg.agent.runner.state_scope import (
|
||||
)
|
||||
|
||||
|
||||
def make_descriptor(runner_id: str = 'plugin:test/my-runner/default') -> AgentRunnerDescriptor:
|
||||
def make_descriptor(runner_id: str = 'plugin:test/my-runner/default') -> RunnerDescriptor:
|
||||
"""Create a test descriptor."""
|
||||
return AgentRunnerDescriptor(
|
||||
return RunnerDescriptor(
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -36,6 +37,7 @@ def make_descriptor(runner_id: str = 'plugin:test/my-runner/default') -> AgentRu
|
||||
|
||||
class FakeActorContext:
|
||||
"""Fake actor context for event testing."""
|
||||
|
||||
def __init__(self, actor_type: str = 'user', actor_id: str = 'user_123', actor_name: str = 'Test User'):
|
||||
self.actor_type = actor_type
|
||||
self.actor_id = actor_id
|
||||
@@ -44,6 +46,7 @@ class FakeActorContext:
|
||||
|
||||
class FakeSubjectContext:
|
||||
"""Fake subject context for event testing."""
|
||||
|
||||
def __init__(self, subject_type: str = 'message', subject_id: str = 'msg_001', data: dict | None = None):
|
||||
self.subject_type = subject_type
|
||||
self.subject_id = subject_id
|
||||
@@ -52,6 +55,7 @@ class FakeSubjectContext:
|
||||
|
||||
class FakeEventEnvelope:
|
||||
"""Fake event envelope for testing event-first state."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_id: str = 'evt_001',
|
||||
@@ -78,6 +82,7 @@ class FakeEventEnvelope:
|
||||
|
||||
class FakeBinding:
|
||||
"""Fake binding for testing state."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
binding_id: str = 'binding_001',
|
||||
@@ -119,10 +124,7 @@ class TestStateScopeHelpers:
|
||||
thread_id='thread_001',
|
||||
)
|
||||
|
||||
keys = {
|
||||
scope: build_state_scope_key(scope, event, binding, descriptor)
|
||||
for scope in VALID_STATE_SCOPES
|
||||
}
|
||||
keys = {scope: build_state_scope_key(scope, event, binding, descriptor) for scope in VALID_STATE_SCOPES}
|
||||
|
||||
assert keys['conversation'].startswith('conversation:v2:')
|
||||
assert keys['actor'].startswith('actor:v2:')
|
||||
@@ -168,6 +170,7 @@ class TestPersistentStateStore:
|
||||
engine = create_async_engine(f'sqlite+aiosqlite:///{db_path}', echo=False)
|
||||
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
@@ -366,9 +369,7 @@ class TestPersistentStateStore:
|
||||
event = FakeEventEnvelope(conversation_id='conv_001')
|
||||
binding = FakeBinding()
|
||||
|
||||
await persistent_store.apply_update_from_event(
|
||||
event, binding, descriptor, 'conversation', 'key', 'value', None
|
||||
)
|
||||
await persistent_store.apply_update_from_event(event, binding, descriptor, 'conversation', 'key', 'value', None)
|
||||
snapshot = await persistent_store.build_snapshot_from_event(event, binding, descriptor)
|
||||
assert snapshot['conversation']['key'] == 'value'
|
||||
|
||||
|
||||
@@ -95,7 +95,10 @@ def _make_app():
|
||||
delete_pipeline=AsyncMock(),
|
||||
_get_default_values_from_schema=Mock(return_value={}),
|
||||
)
|
||||
app.agent_runner_registry = None
|
||||
app.runner_registry = SimpleNamespace(
|
||||
get=AsyncMock(return_value=SimpleNamespace(usages=['agent'])),
|
||||
list_runners=AsyncMock(return_value=[]),
|
||||
)
|
||||
app.tool_mgr = None
|
||||
app.logger = Mock()
|
||||
return app
|
||||
@@ -178,7 +181,7 @@ class TestAgentServiceDebug:
|
||||
}
|
||||
observer = AsyncMock() if streaming else None
|
||||
|
||||
async def run_agent(event, binding, adapter_context):
|
||||
async def run_runner(event, binding, adapter_context):
|
||||
assert binding.delivery_policy.enable_streaming is streaming
|
||||
await adapter_context['_result_observer']({**visible_event, 'private_context': 'must not leak'})
|
||||
await adapter_context['_result_observer']({'type': 'state.updated', 'data': {'private': True}})
|
||||
@@ -191,7 +194,7 @@ class TestAgentServiceDebug:
|
||||
all_content=None,
|
||||
)
|
||||
|
||||
app.agent_run_orchestrator = SimpleNamespace(run=Mock(side_effect=run_agent))
|
||||
app.agent_run_orchestrator = SimpleNamespace(run=Mock(side_effect=run_runner))
|
||||
service = AgentService(app)
|
||||
service.get_agent = AsyncMock(
|
||||
return_value={
|
||||
@@ -462,13 +465,16 @@ class TestAgentServiceCreateUpdateDelete:
|
||||
app = _make_app()
|
||||
runner = SimpleNamespace(
|
||||
id='plugin:langbot-team/LocalAgent/default',
|
||||
usages=['agent'],
|
||||
config_schema=[
|
||||
{'name': 'model', 'default': 'gpt-4.1'},
|
||||
{'name': 'temperature', 'default': 0.2},
|
||||
{'name': 'no-default'},
|
||||
],
|
||||
)
|
||||
app.agent_runner_registry = SimpleNamespace(list_runners=AsyncMock(return_value=[runner]))
|
||||
app.runner_registry = SimpleNamespace(
|
||||
list_runners=AsyncMock(return_value=[runner]), get=AsyncMock(return_value=runner)
|
||||
)
|
||||
app.pipeline_service._get_default_values_from_schema = Mock(
|
||||
return_value={'model': 'gpt-4.1', 'temperature': 0.2}
|
||||
)
|
||||
@@ -800,13 +806,14 @@ async def test_event_processor_can_be_created_before_selecting_a_plugin():
|
||||
|
||||
async def test_event_processor_creation_uses_installed_component_scope():
|
||||
app = _make_app()
|
||||
ref = 'event_processor:test/welcome/default'
|
||||
ref = 'plugin:test/welcome/default'
|
||||
descriptor = SimpleNamespace(
|
||||
component_kind='EventProcessor',
|
||||
component_kind='Runner',
|
||||
usages=['event'],
|
||||
supported_event_patterns=['group.member_joined'],
|
||||
config_schema=[{'name': 'greeting', 'required': True}],
|
||||
)
|
||||
app.agent_runner_registry = SimpleNamespace(get=AsyncMock(return_value=descriptor))
|
||||
app.runner_registry = SimpleNamespace(get=AsyncMock(return_value=descriptor))
|
||||
service = AgentService(app)
|
||||
result = await service.create_agent(
|
||||
WORKSPACE_UUID,
|
||||
@@ -830,11 +837,12 @@ async def test_unconfigured_event_processor_can_select_a_plugin_after_creation()
|
||||
row = _agent_row(config={})
|
||||
row.kind = 'event_processor'
|
||||
row.component_ref = None
|
||||
ref = 'event_processor:test/welcome/default'
|
||||
app.agent_runner_registry = SimpleNamespace(
|
||||
ref = 'plugin:test/welcome/default'
|
||||
app.runner_registry = SimpleNamespace(
|
||||
get=AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
component_kind='EventProcessor',
|
||||
component_kind='Runner',
|
||||
usages=['event'],
|
||||
supported_event_patterns=['group.member_joined'],
|
||||
config_schema=[{'name': 'greeting', 'required': True}],
|
||||
)
|
||||
@@ -857,27 +865,26 @@ async def test_event_processor_rejects_invalid_component_and_missing_parameters(
|
||||
app = _make_app()
|
||||
service = AgentService(app)
|
||||
with pytest.raises(ValueError, match='Select an installed'):
|
||||
await service.create_agent(WORKSPACE_UUID, {'kind': 'event_processor', 'component_ref': 'plugin:a/b/c'})
|
||||
app.agent_runner_registry = SimpleNamespace(
|
||||
await service.create_agent(WORKSPACE_UUID, {'kind': 'event_processor', 'component_ref': 'invalid:a/b/c'})
|
||||
app.runner_registry = SimpleNamespace(
|
||||
get=AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
component_kind='EventProcessor',
|
||||
component_kind='Runner',
|
||||
usages=['event'],
|
||||
supported_event_patterns=['*'],
|
||||
config_schema=[{'name': 'greeting', 'required': True}],
|
||||
)
|
||||
)
|
||||
)
|
||||
with pytest.raises(ValueError, match='Required processor parameter'):
|
||||
await service.create_agent(
|
||||
WORKSPACE_UUID, {'kind': 'event_processor', 'component_ref': 'event_processor:a/b/c'}
|
||||
)
|
||||
await service.create_agent(WORKSPACE_UUID, {'kind': 'event_processor', 'component_ref': 'plugin:a/b/c'})
|
||||
|
||||
|
||||
async def test_unavailable_event_processor_can_still_be_renamed():
|
||||
app = _make_app()
|
||||
row = _agent_row(config={'runner': {'id': 'event_processor:a/b/c'}, 'runner_config': {'event_processor:a/b/c': {}}})
|
||||
row = _agent_row(config={'runner': {'id': 'plugin:a/b/c'}, 'runner_config': {'plugin:a/b/c': {}}})
|
||||
row.kind = 'event_processor'
|
||||
row.component_ref = 'event_processor:a/b/c'
|
||||
row.component_ref = 'plugin:a/b/c'
|
||||
service = AgentService(app)
|
||||
service._get_agent_row = AsyncMock(return_value=row)
|
||||
await service.update_agent(WORKSPACE_UUID, row.uuid, {'name': 'Renamed'})
|
||||
|
||||
@@ -6,7 +6,7 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.api.http.service.pipeline import PipelineService
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class FakeRegistry:
|
||||
|
||||
def make_runner(runner_id: str, config_schema: list[dict]):
|
||||
parts = runner_id.removeprefix('plugin:').split('/')
|
||||
return AgentRunnerDescriptor(
|
||||
return RunnerDescriptor(
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': runner_id},
|
||||
@@ -51,7 +51,7 @@ async def test_default_pipeline_config_uses_first_installed_runner_schema():
|
||||
)
|
||||
ap = SimpleNamespace(
|
||||
logger=FakeLogger(),
|
||||
agent_runner_registry=FakeRegistry([custom_agent, local_agent]),
|
||||
runner_registry=FakeRegistry([custom_agent, local_agent]),
|
||||
)
|
||||
|
||||
config = await PipelineService(ap).get_default_pipeline_config('workspace-test')
|
||||
@@ -68,7 +68,7 @@ async def test_default_pipeline_config_uses_first_installed_runner_schema():
|
||||
async def test_default_pipeline_config_stays_neutral_without_installed_runners():
|
||||
ap = SimpleNamespace(
|
||||
logger=FakeLogger(),
|
||||
agent_runner_registry=FakeRegistry([]),
|
||||
runner_registry=FakeRegistry([]),
|
||||
)
|
||||
|
||||
config = await PipelineService(ap).get_default_pipeline_config('workspace-test')
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Preserve persisted invocation state while normalizing the Runner table name."""
|
||||
|
||||
import importlib
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
migration = importlib.import_module('langbot.pkg.persistence.alembic.versions.0024_unify_runner_state')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('current_table_exists', [False, True])
|
||||
def test_upgrade_preserves_state_and_downgrade_restores_name(current_table_exists):
|
||||
engine = sa.create_engine('sqlite://')
|
||||
with engine.begin() as conn:
|
||||
conn.execute(sa.text('CREATE TABLE agent_runner_state (id INTEGER PRIMARY KEY, value_json TEXT)'))
|
||||
conn.execute(sa.text("INSERT INTO agent_runner_state VALUES (1, 'saved value')"))
|
||||
if current_table_exists:
|
||||
conn.execute(sa.text('CREATE TABLE runner_state (id INTEGER PRIMARY KEY, value_json TEXT)'))
|
||||
with patch.object(migration, 'op', Operations(MigrationContext.configure(conn))):
|
||||
migration.upgrade()
|
||||
assert 'agent_runner_state' not in sa.inspect(conn).get_table_names()
|
||||
assert conn.scalar(sa.text('SELECT value_json FROM runner_state')) == 'saved value'
|
||||
migration.downgrade()
|
||||
assert 'runner_state' not in sa.inspect(conn).get_table_names()
|
||||
assert conn.scalar(sa.text('SELECT value_json FROM agent_runner_state')) == 'saved value'
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_two_populated_tables_fail_without_discarding_either():
|
||||
engine = sa.create_engine('sqlite://')
|
||||
with engine.begin() as conn:
|
||||
for name in ('agent_runner_state', 'runner_state'):
|
||||
conn.execute(sa.text(f'CREATE TABLE {name} (id INTEGER PRIMARY KEY)'))
|
||||
conn.execute(sa.text(f'INSERT INTO {name} VALUES (1)'))
|
||||
with patch.object(migration, 'op', Operations(MigrationContext.configure(conn))):
|
||||
with pytest.raises(RuntimeError, match='Both .* contain state'):
|
||||
migration.upgrade()
|
||||
assert set(sa.inspect(conn).get_table_names()) == {'agent_runner_state', 'runner_state'}
|
||||
engine.dispose()
|
||||
@@ -395,7 +395,7 @@ async def test_runtime_pipeline_revalidates_after_awaited_stage(
|
||||
|
||||
|
||||
def test_runtime_pipeline_prefers_local_agent_mcp_resources(mock_app):
|
||||
"""AgentRunner resource selection should override legacy extension prefs."""
|
||||
"""Runner resource selection should override legacy extension prefs."""
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from importlib import import_module
|
||||
|
||||
from langbot_plugin.api.entities.builtin.provider import session as provider_session
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from tests.factories import (
|
||||
FakeApp,
|
||||
text_query,
|
||||
@@ -30,8 +30,8 @@ from tests.factories import (
|
||||
RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
def attach_agent_runner_descriptor(app):
|
||||
descriptor = AgentRunnerDescriptor(
|
||||
def attach_runner_descriptor(app):
|
||||
descriptor = RunnerDescriptor(
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
@@ -44,8 +44,8 @@ def attach_agent_runner_descriptor(app):
|
||||
],
|
||||
capabilities={'tool_calling': True, 'multimodal_input': True},
|
||||
)
|
||||
app.agent_runner_registry = Mock()
|
||||
app.agent_runner_registry.get = AsyncMock(return_value=descriptor)
|
||||
app.runner_registry = Mock()
|
||||
app.runner_registry.get = AsyncMock(return_value=descriptor)
|
||||
app.tool_mgr.get_resolved_tool_catalog = AsyncMock(return_value=[])
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ class TestPreProcessorModelSelection:
|
||||
mock_model = Mock()
|
||||
mock_model.model_entity = Mock(uuid='primary-model-uuid', abilities=['func_call'])
|
||||
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=mock_model)
|
||||
attach_agent_runner_descriptor(app)
|
||||
attach_runner_descriptor(app)
|
||||
|
||||
mock_event_ctx = Mock()
|
||||
mock_event_ctx.event = Mock(default_prompt=[], prompt=[])
|
||||
@@ -393,7 +393,7 @@ class TestPreProcessorModelSelection:
|
||||
raise ValueError(f'Model {uuid} not found')
|
||||
|
||||
app.model_mgr.get_model_by_uuid = AsyncMock(side_effect=mock_get_model)
|
||||
attach_agent_runner_descriptor(app)
|
||||
attach_runner_descriptor(app)
|
||||
|
||||
mock_event_ctx = Mock()
|
||||
mock_event_ctx.event = Mock(default_prompt=[], prompt=[])
|
||||
@@ -518,7 +518,7 @@ class TestPreProcessorToolSelection:
|
||||
mock_model = Mock()
|
||||
mock_model.model_entity = Mock(uuid='primary-model-uuid', abilities=['func_call'])
|
||||
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=mock_model)
|
||||
attach_agent_runner_descriptor(app)
|
||||
attach_runner_descriptor(app)
|
||||
app.tool_mgr.get_resolved_tool_catalog = AsyncMock(
|
||||
return_value=[
|
||||
{'name': 'exec', 'source': 'builtin'},
|
||||
|
||||
@@ -13,10 +13,10 @@ from langbot_plugin.api.entities.builtin.provider import session as provider_ses
|
||||
RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
def _attach_agent_runner_descriptor(app):
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
def _attach_runner_descriptor(app):
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
|
||||
descriptor = AgentRunnerDescriptor(
|
||||
descriptor = RunnerDescriptor(
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
@@ -29,8 +29,8 @@ def _attach_agent_runner_descriptor(app):
|
||||
],
|
||||
capabilities={'tool_calling': True, 'multimodal_input': True},
|
||||
)
|
||||
app.agent_runner_registry = Mock()
|
||||
app.agent_runner_registry.get = AsyncMock(return_value=descriptor)
|
||||
app.runner_registry = Mock()
|
||||
app.runner_registry.get = AsyncMock(return_value=descriptor)
|
||||
|
||||
|
||||
def _pipeline_config(model_config):
|
||||
@@ -72,7 +72,7 @@ async def test_preprocessor_keeps_image_placeholder_for_text_only_local_agent(mo
|
||||
model.model_entity.abilities = []
|
||||
|
||||
mock_app.model_mgr.get_model_by_uuid = AsyncMock(return_value=model)
|
||||
_attach_agent_runner_descriptor(mock_app)
|
||||
_attach_runner_descriptor(mock_app)
|
||||
mock_app.sess_mgr.get_session = AsyncMock(
|
||||
return_value=provider_session.Session(
|
||||
launcher_type=sample_query.launcher_type,
|
||||
|
||||
@@ -732,7 +732,7 @@ async def test_bound_event_processor_receives_one_complete_typed_event():
|
||||
if False:
|
||||
yield None
|
||||
|
||||
ref = 'event_processor:test/welcome/default'
|
||||
ref = 'plugin:test/welcome/default'
|
||||
bot.ap = SimpleNamespace(
|
||||
workspace_service=active_workspace_service(),
|
||||
agent_service=SimpleNamespace(
|
||||
|
||||
@@ -80,7 +80,7 @@ def configure_handler(connector, runtime_handler):
|
||||
async def _collect_agent_results(connector, context):
|
||||
return [
|
||||
result
|
||||
async for result in connector.run_agent(
|
||||
async for result in connector.run_runner(
|
||||
'qa',
|
||||
'agent-runner',
|
||||
'default',
|
||||
@@ -98,7 +98,7 @@ class TestRunAgent:
|
||||
class RuntimeHandler:
|
||||
installation_scope = Mock(side_effect=lambda _binding: nullcontext())
|
||||
|
||||
async def run_agent(self, *_args):
|
||||
async def run_runner(self, *_args):
|
||||
yield {'type': 'run.completed'}
|
||||
|
||||
configure_handler(connector, RuntimeHandler())
|
||||
|
||||
@@ -356,7 +356,7 @@ async def test_marketplace_upgrade_reports_multistep_progress():
|
||||
connector._persist_installation_package = AsyncMock(side_effect=persist)
|
||||
connector.handler.apply_plugin_installation = AsyncMock(side_effect=apply)
|
||||
connector._wait_for_installed_plugin_ready = AsyncMock(side_effect=wait_until_ready)
|
||||
connector._refresh_agent_runner_registry = AsyncMock(side_effect=refresh_registry)
|
||||
connector._refresh_runner_registry = AsyncMock(side_effect=refresh_registry)
|
||||
|
||||
await connector.upgrade_plugin('author', 'plugin', task_context=task_context)
|
||||
|
||||
|
||||
@@ -816,7 +816,7 @@ class TestHandlerQueryLookup:
|
||||
|
||||
|
||||
class TestAgentRunProxyActions:
|
||||
"""Tests for AgentRunner proxy actions that need host Query semantics."""
|
||||
"""Tests for Runner proxy actions that need host Query semantics."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
@@ -1556,8 +1556,8 @@ class TestAgentRunProxyActions:
|
||||
from langbot.pkg.agent.runner.session_registry import get_session_registry
|
||||
from langbot.pkg.provider.tools.loaders.native import NativeToolLoader
|
||||
from langbot.pkg.provider.tools.toolmgr import ToolManager
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
|
||||
event = AgentEventEnvelope(
|
||||
event_id='event-native-exec',
|
||||
|
||||
@@ -13,7 +13,7 @@ from langbot_plugin.api.entities.builtin.resource import tool as resource_tool
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fake_requester_counts_messages_and_tools(runtime_provider):
|
||||
"""Fake requester should support token-free AgentRunner context budgeting."""
|
||||
"""Fake requester should support token-free Runner context budgeting."""
|
||||
runtime_model = requester.RuntimeLLMModel(
|
||||
model_entity=persistence_model.LLMModel(
|
||||
uuid='fake-count-model',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Unit tests for LiteLLMRequester message/tool conversion.
|
||||
|
||||
This includes provider_specific_fields round-trip coverage for GitHub issue
|
||||
#1899 and token counting preflight behavior for AgentRunner context budgeting.
|
||||
#1899 and token counting preflight behavior for Runner context budgeting.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
@@ -73,7 +73,9 @@ async def test_count_tokens_uses_litellm_counter_with_request_messages_and_tools
|
||||
func=lambda **kwargs: None,
|
||||
)
|
||||
|
||||
with patch('langbot.pkg.provider.modelmgr.requesters.litellmchat.litellm.token_counter', return_value=42) as counter:
|
||||
with patch(
|
||||
'langbot.pkg.provider.modelmgr.requesters.litellmchat.litellm.token_counter', return_value=42
|
||||
) as counter:
|
||||
tokens = await req.count_tokens(
|
||||
model=model,
|
||||
messages=[
|
||||
|
||||
@@ -16,7 +16,7 @@ from langbot_plugin.api.entities.builtin.provider.message import Message
|
||||
from langbot_plugin.api.entities.builtin.provider.prompt import Prompt
|
||||
from langbot_plugin.api.entities.builtin.provider.session import Conversation, LauncherTypes, Session
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ def _make_app(*, skill_service) -> SimpleNamespace:
|
||||
conversation = _make_conversation()
|
||||
model = SimpleNamespace(model_entity=SimpleNamespace(uuid='model-1', abilities={'func_call'}))
|
||||
tool_mgr = SimpleNamespace(get_resolved_tool_catalog=AsyncMock(return_value=[]))
|
||||
descriptor = AgentRunnerDescriptor(
|
||||
descriptor = RunnerDescriptor(
|
||||
id=_RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
@@ -105,7 +105,7 @@ def _make_app(*, skill_service) -> SimpleNamespace:
|
||||
get_conversation=AsyncMock(return_value=conversation),
|
||||
),
|
||||
model_mgr=SimpleNamespace(get_model_by_uuid=AsyncMock(return_value=model)),
|
||||
agent_runner_registry=SimpleNamespace(get=AsyncMock(return_value=descriptor)),
|
||||
runner_registry=SimpleNamespace(get=AsyncMock(return_value=descriptor)),
|
||||
tool_mgr=tool_mgr,
|
||||
plugin_connector=SimpleNamespace(
|
||||
emit_event=AsyncMock(
|
||||
@@ -207,7 +207,7 @@ async def test_preproc_disables_mcp_resource_tools_when_agent_reading_is_disable
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preproc_leaves_skill_prompt_projection_to_agent_runner_resources():
|
||||
async def test_preproc_leaves_skill_prompt_projection_to_runner_resources():
|
||||
preproc_module, entities_module = _import_preproc_modules()
|
||||
|
||||
app = _make_app(skill_service=SimpleNamespace())
|
||||
|
||||
Reference in New Issue
Block a user