benchflow 0.7.2.dev1998__py3-none-any.whl → 0.7.2.dev2000__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchflow/cli/traj.py +38 -22
- benchflow/trajectories/viewer.py +90 -40
- {benchflow-0.7.2.dev1998.dist-info → benchflow-0.7.2.dev2000.dist-info}/METADATA +1 -1
- {benchflow-0.7.2.dev1998.dist-info → benchflow-0.7.2.dev2000.dist-info}/RECORD +7 -7
- {benchflow-0.7.2.dev1998.dist-info → benchflow-0.7.2.dev2000.dist-info}/WHEEL +0 -0
- {benchflow-0.7.2.dev1998.dist-info → benchflow-0.7.2.dev2000.dist-info}/entry_points.txt +0 -0
- {benchflow-0.7.2.dev1998.dist-info → benchflow-0.7.2.dev2000.dist-info}/licenses/LICENSE +0 -0
benchflow/cli/traj.py
CHANGED
|
@@ -292,17 +292,23 @@ def register_traj(app: typer.Typer) -> None:
|
|
|
292
292
|
def _run_upload(options: _UploadOptions) -> None:
|
|
293
293
|
prompted = options.path is None
|
|
294
294
|
path = options.path or _prompt_for_path()
|
|
295
|
-
|
|
295
|
+
detected: _DetectedRepo | None = None
|
|
296
296
|
if options.source_id is not None:
|
|
297
297
|
source_id = options.source_id
|
|
298
298
|
else:
|
|
299
299
|
if options.repo:
|
|
300
|
-
|
|
301
|
-
source_id = f"repo/{
|
|
302
|
-
if
|
|
303
|
-
# Contributor-visible metadata: surface the tag
|
|
304
|
-
# sessions can opt out before anything leaves
|
|
305
|
-
|
|
300
|
+
detected = _detect_repo_slug(path)
|
|
301
|
+
source_id = f"repo/{detected.slug}" if detected else default_source_id(path)
|
|
302
|
+
if detected:
|
|
303
|
+
# Contributor-visible metadata: surface the tag and where it came
|
|
304
|
+
# from so private-repo sessions can opt out before anything leaves
|
|
305
|
+
# the machine. The local path stays terminal-only; the uploaded
|
|
306
|
+
# source id is just repo/<owner>/<name>. Plain print keeps the line
|
|
307
|
+
# one physical line (no Rich wrapping) so the path stays selectable.
|
|
308
|
+
print(
|
|
309
|
+
f"Repo: {detected.slug} "
|
|
310
|
+
f"(from session cwd {detected.session_cwd}; use --no-repo to omit)"
|
|
311
|
+
)
|
|
306
312
|
destination = _resolve_destination(options)
|
|
307
313
|
|
|
308
314
|
with (
|
|
@@ -419,25 +425,35 @@ def _infer_email() -> str:
|
|
|
419
425
|
_SESSION_CWD_SCAN_LINES = 50
|
|
420
426
|
|
|
421
427
|
|
|
422
|
-
|
|
428
|
+
@dataclass(frozen=True)
|
|
429
|
+
class _DetectedRepo:
|
|
430
|
+
slug: str
|
|
431
|
+
session_cwd: Path
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _detect_repo_slug(path: Path) -> _DetectedRepo | None:
|
|
423
435
|
"""Best-effort ``owner/name`` for the repository the session was about.
|
|
424
436
|
|
|
425
437
|
Reads the working directory the session recorded (Claude events carry a
|
|
426
|
-
``cwd`` field; Codex ``session_meta`` payloads do too)
|
|
427
|
-
directory's git for the ``origin`` remote
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
438
|
+
``cwd`` field; Codex ``session_meta`` payloads do too) and asks that
|
|
439
|
+
directory's git for the ``origin`` remote. The trajectory's own recorded
|
|
440
|
+
cwd is the ONLY provenance source: there is deliberately no fallback to
|
|
441
|
+
the upload invocation directory, which mis-attributed sessions recorded
|
|
442
|
+
outside a repo to whatever checkout the contributor happened to upload
|
|
443
|
+
from. No session cwd, a missing directory, or no GitHub remote all mean
|
|
444
|
+
no tag. Every failure is silent — repo tagging must never break an
|
|
445
|
+
upload — and local-path remotes never produce a tag, so no local
|
|
446
|
+
absolute path can leak into the manifest.
|
|
431
447
|
"""
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
448
|
+
session_cwd = _session_cwd(path)
|
|
449
|
+
if session_cwd is None or not session_cwd.is_dir():
|
|
450
|
+
return None
|
|
451
|
+
remote = _command_stdout(
|
|
452
|
+
"git", "-C", str(session_cwd), "remote", "get-url", "origin"
|
|
453
|
+
)
|
|
454
|
+
slug = _repo_slug_from_remote(remote) if remote else None
|
|
455
|
+
if slug:
|
|
456
|
+
return _DetectedRepo(slug=slug, session_cwd=session_cwd)
|
|
441
457
|
return None
|
|
442
458
|
|
|
443
459
|
|
benchflow/trajectories/viewer.py
CHANGED
|
@@ -431,26 +431,18 @@ def render_rollout(rollout_dir: Path, prompts: list[str] | None = None) -> str:
|
|
|
431
431
|
while len(prompts) < len(turn_files):
|
|
432
432
|
prompts.append("")
|
|
433
433
|
|
|
434
|
-
|
|
435
|
-
sys_event = next((e for e in first_events if e.get("type") == "system"), {})
|
|
436
|
-
total_cost = 0
|
|
437
|
-
total_turns_count = 0
|
|
438
|
-
|
|
434
|
+
all_events: list[dict] = []
|
|
439
435
|
all_blocks = []
|
|
440
436
|
for i, tf in enumerate(turn_files):
|
|
441
437
|
events = _parse_jsonl(tf.read_text())
|
|
438
|
+
all_events.extend(events)
|
|
442
439
|
all_blocks.append(render_turn(events, i + 1, prompts[i]))
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
# stream-json (e.g. "session_id": null) bypasses the default and would crash
|
|
450
|
-
# html.escape() / the [:16] slice below with a raw TypeError.
|
|
451
|
-
session_id = str(sys_event.get("session_id") or "?")
|
|
452
|
-
model = str(sys_event.get("model") or "?")
|
|
453
|
-
version = str(sys_event.get("claude_code_version") or "?")
|
|
440
|
+
|
|
441
|
+
badges = _stream_header_badges(all_events)
|
|
442
|
+
badges.append(("turns", str(len(turn_files))))
|
|
443
|
+
total_cost = _result_cost(all_events)
|
|
444
|
+
if total_cost is not None:
|
|
445
|
+
badges.append(("total cost", f"${total_cost:.4f}"))
|
|
454
446
|
|
|
455
447
|
return f"""<!DOCTYPE html>
|
|
456
448
|
<html>
|
|
@@ -464,14 +456,7 @@ def render_rollout(rollout_dir: Path, prompts: list[str] | None = None) -> str:
|
|
|
464
456
|
<div class="header">
|
|
465
457
|
{_WORDMARK_HTML}
|
|
466
458
|
<h1>{html.escape(rollout_dir.name)}</h1>
|
|
467
|
-
|
|
468
|
-
<span>model: {html.escape(model)}</span>
|
|
469
|
-
<span>session: {html.escape(session_id[:16])}...</span>
|
|
470
|
-
<span>claude code: {html.escape(version)}</span>
|
|
471
|
-
<span>turns: {len(turn_files)}</span>
|
|
472
|
-
<span>total cost: ${total_cost:.4f}</span>
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
459
|
+
{_meta_badges_html(badges)}</div>
|
|
475
460
|
{_join_with_divider(all_blocks)}
|
|
476
461
|
</body>
|
|
477
462
|
</html>"""
|
|
@@ -672,18 +657,89 @@ def render_jsonl_file(path: Path) -> str:
|
|
|
672
657
|
body = render_turn(events, 1, "")
|
|
673
658
|
if not body.strip():
|
|
674
659
|
return _NO_TRAJECTORIES_HTML
|
|
675
|
-
return _stream_json_page(path.name, events, [body])
|
|
660
|
+
return _stream_json_page(path.name, events, [body], session_fallback=path.stem)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _stream_header_badges(
|
|
664
|
+
events: list[dict], *, session_fallback: str | None = None
|
|
665
|
+
) -> list[tuple[str, str]]:
|
|
666
|
+
"""Header badges derivable from what the stream actually contains.
|
|
667
|
+
|
|
668
|
+
``claude -p`` stream-json carries a ``type: system`` init event
|
|
669
|
+
(``session_id`` / ``model`` / ``claude_code_version``); real ``~/.claude``
|
|
670
|
+
session files don't — their metadata lives per event (``sessionId``,
|
|
671
|
+
``version``) and on assistant events (``message.model``). Pull from
|
|
672
|
+
whichever is present and omit anything unknown: a header of ``?`` badges
|
|
673
|
+
at the approve moment reads as a broken viewer, not as missing data.
|
|
674
|
+
Truthiness (not ``.get`` defaults) also swallows present-but-null values
|
|
675
|
+
like ``"session_id": null``, which previously needed an explicit guard to
|
|
676
|
+
avoid a TypeError.
|
|
677
|
+
"""
|
|
678
|
+
sys_event = next((e for e in events if e.get("type") == "system"), {})
|
|
676
679
|
|
|
680
|
+
def _first(values) -> object | None:
|
|
681
|
+
return next((value for value in values if value), None)
|
|
677
682
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
683
|
+
model = sys_event.get("model") or _first(
|
|
684
|
+
event["message"].get("model")
|
|
685
|
+
for event in events
|
|
686
|
+
if event.get("type") == "assistant" and isinstance(event.get("message"), dict)
|
|
687
|
+
)
|
|
688
|
+
session_id = (
|
|
689
|
+
sys_event.get("session_id")
|
|
690
|
+
or _first(event.get("sessionId") for event in events)
|
|
691
|
+
or session_fallback
|
|
692
|
+
)
|
|
693
|
+
version = sys_event.get("claude_code_version") or _first(
|
|
694
|
+
event.get("version") for event in events
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
badges: list[tuple[str, str]] = []
|
|
698
|
+
if model:
|
|
699
|
+
badges.append(("model", str(model)))
|
|
700
|
+
if session_id:
|
|
701
|
+
sid = str(session_id)
|
|
702
|
+
badges.append(("session", sid[:16] + ("..." if len(sid) > 16 else "")))
|
|
703
|
+
if version:
|
|
704
|
+
badges.append(("claude code", str(version)))
|
|
705
|
+
return badges
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _result_cost(events: list[dict]) -> float | None:
|
|
709
|
+
"""Total cost summed from result events, or ``None`` when no event
|
|
710
|
+
carries cost data — so the header can hide the badge instead of
|
|
711
|
+
asserting a fictional ``$0.0000``."""
|
|
712
|
+
total = 0.0
|
|
713
|
+
seen = False
|
|
681
714
|
for event in events:
|
|
682
|
-
if event.get("type") == "result":
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
715
|
+
if event.get("type") == "result" and event.get("total_cost_usd") is not None:
|
|
716
|
+
total += float(event.get("total_cost_usd") or 0)
|
|
717
|
+
seen = True
|
|
718
|
+
return total if seen else None
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _meta_badges_html(badges: list[tuple[str, str]]) -> str:
|
|
722
|
+
"""The header's ``.meta`` badge row; empty string when nothing is known."""
|
|
723
|
+
if not badges:
|
|
724
|
+
return ""
|
|
725
|
+
spans = "\n".join(
|
|
726
|
+
f"<span>{html.escape(label)}: {html.escape(value)}</span>"
|
|
727
|
+
for label, value in badges
|
|
728
|
+
)
|
|
729
|
+
return f'<div class="meta">\n{spans}\n</div>\n'
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _stream_json_page(
|
|
733
|
+
title: str,
|
|
734
|
+
events: list[dict],
|
|
735
|
+
turn_blocks: list[str],
|
|
736
|
+
*,
|
|
737
|
+
session_fallback: str | None = None,
|
|
738
|
+
) -> str:
|
|
739
|
+
badges = _stream_header_badges(events, session_fallback=session_fallback)
|
|
740
|
+
total_cost = _result_cost(events)
|
|
741
|
+
if total_cost is not None:
|
|
742
|
+
badges.append(("total cost", f"${total_cost:.4f}"))
|
|
687
743
|
return f"""<!DOCTYPE html>
|
|
688
744
|
<html>
|
|
689
745
|
<head>
|
|
@@ -696,13 +752,7 @@ def _stream_json_page(title: str, events: list[dict], turn_blocks: list[str]) ->
|
|
|
696
752
|
<div class="header">
|
|
697
753
|
{_WORDMARK_HTML}
|
|
698
754
|
<h1>{html.escape(title)}</h1>
|
|
699
|
-
|
|
700
|
-
<span>model: {html.escape(model)}</span>
|
|
701
|
-
<span>session: {html.escape(session_id[:16])}...</span>
|
|
702
|
-
<span>claude code: {html.escape(version)}</span>
|
|
703
|
-
<span>total cost: ${total_cost:.4f}</span>
|
|
704
|
-
</div>
|
|
705
|
-
</div>
|
|
755
|
+
{_meta_badges_html(badges)}</div>
|
|
706
756
|
{_join_with_divider(turn_blocks)}
|
|
707
757
|
</body>
|
|
708
758
|
</html>"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: benchflow
|
|
3
|
-
Version: 0.7.2.
|
|
3
|
+
Version: 0.7.2.dev2000
|
|
4
4
|
Summary: Multi-turn agent benchmarking with ACP — run any agent, any model, any provider.
|
|
5
5
|
Project-URL: Homepage, https://github.com/benchflow-ai/benchflow
|
|
6
6
|
Project-URL: Repository, https://github.com/benchflow-ai/benchflow
|
|
@@ -116,7 +116,7 @@ benchflow/cli/skills.py,sha256=2Apv1iqvmstWEhY6O-PRdxKc8RYwR6f7MvbYfl2dWm0,7497
|
|
|
116
116
|
benchflow/cli/tasks.py,sha256=b1XP1EZ79ZtK_1shF65qZyv27EcKKFZYV35M4DzxYKI,18534
|
|
117
117
|
benchflow/cli/trace_import.py,sha256=NHDieFdiKQ-2Sr1-GF8Z5EDcbGsz2qq_kFE0rXacC2o,14705
|
|
118
118
|
benchflow/cli/train.py,sha256=MIP_JxptitH348bWWURpcWt-TmDAw1s8d-RUU7jBaIg,23635
|
|
119
|
-
benchflow/cli/traj.py,sha256=
|
|
119
|
+
benchflow/cli/traj.py,sha256=uisYWo-f_R_e06sKJPMObYbvDAPJac0zzyU4byGHU7U,25515
|
|
120
120
|
benchflow/continue_run/__init__.py,sha256=dWidng7kJphVUtbP1a1Ma0Lyjxj5yLgQZIApaKCJUew,1479
|
|
121
121
|
benchflow/continue_run/batch.py,sha256=8BNSInyp_Ol6Fcrg4ezBJdi3cBS12nC8mf963_x4fIg,4146
|
|
122
122
|
benchflow/continue_run/orchestrator.py,sha256=wYoseBNE_rZ0EjNwWDyqfave54xNRe43sCd_O4VJjyk,27676
|
|
@@ -291,9 +291,9 @@ benchflow/trajectories/sessions.py,sha256=u_6x1WyT8YtKUVOb6b3myTCHhqn58a8NHCLAvQ
|
|
|
291
291
|
benchflow/trajectories/tree.py,sha256=c0jyoP9OurDQq-zRtPc66edzWUvLGwQQz0jgtyqSw2U,5576
|
|
292
292
|
benchflow/trajectories/trl_sft_tokenization.py,sha256=-Ef0bP9cuzfzOz5nIv2mEF3taQIEw68h6SM1YO-Mwn4,8643
|
|
293
293
|
benchflow/trajectories/types.py,sha256=EG_TDTh0JveXk5WJX4YMcEDBjfe3526lC7JjDvQmiw8,30067
|
|
294
|
-
benchflow/trajectories/viewer.py,sha256=
|
|
295
|
-
benchflow-0.7.2.
|
|
296
|
-
benchflow-0.7.2.
|
|
297
|
-
benchflow-0.7.2.
|
|
298
|
-
benchflow-0.7.2.
|
|
299
|
-
benchflow-0.7.2.
|
|
294
|
+
benchflow/trajectories/viewer.py,sha256=L-tMiR15qdGBaTlVNgY4dOS1pIrf4PB24MvNic6lj4s,37715
|
|
295
|
+
benchflow-0.7.2.dev2000.dist-info/METADATA,sha256=IYq47CWBBGAdRObAxld55c_Nbt_mma-kzn0JFrADhbw,15565
|
|
296
|
+
benchflow-0.7.2.dev2000.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
297
|
+
benchflow-0.7.2.dev2000.dist-info/entry_points.txt,sha256=qPMBVtH4qz3P4nMALEAdTbvpxvsVch9RhqshZKQn1dE,84
|
|
298
|
+
benchflow-0.7.2.dev2000.dist-info/licenses/LICENSE,sha256=ohnbHkuBylrTtOvaZJk9Hz7dRwmaEEGAb5ZcBV4Xuhw,10779
|
|
299
|
+
benchflow-0.7.2.dev2000.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|