diff --git a/pyproject.toml b/pyproject.toml index 629a450e8..4488201b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -232,4 +232,4 @@ line-ending = "auto" [tool.uv.sources] # Development contract: update to the matching SDK release before publishing. -langbot-plugin = { git = "https://github.com/langbot-app/langbot-plugin-sdk", rev = "92a9e03fa9c791f4ed30cc3f5f0602c13b800d28" } +langbot-plugin = { git = "https://github.com/langbot-app/langbot-plugin-sdk", rev = "c67e6c85a0cde8ae2b20cbd89e33805a68382563" } diff --git a/skills/skills/langbot-testing/fixtures/plugins/qa-agent-runner/components/runner/default.yaml b/skills/skills/langbot-testing/fixtures/plugins/qa-agent-runner/components/runner/default.yaml index 75b04f2e4..3e8e65057 100644 --- a/skills/skills/langbot-testing/fixtures/plugins/qa-agent-runner/components/runner/default.yaml +++ b/skills/skills/langbot-testing/fixtures/plugins/qa-agent-runner/components/runner/default.yaml @@ -9,6 +9,7 @@ metadata: en_US: Deterministic runner fixture that returns stable QA sentinel output. zh_Hans: 返回稳定 QA 哨兵输出的确定性 runner 夹具。 spec: + usages: [agent] capabilities: streaming: true tool_calling: false diff --git a/src/langbot/pkg/agent/runner/descriptor.py b/src/langbot/pkg/agent/runner/descriptor.py index 5933f8ad9..aab6805a1 100644 --- a/src/langbot/pkg/agent/runner/descriptor.py +++ b/src/langbot/pkg/agent/runner/descriptor.py @@ -55,7 +55,7 @@ class RunnerDescriptor(pydantic.BaseModel): """Original manifest for reference""" component_kind: typing.Literal['Runner'] = 'Runner' - usages: list[typing.Literal['agent', 'event']] = pydantic.Field(default_factory=lambda: ['agent']) + usages: list[typing.Literal['agent', 'event']] = pydantic.Field(min_length=1) supported_event_patterns: list[str] = pydantic.Field(default_factory=lambda: ['*']) model_config = pydantic.ConfigDict( diff --git a/tests/unit_tests/agent/test_context_builder_state.py b/tests/unit_tests/agent/test_context_builder_state.py index 746d9d5ea..0c38042ec 100644 --- a/tests/unit_tests/agent/test_context_builder_state.py +++ b/tests/unit_tests/agent/test_context_builder_state.py @@ -32,6 +32,7 @@ def make_descriptor( permissions: dict | None = None, ) -> RunnerDescriptor: return RunnerDescriptor( + usages=['agent'], id='plugin:test/runner/default', source='plugin', label={'en_US': 'Test Runner'}, diff --git a/tests/unit_tests/agent/test_context_validation.py b/tests/unit_tests/agent/test_context_validation.py index dc01c743a..bb47a6e12 100644 --- a/tests/unit_tests/agent/test_context_validation.py +++ b/tests/unit_tests/agent/test_context_validation.py @@ -91,6 +91,7 @@ class TestContextValidation: def _make_descriptor(self): """Create a mock runner descriptor.""" return RunnerDescriptor( + usages=['agent'], id='plugin:test/plugin/runner', source='plugin', label={'en_US': 'Test Runner'}, diff --git a/tests/unit_tests/agent/test_handler_auth.py b/tests/unit_tests/agent/test_handler_auth.py index 72a1638d7..60668414e 100644 --- a/tests/unit_tests/agent/test_handler_auth.py +++ b/tests/unit_tests/agent/test_handler_auth.py @@ -125,6 +125,7 @@ class MockApplication: class FakeRunnerRegistry: async def get(self, context, runner_id, bound_plugins=None): return RunnerDescriptor( + usages=['agent'], id=runner_id, source='plugin', label={'en_US': 'Test Runner'}, diff --git a/tests/unit_tests/agent/test_local_runner_e2e_fixtures.py b/tests/unit_tests/agent/test_local_runner_e2e_fixtures.py index 46225fe20..f7a354f6c 100644 --- a/tests/unit_tests/agent/test_local_runner_e2e_fixtures.py +++ b/tests/unit_tests/agent/test_local_runner_e2e_fixtures.py @@ -23,6 +23,7 @@ SOURCE = {'source': 'native', 'source_id': None} async def _resources(tool_mgr, rag_mgr, binding): descriptor = RunnerDescriptor( + usages=['agent'], id=LOCAL_RUNNER_ID, source='plugin', label={'en_US': 'Local Agent'}, diff --git a/tests/unit_tests/agent/test_orchestrator_integration.py b/tests/unit_tests/agent/test_orchestrator_integration.py index 3d4697eae..ad55fbd70 100644 --- a/tests/unit_tests/agent/test_orchestrator_integration.py +++ b/tests/unit_tests/agent/test_orchestrator_integration.py @@ -164,6 +164,7 @@ class FakeConversation: def make_descriptor() -> RunnerDescriptor: return RunnerDescriptor( + usages=['agent'], id=RUNNER_ID, source='plugin', label={'en_US': 'Local Agent'}, diff --git a/tests/unit_tests/agent/test_registry.py b/tests/unit_tests/agent/test_registry.py index 2cc0dc3af..da7780622 100644 --- a/tests/unit_tests/agent/test_registry.py +++ b/tests/unit_tests/agent/test_registry.py @@ -55,6 +55,7 @@ class FakeApplication: 'id': 'plugin:langbot-team/LocalAgent/default', 'name': 'default', 'label': {'en_US': 'Local Agent'}, + 'usages': ['agent'], 'capabilities': {'streaming': True}, 'permissions': {}, 'config_schema': [], @@ -68,6 +69,7 @@ class FakeApplication: 'id': 'plugin:alice/my-agent/custom', 'name': 'custom', 'label': {'en_US': 'Custom Agent'}, + 'usages': ['agent'], 'capabilities': {}, 'permissions': {}, 'config_schema': [{'name': 'param1', 'type': 'string'}], @@ -308,6 +310,7 @@ class TestDescriptorValidation: def test_validate_runner_descriptor(self): """Validate correctly built descriptor.""" descriptor = RunnerDescriptor( + usages=['agent'], id='plugin:test/my-runner/default', source='plugin', label={'en_US': 'Test Runner'}, @@ -323,6 +326,7 @@ class TestDescriptorValidation: def test_descriptor_capabilities(self): """Descriptor capability helper methods.""" descriptor = RunnerDescriptor( + usages=['agent'], id='plugin:test/my-runner/default', source='plugin', label={'en_US': 'Test Runner'}, @@ -365,3 +369,18 @@ async def test_registry_filters_usages_without_splitting_component_identity(): 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'] + + +@pytest.mark.asyncio +async def test_discovery_rejects_runner_without_usage_declaration(): + ap = FakeApplication() + original = ap.plugin_connector.list_runners + + async def missing_usage(bound_plugins=None): + runners = await original(bound_plugins) + runners[0]['manifest'].pop('usages') + return runners + + ap.plugin_connector.list_runners = missing_usage + runners = await RunnerRegistry(ap).list_runners(TEST_CONTEXT, use_cache=False) + assert [runner.id for runner in runners] == ['plugin:alice/my-agent/custom'] diff --git a/tests/unit_tests/agent/test_resource_builder.py b/tests/unit_tests/agent/test_resource_builder.py index 67fa16295..03c2fc69a 100644 --- a/tests/unit_tests/agent/test_resource_builder.py +++ b/tests/unit_tests/agent/test_resource_builder.py @@ -39,6 +39,7 @@ def make_descriptor( permissions: dict | None = None, ) -> RunnerDescriptor: return RunnerDescriptor( + usages=['agent'], id=RUNNER_ID, source='plugin', label={'en_US': 'Test Runner'}, diff --git a/tests/unit_tests/agent/test_result_normalizer.py b/tests/unit_tests/agent/test_result_normalizer.py index 6a332ceac..b4dbe0970 100644 --- a/tests/unit_tests/agent/test_result_normalizer.py +++ b/tests/unit_tests/agent/test_result_normalizer.py @@ -37,6 +37,7 @@ class FakeApplication: def make_descriptor(): """Create a test descriptor.""" return RunnerDescriptor( + usages=['agent'], id='plugin:langbot-team/LocalAgent/default', source='plugin', label={'en_US': 'Local Agent', 'zh_Hans': '内置 Agent'}, diff --git a/tests/unit_tests/agent/test_state_store.py b/tests/unit_tests/agent/test_state_store.py index 756904af3..4154cd840 100644 --- a/tests/unit_tests/agent/test_state_store.py +++ b/tests/unit_tests/agent/test_state_store.py @@ -25,6 +25,7 @@ from langbot.pkg.agent.runner.state_scope import ( def make_descriptor(runner_id: str = 'plugin:test/my-runner/default') -> RunnerDescriptor: """Create a test descriptor.""" return RunnerDescriptor( + usages=['agent'], id=runner_id, source='plugin', label={'en_US': 'Test Runner'}, diff --git a/tests/unit_tests/api/test_pipeline_service_defaults.py b/tests/unit_tests/api/test_pipeline_service_defaults.py index 710565503..dd7687d87 100644 --- a/tests/unit_tests/api/test_pipeline_service_defaults.py +++ b/tests/unit_tests/api/test_pipeline_service_defaults.py @@ -26,6 +26,7 @@ class FakeRegistry: def make_runner(runner_id: str, config_schema: list[dict]): parts = runner_id.removeprefix('plugin:').split('/') return RunnerDescriptor( + usages=['agent'], id=runner_id, source='plugin', label={'en_US': runner_id}, diff --git a/tests/unit_tests/pipeline/test_preproc.py b/tests/unit_tests/pipeline/test_preproc.py index 9ebb6dd75..46cccadad 100644 --- a/tests/unit_tests/pipeline/test_preproc.py +++ b/tests/unit_tests/pipeline/test_preproc.py @@ -32,6 +32,7 @@ RUNNER_ID = 'plugin:langbot-team/LocalAgent/default' def attach_runner_descriptor(app): descriptor = RunnerDescriptor( + usages=['agent'], id=RUNNER_ID, source='plugin', label={'en_US': 'Local Agent'}, diff --git a/tests/unit_tests/pipeline/test_preproc_media_fallback.py b/tests/unit_tests/pipeline/test_preproc_media_fallback.py index c596fc1cd..52da1210f 100644 --- a/tests/unit_tests/pipeline/test_preproc_media_fallback.py +++ b/tests/unit_tests/pipeline/test_preproc_media_fallback.py @@ -17,6 +17,7 @@ def _attach_runner_descriptor(app): from langbot.pkg.agent.runner.descriptor import RunnerDescriptor descriptor = RunnerDescriptor( + usages=['agent'], id=RUNNER_ID, source='plugin', label={'en_US': 'Local Agent'}, diff --git a/tests/unit_tests/test_preproc.py b/tests/unit_tests/test_preproc.py index 3b67547f1..13dd9d75b 100644 --- a/tests/unit_tests/test_preproc.py +++ b/tests/unit_tests/test_preproc.py @@ -80,6 +80,7 @@ def _make_app(*, skill_service) -> SimpleNamespace: model = SimpleNamespace(model_entity=SimpleNamespace(uuid='model-1', abilities={'func_call'})) tool_mgr = SimpleNamespace(get_resolved_tool_catalog=AsyncMock(return_value=[])) descriptor = RunnerDescriptor( + usages=['agent'], id=_RUNNER_ID, source='plugin', label={'en_US': 'Local Agent'}, diff --git a/uv.lock b/uv.lock index 623f676c9..bc64d46a4 100644 --- a/uv.lock +++ b/uv.lock @@ -2119,7 +2119,7 @@ requires-dist = [ { name = "ebooklib", specifier = ">=0.18" }, { name = "gewechat-client", specifier = ">=0.1.5" }, { name = "html2text", specifier = ">=2024.2.26" }, - { name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=92a9e03fa9c791f4ed30cc3f5f0602c13b800d28" }, + { name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=c67e6c85a0cde8ae2b20cbd89e33805a68382563" }, { name = "langchain", specifier = ">=1.3.9" }, { name = "langchain-core", specifier = ">=1.3.3" }, { name = "langchain-text-splitters", specifier = ">=1.1.2" }, @@ -2186,7 +2186,7 @@ dev = [ [[package]] name = "langbot-plugin" version = "0.5.5" -source = { git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=92a9e03fa9c791f4ed30cc3f5f0602c13b800d28#92a9e03fa9c791f4ed30cc3f5f0602c13b800d28" } +source = { git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=c67e6c85a0cde8ae2b20cbd89e33805a68382563#c67e6c85a0cde8ae2b20cbd89e33805a68382563" } dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, diff --git a/web/src/app/home/agents/components/PluginProcessorSettings.tsx b/web/src/app/home/agents/components/PluginProcessorSettings.tsx index 349f07463..34bc94ce8 100644 --- a/web/src/app/home/agents/components/PluginProcessorSettings.tsx +++ b/web/src/app/home/agents/components/PluginProcessorSettings.tsx @@ -115,7 +115,7 @@ export default function PluginProcessorSettings({