Add reset to workflow

This commit is contained in:
Tao Chen
2026-06-08 13:42:42 -07:00
Unverified
parent c5e6a7797f
commit 65522bdbee
19 changed files with 4986 additions and 4136 deletions
@@ -598,3 +598,25 @@ 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,6 +327,7 @@ 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
@@ -337,6 +338,25 @@ 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,
@@ -1263,6 +1263,14 @@ 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
@override
async def on_checkpoint_save(self) -> dict[str, Any]:
"""Capture current orchestrator state for checkpointing."""
@@ -1097,3 +1097,139 @@ 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