# 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 '