Skill labeling pipeline for the COS-PLAY co-evolution framework (COLM 2026). Annotates cold-start episode trajectories with concise labels suitable for RAG retrieval, the Decision Agent, and downstream skill extraction by the Skill Bank Agent.
There are three pipelines available:
| Script | Purpose |
|---|---|
label_episodes_gpt54.py |
Labels only — annotates episodes with summary_state, summary, intentions (leaves skills null). |
label_episodes_with_skills.py |
Labels + Skill Selection + GRPO Cold-Start — same annotations as above, plus loads a pre-built skill bank, runs top-k skill selection at each step, and exports GRPO cold-start training data for both action-taking and skill-selection LoRA adapters. |
extract_skillbank_gpt54.py |
Skills only — reads already-labeled rollouts (e.g. from labeling/output/gpt54/), runs the full SkillBankAgent pipeline, and writes the skill bank and catalogs. No labeling step. |
For each experience step in an episode:
| Field | Format | LLM? | Description |
|---|---|---|---|
summary_state |
key=value | key=value |
No | Deterministic, game-aware structured facts. Optimised for RAG embedding retrieval. |
summary |
summary_state | note=<strategic note> |
Yes (≤25 tokens) | Same facts as summary_state plus a short LLM-generated threat/opportunity note grounded in what changed since the previous step. |
intentions |
[TAG] subgoal phrase |
Yes (≤40 tokens) | Tagged subgoal with delta-aware tag evolution. Tag shifts when the game situation changes significantly. |
skills |
{skill_id, skill_name, skill_summary, ...} or null |
Yes (when using skill extraction) | Skill assignment from the SkillBankAgent pipeline. Contains RAG-friendly summaries, effects contracts, and segment boundaries. Null when using labels-only pipeline. |
Intentions use one of these categorical tags for skill segmentation:
SETUP | CLEAR | MERGE | ATTACK | DEFEND | NAVIGATE | POSITION |
COLLECT | BUILD | SURVIVE | OPTIMIZE | EXPLORE | EXECUTE
- Deterministic-first:
summary_stateis fully deterministic (0 LLM tokens). Game-aware extractors pull structured facts (e.g. Tetris holes, 2048 highest tile) so key information is never lost even if the LLM call fails. - Delta-aware: Both
summaryandintentionsreceive a state-delta from the previous step (e.g.holes:10->32), ensuring the LLM note and tag reflect what actually changed rather than repeating generic advice. - Urgency detection: Absolute-value thresholds trigger urgency hints for
critical situations (e.g. Tetris holes>25, 2048 empty<3), so intention tags
shift to
[SURVIVE]or[CLEAR]even when per-step deltas are small. - Concise for RAG:
summary_stateuseskey=valuepairs that tokenise cleanly for embedding models. The keyword scoring inEpisodicMemoryStoresplits on=|:,;/so individual keys and values match independently. - Tight LLM budget: The LLM adds only a short strategic note (~10 words, 25 tokens) and a tagged subgoal phrase (~15 words, 40 tokens) — designed for 14B models with minimal output budgets.
step 0/86 (opening):
summary_state: game=tetris | phase=opening | step=0/86 | stack_h=0 | piece=L | next=S,O,I,J | level=1 | reward=+1
summary: game=tetris | phase=opening | step=0/86 | stack_h=0 | piece=L | next=S,O,I,J | level=1 | reward=+1 | note=Flat left stack; avoid right-side overhangs.
intentions: [SETUP] Place L flat to preserve a clean, flexible opening stack
step 50/86 (midgame — holes spiked):
summary_state: game=tetris | phase=midgame | step=50/86 | stack_h=14 | holes=32 | next=T,Z,I,J | level=1
summary: game=tetris | phase=midgame | step=50/86 | stack_h=14 | holes=32 | next=T,Z,I,J | level=1 | note=Sharp hole spike from recent stack rise; avoid covering center wells.
intentions: [CLEAR] Place S flat to reduce holes and set up immediate line clears
step 85/86 (endgame — near ceiling):
summary_state: game=tetris | phase=endgame | step=85/86 | stack_h=17 | holes=38 | piece=Z | next=T,L,O,S | level=1 | reward=+1
summary: game=tetris | phase=endgame | step=85/86 | stack_h=17 | holes=38 | piece=Z | next=T,L,O,S | level=1 | reward=+1 | note=Z increased holes; avoid center lock, preserve left well.
intentions: [SURVIVE] Place Z vertically to avoid overhangs and prevent topping out
Tag evolution: [SETUP] → [POSITION] → [CLEAR] → [SURVIVE] as holes grow and stack nears ceiling.
step 0/152 (opening):
summary_state: game=2048 | phase=opening | step=0/152 | highest=2 | empty=14 | tiles=2,2
summary: game=2048 | phase=opening | step=0/152 | highest=2 | empty=14 | tiles=2,2 | note=Two isolated 2s; merge by moving right or down.
intentions: [SETUP] Consolidate the two 2-tiles toward one side for an early merge
step 75/152 (midgame):
summary_state: game=2048 | phase=midgame | step=75/152 | highest=64 | empty=4 | tiles=64,32,16,16,8 | merges=1 | reward=+4
summary: game=2048 | phase=midgame | step=75/152 | highest=64 | empty=4 | tiles=64,32,16,16,8 | merges=1 | reward=+4 | note=Rightmost column open; safe right/down merges available
intentions: [MERGE] Merge left for space while keeping the 64 anchored and middle chain stable
step 151/152 (endgame — board full):
summary_state: game=2048 | phase=endgame | step=151/152 | highest=128 | empty=0 | tiles=128,64,32,16,16 | merges=1 | reward=+4
summary: game=2048 | phase=endgame | step=151/152 | highest=128 | empty=0 | tiles=128,64,32,16,16 | merges=1 | reward=+4 | note=No empty cells; next move must create one.
intentions: [SURVIVE] Merge vertical 2s now to create space and survive
Tag evolution: [SETUP] → [MERGE] → [EXECUTE] → [SURVIVE] as empty cells deplete.
step 0/50 (opening):
summary_state: game=candy crush | phase=opening | step=0/50 | score=0 | moves=50 | board=8x8 | pairs=18 | reward=+3
summary: game=candy crush | phase=opening | step=0/50 | score=0 | moves=50 | board=8x8 | pairs=18 | reward=+3 | note=No immediate 3-match; look for central column setup.
intentions: [ATTACK] Make immediate 3-candy match with swap maximizing score and preserving flexibility
step 32/50 (midgame):
summary_state: game=candy crush | phase=midgame | step=32/50 | score=366 | moves=18 | board=8x8 | pairs=18 | reward=+6
summary: game=candy crush | phase=midgame | step=32/50 | score=366 | moves=18 | board=8x8 | pairs=18 | reward=+6 | note=Center-right red cluster nearly aligned for next-turn match.
intentions: [ATTACK] Swap for an immediate match and better cascade potential
step 49/50 (endgame — final move):
summary_state: game=candy crush | phase=endgame | step=49/50 | score=507 | moves=1 | board=8x8 | pairs=14 | reward=+12
summary: game=candy crush | phase=endgame | step=49/50 | score=507 | moves=1 | board=8x8 | pairs=14 | reward=+12 | note=New 4-candy setup available; exploit final move for bigger clear.
intentions: [EXECUTE] Use the final move to make the immediate horizontal match on row 3
Tag evolution: [ATTACK] → [CLEAR] → [ATTACK] → [EXECUTE] as moves deplete and focus shifts to maximising remaining turns.
| File | Purpose |
|---|---|
label_episodes_gpt54.py |
Labels-only script. Reads episode JSONs, calls GPT-5.4, writes labeled output with skills=null. |
label_episodes_with_skills.py |
Labels + Skill Selection + GRPO Cold-Start. Loads a pre-built skill bank, runs top-k skill selection per step, exports GRPO training data for action-taking and skill-selection LoRA adapters. |
extract_skillbank_gpt54.py |
Skills only: reads already-labeled rollouts, runs SkillBankAgent pipeline, writes skill bank and catalogs. No labeling. |
run_labeling.sh |
Shell wrapper for label_episodes_gpt54.py. |
run_label_with_skills.sh |
Shell wrapper for label_episodes_with_skills.py. |
run_extract_skillbank.sh |
Shell wrapper for extract_skillbank_gpt54.py. |
readme.md |
This file. |
# From Game-AI-Agent root
export OPENROUTER_API_KEY="sk-or-..."
export PYTHONPATH="$(pwd):$(pwd)/../GamingAgent:$PYTHONPATH"
# Label all games (reads from cold_start/output/gpt54/, writes to labeling/output/gpt54/)
python labeling/label_episodes_gpt54.py
# Label specific game(s)
python labeling/label_episodes_gpt54.py --games tetris candy_crush
# Label a single episode file
python labeling/label_episodes_gpt54.py --input_file cold_start/output/gpt54/tetris/episode_000.json
# Dry run — preview labeling on one episode without saving
python labeling/label_episodes_gpt54.py --dry_run --games tetris --max_episodes 1
# Process exactly one rollout per game (quick test across all games)
python labeling/label_episodes_gpt54.py --one_per_game -v
# Write labels in-place (overwrite originals)
python labeling/label_episodes_gpt54.py --in_place
# Or use the shell wrapper:
bash labeling/run_labeling.sh --games tetris -vThis pipeline extends the labels-only pipeline by loading a pre-built skill bank (e.g. from skill_agents), running top-k skill selection at each step, and exporting GRPO cold-start training data for decision-agent LoRA fine-tuning.
Prerequisites: A skill bank must already exist (e.g. from skill_agents/extract_skillbank/). The script auto-discovers per-game banks under the default search directories.
# From Game-AI-Agent root
export OPENROUTER_API_KEY="sk-or-..."
export PYTHONPATH="$(pwd):$(pwd)/../GamingAgent:$PYTHONPATH"
# Quick dry run — preview one episode per game
python labeling/label_episodes_with_skills.py \
--one_per_game --dry_run -v \
--bank skill_agents/extract_skillbank/output/gpt54_skillbank_grpo
# Full run — all episodes, all games, auto-discover banks
python labeling/label_episodes_with_skills.py \
--bank skill_agents/extract_skillbank/output/gpt54_skillbank_grpo \
-v
# Specific games
python labeling/label_episodes_with_skills.py --games tetris candy_crush -v
# Without skill bank (skills will be null, no GRPO data)
python labeling/label_episodes_with_skills.py --no-bank
# Custom top-k (retrieve 5 candidates before LLM picks one)
python labeling/label_episodes_with_skills.py --top_k 5
# Label in-place (overwrite originals)
python labeling/label_episodes_with_skills.py --in_place \
--bank skill_agents/extract_skillbank/output/gpt54_skillbank_grpo| Flag | Default | Description |
|---|---|---|
--bank |
auto-discover | Skill bank directory (or JSONL file). Auto-discovers per-game banks within. |
--bank_dir |
— | Additional skill bank root director(ies) to scan. |
--no-bank |
off | Disable skill bank entirely (skills field will be null). |
--top_k |
3 |
Number of skill candidates to retrieve before LLM selection. |
--no-query-engine |
off | Disable SkillQueryEngine (use TF-IDF keyword fallback only). |
labeling/output/gpt54_skill_labeled/
├── tetris/
│ ├── episode_000.json # labeled episode with skills populated
│ ├── episode_001.json
│ └── labeling_summary.json
├── candy_crush/
│ └── ...
├── grpo_coldstart/ # GRPO cold-start training data
│ ├── tetris/
│ │ ├── action_taking.jsonl # state + actions → chosen action (every step)
│ │ └── skill_selection.jsonl # state + candidates → chosen skill (steps with ≥2 candidates)
│ ├── candy_crush/
│ │ ├── action_taking.jsonl
│ │ └── skill_selection.jsonl
│ └── ...
└── labeling_batch_summary.json
The grpo_coldstart/ directory contains two JSONL files per game, designed for GRPO LoRA training of a decision agent. Both files share a common schema so a single GRPO trainer can consume either.
Each row contains the full action-selection prompt (state + available actions + active skill guidance) and the expert action chosen by GPT-5.4.
{
"type": "action_taking",
"game": "tetris",
"episode": "78bf8bfa-...",
"step": 5,
"prompt": "<system prompt + skill guidance block + state + numbered action list>",
"completion": "REASONING: Expert play.\nACTION: 7",
"chosen_action": "hard_drop",
"available_actions": ["no_op","left","right","rotate_left","rotate_right","soft_drop","hard_drop"],
"reward": 1.0,
"summary_state": "game=tetris | phase=opening | ...",
"intention": "[SETUP] Place L-piece to build flat stack",
"active_skill": "sk_clear_rows"
}Each row contains the skill-selection prompt (state + intention + numbered candidate menu) and the GPT-5.4 expert skill choice.
{
"type": "skill_selection",
"game": "tetris",
"episode": "78bf8bfa-...",
"step": 5,
"prompt": "<system prompt + state + intention + numbered candidate menu>",
"completion": "REASONING: Holes are severe, clearing is priority.\nSKILL: 1",
"chosen_idx": 0,
"skill_candidates": ["sk_clear_rows", "sk_stack_flat", "sk_survive"],
"chosen_skill_id": "sk_clear_rows",
"reward": 1.0,
"summary_state": "game=tetris | phase=opening | ...",
"intention": "[SETUP] Place L-piece to build flat stack"
}The cold-start data provides expert demonstrations (from GPT-5.4) that seed the GRPO training loop. The typical workflow is:
Step 1: Generate cold-start episodes
└─ cold_start/generate_cold_start_gpt54.py
Step 2: Extract skill banks
└─ skill_agents/extract_skillbank/extract_skillbank_grpo_gpt54.py
Step 3: Label episodes + select skills + export GRPO data ← this script
└─ labeling/label_episodes_with_skills.py
Step 4: Train LoRA adapters via GRPO
└─ Use grpo_coldstart/{game}/action_taking.jsonl
and grpo_coldstart/{game}/skill_selection.jsonl
as initial training data for two LoRA adapters
Two LoRA adapters for the decision agent:
| Adapter | Training data | Input (prompt) | Output (completion) | Reward signal |
|---|---|---|---|---|
| action | action_taking.jsonl |
State + available actions + active skill guidance | REASONING: ... ACTION: <number> |
Step reward from episode |
| skill_select | skill_selection.jsonl |
State + intention + numbered skill candidates | REASONING: ... SKILL: <number> |
Step reward from episode |
GRPO training loop (per adapter):
- Cold-start phase: Load JSONL, treat GPT-5.4 completions as expert demonstrations. Fine-tune the LoRA adapter using supervised loss on
(prompt, completion)pairs, weighted byreward. - Rollout phase: Generate G completions per prompt at higher temperature. Evaluate each completion with the reward function (step reward, or a learned reward model).
- Training phase: Compute GRPO advantages from the G-sample rewards. Update LoRA weights via policy gradient with clipping.
This follows the same two-phase pattern used by the existing segment/contract/curator adapters in skill_agents/grpo/. To integrate, add ACTION and SKILL_SELECT to the SkillFunction enum in skill_agents/lora/skill_function.py.
# Example: loading cold-start data for training
import json
from pathlib import Path
grpo_dir = Path("labeling/output/gpt54_skill_labeled/grpo_coldstart/tetris")
# Action-taking samples
with open(grpo_dir / "action_taking.jsonl") as f:
action_samples = [json.loads(line) for line in f]
# Skill-selection samples
with open(grpo_dir / "skill_selection.jsonl") as f:
skill_samples = [json.loads(line) for line in f]
print(f"Action samples: {len(action_samples)}")
print(f"Skill samples: {len(skill_samples)}")
# Each sample has: prompt, completion, reward — ready for GRPO
for s in action_samples[:2]:
print(f" step {s['step']}: action={s['chosen_action']}, reward={s['reward']}")label_experience()
├─ summary_state = build_rag_summary() # deterministic, 0 LLM tokens
├─ [skill selection] # only when skill bank loaded
│ ├─ extract_game_facts() → structured_state
│ ├─ get_top_k_skill_candidates() # SkillQueryEngine or TF-IDF
│ └─ select_skill_via_llm() # LLM picks best of top-k
├─ summary = summary_state + LLM note # 1 LLM call, ≤25 tokens
├─ intentions = [TAG] phrase # 1 LLM call, ≤40 tokens
├─ skills = _skill_guidance_to_label(guidance) # structured skill dict
└─ GRPO metadata: # stored on experience
├─ skill_candidates, skill_chosen_idx
└─ skill_reasoning
export_grpo_coldstart_data() # called after each episode
├─ action_taking.jsonl # every step
│ └─ prompt = system + skill_guidance + state + actions
└─ skill_selection.jsonl # steps with ≥2 candidates
└─ prompt = system + state + intention + candidates
| What | label_episodes_gpt54.py |
label_episodes_with_skills.py |
qwen3_decision_agent.py |
|---|---|---|---|
| Summary | identical | identical | get_state_summary() |
| Intentions | identical | identical | infer_intention() |
| Skill selection | none (skills=null) |
top-k + LLM selection | top-k + LLM selection |
| Action selection | none (offline labeling) | none (offline labeling) | LLM action selection |
| GRPO export | none | action_taking + skill_selection JSONL | stored on Experience objects |
| Output | labeled episodes | labeled episodes + GRPO cold-start | live rollouts |
Use this when you already have labeled episodes (e.g. in labeling/output/gpt54/) and want to run only the skill extraction pipeline (no labeling, no Phase 1).
Input: labeling/output/gpt54/ (or --input_dir <path>) — directory with <game>/episode_*.json where each episode has summary_state, summary, and intentions filled in.
Output: labeling/output/gpt54_skillbank/ (or --output_dir <path>) — per-game skill banks, catalogs, sub_episodes, and cross-game archetypes.
# From Game-AI-Agent root
export OPENROUTER_API_KEY="sk-or-..."
export PYTHONPATH="$(pwd):$(pwd)/../GamingAgent:$PYTHONPATH"
# Extract skills for all games (reads labeling/output/gpt54, writes labeling/output/gpt54_skillbank)
python labeling/extract_skillbank_gpt54.py
# Specific game(s)
python labeling/extract_skillbank_gpt54.py --games tetris super_mario
# Custom input/output
python labeling/extract_skillbank_gpt54.py --input_dir labeling/output/gpt54 --output_dir path/to/out
# Quick test: one episode per game
python labeling/extract_skillbank_gpt54.py --one_per_game -v
# Preview without running (dry run)
python labeling/extract_skillbank_gpt54.py --dry_run
# Re-segment against seeded bank (second pass); save annotated episodes to output
python labeling/extract_skillbank_gpt54.py --resegment --save_annotated
# Skip cross-game archetype aggregation
python labeling/extract_skillbank_gpt54.py --skip_archetypes
# Or use the shell wrapper (sets PYTHONPATH, checks input dir):
bash labeling/run_extract_skillbank.sh
bash labeling/run_extract_skillbank.sh --games tetris -v
bash labeling/run_extract_skillbank.sh --dry_run| Flag | Default | Description |
|---|---|---|
--input_dir |
cold_start/output/gpt54 |
Input directory with <game>/episode_*.json |
--input_file |
— | Label a single file instead of scanning a directory |
--output_dir |
labeling/output/gpt54 or gpt54_skills |
Output directory for labeled episodes |
--games |
all found | Filter to specific game(s) |
--model |
gpt-5.4 |
LLM model for labeling |
--max_episodes |
all | Cap episodes per game |
--one_per_game |
off | Process only the first episode for each game |
--delay |
0.1 |
Seconds between API calls (rate limiting) |
--overwrite |
off | Re-label already-labeled episodes |
--in_place |
off | Write back to original input files |
--dry_run |
off | Preview without saving |
--verbose / -v |
off | Print per-step details |
| Flag | Default | Description |
|---|---|---|
--input_dir |
labeling/output/gpt54 |
Directory with labeled game sub-folders (<game>/episode_*.json). Episodes must have summary_state, summary, intentions. |
--output_dir |
labeling/output/gpt54_skillbank |
Root for per-game skill banks, catalogs, sub_episodes, archetypes. |
--games |
all found | Only process these games. |
--model |
gpt-5.4 |
LLM model for skill naming/description. |
--max_episodes |
all | Cap episodes per game. |
--one_per_game |
off | Process only the first episode per game. |
--resegment |
off | Re-run pipeline against seeded bank (second pass). |
--skip_archetypes |
off | Skip cross-game archetype aggregation. |
--save_annotated |
off | Write episode JSONs with skills populated to output. |
--dry_run |
off | Preview games/episodes without running extraction. |
--verbose / -v |
off | Per-step details. |
labeling/output/gpt54/
├── tetris/
│ ├── episode_000.json # labeled episode (skills=null)
│ ├── episode_001.json
│ └── labeling_summary.json # per-game stats
├── candy_crush/
│ └── ...
└── labeling_batch_summary.json # overall run stats
Input: Already-labeled episode JSONs with summary_state, summary, and intentions populated (e.g. from label_episodes_gpt54.py or the labels-only phase). Default input directory: labeling/output/gpt54/ with layout <input_dir>/<game>/episode_*.json.
Output: Default output directory: labeling/output/gpt54_skillbank/. No episode JSONs are written unless --save_annotated is used.
labeling/output/gpt54_skillbank/
├── tetris/
│ ├── skill_bank.jsonl # persistent skill bank (contracts)
│ ├── skill_catalog.json # RAG-friendly skill catalog (per-game)
│ ├── sub_episodes.json # SubTask_Experience instances
│ ├── extraction_summary.json # per-game stats (episodes, skills, sub_episodes)
│ └── reports/ # skill extraction diagnostics (if any)
├── candy_crush/
│ └── ...
├── skill_archetypes.json # cross-game archetype aggregation
├── skill_rag_index.json # flat RAG index (archetypes + instances)
├── extraction_batch_summary.json # overall run stats
└── skill_catalog_all.json # combined per-game catalog
| Option | Default | Description |
|---|---|---|
| Input | labeling/output/gpt54 |
Directory with game sub-folders; each game folder contains episode_*.json (labeled, with intentions). |
| Output | labeling/output/gpt54_skillbank |
Root for per-game skill banks, catalogs, sub_episodes, and cross-game archetypes. |
--save_annotated |
off | If set, writes episode JSONs with the skills field populated into the per-game output folder. |
The extract_skillbank_gpt54.py script (and the equivalent pipeline in label_episodes_with_skills.py) runs four phases:
Phase 1 — Annotation (same as label_episodes_gpt54.py)
└─ for each step:
├─ summary_state = build_rag_summary() # deterministic
├─ summary = summary_state + LLM note # 1 LLM call
└─ intentions = [TAG] phrase # 1 LLM call
Phase 2 — Skill Extraction (per game, via SkillBankAgent)
├─ Stage 1: Boundary proposal (predicate flips, events, change-points)
├─ Stage 2: Skill decoding (preference-learned scorer + Viterbi DP)
├─ Stage 3: Contract learning (eff_add / eff_del / eff_event)
├─ Materialize NEW: promote __NEW__ segments to named skills
├─ GPT-5.4 naming: generate skill_name + RAG summary per skill
└─ Annotate: populate skills field on each experience
Phase 3 — Cross-Game Archetype Aggregation (runs after all games)
├─ Group skills by dominant SUBGOAL_TAG across all games
├─ GPT-5.4: generate archetype name, description, transfer summary
├─ skill_archetypes.json # archetype → game instances
└─ skill_rag_index.json # flat index for vector store
Phase 4 — Persistence
├─ <game>/skill_bank.jsonl # per-game JSONL skill bank
├─ <game>/skill_catalog.json # per-game RAG catalog
├─ skill_catalog_all.json # combined per-game catalog
└─ labeling_batch_summary.json # run statistics
When skill extraction is enabled, each experience gets a skills dict:
{
"skill_id": "S_new_1741779200_0",
"skill_name": "Clear Bottom Rows",
"skill_summary": "game=tetris | skill=clear_bottom | effects=rows_cleared,stack_lowered | context=when holes accumulate in bottom rows",
"description": "Targets bottom-row line clears in Tetris when holes accumulate below row 10. Use when stack height exceeds 12 and holes concentrate in the lower half.",
"segment_start": 30,
"segment_end": 45,
"eff_add": ["rows_cleared", "stack_lowered"],
"eff_del": ["holes_bottom"],
"eff_event": ["line_clear"]
}The skill_catalog.json is designed for easy ingestion into a vector store:
{
"game": "tetris",
"model": "gpt-5.4",
"n_skills": 5,
"skills": [
{
"skill_id": "S_new_1741779200_0",
"name": "Clear Bottom Rows",
"summary": "game=tetris | skill=clear_bottom | effects=rows_cleared,stack_lowered | context=holes in lower half",
"description": "Targets bottom-row line clears when holes accumulate below row 10.",
"eff_add": ["rows_cleared", "stack_lowered"],
"eff_del": ["holes_bottom"],
"eff_event": ["line_clear"],
"n_instances": 12,
"version": 1
}
]
}Each skill's summary field uses the same key=value format as summary_state,
optimised for RAG embedding retrieval. The description field provides natural
language for human readability and broader semantic matching.
The persistent skill bank is written as one JSON object per line in
<game>/skill_bank.jsonl. Each line has the form
{"skill": <Skill>, "report": <VerificationReport>}. The Skill object is what
the decision agent and skill pipeline use.
Key fields on each skill:
| Field | Purpose |
|---|---|
skill_id, name, version |
Identity and display. |
strategic_description |
Short “what this skill does and when to use it”; shown in the agent prompt (first ~90 chars). Should be a complete sentence — avoid mid-sentence truncation. |
tags |
Dominant intention tag(s), e.g. ["CLEAR"], ["MERGE"]. Populated from intention-based segmentation. |
protocol |
preconditions, steps, success_criteria, abort_criteria, expected_duration. The decision agent follows steps as the execution plan and uses success_criteria (or execution_hint.termination_cues) to know when the skill is “done”. |
contract |
eff_add, eff_del, eff_event, n_instances. Effects contract; used for verification and for select_skill expected effects. |
sub_episodes |
List of SubEpisodeRef (pointers into rollout storage with seg_start/seg_end, summary, outcome). Used for confidence and success rate; must be linked by the extraction pipeline. |
expected_tag_pattern |
Intention tags expected when this skill is active; populated from the same tag as tags. |
execution_hint |
Optional: termination_cues, common_failure_modes, execution_description. The decision agent uses termination_cues for the “done:” line in the skill list when present. |
n_instances, retired |
Instance count and retirement flag. |
What the decision agent reads (see decision_agents/agent_helper.py):
- Prompt (skill list):
name,strategic_description(truncated),protocol.preconditions(first 2),protocol.expected_duration,confidence(fromn_instancesandsub_episodesoutcomes). For “done:” it usesexecution_hint.termination_cuesif set, elseprotocol.success_criteria. - On skill selection: Full
protocol(steps, preconditions, success/abort criteria),execution_hint, and derived expected effects from the contract.
Notes:
- Re-run extraction after pipeline changes. The bank is generated by
extract_skillbank_gpt54.py. Any change that fixes truncation, addssub_episodeslinking, fillstags/expected_tag_pattern, or generatesexecution_hintrequires re-running the script to refreshskill_bank.jsonl. - Interface (env/game) is not on the skill. Each experience inside
sub_episodes.jsonhas aninterface(e.g.env_name,game_name). The skill record itself does not store interface; the decision agent infers game from context or from the environment. - Avoid cut-off text. Descriptions and protocol steps are generated by the LLM with token limits. The pipeline trims incomplete last sentences in steps/preconditions/success_criteria and uses fallbacks when success_criteria or eff_add/eff_event are empty so the agent always has usable “when” and “done” guidance.
Skills are aggregated across games by their dominant SUBGOAL_TAG. Each
archetype represents an abstract strategic pattern with game-specific instances:
{
"n_archetypes": 8,
"n_total_skills": 23,
"n_games": 4,
"archetypes": [
{
"archetype_id": "archetype_survive",
"tag": "SURVIVE",
"name": "Emergency Resource Recovery",
"description": "Emergency pattern triggered when a critical resource is nearly depleted. Prioritises immediate relief over long-term optimisation.",
"transfer_summary": "archetype=survive | pattern=emergency_recovery | trigger=resource_critical | strategy=prioritize_immediate_relief | games=tetris,2048,candy_crush",
"games": ["tetris", "2048", "candy_crush"],
"n_skills": 5,
"instances": [
{
"game": "tetris",
"skill_id": "S_new_...",
"skill_name": "Clear Bottom Rows",
"summary": "game=tetris | skill=clear_bottom | effects=rows_cleared,stack_lowered",
"description": "Targets bottom-row line clears when holes accumulate."
},
{
"game": "2048",
"skill_id": "S_new_...",
"skill_name": "Emergency Merge",
"summary": "game=2048 | skill=emergency_merge | effects=space_created",
"description": "Force merges to create space when board is nearly full."
}
]
}
]
}Key design: Per-game skill banks stay isolated (game-specific predicates don't mix). Archetypes are a read-only overlay that groups skills by strategic intent for cross-game transfer during RAG retrieval.
A flat list of entries ready for vector-store ingestion. Contains two entry types:
archetype: cross-game pattern withtransfer_summaryfor retrievalskill_instance: game-specific skill linked to its parent archetype
{
"n_entries": 31,
"entries": [
{
"id": "archetype_survive",
"type": "archetype",
"tag": "SURVIVE",
"name": "Emergency Resource Recovery",
"text": "archetype=survive | pattern=emergency_recovery | trigger=resource_critical | strategy=prioritize_immediate_relief | games=tetris,2048,candy_crush",
"description": "Emergency pattern triggered when a critical resource is nearly depleted.",
"games": ["tetris", "2048", "candy_crush"]
},
{
"id": "archetype_survive_tetris_S_new_...",
"type": "skill_instance",
"tag": "SURVIVE",
"archetype": "archetype_survive",
"game": "tetris",
"name": "Clear Bottom Rows",
"text": "game=tetris | skill=clear_bottom | effects=rows_cleared,stack_lowered",
"description": "Targets bottom-row line clears when holes accumulate."
}
]
}Use the text field as the embedding content and type field to filter
between archetypes (cross-game patterns) and instances (game-specific skills).
label_episode()
└─ for each step:
├─ summary_state = build_rag_summary() # deterministic, 0 LLM tokens
│ ├─ extract_game_facts() # game-specific parsers
│ └─ estimate_game_phase() # opening/midgame/endgame
│
├─ delta = _compute_state_delta(prev, curr) # what changed since last step
│
├─ summary = summary_state + LLM note # 1 LLM call, ≤25 tokens
│ └─ prompt includes delta → note is specific to the change
│
└─ intentions = [TAG] phrase # 1 LLM call, ≤40 tokens
├─ prompt includes delta + urgency hints
└─ tag shifts when situation changes significantly
main()
├─ Phase 1: load labeled episodes # from labeling/output/gpt54/
│
├─ Phase 2: extract_skills_for_game() # per game
│ ├─ dict_to_episode() # convert JSON → Episode objects
│ ├─ SkillBankAgent.segment_episode() # Stage 1+2 per episode
│ │ ├─ boundary_proposal (Stage 1) # candidate cut points
│ │ └─ infer_segmentation (Stage 2) # preference-learned decoding
│ ├─ SkillBankAgent.run_contract_learning() # Stage 3 effects contracts
│ ├─ SkillBankAgent.materialize_new_skills() # promote __NEW__ → named skills
│ ├─ generate_skill_name() # GPT-5.4 name + RAG summary
│ ├─ generate_skill_description() # GPT-5.4 description
│ └─ annotate_episodes_with_skills() # populate skills field
│
├─ Phase 3: aggregate_cross_game_archetypes() # runs AFTER all games
│ ├─ extract_dominant_tag() # classify by SUBGOAL_TAG
│ ├─ Group skills by tag across games # SURVIVE, CLEAR, etc.
│ ├─ GPT-5.4: archetype name + transfer summary # cross-game RAG text
│ ├─ skill_archetypes.json # structured archetypes
│ └─ skill_rag_index.json # flat vector-store index
│
└─ Phase 4: save outputs
├─ <game>/skill_bank.jsonl
├─ <game>/skill_catalog.json
├─ skill_catalog_all.json
└─ extraction_batch_summary.json
The labeled episodes follow the same Episode / Experience schema from
data_structure/experience.py and can be loaded directly:
from data_structure.experience import Episode
import json
# Load labeled episode (with or without skills)
with open("labeling/output/gpt54_skills/tetris/episode_000.json") as f:
ep = Episode.from_dict(json.load(f))
for exp in ep.experiences:
print(exp.summary_state) # structured key=value facts for RAG
print(exp.summary) # summary_state + strategic note
print(exp.intentions) # [TAG] subgoal phrase
print(exp.skills) # skill assignment dict (or None)import json
with open("labeling/output/gpt54_skills/tetris/skill_catalog.json") as f:
catalog = json.load(f)
for skill in catalog["skills"]:
# Index these into your vector store
print(skill["skill_id"], skill["name"])
print(f" summary: {skill['summary']}")
print(f" description: {skill['description']}")import json
# Load the flat RAG index — ready for vector store ingestion
with open("labeling/output/gpt54_skills/skill_rag_index.json") as f:
index = json.load(f)
for entry in index["entries"]:
if entry["type"] == "archetype":
# Cross-game pattern — embed entry["text"] for broad retrieval
print(f"[ARCHETYPE] {entry['name']}: {entry['text']}")
else:
# Game-specific instance — embed for precise in-game retrieval
print(f" [{entry['game']}] {entry['name']}: {entry['text']}")
# Or load the structured archetypes for programmatic access
with open("labeling/output/gpt54_skills/skill_archetypes.json") as f:
archetypes = json.load(f)
for arch in archetypes["archetypes"]:
print(f"\n[{arch['tag']}] {arch['name']} — {arch['n_games']} games, {arch['n_skills']} skills")
for inst in arch["instances"]:
print(f" {inst['game']}: {inst['skill_name']}")from skill_agents.pipeline import SkillBankAgent, PipelineConfig
from skill_agents.skill_bank.bank import SkillBankMVP
# Load the extracted skill bank
bank = SkillBankMVP(path="labeling/output/gpt54_skills/tetris/skill_bank.jsonl")
bank.load()
agent = SkillBankAgent(bank=bank)
result = agent.query_skill("clear bottom rows to reduce holes")| Function | Source | Role in Labeling |
|---|---|---|
build_rag_summary() |
agent_helper.py |
Deterministic key=value summary from game-aware fact extraction |
extract_game_facts() |
agent_helper.py |
Game-specific parsers (Tetris holes, 2048 tiles, Candy score, etc.) |
compact_text_observation() |
agent_helper.py |
Fallback state pre-compression when game-specific extraction is sparse |
get_state_summary() |
agent_helper.py |
Structured/text state summarisation backbone |
infer_intention() |
agent_helper.py |
Fallback intention inference when GPT-5.4 call fails |
strip_think_tags() |
agent_helper.py |
Strip <think> blocks from reasoning model output |
SUBGOAL_TAGS |
agent_helper.py |
Canonical list of 13 subgoal tag categories |
HARD_SUMMARY_CHAR_LIMIT |
agent_helper.py |
Maximum character limit for summary strings |
| Function / Class | Source | Role in Skill Extraction |
|---|---|---|
SkillBankAgent |
skill_agents/pipeline.py |
Orchestrates the full skill extraction pipeline (Stages 1–4) |
PipelineConfig |
skill_agents/pipeline.py |
Configuration for all pipeline stages |
SkillBankMVP |
skill_agents/skill_bank/bank.py |
Persistent effects-only contract storage (JSONL) |
SkillEffectsContract |
skill_agents/stage3_mvp/schemas.py |
Per-skill effects contract (eff_add, eff_del, eff_event) |
SegmentRecord |
skill_agents/stage3_mvp/schemas.py |
Enriched segment with predicate summaries and effects |
SkillQueryEngine |
skill_agents/query.py |
RAG-based skill retrieval and selection |