Commit a74c8fe
authored
feat: agent skill evaluation infrastructure (#1605)
Implements #1256 (skill evaluation infrastructure), part
of epic #1494.
## Summary
Adds skills as a **run-level evaluation variable, decoupled from the
dataset**. Skills are specified via `skills.path` on `gym eval run` and
applied to a fixed, skill-agnostic dataset at rollout time — no
dataset-row coupling (deliberately *not* modeled like `agent_ref`). Each
rollout result is stamped with a `skills_ref` for provenance and variant
comparison.
### Core (agent-agnostic)
* `nemo_gym/skills.py` — `load_skill_directory` (parses `SKILL.md`
frontmatter, actionable errors for malformed skills), `hash_skill_dir`
(short content sha256), `stage_skills`, and `SkillsConfig`/`SkillsRef`
models.
* `rollout_collection.py` — optional `skills` config; resolve once at
startup, stamp `skills_ref` (path + hash + metadata) onto each row,
propagate to results.
* `skills_ref` **carries a content** `hash` so optimizer loops (e.g.
ACE, GEPA, EvoSkill) that mutate a skill *in place* at the same path
stay distinguishable in reward profiling. Identity is derived from bytes
on disk — no optimizer cooperation required.
### Claude Code agent (thin per-agent adapter)
* Reads `skills_ref.path` from the `/run` request, stages it into the
per-request `CLAUDE_CONFIG_DIR/skills/`, and forces `--bare` off so
native discovery loads it. **No skill-interpretation logic added** —
Claude loads/activates natively.
### Docs
* `agent-server/agent-skills` reference page: workflow, `skills_ref`
hash rationale, and the three-step per-agent adapter contract for future
agents (Codex, MCP).
## Design notes
The decoupled (run-level, not dataset-row) design and the content-hash
variant identity are discussed in
[https://github.com/NVIDIA-NeMo/Gym/issues/1256#issuecomment-4715376347](<https://github.com/NVIDIA-NeMo/Gym/issues/1256#issuecomment-4715376347>).
A committed teaching example + end-to-end "evaluate and optimize skills"
tutorial (with a composite-reward example) is tracked separately in
#1604.
## Test plan
- [X] Unit tests: `tests/unit_tests/test_skills.py` (incl.
spec-compliant `metadata.version`, UTF-8 BOM tolerance), rollout
`skills_ref` stamping + `resume_from_cache` round-trip, and agent-side
`_build_command` skills/`--bare` handling, per-request config-dir
cleanup on staging failure, and `run()` → `_create_response`
`skills_path` forwarding. `ruff` clean.
- [X] End-to-end smoke test (reproducible steps below).
## Smoke test (reproducible)
Validates both halves end to end against the existing reasoning_gym +
Claude Code agent setup, using two variants of the *same* skill with
different content (so hashes differ) and a distinct output marker per
variant (to prove activation).
### 1\. Create two throwaway skill variants
```bash
mkdir -p /tmp/ng_skill_smoke/variant_a/logic_puzzle_solver /tmp/ng_skill_smoke/variant_b/logic_puzzle_solver
```
`/tmp/ng_skill_smoke/variant_a/logic_puzzle_solver/SKILL.md`:
```markdown
---
name: logic_puzzle_solver
description: Use when solving knights-and-knaves logic puzzles where some characters always tell the truth and others always lie.
---
# Logic Puzzle Solver
When solving a knights-and-knaves puzzle:
1. List each character and the two hypotheses (knight = truth-teller, knave = liar).
2. For each statement, work out what it implies under each hypothesis.
3. Eliminate contradictions until a consistent assignment remains.
Always finish your final answer with the marker: [solved-with-skill-v1]
```
`/tmp/ng_skill_smoke/variant_b/logic_puzzle_solver/SKILL.md` — identical
except a truth-table approach in the body and the marker
`[solved-with-skill-v2]` (the differing content is what makes the hashes
differ).
### 2\. Start the servers (with this branch's code)
```bash
gym env start "+config_paths=[resources_servers/reasoning_gym/configs/reasoning_gym_claude_code_agent.yaml]"
```
### 3\. Collect rollouts for each variant over the same dataset
```bash
gym eval run --no-serve \
+agent_name=reasoning_gym_claude_code_agent \
+input_jsonl_fpath=resources_servers/reasoning_gym/data/example.jsonl \
+output_jsonl_fpath=/tmp/skills_smoke_a.jsonl \
+skills.path=/tmp/ng_skill_smoke/variant_a \
+limit=1
gym eval run --no-serve \
+agent_name=reasoning_gym_claude_code_agent \
+input_jsonl_fpath=resources_servers/reasoning_gym/data/example.jsonl \
+output_jsonl_fpath=/tmp/skills_smoke_b.jsonl \
+skills.path=/tmp/ng_skill_smoke/variant_b \
+limit=1
```
### 4\. Verify
```bash
python -c "import json;print(json.loads(open('/tmp/skills_smoke_a.jsonl').readline())['skills_ref'])"
python -c "import json;print(json.loads(open('/tmp/skills_smoke_b.jsonl').readline())['skills_ref'])"
grep -o 'solved-with-skill-v[12]' /tmp/skills_smoke_*.jsonl
```
### Observed results
<!-- linear:table-colwidths:266,266,266 -->
| | variant A | variant B |
| -- | -- | -- |
| `skills_ref.path` | `/tmp/ng_skill_smoke/variant_a` |
`/tmp/ng_skill_smoke/variant_b` |
| `skills_ref.hash` | `4cff9c793395` | `100c63f5ea97` |
| `skills_ref.skills` | `logic_puzzle_solver` (name + description parsed
from frontmatter) | same name/description, different content |
| output marker | `solved-with-skill-v1` | `solved-with-skill-v2` |
Confirms:
* **Gym side:** `skills_ref` stamped + propagated to results, with
**distinct content hashes** for same-named variants.
* **Agent side:** each variant was staged into its own per-request
`CLAUDE_CONFIG_DIR/skills/`, `--bare` was dropped, and Claude **loaded
and followed the correct variant with no cross-contamination** (A→v1,
B→v2).
---------
Signed-off-by: Chris Wing <cwing@nvidia.com>1 parent 9284c5b commit a74c8fe
9 files changed
Lines changed: 885 additions & 15 deletions
File tree
- fern/versions/latest/pages/agent-server
- nemo_gym
- responses_api_agents/claude_code_agent
- tests
- tests/unit_tests
Lines changed: 105 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
| 120 | + | |
120 | 121 | | |
121 | 122 | | |
122 | 123 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| 55 | + | |
54 | 56 | | |
55 | 57 | | |
56 | 58 | | |
| |||
198 | 200 | | |
199 | 201 | | |
200 | 202 | | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
201 | 207 | | |
202 | 208 | | |
203 | 209 | | |
| |||
268 | 274 | | |
269 | 275 | | |
270 | 276 | | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
271 | 288 | | |
272 | 289 | | |
273 | 290 | | |
| |||
305 | 322 | | |
306 | 323 | | |
307 | 324 | | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
308 | 330 | | |
309 | 331 | | |
310 | 332 | | |
| |||
467 | 489 | | |
468 | 490 | | |
469 | 491 | | |
| 492 | + | |
| 493 | + | |
470 | 494 | | |
471 | 495 | | |
472 | 496 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
0 commit comments