mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Remove reset
This commit is contained in:
-22
@@ -598,25 +598,3 @@ class BaseGroupChatOrchestrator(Executor, ABC):
|
||||
metadata: Pattern-specific state dict
|
||||
"""
|
||||
pass
|
||||
|
||||
@override
|
||||
async def reset(self) -> None:
|
||||
"""Reset the orchestrator to its initial state for a new workflow run.
|
||||
|
||||
Clears the shared conversation history and round counter, then delegates
|
||||
to ``_reset_pattern_state()`` so subclasses can clean up any
|
||||
pattern-specific per-run state (caches, sessions, ledgers, etc.).
|
||||
"""
|
||||
logger.debug("%s %s: Resetting state", self.__class__.__name__, self.id)
|
||||
self._full_conversation.clear()
|
||||
self._round_index = 0
|
||||
self._reset_pattern_state()
|
||||
|
||||
def _reset_pattern_state(self) -> None:
|
||||
"""Reset pattern-specific state.
|
||||
|
||||
Override this method in subclasses to clear pattern-specific per-run state
|
||||
when ``reset()`` is invoked. Called after the base class clears the shared
|
||||
conversation and round counter.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -327,7 +327,6 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator):
|
||||
)
|
||||
self._agent = agent
|
||||
self._retry_attempts = retry_attempts
|
||||
self._session_supplied_by_caller = session is not None
|
||||
self._session = session or agent.create_session()
|
||||
# Cache for messages since last agent invocation
|
||||
# This is different from the full conversation history maintained by the base orchestrator
|
||||
@@ -338,25 +337,6 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator):
|
||||
self._cache.extend(messages)
|
||||
return super()._append_messages(messages)
|
||||
|
||||
@override
|
||||
def _reset_pattern_state(self) -> None:
|
||||
"""Reset pattern-specific state for a new workflow run.
|
||||
|
||||
Clears the per-run message cache and rotates the orchestrator agent's
|
||||
session unless the caller supplied a session explicitly (in which case
|
||||
the caller is responsible for the session's lifecycle).
|
||||
"""
|
||||
self._cache.clear()
|
||||
if self._session_supplied_by_caller:
|
||||
logger.warning(
|
||||
"%s %s: Session was supplied by the caller and will not be reset. "
|
||||
"If you want a fresh session for the next run, reset or replace it before invoking the workflow.",
|
||||
self.__class__.__name__,
|
||||
self.id,
|
||||
)
|
||||
else:
|
||||
self._session = self._agent.create_session()
|
||||
|
||||
@override
|
||||
async def _handle_messages(
|
||||
self,
|
||||
|
||||
@@ -509,14 +509,6 @@ class MagenticManagerBase(ABC):
|
||||
"""Restore runtime state from checkpoint data."""
|
||||
return
|
||||
|
||||
def on_reset(self) -> None:
|
||||
"""Reset per-run runtime state for a new workflow run.
|
||||
|
||||
Subclasses should clear any state accumulated during a run (e.g. cached
|
||||
ledgers, agent sessions) so the manager is ready to start fresh.
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
class StandardMagenticManager(MagenticManagerBase):
|
||||
"""Standard Magentic manager that performs real LLM calls via a Agent.
|
||||
@@ -769,12 +761,6 @@ class StandardMagenticManager(MagenticManagerBase):
|
||||
except Exception: # pragma: no cover - defensive
|
||||
logger.warning("Failed to restore manager agent session from checkpoint state")
|
||||
|
||||
@override
|
||||
def on_reset(self) -> None:
|
||||
"""Clear cached ledger and start a fresh agent session for a new run."""
|
||||
self.task_ledger = None
|
||||
self._session = self._agent.create_session()
|
||||
|
||||
|
||||
# endregion Magentic Manager
|
||||
|
||||
@@ -1277,15 +1263,6 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator):
|
||||
# a target will broadcast to all.
|
||||
await ctx.send_message(MagenticResetSignal())
|
||||
|
||||
@override
|
||||
def _reset_pattern_state(self) -> None:
|
||||
"""Reset Magentic-specific per-run state for a new workflow run."""
|
||||
self._magentic_context = None
|
||||
self._task_ledger = None
|
||||
self._progress_ledger = None
|
||||
self._terminated = False
|
||||
self._manager.on_reset()
|
||||
|
||||
@override
|
||||
async def on_checkpoint_save(self) -> dict[str, Any]:
|
||||
"""Capture current orchestrator state for checkpointing."""
|
||||
|
||||
@@ -1097,139 +1097,3 @@ def test_group_chat_orchestrator_factory_invalid_return_type():
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Reset
|
||||
|
||||
|
||||
async def test_base_orchestrator_reset_clears_conversation_and_round_index() -> None:
|
||||
"""reset() clears the conversation history and the round counter."""
|
||||
from agent_framework.orchestrations import GroupChatOrchestrator
|
||||
|
||||
from agent_framework_orchestrations._base_group_chat_orchestrator import ParticipantRegistry
|
||||
|
||||
selector = make_sequence_selector()
|
||||
orchestrator = GroupChatOrchestrator(
|
||||
id="orch",
|
||||
participant_registry=ParticipantRegistry([]),
|
||||
selection_func=selector,
|
||||
max_rounds=2,
|
||||
)
|
||||
orchestrator._full_conversation = [Message(role="user", contents=["hi"], author_name="user")]
|
||||
orchestrator._round_index = 4
|
||||
|
||||
await orchestrator.reset()
|
||||
|
||||
assert orchestrator._full_conversation == []
|
||||
assert orchestrator._round_index == 0
|
||||
|
||||
|
||||
async def test_base_orchestrator_reset_invokes_pattern_state_hook() -> None:
|
||||
"""reset() calls _reset_pattern_state() so subclasses can clean up their own state."""
|
||||
from agent_framework.orchestrations import GroupChatOrchestrator
|
||||
|
||||
from agent_framework_orchestrations._base_group_chat_orchestrator import ParticipantRegistry
|
||||
|
||||
selector = make_sequence_selector()
|
||||
|
||||
class TrackingOrchestrator(GroupChatOrchestrator):
|
||||
reset_calls: int = 0
|
||||
|
||||
def _reset_pattern_state(self) -> None:
|
||||
type(self).reset_calls += 1
|
||||
|
||||
orchestrator = TrackingOrchestrator(
|
||||
id="orch",
|
||||
participant_registry=ParticipantRegistry([]),
|
||||
selection_func=selector,
|
||||
max_rounds=2,
|
||||
)
|
||||
|
||||
await orchestrator.reset()
|
||||
await orchestrator.reset()
|
||||
|
||||
assert TrackingOrchestrator.reset_calls == 2
|
||||
|
||||
|
||||
async def test_agent_based_orchestrator_reset_clears_cache_and_rotates_session() -> None:
|
||||
"""When the session was not supplied by the caller, reset() rotates the session and clears the cache."""
|
||||
from agent_framework.orchestrations import AgentBasedGroupChatOrchestrator
|
||||
|
||||
from agent_framework_orchestrations._base_group_chat_orchestrator import ParticipantRegistry
|
||||
|
||||
agent = cast(Agent, StubManagerAgent())
|
||||
orchestrator = AgentBasedGroupChatOrchestrator(
|
||||
agent=agent,
|
||||
participant_registry=ParticipantRegistry([]),
|
||||
max_rounds=2,
|
||||
)
|
||||
original_session = orchestrator._session
|
||||
orchestrator._cache = [Message(role="assistant", contents=["x"], author_name="agent")]
|
||||
orchestrator._full_conversation = [Message(role="user", contents=["x"], author_name="user")]
|
||||
orchestrator._round_index = 3
|
||||
|
||||
await orchestrator.reset()
|
||||
|
||||
assert orchestrator._cache == []
|
||||
assert orchestrator._full_conversation == []
|
||||
assert orchestrator._round_index == 0
|
||||
assert orchestrator._session is not original_session
|
||||
|
||||
|
||||
async def test_agent_based_orchestrator_reset_warns_when_session_supplied(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""When the caller supplied a session, reset() preserves it and logs a warning."""
|
||||
import logging
|
||||
|
||||
from agent_framework.orchestrations import AgentBasedGroupChatOrchestrator
|
||||
|
||||
from agent_framework_orchestrations._base_group_chat_orchestrator import ParticipantRegistry
|
||||
|
||||
agent = cast(Agent, StubManagerAgent())
|
||||
supplied_session = agent.create_session()
|
||||
orchestrator = AgentBasedGroupChatOrchestrator(
|
||||
agent=agent,
|
||||
participant_registry=ParticipantRegistry([]),
|
||||
session=supplied_session,
|
||||
max_rounds=2,
|
||||
)
|
||||
orchestrator._cache = [Message(role="assistant", contents=["x"], author_name="agent")]
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="agent_framework_orchestrations._group_chat"):
|
||||
await orchestrator.reset()
|
||||
|
||||
assert orchestrator._cache == []
|
||||
# The caller-owned session must be preserved.
|
||||
assert orchestrator._session is supplied_session
|
||||
warnings = [
|
||||
r for r in caplog.records if r.levelno == logging.WARNING and "Session was supplied by the caller" in r.message
|
||||
]
|
||||
assert warnings, f"expected a warning about caller-supplied session, got: {[r.message for r in caplog.records]}"
|
||||
|
||||
|
||||
async def test_workflow_reset_resets_group_chat_orchestrator() -> None:
|
||||
"""End-to-end: workflow.reset_for_new_run() resets the orchestrator's conversation state."""
|
||||
selector = make_sequence_selector()
|
||||
alpha = StubAgent("alpha", "ack from alpha")
|
||||
beta = StubAgent("beta", "ack from beta")
|
||||
|
||||
workflow = GroupChatBuilder(
|
||||
participants=[alpha, beta],
|
||||
max_rounds=2,
|
||||
selection_func=selector,
|
||||
orchestrator_name="manager",
|
||||
).build()
|
||||
|
||||
async for _ in workflow.run("first task", stream=True):
|
||||
pass
|
||||
|
||||
orchestrator = cast(BaseGroupChatOrchestrator, workflow.executors[GroupChatBuilder.DEFAULT_ORCHESTRATOR_ID])
|
||||
assert orchestrator._full_conversation, "orchestrator should have accumulated conversation after first run"
|
||||
assert orchestrator._round_index > 0
|
||||
|
||||
await workflow.reset_for_new_run()
|
||||
|
||||
assert orchestrator._full_conversation == []
|
||||
assert orchestrator._round_index == 0
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -1243,135 +1243,3 @@ def test_standard_manager_checkpoint_restore_empty_state():
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Manager Reset Tests
|
||||
|
||||
|
||||
def test_magentic_manager_base_on_reset_is_noop_by_default():
|
||||
"""MagenticManagerBase.on_reset() is a no-op so subclasses can opt in."""
|
||||
mgr = FakeManager()
|
||||
# Seed some state on the fake to confirm the base hook does not touch it.
|
||||
mgr.task_ledger = _SimpleLedger(
|
||||
facts=Message("assistant", ["facts"]),
|
||||
plan=Message("assistant", ["plan"]),
|
||||
)
|
||||
|
||||
mgr.on_reset() # base implementation is a no-op
|
||||
|
||||
assert mgr.task_ledger is not None
|
||||
|
||||
|
||||
def test_standard_manager_on_reset_clears_ledger_and_rotates_session():
|
||||
"""StandardMagenticManager.on_reset() clears the cached ledger and creates a fresh session."""
|
||||
from agent_framework_orchestrations._magentic import _MagenticTaskLedger # type: ignore[reportPrivateUsage]
|
||||
|
||||
agent = StubManagerAgent()
|
||||
mgr = StandardMagenticManager(agent=agent)
|
||||
mgr.task_ledger = _MagenticTaskLedger(
|
||||
facts=Message("assistant", ["facts"]),
|
||||
plan=Message("assistant", ["plan"]),
|
||||
)
|
||||
original_session = mgr._session
|
||||
|
||||
mgr.on_reset()
|
||||
|
||||
assert mgr.task_ledger is None
|
||||
assert mgr._session is not original_session
|
||||
assert mgr._session.session_id != original_session.session_id
|
||||
|
||||
|
||||
async def test_magentic_orchestrator_reset_invokes_manager_on_reset() -> None:
|
||||
"""_reset_pattern_state() clears orchestrator state and delegates to manager.on_reset()."""
|
||||
from agent_framework_orchestrations._base_group_chat_orchestrator import (
|
||||
ParticipantRegistry, # type: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
class TrackingManager(FakeManager):
|
||||
reset_calls: int = 0
|
||||
|
||||
@override
|
||||
def on_reset(self) -> None:
|
||||
type(self).reset_calls += 1
|
||||
|
||||
manager = TrackingManager()
|
||||
orchestrator = MagenticOrchestrator(
|
||||
manager=manager,
|
||||
participant_registry=ParticipantRegistry([]),
|
||||
)
|
||||
# Seed magentic-specific state to confirm it is cleared.
|
||||
orchestrator._magentic_context = MagenticContext( # type: ignore[reportPrivateUsage]
|
||||
task="task",
|
||||
participant_descriptions={"agentA": "desc"},
|
||||
)
|
||||
orchestrator._task_ledger = Message("assistant", ["ledger"]) # type: ignore[reportPrivateUsage]
|
||||
orchestrator._progress_ledger = MagenticProgressLedger( # type: ignore[reportPrivateUsage]
|
||||
is_request_satisfied=MagenticProgressLedgerItem(reason="r", answer=False),
|
||||
is_in_loop=MagenticProgressLedgerItem(reason="r", answer=False),
|
||||
is_progress_being_made=MagenticProgressLedgerItem(reason="r", answer=True),
|
||||
next_speaker=MagenticProgressLedgerItem(reason="r", answer="agentA"),
|
||||
instruction_or_question=MagenticProgressLedgerItem(reason="r", answer="do"),
|
||||
)
|
||||
orchestrator._terminated = True # type: ignore[reportPrivateUsage]
|
||||
|
||||
await orchestrator.reset()
|
||||
|
||||
assert orchestrator._magentic_context is None # type: ignore[reportPrivateUsage]
|
||||
assert orchestrator._task_ledger is None # type: ignore[reportPrivateUsage]
|
||||
assert orchestrator._progress_ledger is None # type: ignore[reportPrivateUsage]
|
||||
assert orchestrator._terminated is False # type: ignore[reportPrivateUsage]
|
||||
assert TrackingManager.reset_calls == 1
|
||||
|
||||
|
||||
async def test_magentic_orchestrator_reset_propagates_manager_on_reset_failure() -> None:
|
||||
"""Failures in manager.on_reset() must propagate so callers can react."""
|
||||
from agent_framework_orchestrations._base_group_chat_orchestrator import (
|
||||
ParticipantRegistry, # type: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
class FailingManager(FakeManager):
|
||||
@override
|
||||
def on_reset(self) -> None:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
manager = FailingManager()
|
||||
orchestrator = MagenticOrchestrator(
|
||||
manager=manager,
|
||||
participant_registry=ParticipantRegistry([]),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
await orchestrator.reset()
|
||||
|
||||
|
||||
async def test_workflow_reset_resets_magentic_orchestrator_and_manager() -> None:
|
||||
"""End-to-end: workflow.reset_for_new_run() resets Magentic orchestrator and manager state."""
|
||||
manager = FakeManager()
|
||||
manager.task_ledger = _SimpleLedger(
|
||||
facts=Message("assistant", ["seeded facts"]),
|
||||
plan=Message("assistant", ["seeded plan"]),
|
||||
)
|
||||
workflow = MagenticBuilder(
|
||||
participants=[StubAgent(manager.next_speaker_name, "first draft")],
|
||||
manager=manager,
|
||||
).build()
|
||||
|
||||
async for _ in workflow.run("first task", stream=True):
|
||||
pass
|
||||
|
||||
orchestrator = next(e for e in workflow.executors.values() if isinstance(e, MagenticOrchestrator))
|
||||
assert orchestrator._terminated is True # type: ignore[reportPrivateUsage]
|
||||
assert orchestrator._task_ledger is not None # type: ignore[reportPrivateUsage]
|
||||
assert manager.task_ledger is not None
|
||||
|
||||
await workflow.reset_for_new_run()
|
||||
|
||||
assert orchestrator._magentic_context is None # type: ignore[reportPrivateUsage]
|
||||
assert orchestrator._task_ledger is None # type: ignore[reportPrivateUsage]
|
||||
assert orchestrator._progress_ledger is None # type: ignore[reportPrivateUsage]
|
||||
assert orchestrator._terminated is False # type: ignore[reportPrivateUsage]
|
||||
# FakeManager.on_reset is the base no-op, but the orchestrator still tolerates that case.
|
||||
# For the standard manager we exercise full clearing in the dedicated unit test above.
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
Reference in New Issue
Block a user