Skip to content

Python: [Bug]: Magentic reset does not reset participant sessions, so agents keep their history across replans #9129

Description

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.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

orchestrationUsage: [Issues, PRs], Target: multi-agent orchestration (high-level patterns)pythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions