Skip to content

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.

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.
}

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 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 are a discriminated union on the type field. Every event has a timestamp.

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 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"
}
}

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.

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"
}
}

Conversation turn boundaries.

{ "type": "turn_start", "timestamp": "...", "data": { "turnId": "turn-1" } }
{ "type": "turn_end", "timestamp": "...", "data": { "turnId": "turn-1" } }

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()" } }

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"]
}
}

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." }
}

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
}
}

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.

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;
}
Terminal window
# 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 run
cat ./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'