mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-09 23:36:02 +00:00
feat(agent-runner): expose skill resources through host context
This commit is contained in:
@@ -71,13 +71,6 @@ def supports_skill_authoring(descriptor: AgentRunnerDescriptor | None) -> bool:
|
||||
return bool(descriptor.capabilities.get('skill_authoring', False))
|
||||
|
||||
|
||||
def supports_skill_injection(descriptor: AgentRunnerDescriptor | None) -> bool:
|
||||
"""Return whether the runner wants the Host skill index in the effective prompt."""
|
||||
if descriptor is None:
|
||||
return False
|
||||
return bool(descriptor.capabilities.get('skill_injection', False))
|
||||
|
||||
|
||||
def extract_prompt_config(
|
||||
descriptor: AgentRunnerDescriptor | None,
|
||||
runner_config: dict[str, typing.Any],
|
||||
|
||||
@@ -84,6 +84,14 @@ class KnowledgeBaseResource(typing.TypedDict):
|
||||
kb_type: str | None
|
||||
|
||||
|
||||
class SkillResource(typing.TypedDict):
|
||||
"""Skill resource payload."""
|
||||
|
||||
skill_name: str
|
||||
display_name: str | None
|
||||
description: str | None
|
||||
|
||||
|
||||
class FileResource(typing.TypedDict):
|
||||
"""File resource payload."""
|
||||
|
||||
@@ -106,6 +114,7 @@ class AgentResources(typing.TypedDict):
|
||||
models: list[ModelResource]
|
||||
tools: list[ToolResource]
|
||||
knowledge_bases: list[KnowledgeBaseResource]
|
||||
skills: list[SkillResource]
|
||||
files: list[FileResource]
|
||||
storage: StorageResource
|
||||
platform_capabilities: dict[str, typing.Any]
|
||||
|
||||
@@ -95,6 +95,9 @@ class ResourcePolicy(pydantic.BaseModel):
|
||||
allowed_kb_uuids: list[str] | None = None
|
||||
"""Additional knowledge base UUID grants. None means no additional KB grants."""
|
||||
|
||||
allowed_skill_names: list[str] | None = None
|
||||
"""Allowed skill names. None means all currently visible skills are allowed."""
|
||||
|
||||
allow_plugin_storage: bool = True
|
||||
"""Whether plugin storage is allowed."""
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from .result_normalizer import AgentResultNormalizer
|
||||
from .run_journal import AgentRunJournal, MAX_ARTIFACT_INLINE_BYTES as _MAX_ARTIFACT_INLINE_BYTES
|
||||
from .session_registry import AgentRunSessionRegistry, get_session_registry
|
||||
from .state_scope import build_state_context
|
||||
from ...provider.tools.loaders import skill as skill_loader
|
||||
|
||||
|
||||
MAX_ARTIFACT_INLINE_BYTES = _MAX_ARTIFACT_INLINE_BYTES
|
||||
@@ -86,6 +87,13 @@ class AgentRunOrchestrator:
|
||||
|
||||
session_query_id = None
|
||||
if adapter_context:
|
||||
query = adapter_context.get('_query')
|
||||
if query is not None:
|
||||
skill_loader.restore_activated_skills_from_state(
|
||||
self.ap,
|
||||
query,
|
||||
context.get('state', {}),
|
||||
)
|
||||
session_query_id = adapter_context.get('query_id')
|
||||
if 'params' in adapter_context:
|
||||
context['adapter']['extra']['params'] = adapter_context['params']
|
||||
@@ -175,11 +183,13 @@ class AgentRunOrchestrator:
|
||||
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
|
||||
"""Run an AgentRunner from the current Pipeline Query entry point."""
|
||||
plan = self.query_bridge.build_plan(query)
|
||||
adapter_context = dict(plan.adapter_context)
|
||||
adapter_context['_query'] = query
|
||||
async for result in self.run(
|
||||
plan.event,
|
||||
plan.binding,
|
||||
bound_plugins=plan.bound_plugins,
|
||||
adapter_context=plan.adapter_context,
|
||||
adapter_context=adapter_context,
|
||||
):
|
||||
yield result
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ class QueryEntryAdapter:
|
||||
allowed_model_uuids=cls._extract_allowed_models(query),
|
||||
allowed_tool_names=cls._extract_allowed_tools(query),
|
||||
allowed_kb_uuids=cls._extract_allowed_kbs(query),
|
||||
allowed_skill_names=cls._extract_allowed_skills(query),
|
||||
)
|
||||
|
||||
# Build state policy
|
||||
@@ -583,3 +584,19 @@ class QueryEntryAdapter:
|
||||
if kb_uuids:
|
||||
return kb_uuids
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _extract_allowed_skills(
|
||||
cls,
|
||||
query: pipeline_query.Query,
|
||||
) -> list[str] | None:
|
||||
"""Extract pipeline-visible skill names from query."""
|
||||
variables = getattr(query, 'variables', None)
|
||||
if not variables or '_pipeline_bound_skills' not in variables:
|
||||
return None
|
||||
bound_skills = variables.get('_pipeline_bound_skills')
|
||||
if bound_skills is None:
|
||||
return None
|
||||
if not isinstance(bound_skills, list):
|
||||
return []
|
||||
return [str(skill_name) for skill_name in bound_skills if skill_name]
|
||||
|
||||
@@ -10,6 +10,7 @@ from .context_builder import (
|
||||
ModelResource,
|
||||
ToolResource,
|
||||
KnowledgeBaseResource,
|
||||
SkillResource,
|
||||
StorageResource,
|
||||
)
|
||||
from . import config_schema
|
||||
@@ -36,6 +37,7 @@ class AgentResourceBuilder:
|
||||
- ModelResource: model_id, model_type, provider
|
||||
- ToolResource: tool_name, tool_type, description
|
||||
- KnowledgeBaseResource: kb_id, kb_name, kb_type
|
||||
- SkillResource: skill_name, display_name, description
|
||||
- StorageResource: plugin_storage, workspace_storage
|
||||
"""
|
||||
|
||||
@@ -81,12 +83,16 @@ class AgentResourceBuilder:
|
||||
knowledge_bases = await self._build_knowledge_bases_from_binding(
|
||||
manifest_perms, resource_policy, descriptor, runner_config
|
||||
)
|
||||
skills = self._build_skills_from_binding(
|
||||
resource_policy, descriptor
|
||||
)
|
||||
storage = self._build_storage_from_binding(manifest_perms, binding)
|
||||
|
||||
return {
|
||||
'models': models,
|
||||
'tools': tools,
|
||||
'knowledge_bases': knowledge_bases,
|
||||
'skills': skills,
|
||||
'files': [], # Files are populated at runtime
|
||||
'storage': storage,
|
||||
'platform_capabilities': {}, # Reserved for EBA
|
||||
@@ -193,6 +199,36 @@ class AgentResourceBuilder:
|
||||
|
||||
return kb_resources
|
||||
|
||||
def _build_skills_from_binding(
|
||||
self,
|
||||
resource_policy: typing.Any,
|
||||
descriptor: AgentRunnerDescriptor,
|
||||
) -> list[SkillResource]:
|
||||
"""Build pipeline-visible skill resource facts."""
|
||||
if not config_schema.supports_skill_authoring(descriptor):
|
||||
return []
|
||||
|
||||
skill_mgr = getattr(self.ap, 'skill_mgr', None)
|
||||
if skill_mgr is None:
|
||||
return []
|
||||
|
||||
loaded_skills = getattr(skill_mgr, 'skills', {}) or {}
|
||||
allowed_names = resource_policy.allowed_skill_names
|
||||
if allowed_names is None:
|
||||
names = sorted(loaded_skills.keys())
|
||||
else:
|
||||
names = sorted(name for name in allowed_names if name in loaded_skills)
|
||||
|
||||
skills: list[SkillResource] = []
|
||||
for skill_name in names:
|
||||
skill_data = loaded_skills.get(skill_name) or {}
|
||||
skills.append({
|
||||
'skill_name': skill_name,
|
||||
'display_name': skill_data.get('display_name') or skill_data.get('name') or skill_name,
|
||||
'description': skill_data.get('description') or None,
|
||||
})
|
||||
return skills
|
||||
|
||||
def _build_storage_from_binding(
|
||||
self,
|
||||
manifest_perms: dict[str, list[str]],
|
||||
|
||||
@@ -140,6 +140,7 @@ class AgentRunSessionRegistry:
|
||||
'model': {m.get('model_id') for m in resources.get('models', [])},
|
||||
'tool': {t.get('tool_name') for t in resources.get('tools', [])},
|
||||
'knowledge_base': {kb.get('kb_id') for kb in resources.get('knowledge_bases', [])},
|
||||
'skill': {s.get('skill_name') for s in resources.get('skills', [])},
|
||||
'file': {f.get('file_id') for f in resources.get('files', [])},
|
||||
}
|
||||
|
||||
@@ -197,7 +198,7 @@ class AgentRunSessionRegistry:
|
||||
authorized_ids = authorization['authorized_ids']
|
||||
resources = authorization['resources']
|
||||
|
||||
if resource_type in ('model', 'tool', 'knowledge_base', 'file'):
|
||||
if resource_type in ('model', 'tool', 'knowledge_base', 'skill', 'file'):
|
||||
return resource_id in authorized_ids.get(resource_type, set())
|
||||
|
||||
if resource_type == 'storage':
|
||||
|
||||
Reference in New Issue
Block a user