feat: generate auditable agent trial reports
This commit is contained in:
@@ -221,6 +221,10 @@ stable.
|
|||||||
manual-audited reports. Two data-driven challenges exist: browser-click and
|
manual-audited reports. Two data-driven challenges exist: browser-click and
|
||||||
report-workflow. The central `run_trials.py` runner accepts any challenge
|
report-workflow. The central `run_trials.py` runner accepts any challenge
|
||||||
manifest.
|
manifest.
|
||||||
|
- Completed: report projections generate bounded Markdown and JSON reports for
|
||||||
|
every V2 trial and regenerate both after audit without mutating raw evidence.
|
||||||
|
Implementation:
|
||||||
|
[`report projections`](historical/superpowers/plans/2026-06-23-agent-challenge-report-projections.md).
|
||||||
|
|
||||||
## Historical References
|
## Historical References
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,452 @@
|
|||||||
|
# Agent Challenge Report Projections Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Generate equivalent bounded Markdown and JSON reports for every V2 trial and regenerate both after audit without mutating raw evidence.
|
||||||
|
|
||||||
|
**Architecture:** Add strict Pydantic report DTOs, project one `TrialReport` to Markdown and JSON, wire projection generation after raw result persistence, and make V2 audit rebuild both. Preserve referenced V1 behavior.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, Pydantic v2, PyYAML, pathlib, argparse, pytest, Ruff, basedpyright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Create `examples/agent_challenges/report_models.py`.
|
||||||
|
- Modify `examples/agent_challenges/reports.py`, `runner.py`, `run_trials.py`, `audit.py`, and `base-prompt.md`.
|
||||||
|
- Create `tests/examples/test_agent_challenge_reports.py`.
|
||||||
|
- Modify `tests/examples/test_agent_challenge_harness_v2.py`.
|
||||||
|
- Update `docs/current_roadmap.md` and archive this plan.
|
||||||
|
|
||||||
|
### Task 1: Define The Normalized Report
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `examples/agent_challenges/report_models.py`
|
||||||
|
- Modify: `examples/agent_challenges/reports.py`
|
||||||
|
- Create: `tests/examples/test_agent_challenge_reports.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing bounded-report test**
|
||||||
|
|
||||||
|
Build a `_raw_result(tmp_path)` fixture matching the current V2 result shape,
|
||||||
|
including explicit paths, identity, challenge report, parsed text, metrics,
|
||||||
|
policy, tool calls, and deliberately large stdout/output previews.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_trial_report_is_bounded_machine_projection(tmp_path: Path) -> None:
|
||||||
|
payload = build_trial_report(_raw_result(tmp_path), audit=None).model_dump(
|
||||||
|
mode="json"
|
||||||
|
)
|
||||||
|
assert payload["schema_version"] == 1
|
||||||
|
assert payload["identity"]["challenge_id"] == "fixture"
|
||||||
|
assert payload["outcome"]["task_outcome"] == "success"
|
||||||
|
assert payload["commands_and_tools"][0]["detail"].endswith(
|
||||||
|
"workflow.plan.json"
|
||||||
|
)
|
||||||
|
serialized = json.dumps(payload)
|
||||||
|
assert "large raw stream" not in serialized
|
||||||
|
assert "full tool output" not in serialized
|
||||||
|
assert payload["manual_audit"]["status"] == "pending"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run and verify import failure**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_reports.py::test_trial_report_is_bounded_machine_projection -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement strict DTOs**
|
||||||
|
|
||||||
|
Create `TrialIdentity`, `TrialOutcome`, `CommandToolBrief`, `TokenSummary`,
|
||||||
|
`AutomaticEvidence`, `ManualAuditSummary`, and `TrialReport` using
|
||||||
|
`ConfigDict(extra="forbid")`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class StrictReportModel(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|
||||||
|
class TrialIdentity(StrictReportModel):
|
||||||
|
challenge_id: str
|
||||||
|
model: str
|
||||||
|
variant: str
|
||||||
|
instruction_profile: str
|
||||||
|
trial_index: int
|
||||||
|
repository_commit: str | None = None
|
||||||
|
repository_dirty: bool | None = None
|
||||||
|
prompt_hashes: dict[str, str] = Field(default_factory=dict)
|
||||||
|
raw_result_path: str
|
||||||
|
workspace_path: str
|
||||||
|
|
||||||
|
|
||||||
|
class TrialOutcome(StrictReportModel):
|
||||||
|
task_outcome: str
|
||||||
|
evaluation_validity: str
|
||||||
|
duration_seconds: float
|
||||||
|
returncode: int | None
|
||||||
|
assertion_failures: list[str] = Field(default_factory=list)
|
||||||
|
parse_errors: dict[str, dict[str, str]] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandToolBrief(StrictReportModel):
|
||||||
|
ordinal: int
|
||||||
|
tool: str
|
||||||
|
status: str
|
||||||
|
title: str
|
||||||
|
detail: str | None = None
|
||||||
|
failed: bool
|
||||||
|
output_chars: int
|
||||||
|
output_sha256: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenSummary(StrictReportModel):
|
||||||
|
total: int = 0
|
||||||
|
input: int = 0
|
||||||
|
output: int = 0
|
||||||
|
reasoning: int = 0
|
||||||
|
cache_read: int = 0
|
||||||
|
cache_write: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class AutomaticEvidence(StrictReportModel):
|
||||||
|
step_count: int = 0
|
||||||
|
tool_call_count: int = 0
|
||||||
|
failed_tool_call_count: int = 0
|
||||||
|
tool_counts: dict[str, int] = Field(default_factory=dict)
|
||||||
|
tokens: TokenSummary = Field(default_factory=TokenSummary)
|
||||||
|
cost: float = 0.0
|
||||||
|
unknown_event_count: int = 0
|
||||||
|
reads_by_category: dict[str, list[str]] = Field(default_factory=dict)
|
||||||
|
escalated_to_product_code: bool = False
|
||||||
|
disallowed_reads: list[str] = Field(default_factory=list)
|
||||||
|
opaque_shell_commands: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ManualAuditSummary(StrictReportModel):
|
||||||
|
status: Literal["pending", "complete"] = "pending"
|
||||||
|
official_outcome: str | None = None
|
||||||
|
auditor: str | None = None
|
||||||
|
audited_at: str | None = None
|
||||||
|
corrections: list[str] = Field(default_factory=list)
|
||||||
|
notes: str = ""
|
||||||
|
read_flags: dict[str, bool] = Field(default_factory=dict)
|
||||||
|
evidence: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class TrialReport(StrictReportModel):
|
||||||
|
schema_version: Literal[1] = 1
|
||||||
|
identity: TrialIdentity
|
||||||
|
outcome: TrialOutcome
|
||||||
|
agent_self_report: dict[str, Any] | None = None
|
||||||
|
final_agent_answer: str | None = None
|
||||||
|
commands_and_tools: list[CommandToolBrief] = Field(default_factory=list)
|
||||||
|
automatic_evidence: AutomaticEvidence
|
||||||
|
policy_findings: list[str] = Field(default_factory=list)
|
||||||
|
self_report_discrepancies: list[str] = Field(default_factory=list)
|
||||||
|
manual_audit: ManualAuditSummary = Field(default_factory=ManualAuditSummary)
|
||||||
|
follow_up_notes: list[str] = Field(default_factory=list)
|
||||||
|
```
|
||||||
|
|
||||||
|
Identity holds challenge/model/variant/profile/index, repository provenance,
|
||||||
|
prompt hashes, raw path, and workspace path. Outcome holds task outcome,
|
||||||
|
validity, duration, return code, assertions, and parser errors. Evidence holds
|
||||||
|
bounded metrics, read categories, disallowed reads, and opaque commands.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement the builder**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def build_trial_report(
|
||||||
|
result: dict[str, object],
|
||||||
|
*,
|
||||||
|
audit: dict[str, object] | None,
|
||||||
|
) -> TrialReport:
|
||||||
|
"""Project immutable raw evidence and optional audit into one report."""
|
||||||
|
```
|
||||||
|
|
||||||
|
Use focused helpers. Require explicit paths; bound final text to 8,000 chars and
|
||||||
|
command detail to 1,000; retain only tool ordinal/name/status/title/detail,
|
||||||
|
failure, output size/hash; exclude stdout/stderr/previews/metadata/full input.
|
||||||
|
Flag observable self-report conflicts. Treat example reads plus
|
||||||
|
`existing_solution=false` as a manual follow-up, not automatic guilt.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify and commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_reports.py -q
|
||||||
|
git add examples/agent_challenges/report_models.py examples/agent_challenges/reports.py tests/examples/test_agent_challenge_reports.py
|
||||||
|
git commit -m "feat: add normalized agent trial report model"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Render And Write Both Projections
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `examples/agent_challenges/reports.py`
|
||||||
|
- Modify: `tests/examples/test_agent_challenge_reports.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing projection tests**
|
||||||
|
|
||||||
|
Assert writes to `workspace/final-report.md` and `results/trial.report.json`,
|
||||||
|
stable heading order, bounded commands, no raw outputs, pending audit, valid
|
||||||
|
JSON, and no temporary-file residue.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run and verify missing APIs**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_reports.py -k projection -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement Markdown renderer**
|
||||||
|
|
||||||
|
Use exactly these headings:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Trial Report
|
||||||
|
## Outcome
|
||||||
|
## Agent Self-Report
|
||||||
|
## Commands And Tool Calls
|
||||||
|
## Automatic Evidence
|
||||||
|
## Policy Findings
|
||||||
|
## Self-Report Discrepancies
|
||||||
|
## Manual Audit
|
||||||
|
## Follow-Up Notes
|
||||||
|
```
|
||||||
|
|
||||||
|
Render empty sections explicitly and commands as ordered bounded entries.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement atomic writes**
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TrialReportPaths:
|
||||||
|
markdown: Path
|
||||||
|
machine: Path
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_write_text(path: Path, text: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_name(f".{path.name}.tmp")
|
||||||
|
temporary.write_text(text, encoding="utf-8")
|
||||||
|
temporary.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def write_trial_report_projections(
|
||||||
|
report: TrialReport,
|
||||||
|
*,
|
||||||
|
markdown_path: Path,
|
||||||
|
machine_path: Path,
|
||||||
|
) -> TrialReportPaths:
|
||||||
|
machine = json.dumps(
|
||||||
|
report.model_dump(mode="json"), indent=2, sort_keys=True
|
||||||
|
) + "\n"
|
||||||
|
markdown = render_trial_report_markdown(report).rstrip() + "\n"
|
||||||
|
_atomic_write_text(machine_path, machine)
|
||||||
|
_atomic_write_text(markdown_path, markdown)
|
||||||
|
return TrialReportPaths(markdown=markdown_path, machine=machine_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify and commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_reports.py -q
|
||||||
|
git add examples/agent_challenges/reports.py tests/examples/test_agent_challenge_reports.py
|
||||||
|
git commit -m "feat: write human and machine trial reports"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Generate Reports From The Runner
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `examples/agent_challenges/runner.py`
|
||||||
|
- Modify: `examples/agent_challenges/run_trials.py`
|
||||||
|
- Modify: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend runner integration tests**
|
||||||
|
|
||||||
|
For success and timeout, assert explicit `workspace_path`, `result_path`, and
|
||||||
|
`report_paths`; raw, Markdown, and machine files exist; machine challenge id is
|
||||||
|
correct; Markdown contains the final answer.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run and verify failure**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k "runner_to_report or timeout" -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add explicit paths before the single raw write**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"challenge_id": challenge.manifest.id,
|
||||||
|
"workspace_path": str(workspace.root.resolve()),
|
||||||
|
"result_path": str(result_path.resolve()),
|
||||||
|
"report_paths": {
|
||||||
|
"markdown": str((workspace.root / "final-report.md").resolve()),
|
||||||
|
"machine": str(result_path.with_suffix(".report.json").resolve()),
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Include `reads_by_category` in policy. Write raw result once before projection
|
||||||
|
generation; never rewrite it.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Generate pending-audit projections**
|
||||||
|
|
||||||
|
Call `build_trial_report(result, audit=None)` and
|
||||||
|
`write_trial_report_projections`. On failure preserve raw evidence and return a
|
||||||
|
concise `report_generation_error`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Print report paths**
|
||||||
|
|
||||||
|
Add `result_path` and `report_paths` to each central-runner summary.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify and commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k "runner or timeout" -q
|
||||||
|
git add examples/agent_challenges/runner.py examples/agent_challenges/run_trials.py tests/examples/test_agent_challenge_harness_v2.py
|
||||||
|
git commit -m "feat: generate reports after agent trials"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Regenerate Reports After Manual Audit
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `examples/agent_challenges/audit.py`
|
||||||
|
- Modify: `examples/agent_challenges/save_manual_audit.py`
|
||||||
|
- Modify: `tests/examples/test_agent_challenge_reports.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing regeneration test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
paths = save_v2_manual_audit(
|
||||||
|
result_path,
|
||||||
|
official_outcome="pass",
|
||||||
|
auditor="reviewer",
|
||||||
|
audited_at="2026-06-23T00:00:00Z",
|
||||||
|
read_overrides={"existing_solution": True},
|
||||||
|
corrections=["Agent inspected a ready-made workflow plan."],
|
||||||
|
notes="Technical run passed; self-report corrected.",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert YAML, JSON, and Markdown contain the official grade/corrections.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add invalid-audit preservation test**
|
||||||
|
|
||||||
|
Use outcome `maybe`, expect `ValueError`, and assert previous projections remain
|
||||||
|
byte-for-byte unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run and verify missing API**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_reports.py -k "manual_audit or invalid" -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement V2 audit writer**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class V2AuditPaths:
|
||||||
|
audit: Path
|
||||||
|
markdown: Path
|
||||||
|
machine: Path
|
||||||
|
|
||||||
|
|
||||||
|
def save_v2_manual_audit(
|
||||||
|
result_path: Path,
|
||||||
|
*,
|
||||||
|
official_outcome: str,
|
||||||
|
auditor: str = "human",
|
||||||
|
audited_at: str | None = None,
|
||||||
|
read_overrides: dict[str, bool] | None = None,
|
||||||
|
evidence_overrides: dict[str, object] | None = None,
|
||||||
|
corrections: list[str] | None = None,
|
||||||
|
notes: str = "",
|
||||||
|
) -> V2AuditPaths:
|
||||||
|
"""Write authoritative audit data and regenerate both report projections."""
|
||||||
|
```
|
||||||
|
|
||||||
|
Allow only `pass`, `fail`, `invalid`; require V2; use explicit raw paths;
|
||||||
|
validate before writes; atomically write audit; rebuild both projections; never
|
||||||
|
rewrite raw result.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Route existing CLI by harness version**
|
||||||
|
|
||||||
|
For V2, interpret `--manual-classification` as official outcome, reject
|
||||||
|
`--from-report`, and print JSON containing all three paths. Preserve referenced
|
||||||
|
V1 behavior.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify and commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_reports.py -q
|
||||||
|
uv run python examples/agent_challenges/save_manual_audit.py --help
|
||||||
|
git add examples/agent_challenges/audit.py examples/agent_challenges/save_manual_audit.py tests/examples/test_agent_challenge_reports.py
|
||||||
|
git commit -m "feat: regenerate trial reports after audit"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Clarify Shared Self-Reporting Rules
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `examples/agent_challenges/base-prompt.md`
|
||||||
|
- Modify: `tests/examples/test_agent_challenge_harness_v2.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing assertions**
|
||||||
|
|
||||||
|
Assert the prompt mentions `tests/`, `examples/`, `read.product_code: true`,
|
||||||
|
`read.existing_solution: true`, and `read.adjacent_attempts: true`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add approved paragraph**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Files under `tests/` and `examples/` may contain complete or partial solutions.
|
||||||
|
If you inspect them, report `read.product_code: true`; also report
|
||||||
|
`read.existing_solution: true` when they provide a ready-made solution, or
|
||||||
|
`read.adjacent_attempts: true` when they contain prior trial outputs.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify and commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_harness_v2.py -k base_prompt -q
|
||||||
|
git add examples/agent_challenges/base-prompt.md tests/examples/test_agent_challenge_harness_v2.py
|
||||||
|
git commit -m "docs: clarify challenge self-report rules"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 6: Final Verification And Documentation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/current_roadmap.md`
|
||||||
|
- Move this plan to `docs/historical/superpowers/plans/`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run verification**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/examples/test_agent_challenge_reports.py tests/examples/test_agent_challenge_harness_v2.py tests/examples/test_agent_challenge_skill_bundle.py tests/examples/test_opencode_browser_click_challenge.py tests/examples/test_report_workflow_challenge.py -q
|
||||||
|
uv run ruff check examples/agent_challenges tests/examples/test_agent_challenge_reports.py tests/examples/test_agent_challenge_harness_v2.py
|
||||||
|
uv run ruff format --check examples/agent_challenges tests/examples/test_agent_challenge_reports.py tests/examples/test_agent_challenge_harness_v2.py
|
||||||
|
uv run basedpyright --level error examples/agent_challenges tests/examples/test_agent_challenge_reports.py tests/examples/test_agent_challenge_harness_v2.py
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Smoke CLI help**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run python examples/agent_challenges/run_trials.py --help
|
||||||
|
uv run python examples/agent_challenges/save_manual_audit.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update roadmap and archive**
|
||||||
|
|
||||||
|
Record completion, then move this plan to
|
||||||
|
`docs/historical/superpowers/plans/2026-06-23-agent-challenge-report-projections.md`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Review boundaries**
|
||||||
|
|
||||||
|
Confirm raw result is written once, machine report excludes raw outputs,
|
||||||
|
Markdown headings are stable, explicit paths are used, audit regenerates both,
|
||||||
|
V1 remains intact, and no runbook/branching challenge leaked into this slice.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit completion docs**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add docs/current_roadmap.md docs/superpowers/plans/2026-06-23-agent-challenge-report-projections.md docs/historical/superpowers/plans/2026-06-23-agent-challenge-report-projections.md
|
||||||
|
git commit -m "docs: record agent challenge report projections"
|
||||||
|
```
|
||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -9,7 +10,11 @@ from typing import Any
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from examples.agent_challenges.classification import extract_challenge_report
|
from examples.agent_challenges.classification import extract_challenge_report
|
||||||
from examples.agent_challenges.reports import report_from_result
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
from examples.agent_challenges.reports import (
|
||||||
|
report_from_result,
|
||||||
|
write_trial_report_projections,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def audit_from_result(
|
def audit_from_result(
|
||||||
@@ -256,3 +261,109 @@ def manual_audit_from_v2_result(
|
|||||||
output_path = Path(output_name)
|
output_path = Path(output_name)
|
||||||
|
|
||||||
return output_path, audit
|
return output_path, audit
|
||||||
|
|
||||||
|
|
||||||
|
_VALID_OFFICIAL_OUTCOMES = frozenset({"pass", "fail", "invalid"})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class V2AuditPaths:
|
||||||
|
audit: Path
|
||||||
|
markdown: Path
|
||||||
|
machine: Path
|
||||||
|
|
||||||
|
|
||||||
|
def save_v2_manual_audit(
|
||||||
|
result_path: Path,
|
||||||
|
*,
|
||||||
|
official_outcome: str,
|
||||||
|
auditor: str = "human",
|
||||||
|
audited_at: str | None = None,
|
||||||
|
read_overrides: dict[str, bool] | None = None,
|
||||||
|
evidence_overrides: dict[str, object] | None = None,
|
||||||
|
corrections: list[str] | None = None,
|
||||||
|
notes: str = "",
|
||||||
|
) -> V2AuditPaths:
|
||||||
|
"""Write authoritative audit data and regenerate both report projections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result_path: Path to the V2 raw result JSON file.
|
||||||
|
official_outcome: One of 'pass', 'fail', 'invalid'.
|
||||||
|
auditor: Name or identifier of the human auditor.
|
||||||
|
audited_at: ISO-8601 timestamp string.
|
||||||
|
read_overrides: Override read flags from the challenge report.
|
||||||
|
evidence_overrides: Override evidence values from the challenge report.
|
||||||
|
corrections: List of correction strings.
|
||||||
|
notes: Free-form auditor notes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
V2AuditPaths with paths to audit YAML, markdown report, and machine report.
|
||||||
|
"""
|
||||||
|
if official_outcome not in _VALID_OFFICIAL_OUTCOMES:
|
||||||
|
raise ValueError(
|
||||||
|
f"official_outcome must be one of {sorted(_VALID_OFFICIAL_OUTCOMES)}, "
|
||||||
|
f"got {official_outcome!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
raise ValueError("result file must contain a JSON object")
|
||||||
|
|
||||||
|
harness_version = result.get("harness_version")
|
||||||
|
if harness_version != "v2":
|
||||||
|
raise ValueError(
|
||||||
|
f"save_v2_manual_audit requires harness_version='v2', "
|
||||||
|
f"got {harness_version!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_path_str = result.get("workspace_path")
|
||||||
|
if not isinstance(workspace_path_str, str) or not workspace_path_str:
|
||||||
|
raise ValueError("result is missing workspace_path")
|
||||||
|
workspace_path = Path(workspace_path_str)
|
||||||
|
|
||||||
|
result_path_str = result.get("result_path")
|
||||||
|
if not isinstance(result_path_str, str) or not result_path_str:
|
||||||
|
raise ValueError("result is missing result_path")
|
||||||
|
|
||||||
|
audit_payload: dict[str, object] = {
|
||||||
|
"manual_audit": {
|
||||||
|
"official_outcome": official_outcome,
|
||||||
|
"auditor": auditor,
|
||||||
|
"audited_at": audited_at or _utc_now(),
|
||||||
|
"task_outcome": result.get("task_outcome"),
|
||||||
|
"evaluation_validity": result.get("evaluation_validity"),
|
||||||
|
"corrections": corrections or [],
|
||||||
|
"notes": notes,
|
||||||
|
"read_flags": dict(read_overrides or {}),
|
||||||
|
"evidence": dict(evidence_overrides or {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yaml_text = yaml.safe_dump(audit_payload, sort_keys=False, allow_unicode=True)
|
||||||
|
|
||||||
|
trial_report = build_trial_report(
|
||||||
|
result,
|
||||||
|
audit=audit_payload,
|
||||||
|
raw_result_path=result_path_str,
|
||||||
|
workspace_path=workspace_path_str,
|
||||||
|
)
|
||||||
|
|
||||||
|
markdown_path = workspace_path / "final-report.md"
|
||||||
|
machine_path = result_path.with_suffix(".report.json")
|
||||||
|
write_trial_report_projections(
|
||||||
|
trial_report,
|
||||||
|
markdown_path=markdown_path,
|
||||||
|
machine_path=machine_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
audit_path = workspace_path / "manual-audit.yaml"
|
||||||
|
audit_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = audit_path.with_name(f".{audit_path.name}.tmp")
|
||||||
|
temporary.write_text(yaml_text, encoding="utf-8")
|
||||||
|
temporary.replace(audit_path)
|
||||||
|
|
||||||
|
return V2AuditPaths(
|
||||||
|
audit=audit_path,
|
||||||
|
markdown=markdown_path,
|
||||||
|
machine=machine_path,
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,3 +14,8 @@ Use this command prefix:
|
|||||||
Your writable trial workspace is `{{workspace_path}}`. Write attempt files only
|
Your writable trial workspace is `{{workspace_path}}`. Write attempt files only
|
||||||
inside it. End with the challenge's requested YAML self-report. The self-report
|
inside it. End with the challenge's requested YAML self-report. The self-report
|
||||||
will be checked against observed tool calls and manually audited.
|
will be checked against observed tool calls and manually audited.
|
||||||
|
|
||||||
|
Files under `tests/` and `examples/` may contain complete or partial solutions.
|
||||||
|
If you inspect them, report `read.product_code: true`; also report
|
||||||
|
`read.existing_solution: true` when they provide a ready-made solution, or
|
||||||
|
`read.adjacent_attempts: true` when they contain prior trial outputs.
|
||||||
|
|||||||
@@ -0,0 +1,501 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class StrictReportModel(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|
||||||
|
class TrialIdentity(StrictReportModel):
|
||||||
|
challenge_id: str
|
||||||
|
model: str
|
||||||
|
variant: str
|
||||||
|
instruction_profile: str
|
||||||
|
trial_index: int
|
||||||
|
repository_commit: str | None = None
|
||||||
|
repository_dirty: bool | None = None
|
||||||
|
prompt_hashes: dict[str, str] = Field(default_factory=dict)
|
||||||
|
raw_result_path: str
|
||||||
|
workspace_path: str
|
||||||
|
|
||||||
|
|
||||||
|
class TrialOutcome(StrictReportModel):
|
||||||
|
task_outcome: str
|
||||||
|
evaluation_validity: str
|
||||||
|
duration_seconds: float
|
||||||
|
returncode: int | None = None
|
||||||
|
assertion_failures: list[str] = Field(default_factory=list)
|
||||||
|
parse_errors: dict[str, dict[str, str]] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandToolBrief(StrictReportModel):
|
||||||
|
ordinal: int
|
||||||
|
tool: str
|
||||||
|
status: str
|
||||||
|
title: str
|
||||||
|
detail: str | None = None
|
||||||
|
failed: bool
|
||||||
|
output_chars: int
|
||||||
|
output_sha256: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenSummary(StrictReportModel):
|
||||||
|
total: int = 0
|
||||||
|
input: int = 0
|
||||||
|
output: int = 0
|
||||||
|
reasoning: int = 0
|
||||||
|
cache_read: int = 0
|
||||||
|
cache_write: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class AutomaticEvidence(StrictReportModel):
|
||||||
|
step_count: int = 0
|
||||||
|
tool_call_count: int = 0
|
||||||
|
failed_tool_call_count: int = 0
|
||||||
|
tool_counts: dict[str, int] = Field(default_factory=dict)
|
||||||
|
tokens: TokenSummary = Field(default_factory=TokenSummary)
|
||||||
|
cost: float = 0.0
|
||||||
|
unknown_event_count: int = 0
|
||||||
|
reads_by_category: dict[str, list[str]] = Field(default_factory=dict)
|
||||||
|
escalated_to_product_code: bool = False
|
||||||
|
disallowed_reads: list[str] = Field(default_factory=list)
|
||||||
|
opaque_shell_commands: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ManualAuditSummary(StrictReportModel):
|
||||||
|
status: Literal["pending", "complete"] = "pending"
|
||||||
|
official_outcome: str | None = None
|
||||||
|
auditor: str | None = None
|
||||||
|
audited_at: str | None = None
|
||||||
|
corrections: list[str] = Field(default_factory=list)
|
||||||
|
notes: str = ""
|
||||||
|
read_flags: dict[str, bool] = Field(default_factory=dict)
|
||||||
|
evidence: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class TrialReport(StrictReportModel):
|
||||||
|
schema_version: Literal[1] = 1
|
||||||
|
identity: TrialIdentity
|
||||||
|
outcome: TrialOutcome
|
||||||
|
agent_self_report: dict[str, Any] | None = None
|
||||||
|
final_agent_answer: str | None = None
|
||||||
|
commands_and_tools: list[CommandToolBrief] = Field(default_factory=list)
|
||||||
|
automatic_evidence: AutomaticEvidence
|
||||||
|
policy_findings: list[str] = Field(default_factory=list)
|
||||||
|
self_report_discrepancies: list[str] = Field(default_factory=list)
|
||||||
|
manual_audit: ManualAuditSummary = Field(default_factory=ManualAuditSummary)
|
||||||
|
follow_up_notes: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
_MAX_FINAL_TEXT_CHARS = 8_000
|
||||||
|
_MAX_COMMAND_DETAIL_CHARS = 1_000
|
||||||
|
|
||||||
|
|
||||||
|
def _build_identity(
|
||||||
|
result: dict[str, object], raw_result_path: str, workspace_path: str
|
||||||
|
) -> TrialIdentity:
|
||||||
|
return TrialIdentity(
|
||||||
|
challenge_id=_str(result.get("challenge_id")),
|
||||||
|
model=_str(result.get("model")),
|
||||||
|
variant=_str(result.get("variant")),
|
||||||
|
instruction_profile=_str(result.get("instruction_profile")),
|
||||||
|
trial_index=_int(result.get("trial_index", result.get("index"))),
|
||||||
|
repository_commit=_str_none(result.get("repository_commit")),
|
||||||
|
repository_dirty=_bool_none(result.get("repository_dirty")),
|
||||||
|
prompt_hashes=_dict_str_str(result.get("prompt_hashes")),
|
||||||
|
raw_result_path=raw_result_path,
|
||||||
|
workspace_path=workspace_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_outcome(result: dict[str, object]) -> TrialOutcome:
|
||||||
|
return TrialOutcome(
|
||||||
|
task_outcome=_str(result.get("task_outcome")),
|
||||||
|
evaluation_validity=_str(result.get("evaluation_validity")),
|
||||||
|
duration_seconds=_float(result.get("duration_seconds")),
|
||||||
|
returncode=_int_none(result.get("returncode")),
|
||||||
|
assertion_failures=_list_str(result.get("assertion_failures")),
|
||||||
|
parse_errors=_parse_errors(result),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tool_briefs(result: dict[str, object]) -> list[CommandToolBrief]:
|
||||||
|
metrics = result.get("metrics")
|
||||||
|
if not isinstance(metrics, dict):
|
||||||
|
return []
|
||||||
|
tool_calls = metrics.get("tool_calls")
|
||||||
|
if not isinstance(tool_calls, list):
|
||||||
|
return []
|
||||||
|
briefs: list[CommandToolBrief] = []
|
||||||
|
for tc in tool_calls:
|
||||||
|
if not isinstance(tc, dict):
|
||||||
|
continue
|
||||||
|
raw_input = tc.get("input")
|
||||||
|
tc_input: dict[str, object] = {}
|
||||||
|
if isinstance(raw_input, dict):
|
||||||
|
tc_input = raw_input
|
||||||
|
detail_str = ""
|
||||||
|
path_val = (
|
||||||
|
tc_input.get("path")
|
||||||
|
or tc_input.get("filePath")
|
||||||
|
or tc_input.get("file")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
if isinstance(path_val, str) and path_val:
|
||||||
|
detail_str = path_val[:_MAX_COMMAND_DETAIL_CHARS]
|
||||||
|
elif "command" in tc_input:
|
||||||
|
cmd = tc_input["command"]
|
||||||
|
if isinstance(cmd, str):
|
||||||
|
detail_str = cmd[:_MAX_COMMAND_DETAIL_CHARS]
|
||||||
|
|
||||||
|
briefs.append(
|
||||||
|
CommandToolBrief(
|
||||||
|
ordinal=_int(tc.get("ordinal")),
|
||||||
|
tool=_str(tc.get("tool")),
|
||||||
|
status=_str(tc.get("status")),
|
||||||
|
title=_str(tc.get("title")),
|
||||||
|
detail=detail_str or None,
|
||||||
|
failed=bool(tc.get("failed", False)),
|
||||||
|
output_chars=_int(tc.get("output_chars")),
|
||||||
|
output_sha256=_str(tc.get("output_sha256")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return briefs
|
||||||
|
|
||||||
|
|
||||||
|
def _build_automatic_evidence(result: dict[str, object]) -> AutomaticEvidence:
|
||||||
|
metrics = result.get("metrics")
|
||||||
|
tokens = TokenSummary()
|
||||||
|
cost = 0.0
|
||||||
|
tool_counts: dict[str, int] = {}
|
||||||
|
step_count = 0
|
||||||
|
tool_call_count = 0
|
||||||
|
failed_tool_call_count = 0
|
||||||
|
unknown_event_count = 0
|
||||||
|
if isinstance(metrics, dict):
|
||||||
|
step_count = _int(metrics.get("step_count"))
|
||||||
|
tool_call_count = _int(metrics.get("tool_call_count"))
|
||||||
|
failed_tool_call_count = _int(metrics.get("failed_tool_call_count"))
|
||||||
|
tool_counts = _dict_str_int(metrics.get("tool_counts"))
|
||||||
|
unknown_event_count = _int(metrics.get("unknown_event_count"))
|
||||||
|
tokens_raw = metrics.get("tokens")
|
||||||
|
if isinstance(tokens_raw, dict):
|
||||||
|
tokens = TokenSummary(
|
||||||
|
total=_int(tokens_raw.get("total")),
|
||||||
|
input=_int(tokens_raw.get("input")),
|
||||||
|
output=_int(tokens_raw.get("output")),
|
||||||
|
reasoning=_int(tokens_raw.get("reasoning")),
|
||||||
|
cache_read=_int(tokens_raw.get("cache_read")),
|
||||||
|
cache_write=_int(tokens_raw.get("cache_write")),
|
||||||
|
)
|
||||||
|
cost = _float(metrics.get("cost"))
|
||||||
|
|
||||||
|
policy = result.get("policy")
|
||||||
|
reads_by_category: dict[str, list[str]] = {}
|
||||||
|
disallowed_reads: list[str] = []
|
||||||
|
escalated_to_product_code = False
|
||||||
|
opaque_shell_commands: list[str] = []
|
||||||
|
if isinstance(policy, dict):
|
||||||
|
reads_in = policy.get("reads_by_category")
|
||||||
|
if isinstance(reads_in, dict):
|
||||||
|
reads_by_category = {
|
||||||
|
k: list(v) if isinstance(v, (list, tuple)) else [str(v)]
|
||||||
|
for k, v in reads_in.items()
|
||||||
|
}
|
||||||
|
disallowed_reads = _list_str(policy.get("disallowed_reads"))
|
||||||
|
escalated_to_product_code = bool(policy.get("escalated_to_product_code", False))
|
||||||
|
opaque_shell_commands = _list_str(policy.get("opaque_shell_commands"))
|
||||||
|
|
||||||
|
return AutomaticEvidence(
|
||||||
|
step_count=step_count,
|
||||||
|
tool_call_count=tool_call_count,
|
||||||
|
failed_tool_call_count=failed_tool_call_count,
|
||||||
|
tool_counts=tool_counts,
|
||||||
|
tokens=tokens,
|
||||||
|
cost=cost,
|
||||||
|
unknown_event_count=unknown_event_count,
|
||||||
|
reads_by_category=reads_by_category,
|
||||||
|
escalated_to_product_code=escalated_to_product_code,
|
||||||
|
disallowed_reads=disallowed_reads,
|
||||||
|
opaque_shell_commands=opaque_shell_commands,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_trial_report(
|
||||||
|
result: dict[str, object],
|
||||||
|
*,
|
||||||
|
audit: dict[str, object] | None,
|
||||||
|
raw_result_path: str,
|
||||||
|
workspace_path: str,
|
||||||
|
) -> TrialReport:
|
||||||
|
identity = _build_identity(result, raw_result_path, workspace_path)
|
||||||
|
outcome = _build_outcome(result)
|
||||||
|
commands_and_tools = _build_tool_briefs(result)
|
||||||
|
automatic_evidence = _build_automatic_evidence(result)
|
||||||
|
|
||||||
|
agent_self_report: dict[str, Any] | None = None
|
||||||
|
challenge_report = result.get("challenge_report")
|
||||||
|
if isinstance(challenge_report, dict):
|
||||||
|
agent_self_report = challenge_report
|
||||||
|
|
||||||
|
final_agent_answer: str | None = None
|
||||||
|
parsed = result.get("parsed")
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
text = parsed.get("text")
|
||||||
|
if isinstance(text, str) and text.strip():
|
||||||
|
final_agent_answer = text[:_MAX_FINAL_TEXT_CHARS]
|
||||||
|
|
||||||
|
policy_findings: list[str] = _build_policy_findings(result, automatic_evidence)
|
||||||
|
self_report_discrepancies: list[str] = _build_self_report_discrepancies(
|
||||||
|
result, agent_self_report
|
||||||
|
)
|
||||||
|
follow_up_notes: list[str] = _build_follow_up_notes(result, automatic_evidence)
|
||||||
|
manual_audit = _build_manual_audit(audit)
|
||||||
|
|
||||||
|
return TrialReport(
|
||||||
|
identity=identity,
|
||||||
|
outcome=outcome,
|
||||||
|
agent_self_report=agent_self_report,
|
||||||
|
final_agent_answer=final_agent_answer,
|
||||||
|
commands_and_tools=commands_and_tools,
|
||||||
|
automatic_evidence=automatic_evidence,
|
||||||
|
policy_findings=policy_findings,
|
||||||
|
self_report_discrepancies=self_report_discrepancies,
|
||||||
|
follow_up_notes=follow_up_notes,
|
||||||
|
manual_audit=manual_audit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_policy_findings(
|
||||||
|
result: dict[str, object], evidence: AutomaticEvidence
|
||||||
|
) -> list[str]:
|
||||||
|
findings: list[str] = []
|
||||||
|
policy = result.get("policy")
|
||||||
|
if isinstance(policy, dict):
|
||||||
|
validity = policy.get("validity")
|
||||||
|
if isinstance(validity, str) and validity != "clean":
|
||||||
|
findings.append(f"Evaluation validity: {validity}")
|
||||||
|
if evidence.disallowed_reads:
|
||||||
|
findings.append(f"Disallowed reads ({len(evidence.disallowed_reads)} paths)")
|
||||||
|
if evidence.opaque_shell_commands:
|
||||||
|
findings.append(
|
||||||
|
f"Opaque shell commands ({len(evidence.opaque_shell_commands)} commands)"
|
||||||
|
)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def _build_self_report_discrepancies(
|
||||||
|
result: dict[str, object], agent_self_report: dict[str, Any] | None
|
||||||
|
) -> list[str]:
|
||||||
|
discrepancies: list[str] = []
|
||||||
|
if agent_self_report is None:
|
||||||
|
return discrepancies
|
||||||
|
|
||||||
|
task_outcome = _str(result.get("task_outcome"))
|
||||||
|
agent_run_failed = agent_self_report.get("run_failed")
|
||||||
|
|
||||||
|
if agent_run_failed is True and task_outcome == "success":
|
||||||
|
discrepancies.append(
|
||||||
|
"Agent reported run_failed=true but task_outcome is 'success'"
|
||||||
|
)
|
||||||
|
elif agent_run_failed is False and task_outcome == "failed":
|
||||||
|
discrepancies.append(
|
||||||
|
"Agent reported run_failed=false but task_outcome is 'failed'"
|
||||||
|
)
|
||||||
|
elif agent_run_failed is False and task_outcome == "timeout":
|
||||||
|
discrepancies.append(
|
||||||
|
"Agent reported run_failed=false but task ended in timeout"
|
||||||
|
)
|
||||||
|
|
||||||
|
_check_escalation_discrepancy(result, agent_self_report, discrepancies)
|
||||||
|
|
||||||
|
return discrepancies
|
||||||
|
|
||||||
|
|
||||||
|
def _check_escalation_discrepancy(
|
||||||
|
result: dict[str, object],
|
||||||
|
agent_self_report: dict[str, Any],
|
||||||
|
discrepancies: list[str],
|
||||||
|
) -> None:
|
||||||
|
policy_raw = result.get("policy")
|
||||||
|
if not isinstance(policy_raw, dict):
|
||||||
|
return
|
||||||
|
escalated = policy_raw.get("escalated_to_product_code")
|
||||||
|
if escalated is not True:
|
||||||
|
return
|
||||||
|
agent_read_raw = agent_self_report.get("read")
|
||||||
|
if isinstance(agent_read_raw, dict):
|
||||||
|
for k, v in agent_read_raw.items():
|
||||||
|
if k == "product_code" and v is True:
|
||||||
|
return
|
||||||
|
discrepancies.append(
|
||||||
|
"Agent escalated to product code but did not report read.product_code"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_follow_up_notes(
|
||||||
|
result: dict[str, object], evidence: AutomaticEvidence
|
||||||
|
) -> list[str]:
|
||||||
|
notes: list[str] = []
|
||||||
|
policy_raw = result.get("policy")
|
||||||
|
if isinstance(policy_raw, dict):
|
||||||
|
reads_by_cat_raw = policy_raw.get("reads_by_category")
|
||||||
|
if isinstance(reads_by_cat_raw, dict):
|
||||||
|
examples = reads_by_cat_raw.get("examples", [])
|
||||||
|
tests = reads_by_cat_raw.get("tests", [])
|
||||||
|
existing_solution_raw = reads_by_cat_raw.get("existing_solution", [])
|
||||||
|
if isinstance(examples, (list, tuple)) and examples:
|
||||||
|
notes.append(
|
||||||
|
f"Agent read {len(examples)} example file(s); verify whether "
|
||||||
|
"existing_solution applies"
|
||||||
|
)
|
||||||
|
if isinstance(tests, (list, tuple)) and tests:
|
||||||
|
notes.append(
|
||||||
|
f"Agent read {len(tests)} test file(s); verify whether "
|
||||||
|
"existing_solution applies"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
isinstance(existing_solution_raw, (list, tuple))
|
||||||
|
and existing_solution_raw
|
||||||
|
):
|
||||||
|
notes.append(
|
||||||
|
f"Agent found {len(existing_solution_raw)} existing solution "
|
||||||
|
"reference(s); verify self-report accuracy"
|
||||||
|
)
|
||||||
|
if evidence.disallowed_reads:
|
||||||
|
notes.append(
|
||||||
|
f"Disallowed reads ({len(evidence.disallowed_reads)} path(s)); "
|
||||||
|
"review for contamination impact"
|
||||||
|
)
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
def _build_manual_audit(
|
||||||
|
audit: dict[str, object] | None,
|
||||||
|
) -> ManualAuditSummary:
|
||||||
|
if audit is None:
|
||||||
|
return ManualAuditSummary()
|
||||||
|
manual = audit.get("manual_audit")
|
||||||
|
if isinstance(manual, dict):
|
||||||
|
return ManualAuditSummary(
|
||||||
|
status="complete",
|
||||||
|
official_outcome=_str_none(manual.get("official_outcome")),
|
||||||
|
auditor=_str_none(manual.get("auditor")),
|
||||||
|
audited_at=_str_none(manual.get("audited_at")),
|
||||||
|
corrections=_list_str(manual.get("corrections")),
|
||||||
|
notes=_str(manual.get("notes")),
|
||||||
|
read_flags=_dict_str_bool(manual.get("read_flags")),
|
||||||
|
evidence=_dict_any(manual.get("evidence")),
|
||||||
|
)
|
||||||
|
return ManualAuditSummary()
|
||||||
|
|
||||||
|
|
||||||
|
def build_trial_report(
|
||||||
|
result: dict[str, object],
|
||||||
|
*,
|
||||||
|
audit: dict[str, object] | None,
|
||||||
|
raw_result_path: str | None = None,
|
||||||
|
workspace_path: str | None = None,
|
||||||
|
) -> TrialReport:
|
||||||
|
if raw_result_path is None:
|
||||||
|
raw_result_path = _str(result.get("result_path"))
|
||||||
|
if workspace_path is None:
|
||||||
|
workspace_path = _str(result.get("workspace_path"))
|
||||||
|
if not raw_result_path:
|
||||||
|
raise ValueError("raw_result_path is required")
|
||||||
|
if not workspace_path:
|
||||||
|
raise ValueError("workspace_path is required")
|
||||||
|
return _build_trial_report(
|
||||||
|
result,
|
||||||
|
audit=audit,
|
||||||
|
raw_result_path=raw_result_path,
|
||||||
|
workspace_path=workspace_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _str(value: object, *, default: str = "") -> str:
|
||||||
|
return value if isinstance(value, str) else default
|
||||||
|
|
||||||
|
|
||||||
|
def _str_none(value: object) -> str | None:
|
||||||
|
return value if isinstance(value, str) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _int(value: object, *, default: int = 0) -> int:
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return int(value)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _int_none(value: object) -> int | None:
|
||||||
|
return value if isinstance(value, int) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _float(value: object, *, default: float = 0.0) -> float:
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _bool_none(value: object) -> bool | None:
|
||||||
|
return value if isinstance(value, bool) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _list_str(value: object) -> list[str]:
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
return [v for v in value if isinstance(v, str)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_str_str(value: object) -> dict[str, str]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {
|
||||||
|
k: str(v)
|
||||||
|
for k, v in value.items()
|
||||||
|
if isinstance(k, str) and isinstance(v, str)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_str_int(value: object) -> dict[str, int]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {
|
||||||
|
k: int(v)
|
||||||
|
for k, v in value.items()
|
||||||
|
if isinstance(k, str) and isinstance(v, (int, float))
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_str_bool(value: object) -> dict[str, bool]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {
|
||||||
|
k: bool(v)
|
||||||
|
for k, v in value.items()
|
||||||
|
if isinstance(k, str) and isinstance(v, bool)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_any(value: object) -> dict[str, Any]:
|
||||||
|
return value if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_errors(result: dict[str, object]) -> dict[str, dict[str, str]]:
|
||||||
|
errors: dict[str, dict[str, str]] = {}
|
||||||
|
parse_error = result.get("parse_error")
|
||||||
|
if isinstance(parse_error, dict):
|
||||||
|
err_type = parse_error.get("type")
|
||||||
|
err_msg = parse_error.get("message")
|
||||||
|
if isinstance(err_type, str) and isinstance(err_msg, str):
|
||||||
|
errors["parse_error"] = {"type": err_type, "message": err_msg}
|
||||||
|
report_parse_error = result.get("report_parse_error")
|
||||||
|
if isinstance(report_parse_error, dict):
|
||||||
|
err_type = report_parse_error.get("type")
|
||||||
|
err_msg = report_parse_error.get("message")
|
||||||
|
if isinstance(err_type, str) and isinstance(err_msg, str):
|
||||||
|
errors["report_parse_error"] = {"type": err_type, "message": err_msg}
|
||||||
|
return errors
|
||||||
@@ -3,10 +3,12 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from examples.agent_challenges.classification import extract_challenge_report
|
from examples.agent_challenges.classification import extract_challenge_report
|
||||||
from examples.agent_challenges.opencode_io import parse_opencode_output, result_text
|
from examples.agent_challenges.opencode_io import parse_opencode_output, result_text
|
||||||
|
from examples.agent_challenges.report_models import TrialReport
|
||||||
|
|
||||||
|
|
||||||
def save_report(
|
def save_report(
|
||||||
@@ -198,3 +200,169 @@ def report_from_v2_result(result: dict[str, object]) -> str:
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TrialReportPaths:
|
||||||
|
markdown: Path
|
||||||
|
machine: Path
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_write_text(path: Path, text: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_name(f".{path.name}.tmp")
|
||||||
|
temporary.write_text(text, encoding="utf-8")
|
||||||
|
temporary.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def render_trial_report_markdown(report: TrialReport) -> str:
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
lines.append("# Trial Report")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Outcome")
|
||||||
|
lines.append("")
|
||||||
|
o = report.outcome
|
||||||
|
lines.append(f"- Task outcome: {o.task_outcome}")
|
||||||
|
lines.append(f"- Evaluation validity: {o.evaluation_validity}")
|
||||||
|
lines.append(f"- Duration: {o.duration_seconds}s")
|
||||||
|
if o.returncode is not None:
|
||||||
|
lines.append(f"- Return code: {o.returncode}")
|
||||||
|
if o.assertion_failures:
|
||||||
|
lines.append("- Assertion failures:")
|
||||||
|
for af in o.assertion_failures:
|
||||||
|
lines.append(f" - {af}")
|
||||||
|
if o.parse_errors:
|
||||||
|
for key, err in o.parse_errors.items():
|
||||||
|
lines.append(
|
||||||
|
f"- Parse error ({key}): {err.get('type', '')} - {err.get('message', '')}"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Agent Self-Report")
|
||||||
|
lines.append("")
|
||||||
|
if report.agent_self_report is not None:
|
||||||
|
lines.append("```yaml")
|
||||||
|
for key, value in report.agent_self_report.items():
|
||||||
|
lines.append(f"{key}: {value}")
|
||||||
|
lines.append("```")
|
||||||
|
else:
|
||||||
|
lines.append("No agent self-report captured.")
|
||||||
|
if report.final_agent_answer:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Final agent answer:")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(report.final_agent_answer)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Commands And Tool Calls")
|
||||||
|
lines.append("")
|
||||||
|
if report.commands_and_tools:
|
||||||
|
for cmd in report.commands_and_tools:
|
||||||
|
parts = [
|
||||||
|
f"{cmd.ordinal}. **{cmd.tool}** ({cmd.status})",
|
||||||
|
f" - Title: {cmd.title}",
|
||||||
|
]
|
||||||
|
if cmd.detail:
|
||||||
|
parts.append(f" - Detail: `{cmd.detail}`")
|
||||||
|
parts.append(
|
||||||
|
f" - Output: {cmd.output_chars} chars, sha256: `{cmd.output_sha256}`"
|
||||||
|
)
|
||||||
|
lines.extend(parts)
|
||||||
|
else:
|
||||||
|
lines.append("No commands or tool calls recorded.")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Automatic Evidence")
|
||||||
|
lines.append("")
|
||||||
|
ev = report.automatic_evidence
|
||||||
|
lines.append(f"- Steps: {ev.step_count}")
|
||||||
|
lines.append(
|
||||||
|
f"- Tool calls: {ev.tool_call_count} ({ev.failed_tool_call_count} failed)"
|
||||||
|
)
|
||||||
|
if ev.tool_counts:
|
||||||
|
for tool, count in sorted(ev.tool_counts.items()):
|
||||||
|
lines.append(f" - {tool}: {count}")
|
||||||
|
lines.append(
|
||||||
|
f"- Tokens: total={ev.tokens.total}, input={ev.tokens.input}, output={ev.tokens.output}, reasoning={ev.tokens.reasoning}, cache_read={ev.tokens.cache_read}, cache_write={ev.tokens.cache_write}"
|
||||||
|
)
|
||||||
|
lines.append(f"- Cost: {ev.cost}")
|
||||||
|
if ev.unknown_event_count:
|
||||||
|
lines.append(f"- Unknown events: {ev.unknown_event_count}")
|
||||||
|
if ev.reads_by_category:
|
||||||
|
for category, paths in sorted(ev.reads_by_category.items()):
|
||||||
|
lines.append(f"- {category}: {len(paths)} path(s)")
|
||||||
|
if ev.disallowed_reads:
|
||||||
|
lines.append(f"- Disallowed reads: {len(ev.disallowed_reads)} path(s)")
|
||||||
|
if ev.opaque_shell_commands:
|
||||||
|
lines.append(
|
||||||
|
f"- Opaque shell commands: {len(ev.opaque_shell_commands)} command(s)"
|
||||||
|
)
|
||||||
|
if ev.escalated_to_product_code:
|
||||||
|
lines.append("- Escalated to product code: yes")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Policy Findings")
|
||||||
|
lines.append("")
|
||||||
|
if report.policy_findings:
|
||||||
|
for pf in report.policy_findings:
|
||||||
|
lines.append(f"- {pf}")
|
||||||
|
else:
|
||||||
|
lines.append("No policy findings.")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Self-Report Discrepancies")
|
||||||
|
lines.append("")
|
||||||
|
if report.self_report_discrepancies:
|
||||||
|
for sd in report.self_report_discrepancies:
|
||||||
|
lines.append(f"- {sd}")
|
||||||
|
else:
|
||||||
|
lines.append("No discrepancies detected.")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Manual Audit")
|
||||||
|
lines.append("")
|
||||||
|
ma = report.manual_audit
|
||||||
|
lines.append(f"- Status: {ma.status}")
|
||||||
|
if ma.official_outcome is not None:
|
||||||
|
lines.append(f"- Official outcome: {ma.official_outcome}")
|
||||||
|
if ma.auditor is not None:
|
||||||
|
lines.append(f"- Auditor: {ma.auditor}")
|
||||||
|
if ma.audited_at is not None:
|
||||||
|
lines.append(f"- Audited at: {ma.audited_at}")
|
||||||
|
if ma.corrections:
|
||||||
|
for c in ma.corrections:
|
||||||
|
lines.append(f"- Correction: {c}")
|
||||||
|
if ma.notes:
|
||||||
|
lines.append(f"- Notes: {ma.notes}")
|
||||||
|
if ma.read_flags:
|
||||||
|
for key, val in ma.read_flags.items():
|
||||||
|
lines.append(f"- read.{key}: {val}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("## Follow-Up Notes")
|
||||||
|
lines.append("")
|
||||||
|
if report.follow_up_notes:
|
||||||
|
for fn in report.follow_up_notes:
|
||||||
|
lines.append(f"- {fn}")
|
||||||
|
else:
|
||||||
|
lines.append("No follow-up notes.")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def write_trial_report_projections(
|
||||||
|
report: TrialReport,
|
||||||
|
*,
|
||||||
|
markdown_path: Path,
|
||||||
|
machine_path: Path,
|
||||||
|
) -> TrialReportPaths:
|
||||||
|
machine = (
|
||||||
|
json.dumps(report.model_dump(mode="json"), indent=2, sort_keys=True) + "\n"
|
||||||
|
)
|
||||||
|
markdown = render_trial_report_markdown(report).rstrip() + "\n"
|
||||||
|
_atomic_write_text(machine_path, machine)
|
||||||
|
_atomic_write_text(markdown_path, markdown)
|
||||||
|
return TrialReportPaths(markdown=markdown_path, machine=machine_path)
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
"task_outcome": result["task_outcome"],
|
"task_outcome": result["task_outcome"],
|
||||||
"evaluation_validity": result["evaluation_validity"],
|
"evaluation_validity": result["evaluation_validity"],
|
||||||
"duration_seconds": result["duration_seconds"],
|
"duration_seconds": result["duration_seconds"],
|
||||||
|
"result_path": result.get("result_path"),
|
||||||
|
"report_paths": result.get("report_paths"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
print(json.dumps(summaries[-1], sort_keys=True))
|
print(json.dumps(summaries[-1], sort_keys=True))
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ from examples.agent_challenges.opencode_io import ( # noqa: E402
|
|||||||
parse_opencode_output,
|
parse_opencode_output,
|
||||||
result_text,
|
result_text,
|
||||||
)
|
)
|
||||||
|
from examples.agent_challenges.report_models import ( # noqa: E402
|
||||||
|
build_trial_report,
|
||||||
|
)
|
||||||
from examples.agent_challenges.reports import ( # noqa: E402
|
from examples.agent_challenges.reports import ( # noqa: E402
|
||||||
save_report_from_result_payload,
|
save_report_from_result_payload,
|
||||||
|
write_trial_report_projections,
|
||||||
)
|
)
|
||||||
from examples.agent_challenges.workspace import ( # noqa: E402
|
from examples.agent_challenges.workspace import ( # noqa: E402
|
||||||
ChallengeDef,
|
ChallengeDef,
|
||||||
@@ -575,7 +579,23 @@ def run_v2_trial(
|
|||||||
if assertion_failures:
|
if assertion_failures:
|
||||||
task_outcome = "failed"
|
task_outcome = "failed"
|
||||||
|
|
||||||
|
results_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
result_path = (
|
||||||
|
results_dir
|
||||||
|
/ f"{model.replace('/', '_').replace(':', '_')}-trial-{index:03d}.json"
|
||||||
|
)
|
||||||
|
workspace_root_str = str(workspace.root.resolve())
|
||||||
|
result_path_str = str(result_path.resolve())
|
||||||
|
|
||||||
|
markdown_path = workspace.root / "final-report.md"
|
||||||
|
machine_report_path = result_path.with_suffix(".report.json")
|
||||||
|
report_paths = {
|
||||||
|
"markdown": str(markdown_path.resolve()),
|
||||||
|
"machine": str(machine_report_path.resolve()),
|
||||||
|
}
|
||||||
|
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
|
"challenge_id": challenge.manifest.id,
|
||||||
"instruction_profile": profile.value,
|
"instruction_profile": profile.value,
|
||||||
"task_outcome": task_outcome,
|
"task_outcome": task_outcome,
|
||||||
"evaluation_validity": policy.validity.value,
|
"evaluation_validity": policy.validity.value,
|
||||||
@@ -591,6 +611,9 @@ def run_v2_trial(
|
|||||||
"disallowed_reads": list(policy.disallowed_reads),
|
"disallowed_reads": list(policy.disallowed_reads),
|
||||||
"escalated_to_product_code": policy.escalated_to_product_code,
|
"escalated_to_product_code": policy.escalated_to_product_code,
|
||||||
"opaque_shell_commands": list(policy.opaque_shell_commands),
|
"opaque_shell_commands": list(policy.opaque_shell_commands),
|
||||||
|
"reads_by_category": {
|
||||||
|
k: list(v) for k, v in policy.reads_by_category.items()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"repository_commit": _get_git_commit(),
|
"repository_commit": _get_git_commit(),
|
||||||
"repository_dirty": _get_git_dirty(),
|
"repository_dirty": _get_git_dirty(),
|
||||||
@@ -598,11 +621,15 @@ def run_v2_trial(
|
|||||||
"index": index,
|
"index": index,
|
||||||
"model": model,
|
"model": model,
|
||||||
"variant": variant,
|
"variant": variant,
|
||||||
|
"trial_index": index,
|
||||||
"duration_seconds": round(duration_seconds, 3),
|
"duration_seconds": round(duration_seconds, 3),
|
||||||
"returncode": returncode,
|
"returncode": returncode,
|
||||||
"stdout": stdout,
|
"stdout": stdout,
|
||||||
"stderr": stderr,
|
"stderr": stderr,
|
||||||
"parsed": parsed_output,
|
"parsed": parsed_output,
|
||||||
|
"workspace_path": workspace_root_str,
|
||||||
|
"result_path": result_path_str,
|
||||||
|
"report_paths": report_paths,
|
||||||
}
|
}
|
||||||
|
|
||||||
if assertion_failures:
|
if assertion_failures:
|
||||||
@@ -614,13 +641,25 @@ def run_v2_trial(
|
|||||||
if challenge_report is not None:
|
if challenge_report is not None:
|
||||||
result["challenge_report"] = challenge_report
|
result["challenge_report"] = challenge_report
|
||||||
|
|
||||||
results_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
result_path = (
|
|
||||||
results_dir
|
|
||||||
/ f"{model.replace('/', '_').replace(':', '_')}-trial-{index:03d}.json"
|
|
||||||
)
|
|
||||||
result_path.write_text(
|
result_path.write_text(
|
||||||
json.dumps(result, indent=2, sort_keys=True), encoding="utf-8"
|
json.dumps(result, indent=2, sort_keys=True), encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
report_generation_error: str | None = None
|
||||||
|
try:
|
||||||
|
trial_report = build_trial_report(
|
||||||
|
result,
|
||||||
|
audit=None,
|
||||||
|
raw_result_path=result_path_str,
|
||||||
|
workspace_path=workspace_root_str,
|
||||||
|
)
|
||||||
|
write_trial_report_projections(
|
||||||
|
trial_report,
|
||||||
|
markdown_path=markdown_path,
|
||||||
|
machine_path=machine_report_path,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
report_generation_error = str(exc)
|
||||||
|
result["report_generation_error"] = report_generation_error
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,22 +1,110 @@
|
|||||||
"""Central CLI for saving agent challenge manual audits."""
|
"""Central CLI for saving agent challenge manual audits.
|
||||||
|
|
||||||
|
Routes to V1 or V2 audit logic based on the harness version in the result file.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .audit import main as audit_main
|
from .audit import main as audit_main
|
||||||
|
from .audit import save_v2_manual_audit
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_project_root = Path(__file__).resolve().parents[2]
|
_project_root = Path(__file__).resolve().parents[2]
|
||||||
if str(_project_root) not in sys.path:
|
if str(_project_root) not in sys.path:
|
||||||
sys.path.insert(0, str(_project_root))
|
sys.path.insert(0, str(_project_root))
|
||||||
from examples.agent_challenges.audit import main as audit_main
|
from examples.agent_challenges.audit import main as audit_main
|
||||||
|
from examples.agent_challenges.audit import save_v2_manual_audit
|
||||||
|
|
||||||
|
|
||||||
|
def _is_v2_result(result_path: Path) -> bool:
|
||||||
|
try:
|
||||||
|
result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||||
|
return isinstance(result, dict) and result.get("harness_version") == "v2"
|
||||||
|
except json.JSONDecodeError, OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--from-result", type=Path, required=True)
|
||||||
|
parser.add_argument("--from-report", type=Path, default=None)
|
||||||
|
parser.add_argument("--manual-classification", required=True)
|
||||||
|
parser.add_argument("--auditor", default="human")
|
||||||
|
parser.add_argument("--audited-at", default=None)
|
||||||
|
parser.add_argument("--set-read", action="append", default=[])
|
||||||
|
parser.add_argument("--set-evidence", action="append", default=[])
|
||||||
|
parser.add_argument("--correction", action="append", default=[])
|
||||||
|
parser.add_argument("--notes", default="")
|
||||||
|
parser.add_argument("--output-name", default="manual-audit.yaml")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
result_path = args.from_result
|
||||||
|
|
||||||
|
if _is_v2_result(result_path):
|
||||||
|
if args.from_report is not None:
|
||||||
|
parser.error("--from-report is not supported for V2 results")
|
||||||
|
read_overrides = dict(_parse_bool_assignment(item) for item in args.set_read)
|
||||||
|
evidence_overrides = dict(
|
||||||
|
_parse_value_assignment(item) for item in args.set_evidence
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
paths = save_v2_manual_audit(
|
||||||
|
result_path,
|
||||||
|
official_outcome=args.manual_classification,
|
||||||
|
auditor=args.auditor,
|
||||||
|
audited_at=args.audited_at,
|
||||||
|
read_overrides=read_overrides,
|
||||||
|
evidence_overrides=evidence_overrides,
|
||||||
|
corrections=list(args.correction),
|
||||||
|
notes=args.notes,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"audit": paths.audit.as_posix(),
|
||||||
|
"markdown": paths.markdown.as_posix(),
|
||||||
|
"machine": paths.machine.as_posix(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
except ValueError as exc:
|
||||||
|
parser.error(str(exc))
|
||||||
|
|
||||||
return audit_main(argv)
|
return audit_main(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_bool_assignment(value: str) -> tuple[str, bool]:
|
||||||
|
key, separator, raw = value.partition("=")
|
||||||
|
if separator != "=" or not key:
|
||||||
|
raise ValueError("expected KEY=true or KEY=false")
|
||||||
|
lowered = raw.lower()
|
||||||
|
if lowered == "true":
|
||||||
|
return key, True
|
||||||
|
if lowered == "false":
|
||||||
|
return key, False
|
||||||
|
raise ValueError("boolean override value must be true or false")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_value_assignment(value: str) -> tuple[str, object]:
|
||||||
|
key, separator, raw = value.partition("=")
|
||||||
|
if separator != "=" or not key:
|
||||||
|
raise ValueError("expected KEY=VALUE")
|
||||||
|
lowered = raw.lower()
|
||||||
|
if lowered == "true":
|
||||||
|
return key, True
|
||||||
|
if lowered == "false":
|
||||||
|
return key, False
|
||||||
|
try:
|
||||||
|
return key, int(raw)
|
||||||
|
except ValueError:
|
||||||
|
return key, raw
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -1092,3 +1092,132 @@ def test_both_challenges_produce_different_challenge_hashes_but_same_base(
|
|||||||
assert hashes["browser_none"] != hashes["report_none"]
|
assert hashes["browser_none"] != hashes["report_none"]
|
||||||
for profile in InstructionProfile:
|
for profile in InstructionProfile:
|
||||||
assert hashes[f"browser_{profile.value}"] != hashes[f"report_{profile.value}"]
|
assert hashes[f"browser_{profile.value}"] != hashes[f"report_{profile.value}"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_runner_to_report_success(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.runner import run_v2_trial
|
||||||
|
|
||||||
|
challenge = load_challenge_manifest(_write_manifest(tmp_path / "challenge"))
|
||||||
|
bundle = ROOT / "examples/agent_challenges/instruction_bundles/workflow_cli.yaml"
|
||||||
|
workspaces_dir = tmp_path / "workspaces"
|
||||||
|
results_dir = tmp_path / "results"
|
||||||
|
results_dir.mkdir()
|
||||||
|
|
||||||
|
agent_answer = "Deployment dep_abc created."
|
||||||
|
stdout_jsonl = "\n".join(
|
||||||
|
[
|
||||||
|
json.dumps({"type": "step_start", "step": 1}),
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": "step_finish",
|
||||||
|
"tokens": {"total": 50, "input": 20, "output": 15},
|
||||||
|
"cost": 0.002,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"part": {"text": f"Final answer: {agent_answer}"},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_run(
|
||||||
|
command: list[str],
|
||||||
|
*,
|
||||||
|
cwd: str,
|
||||||
|
text: bool,
|
||||||
|
capture_output: bool,
|
||||||
|
timeout: float | None,
|
||||||
|
check: bool,
|
||||||
|
) -> object:
|
||||||
|
return type(
|
||||||
|
"Result",
|
||||||
|
(),
|
||||||
|
{"returncode": 0, "stdout": stdout_jsonl, "stderr": ""},
|
||||||
|
)()
|
||||||
|
|
||||||
|
result = run_v2_trial(
|
||||||
|
challenge,
|
||||||
|
profile=InstructionProfile.NONE,
|
||||||
|
model="test-model",
|
||||||
|
variant="high",
|
||||||
|
index=1,
|
||||||
|
workspaces_dir=workspaces_dir,
|
||||||
|
results_dir=results_dir,
|
||||||
|
instruction_bundle=bundle,
|
||||||
|
run_fn=fake_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["challenge_id"] == "fixture"
|
||||||
|
assert isinstance(result["workspace_path"], str)
|
||||||
|
assert isinstance(result["result_path"], str)
|
||||||
|
assert isinstance(result["report_paths"], dict)
|
||||||
|
assert "markdown" in result["report_paths"]
|
||||||
|
assert "machine" in result["report_paths"]
|
||||||
|
|
||||||
|
raw_path = Path(result["result_path"])
|
||||||
|
md_path = Path(result["report_paths"]["markdown"])
|
||||||
|
machine_path = Path(result["report_paths"]["machine"])
|
||||||
|
|
||||||
|
assert raw_path.is_file()
|
||||||
|
assert md_path.is_file()
|
||||||
|
assert machine_path.is_file()
|
||||||
|
|
||||||
|
machine = json.loads(machine_path.read_text(encoding="utf-8"))
|
||||||
|
assert machine["identity"]["challenge_id"] == "fixture"
|
||||||
|
|
||||||
|
md = md_path.read_text(encoding="utf-8")
|
||||||
|
assert agent_answer in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_runner_to_report_timeout(tmp_path: Path) -> None:
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
from examples.agent_challenges.runner import run_v2_trial
|
||||||
|
|
||||||
|
challenge = load_challenge_manifest(_write_manifest(tmp_path / "challenge"))
|
||||||
|
bundle = ROOT / "examples/agent_challenges/instruction_bundles/workflow_cli.yaml"
|
||||||
|
workspaces_dir = tmp_path / "workspaces"
|
||||||
|
results_dir = tmp_path / "results"
|
||||||
|
results_dir.mkdir()
|
||||||
|
|
||||||
|
def fake_run(
|
||||||
|
command: list[str],
|
||||||
|
*,
|
||||||
|
cwd: str,
|
||||||
|
text: bool,
|
||||||
|
capture_output: bool,
|
||||||
|
timeout: float | None,
|
||||||
|
check: bool,
|
||||||
|
) -> object:
|
||||||
|
raise sp.TimeoutExpired(cmd=command, timeout=3600)
|
||||||
|
|
||||||
|
result = run_v2_trial(
|
||||||
|
challenge,
|
||||||
|
profile=InstructionProfile.NONE,
|
||||||
|
model="test-model",
|
||||||
|
variant="high",
|
||||||
|
index=1,
|
||||||
|
workspaces_dir=workspaces_dir,
|
||||||
|
results_dir=results_dir,
|
||||||
|
instruction_bundle=bundle,
|
||||||
|
run_fn=fake_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["task_outcome"] == "timeout"
|
||||||
|
assert isinstance(result.get("workspace_path"), str)
|
||||||
|
assert isinstance(result.get("report_paths"), dict)
|
||||||
|
|
||||||
|
|
||||||
|
BASE_PROMPT = ROOT / "examples/agent_challenges/base-prompt.md"
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_prompt_mentions_self_report_rules() -> None:
|
||||||
|
text = BASE_PROMPT.read_text(encoding="utf-8")
|
||||||
|
assert "tests/" in text or "tests" in text
|
||||||
|
assert "examples/" in text or "examples" in text
|
||||||
|
assert "read.product_code" in text
|
||||||
|
assert "read.existing_solution" in text
|
||||||
|
assert "read.adjacent_attempts" in text
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def _raw_result(tmp_path: Path) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"instruction_profile": "none",
|
||||||
|
"task_outcome": "success",
|
||||||
|
"evaluation_validity": "clean",
|
||||||
|
"challenge_id": "fixture",
|
||||||
|
"model": "test-model",
|
||||||
|
"variant": "high",
|
||||||
|
"trial_index": 1,
|
||||||
|
"prompt_hashes": {
|
||||||
|
"base": "abc",
|
||||||
|
"profile": "def",
|
||||||
|
"challenge": "ghi",
|
||||||
|
"rendered": "jkl",
|
||||||
|
},
|
||||||
|
"repository_commit": "abc123def",
|
||||||
|
"repository_dirty": False,
|
||||||
|
"result_path": str(tmp_path / "results" / "trial.json"),
|
||||||
|
"workspace_path": str(tmp_path),
|
||||||
|
"metrics": {
|
||||||
|
"step_count": 2,
|
||||||
|
"tool_call_count": 3,
|
||||||
|
"failed_tool_call_count": 0,
|
||||||
|
"tool_counts": {"bash": 2, "read": 1},
|
||||||
|
"tokens": {
|
||||||
|
"total": 500,
|
||||||
|
"input": 200,
|
||||||
|
"output": 200,
|
||||||
|
"reasoning": 50,
|
||||||
|
"cache_read": 50,
|
||||||
|
"cache_write": 0,
|
||||||
|
},
|
||||||
|
"cost": 0.025,
|
||||||
|
"unknown_event_count": 0,
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"call_id": "c1",
|
||||||
|
"tool": "read",
|
||||||
|
"status": "success",
|
||||||
|
"title": "Read workflow plan",
|
||||||
|
"input": {"path": str(tmp_path / "workflow.plan.json")},
|
||||||
|
"metadata": {},
|
||||||
|
"output_chars": 500,
|
||||||
|
"output_preview": "full tool output",
|
||||||
|
"output_sha256": "abc",
|
||||||
|
"failed": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"call_id": "c2",
|
||||||
|
"tool": "bash",
|
||||||
|
"status": "success",
|
||||||
|
"title": "Run workflow",
|
||||||
|
"input": {"command": "uv run wf status"},
|
||||||
|
"metadata": {},
|
||||||
|
"output_chars": 200,
|
||||||
|
"output_preview": "large raw stream",
|
||||||
|
"output_sha256": "def",
|
||||||
|
"failed": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"validity": "clean",
|
||||||
|
"disallowed_reads": [],
|
||||||
|
"escalated_to_product_code": False,
|
||||||
|
"opaque_shell_commands": [],
|
||||||
|
"reads_by_category": {
|
||||||
|
"workspace": [str(tmp_path / "workflow.plan.json")],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"stdout": "large raw stdout content that should not appear in bounded report\n"
|
||||||
|
* 1000,
|
||||||
|
"stderr": "",
|
||||||
|
"parsed": {"text": "The deployment succeeded with id dep_123."},
|
||||||
|
"assertion_failures": [],
|
||||||
|
"parse_errors": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_trial_report_is_bounded_machine_projection(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
payload = build_trial_report(_raw_result(tmp_path), audit=None).model_dump(
|
||||||
|
mode="json"
|
||||||
|
)
|
||||||
|
assert payload["schema_version"] == 1
|
||||||
|
assert payload["identity"]["challenge_id"] == "fixture"
|
||||||
|
assert payload["identity"]["raw_result_path"] == str(
|
||||||
|
tmp_path / "results" / "trial.json"
|
||||||
|
)
|
||||||
|
assert payload["identity"]["workspace_path"] == str(tmp_path)
|
||||||
|
assert payload["outcome"]["task_outcome"] == "success"
|
||||||
|
assert payload["commands_and_tools"][0]["detail"].endswith("workflow.plan.json")
|
||||||
|
serialized = json.dumps(payload)
|
||||||
|
assert "large raw stream" not in serialized
|
||||||
|
assert "full tool output" not in serialized
|
||||||
|
assert payload["commands_and_tools"][0]["output_chars"] == 500
|
||||||
|
assert payload["commands_and_tools"][0]["output_sha256"] == "abc"
|
||||||
|
assert payload["manual_audit"]["status"] == "pending"
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_brief_supports_filepath(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
metrics = result.get("metrics", {})
|
||||||
|
if isinstance(metrics, dict):
|
||||||
|
metrics["tool_calls"] = [
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"call_id": "c3",
|
||||||
|
"tool": "read",
|
||||||
|
"status": "success",
|
||||||
|
"title": "Read source file",
|
||||||
|
"input": {"filePath": str(tmp_path / "src" / "app.py")},
|
||||||
|
"metadata": {},
|
||||||
|
"output_chars": 100,
|
||||||
|
"output_preview": "content",
|
||||||
|
"output_sha256": "xyz",
|
||||||
|
"failed": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
payload = build_trial_report(result, audit=None).model_dump(mode="json")
|
||||||
|
cmd = payload["commands_and_tools"][0]
|
||||||
|
assert cmd["detail"] is not None
|
||||||
|
assert cmd["detail"].endswith("app.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_markdown_projection_has_stable_headings(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
from examples.agent_challenges.reports import render_trial_report_markdown
|
||||||
|
|
||||||
|
report = build_trial_report(_raw_result(tmp_path), audit=None)
|
||||||
|
md = render_trial_report_markdown(report)
|
||||||
|
|
||||||
|
expected_headings = [
|
||||||
|
"# Trial Report",
|
||||||
|
"## Outcome",
|
||||||
|
"## Agent Self-Report",
|
||||||
|
"## Commands And Tool Calls",
|
||||||
|
"## Automatic Evidence",
|
||||||
|
"## Policy Findings",
|
||||||
|
"## Self-Report Discrepancies",
|
||||||
|
"## Manual Audit",
|
||||||
|
"## Follow-Up Notes",
|
||||||
|
]
|
||||||
|
for heading in expected_headings:
|
||||||
|
assert heading in md, f"Missing heading: {heading}"
|
||||||
|
|
||||||
|
assert "Final agent answer" in md
|
||||||
|
assert "The deployment succeeded" in md
|
||||||
|
assert "large raw stream" not in md
|
||||||
|
assert "full tool output" not in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_projections_write_both_files(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
from examples.agent_challenges.reports import (
|
||||||
|
TrialReportPaths,
|
||||||
|
write_trial_report_projections,
|
||||||
|
)
|
||||||
|
|
||||||
|
report = build_trial_report(_raw_result(tmp_path), audit=None)
|
||||||
|
markdown_path = tmp_path / "final-report.md"
|
||||||
|
machine_path = tmp_path / "trial.report.json"
|
||||||
|
|
||||||
|
paths = write_trial_report_projections(
|
||||||
|
report,
|
||||||
|
markdown_path=markdown_path,
|
||||||
|
machine_path=machine_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(paths, TrialReportPaths)
|
||||||
|
assert markdown_path.is_file()
|
||||||
|
assert machine_path.is_file()
|
||||||
|
|
||||||
|
md_content = markdown_path.read_text(encoding="utf-8")
|
||||||
|
assert "# Trial Report" in md_content
|
||||||
|
assert "## Outcome" in md_content
|
||||||
|
|
||||||
|
machine_content = json.loads(machine_path.read_text(encoding="utf-8"))
|
||||||
|
assert machine_content["schema_version"] == 1
|
||||||
|
assert machine_content["manual_audit"]["status"] == "pending"
|
||||||
|
|
||||||
|
assert list(tmp_path.iterdir()) == [markdown_path, machine_path]
|
||||||
|
|
||||||
|
|
||||||
|
def test_projections_exclude_raw_outputs(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
from examples.agent_challenges.reports import write_trial_report_projections
|
||||||
|
|
||||||
|
report = build_trial_report(_raw_result(tmp_path), audit=None)
|
||||||
|
markdown_path = tmp_path / "final-report.md"
|
||||||
|
machine_path = tmp_path / "trial.report.json"
|
||||||
|
|
||||||
|
write_trial_report_projections(
|
||||||
|
report,
|
||||||
|
markdown_path=markdown_path,
|
||||||
|
machine_path=machine_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
md = markdown_path.read_text(encoding="utf-8")
|
||||||
|
assert "large raw stream" not in md
|
||||||
|
assert "full tool output" not in md
|
||||||
|
|
||||||
|
machine = json.loads(machine_path.read_text(encoding="utf-8"))
|
||||||
|
serialized = json.dumps(machine)
|
||||||
|
assert "large raw stream" not in serialized
|
||||||
|
assert "full tool output" not in serialized
|
||||||
|
|
||||||
|
|
||||||
|
def _write_v2_result(tmp_path: Path) -> Path:
|
||||||
|
result_dir = tmp_path / "results"
|
||||||
|
result_dir.mkdir(parents=True)
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
result["harness_version"] = "v2"
|
||||||
|
result["result_path"] = str(tmp_path / "results" / "trial.json")
|
||||||
|
result["workspace_path"] = str(tmp_path)
|
||||||
|
path = result_dir / "trial.json"
|
||||||
|
path.write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _write_v2_result_with_projections(tmp_path: Path) -> Path:
|
||||||
|
result_path = _write_v2_result(tmp_path)
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
from examples.agent_challenges.reports import write_trial_report_projections
|
||||||
|
|
||||||
|
result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
write_trial_report_projections(
|
||||||
|
report,
|
||||||
|
markdown_path=tmp_path / "final-report.md",
|
||||||
|
machine_path=tmp_path / "results" / "trial.report.json",
|
||||||
|
)
|
||||||
|
return result_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_audit_regenerates_projections(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.audit import save_v2_manual_audit
|
||||||
|
|
||||||
|
result_path = _write_v2_result(tmp_path)
|
||||||
|
|
||||||
|
paths = save_v2_manual_audit(
|
||||||
|
result_path,
|
||||||
|
official_outcome="pass",
|
||||||
|
auditor="reviewer",
|
||||||
|
audited_at="2026-06-23T00:00:00Z",
|
||||||
|
read_overrides={"existing_solution": True},
|
||||||
|
corrections=["Agent inspected a ready-made workflow plan."],
|
||||||
|
notes="Technical run passed; self-report corrected.",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert paths.audit.is_file()
|
||||||
|
assert paths.markdown.is_file()
|
||||||
|
assert paths.machine.is_file()
|
||||||
|
|
||||||
|
audit_yaml = paths.audit.read_text(encoding="utf-8")
|
||||||
|
assert "official_outcome: pass" in audit_yaml
|
||||||
|
assert "Agent inspected a ready-made workflow plan." in audit_yaml
|
||||||
|
|
||||||
|
md = paths.markdown.read_text(encoding="utf-8")
|
||||||
|
assert "Official outcome: pass" in md
|
||||||
|
assert "Agent inspected a ready-made workflow plan." in md
|
||||||
|
|
||||||
|
machine = json.loads(paths.machine.read_text(encoding="utf-8"))
|
||||||
|
assert machine["manual_audit"]["official_outcome"] == "pass"
|
||||||
|
assert machine["manual_audit"]["status"] == "complete"
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_audit_invalid_outcome_raises_and_preserves_projections(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
from examples.agent_challenges.audit import save_v2_manual_audit
|
||||||
|
|
||||||
|
result_path = _write_v2_result_with_projections(tmp_path)
|
||||||
|
md_path = tmp_path / "final-report.md"
|
||||||
|
machine_path = tmp_path / "results" / "trial.report.json"
|
||||||
|
|
||||||
|
md_before = md_path.read_bytes()
|
||||||
|
machine_before = machine_path.read_bytes()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="official_outcome"):
|
||||||
|
save_v2_manual_audit(
|
||||||
|
result_path,
|
||||||
|
official_outcome="maybe",
|
||||||
|
auditor="reviewer",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert md_path.read_bytes() == md_before
|
||||||
|
assert machine_path.read_bytes() == machine_before
|
||||||
|
|
||||||
|
|
||||||
|
def test_discrepancy_detects_run_failed_contradiction(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
result["task_outcome"] = "failed"
|
||||||
|
result["challenge_report"] = {"run_failed": False, "used_product_path": True}
|
||||||
|
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
assert any(
|
||||||
|
"run_failed=false" in d and "task_outcome is 'failed'" in d
|
||||||
|
for d in report.self_report_discrepancies
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_discrepancy_detects_escalation_not_reported(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
result["task_outcome"] = "success"
|
||||||
|
policy = result.get("policy", {})
|
||||||
|
if isinstance(policy, dict):
|
||||||
|
policy["escalated_to_product_code"] = True
|
||||||
|
result["challenge_report"] = {
|
||||||
|
"run_failed": False,
|
||||||
|
"read": {"product_code": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
assert any("read.product_code" in d for d in report.self_report_discrepancies)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_discrepancy_when_product_code_reported(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
result["task_outcome"] = "success"
|
||||||
|
policy = result.get("policy", {})
|
||||||
|
if isinstance(policy, dict):
|
||||||
|
policy["escalated_to_product_code"] = True
|
||||||
|
result["challenge_report"] = {
|
||||||
|
"run_failed": False,
|
||||||
|
"read": {"product_code": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
assert not any("read.product_code" in d for d in report.self_report_discrepancies)
|
||||||
|
|
||||||
|
|
||||||
|
def test_example_read_does_not_create_discrepancy(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
policy = result.get("policy", {})
|
||||||
|
if isinstance(policy, dict):
|
||||||
|
policy["reads_by_category"] = {
|
||||||
|
"examples": [str(tmp_path / "examples" / "solution.py")],
|
||||||
|
}
|
||||||
|
result["challenge_report"] = {
|
||||||
|
"run_failed": False,
|
||||||
|
"read": {"product_code": True, "existing_solution": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
assert not any("existing solution" in d for d in report.self_report_discrepancies)
|
||||||
|
assert any("example file(s)" in n for n in report.follow_up_notes)
|
||||||
|
|
||||||
|
|
||||||
|
def _policy_mut(result: dict[str, object]) -> dict[str, object]:
|
||||||
|
p = result.get("policy", {})
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
p = {}
|
||||||
|
result["policy"] = p
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_follow_up_notes_for_example_reads(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
_policy_mut(result)["reads_by_category"] = {
|
||||||
|
"examples": [str(tmp_path / "examples" / "solution.py")],
|
||||||
|
}
|
||||||
|
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
assert any("example file(s)" in n for n in report.follow_up_notes)
|
||||||
|
|
||||||
|
|
||||||
|
def test_follow_up_notes_for_test_reads(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
_policy_mut(result)["reads_by_category"] = {
|
||||||
|
"tests": [str(tmp_path / "tests" / "test_app.py")],
|
||||||
|
}
|
||||||
|
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
assert any("test file(s)" in n for n in report.follow_up_notes)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_report_raises_on_missing_paths(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
del result["result_path"]
|
||||||
|
del result["workspace_path"]
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="raw_result_path"):
|
||||||
|
build_trial_report(result, audit=None)
|
||||||
Reference in New Issue
Block a user