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 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
- repo_slug: str | None = None
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
- repo_slug = _detect_repo_slug(path)
301
- source_id = f"repo/{repo_slug}" if repo_slug else default_source_id(path)
302
- if repo_slug:
303
- # Contributor-visible metadata: surface the tag so private-repo
304
- # sessions can opt out before anything leaves the machine.
305
- console.print(f"Repo: {repo_slug} (use --no-repo to omit)")
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
- def _detect_repo_slug(path: Path) -> str | None:
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), asks that
427
- directory's git for the ``origin`` remote, and falls back to the
428
- invocation directory. Every failure is silent — repo tagging must never
429
- break an upload — and local-path remotes never produce a tag, so no
430
- local absolute path can leak into the manifest.
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
- for candidate in (_session_cwd(path), Path.cwd()):
433
- if candidate is None or not candidate.is_dir():
434
- continue
435
- remote = _command_stdout(
436
- "git", "-C", str(candidate), "remote", "get-url", "origin"
437
- )
438
- slug = _repo_slug_from_remote(remote) if remote else None
439
- if slug:
440
- return slug
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
 
@@ -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
- first_events = _parse_jsonl(turn_files[0].read_text())
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
- for e in events:
444
- if e.get("type") == "result":
445
- total_cost += e.get("total_cost_usd", 0)
446
- total_turns_count += e.get("num_turns", 0)
447
-
448
- # `or "?"` (not just a .get default): a present-but-null value in the
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
- <div class="meta">
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
- def _stream_json_page(title: str, events: list[dict], turn_blocks: list[str]) -> str:
679
- sys_event = next((e for e in events if e.get("type") == "system"), {})
680
- total_cost = 0.0
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
- total_cost += float(event.get("total_cost_usd") or 0)
684
- session_id = str(sys_event.get("session_id") or "?")
685
- model = str(sys_event.get("model") or "?")
686
- version = str(sys_event.get("claude_code_version") or "?")
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
- <div class="meta">
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.dev1998
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=NHglRnCEmckvpVzU6ZqVXOP-3dLt7XjmroGHpBxOQuc,24798
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=9TW2uct0W8tWZPdRQsTm6hyunaWY9JQKatYIn1oJo7s,36029
295
- benchflow-0.7.2.dev1998.dist-info/METADATA,sha256=NGO_FL32422wl7_CwHCaI9twkfulYT2Fyqix8nKcIHI,15565
296
- benchflow-0.7.2.dev1998.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
297
- benchflow-0.7.2.dev1998.dist-info/entry_points.txt,sha256=qPMBVtH4qz3P4nMALEAdTbvpxvsVch9RhqshZKQn1dE,84
298
- benchflow-0.7.2.dev1998.dist-info/licenses/LICENSE,sha256=ohnbHkuBylrTtOvaZJk9Hz7dRwmaEEGAb5ZcBV4Xuhw,10779
299
- benchflow-0.7.2.dev1998.dist-info/RECORD,,
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,,