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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Versions follow [SemVer](https://semver.org).

## [Unreleased]

- Session end records each tool call from the retained Claude or Codex log as a `tool.call` trajectory event, once per call even when a session resumes. `outerloop trajectory` and a new Process section in the PR body show counts by tool. Older kernels ignore these events.
- `outerloop status` shows a run's priced session cost as a lower bound when some legs are unpriced (a wake killed mid-turn leaves no usage event), for example `session-cost>=$4.2000 (1 unpriced)`, instead of hiding the priced part behind `unknown`; the status row and JSON gain the priced-leg count (#516).
- The pre-PR panel accepts a composite change — a PR that carries several changes — when the report gives each component's isolated measurement and the combination's under the same protocol and the combination is what the kernel measured. A bundle without those isolated measurements stays blocked (#489).
- Claude sessions can use a subscription token from `claude setup-token` (`sk-ant-oat...`). The harness sends it as `CLAUDE_CODE_OAUTH_TOKEN`, not `ANTHROPIC_API_KEY`, and `init` accepts it. A panel judge on a token runs `claude --safe-mode`, because `--bare` cannot read OAuth tokens. Both modes ignore the checkout's CLAUDE.md, hooks, plugins and MCP servers. The pinned Claude Code release (2.1.285) has `--safe-mode`.
Expand Down
8 changes: 7 additions & 1 deletion docs/design/trajectory.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ directory. Self-reported measures record the ruler alone: they have no
evaluation image or result files. A pending submission does not yet name the
trajectory head: kernels before this one reject unknown pending fields, so the
pending reader now ignores them, and a later release can add the field without
breaking this one. Tool calls and attestation are later steps.
breaking this one. Session end also appends one `tool.call` event per tool call
found in the retained, redacted native log: Claude project transcripts and Codex
rollout JSONL are parsed host-side, bounded to 500 calls per session, and Hermes
or unknown formats yield nothing. Each event carries the tool name, repo-relative
files, a bounded shell command and, when the log has them, the exit code and
outcome — never file contents or tool output. `outerloop trajectory` counts them
by tool and the PR body summarizes the top tools. Attestation is a later step.
11 changes: 11 additions & 0 deletions src/outerloop/attempt.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,22 @@
from outerloop.syscall import refresh_tool as syscall_refresh_tool
from outerloop.syscall import write_siblings as syscall_write_siblings
from outerloop.trajectory import file_refs
from outerloop.trajectory import read as read_trajectory
from outerloop.trajectory import record as record_event
from outerloop.trajectory import summarize as summarize_trajectory
from outerloop.verifier import MAX_CLAIM_CHARS

REPLY_MARKER = marker("followup")


def tool_call_counts(run_dir: Path) -> dict[str, int]:
"""The run's recorded tool calls by tool; an unreadable chain reads as none."""
try:
return summarize_trajectory(read_trajectory(run_dir))["tool_calls_by_tool"]
except OSError:
return {}


def _pr_number(pr_url: str) -> int:
tail = pr_url.rstrip("/").rsplit("/", 1)[-1]
if not tail.isdigit():
Expand Down Expand Up @@ -4724,6 +4734,7 @@ def refuse(
display_digits=bench.display_digits,
experiments=experiments_rows(run_dir),
base_sha=base_sha,
tool_calls=tool_call_counts(run_dir),
)
body += f"\n\n{progress_link(config.target, github)}\n"
if issue_number:
Expand Down
9 changes: 8 additions & 1 deletion src/outerloop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1360,9 +1360,16 @@ def main(argv: list[str] | None = None) -> int:
f"{verb} {count}" for verb, count in summary["refusals_by_verb"].items()
)
authors = ", ".join(summary["authors"]) or "unknown"
tools = ", ".join(
f"{tool} {count}"
for tool, count in sorted(
summary["tool_calls_by_tool"].items(), key=lambda item: (-item[1], item[0])
)
)
print(
f"refusals: {summary['refusals']} of {summary['decisions']} decisions"
f"{f' ({verbs})' if verbs else ''}; author: {authors}"
f"{f' ({verbs})' if verbs else ''}; author: {authors}; "
f"tool calls: {summary['tool_calls']}{f' ({tools})' if tools else ''}"
)
return 1 if problems and problems != ["no trajectory"] else 0
if args.command == "status":
Expand Down
17 changes: 17 additions & 0 deletions src/outerloop/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2821,6 +2821,20 @@ def experiments_section(rows: list[dict[str, Any]]) -> list[str]:
return lines


MAX_PROCESS_TOOLS = 5


def process_section(tool_calls: dict[str, int] | None) -> list[str]:
"""Total and top tools from the run's trajectory; empty when there are none."""
total = sum((tool_calls or {}).values())
if not total:
return []
ranked = sorted((tool_calls or {}).items(), key=lambda item: (-item[1], item[0]))
top = ", ".join(f"{name} {count}" for name, count in ranked[:MAX_PROCESS_TOOLS])
calls = "call" if total == 1 else "calls"
return ["", "## Process", "", f"{total} tool {calls} from the run's trajectory: {top}."]


def pr_body(
result: AttemptResult,
config: RunConfig,
Expand All @@ -2829,6 +2843,7 @@ def pr_body(
experiments: list[dict[str, Any]] | None = None,
*,
base_sha: str = "",
tool_calls: dict[str, int] | None = None,
) -> str:
"""The PR body for an improved run: the author's report, the experiments
the run actually ran (from the launch ledger), the measured table, and
Expand Down Expand Up @@ -2909,6 +2924,7 @@ def pr_body(
"",
*report_lines,
*experiments_section(experiments or []),
*process_section(tool_calls),
"",
"## Measured",
"",
Expand Down Expand Up @@ -2940,6 +2956,7 @@ def pr_body(
"",
redact(result.submit_report, redact_secrets)[:MAX_REPORT_BODY],
*experiments_section(experiments or []),
*process_section(tool_calls),
"",
"## Self-reported values",
"",
Expand Down
5 changes: 5 additions & 0 deletions src/outerloop/session_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

from outerloop.tool_calls import record_tool_calls
from outerloop.trajectory import session_event

if TYPE_CHECKING:
Expand Down Expand Up @@ -320,6 +321,10 @@ def wrapped(
)
except Exception:
log.warning("could not retain session evidence", exc_info=True)
try:
record_tool_calls(workspace, backend, result.session_record_path)
except Exception:
log.warning("could not record session tool calls", exc_info=True)
session_event(
workspace, "session.end", backend, self.model, result.session_id, result=result
)
Expand Down
Loading
Loading