chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
"""TurnRunner._build_tools surfaces meta_invoke when meta-skills are loaded.
|
||||
|
||||
meta_invoke is registered with ``exposed_by_default=False`` so the tool
|
||||
catalogue stays clean in deployments that don't ship meta-skills. When
|
||||
at least one ``kind=meta`` skill IS loaded, ``_build_tools`` must add
|
||||
``"meta_invoke"`` to ``ctx.surfaced_tools`` so the registry's visibility
|
||||
check at :func:`ToolRegistry._is_visible` lets it through.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opensquilla.engine.pipeline import TurnContext
|
||||
from opensquilla.engine.runtime import TurnRunner
|
||||
from opensquilla.provider import ToolDefinition, ToolInputSchema
|
||||
from opensquilla.skills.loader import SkillLoader
|
||||
from opensquilla.tools.registry import get_default_registry
|
||||
from opensquilla.tools.types import ToolContext
|
||||
|
||||
|
||||
def _make_loader_with_meta(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
disable_model_invocation: bool = False,
|
||||
) -> SkillLoader:
|
||||
bundled = tmp_path / "skills" / "bundled"
|
||||
bundled.mkdir(parents=True)
|
||||
skill_dir = bundled / "meta-tiny"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
'---\n'
|
||||
'name: meta-tiny\n'
|
||||
'kind: meta\n'
|
||||
'description: tiny meta-skill\n'
|
||||
f'disable-model-invocation: {str(disable_model_invocation).lower()}\n'
|
||||
'triggers: [tiny-meta-trigger]\n'
|
||||
'composition:\n'
|
||||
' steps:\n'
|
||||
' - id: c\n'
|
||||
' kind: llm_classify\n'
|
||||
' output_choices: [A, B]\n'
|
||||
' with: {text: "x"}\n'
|
||||
'---\n'
|
||||
'# meta-tiny\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader = SkillLoader(bundled_dir=bundled, snapshot_path=tmp_path / "snap.json")
|
||||
loader.invalidate_cache()
|
||||
loader.load_all()
|
||||
return loader
|
||||
|
||||
|
||||
def _make_loader_without_meta(tmp_path: Path) -> SkillLoader:
|
||||
bundled = tmp_path / "skills" / "bundled"
|
||||
bundled.mkdir(parents=True)
|
||||
loader = SkillLoader(bundled_dir=bundled, snapshot_path=tmp_path / "snap.json")
|
||||
loader.invalidate_cache()
|
||||
loader.load_all()
|
||||
return loader
|
||||
|
||||
|
||||
def test_build_tools_surfaces_meta_invoke_when_meta_skill_present(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = get_default_registry()
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
runner = TurnRunner(provider_selector=None, config=_meta_cfg(auto_trigger=True))
|
||||
runner._tool_registry = registry
|
||||
runner._skill_loader = loader
|
||||
|
||||
ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path))
|
||||
tool_defs, _handler = runner._build_tools(ctx)
|
||||
names = {getattr(td, "name", "") for td in tool_defs}
|
||||
|
||||
assert "meta_invoke" in names, (
|
||||
f"meta_invoke should be surfaced when meta-skills present; got {sorted(names)[:20]}"
|
||||
)
|
||||
assert ctx.surfaced_tools is not None
|
||||
assert "meta_invoke" in ctx.surfaced_tools
|
||||
|
||||
|
||||
def test_build_tools_does_not_surface_meta_invoke_without_meta_skills(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""When no meta-skills are loaded, meta_invoke stays hidden — its
|
||||
``exposed_by_default=False`` keeps the catalogue tight for deployments
|
||||
that don't ship meta-skills."""
|
||||
registry = get_default_registry()
|
||||
loader = _make_loader_without_meta(tmp_path)
|
||||
runner = TurnRunner(provider_selector=None, config=None)
|
||||
runner._tool_registry = registry
|
||||
runner._skill_loader = loader
|
||||
|
||||
ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path))
|
||||
tool_defs, _handler = runner._build_tools(ctx)
|
||||
names = {getattr(td, "name", "") for td in tool_defs}
|
||||
|
||||
assert "meta_invoke" not in names, (
|
||||
f"meta_invoke should be hidden when no meta-skills present; got {sorted(names)[:20]}"
|
||||
)
|
||||
# surfaced_tools may stay None (no mutation) — either way meta_invoke
|
||||
# must not be inside it
|
||||
assert ctx.surfaced_tools is None or "meta_invoke" not in ctx.surfaced_tools
|
||||
|
||||
|
||||
def test_build_tools_does_not_surface_meta_invoke_for_disabled_meta_skill(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = get_default_registry()
|
||||
loader = _make_loader_with_meta(tmp_path, disable_model_invocation=True)
|
||||
runner = TurnRunner(provider_selector=None, config=None)
|
||||
runner._tool_registry = registry
|
||||
runner._skill_loader = loader
|
||||
|
||||
ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path))
|
||||
tool_defs, _handler = runner._build_tools(ctx)
|
||||
names = {getattr(td, "name", "") for td in tool_defs}
|
||||
|
||||
assert "meta_invoke" not in names
|
||||
assert ctx.surfaced_tools is None or "meta_invoke" not in ctx.surfaced_tools
|
||||
|
||||
|
||||
def test_build_tools_does_not_surface_meta_invoke_when_meta_skill_disabled_by_config(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from types import SimpleNamespace
|
||||
|
||||
registry = get_default_registry()
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
runner = TurnRunner(
|
||||
provider_selector=None,
|
||||
config=SimpleNamespace(meta_skill=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
runner._tool_registry = registry
|
||||
runner._skill_loader = loader
|
||||
|
||||
ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path))
|
||||
metadata: dict[str, object] = {}
|
||||
tool_defs, _handler = runner._build_tools(ctx, metadata=metadata)
|
||||
names = {getattr(td, "name", "") for td in tool_defs}
|
||||
|
||||
assert "meta_invoke" not in names
|
||||
assert ctx.surfaced_tools is None or "meta_invoke" not in ctx.surfaced_tools
|
||||
assert metadata["meta_skill_enabled"] is False
|
||||
|
||||
|
||||
def test_build_tools_preserves_existing_surfaced_tools(tmp_path: Path) -> None:
|
||||
"""If the caller pre-populates ctx.surfaced_tools (e.g. for a custom
|
||||
per-request tool surface), _build_tools must add to it, not overwrite."""
|
||||
registry = get_default_registry()
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
runner = TurnRunner(provider_selector=None, config=_meta_cfg(auto_trigger=True))
|
||||
runner._tool_registry = registry
|
||||
runner._skill_loader = loader
|
||||
|
||||
ctx = ToolContext(
|
||||
is_owner=True,
|
||||
workspace_dir=str(tmp_path),
|
||||
surfaced_tools={"some_other_tool"},
|
||||
)
|
||||
runner._build_tools(ctx)
|
||||
assert ctx.surfaced_tools is not None
|
||||
assert "meta_invoke" in ctx.surfaced_tools
|
||||
assert "some_other_tool" in ctx.surfaced_tools, (
|
||||
"must extend existing surfaced_tools, not replace it"
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_does_not_hard_auto_invoke_meta_match() -> None:
|
||||
"""Meta trigger matches must go through the outer LLM prompt/tool path.
|
||||
|
||||
The meta_resolution step already injects a system-prompt hint and exposes
|
||||
meta_invoke. Runtime must not bypass that prompt by directly calling
|
||||
_run_one_streaming when metadata["meta_match"] is present.
|
||||
"""
|
||||
source = inspect.getsource(TurnRunner._run_turn)
|
||||
|
||||
assert "meta_resolution.auto_invoke" not in source
|
||||
assert "auto_meta_invoke_" not in source
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_pipeline_runs_meta_resolution_before_skill_filter(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def noop_router(ctx: TurnContext) -> TurnContext:
|
||||
return ctx
|
||||
|
||||
noop_router.__name__ = "apply_squilla_router"
|
||||
monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", noop_router)
|
||||
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
runner = TurnRunner(provider_selector=None, config=_meta_cfg(auto_trigger=True))
|
||||
runner._skill_loader = loader
|
||||
|
||||
turn, _provider = await runner._run_pipeline(
|
||||
"please run tiny-meta-trigger for this request",
|
||||
"agent:main:test-meta-resolution",
|
||||
None,
|
||||
None,
|
||||
[
|
||||
ToolDefinition(
|
||||
name="meta_invoke",
|
||||
description="invoke meta skills",
|
||||
input_schema=ToolInputSchema(),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="web_search",
|
||||
description="search the web",
|
||||
input_schema=ToolInputSchema(),
|
||||
),
|
||||
],
|
||||
"base prompt",
|
||||
[],
|
||||
)
|
||||
|
||||
step_names = [record.step_name for record in turn.metadata["pipeline_steps"]]
|
||||
assert step_names.index("meta_resolution") < step_names.index("filter_skills")
|
||||
assert turn.metadata["meta_match"].plan.name == "meta-tiny"
|
||||
assert "meta_invoke(name=\"meta-tiny\")" in str(turn.system_prompt)
|
||||
assert "meta-tiny" in str(turn.system_prompt)
|
||||
|
||||
# Deterministic trigger matches force the first tool call to meta_invoke so
|
||||
# cheaper routed models do not bypass the meta DAG by calling ordinary tools.
|
||||
assert {tool.name for tool in turn.tool_defs} == {"meta_invoke", "web_search"}
|
||||
assert "meta_match_tool_surface_restricted" not in turn.metadata
|
||||
assert turn.metadata["meta_match_tool_choice"] == {
|
||||
"type": "function",
|
||||
"function": {"name": "meta_invoke"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_pipeline_restores_mainline_meta_and_coding_order(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def noop_router(ctx: TurnContext) -> TurnContext:
|
||||
return ctx
|
||||
|
||||
noop_router.__name__ = "apply_squilla_router"
|
||||
monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", noop_router)
|
||||
|
||||
runner = TurnRunner(provider_selector=None, config=_meta_cfg(auto_trigger=False))
|
||||
runner._skill_loader = _make_loader_without_meta(tmp_path)
|
||||
|
||||
turn, _provider = await runner._run_pipeline(
|
||||
"hello",
|
||||
"agent:main:test-mainline-pipeline-order",
|
||||
None,
|
||||
None,
|
||||
[
|
||||
ToolDefinition(
|
||||
name="web_search",
|
||||
description="search the web",
|
||||
input_schema=ToolInputSchema(),
|
||||
),
|
||||
],
|
||||
"base prompt",
|
||||
[],
|
||||
)
|
||||
|
||||
assert [record.step_name for record in turn.metadata["pipeline_steps"]] == [
|
||||
"resolve_model",
|
||||
"apply_vision_followup_gate",
|
||||
"apply_squilla_router",
|
||||
"observe_reasoning_hint",
|
||||
"meta_resolution",
|
||||
"enforce_coding_mode",
|
||||
"meta_command_launch",
|
||||
"filter_skills",
|
||||
"inject_subagent_grounding",
|
||||
"inject_platform_hint",
|
||||
"apply_prompt_cache",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_pipeline_pins_meta_skill_when_skill_filter_enabled(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When the retriever is on, ``<available_skills>`` may drop the
|
||||
meta-skill description out of the prompt. The matched meta-skill is
|
||||
pinned, and deterministic trigger matches force meta_invoke as the first
|
||||
tool call while leaving the broader tool surface intact for later turns.
|
||||
"""
|
||||
async def noop_router(ctx: TurnContext) -> TurnContext:
|
||||
return ctx
|
||||
|
||||
noop_router.__name__ = "apply_squilla_router"
|
||||
monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", noop_router)
|
||||
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
skills_cfg = SimpleNamespace(
|
||||
filter_enabled=True,
|
||||
filter_top_k=5,
|
||||
max_skills_prompt_chars=8000,
|
||||
injection_mode="system",
|
||||
)
|
||||
runner = TurnRunner(
|
||||
provider_selector=None,
|
||||
config=SimpleNamespace(
|
||||
skills=skills_cfg,
|
||||
meta_skill=SimpleNamespace(enabled=True, auto_trigger=True),
|
||||
),
|
||||
)
|
||||
runner._skill_loader = loader
|
||||
|
||||
turn, _provider = await runner._run_pipeline(
|
||||
"please run tiny-meta-trigger for this request",
|
||||
"agent:main:test-meta-restrict",
|
||||
None,
|
||||
None,
|
||||
[
|
||||
ToolDefinition(
|
||||
name="meta_invoke",
|
||||
description="invoke meta skills",
|
||||
input_schema=ToolInputSchema(),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="web_search",
|
||||
description="search the web",
|
||||
input_schema=ToolInputSchema(),
|
||||
),
|
||||
],
|
||||
"base prompt",
|
||||
[],
|
||||
)
|
||||
|
||||
assert turn.metadata["meta_match"].plan.name == "meta-tiny"
|
||||
assert {tool.name for tool in turn.tool_defs} == {"meta_invoke", "web_search"}
|
||||
assert "meta_match_tool_surface_restricted" not in turn.metadata
|
||||
assert turn.metadata["meta_match_tool_choice"] == {
|
||||
"type": "function",
|
||||
"function": {"name": "meta_invoke"},
|
||||
}
|
||||
assert "meta-tiny" in str(turn.system_prompt)
|
||||
|
||||
|
||||
def _meta_cfg(auto_trigger: bool) -> SimpleNamespace:
|
||||
return SimpleNamespace(meta_skill=SimpleNamespace(enabled=True, auto_trigger=auto_trigger))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipeline_hides_meta_skill_from_prompt_when_auto_trigger_off(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def noop_router(ctx: TurnContext) -> TurnContext:
|
||||
return ctx
|
||||
|
||||
noop_router.__name__ = "apply_squilla_router"
|
||||
monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", noop_router)
|
||||
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
runner = TurnRunner(provider_selector=None, config=_meta_cfg(auto_trigger=False))
|
||||
runner._skill_loader = loader
|
||||
|
||||
turn, _provider = await runner._run_pipeline(
|
||||
"what is the capital of France?", # non-triggering: isolates skills_filter
|
||||
"agent:main:test-meta-hidden",
|
||||
None,
|
||||
None,
|
||||
[
|
||||
ToolDefinition(name="web_search", description="search", input_schema=ToolInputSchema()),
|
||||
],
|
||||
"base prompt",
|
||||
[],
|
||||
)
|
||||
|
||||
assert "meta-tiny" not in str(turn.system_prompt)
|
||||
assert "meta-tiny" not in (turn.metadata.get("filtered_skill_ids") or [])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipeline_shows_meta_skill_when_auto_trigger_on(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def noop_router(ctx: TurnContext) -> TurnContext:
|
||||
return ctx
|
||||
|
||||
noop_router.__name__ = "apply_squilla_router"
|
||||
monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", noop_router)
|
||||
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
runner = TurnRunner(provider_selector=None, config=_meta_cfg(auto_trigger=True))
|
||||
runner._skill_loader = loader
|
||||
|
||||
turn, _provider = await runner._run_pipeline(
|
||||
"what is the capital of France?", # non-triggering: isolates skills_filter
|
||||
"agent:main:test-meta-shown",
|
||||
None,
|
||||
None,
|
||||
[
|
||||
ToolDefinition(
|
||||
name="meta_invoke",
|
||||
description="invoke",
|
||||
input_schema=ToolInputSchema(),
|
||||
),
|
||||
ToolDefinition(name="web_search", description="search", input_schema=ToolInputSchema()),
|
||||
],
|
||||
"base prompt",
|
||||
[],
|
||||
)
|
||||
|
||||
assert "meta-tiny" in str(turn.system_prompt)
|
||||
|
||||
|
||||
def test_build_tools_hides_meta_invoke_when_auto_trigger_off(tmp_path: Path) -> None:
|
||||
"""Default manual-only: meta-skill present but auto_trigger off => no meta_invoke."""
|
||||
registry = get_default_registry()
|
||||
loader = _make_loader_with_meta(tmp_path)
|
||||
runner = TurnRunner(provider_selector=None, config=_meta_cfg(auto_trigger=False))
|
||||
runner._tool_registry = registry
|
||||
runner._skill_loader = loader
|
||||
|
||||
ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path))
|
||||
tool_defs, _handler = runner._build_tools(ctx)
|
||||
names = {getattr(td, "name", "") for td in tool_defs}
|
||||
|
||||
assert "meta_invoke" not in names
|
||||
assert ctx.surfaced_tools is None or "meta_invoke" not in ctx.surfaced_tools
|
||||
Reference in New Issue
Block a user