Skip to content

Agent swaps and forks

These commands fork a finished run into a new one, replaying its remaining rounds under a controlled change: a different agent in one seat, an agent imported from another run, or the same team under different knobs.

Every fork cuts at the end of a round: --after-round N keeps rounds 1..N complete, verdict and postmortem included, and the new run plays round N+1 onward.

Command What it does
replace-agent Fork after round N with one agent restarted on a fresh history
cross-run-replace-agent Same, but the seat is filled by an agent carrying its full history from a different run
fork-at-round Fork after round N with no agent replaced, under merged knob overrides
scheduled_events Swap agents at round boundaries inside one live run

All four keep every non-replaced agent on its exact original model and full reconstructed history. To carry on inside an existing run instead of forking it into a new one, see Continuing a run.

Each command prepares the new run directory, prints new_run_id= and new_run_dir=, and spawns a detached simulation. Watch <new_run_dir>/<scenario>_stdout.log for progress. From a checkout, spell each command VIRTUAL_ENV= uv run --no-sync python -m glossogen ....

For the mechanics behind all of this (how a run is cloned, how a history is rebuilt, how the boundary round is ordered), see Architecture: replace-agent, cross-run, fork-at-round, in-run swaps.

Replace an agent at a round boundary

Keeps rounds 1..N as the source played them and puts a new agent in one seat for round N+1 onward, while everyone else continues from where they were. The question it answers: could a newcomer pick up the protocol the others built?

Replacing an agent at a round boundary

glossogen replace-agent veyru \
  --source-run-dir ./runs/veyru/<timestamp> \
  --after-round 4 \
  --replaced-agent-id field_observer \
  --model claude-sonnet-4-6 --provider anthropic \
  --runs-dir ./runs \
  [--rounds-after K] \
  [--visible-history-channel CHANNEL ...] \
  [--history-from-round R] \
  [--knobs <preset-name|path>]
  • --after-round must be ≥ 1.
  • --rounds-after defaults to the source rounds past the boundary (source_round_count - after_round). The new run's round_count becomes after_round + rounds_after. A round_count carried by --knobs (every shipped preset has one) sets the total when the flag is omitted, and must agree with it when both are given, so a whole preset alongside --rounds-after errors unless the numbers line up. Leave round_count out of override files. The same rule holds on cross-run-replace-agent and fork-at-round.
  • --model / --provider set the replacement agent only.
  • --knobs names a preset the scenario ships, or a path to a JSON file of your own, merged onto the source's recorded scenario_config before validation. It resolves exactly as --config does on run, and is optional: no overrides is a resumed run's normal state. A whole preset passed here replaces every field it declares, so an override file naming only what changes is usually what you want.

The replaced agent's own event log is preserved on disk. What it sees is reconstructed without text and thinking parts, and without tool calls on channels the scenario blocks (veyru's postmortem, for instance). Every other tool call its predecessor made stays, result included. In a scenario without channels, such as signal_game, that is the predecessor's whole record of moves and boards; --history-from-round windows it (see Channel history visibility).

Passing postmortem_disabled_at_start: true here drops the postmortem channel for the rest of the resumed run: no postmortem injections, no postmortem phase, sends rejected.

Import an agent from another run

Fills the seat with an agent from a different completed run, carrying its full history (text, thinking, tool calls) from that run. Same scenario and same agent_id only. The question it answers: how does an agent that learned one protocol behave when dropped into a team that learned another?

Importing an agent from another run

glossogen cross-run-replace-agent veyru \
  --source-a-run-dir ./runs/veyru/<sim_a_timestamp> \
  --source-b-run-dir ./runs/veyru/<sim_b_timestamp> \
  --replaced-agent-id field_observer \
  --after-round 14 \
  --runs-dir ./runs \
  [--source-b-round-end N] \
  [--model M --provider P] \
  [--rounds-after K] \
  [--visible-history-channel CHANNEL ...] \
  [--knobs <preset-name|path>]

Sim A is the timeline that continues. Sim B is where the imported agent comes from.

  • --source-b-round-end defaults to min(after_round, B_max_round), the largest slice of B that is temporally aligned with A's swap point without exceeding what B actually played.
  • --model / --provider default to whatever the imported agent ran under in Sim B. Pass both together to override, which is how you test a different model in the same seat.
  • The imported agent runs with Sim A's agent_model_settings, not the ones Sim B recorded for it, unless --knobs names model_settings in its model_overrides entry. When the two runs differ (a thinking level, say), the imported agent changes settings at the swap; pass Sim B's settings in that entry to keep them. The entry names the same model and provider the imported agent runs under, and a model_overrides in --knobs replaces Sim A's recorded map whole, so list every other seat whose own settings the run should keep.

A cross-run run cannot itself be forked again: its log holds the replaced-away agent's turns before the import boundary, so a fork would seed the seat with the wrong agent.

Set postmortem_disabled_at_start for cross-team runs. This flow does not set it. Without it the agents have a backchannel that re-aligns their protocols within a round or two, which washes out the effect you are trying to measure.

Label cross-run runs cross_team plus the range of rounds played after the swap, such as 15-25, as soon as the run directory exists. Analysis groups them by those labels to compare round_success_after_resume per imported model and boundary round against both sources over the same rounds.

Fork at a round (no replacement)

Forks after round N with no agent replaced. Every agent keeps its full history, and the fork differs from the source only through merged knob overrides. Useful for injecting scheduled_events after the fact, toggling the postmortem mid-experiment, or replaying the remaining rounds on a different configuration. --rounds-after sets how far the fork plays, past the source's own end included, and resolves against a round_count in --knobs as on replace-agent.

Forking a run at a round

glossogen fork-at-round veyru \
  --source-run-dir ./runs/veyru/<timestamp> \
  --after-round 15 \
  --runs-dir ./runs \
  [--knobs <preset-name|path>] \
  [--rounds-after K]

Every agent is pinned to whichever model it was running at the boundary.

Forking after the final round. A completed run can be forked past its own end: --after-round <final round> with an explicit --rounds-after plays rounds the source never did. No default exists there, because the default replays the source's remaining rounds and a final-round fork has none. The resumed clock records the advance into the new round as RoundAdvanced(trigger="fork_after_round"). The clone ends before the source told its agents the game was over, so every rebuilt agent remembers the final round and its verdict but not the ending, and keeps playing.

Inherited scheduled_events. The source's schedule carries over unless --knobs overrides it. Entries at at_round ≤ after_round never re-fire: the fork keeps those rounds as the source played them. An entry at after_round + 1 fires on resume, because the clone is captured before the source dispatched that boundary.

Knob-schema drift. If the scenario gained a required knob after the source ran, pass it via --knobs or validation rejects the merged config.

If a fork crashes. --resume on the fork's directory continues it from where it stopped, like any interrupted run. Only a fork that never played resumes at its boundary. A crashed cross-run fork is the exception: its imported history cannot be rebuilt past the boundary, so re-create it.

In-run swaps

scheduled_events swaps agents at round boundaries inside a single live run, on one continuous timeline. Three swaps produce four phases (A → B → C → D).

In-run swaps on one timeline

{
  "scheduled_events": [
    { "type": "set_postmortem", "at_round": 16, "enabled": false },
    { "type": "swap_agent", "at_round": 16, "agent_id": "field_observer",
      "model": "claude-sonnet-4-6", "provider": "anthropic",
      "channel_visibility": { "link": { "kind": "full" } } },
    { "type": "swap_agent", "at_round": 31, "agent_id": "stabilization_engineer",
      "model": "claude-sonnet-4-6", "provider": "anthropic",
      "channel_visibility": { "link": { "kind": "from_round", "round_floor": 16 } } }
  ]
}

Each swap writes an AgentSwappedMidRun event and a resume_context_<agent_id>_round_<R>.json file holding the seed history the swapped-in agent received.

Fork a swapped source before its first swap fires. All three fork commands refuse a boundary behind an in-run swap that already fired. That swap's history filters and swapped-in model live only in its config and its AgentSwappedMidRun event, so a fork would rebuild the seat with its predecessor's full turns under the pre-swap model. Post-hoc swap studies inject new scheduled_events into a clean source instead. Replacing that same agent is the one fork allowed past the swap.

Channel history visibility

All the swap flows choose, per channel, how much of the predecessor's history the new agent can see. In scheduled_events this is the channel_visibility map, a per-channel discriminated union:

  • {"kind": "full"} — the whole prior history. The default for any channel not listed
  • {"kind": "none"} — the channel is hidden entirely: no reads, no sends, nothing retained
  • {"kind": "from_round", "round_floor": R} — windowed to round R onward. The predecessor's send_message calls are kept from round R, and all its read_channel calls are dropped, since their results would carry older messages

With flags, on the replace flows

On the two flows that replace an agent the same choice is made with flags. --visible-history-channel (repeatable) names the channels that keep their history. Every other channel the agent belongs to has its join index bumped, so read_channel there returns only post-resume messages. Omit the flag and the source's replace_agent_default_channel_visibility knob decides, defaulting to visible for any channel it does not list.

On replace-agent, --history-from-round R then windows the replaced agent's history to round R onward: the visible channels, and every other tool call its predecessor made (read_notifications, the scenario's own tools). For the previous P rounds before the boundary, pass after_round - P + 1. Passing after_round + 1 keeps no prior history at all, which in a scenario without channels is the only way to give the seat nothing of its predecessor's. cross-run-replace-agent has no such flag, because the imported agent brings its own history rather than inheriting a predecessor's, and fork-at-round replaces no agent, so neither flag applies.

A channel the scenario has globally disabled is forced to none regardless of what the config asks for.

Scoring a swap

round_success_after_resume re-scores the post-swap window and states the difference against a baseline: the source run over the same rounds for the CLI flows (Sim A for a cross-run run), the previous phase for an in-run swap. The summary gives the baseline's score and the difference as Δ vs source: ±N pp. It emits one measurement per swap, named round_success_after_resume_round_<R>_<agent_id> for in-run swaps.

glossogen evaluate veyru \
  --run-dir ./runs/veyru/<new_timestamp> \
  --metrics round_success,round_success_after_resume,protocol_learned_after_swap \
  --model claude-haiku-4-5-20251001 --provider anthropic

protocol_learned_after_swap asks the complementary question: is there observable evidence the newcomer picked up the protocol? See Evaluation.

Verifying the reconstructed history

Every resumed run writes resume_context_<agent_id>.json, the exact pydantic-ai messages handed to that agent on its first turn. Read it before trusting a result.

For a cross-run run in particular, the tail of the imported agent's file should match Sim B's last few messages for that role verbatim. That is what confirms the history came from B and was not contaminated by A.

Each thinking entry in that file shows the part's id, provider_name and whether a signature_recorded. Those are what the provider needs to take the part back as its own reasoning (OpenAI's reasoning item id and encrypted content, Anthropic's block signature), and the run records them per part on every llm_response_received event as thinking_parts. A run recorded before that holds its reasoning as text only, and a history rebuilt from it carries no thinking at all: pydantic-ai would otherwise send the text as a tagged assistant message, a form no live request uses, so a fork of such a run differs from its source in what the model remembers of its own reasoning either way.