Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pip
- run: pip install -e ".[dev]"
- name: Lint with Ruff
if: matrix.python-version == '3.12'
run: ruff check .
- run: pytest -q
# No services, no API keys, no network: it reads local files, so CI runs the real thing.
- name: Smoke the CLI against the bundled fixture session
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Thanks for considering a contribution — issues and pull requests are welcome.
## Making a change

- Keep pull requests focused and describe **what** changed and **why**.
- Run the project's format, lint, and test steps (see the README) before pushing.
- Run `ruff check .` and the project's test steps (see the README) before pushing.
- Formatting is not currently enforced; Ruff is used for linting.
- Add or update tests when you change behavior.
- Update docs or the CHANGELOG when relevant.

Expand Down
20 changes: 10 additions & 10 deletions agentrace/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
from __future__ import annotations

import re
from collections.abc import Callable
from dataclasses import dataclass
from typing import Callable

from .parse import AgentRun

Expand Down Expand Up @@ -68,7 +68,7 @@ def check_refused_or_gave_up(run: AgentRun) -> list[Finding]:
]
head = run.result[:1500]
for p in patterns:
m = re.search(p, head, re.I)
m = re.search(p, head, re.IGNORECASE)
if m:
return [
Finding(
Expand All @@ -93,11 +93,11 @@ def check_destructive_command(run: AgentRun) -> list[Finding]:
r"\bkubectl\s+delete\b",
r":\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:",
]
caution = re.compile(r"\b(?:dry[- ]?run|backup|verify first|check first|confirm first)\b|--dry-run", re.I)
caution = re.compile(r"\b(?:dry[- ]?run|backup|verify first|check first|confirm first)\b|--dry-run", re.IGNORECASE)
head = run.result[:1500]

for pattern in patterns:
m = re.search(pattern, head, re.I)
m = re.search(pattern, head, re.IGNORECASE)
if not m:
continue
start = max(0, m.start() - 140)
Expand Down Expand Up @@ -132,7 +132,7 @@ def check_unverified_claim(run: AgentRun) -> list[Finding]:
]
hits = []
for p in hedges:
for m in re.finditer(p, run.result, re.I):
for m in re.finditer(p, run.result, re.IGNORECASE):
hits.append(_context(run.result, m.start()))
if hits:
return [
Expand Down Expand Up @@ -160,7 +160,7 @@ def check_absence_as_evidence(run: AgentRun) -> list[Finding]:
r"\bboard is empty\b",
]
for p in patterns:
m = re.search(p, run.result, re.I)
m = re.search(p, run.result, re.IGNORECASE)
if m:
return [
Finding(
Expand Down Expand Up @@ -208,7 +208,7 @@ def check_url_without_verification(run: AgentRun) -> list[Finding]:
urls = re.findall(r"https?://[^\s)\]<>\"']+", run.result)
if len(urls) < 5:
return []
verified = re.search(r"\b(?:verified|confirmed|checked|fetched|HTTP 200|status 200)\b", run.result, re.I)
verified = re.search(r"\b(?:verified|confirmed|checked|fetched|HTTP 200|status 200)\b", run.result, re.IGNORECASE)
if not verified:
return [
Finding(
Expand Down Expand Up @@ -236,7 +236,7 @@ def check_prompt_hygiene(run: AgentRun) -> list[Finding]:
r"\b(?:output|outputs|return|returns|format|formatted|respond|reply|write|writing|report|"
r"summar[iy]|list|table|json|csv|markdown|schema|fields|columns|deliver)\b",
run.prompt,
re.I,
re.IGNORECASE,
)

# Length alone is not the defect. "Run the suite and report every failing test as node ids
Expand Down Expand Up @@ -274,7 +274,7 @@ def check_unquantified(run: AgentRun) -> list[Finding]:
r"\b(?:count|counts|how many|number of|enumerate|enumerat(?:e|ing)|"
r"list (?:every|all|each)|report every|every \w+ as)\b",
run.prompt,
re.I,
re.IGNORECASE,
)
if not asks_to_count:
return []
Expand All @@ -289,7 +289,7 @@ def check_unquantified(run: AgentRun) -> list[Finding]:
vague = re.search(
r"\b(?:several|some|a few|a number of|various|numerous|many|multiple)\b",
result,
re.I,
re.IGNORECASE,
)
if not vague:
return []
Expand Down
4 changes: 2 additions & 2 deletions agentrace/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
from __future__ import annotations

import json
from collections.abc import Iterator
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Iterator


@dataclass
Expand Down Expand Up @@ -77,7 +77,7 @@ def _ts(value: str | None) -> datetime | None:
if not value:
return None
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
parsed = datetime.fromisoformat(value)
# Treat missing offsets as UTC, independently of the reader's local timezone.
return parsed.replace(tzinfo=UTC) if parsed.tzinfo is None else parsed
except ValueError:
Expand Down
24 changes: 11 additions & 13 deletions tests/test_agentrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,21 @@
from __future__ import annotations

import json
from datetime import datetime, timezone

import pytest
from datetime import UTC, datetime

from agentrace.checks import analyse
from agentrace.parse import AgentRun, parse_session


def _run(result: str = "x" * 500, prompt: str = "y" * 500, **kw) -> AgentRun:
defaults = dict(
tool_use_id="toolu_test123456",
description="test run",
prompt=prompt,
result=result,
started_at=datetime(2026, 7, 16, 12, 0, tzinfo=timezone.utc),
ended_at=datetime(2026, 7, 16, 12, 1, tzinfo=timezone.utc),
)
defaults = {
"tool_use_id": "toolu_test123456",
"description": "test run",
"prompt": prompt,
"result": result,
"started_at": datetime(2026, 7, 16, 12, 0, tzinfo=UTC),
"ended_at": datetime(2026, 7, 16, 12, 1, tzinfo=UTC),
}
defaults.update(kw)
return AgentRun(**defaults)

Expand Down Expand Up @@ -292,8 +290,8 @@ def test_unquantified_clean_when_result_has_counts():

def test_slow_run_is_flagged():
r = _run(
started_at=datetime(2026, 7, 16, 12, 0, tzinfo=timezone.utc),
ended_at=datetime(2026, 7, 16, 12, 30, tzinfo=timezone.utc),
started_at=datetime(2026, 7, 16, 12, 0, tzinfo=UTC),
ended_at=datetime(2026, 7, 16, 12, 30, tzinfo=UTC),
)
assert "slow_run" in _codes(r)

Expand Down
6 changes: 3 additions & 3 deletions tests/test_destructive_command.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime

from agentrace.checks import analyse
from agentrace.parse import AgentRun
Expand All @@ -10,8 +10,8 @@ def _run(result: str) -> AgentRun:
description="test run",
prompt="Review the operation and report what should be run.",
result=result,
started_at=datetime(2026, 7, 16, 12, 0, tzinfo=timezone.utc),
ended_at=datetime(2026, 7, 16, 12, 1, tzinfo=timezone.utc),
started_at=datetime(2026, 7, 16, 12, 0, tzinfo=UTC),
ended_at=datetime(2026, 7, 16, 12, 1, tzinfo=UTC),
)


Expand Down
Loading