Trajectory Format
A trajectory is the complete record of a single agent run. Saved as JSON via --output-dir, trajectories can be re-graded, compared, and analyzed.
Top-level structure
Section titled “Top-level structure”interface Trajectory { id: string; // Unique run identifier stimulus: Stimulus; // The prompt + config that produced this events: TrajectoryEvent[]; // Flat array of typed events metrics: TrajectoryMetrics; // Computed aggregates output: string; // Final agent output text workDir: string; // Workspace path during run artifactDir?: string; // Artifact dir outside the workspace; some graders (e.g. custom-metrics) read it before workDir artifactDirStrict?: boolean; // When true, artifactDir is authoritative: artifact-reading graders do NOT fall back to workDir diff?: string; // Cumulative workspace diff diffPath?: string; // Cumulative diff sidecar path turnDiffs?: Array< { turn: number } & ({ diff: string } | { diffPath: string } | { error: string }) >; metadata: TrajectoryMetadata; // Execution context raw?: unknown; // Escape hatch: unconverted source doc (e.g. raw ATIF). In-process only — stripped from JSONL output, never persisted.}Workspace diff artifacts
Section titled “Workspace diff artifacts”diff / diffPath represent the cumulative workspace change from the start of a run to its
final state. Multi-turn runs that use a turn-scoped static diff grader also persist turnDiffs:
one 0-based record per turn Vally attempted to bound, containing exactly one of an inline diff,
a relative diffPath sidecar, or an error when Vally could not safely capture that boundary.
These diffs may contain raw workspace content and should be treated as sensitive.
An absent, malformed, or error turn record fails a scoped diff grader; Vally never substitutes
the cumulative diff except for turn 0 when both the caller-supplied and recorded stimuli are
explicitly single-turn, where the two are equivalent by construction. JSONL sidecar paths resolve relative to
the run directory supplied to
vally grade with --run-dir.
Library users calling sliceTrajectoryToTurn(trajectory, turn) receive an event/output/metrics
view that preserves the trajectory’s other fields, including cumulative diff evidence. The
grading pipeline uses a separate internal projection for turn-scoped static diff graders, which
strips cumulative diff sources before injecting only the selected turnDiffs record.
raw — the escape hatch
Section titled “raw — the escape hatch”raw carries the original, unconverted source document a trajectory was built
from (e.g. the raw ATIF object) for custom graders that need fields the
normalized events array doesn’t capture. It is populated only by offline
adapters like fromAtif — live executor runs leave it undefined — and is
stripped from JSONL output, so it reaches graders during a run but is never
persisted. Because it isn’t persisted, a grader that reads raw works only on
the live pass: re-grading from JSONL (vally grade <file.jsonl>) will see
undefined.
artifactDir / artifactDirStrict — where artifact graders read
Section titled “artifactDir / artifactDirStrict — where artifact graders read”artifactDir is a directory of run artifacts written outside the graded
workDir (e.g. a benchmark harness’s output dir). Artifact-reading graders such
as custom-metrics look up their files
there first, then fall back to workDir when the file is absent from the
artifact dir. Live executor runs set artifactDir and keep this lenient
fallback.
artifactDirStrict makes artifactDir authoritative: when it is true,
those graders do not fall back to workDir, so a file missing from the
artifact dir fails the grader instead of silently resolving against a (possibly
stale) workspace copy. It is set by grade --artifact-dir
for offline re-grade, where the workspace is usually absent or stale and a
silent fallback would report a misleading result. Unlike raw, it is a normal
additive field that round-trips through JSONL.
Events
Section titled “Events”Events are a discriminated union on the type field. Every event has a timestamp.
tool_call
Section titled “tool_call”Agent invoked a tool.
{ "type": "tool_call", "timestamp": "2025-01-15T10:30:00.000Z", "data": { "toolName": "write_file", "toolCallId": "call_abc123", "turnId": "0", "arguments": { "path": "add.test.js", "content": "..." } }}turnId (optional) identifies the model-response turn that issued the call, matching the
surrounding turn_start/turn_end events when present (subagent-inlined calls carry a
turn id but no surrounding boundaries). Graders use it to group parallel tool-call
batches by turn identity (see the tool-calls grader);
it is absent for executors that don’t carry a turn id.
When tool-call simulation intercepts a call, the
event carries "simulated": true so graders and reports can distinguish it from a
real invocation.
tool_result
Section titled “tool_result”Tool returned a result.
{ "type": "tool_result", "timestamp": "2025-01-15T10:30:01.000Z", "data": { "toolName": "write_file", "toolCallId": "call_abc123", "success": true, "result": "File written successfully" }}token_usage
Section titled “token_usage”LLM token counts from a single API call.
{ "type": "token_usage", "timestamp": "2025-01-15T10:30:00.500Z", "data": { "inputTokens": 1500, "outputTokens": 350, "model": "gpt-5.5", "cacheReadTokens": 200, "cacheWriteTokens": 0, "cost": { "provider": "github-copilot", "unit": "nano-aiu", "amount": 1250000000 } }}Copilot SDK sources can include an optional provider-labelled cost. Live SDK events report a request cost; saved Copilot logs are reconstructed from per-model session totals. Other executors and older Copilot logs may omit cost.
cost_unavailable
Section titled “cost_unavailable”The source could not provide a complete usage summary for a provider and unit.
This event prevents tokenUsage.cost from being reported as a misleading
subtotal while preserving token metrics from other valid events.
{ "type": "cost_unavailable", "timestamp": "2025-01-15T10:30:00.500Z", "data": { "provider": "github-copilot", "unit": "nano-aiu", "model": "gpt-5.5", "reason": "malformed_usage" }}turn_start / turn_end
Section titled “turn_start / turn_end”Conversation turn boundaries.
{ "type": "turn_start", "timestamp": "...", "data": { "turnId": "turn-1" } }{ "type": "turn_end", "timestamp": "...", "data": { "turnId": "turn-1" } }assistant_message / user_message
Section titled “assistant_message / user_message”Text messages in the conversation.
{ "type": "assistant_message", "timestamp": "...", "data": { "content": "I'll write tests for..." } }{ "type": "user_message", "timestamp": "...", "data": { "content": "Write tests for add()" } }skill_activation
Section titled “skill_activation”A skill was loaded by the agent.
{ "type": "skill_activation", "timestamp": "...", "data": { "name": "test-writer", "path": "/skills/test-writer/SKILL.md", "pluginName": "copilot", "allowedTools": ["write_file", "read_file"] }}reasoning
Section titled “reasoning”The agent’s reasoning (thinking) for a step, when the source captures it. Emitted before the step’s assistant_message.
{ "type": "reasoning", "timestamp": "...", "data": { "content": "First I'll inspect the failing test, then trace the null deref." }}system
Section titled “system”A typed system-level event, such as context compaction. eventType carries the semantic kind (e.g. "compaction") so graders can match on it rather than parsing free text. message and observation are optional and omitted when the source provides no text.
A system event is emitted only when the source marks the step with a typed kind (from ATIF extra.context_management.type). Other system steps remain custom events, so consumers should not expect every system step to appear as a system event.
{ "type": "system", "timestamp": "...", "data": { "eventType": "compaction", "message": "Compacting 40 messages", "observation": "Reduced to 12 messages, removed 8000 tokens" }}Something went wrong during the run.
{ "type": "error", "timestamp": "...", "data": { "message": "Request timed out", "type": "TimeoutError", "code": 408 }}Metrics
Section titled “Metrics”Computed from events after the run completes:
interface TrajectoryMetrics { tokenUsage: { inputTokens: number; outputTokens: number; totalTokens: number; cacheReadTokens: number; cacheWriteTokens: number; callCount: number; cost?: ProviderCost; byModel: Record< string, { inputTokens: number; outputTokens: number; callCount: number; } >; }; toolCallCount: number; toolCallBreakdown: Record<string, number>; // { "write_file": 3, "read_file": 1 } simulatedToolCallCount: number; // tool calls that were simulated, not run for real skillActivationCount: number; skillActivationBreakdown: Record<string, number>; // { "test-writer": 1 } turnCount: number; wallTimeMs: number; errorCount: number;}
type ProviderCost = { provider: "github-copilot"; unit: "nano-aiu"; amount: number;};tokenUsage.cost is the provider-labelled usage cost for the evaluated agent
across the trajectory’s LLM calls. It is present only when every
token_usage event reports the same provider and unit with a safe,
non-negative integer amount; an explicit 0 is preserved, while an omitted
field means the complete total is unknown. It does not include LLM grader or
judge usage, and Vally does not convert nano-AI units to displayed AI Credits.
Metadata
Section titled “Metadata”interface TrajectoryMetadata { model: string; // Model used for execution skillsLoaded: string[]; // Names of skills that were loaded startedAt: Date; completedAt: Date; executor: string; // Which executor ran this sessionID: string;}Working with trajectories
Section titled “Working with trajectories”# Save trajectories (one trial-result record per trial, in results.jsonl)vally eval --eval-spec eval.yaml --output-dir ./vally-results
# Re-grade trajectories from a previous runcat ./vally-results/*/results.jsonl | vally grade --eval-spec eval.yaml
# Inspect trial trajectories with jq. `results.jsonl` interleaves# `trial-result` records (with a nested `trajectory` field) with a final# `run-summary` line, so filter on `.type == "trial-result"` first.cat ./vally-results/*/results.jsonl | jq 'select(.type == "trial-result") | .trajectory.metrics'cat ./vally-results/*/results.jsonl | jq 'select(.type == "trial-result") | .trajectory.events[] | select(.type == "tool_call") | .data.toolName'cat ./vally-results/*/results.jsonl | jq 'select(.type == "trial-result") | .trajectory.events | length'