Skip to content

feat(checkpoint): checkpoint Workplace Assistant sessions - #3894

Merged
ananthsub merged 1 commit into
mainfrom
ananthsub/partial-ckpt-workplace-assistant
Oct 20, 2026
Merged

ananthsub merged 1 commit into
mainfrom
ananthsub/partial-ckpt-workplace-assistant

Conversation

@ananthsub

@ananthsub ananthsub commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
  • Branch: ananthsub/partial-ckpt-workplace-assistant
  • Base branch: main
  • Suggested labels: feature, area:env-infra

What changed and why

Workplace Assistant used the default restart_only mode, so every rollout with a live Workplace session was a restart and started over from scratch after a crash. This PR lets those sessions be checkpointed and continued.

  • The server declares checkpoint_mode = "exported" and implements the three session hooks over its per-session state. That state is the tool environment created at /seed_session: a set of in-memory pandas tables for email, calendar, analytics, project management, the customer relationship manager, and the company directory.
  • export_session_states encodes every table column by column. It keeps column dtypes, such as the boolean user_engaged column, and index labels, which matter because the analytics tool appends rows by label. It also keeps the difference between empty CSV cells, which load as NaN, and values a tool stored as None. JSON writes both as null, so NaN positions are listed separately. Encoding runs in a worker thread. Admission is closed during a commit, so no tool call changes the tables while they are encoded.
  • restore_session_states rebuilds every session's tool environment first and installs them only after all of them are valid. A state with missing or extra containers or tables, or with mismatched column lengths, raises ValueError and installs nothing.
  • Export leaves out a session the server no longer holds. /verify discards the session's tool environment in a finally block. If verify raises, the session is gone but the participant still tracks it, so the commit must not fail on it.
  • retire_session_state drops the session's tool environment.
  • checkpoint_verify stays at the default wait. Verify computes the reward only from the response and the ground truth, but it deletes the session's state, so it is not replay-safe.
  • The toolkit list is now one module constant, _TOOLKITS, instead of being repeated in seed_session and mcp_tools.

How it works

Where this PR sits in the overall flow

The highlighted part is what this PR adds.

flowchart LR
  C["Controller<br/>NeMo RL, or rollout collection"]
  CO["Coordination<br/>prepare, commit, restore, resume,<br/>retire, forget"]
  K["Control plane on every server<br/>phases, lease, storage,<br/>retire: refuse, stop, free;<br/>forget"]
  subgraph G["One participant per Gym server"]
    E["Environment server<br/>episode steps"]
    M["Policy model<br/>held responses, generation cuts"]
    A["Agent<br/>sessions parked at boundaries"]
    R["Resources server<br/>session state"]
  end
  W["Inference worker<br/>stages cut prefixes"]
  D[("Checkpoint directory<br/>records, then manifest")]
  L[("Capture ledger<br/>retire and delete from #3938, #3939")]
  C --> CO --> K
  K --> E & M & A & R
  M --> W
  M --> L
  G --> D
  classDef this fill:#fde68a,stroke:#b45309,stroke-width:2px,color:#1f2937
  class R this
Loading

One checkpoint, a crash, and the restore, end to end:

sequenceDiagram
  participant C as Controller
  participant G as Gym participants
  participant D as Checkpoint directory
  C->>G: prepare, in order environment, model, agent, resources
  Note over G: admission closes, in-flight work parks at a boundary,<br/>undelivered model responses are held
  G-->>C: prepared, or blockers at the deadline
  C->>G: commit with the episodes the controller continues
  G->>D: each participant writes its records, then its manifest
  Note over G: restored state the commit's scope leaves out is deleted
  C->>C: publish the checkpoint with the controller's own state
  C->>G: resume, in order resources, agent, model, environment
  Note over C,G: crash - every Gym process dies
  C->>G: restore the checkpoint in fresh processes, all or nothing
  D-->>G: records installed under attempt N+1, attempt N's capture ledger retired
  C->>G: resume
  C->>G: /run as attempt N+1 continues each episode from its boundary
  Note over C,G: retiring an episode, only while no checkpoint is open
  C->>G: retire - environment, then agent, then model and resources
  Note over G: each server marks the attempt, stops its work, waits, frees its state, then replies
  C->>G: forget, once the rollout is done for good - same order
  Note over G: the mark refusing the retired attempt goes away
Loading

This PR

Each session's tool environment is a set of tables. A checkpoint exports them whole, and a restore rebuilds them before any of them is installed.

sequenceDiagram
  participant A as Agent
  participant W as Workplace Assistant
  participant D as Checkpoint directory
  A->>W: /seed_session creates the session's tables
  A->>W: tool calls read and change the tables
  Note over W: commit - export_session_states writes each table column by column,<br/>with column types, index labels, and where cells are empty
  W->>D: one record per session
  Note over A,D: crash, then restore into a fresh server
  D-->>W: restore_session_states rebuilds every session, then installs them all
  A->>W: tool calls continue on the same cookie session
  A->>W: /verify scores and drops the session
Loading

Where this sits in the stack

This PR uses the resources server checkpoint participant from #3886, which has merged, and needs nothing else from the open part of the partial-rollout checkpointing stack. It is one of five PRs that port the environments the old stack checkpointed onto the new hooks. Workplace Assistant and the Gymnasium fix target main, and Blackjack builds on the Gymnasium fix. Indirect prompt injection and proof refinement are still based on the end-to-end suite (#3889):

Relationship to the old stack

  • Ported from feat(checkpoint): adapt initial stateful environments #2946, which carried the Workplace Assistant adapter on the old hooks. It covers gap 7 of the re-cut, where this adapter was not carried.
  • Dropped from feat(checkpoint): adapt initial stateful environments #2946:
    • the execution_to_session map and the identity lookups in the tool route and verify. The v2 hooks are keyed by the cookie session ID, so neither is needed.
    • route classification of tool calls as mutations. In v2, admission refuses every data route while a checkpoint is open.
    • synthetic checkpoint:<rollout>:a<attempt> session IDs on restore. The restored session keeps its original cookie session ID.
  • The old encoding used pandas to_json(orient="split") and repaired types after reading the JSON back. That turned NaN into None and lost the difference between them. The new encoding keeps it.
  • Not ported from feat(checkpoint): add durable turn-level rollout recovery #3349: the audit-trail test fixture (checkpoint_test_app.py) and its test. Both depend on old-stack rollout identities and on the test agent from that PR.

Issue

No tracking issue exists for this re-cut. The re-cut series and the old stack's PRs carry the discussion.

Validation

Run on this branch, on the current main:

  • RAY_TMPDIR=/tmp .venv/bin/python -m pytest -q resources_servers/workplace_assistant/tests: passed, including the new tests/test_checkpoint.py. Its tests drive the real control routes over ASGI:
    • A session is seeded, mutated by three tool calls, prepared, and committed. It is restored into a fresh server instance with the same cookies and continues with four more tool calls and /verify. Tool outputs, every table, and the reward all match an uncheckpointed run of the same steps.
    • When one of two sessions has an invalid state, restore raises and installs neither session.
    • A retired session's tool environment is gone and the next commit exports nothing. A retire stops and frees the session instead of fencing it, so a later tool call is refused and does not recreate the environment.
    • A session whose /verify raised is left out of the export. The commit succeeds with only the live session, which then continues after restore.
  • A mutation check confirmed the equivalence test is not vacuous. Without restoring NaN cells, the test fails.
  • RAY_TMPDIR=/tmp .venv/bin/python -m pytest -q tests/unit_tests/test_checkpoint_*.py: passed.
  • ruff check and ruff format --check on the changed files: clean.
  • pre-commit run --files <changed files>: all hooks passed.
  • The commit carries a Signed-off-by line.

Rollout evidence

Setup

The model was Qwen/Qwen3-4B-Instruct-2507, served by vLLM 0.24.0 with the hermes tool-call parser on one NVIDIA RTX 6000 Ada Generation (48 GB).
Gym's vllm_model server ran with uses_reasoning_parser=false, because the served model is an instruct model without a reasoning parser.
Every rollout sampled at temperature 1.0, used the five tasks in resources_servers/workplace_assistant/data/example.jsonl, and ran Simple Agent with max_steps: 6 behind the flat-row wrapper.
Each tree ran in its own worktree, with server venvs created by gym env start.

Uninterrupted baseline

This change on main ran with checkpointing off, eight rollouts per task:

gym env start --config resources_servers/workplace_assistant/configs/workplace_assistant.yaml --model-type vllm_model \
    --model Qwen/Qwen3-4B-Instruct-2507 --model-url http://127.0.0.1:8100/v1 --model-api-key dummy \
    ++policy_model.responses_api_models.vllm_model.uses_reasoning_parser=false
gym eval run --no-serve --config resources_servers/workplace_assistant/configs/workplace_assistant.yaml --model-type vllm_model \
    --model Qwen/Qwen3-4B-Instruct-2507 --model-url http://127.0.0.1:8100/v1 --model-api-key dummy \
    ++policy_model.responses_api_models.vllm_model.uses_reasoning_parser=false \
    --agent workplace_assistant_simple_agent --input resources_servers/workplace_assistant/data/example.jsonl \
    --output rollouts.jsonl --num-repeats 8 --concurrency 16 --temperature 1.0

The 40 rollouts had a mean reward of 0.25.
By task, the email reply succeeded 7 of 8 times, the CRM status move 3 of 8 times, and the calendar rename and the two other CRM tasks never succeeded.
The same command on the checkpoint stack with this change, still with checkpointing off, also gave 0.25 over 40 rollouts.
Neither run had failed or masked rollouts or tracebacks in the server logs.

Checkpoint, crash and restore

The tree was the checkpoint stack (ananthsub/partial-ckpt-e2e) with this change cherry-picked on top; it applied without conflicts.
A driver built on the process-level checkpoint suite (tests/e2e/checkpoint/checkpoint_deployment.py) started the policy model, the Workplace Assistant resources server, Simple Agent and the flat-row wrapper, each as its own process from its own venv.
It sent the rollouts to the wrapper's /run and drove the checkpoint through nemo_gym._checkpoint.coordination, as a training controller would:

  1. Prepare and commit a checkpoint while rollouts are mid-turn.
  2. Kill every Gym server process group with SIGKILL, start fresh processes from the same configs, restore the committed episodes and resume.
  3. Take a second checkpoint of the restored state before the replacements start, then resume again.
  4. Run every unfinished rollout again as attempt 1, which continues from its checkpointed boundary.

Four runs cut the checkpoint at different points, so that later cuts catch episodes after their state-changing tool calls:

Run Rollouts Finished before the checkpoint Continued Continued with changed Workplace state Mean reward of the run
1 40 2 38 0 0.275
2 40 13 27 0 0.300
3 60 28 32 7 0.317
4 80 46 34 10 0.288

Every prepare returned in 8 ms or less and reported no restarts.
Simple Agent reported every in-flight episode at a boundary, and the Workplace server exported one session per continued episode.

The 17 episodes with changed state had already made one or two successful tool calls that changed their session's tables.
Twelve had sent the email reply and five had moved one or two CRM customers to "Lost" before the checkpoint.
Each export differs from a freshly seeded session exactly where those calls changed it.
An email episode's export has one added row, the reply "RE: Task Update on Develop prototype for report generation", and every other email row equal to the seeded data.
A CRM episode that had updated customers 00000037 and 00000195 exports both with status "Lost" where the seeded data has "Proposal", and no other cell differs.

After the restore, all 131 continued episodes resumed rather than restarted:

  • All 131 replacements returned HTTP 200 with a reward.
  • Each final trajectory begins with the tool calls made before the crash, with the same call IDs and arguments, and the model then continued from there.
  • The second checkpoint, taken in the fresh processes before any replacement ran, exported the same tables as the first for all 131 sessions.
  • The five CRM episodes kept updating customers on their restored sessions: three or four more updates each, every one answered "Customer updated successfully."
  • None of the 276 tool results returned after the restore reported a missing session or a stale attempt.

At the end, the Workplace server and Simple Agent held no sessions.

The rewards are comparable to the uninterrupted run.
Over all 220 rollouts the mean was 0.295 (65 of 220), against 0.25 (10 of 40) uninterrupted.
By task, the email reply succeeded 44 of 44 times and the CRM status move 20 of 44 times, against 7 of 8 and 3 of 8.
The calendar rename succeeded once in 44 and the other two CRM tasks never, as in the baseline.
Of the 17 episodes caught after changing their state, 16 earned a reward of 1.

What is not covered

  • /verify scores a trajectory by replaying its tool calls on a fresh environment, so the reward does not depend on the restored session.
    The restored state is shown instead by the second export and by the tool calls that ran on the restored sessions.
  • Only the flat-row wrapper ran.
    The typed single_agent_turn request path did not run with Workplace Assistant.
  • Only the five example tasks ran; the train and validation splits are downloaded from Hugging Face and were not used.
    The model rarely solves three of the five tasks.
  • MCP tool exposure, token capture and generation cuts stayed off.

Compatibility

  • Without the global checkpoint: block set to enabled: true, behavior is unchanged. The hooks are only called by the checkpoint control routes.
  • With checkpointing on, Workplace Assistant sessions no longer block a checkpoint. They are exported, at about 350 KB of JSON per session.
  • Tool behavior, verify, and rewards are unchanged.

@ananthsub ananthsub added feature New capabilities, enhancements, or enablement work area:environment Individual environments, benchmarks, verifiers, and environment-specific resources servers labels Oct 1, 2026
@copy-pr-bot

copy-pr-bot Bot commented Oct 1, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 5c5b53b to 4a44fa9 Compare October 1, 2026 21:53
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 87e1f0c to 207f588 Compare October 1, 2026 21:53
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 207f588 to afee845 Compare October 1, 2026 22:05
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 4a44fa9 to 44260b9 Compare October 1, 2026 22:05
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from afee845 to 6a3c667 Compare October 2, 2026 13:22
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 44260b9 to 031e9f6 Compare October 2, 2026 13:22
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 6a3c667 to f3ade85 Compare October 2, 2026 19:43
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 031e9f6 to 7948699 Compare October 2, 2026 19:43
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from f3ade85 to ca27fd2 Compare October 2, 2026 21:17
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 7948699 to 4317d35 Compare October 2, 2026 21:17
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from ca27fd2 to f98be7a Compare October 2, 2026 23:32
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 4317d35 to 92c9aad Compare October 2, 2026 23:32
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from f98be7a to beab099 Compare October 6, 2026 19:28
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch 2 times, most recently from 8527bf1 to fe6b109 Compare October 6, 2026 20:54
@copy-pr-bot

copy-pr-bot Bot commented Oct 6, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from f39aa55 to 17a509f Compare October 13, 2026 20:27
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from ce82ebf to 2b249aa Compare October 13, 2026 22:56
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 17a509f to 3baed96 Compare October 13, 2026 22:56
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 3baed96 to 726383b Compare October 14, 2026 00:27
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 2b249aa to 1c4ed3c Compare October 14, 2026 16:01
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 726383b to 009480a Compare October 14, 2026 16:01
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 1c4ed3c to a5a36f0 Compare October 14, 2026 17:33
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch 2 times, most recently from a354cb3 to 3a8ddf3 Compare October 14, 2026 20:36
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from a5a36f0 to 3073950 Compare October 14, 2026 20:36
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 3a8ddf3 to dc6f470 Compare October 15, 2026 13:21
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 3073950 to 5830891 Compare October 15, 2026 13:21
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from dc6f470 to d83a4e0 Compare October 15, 2026 15:41
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 5830891 to 1923e7f Compare October 15, 2026 15:41
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from d83a4e0 to 9fd69f4 Compare October 15, 2026 16:54
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 1923e7f to 63809c2 Compare October 15, 2026 17:37
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 9fd69f4 to 31ad088 Compare October 15, 2026 17:37
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from 63809c2 to c908576 Compare October 15, 2026 18:15
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 31ad088 to 53aa609 Compare October 15, 2026 18:16
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-workplace-assistant branch from c908576 to 9e7ab61 Compare October 15, 2026 18:55
@ananthsub
ananthsub force-pushed the ananthsub/partial-ckpt-e2e branch from 53aa609 to 9b5c297 Compare October 15, 2026 18:55
Declare the Workplace Assistant resources server checkpoint_mode =
"exported" and implement the three session hooks over each session's
tool environment. Export encodes every in-memory table column by column,
keeping dtypes, index labels, and the difference between empty CSV cells
(NaN) and values a tool stored as None. Restore rebuilds every session
before installing any, so one invalid state installs nothing. Export
leaves out a session /verify already discarded, and retire drops the
session's tool environment.

/verify keeps the default "wait" mode because it discards the session.

Ported from #2946 onto the v2 checkpoint hooks, without
the execution-to-session index and identity lookups that v2 replaces
with cookie session IDs.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>

@zyzhou5 zyzhou5 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39343677971

@github-actions

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39344263376

@ananthsub

Copy link
Copy Markdown
Contributor Author

Rollout evidence rerun on vLLM 0.29.0, the version Gym's vllm extra pins. The evidence in the description ran on vLLM 0.24.0.

The tree was main at dc709bcfc3, which contains this change, #3895 and #3896. The model was Qwen/Qwen3-4B-Instruct-2507, served with the hermes tool-call parser on one NVIDIA RTX 6000 Ada Generation, with uses_reasoning_parser=false and temperature 1.0. The commands and the checkpoint driver are the ones in the description.

Uninterrupted baseline. 40 rollouts, eight per task, gave a mean reward of 0.275. The email reply succeeded 8 of 8 times, the CRM status move 3 of 8 times, and the other three tasks never succeeded. That is the same pattern as on vLLM 0.24.0, where the email reply succeeded 7 of 8 times. There were no failed or masked rollouts and no tracebacks.

Checkpoint, crash and restore. Two runs cut the checkpoint at different points. Each crashed every Gym process with SIGKILL, restored into fresh processes, took a second checkpoint of the restored state, and ran every unfinished rollout again as attempt 1.

Cut Rollouts Finished before the checkpoint Continued Continued with changed Workplace state Mean reward of the run
Early 40 2 38 0 0.275
Late 80 45 35 12 0.288
  • Prepare returned in 4 ms in both runs and reported no restarts. Simple Agent held every in-flight episode at a boundary, and the Workplace server exported one session for each.
  • Six of the 12 episodes with changed state had sent the email reply. Each export adds exactly that reply and leaves every seeded email unchanged. The other six had updated one to three CRM customers, and each export differs from a freshly seeded session only in the cells those successful calls changed. For example, one episode had reassigned customers 00000076 and 00000198 to raj@company.com.
  • In both runs, the second export of every restored session equals the first.
  • All 73 continued episodes resumed as attempt 1 and returned 200. Each final trajectory starts with exactly the calls, arguments and outputs recorded at the checkpoint. Six of the episodes with changed CRM state went on to update more customers on the restored session. None of the 124 tool outputs returned after the restores reported a missing or stale session.

At the end of both runs, every participant was idle and the Workplace server held no sessions. No server log had a traceback.

This branch was previously deployed

1 inactive deployment
public — 8ed4f964 Deployed Oct 16, 2026 by copy-pr-bot[bot] via release / finalize / notify #5589
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:environment Individual environments, benchmarks, verifiers, and environment-specific resources servers complexity:medium Single-domain change with interacting parts or a moderate review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer sla:review-overdue Review response is over the one-business-day SLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants