Ctrl K
ComunidadGuías…

test/test_agent_resume.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0

"""Tests for ``Agent.resume`` — resuming a turn from a recorded trajectory.

``resume`` blesses, as public API, the ``_execute(trigger)`` path: it seeds the
stream history with a recorded trajectory and re-enters the agent loop with an
arbitrary ``BaseEvent`` trigger (typically a ``ToolResultsEvent``), so the model
reacts to a tool result mid-loop without the tool being re-executed.
"""

import pytest

from ag2 import Agent
from ag2.events import (
    ModelMessage,
    ModelRequest,
    ModelResponse,
    TextInput,
    ToolCallEvent,
    ToolCallsEvent,
    ToolResultEvent,
    ToolResultsEvent,
)
from ag2.stream import MemoryStream
from ag2.testing import TestConfig, TrackingConfig


def _seed_through_tool_call(question: str, tool_name: str, args: str, tc_id: str):
    """Build the trajectory [user question, assistant tool-call] to seed."""
    tc = ToolCallEvent(name=tool_name, arguments=args, id=tc_id)
    history = [
        ModelRequest([TextInput(question)]),
        ModelResponse(message=ModelMessage(""), tool_calls=ToolCallsEvent([tc])),
    ]
    return history, tc


class TestAgentResume:
    @pytest.mark.asyncio
    async def test_resume_from_tool_results_continues_turn(self) -> None:
        """A ToolResultsEvent trigger drives one LLM call → final answer."""
        history, tc = _seed_through_tool_call("Where is X?", "get_node_details", '{"id": "X"}', "call-1")
        agent = Agent("retriever", config=TestConfig("Final answer grounded on the result."))

        trigger = ToolResultsEvent([ToolResultEvent.from_call(tc, "X is at file.cc:42")])
        reply = await agent.resume(*history, trigger)

        assert reply.body == "Final answer grounded on the result."

        events = list(await reply.context.stream.history.get_events())
        # Seeded prefix is preserved, the trigger is appended, and the final
        # model response lands after it — contiguous, no re-execution.
        assert any(isinstance(e, ModelRequest) for e in events)
        assert any(isinstance(e, ToolResultsEvent) for e in events)
        assert isinstance(events[-1], ModelResponse)

    @pytest.mark.asyncio
    async def test_resume_feeds_seeded_history_and_trigger_to_model(self) -> None:
        """The LLM call sees the seeded history, with the trigger as the last message."""
        history, tc = _seed_through_tool_call("Q?", "search_docs", "{}", "call-1")
        tracking = TrackingConfig(TestConfig("done"))
        agent = Agent("retriever", config=tracking)

        trigger = ToolResultsEvent([ToolResultEvent.from_call(tc, "evidence")])
        await agent.resume(*history, trigger)

        # Exactly one LLM call; its last message is the tool-results trigger.
        tracking.mock.assert_called_once()
        last_message = tracking.mock.call_args.args[0]
        assert isinstance(last_message, ToolResultsEvent)

    @pytest.mark.asyncio
    async def test_resume_continues_into_more_tool_calls(self) -> None:
        """After reacting to the result, the model may issue further tool calls."""
        history, tc = _seed_through_tool_call("Q?", "search_docs", "{}", "call-1")

        def echo(value: str) -> str:
            return f"echoed:{value}"

        # React to the seeded result by calling another tool, then finish.
        next_call = ToolCallEvent(name="echo", arguments='{"value": "again"}', id="call-2")
        agent = Agent("retriever", tools=[echo], config=TestConfig(next_call, "All done."))

        trigger = ToolResultsEvent([ToolResultEvent.from_call(tc, "first result")])
        reply = await agent.resume(*history, trigger)

        assert reply.body == "All done."
        events = list(await reply.context.stream.history.get_events())
        # The live continuation actually executed the echo tool.
        assert any(
            isinstance(e, ToolResultsEvent) and any("echoed:again" in str(r.result) for r in e.results) for e in events
        )

    @pytest.mark.asyncio
    async def test_resume_replaces_existing_stream_history(self) -> None:
        """A supplied stream's pre-existing history is replaced by ``history``."""
        stream = MemoryStream()
        await stream.history.replace([ModelRequest([TextInput("stale prior turn")])])

        history, tc = _seed_through_tool_call("fresh question", "search_docs", "{}", "call-1")
        agent = Agent("retriever", config=TestConfig("answer"))

        trigger = ToolResultsEvent([ToolResultEvent.from_call(tc, "r")])
        reply = await agent.resume(*history, trigger, stream=stream)

        events = list(await reply.context.stream.history.get_events())
        texts = [p.content for e in events if isinstance(e, ModelRequest) for p in e.parts if isinstance(p, TextInput)]
        assert "fresh question" in texts
        assert "stale prior turn" not in texts