Viewing files at commit 63ed254f5df1Commit detailsView latest

Browse files
scripts/agent_metrics_schema.py 977 lines · 39.1 KB · python
1
#!/usr/bin/env python3
2
"""Emit and validate Oak agent workflow benchmark result row schema.
3
4
The schema is JSON-schema-like but intentionally dependency-free. Running this
5
script with no arguments prints the schema as JSON. Use --validate-jsonl to check
6
JSON or JSONL result rows against the subset of schema rules implemented here.
7
"""
8
9
from __future__ import annotations
10
11
import argparse
12
import json
13
from pathlib import Path
14
from typing import Any, Iterable, List
15
16
SCHEMA_VERSION = 2
17
18
VCS_MODES = [
19
    "git_single_checkout",
20
    "git_worktree_per_task",
21
    "oak_single_mount",
22
    "oak_mount_per_task",
23
    "oak_space_per_task",
24
]
25
26
SUBJECT_KINDS = ["git", "oak", "other"]
27
OUTCOMES = ["pass", "fail", "timeout", "error", "abandoned"]
28
TOKEN_SOURCES = ["provider_reported", "adapter_estimated", "char_only", "mixed", "unknown"]
29
TURN_SOURCES = ["adapter_stream", "estimated", "unmeasured"]
30
INSTRUCTION_LEVELS = ["zero-shot", "cheat-sheet", "full-docs", "adapter-default"]
31
32
# Schema v2 measurement contract: a field that a runner cannot actually measure
33
# MUST be null, never a fabricated zero. Dashboards must render null as
34
# "unmeasured", not as a good result. Each metric group carries an optional
35
# measurement_source describing how its values were obtained.
36
37
38
def string(description: str = "") -> dict[str, Any]:
39
    schema: dict[str, Any] = {"type": "string"}
40
    if description:
41
        schema["description"] = description
42
    return schema
43
44
45
def optional_string(description: str = "") -> dict[str, Any]:
46
    schema: dict[str, Any] = {"type": ["string", "null"]}
47
    if description:
48
        schema["description"] = description
49
    return schema
50
51
52
def non_negative_integer(description: str = "") -> dict[str, Any]:
53
    schema: dict[str, Any] = {"type": "integer", "minimum": 0}
54
    if description:
55
        schema["description"] = description
56
    return schema
57
58
59
def optional_non_negative_integer(description: str = "") -> dict[str, Any]:
60
    schema: dict[str, Any] = {"type": ["integer", "null"], "minimum": 0}
61
    if description:
62
        schema["description"] = description
63
    return schema
64
65
66
def non_negative_number(description: str = "") -> dict[str, Any]:
67
    schema: dict[str, Any] = {"type": "number", "minimum": 0}
68
    if description:
69
        schema["description"] = description
70
    return schema
71
72
73
def optional_non_negative_number(description: str = "") -> dict[str, Any]:
74
    schema: dict[str, Any] = {"type": ["number", "null"], "minimum": 0}
75
    if description:
76
        schema["description"] = description
77
    return schema
78
79
80
def boolean(description: str = "") -> dict[str, Any]:
81
    schema: dict[str, Any] = {"type": "boolean"}
82
    if description:
83
        schema["description"] = description
84
    return schema
85
86
87
def optional_boolean(description: str = "") -> dict[str, Any]:
88
    schema: dict[str, Any] = {"type": ["boolean", "null"]}
89
    if description:
90
        schema["description"] = description
91
    return schema
92
93
94
def string_array(description: str = "") -> dict[str, Any]:
95
    schema: dict[str, Any] = {"type": "array", "items": {"type": "string"}}
96
    if description:
97
        schema["description"] = description
98
    return schema
99
100
101
TOKEN_METRICS_SCHEMA: dict[str, Any] = {
102
    "type": "object",
103
    "additionalProperties": False,
104
    "required": [
105
        "token_source",
106
        "input_tokens_reported",
107
        "output_tokens_reported",
108
        "reasoning_tokens_reported",
109
        "cache_read_tokens_reported",
110
        "cache_write_tokens_reported",
111
        "total_tokens_reported",
112
        "normalized_input_chars",
113
        "normalized_output_chars",
114
        "transcript_chars",
115
        "context_truncation_events",
116
        "token_budget",
117
        "cost_weighted_total",
118
    ],
119
    "properties": {
120
        "token_source": {"type": "string", "enum": TOKEN_SOURCES},
121
        "tokenizer": optional_string("Tokenizer or accounting method, when known."),
122
        "input_tokens_reported": optional_non_negative_integer("Provider-reported input tokens."),
123
        "output_tokens_reported": optional_non_negative_integer("Provider-reported output tokens."),
124
        "reasoning_tokens_reported": optional_non_negative_integer("Provider-reported reasoning tokens."),
125
        "cache_read_tokens_reported": optional_non_negative_integer("Provider-reported cache-read tokens."),
126
        "cache_write_tokens_reported": optional_non_negative_integer("Provider-reported cache-write tokens."),
127
        "total_tokens_reported": optional_non_negative_integer("Provider-reported total tokens."),
128
        "normalized_input_chars": non_negative_integer("Prompt and admitted tool-result characters."),
129
        "normalized_output_chars": non_negative_integer("Assistant output characters."),
130
        "transcript_chars": non_negative_integer("Full normalized transcript characters."),
131
        "context_truncation_events": optional_non_negative_integer(
132
            "Times context was compacted or truncated. Null when the adapter does not expose compaction events."
133
        ),
134
        "token_budget": optional_non_negative_integer("Configured token budget, if any."),
135
        "cost_weighted_total": optional_non_negative_number(
136
            "Billing-direction weighted token cost. Agent-emitted text (commands, assistant output) is "
137
            "model OUTPUT and weighted higher than agent-ingested tool results (model INPUT)."
138
        ),
139
        "cost_weights": optional_string("Weights used for cost_weighted_total, e.g. 'emitted=5,ingested=1'."),
140
    },
141
}
142
143
TOOL_CALL_METRICS_SCHEMA: dict[str, Any] = {
144
    "type": "object",
145
    "additionalProperties": False,
146
    "required": [
147
        "tool_calls_total",
148
        "tool_calls_by_kind",
149
        "terminal_commands_total",
150
        "file_read_calls",
151
        "file_write_calls",
152
        "search_calls",
153
        "edit_calls",
154
        "vcs_commands_total",
155
        "test_commands_total",
156
        "network_calls_total",
157
        "privileged_calls_total",
158
        "help_calls_total",
159
        "unknown_command_failures_total",
160
        "parallel_batches_total",
161
        "max_concurrency",
162
    ],
163
    "properties": {
164
        "tool_calls_total": non_negative_integer(),
165
        "tool_calls_by_kind": {
166
            "type": "object",
167
            "additionalProperties": non_negative_integer("Count by adapter-defined tool kind."),
168
            "properties": {},
169
        },
170
        "terminal_commands_total": non_negative_integer(),
171
        "file_read_calls": non_negative_integer(),
172
        "file_write_calls": non_negative_integer(),
173
        "search_calls": non_negative_integer(),
174
        "edit_calls": non_negative_integer(),
175
        "vcs_commands_total": non_negative_integer(),
176
        "test_commands_total": non_negative_integer(),
177
        "network_calls_total": non_negative_integer(),
178
        "privileged_calls_total": non_negative_integer(),
179
        "help_calls_total": non_negative_integer(
180
            "Discovery calls: --help/-h flags, help subcommands, and man pages. "
181
            "High counts signal the agent does not know the tool zero-shot."
182
        ),
183
        "unknown_command_failures_total": optional_non_negative_integer(
184
            "Commands rejected as unknown subcommand/flag (hallucinated usage). Null when exit "
185
            "status or error text is not adapter-visible."
186
        ),
187
        "vcs_info_followup_calls_total": optional_non_negative_integer(
188
            "Successful read-only VCS info commands immediately re-asked in the same family "
189
            "(e.g. status, then status with more flags). Each one means the first output did "
190
            "not carry enough information to act on — the hidden cost of compact output. "
191
            "Null when per-call outcomes are not adapter-visible."
192
        ),
193
        "parallel_batches_total": optional_non_negative_integer(
194
            "Tool batches issued in parallel. Null when the adapter does not expose batching."
195
        ),
196
        "max_concurrency": optional_non_negative_integer(
197
            "Peak concurrent tool calls. Null when the adapter does not expose concurrency."
198
        ),
199
        "measurement_source": optional_string("How tool calls were observed, e.g. adapter_stream_events."),
200
    },
201
}
202
203
FAILURE_RETRY_METRICS_SCHEMA: dict[str, Any] = {
204
    "type": "object",
205
    "additionalProperties": False,
206
    "required": [
207
        "failed_tool_calls_total",
208
        "failed_commands_total",
209
        "failed_vcs_commands_total",
210
        "failed_test_commands_total",
211
        "retry_attempts_total",
212
        "retry_chains_total",
213
        "same_command_retries_total",
214
        "retry_after_failure_ms",
215
        "recoverable_failures_total",
216
        "unrecoverable_failures_total",
217
        "turns_to_recovery",
218
        "tokens_to_recovery",
219
    ],
220
    "properties": {
221
        "failed_tool_calls_total": optional_non_negative_integer(
222
            "Null when per-call success is not adapter-visible."
223
        ),
224
        "failed_commands_total": optional_non_negative_integer(),
225
        "failed_vcs_commands_total": optional_non_negative_integer(),
226
        "failed_test_commands_total": optional_non_negative_integer(),
227
        "retry_attempts_total": optional_non_negative_integer(),
228
        "retry_chains_total": optional_non_negative_integer(),
229
        "same_command_retries_total": optional_non_negative_integer(),
230
        "retry_after_failure_ms": optional_non_negative_number(),
231
        "recoverable_failures_total": optional_non_negative_integer(),
232
        "unrecoverable_failures_total": optional_non_negative_integer(),
233
        "turns_to_recovery": optional_non_negative_integer(
234
            "Assistant turns between the first failed tool call and the next successful tool call. "
235
            "Measures error-message ergonomics: a good error resolves in one turn."
236
        ),
237
        "tokens_to_recovery": optional_non_negative_integer(
238
            "Model output tokens spent in the recovery window, when per-turn usage is available."
239
        ),
240
        "failed_command_examples": string_array("Small redacted examples for debugging."),
241
        "measurement_source": optional_string("How failures/retries were observed."),
242
    },
243
}
244
245
WALL_CLOCK_METRICS_SCHEMA: dict[str, Any] = {
246
    "type": "object",
247
    "additionalProperties": False,
248
    "required": [
249
        "total_ms",
250
        "setup_ms",
251
        "agent_active_ms",
252
        "tool_wait_ms",
253
        "vcs_ms",
254
        "test_ms",
255
        "merge_ms",
256
        "cleanup_ms",
257
        "idle_ms",
258
        "timeout_ms",
259
    ],
260
    "properties": {
261
        "total_ms": non_negative_number(),
262
        "setup_ms": non_negative_number(),
263
        "agent_active_ms": non_negative_number(),
264
        "tool_wait_ms": optional_non_negative_number(
265
            "Null when the adapter does not separate tool wait from model time."
266
        ),
267
        "vcs_ms": non_negative_number(),
268
        "test_ms": non_negative_number(),
269
        "merge_ms": optional_non_negative_number("Null when the scenario has no merge phase."),
270
        "cleanup_ms": optional_non_negative_number("Null when cleanup is not separately timed."),
271
        "idle_ms": optional_non_negative_number("Null when harness queueing is not separately timed."),
272
        "timeout_ms": optional_non_negative_number(),
273
        "measurement_source": optional_string("How phases were attributed."),
274
    },
275
}
276
277
BYTES_METRICS_SCHEMA: dict[str, Any] = {
278
    "type": "object",
279
    "additionalProperties": False,
280
    "required": [
281
        "workspace_logical_bytes",
282
        "workspace_materialized_bytes",
283
        "bytes_read_by_agent",
284
        "bytes_written_by_agent",
285
        "bytes_hydrated",
286
        "files_hydrated",
287
        "hydration_events",
288
        "large_file_bytes_scanned",
289
        "binary_bytes_touched",
290
        "command_output_bytes",
291
    ],
292
    "properties": {
293
        "workspace_logical_bytes": non_negative_integer("Fixture size if fully materialized."),
294
        "workspace_materialized_bytes": non_negative_integer("Bytes materialized before timed work."),
295
        "bytes_read_by_agent": optional_non_negative_integer(
296
            "File bytes read through agent-visible read tools. Null when read-tool payloads are "
297
            "not adapter-visible — never transcript output bytes, which live in command_output_bytes. "
298
            "Hydration/read-efficiency claims must not cite a proxy here."
299
        ),
300
        "bytes_written_by_agent": non_negative_integer("File bytes written through agent-visible tools."),
301
        "bytes_hydrated": optional_non_negative_integer(
302
            "Bytes loaded lazily by VCS or filesystem. Null when no hydration probe ran."
303
        ),
304
        "files_hydrated": optional_non_negative_integer(
305
            "Files loaded lazily by VCS or filesystem. Null when no hydration probe ran."
306
        ),
307
        "hydration_events": optional_non_negative_integer(
308
            "Hydration events observed by the harness. Null when no hydration probe ran."
309
        ),
310
        "large_file_bytes_scanned": optional_non_negative_integer(
311
            "Bytes scanned in known large-file fixtures. Null when not instrumented."
312
        ),
313
        "binary_bytes_touched": optional_non_negative_integer(
314
            "Binary bytes read or written. Null when not instrumented."
315
        ),
316
        "command_output_bytes": non_negative_integer("Terminal output bytes admitted to transcript."),
317
        "measurement_source": optional_string("Adapter, filesystem probe, VCS trace, or estimate."),
318
    },
319
}
320
321
VCS_METRICS_SCHEMA: dict[str, Any] = {
322
    "type": "object",
323
    "additionalProperties": False,
324
    "required": [
325
        "commands_total",
326
        "status_commands",
327
        "diff_commands",
328
        "snapshot_commands",
329
        "branch_commands",
330
        "merge_commands",
331
        "cleanup_commands",
332
        "commits_created",
333
        "branches_created",
334
        "conflicts_total",
335
        "conflicts_resolved",
336
        "dirty_files_at_end",
337
        "cleanup_success",
338
    ],
339
    "properties": {
340
        "commands_total": optional_non_negative_integer(
341
            "Null when the agent transcript does not expose VCS command invocations."
342
        ),
343
        "status_commands": optional_non_negative_integer(),
344
        "diff_commands": optional_non_negative_integer(),
345
        "snapshot_commands": optional_non_negative_integer(),
346
        "branch_commands": optional_non_negative_integer(),
347
        "merge_commands": optional_non_negative_integer(),
348
        "cleanup_commands": optional_non_negative_integer(),
349
        "commits_created": non_negative_integer(),
350
        "branches_created": optional_non_negative_integer(),
351
        "conflicts_total": optional_non_negative_integer(
352
            "Null when conflict detection is not instrumented for the scenario."
353
        ),
354
        "conflicts_resolved": optional_non_negative_integer(),
355
        "dirty_files_at_end": non_negative_integer(),
356
        "cleanup_success": boolean(),
357
        "measurement_source": optional_string("How VCS command outcomes were observed."),
358
        # PATH-shim attribution fields (optional, additive). Null when the
359
        # --vcs-shim instrument was off or recorded nothing: unmeasured, not
360
        # zero (ADR-0002).
361
        "shim_sidecar_path": optional_string(
362
            "Artifact path of the shim sidecar JSONL ({command, argv, start_ms, end_ms, "
363
            "elapsed_ms, returncode, shim} per VCS call). Null when the shim was off."
364
        ),
365
        "shim_sidecar_path_unavailable_reason": optional_string(
366
            "Reason shim_sidecar_path is null."
367
        ),
368
        "shim_overhead": optional_non_negative_number(
369
            "Requested alias for shim_overhead_ms: median extra ms per call added by the PATH shim."
370
        ),
371
        "shim_overhead_ms": optional_non_negative_number(
372
            "Median extra ms per call added by the PATH shim, measured per run via a "
373
            "calibration pass. Recorded for interpretation, never subtracted from measurements."
374
        ),
375
        "shim_overhead_unavailable_reason": optional_string(
376
            "Reason shim_overhead/shim_overhead_ms is null."
377
        ),
378
        "vcs_ms": optional_non_negative_number(
379
            "Sum of shim-recorded VCS call wall time (includes shim overhead)."
380
        ),
381
        "vcs_ms_unavailable_reason": optional_string("Reason shim-recorded vcs_ms is null."),
382
        "vcs_call_count_shim": optional_non_negative_integer(
383
            "Number of VCS binary invocations recorded by the PATH shim."
384
        ),
385
        "vcs_call_count_shim_unavailable_reason": optional_string(
386
            "Reason vcs_call_count_shim is null."
387
        ),
388
        "agent_blocked_on_vcs_ms": optional_non_negative_number(
389
            "oakbench.thrash.agent_blocked_on_vcs_ms over the shim sidecar rows."
390
        ),
391
        "agent_blocked_on_vcs_ms_unavailable_reason": optional_string(
392
            "Reason agent_blocked_on_vcs_ms is null."
393
        ),
394
        "vcs_share_of_task_wall": optional_non_negative_number(
395
            "agent_blocked_on_vcs_ms divided by agent wall time."
396
        ),
397
        "vcs_share_of_task_wall_unavailable_reason": optional_string(
398
            "Reason vcs_share_of_task_wall is null."
399
        ),
400
        "thrash_events_count": optional_non_negative_integer(
401
            "Count of oakbench.thrash.thrash_events detected in the shim sidecar rows."
402
        ),
403
        "thrash_events_count_unavailable_reason": optional_string(
404
            "Reason thrash_events_count is null."
405
        ),
406
        "polling_loop_count": optional_non_negative_integer(
407
            "Count of shim-sidecar thrash events with type vcs_polling_loop."
408
        ),
409
        "polling_loop_count_unavailable_reason": optional_string(
410
            "Reason polling_loop_count is null."
411
        ),
412
    },
413
}
414
415
TEST_METRICS_SCHEMA: dict[str, Any] = {
416
    "type": "object",
417
    "additionalProperties": False,
418
    "required": [
419
        "commands_total",
420
        "passed_commands",
421
        "failed_commands",
422
        "assertions_total",
423
        "assertions_failed",
424
        "required_checks_passed",
425
    ],
426
    "properties": {
427
        "commands_total": non_negative_integer(),
428
        "passed_commands": non_negative_integer(),
429
        "failed_commands": non_negative_integer(),
430
        "assertions_total": optional_non_negative_integer(),
431
        "assertions_failed": optional_non_negative_integer(),
432
        "required_checks_passed": boolean(),
433
    },
434
}
435
436
PARALLEL_METRICS_SCHEMA: dict[str, Any] = {
437
    "type": "object",
438
    "additionalProperties": False,
439
    "required": [
440
        "tasks_total",
441
        "tasks_completed",
442
        "max_parallel_tasks",
443
        "overlapping_files_count",
444
        "lost_updates_detected",
445
    ],
446
    "properties": {
447
        "tasks_total": non_negative_integer(),
448
        "tasks_completed": non_negative_integer(),
449
        "max_parallel_tasks": non_negative_integer(),
450
        "overlapping_files_count": optional_non_negative_integer(
451
            "Null when overlap detection did not run (e.g. single-task scenarios)."
452
        ),
453
        "lost_updates_detected": optional_non_negative_integer(
454
            "Null when no integrity check ran. Zero means the check ran and found nothing lost."
455
        ),
456
        "lock_wait_ms": optional_non_negative_number(
457
            "Time spent blocked on or retrying VCS locks under contention."
458
        ),
459
        "commit_throughput_per_s": optional_non_negative_number(
460
            "Successful snapshots per second across all parallel workers."
461
        ),
462
        "integrity_check_passed": optional_boolean(
463
            "fsck-equivalent result after the parallel run. Null when no check ran."
464
        ),
465
        "measurement_source": optional_string("How parallel behavior was observed."),
466
    },
467
}
468
469
TURN_METRICS_SCHEMA: dict[str, Any] = {
470
    "type": "object",
471
    "additionalProperties": False,
472
    "required": [
473
        "turn_source",
474
        "assistant_turns_total",
475
        "tool_calls_per_turn_avg",
476
        "max_tool_calls_per_turn",
477
        "cumulative_input_tokens_reported",
478
        "peak_input_tokens_reported",
479
    ],
480
    "properties": {
481
        "turn_source": {"type": "string", "enum": TURN_SOURCES},
482
        "assistant_turns_total": optional_non_negative_integer(
483
            "Model inference round-trips. Each turn re-pays the transcript as input tokens, so "
484
            "turns dominate agent cost; tool-call counts alone hide this."
485
        ),
486
        "tool_calls_per_turn_avg": optional_non_negative_number(),
487
        "max_tool_calls_per_turn": optional_non_negative_integer(
488
            "Peak batching within one turn. Batched calls are far cheaper than sequential turns."
489
        ),
490
        "cumulative_input_tokens_reported": optional_non_negative_integer(
491
            "Sum of provider-reported input tokens (including cache reads) across all turns. "
492
            "Captures context-residency cost: early verbose output is re-paid every later turn."
493
        ),
494
        "peak_input_tokens_reported": optional_non_negative_integer(
495
            "Largest single-turn input, a proxy for peak context size."
496
        ),
497
        "turn_timeline_path": optional_string(
498
            "Artifact path of per-turn JSONL: turn index, input/output/cache tokens, tool calls."
499
        ),
500
    },
501
}
502
503
SCHEMA: dict[str, Any] = {
504
    "$schema": "https://json-schema.org/draft/2020-12/schema",
505
    "title": "Oak agent workflow benchmark result row",
506
    "schema_version": SCHEMA_VERSION,
507
    "type": "object",
508
    "additionalProperties": False,
509
    "required": [
510
        "schema_version",
511
        "bench_id",
512
        "timestamp_utc",
513
        "scenario",
514
        "run",
515
        "subject",
516
        "subject_kind",
517
        "vcs_mode",
518
        "agent",
519
        "fixture",
520
        "outcome",
521
        "success",
522
        "success_criteria",
523
        "token_metrics",
524
        "turn_metrics",
525
        "tool_call_metrics",
526
        "failed_command_retry_metrics",
527
        "wall_clock_metrics",
528
        "bytes_metrics",
529
        "vcs_metrics",
530
        "test_metrics",
531
        "parallel_metrics",
532
    ],
533
    "properties": {
534
        "schema_version": {"type": "integer", "const": SCHEMA_VERSION},
535
        "bench_id": string("Stable identifier for a benchmark invocation."),
536
        "timestamp_utc": string("UTC ISO-8601 timestamp for row completion."),
537
        "profile": optional_string("Run profile such as smoke, standard, or large."),
538
        "host": optional_string("Host name used for run provenance."),
539
        "platform": optional_string("Platform string used for run provenance."),
540
        "machine": optional_string("Machine architecture used for run provenance."),
541
        "scenario": string("Scenario id from scenarios/agent.yaml."),
542
        "scenario_version": optional_string("Fixture or prompt version for this scenario."),
543
        "operation": optional_string("Optional report grouping operation, e.g. agent.workflow."),
544
        "benchmark_track": optional_string("Command semantics track such as agent-default or core-equivalent."),
545
        "command_semantics_version": optional_string("Version of the command semantics contract."),
546
        "elapsed_ms": optional_non_negative_number("Compatibility mirror of wall_clock_metrics.total_ms."),
547
        "returncode": optional_non_negative_integer("Compatibility mirror: 0 for success, non-zero for failure."),
548
        "raw_output_bytes": optional_non_negative_integer("Compatibility mirror of captured agent stdout+stderr bytes."),
549
        "stdout_bytes": optional_non_negative_integer("Captured agent stdout bytes."),
550
        "stderr_bytes": optional_non_negative_integer("Captured agent stderr bytes."),
551
        "output_truncated": optional_boolean("Whether captured output was truncated before artifact storage."),
552
        "task_id": optional_string("Subtask id for parallel scenarios, if this row is per subtask."),
553
        "task_prompt_id": optional_string("Stable prompt/template id."),
554
        "run": non_negative_integer("Zero-based repetition index."),
555
        "subject": string("Benchmark subject such as git, oak_installed, or oak_local."),
556
        "subject_kind": {"type": "string", "enum": SUBJECT_KINDS},
557
        "subject_label": optional_string("Human label for reports."),
558
        "vcs_mode": {"type": "string", "enum": VCS_MODES},
559
        "agent": {
560
            "type": "object",
561
            "additionalProperties": False,
562
            "required": ["name", "adapter"],
563
            "properties": {
564
                "name": string("Agent or harness adapter name."),
565
                "adapter": string("Runner integration name."),
566
                "provider": optional_string("Provider name, if applicable."),
567
                "model": optional_string("Model id, if applicable."),
568
                "version": optional_string("Agent binary, prompt, or adapter version."),
569
                "environment": optional_string("Agent environment mode, such as minimal or local-default."),
570
                "instruction_level": {
571
                    "type": ["string", "null"],
572
                    "enum": INSTRUCTION_LEVELS + [None],
573
                    "description": (
574
                        "How much VCS-specific instruction the prompt carried. zero-shot measures the "
575
                        "model-familiarity tax: Git is in pretraining data, a new VCS is not."
576
                    ),
577
                },
578
                "temperature": {"type": ["number", "null"]},
579
                "max_output_tokens": optional_non_negative_integer(),
580
            },
581
        },
582
        "fixture": {
583
            "type": "object",
584
            "additionalProperties": False,
585
            "required": ["id", "repo_shape", "fixture_version"],
586
            "properties": {
587
                "id": string("Fixture id used by the runner."),
588
                "repo_shape": string("Human-readable fixture shape."),
589
                "fixture_version": string("Stable fixture content version or hash."),
590
                "logical_file_count": optional_non_negative_integer(),
591
                "logical_bytes": optional_non_negative_integer(),
592
            },
593
        },
594
        "source": {
595
            "type": "object",
596
            "additionalProperties": True,
597
            "properties": {
598
                "oak_hash": optional_string(),
599
                "oak_status": optional_string(),
600
                "benchmark_repo_hash": optional_string(),
601
            },
602
        },
603
        "outcome": {"type": "string", "enum": OUTCOMES},
604
        "success": boolean("True only when deterministic scenario criteria passed."),
605
        "failure_reason": optional_string("Short stable failure reason for dashboards."),
606
        "success_criteria": {
607
            "type": "object",
608
            "additionalProperties": False,
609
            "required": ["passed", "checks"],
610
            "properties": {
611
                "passed": boolean(),
612
                "checks": {
613
                    "type": "object",
614
                    "additionalProperties": boolean("Individual deterministic check result."),
615
                    "properties": {},
616
                },
617
                "notes": optional_string(),
618
            },
619
        },
620
        "token_metrics": TOKEN_METRICS_SCHEMA,
621
        "turn_metrics": TURN_METRICS_SCHEMA,
622
        "tool_call_metrics": TOOL_CALL_METRICS_SCHEMA,
623
        "failed_command_retry_metrics": FAILURE_RETRY_METRICS_SCHEMA,
624
        "wall_clock_metrics": WALL_CLOCK_METRICS_SCHEMA,
625
        "bytes_metrics": BYTES_METRICS_SCHEMA,
626
        "vcs_metrics": VCS_METRICS_SCHEMA,
627
        "test_metrics": TEST_METRICS_SCHEMA,
628
        "parallel_metrics": PARALLEL_METRICS_SCHEMA,
629
        "artifacts": {
630
            "type": "object",
631
            "additionalProperties": False,
632
            "properties": {
633
                "transcript_path": optional_string(),
634
                "command_log_path": optional_string(),
635
                "diff_path": optional_string(),
636
                "workspace_path": optional_string(),
637
                "extra_paths": string_array(),
638
            },
639
        },
640
        "notes": optional_string(),
641
    },
642
}
643
644
645
def type_matches(value: Any, expected_type: Any) -> bool:
646
    if isinstance(expected_type, list):
647
        return any(type_matches(value, item) for item in expected_type)
648
    if expected_type == "null":
649
        return value is None
650
    if expected_type == "boolean":
651
        return isinstance(value, bool)
652
    if expected_type == "integer":
653
        return isinstance(value, int) and not isinstance(value, bool)
654
    if expected_type == "number":
655
        return (isinstance(value, int) or isinstance(value, float)) and not isinstance(value, bool)
656
    if expected_type == "string":
657
        return isinstance(value, str)
658
    if expected_type == "object":
659
        return isinstance(value, dict)
660
    if expected_type == "array":
661
        return isinstance(value, list)
662
    return True
663
664
665
def display_path(path: str, key: str) -> str:
666
    if path == "$":
667
        return "$" + "." + key
668
    return path + "." + key
669
670
671
def validate_value(value: Any, schema: dict[str, Any], path: str) -> List[str]:
672
    errors: List[str] = []
673
    expected_type = schema.get("type")
674
    if expected_type is not None and not type_matches(value, expected_type):
675
        errors.append(f"{path}: expected {expected_type}, got {type(value).__name__}")
676
        return errors
677
678
    if "const" in schema and value != schema["const"]:
679
        errors.append(f"{path}: expected constant {schema['const']!r}, got {value!r}")
680
    if "enum" in schema and value not in schema["enum"]:
681
        errors.append(f"{path}: expected one of {schema['enum']!r}, got {value!r}")
682
    if "minimum" in schema and isinstance(value, (int, float)) and not isinstance(value, bool):
683
        if value < schema["minimum"]:
684
            errors.append(f"{path}: expected >= {schema['minimum']}, got {value!r}")
685
686
    if isinstance(value, dict):
687
        required = schema.get("required", [])
688
        for key in required:
689
            if key not in value:
690
                errors.append(f"{path}: missing required property {key!r}")
691
        properties = schema.get("properties", {})
692
        additional = schema.get("additionalProperties", True)
693
        for key, item in value.items():
694
            child_path = display_path(path, key)
695
            if key in properties:
696
                errors.extend(validate_value(item, properties[key], child_path))
697
            elif additional is False:
698
                errors.append(f"{child_path}: unexpected property")
699
            elif isinstance(additional, dict):
700
                errors.extend(validate_value(item, additional, child_path))
701
    elif isinstance(value, list) and "items" in schema:
702
        item_schema = schema["items"]
703
        for index, item in enumerate(value):
704
            errors.extend(validate_value(item, item_schema, f"{path}[{index}]"))
705
706
    return errors
707
708
709
def validate_schema_shape(schema: dict[str, Any]) -> List[str]:
710
    errors: List[str] = []
711
    for key in ["title", "type", "required", "properties"]:
712
        if key not in schema:
713
            errors.append(f"schema missing {key!r}")
714
    if schema.get("type") != "object":
715
        errors.append("schema root type must be object")
716
    properties = schema.get("properties", {})
717
    for required_key in schema.get("required", []):
718
        if required_key not in properties:
719
            errors.append(f"schema requires {required_key!r} but has no property definition")
720
    return errors
721
722
723
def validate_row(row: Any) -> List[str]:
724
    if not isinstance(row, dict):
725
        return [f"$: expected object row, got {type(row).__name__}"]
726
    return validate_value(row, SCHEMA, "$")
727
728
729
def iter_rows(path: Path) -> Iterable[Any]:
730
    text = path.read_text(encoding="utf-8")
731
    stripped = text.strip()
732
    if not stripped:
733
        return []
734
    if path.suffix == ".json" or stripped[0] == "[":
735
        data = json.loads(stripped)
736
        if isinstance(data, list):
737
            return data
738
        return [data]
739
    rows: List[Any] = []
740
    for line_number, line in enumerate(text.splitlines(), 1):
741
        if not line.strip():
742
            continue
743
        try:
744
            rows.append(json.loads(line))
745
        except json.JSONDecodeError as exc:
746
            rows.append({"__json_error__": f"line {line_number}: {exc}"})
747
    return rows
748
749
750
def validate_file(path: Path) -> dict[str, Any]:
751
    errors: List[str] = []
752
    rows = list(iter_rows(path))
753
    for index, row in enumerate(rows, 1):
754
        if isinstance(row, dict) and "__json_error__" in row:
755
            errors.append(row["__json_error__"])
756
            continue
757
        for error in validate_row(row):
758
            errors.append(f"row {index}: {error}")
759
    return {"ok": not errors, "rows": len(rows), "errors": errors}
760
761
762
def example_row() -> dict[str, Any]:
763
    return {
764
        "schema_version": SCHEMA_VERSION,
765
        "bench_id": "20260609T000000Z",
766
        "timestamp_utc": "2026-06-09T00:00:00Z",
767
        "profile": "smoke",
768
        "scenario": "one_line_bug_fix",
769
        "scenario_version": "v1",
770
        "task_id": None,
771
        "task_prompt_id": "agent-one-line-bug-fix-v1",
772
        "run": 0,
773
        "subject": "oak_local",
774
        "subject_kind": "oak",
775
        "subject_label": "Oak local",
776
        "vcs_mode": "oak_space_per_task",
777
        "agent": {
778
            "name": "example-agent",
779
            "adapter": "example-adapter",
780
            "provider": None,
781
            "model": None,
782
            "version": None,
783
            "environment": "minimal",
784
            "instruction_level": "cheat-sheet",
785
            "temperature": None,
786
            "max_output_tokens": None,
787
        },
788
        "fixture": {
789
            "id": "agent_one_line_bug_fix",
790
            "repo_shape": "small text application",
791
            "fixture_version": "v1",
792
            "logical_file_count": 42,
793
            "logical_bytes": 65536,
794
        },
795
        "source": {"oak_hash": None, "oak_status": None, "benchmark_repo_hash": None},
796
        "outcome": "pass",
797
        "success": True,
798
        "failure_reason": None,
799
        "success_criteria": {
800
            "passed": True,
801
            "checks": {"tests_passed": True, "expected_files_changed": True},
802
            "notes": None,
803
        },
804
        "token_metrics": {
805
            "token_source": "char_only",
806
            "tokenizer": None,
807
            "input_tokens_reported": None,
808
            "output_tokens_reported": None,
809
            "reasoning_tokens_reported": None,
810
            "cache_read_tokens_reported": None,
811
            "cache_write_tokens_reported": None,
812
            "total_tokens_reported": None,
813
            "normalized_input_chars": 12000,
814
            "normalized_output_chars": 1200,
815
            "transcript_chars": 13200,
816
            "context_truncation_events": None,
817
            "token_budget": None,
818
            "cost_weighted_total": 4500.0,
819
            "cost_weights": "emitted=5,ingested=1",
820
        },
821
        "turn_metrics": {
822
            "turn_source": "adapter_stream",
823
            "assistant_turns_total": 6,
824
            "tool_calls_per_turn_avg": 1.33,
825
            "max_tool_calls_per_turn": 2,
826
            "cumulative_input_tokens_reported": 48000,
827
            "peak_input_tokens_reported": 11000,
828
            "turn_timeline_path": None,
829
        },
830
        "tool_call_metrics": {
831
            "tool_calls_total": 8,
832
            "tool_calls_by_kind": {"terminal": 3, "read": 3, "edit": 1, "search": 1},
833
            "terminal_commands_total": 3,
834
            "file_read_calls": 3,
835
            "file_write_calls": 1,
836
            "search_calls": 1,
837
            "edit_calls": 1,
838
            "vcs_commands_total": 2,
839
            "test_commands_total": 1,
840
            "network_calls_total": 0,
841
            "privileged_calls_total": 0,
842
            "help_calls_total": 0,
843
            "unknown_command_failures_total": None,
844
            "parallel_batches_total": None,
845
            "max_concurrency": None,
846
            "measurement_source": "adapter_stream_events",
847
        },
848
        "failed_command_retry_metrics": {
849
            "failed_tool_calls_total": 0,
850
            "failed_commands_total": 0,
851
            "failed_vcs_commands_total": 0,
852
            "failed_test_commands_total": 0,
853
            "retry_attempts_total": None,
854
            "retry_chains_total": None,
855
            "same_command_retries_total": None,
856
            "retry_after_failure_ms": None,
857
            "recoverable_failures_total": None,
858
            "unrecoverable_failures_total": None,
859
            "turns_to_recovery": None,
860
            "tokens_to_recovery": None,
861
            "failed_command_examples": [],
862
            "measurement_source": "adapter_stream_events",
863
        },
864
        "wall_clock_metrics": {
865
            "total_ms": 15000.0,
866
            "setup_ms": 800.0,
867
            "agent_active_ms": 7000.0,
868
            "tool_wait_ms": None,
869
            "vcs_ms": 900.0,
870
            "test_ms": 2100.0,
871
            "merge_ms": None,
872
            "cleanup_ms": None,
873
            "idle_ms": None,
874
            "timeout_ms": None,
875
            "measurement_source": "outer_cli_wall_clock",
876
        },
877
        "bytes_metrics": {
878
            "workspace_logical_bytes": 65536,
879
            "workspace_materialized_bytes": 32768,
880
            "bytes_read_by_agent": 18000,
881
            "bytes_written_by_agent": 140,
882
            "bytes_hydrated": None,
883
            "files_hydrated": None,
884
            "hydration_events": None,
885
            "large_file_bytes_scanned": None,
886
            "binary_bytes_touched": None,
887
            "command_output_bytes": 3000,
888
            "measurement_source": "example-adapter",
889
        },
890
        "vcs_metrics": {
891
            "commands_total": 2,
892
            "status_commands": 1,
893
            "diff_commands": 0,
894
            "snapshot_commands": 1,
895
            "branch_commands": 0,
896
            "merge_commands": 0,
897
            "cleanup_commands": 0,
898
            "commits_created": 1,
899
            "branches_created": 1,
900
            "conflicts_total": None,
901
            "conflicts_resolved": None,
902
            "dirty_files_at_end": 0,
903
            "cleanup_success": True,
904
            "measurement_source": "transcript_command_classification",
905
        },
906
        "test_metrics": {
907
            "commands_total": 1,
908
            "passed_commands": 1,
909
            "failed_commands": 0,
910
            "assertions_total": None,
911
            "assertions_failed": None,
912
            "required_checks_passed": True,
913
        },
914
        "parallel_metrics": {
915
            "tasks_total": 1,
916
            "tasks_completed": 1,
917
            "max_parallel_tasks": 1,
918
            "overlapping_files_count": None,
919
            "lost_updates_detected": None,
920
            "lock_wait_ms": None,
921
            "commit_throughput_per_s": None,
922
            "integrity_check_passed": None,
923
            "measurement_source": "single_task_run",
924
        },
925
        "artifacts": {
926
            "transcript_path": None,
927
            "command_log_path": None,
928
            "diff_path": None,
929
            "workspace_path": None,
930
            "extra_paths": [],
931
        },
932
        "notes": None,
933
    }
934
935
936
def parse_args() -> argparse.Namespace:
937
    parser = argparse.ArgumentParser(description=__doc__)
938
    parser.add_argument("--validate-jsonl", type=Path, help="Validate a JSON or JSONL result file.")
939
    parser.add_argument("--example", action="store_true", help="Print an example valid result row.")
940
    parser.add_argument("--compact", action="store_true", help="Print compact JSON.")
941
    return parser.parse_args()
942
943
944
def print_json(value: Any, compact: bool) -> None:
945
    if compact:
946
        print(json.dumps(value, sort_keys=True, separators=(",", ":")))
947
    else:
948
        print(json.dumps(value, indent=2, sort_keys=True))
949
950
951
def main() -> int:
952
    args = parse_args()
953
    schema_errors = validate_schema_shape(SCHEMA)
954
    if schema_errors:
955
        print_json({"ok": False, "schema_errors": schema_errors}, args.compact)
956
        return 1
957
958
    if args.example:
959
        row = example_row()
960
        errors = validate_row(row)
961
        if errors:
962
            print_json({"ok": False, "example_errors": errors}, args.compact)
963
            return 1
964
        print_json(row, args.compact)
965
        return 0
966
967
    if args.validate_jsonl:
968
        result = validate_file(args.validate_jsonl)
969
        print_json(result, args.compact)
970
        return 0 if result["ok"] else 1
971
972
    print_json(SCHEMA, args.compact)
973
    return 0
974
975
976
if __name__ == "__main__":
977
    raise SystemExit(main())