feat(agent-runner): expose skill resources through host context

This commit is contained in:
huanghuoguoguo
2026-06-07 12:48:06 +08:00
parent a9a2c18719
commit fa7b1b53a6
20 changed files with 463 additions and 193 deletions

View File

@@ -8,6 +8,7 @@ def make_resources(
models: list[dict] | None = None,
tools: list[dict] | None = None,
knowledge_bases: list[dict] | None = None,
skills: list[dict] | None = None,
storage: dict | None = None,
files: list[dict] | None = None,
) -> dict[str, typing.Any]:
@@ -17,6 +18,7 @@ def make_resources(
models: List of model dicts with 'model_id' key
tools: List of tool dicts with 'tool_name' key
knowledge_bases: List of KB dicts with 'kb_id' key
skills: List of skill dicts with 'skill_name' key
storage: Storage permissions dict
files: List of file dicts with 'file_id' key
@@ -27,6 +29,7 @@ def make_resources(
'models': models or [],
'tools': tools or [],
'knowledge_bases': knowledge_bases or [],
'skills': skills or [],
'files': files or [],
'storage': storage or {'plugin_storage': False, 'workspace_storage': False},
'platform_capabilities': {},
@@ -71,6 +74,7 @@ def make_session(
'model': {m.get('model_id') for m in res.get('models', [])},
'tool': {t.get('tool_name') for t in res.get('tools', [])},
'knowledge_base': {kb.get('kb_id') for kb in res.get('knowledge_bases', [])},
'skill': {s.get('skill_name') for s in res.get('skills', [])},
'file': {f.get('file_id') for f in res.get('files', [])},
}

View File

@@ -80,6 +80,7 @@ class TestContextValidation:
'models': [],
'tools': [],
'knowledge_bases': [],
'skills': [],
'files': [],
'storage': {'plugin_storage': True, 'workspace_storage': True},
'platform_capabilities': {},

View File

@@ -124,6 +124,20 @@ class FakeApplication:
self.rag_mgr = types.SimpleNamespace(
get_knowledge_base_by_uuid=AsyncMock(return_value=FakeKnowledgeBase("kb_001"))
)
self.skill_mgr = types.SimpleNamespace(
skills={
"demo": {
"name": "demo",
"display_name": "Demo Skill",
"description": "Helps with demo tasks.",
},
"hidden": {
"name": "hidden",
"display_name": "Hidden Skill",
"description": "Not bound to this pipeline.",
},
}
)
class FakeConversation:
@@ -140,7 +154,12 @@ def make_descriptor() -> AgentRunnerDescriptor:
plugin_name="local-agent",
runner_name="default",
protocol_version="1",
capabilities={"streaming": True, "tool_calling": True, "knowledge_retrieval": True},
capabilities={
"streaming": True,
"tool_calling": True,
"knowledge_retrieval": True,
"skill_authoring": True,
},
config_schema=[
{"name": "model", "type": "model-fallback-selector"},
{"name": "knowledge-bases", "type": "knowledge-base-multi-selector", "default": []},
@@ -211,6 +230,7 @@ def make_query():
variables={
"_pipeline_bound_plugins": ["langbot/local-agent"],
"_fallback_model_uuids": ["model_fallback"],
"_pipeline_bound_skills": ["demo"],
"public_param": "visible",
},
use_llm_model_uuid="model_primary",
@@ -323,12 +343,20 @@ async def test_orchestrator_runs_fake_plugin_with_authorized_context(clean_agent
assert {m["model_id"] for m in resources["models"]} == {"model_primary", "model_fallback"}
assert resources["tools"][0]["tool_name"] == "langbot/test-tool/search"
assert resources["knowledge_bases"][0]["kb_id"] == "kb_001"
assert resources["skills"] == [
{
"skill_name": "demo",
"display_name": "Demo Skill",
"description": "Helps with demo tasks.",
}
]
assert resources["storage"]["plugin_storage"] is True
session_during_run = plugin_connector.sessions_during_run[0]
assert session_during_run is not None
assert session_during_run["plugin_identity"] == "langbot/local-agent"
assert session_during_run["authorization"]["authorized_ids"]["tool"] == {"langbot/test-tool/search"}
assert session_during_run["authorization"]["authorized_ids"]["skill"] == {"demo"}
assert await get_session_registry().get(context["run_id"]) is None
@@ -766,6 +794,50 @@ class TestQueryEntryAdapterHostCapabilities:
snapshot = await persistent_store.build_snapshot_from_event(event, binding, descriptor)
assert snapshot["conversation"]["external.test_key"] == "test_value"
@pytest.mark.asyncio
async def test_run_from_query_restores_activated_skills_from_state(self, clean_agent_state):
"""Persisted activated skill names are restored into the next Query run."""
from langbot.pkg.agent.runner.persistent_state_store import get_persistent_state_store
from langbot.pkg.provider.tools.loaders.skill import (
ACTIVATED_SKILL_NAMES_STATE_KEY,
ACTIVATED_SKILLS_KEY,
)
db_engine = clean_agent_state
descriptor = make_descriptor()
plugin_connector = FakePluginConnector(
results=[
{
"type": "message.completed",
"data": {"message": {"role": "assistant", "content": "restored"}},
}
]
)
ap = FakeApplication(plugin_connector, db_engine)
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(descriptor))
query = make_query()
persistent_store = get_persistent_state_store(db_engine)
event = QueryEntryAdapter.query_to_event(query)
agent_config = QueryEntryAdapter.config_to_agent_config(query, RUNNER_ID)
binding = AgentBindingResolver().resolve_one(event, [agent_config])
success, error = await persistent_store.apply_update_from_event(
event,
binding,
descriptor,
"conversation",
ACTIVATED_SKILL_NAMES_STATE_KEY,
["demo"],
None,
)
assert success is True
assert error is None
messages = [message async for message in orchestrator.run_from_query(query)]
assert len(messages) == 1
assert query.variables[ACTIVATED_SKILLS_KEY]["demo"]["name"] == "demo"
@pytest.mark.asyncio
async def test_event_log_and_transcript_written(self, clean_agent_state):
"""EventLog and Transcript are written via Pipeline path."""

View File

@@ -307,6 +307,21 @@ class TestIsResourceAllowed:
assert registry.is_resource_allowed(session, 'knowledge_base', 'kb_001') is False
def test_skill_allowed(self):
"""Skill in resources should be allowed."""
registry = AgentRunSessionRegistry()
resources = make_resources(
skills=[
{'skill_name': 'demo', 'display_name': 'Demo'},
{'skill_name': 'writer', 'display_name': 'Writer'},
]
)
session = make_session(resources=resources)
assert registry.is_resource_allowed(session, 'skill', 'demo') is True
assert registry.is_resource_allowed(session, 'skill', 'writer') is True
assert registry.is_resource_allowed(session, 'skill', 'hidden') is False
def test_storage_plugin_allowed(self):
"""Plugin storage permission should be checked."""
registry = AgentRunSessionRegistry()