Description
When a Magentic workflow stalls and the orchestrator resets and replans, participant agents keep their full earlier conversation. The reset is supposed to start each participant fresh.
MagenticAgentExecutor.handle_magentic_reset in python/packages/orchestrations/agent_framework_orchestrations/_magentic.py ends with:
# Reset sessions
self._agent_thread = self._agent.create_session()
Nothing reads _agent_thread anymore. AgentExecutor runs the agent with self._session, which #3850 introduced in place of _agent_thread, but this assignment wasn't updated. So the new session is created and thrown away, and the agent keeps using its old session, including whatever its history provider stored before the reset.
The intended behavior is documented in the same class. Its docstring says "the agent threads and other states are reset when requested by the orchestrator", and the handler says it "resets the internal state of the agent executor, including any threads or caches, to prepare for a fresh start after replanning".
Expected: after a reset, a participant's next model call contains only what the orchestrator sends after the replan.
Code Sample
import asyncio
from typing import Any
from agent_framework import Agent, BaseChatClient, ChatResponse, Message
from agent_framework.orchestrations import (
MagenticBuilder,
MagenticContext,
MagenticManagerBase,
MagenticProgressLedger,
MagenticProgressLedgerItem,
)
class RecordingClient(BaseChatClient):
"""Offline chat client that records what the agent sends to the model."""
def __init__(self) -> None:
super().__init__()
self.calls: list[list[str]] = []
def _inner_get_response(self, *, messages: Any, stream: bool, options: Any, **kwargs: Any) -> Any:
self.calls.append([m.text for m in messages])
reply = f"reply-{len(self.calls)}"
async def _get() -> ChatResponse:
return ChatResponse(messages=Message("assistant", [reply]))
return _get()
class StallOnceManager(MagenticManagerBase):
"""Asks agentA, stalls once (forcing a reset and replan), asks agentA again, then finishes."""
def __init__(self) -> None:
super().__init__(max_round_count=10, max_stall_count=0)
self.rounds = 0
async def plan(self, magentic_context: MagenticContext) -> Message:
return Message("assistant", ["plan"])
async def replan(self, magentic_context: MagenticContext) -> Message:
return Message("assistant", ["new plan"])
async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger:
self.rounds += 1
progress = self.rounds != 2 # round 2 stalls, which resets and replans
item = MagenticProgressLedgerItem
return MagenticProgressLedger(
is_request_satisfied=item(reason="r", answer=self.rounds >= 4),
is_in_loop=item(reason="r", answer=not progress),
is_progress_being_made=item(reason="r", answer=progress),
next_speaker=item(reason="r", answer="agentA"),
instruction_or_question=item(reason="r", answer="before reset" if self.rounds == 1 else "after reset"),
)
async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message:
return Message("assistant", ["final"])
async def main() -> None:
client = RecordingClient()
agent = Agent(client=client, name="agentA", description="worker")
workflow = MagenticBuilder(participants=[agent], manager=StallOnceManager()).build()
await workflow.run("do the task")
for number, call in enumerate(client.calls, 1):
print(f"agentA model call {number}: {call}")
asyncio.run(main())
Output on main:
agentA model call 1: ['before reset']
agentA model call 2: ['before reset', 'reply-1', 'after reset']
Expected:
agentA model call 1: ['before reset']
agentA model call 2: ['after reset']
Error Messages / Stack Traces
None. The old history is silently sent again after every reset, so token use also grows with each replan.
Package Versions
agent-framework-orchestrations: 1.3.0, agent-framework-core: 1.20.0 (source checkout of main at 279d97f)
Python Version
Python 3.13
Additional Context
- Reproduced on Windows and Linux.
- No existing test covers what a participant sees after a reset. The reset tests cover the orchestrator's own context and the reset limit, not the participants.
- Fix: assign self._session instead of self._agent_thread. The handler also clears the other pending-request state but not _pending_request_order, so I'd clear that too. I'll open a PR with this and a regression test.
Description
When a Magentic workflow stalls and the orchestrator resets and replans, participant agents keep their full earlier conversation. The reset is supposed to start each participant fresh.
MagenticAgentExecutor.handle_magentic_reset in python/packages/orchestrations/agent_framework_orchestrations/_magentic.py ends with:
Nothing reads _agent_thread anymore. AgentExecutor runs the agent with self._session, which #3850 introduced in place of _agent_thread, but this assignment wasn't updated. So the new session is created and thrown away, and the agent keeps using its old session, including whatever its history provider stored before the reset.
The intended behavior is documented in the same class. Its docstring says "the agent threads and other states are reset when requested by the orchestrator", and the handler says it "resets the internal state of the agent executor, including any threads or caches, to prepare for a fresh start after replanning".
Expected: after a reset, a participant's next model call contains only what the orchestrator sends after the replan.
Code Sample
Output on main:
Expected:
Error Messages / Stack Traces
None. The old history is silently sent again after every reset, so token use also grows with each replan.
Package Versions
agent-framework-orchestrations: 1.3.0, agent-framework-core: 1.20.0 (source checkout of main at 279d97f)
Python Version
Python 3.13
Additional Context