Skip to main content
Use EnvironmentMultiTurn for tasks that take more than one response: a conversation, a tool-using agent, anything where the environment reacts to what the model just did and the model acts again. Subclass it and implement the episode hooks. GRPO rolls out the whole episode, then scores the finished trajectory. OPD can use the same plain multi-turn loop and distills the student’s assistant turns from the managed teacher; native tool-calling environments remain GRPO-only. flash env setup --multi-turn scaffolds a runnable starter on this class — a “guess the secret number” episode with start_episode, step_episode, max_episode_turns, and score_episode wired end-to-end — that you edit into your own task.

The episode loop

During training the trainer drives your hooks in a loop, then scores the result:
  • start_episode(example, prompt_text) returns the initial messages (e.g. a system prompt plus the first user message).
  • max_episode_turns(example) caps the assistant actions Flash may sample before forcing the episode to end, per example.
  • step_episode(example, messages, assistant_response) is the core. messages already includes the assistant action being stepped as its last element. Return an EnvironmentStepResult:
    • done=False, messages=(...) keeps the episode open and injects the environment’s response (a tool result, the next user turn);
    • done=True, final_response_text=... ends it. final_response_text overrides the last assistant message as the text passed to scoring.
  • score_episode(example, episode) grades one completed transcript (episode.messages) and returns a RewardResult, exactly like score_response but over the whole trajectory.
  • score_episodes(example, episodes) is the batch wrapper Flash calls for a GRPO group. You normally do not implement it; the SDK default runs your singular score_episode for each item, using max_score_concurrency.

Full multi-turn contract

EnvironmentStepResult.messages should contain only new environment-side messages to append after the assistant action. Do not repeat the assistant message Flash just sampled. metadata is appended to the completed episode’s metadata under steps, available to score_episode and logs. Keep durable episode state in the transcript, not in hidden mutable attributes.

An action protocol the model can emit as text

The trained policy is an open model, not a tool-use API, so define a plain-text protocol it can produce and your step_episode can parse — for example, a tool-call block versus a final reply:
environment.py

SFT targets for multi-turn

SFT does not execute the multi-turn loop. It builds one supervised row as:
The default sft_completion(example) turns example.output into the target messages (see Datasets). When ChatML role parsing succeeds, Flash supervises authored assistant bodies and masks non-assistant target turns. An unparseable transcript keeps completion-only fallback and logs a warning that observation masking is unproven. See Multi-turn SFT masking. A scalar target remains one assistant turn. SFT does not run step_episode; provide a target transcript or override sft_completion for multi-turn SFT.

Keep step_episode stateless

The SDK doesn’t hand you a per-episode scratch object, and GRPO runs many rollouts of the same example concurrently — so don’t stash mutable world-state on the env keyed by example. If your task has state (an API or service that changes as tools run), make the transcript the source of truth and rebuild state by replaying messages inside step_episode / score_episode.
Preserve these three replay invariants:
  1. messages[-1] is the same current action as assistant_response. Replay messages[:-1], then apply the current action once.
  1. Role alone is insufficient when the prompt contains assistant demonstrations or the environment appends assistant-role replies. Mark or otherwise distinguish sampled actions.
  2. Historical replay must be pure. Perform external effects once for the current action, and make retries idempotent.

Score behaviours, not a script

Open-ended tasks rarely have one correct transcript, so don’t grade by imitation. Score the behaviours you care about — did it take the resolving action, follow required ordering (e.g. authenticate before a privileged call), stay within limits — and return that blend from score_episode. Keep the reward in a plain importable function so it stays unit-testable.

Per-turn credit (GRPO)

By default, multi-turn GRPO assigns one reward to the whole episode (credit_assignment = "per_episode"): every assistant turn shares the trajectory’s group-relative advantage. For a pure multi-turn task where each turn is independently gradable, set credit_assignment = "per_turn" in [train] to give each assistant turn its own group-relative advantage instead. Supply the per-turn signal from score_episode by returning a RewardResult whose metadata carries a per_turn_rewards list, one finite value per recorded sampled assistant turn, in order. episode.turns excludes the seeded start_episode prompt but includes environment replies. When environment replies use only user or tool roles, filter to assistant turns:
If your environment appends assistant-role replies, mark or otherwise distinguish them so they are not graded as sampled actions. Include every recorded sampled assistant turn even when your action parser rejects its content. per_turn_rewards must contain one finite ordered value per recorded sampled assistant turn. A missing or invalid vector makes the affected rollout group use episode-level credit and logs a warning. per_turn credit is rejected for native tool-calling multi-turn environments, and a single-turn run is identical either way.

Configure for multi-turn

For multi-turn runs, the episode budget comes from max_episode_turns. What matters in [train]:
  • max_completion_tokens is per assistant action (one tool call or one reply), not per episode — keep it modest.
  • max_context_tokens must hold the system prompt plus the whole running transcript (every turn, every tool result), so set it well above a single-turn task.
  • group_size is the number of full episodes rolled out per prompt for the GRPO advantage estimate. OPD usually uses group_size = 1.

Managed generation request deadlines

Pure multi-turn GRPO model generations have managed request deadlines between 10 and 60 minutes, based on context size. A timed-out assistant generation is aborted and retried once; timed-out output is never sent to the environment. This covers only model generation in the pure multi-turn GRPO path. It does not cover environment or external-service calls, OPD, or tool-loop paths.

Next steps

Training

Submit a run once your environment is ready.

Deploy & chat

Serve the trained adapter.