Conduct Feature: Wander Fine-Tuning Data Flywheel
Background
Wander is a Sierra Online-style adventure game engine that uses Conduct task routing for flexible player input evaluation (wander_eval). Instead of 1980s-style strict string matching, Conduct evaluates whether a player’s input advances the story, represents valid creative alternatives, or misses the mark — scoring on a gradient rather than binary pass/fail.
Every Wander session already generates (scene_context, player_input, eval_score, model, note) tuples as a side effect of gameplay. This document describes how to capture, structure, and leverage that data as a fine-tuning pipeline targeting a smaller, faster, game-specific wander_eval model.
The Problem
wander_eval currently routes to gemma4:e4b — a capable general-purpose model doing its best to understand Wander’s narrative logic without any game-specific training. As episode count and player sessions grow, a general model becomes the wrong tool: it has no knowledge of world state, character relationships, inventory logic, or the tone and rules of a specific episode.
The Opportunity: A Self-Generating Training Dataset
Wander’s eval structure produces training data that is unusually rich compared to typical game datasets:
| Field |
Value |
scene_description |
What the player sees and knows |
player_input |
The raw input being evaluated |
available_exits |
Valid navigation options |
inventory_state |
Player’s current items |
eval_score |
1–5 from Conduct eval |
model |
Which model produced the eval |
note |
Evaluator rationale |
Most game datasets contain only (input, correct_answer) pairs. This dataset contains scored reasoning across multiple models on the same input — that’s a ranking dataset suitable for DPO (Direct Preference Optimization), not just supervised classification.
Score Interpretation for Training
| Score |
Label |
Training Role |
| 4–5 |
Valid input, advances story or creative alternative |
Positive example |
| 2–3 |
Plausible but wrong, reasonable attempt |
Hard negative |
| 1 |
Nonsensical or completely off-context |
Easy negative |
Shadow eval pairs where one model scores 4+ and another scores 1–2 on the same input become DPO (chosen, rejected) pairs automatically — no additional labeling required.
Proposed Conduct Changes
1. Structured Eval Logging for wander_eval
Eval records are stored in Loki via the Watchtower LGTM stack (Grafana + Loki + Tempo + Mimir + Alloy), with no new infrastructure required. Wander eval events are structured log entries — timestamp, source labels, JSON payload — which maps exactly to Loki’s data model.
Loki Label Strategy
Labels must be low-cardinality:
job="wander_eval"
episode_id="<episode_slug>"
model="<model_name>"
task_type="wander_eval"
Everything else goes in the structured JSON log line, queryable via | json in LogQL.
Log Payload Schema
{
"task_type": "wander_eval",
"episode_id": "string",
"scene_id": "string",
"player_input": "string",
"available_exits": ["string"],
"inventory_state": ["string"],
"eval_score": 1,
"model": "string",
"note": "string",
"shadow_scores": [
{ "model": "string", "score": 1, "note": "string" }
]
}
Note: scene_description is intentionally excluded from the log line to keep chunk sizes manageable. Scenes are resolved by scene_id from the episode’s SQLite package at query time.
Ingestion
Push directly from the Conduct wander_eval worker to Loki’s push API:
import requests, json, time
def log_wander_eval(payload: dict):
requests.post("http://localhost:3100/loki/api/v1/push", json={
"streams": [{
"stream": {
"job": "wander_eval",
"episode_id": payload["episode_id"],
"model": payload["model"],
"task_type": "wander_eval"
},
"values": [[str(time.time_ns()), json.dumps(payload)]]
}]
})
Alloy dual-writes to Sumo Logic alongside Loki automatically — no additional config needed.
Grafana Dashboards
- Eval score distributions per episode and model
- Average score, latency, and cost by model over time
- DPO pair mining:
{job="wander_eval"} | json | score >= 4 vs score <= 2 on the same scene_id
HuggingFace Export
When ready to fine-tune, query Loki via /loki/api/v1/query_range with a LogQL filter and export to JSONL. Loki is the source of truth; HuggingFace receives periodic exports rather than being the primary store.
2. HuggingFace Dataset Push
When sufficient eval data has accumulated in Loki, export to HuggingFace as a private dataset repo. A small Python script queries Loki’s HTTP API and pushes JSONL incrementally — treat each push like a git commit for data.
import requests, json
def export_loki_to_jsonl(output_path: str, min_score: int = 1):
resp = requests.get("http://localhost:3100/loki/api/v1/query_range", params={
"query": '{job="wander_eval"} | json',
"limit": 5000,
"start": 0,
"end": int(time.time()) * 1_000_000_000
})
with open(output_path, "w") as f:
for stream in resp.json()["data"]["result"]:
for _, line in stream["values"]:
f.write(line + "\n")
# Then push to HuggingFace
# huggingface-cli upload wander-eval-dataset ./wander_eval_export.jsonl
3. Fine-Tuning Target
Do not fine-tune a large model. The goal is a 3B–7B model that deeply understands Wander’s world and runs locally in milliseconds — outperforming a general 31B on this specific task.
Recommended approach:
- Method: QLoRA via
unsloth (MLX backend for M5 Max)
- Base model:
llama3.2:3b or gemma4:e2b (already in the Ollama stack)
- Training data: DPO pairs derived from shadow eval score differentials
- Checkpoints: Committed to HuggingFace model repo at regular intervals
4. Deployment Back into Conduct
Once fine-tuned:
-
Merge LoRA adapter into base weights
-
Export to GGUF
-
Import into Ollama via Modelfile:
FROM /path/to/wander-eval-ft.gguf
-
Update wander_eval routing rule to use the fine-tuned model as primary
-
Retain general model (e.g. gemma4:e4b) as a shadow for edge cases and ongoing data collection
5. Variability Input for wander_eval
Add variability and seed to the inputs bag for wander_eval to support:
- Deterministic evaluation (
variability: "low", fixed seed) for regression testing specific scenes
- Exploratory evaluation (
variability: "high") when probing edge cases or ambiguous inputs
{
"inputs": {
"variability": "low",
"seed": 42
}
}
The Flywheel
Wander gameplay
→ wander_eval jobs with shadow scoring
→ structured JSONL log with DPO pairs
→ HuggingFace dataset (incremental push)
→ QLoRA fine-tune on 3B base
→ GGUF → Ollama → new wander_eval primary
→ better evals → better gameplay → more data
Every session makes the eval model smarter. The game engine generates its own training data as a byproduct of being played.
Priority Order
- Structured logging — highest leverage, no model work required, starts accumulating data immediately
- HuggingFace dataset push — low effort, enables future fine-tuning at any time
- Variability/seed inputs — useful for
wander_eval and generalizes to all Conduct task types
- Fine-tuning pipeline — once sufficient eval data exists (rough target: 1,000+ scored examples)
- Routing rule update — swap fine-tuned model in as primary once benchmarks justify it
Conduct Feature: Wander Fine-Tuning Data Flywheel
Background
Wander is a Sierra Online-style adventure game engine that uses Conduct task routing for flexible player input evaluation (
wander_eval). Instead of 1980s-style strict string matching, Conduct evaluates whether a player’s input advances the story, represents valid creative alternatives, or misses the mark — scoring on a gradient rather than binary pass/fail.Every Wander session already generates (scene_context, player_input, eval_score, model, note) tuples as a side effect of gameplay. This document describes how to capture, structure, and leverage that data as a fine-tuning pipeline targeting a smaller, faster, game-specific
wander_evalmodel.The Problem
wander_evalcurrently routes togemma4:e4b— a capable general-purpose model doing its best to understand Wander’s narrative logic without any game-specific training. As episode count and player sessions grow, a general model becomes the wrong tool: it has no knowledge of world state, character relationships, inventory logic, or the tone and rules of a specific episode.The Opportunity: A Self-Generating Training Dataset
Wander’s eval structure produces training data that is unusually rich compared to typical game datasets:
scene_descriptionplayer_inputavailable_exitsinventory_stateeval_scoremodelnoteMost game datasets contain only (input, correct_answer) pairs. This dataset contains scored reasoning across multiple models on the same input — that’s a ranking dataset suitable for DPO (Direct Preference Optimization), not just supervised classification.
Score Interpretation for Training
Shadow eval pairs where one model scores 4+ and another scores 1–2 on the same input become DPO (chosen, rejected) pairs automatically — no additional labeling required.
Proposed Conduct Changes
1. Structured Eval Logging for
wander_evalEval records are stored in Loki via the Watchtower LGTM stack (Grafana + Loki + Tempo + Mimir + Alloy), with no new infrastructure required. Wander eval events are structured log entries — timestamp, source labels, JSON payload — which maps exactly to Loki’s data model.
Loki Label Strategy
Labels must be low-cardinality:
Everything else goes in the structured JSON log line, queryable via
| jsonin LogQL.Log Payload Schema
{ "task_type": "wander_eval", "episode_id": "string", "scene_id": "string", "player_input": "string", "available_exits": ["string"], "inventory_state": ["string"], "eval_score": 1, "model": "string", "note": "string", "shadow_scores": [ { "model": "string", "score": 1, "note": "string" } ] }Note:
scene_descriptionis intentionally excluded from the log line to keep chunk sizes manageable. Scenes are resolved byscene_idfrom the episode’s SQLite package at query time.Ingestion
Push directly from the Conduct
wander_evalworker to Loki’s push API:Alloy dual-writes to Sumo Logic alongside Loki automatically — no additional config needed.
Grafana Dashboards
{job="wander_eval"} | json | score >= 4vsscore <= 2on the samescene_idHuggingFace Export
When ready to fine-tune, query Loki via
/loki/api/v1/query_rangewith a LogQL filter and export to JSONL. Loki is the source of truth; HuggingFace receives periodic exports rather than being the primary store.2. HuggingFace Dataset Push
When sufficient eval data has accumulated in Loki, export to HuggingFace as a private dataset repo. A small Python script queries Loki’s HTTP API and pushes JSONL incrementally — treat each push like a git commit for data.
3. Fine-Tuning Target
Do not fine-tune a large model. The goal is a 3B–7B model that deeply understands Wander’s world and runs locally in milliseconds — outperforming a general 31B on this specific task.
Recommended approach:
unsloth(MLX backend for M5 Max)llama3.2:3borgemma4:e2b(already in the Ollama stack)4. Deployment Back into Conduct
Once fine-tuned:
Merge LoRA adapter into base weights
Export to GGUF
Import into Ollama via Modelfile:
Update
wander_evalrouting rule to use the fine-tuned model as primaryRetain general model (e.g.
gemma4:e4b) as a shadow for edge cases and ongoing data collection5. Variability Input for
wander_evalAdd
variabilityandseedto theinputsbag forwander_evalto support:variability: "low", fixedseed) for regression testing specific scenesvariability: "high") when probing edge cases or ambiguous inputs{ "inputs": { "variability": "low", "seed": 42 } }The Flywheel
Every session makes the eval model smarter. The game engine generates its own training data as a byproduct of being played.
Priority Order
wander_evaland generalizes to all Conduct task types