Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies = [
"rich>=13.0",
# Dataset registry bench_version checks (PEP 440 specifier sets).
"packaging>=24",
"litellm[proxy]==1.89.0",
"litellm[proxy]==1.91.0",
"tomli-w>=1.0",
"typer>=0.9",
]
Expand Down
21 changes: 21 additions & 0 deletions src/benchflow/_utils/timestamps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Timestamp formatting helpers for persisted artifacts."""

from __future__ import annotations

from datetime import UTC, datetime


def artifact_timestamp(value: datetime | None) -> str | None:
"""Return a timezone-aware ISO 8601 timestamp for JSON artifacts.

Runtime paths should pass UTC-aware values at source. Legacy tests and
artifact repair paths may still pass naive ``datetime`` values; treat those
as already-UTC for compatibility and emit a compact ``Z`` suffix.
"""
if value is None:
return None
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
value = value.replace(tzinfo=UTC)
Comment thread
bingran-you marked this conversation as resolved.
else:
value = value.astimezone(UTC)
return value.isoformat().replace("+00:00", "Z")
11 changes: 6 additions & 5 deletions src/benchflow/hosted_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
trajectory_summary_from_events,
)
from benchflow._utils.reward_events import build_rewards_jsonl_events
from benchflow._utils.timestamps import artifact_timestamp
from benchflow.diagnostics import RolloutDiagnostics
from benchflow.rewards.validation import is_valid_reward_number
from benchflow.trajectories.types import redact_acp_trajectory_jsonl
Expand Down Expand Up @@ -560,9 +561,9 @@ def _write_run_artifacts(
"agent": config.agent or None,
"agent_name": "verifiers",
"model": result.normalized_model or result.model or None,
"n_tool_calls": result.total_tool_calls or 0,
"n_prompts": len(prompts),
"agent_result": agent_result,
"n_tool_calls": agent_result["n_tool_calls"],
"n_prompts": agent_result["n_prompts"],
"final_metrics": final_metrics_from_agent_result(agent_result),
"trajectory_summary": trajectory_summary_from_events(
trajectory,
Expand All @@ -583,8 +584,8 @@ def _write_run_artifacts(
**RolloutDiagnostics().to_result_fields(),
"partial_trajectory": False,
"trajectory_source": "hosted_env" if trajectory else None,
"started_at": str(started_at),
"finished_at": str(finished_at),
"started_at": artifact_timestamp(started_at),
"finished_at": artifact_timestamp(finished_at),
"timing": timing,
"scenes": [],
"source": source_provenance,
Expand All @@ -607,7 +608,7 @@ def _write_run_artifacts(
"timeout_sec": None,
"concurrency": config.concurrency,
"agent_idle_timeout_sec": None,
"started_at": str(started_at),
"started_at": artifact_timestamp(started_at),
"agent_env": {},
"scenes": [],
"source": source_provenance,
Expand Down
21 changes: 14 additions & 7 deletions src/benchflow/rollout/_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from benchflow._utils.reward_events import build_rewards_jsonl_events
from benchflow._utils.scoring import classify_error, classify_verifier_error
from benchflow._utils.source_provenance import artifact_source_provenance
from benchflow._utils.timestamps import artifact_timestamp
from benchflow.contracts import (
BaseUser,
DocumentNudgeUser,
Expand Down Expand Up @@ -194,7 +195,7 @@ def _write_config(
"timeout_sec": timeout,
"concurrency": concurrency,
"agent_idle_timeout_sec": agent_idle_timeout,
"started_at": str(started_at),
"started_at": artifact_timestamp(started_at),
"agent_env": recorded_env,
"scenes": _scene_metadata(scenes or []),
"loop": loop_block(loop_strategy),
Expand Down Expand Up @@ -305,7 +306,13 @@ def _build_rollout_result(
runtime_skills_dir=None,
declared_sandbox_skills_dir=None,
)
finished_at = datetime.now()
started_tz = (
started_at.tzinfo
if started_at.tzinfo is not None
and started_at.tzinfo.utcoffset(started_at) is not None
else None
)
finished_at = datetime.now(started_tz) if started_tz is not None else datetime.now()
n_skill_invocations = count_skill_invocations(trajectory)
error_category = (
diagnostics.category_for_channel("error") if error is not None else None
Expand Down Expand Up @@ -387,10 +394,10 @@ def _build_rollout_result(
"agent_name": result.agent_name,
"model": result.model,
**skill_policy.config_metadata(),
"n_tool_calls": result.n_tool_calls,
"n_skill_invocations": result.n_skill_invocations,
"n_prompts": result.n_prompts,
"agent_result": agent_result,
"n_tool_calls": agent_result["n_tool_calls"],
"n_skill_invocations": agent_result["n_skill_invocations"],
"n_prompts": agent_result["n_prompts"],
"final_metrics": final_metrics,
"trajectory_summary": trajectory_summary,
"usage_tracking": usage_tracking,
Expand All @@ -402,8 +409,8 @@ def _build_rollout_result(
**diagnostics.to_result_fields(),
"partial_trajectory": result.partial_trajectory,
"trajectory_source": result.trajectory_source,
"started_at": str(result.started_at),
"finished_at": str(result.finished_at),
"started_at": artifact_timestamp(result.started_at),
"finished_at": artifact_timestamp(result.finished_at),
"timing": timing,
"scenes": _scene_metadata(scenes or []),
"loop": loop or loop_block(None),
Expand Down
4 changes: 2 additions & 2 deletions src/benchflow/rollout/_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import shlex
import tempfile
from collections.abc import Callable
from datetime import datetime
from datetime import UTC, datetime
from pathlib import Path, PurePosixPath
from typing import Any

Expand Down Expand Up @@ -269,7 +269,7 @@ def _init_rollout(
rollout_name = rollout_name or f"{task_path.name}__{uuid4().hex[:8]}"
rollout_dir = Path(jobs_dir) / job_name / rollout_name
rollout_paths = RolloutPaths(rollout_dir=rollout_dir)
started_at = datetime.now()
started_at = datetime.now(UTC)
rollout_dir.mkdir(parents=True, exist_ok=True)
for subdir in ("agent", "verifier", "artifacts", "trajectory"):
(rollout_dir / subdir).mkdir(exist_ok=True)
Expand Down
99 changes: 99 additions & 0 deletions tests/test_artifact_timestamps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Artifact timestamp serialization regressions."""

from __future__ import annotations

import json
from datetime import UTC, datetime, timedelta, timezone


def test_result_timestamps_are_timezone_aware_iso8601(tmp_path):
"""Guards PR #928 / issue #528: result.json timestamps are UTC ISO."""
from benchflow.rollout import _build_rollout_result

rollout_dir = tmp_path / "trial"
rollout_dir.mkdir()
_build_rollout_result(
rollout_dir,
task_name="t1",
rollout_name="trial-1",
agent="test",
agent_name="openhands",
model="m",
n_tool_calls=0,
prompts=[],
error=None,
verifier_error=None,
trajectory=[],
partial_trajectory=False,
rewards={"reward": 1.0},
started_at=datetime(2026, 3, 24, 10, 0),
timing={},
)

data = json.loads((rollout_dir / "result.json").read_text())
assert data["started_at"] == "2026-03-24T10:00:00Z"
assert data["finished_at"].endswith("Z")
assert " " not in data["finished_at"]
assert datetime.fromisoformat(data["finished_at"].replace("Z", "+00:00"))


def test_result_timestamps_normalize_offsets_to_utc(tmp_path):
"""Guards PR #928 / issue #528: aware result timestamps serialize in UTC."""
from benchflow.rollout import _build_rollout_result

rollout_dir = tmp_path / "trial"
rollout_dir.mkdir()
_build_rollout_result(
rollout_dir,
task_name="t1",
rollout_name="trial-1",
agent="test",
agent_name="openhands",
model="m",
n_tool_calls=0,
prompts=[],
error=None,
verifier_error=None,
trajectory=[],
partial_trajectory=False,
rewards={"reward": 1.0},
started_at=datetime(2026, 3, 24, 3, 0, tzinfo=timezone(timedelta(hours=-7))),
timing={},
)

data = json.loads((rollout_dir / "result.json").read_text())
assert data["started_at"] == "2026-03-24T10:00:00Z"


def test_native_rollout_started_at_is_utc_aware_at_source(tmp_path, monkeypatch):
"""Guards PR #928: native rollout timestamps are not local naive values."""
from benchflow.rollout import _setup as rollout_setup
from benchflow.rollout._setup import _init_rollout

task_dir = tmp_path / "task"
task_dir.mkdir()
(task_dir / "instruction.md").write_text("Do it.\n")
(task_dir / "task.toml").write_text('version = "1.0"\n')

local_wall_time = datetime(2026, 7, 23, 5, 10, 58)
utc_instant = datetime(2026, 7, 23, 12, 10, 58, tzinfo=UTC)

class FakeDateTime:
@classmethod
def now(cls, tz=None):
if tz is None:
return local_wall_time
assert tz is UTC
return utc_instant

monkeypatch.setattr(rollout_setup, "datetime", FakeDateTime)

_, _, _, started_at, job_name, _ = _init_rollout(
task_dir,
job_name=None,
rollout_name=None,
jobs_dir=tmp_path / "jobs",
)

assert job_name == "2026-07-23__05-10-58"
assert started_at == utc_instant
7 changes: 6 additions & 1 deletion tests/test_hosted_env_rollout_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def _classify(result) -> str:


def test_hosted_env_writes_contract_result_json(tmp_path, monkeypatch):
"""result.json carries the rollout-contract field set, not a custom shape."""
"""Guards PR #928 / issue #528 and PR #419: hosted result.json contract."""
result = _run(tmp_path, monkeypatch)
payload = json.loads((result.run_dir / "result.json").read_text())

Expand Down Expand Up @@ -151,6 +151,11 @@ def test_hosted_env_writes_contract_result_json(tmp_path, monkeypatch):
}
assert payload["trajectory_summary"]["steps"] >= 0
assert payload["trajectory_summary"]["tool_call_steps"] >= 0
assert payload["n_tool_calls"] == agent_result["n_tool_calls"]
assert payload["n_prompts"] == agent_result["n_prompts"]
assert payload["started_at"].endswith("Z")
assert payload["finished_at"].endswith("Z")
assert "T" in payload["started_at"]


def test_hosted_env_writes_rewards_jsonl(tmp_path, monkeypatch):
Expand Down
4 changes: 3 additions & 1 deletion tests/test_trace_import_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def test_tasks_generate_rejects_removed_short_options(
)

assert result.exit_code != 0
assert f"No such option: {alias}" in click.unstyle(result.output)
output = click.unstyle(result.output)
assert "No such option" in output
assert alias in output


def test_tasks_generate_dry_run_uses_generation_filters(
Expand Down
Loading
Loading