feat: make agent runner config schema driven

This commit is contained in:
huanghuoguoguo
2026-05-19 12:20:28 +08:00
parent f4f91c43b5
commit be8d30894a
20 changed files with 901 additions and 236 deletions

View File

@@ -132,7 +132,7 @@ class TestResolveRunnerConfig:
assert config == {'model': 'uuid-123', 'max_round': 10}
def test_resolve_old_format_config(self):
"""Resolve runner config from old format."""
"""Runtime config resolver should not read old format."""
pipeline_config = {
'ai': {
'local-agent': {
@@ -146,6 +146,23 @@ class TestResolveRunnerConfig:
pipeline_config,
'plugin:langbot/local-agent/default',
)
assert config == {}
def test_resolve_legacy_config_for_migration(self):
"""Migration helper should read old format."""
pipeline_config = {
'ai': {
'local-agent': {
'model': 'uuid-123',
'max_round': 10,
},
},
}
config = ConfigMigration.resolve_legacy_runner_config(
pipeline_config,
'plugin:langbot/local-agent/default',
)
assert config == {'model': 'uuid-123', 'max_round': 10}
def test_resolve_no_config(self):
@@ -228,4 +245,4 @@ class TestGetOldRunnerName:
def test_get_old_runner_name_not_mapped(self):
"""Get old runner name for unmapped runner ID."""
old_name = ConfigMigration.get_old_runner_name('plugin:alice/my-agent/custom')
assert old_name is None
assert old_name is None

View File

@@ -229,8 +229,8 @@ class TestResolveRunnerIdBackwardCompat:
assert runner_id == 'plugin:new-runner/default'
class TestResolveRunnerConfigBackwardCompat:
"""Tests for backward compatibility in resolve_runner_config."""
class TestResolveRunnerConfig:
"""Tests for runtime runner config resolution."""
def test_resolve_new_format_config(self):
"""resolve_runner_config should read from runner_config."""
@@ -245,13 +245,23 @@ class TestResolveRunnerConfigBackwardCompat:
assert runner_config['max-round'] == 20
def test_resolve_old_format_config(self):
"""resolve_runner_config should read from old ai.local-agent."""
"""resolve_runner_config should not read old ai.local-agent at runtime."""
config = {
'ai': {
'local-agent': {'max-round': 15},
},
}
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
assert runner_config == {}
def test_resolve_legacy_runner_config_for_migration(self):
"""resolve_legacy_runner_config should read old ai.local-agent for migration."""
config = {
'ai': {
'local-agent': {'max-round': 15},
},
}
runner_config = ConfigMigration.resolve_legacy_runner_config(config, 'plugin:langbot/local-agent/default')
assert runner_config['max-round'] == 15
def test_resolve_new_format_priority(self):

View File

@@ -16,8 +16,9 @@ import pytest
import types
from unittest.mock import AsyncMock, MagicMock
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
from langbot.pkg.plugin.handler import _build_tool_detail
from langbot.pkg.plugin.handler import _build_tool_detail, _get_pipeline_knowledge_base_uuids
# Import shared test fixtures from conftest.py
from .conftest import make_resources
@@ -105,11 +106,53 @@ class MockApplication:
self.persistence_mgr.execute_async = AsyncMock(return_value=MagicMock(first=lambda: None))
class FakeAgentRunnerRegistry:
async def get(self, runner_id, bound_plugins=None):
return AgentRunnerDescriptor(
id=runner_id,
source='plugin',
label={'en_US': 'Test Runner'},
plugin_author='test',
plugin_name='runner',
runner_name='default',
config_schema=[
{'name': 'knowledge-bases', 'type': 'knowledge-base-multi-selector', 'default': []},
],
capabilities={'knowledge_retrieval': True},
permissions={'knowledge_bases': ['list', 'retrieve']},
)
class MockConnection:
"""Mock connection for testing."""
pass
class TestPipelineKnowledgeBaseScope:
"""Tests for schema-driven pipeline KB scope resolution."""
@pytest.mark.asyncio
async def test_uses_preprocessed_query_scope(self):
app = MockApplication()
query = MockQuery()
query.variables = {'_knowledge_base_uuids': ['kb_var', '__none__', 'kb_var']}
kb_uuids = await _get_pipeline_knowledge_base_uuids(app, query)
assert kb_uuids == ['kb_var']
@pytest.mark.asyncio
async def test_uses_runner_schema_when_query_scope_not_preprocessed(self):
app = MockApplication()
app.agent_runner_registry = FakeAgentRunnerRegistry()
query = MockQuery()
query.variables = {}
kb_uuids = await _get_pipeline_knowledge_base_uuids(app, query)
assert kb_uuids == ['kb_001', 'kb_002']
class MockDisconnectCallback:
"""Mock disconnect callback for testing."""
async def __call__(self):

View File

@@ -1,6 +1,7 @@
"""Integration-style tests for AgentRunOrchestrator with a fake plugin runner."""
from __future__ import annotations
import asyncio
import datetime
import types
from unittest.mock import AsyncMock
@@ -61,9 +62,10 @@ class FakeKnowledgeBase:
class FakePluginConnector:
is_enable_plugin = True
def __init__(self, results=None, error: Exception | None = None):
def __init__(self, results=None, error: Exception | None = None, delay: float = 0):
self.results = results or []
self.error = error
self.delay = delay
self.calls: list[dict] = []
self.contexts: list[dict] = []
self.sessions_during_run: list[dict | None] = []
@@ -83,6 +85,8 @@ class FakePluginConnector:
raise self.error
for result in self.results:
if self.delay:
await asyncio.sleep(self.delay)
yield result
@@ -125,7 +129,11 @@ def make_descriptor() -> AgentRunnerDescriptor:
plugin_name="local-agent",
runner_name="default",
protocol_version="1",
capabilities={"streaming": True, "tool_calling": True},
capabilities={"streaming": True, "tool_calling": True, "knowledge_retrieval": True},
config_schema=[
{"name": "model", "type": "model-fallback-selector"},
{"name": "knowledge-bases", "type": "knowledge-base-multi-selector", "default": []},
],
permissions={
"models": ["invoke", "stream"],
"tools": ["list", "detail", "call"],
@@ -367,3 +375,27 @@ async def test_orchestrator_unregisters_session_after_runner_failure():
context = plugin_connector.contexts[0]
assert plugin_connector.sessions_during_run[0] is not None
assert await get_session_registry().get(context["run_id"]) is None
@pytest.mark.asyncio
async def test_orchestrator_enforces_total_runner_deadline():
descriptor = make_descriptor()
plugin_connector = FakePluginConnector(
results=[
{
"type": "message.completed",
"data": {"message": {"role": "assistant", "content": "too late"}},
}
],
delay=0.05,
)
orchestrator = AgentRunOrchestrator(FakeApplication(plugin_connector), FakeRegistry(descriptor))
query = make_query()
query.pipeline_config["ai"]["runner_config"][RUNNER_ID]["timeout"] = 0.01
with pytest.raises(RunnerExecutionError) as exc_info:
[message async for message in orchestrator.run_from_query(query)]
assert exc_info.value.retryable is True
assert "runner.timeout" in str(exc_info.value)
assert await get_session_registry().get(plugin_connector.contexts[0]["run_id"]) is None