# Copyright (c) 2026, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors # # SPDX-License-Identifier: Apache-2.0 import json from collections.abc import Awaitable, Callable from pathlib import Path from typing import Annotated import pytest from pydantic import ValidationError from ag2 import Agent, Context, Depends, Variable from ag2.context import ConversationContext from ag2.events import ToolCallEvent, ToolResultsEvent from ag2.exceptions import SkillError, SkillNotFoundError, ToolNotFoundError from ag2.stream import MemoryStream from ag2.testing import TestConfig, TrackingConfig from ag2.tools.final.function_tool import FunctionTool from ag2.tools.skills import MemoryRuntime, MemorySkill, SkillPlugin def _call(tool: str, **arguments: object) -> ToolCallEvent: return ToolCallEvent(name=tool, arguments=json.dumps(arguments)) def _tool_result(tracking: TrackingConfig, call: int = 1) -> str: """Content of the first tool result fed back to the LLM on the *call*-th turn.""" msg: ToolResultsEvent = tracking.mock.call_args_list[call][0][0] return msg.results[0].result.parts[0].content @pytest.mark.asyncio class TestCatalog: async def test_memory_skill_listed_in_catalog(self) -> None: skill = MemorySkill(name="greeter", description="Greets people", instructions="Say hi") agent = Agent("a", config=TestConfig("done"), plugins=[SkillPlugin(skill)]) reply = await agent.ask("hi") [catalog] = reply.context.prompt assert "greeter" in catalog assert "Greets people" in catalog async def test_load_skill_returns_instructions(self) -> None: skill = MemorySkill(name="greeter", description="d", instructions="Say hello warmly.") tracking = TrackingConfig(TestConfig(_call("load_skill", name="greeter"), "done")) agent = Agent("a", config=tracking, plugins=[SkillPlugin(skill)]) await agent.ask("hi") assert "Say hello warmly." in _tool_result(tracking) @pytest.mark.asyncio class TestDynamicInstructions: async def test_callable_body_renders_against_the_live_run(self) -> None: # The whole point of an async, context-aware read: the body is produced # per read and resolves dependencies like a resource or a script does. async def body(region: Annotated[str, Variable("region")]) -> str: return f"Deploy to {region}." skill = MemorySkill(name="deploy", description="d", instructions=body) tracking = TrackingConfig(TestConfig(_call("load_skill", name="deploy"), "done")) agent = Agent("a", config=tracking, plugins=[SkillPlugin(skill)]) await agent.ask("hi", variables={"region": "eu-west-1"}) assert "Deploy to eu-west-1." in _tool_result(tracking) async def test_decorator_registers_the_body(self) -> None: skill = MemorySkill(name="greeter", description="d") @skill.instructions def body() -> str: return "Decorated body." tracking = TrackingConfig(TestConfig(_call("load_skill", name="greeter"), "done")) agent = Agent("a", config=tracking, plugins=[SkillPlugin(skill)]) await agent.ask("hi") assert "Decorated body." in _tool_result(tracking) async def test_decorator_overrides_constructor_instructions(self) -> None: skill = MemorySkill(name="s", description="d", instructions="static body") @skill.instructions def body() -> str: return "dynamic body" tracking = TrackingConfig(TestConfig(_call("load_skill", name="s"), "done")) agent = Agent("a", config=tracking, plugins=[SkillPlugin(skill)]) await agent.ask("hi") content = _tool_result(tracking) assert "dynamic body" in content assert "static body" not in content async def test_body_is_rendered_on_every_read(self) -> None: calls = 0 def body() -> str: nonlocal calls calls += 1 return f"read #{calls}" skill = MemorySkill(name="s", description="d", instructions=body) tracking = TrackingConfig(TestConfig(_call("load_skill", name="s"), _call("load_skill", name="s"), "done")) agent = Agent("a", config=tracking, plugins=[SkillPlugin(skill)]) await agent.ask("hi") assert "read #1" in _tool_result(tracking, 1) assert "read #2" in _tool_result(tracking, 2) async def test_callable_body_keeps_resource_and_script_blocks(self) -> None: skill = MemorySkill(name="s", description="d", instructions=lambda: "dynamic body") @skill.resource def roster() -> str: return "Alice" @skill.script def go(text: str) -> str: """Do the thing.""" return text tracking = TrackingConfig(TestConfig(_call("load_skill", name="s"), "done")) agent = Agent("a", config=tracking, plugins=[SkillPlugin(skill)]) await agent.ask("hi") content = _tool_result(tracking) assert "dynamic body" in content assert "roster" in content assert '