Trajectory Export
Prax automatically exports every conversation turn as a training trajectory in real time. This feeds the fine-tuning pipeline (vLLM + QLoRA) with high-quality, outcome-tagged data without any manual batch processing.
f7a8cb56
View source ↗
Prax automatically exports every conversation turn as a training trajectory in real time. This feeds the fine-tuning pipeline (vLLM + QLoRA) with high-quality, outcome-tagged data without any manual batch processing.
How it works
User message → Orchestrator → Agent processes → Response delivered
↓
export_trajectory()
↓
completed.jsonl or failed.jsonl
At the end of every turn, the orchestrator calls export_trajectory() which:
- Classifies the outcome (success, correction, negative, tool_failure, empty)
- Builds a ChatML training example with context
- Appends one line to the appropriate JSONL file in the user’s workspace
This is fire-and-forget — it never blocks the response and silently recovers from errors.
Output format
Trajectories use ChatML format — the standard for OpenAI-compatible fine-tuning:
{
"messages": [
{"role": "system", "content": "You are Prax, a capable AI assistant."},
{"role": "user", "content": "What's the weather?"},
{"role": "assistant", "content": "Let me check... It's 72°F in LA."}
],
"metadata": {
"user_id": "12345",
"outcome": "success",
"timestamp": "2026-04-03T20:00:00+00:00",
"agent": "Prax",
"tool_count": 1
}
}
Each line in the JSONL file is one complete training example. The metadata field is stripped before fine-tuning — it’s for filtering and analysis.
Why ChatML?
| Format | Structure | Supported by |
|---|---|---|
| ChatML | {"messages": [{"role": "...", "content": "..."}]} |
OpenAI, Unsloth, Axolotl, LLaMA-Factory, vLLM |
| ShareGPT | {"conversations": [{"from": "human", "value": "..."}]} |
Axolotl, LLaMA-Factory |
| Alpaca | {"instruction": "...", "input": "...", "output": "..."} |
Most frameworks |
ChatML is the most widely supported format and maps directly to how LLMs process multi-turn conversations. It’s also what Prax’s existing fine-tuning pipeline uses (Unsloth + QLoRA), so no conversion is needed.
File locations
{RECORDS_DIR}/users/{workspace}/trajectories/ (was {workspace}/{user_id}/.prax/trajectories/)
├── completed.jsonl — successful exchanges
└── failed.jsonl — corrections, tool failures, complaints
Full multi-agent trajectories (FULL_TRAJECTORIES_ENABLED)
The files above keep the user’s message and Prax’s final reply, and nothing in between. Training on Prax’s own multi-agent traffic needs the rest:
- the orchestrator’s tool calls and delegations;
- every spoke’s own conversation.
In the one multi-agent training study we have assessed (MASS), sub-agent conversations were 2,054 of 2,435 training windows.
With FULL_TRAJECTORIES_ENABLED=true (off by default), each turn also
appends one line per agent to agents.jsonl, in the same directory:
- Format: OpenAI chat format, with
tool_callson assistant messages andtoolmessages for results. - Metadata:
role(orchestratororspoke:<name>),outcome, the turn’ssession_idandtrace_id, and the agent’sspan_id. - Linking: a turn’s lines share a
trace_id, so an orchestrator line and the spoke lines it delegated to can be paired. - Secrets: this turn’s secret values are masked, in arguments and results alike.
- Size: tool results longer than 16,000 characters are clipped.
- Scheduled turns are kept, and marked
scheduled.
The file holds the content of conversations and fetched pages, like the trace log, and lives with the user’s other records.
Before training on it:
- Rank by a grounded signal, such as the user’s acceptance or a verifiable check, never by the model judging its own output. MASS’s self-ranked data cost it the two benchmarks with executable checks.
- Run a prompt-only control. The same paper found that telling the base model to organize a team captured most of what training bought.
Outcome classification
Each trajectory is tagged with one of:
| Outcome | File | Trigger |
|---|---|---|
success |
completed.jsonl | Normal exchange, no issues detected |
correction |
failed.jsonl | User corrected the agent (“no, I meant…”, “that’s wrong”) |
negative |
failed.jsonl | User expressed dissatisfaction (“doesn’t work”, “useless”) |
tool_failure |
failed.jsonl | A tool returned an error during the turn |
empty_response |
failed.jsonl | Agent produced no output |
What gets skipped
- Scheduled task prompts (
[SCHEDULED_TASK ...]) - Reminder deliveries (
[Reminder] ...) - Anonymous/no-user messages
Using trajectories for fine-tuning
Approach 1: Train on successes only
# Use completed.jsonl directly — every line is a good example.
# scripts/finetune_train.py is the training entry point (Unsloth QLoRA, needs a GPU);
# --base-model, --data and --output are required.
uv run python scripts/finetune_train.py --base-model <hf-model-id> --data completed.jsonl --output <adapter-dir>
There is no python -m prax.services.finetune_service CLI — the service calls the
same script as a subprocess. Inside Prax, the finetune_* tools (behind
FINETUNE_ENABLED=true) drive it.
Approach 2: Train on corrections (DPO/ORPO)
Failed trajectories contain the wrong response. When paired with the corrected follow-up (which becomes a success trajectory), you can build preference pairs for DPO (Direct Preference Optimization):
- Chosen: the corrected response (from completed.jsonl)
- Rejected: the original wrong response (from failed.jsonl)
Approach 3: Filter by metadata
import json
# Only high-quality examples (success + used tools)
with open("completed.jsonl") as f:
examples = [json.loads(line) for line in f]
rich = [e for e in examples if e["metadata"]["tool_count"] > 0]
Session grouping
Conversations are multi-turn workflows, not isolated exchanges. “Research X → write a note → schedule a reminder” is one coherent session spanning 3+ turns. Training on individual turns loses this thread.
How it works
At the start of each turn, a cheap LLM classifies whether the new message continues the current session or starts a new one:
Turn 1: "Research quantum computing" → session abc123 (new)
Turn 2: "Write a note summarizing that" → session abc123 (continuation)
Turn 3: "Schedule a reminder to revisit" → session abc123 (continuation)
Turn 4: "What's the weather?" → session def456 (new topic)
Classification rules (in order):
- No existing session → always new
- Time gap > 30 minutes → always new
- LLM classification: “Is this a continuation of: {topic summary}?” → yes/no
After each turn, the topic summary is updated via LLM so the next classification has accurate context.
Session IDs in trajectories
Each trajectory line includes session_id in its metadata:
{"messages": [...], "metadata": {"session_id": "abc123", "outcome": "success", ...}}
Assembling multi-turn training examples
import json
from collections import defaultdict
# Group by session
sessions = defaultdict(list)
for filepath in ["completed.jsonl", "failed.jsonl"]:
with open(filepath) as f:
for line in f:
ex = json.loads(line)
sid = ex["metadata"].get("session_id", "")
if sid:
sessions[sid].append(ex)
# Build multi-turn training examples
for sid, turns in sessions.items():
if len(turns) < 2:
continue
# Concatenate all turns into one training example
multi_turn = {"messages": [turns[0]["messages"][0]]} # system prompt
for turn in turns:
multi_turn["messages"].extend(turn["messages"][1:]) # user + assistant
# Use for fine-tuning...
Correcting session assignments
If the LLM misclassifies a session boundary, users can reassign traces in the Execution Graph panel:
- Hover over a trace → click the ↔ (move) icon
- Type or paste the target session ID
- Press Enter
The change is persisted to disk and reflected in trajectory metadata.
Relationship to existing correction harvesting
Prax has two training data pipelines:
| Pipeline | When it runs | What it captures | Best for |
|---|---|---|---|
| Trajectory export (new) | Real-time, every turn | All exchanges with outcome tags | Continuous training, broad coverage |
| Correction harvester (existing) | Batch, on-demand | Only user corrections from history | Targeted error correction |
They complement each other. Trajectory export captures everything with no delay. Correction harvesting does deeper analysis of conversation history to find correction patterns that the real-time classifier might miss.
Configuration
Trajectory export is always on when FINETUNE_ENABLED=true. No additional configuration needed. Files accumulate in the workspace and can be cleared manually or via the workspace file browser.
To check trajectory counts:
from prax.services.trajectory_service import get_trajectory_stats
stats = get_trajectory_stats(user_id)
# {"completed": 142, "failed": 8}