whileai 0.54__tar.gz → 0.55__tar.gz

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.
Files changed (88) hide show
  1. {whileai-0.54/whileai.egg-info → whileai-0.55}/PKG-INFO +1 -1
  2. {whileai-0.54 → whileai-0.55}/pyproject.toml +1 -1
  3. {whileai-0.54 → whileai-0.55}/whileai/simulations/__init__.py +4 -0
  4. {whileai-0.54 → whileai-0.55}/whileai/simulations/export.py +7 -0
  5. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/agents.py +34 -2
  6. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/grading.py +11 -2
  7. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/judging.py +42 -0
  8. {whileai-0.54 → whileai-0.55}/whileai/simulations/training.py +58 -6
  9. {whileai-0.54 → whileai-0.55/whileai.egg-info}/PKG-INFO +1 -1
  10. {whileai-0.54 → whileai-0.55}/LICENSE +0 -0
  11. {whileai-0.54 → whileai-0.55}/MANIFEST.in +0 -0
  12. {whileai-0.54 → whileai-0.55}/README.md +0 -0
  13. {whileai-0.54 → whileai-0.55}/setup.cfg +0 -0
  14. {whileai-0.54 → whileai-0.55}/whileai/__init__.py +0 -0
  15. {whileai-0.54 → whileai-0.55}/whileai/_env.py +0 -0
  16. {whileai-0.54 → whileai-0.55}/whileai/auth.py +0 -0
  17. {whileai-0.54 → whileai-0.55}/whileai/cli.py +0 -0
  18. {whileai-0.54 → whileai-0.55}/whileai/ingest.py +0 -0
  19. {whileai-0.54 → whileai-0.55}/whileai/py.typed +0 -0
  20. {whileai-0.54 → whileai-0.55}/whileai/simulations/__main__.py +0 -0
  21. {whileai-0.54 → whileai-0.55}/whileai/simulations/data.py +0 -0
  22. {whileai-0.54 → whileai-0.55}/whileai/simulations/environment.py +0 -0
  23. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/__init__.py +0 -0
  24. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/actionspace.py +0 -0
  25. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/adapters.py +0 -0
  26. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/coverage.py +0 -0
  27. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/diversity.py +0 -0
  28. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/embeddings.py +0 -0
  29. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/explore.py +0 -0
  30. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/generator.py +0 -0
  31. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/offline_agent.py +0 -0
  32. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/scenarios.py +0 -0
  33. {whileai-0.54 → whileai-0.55}/whileai/simulations/generate/usage_meter.py +0 -0
  34. {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/__init__.py +0 -0
  35. {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/otel.py +0 -0
  36. {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/platform.py +0 -0
  37. {whileai-0.54 → whileai-0.55}/whileai/simulations/ingest/traces.py +0 -0
  38. {whileai-0.54 → whileai-0.55}/whileai/simulations/monitor.py +0 -0
  39. {whileai-0.54 → whileai-0.55}/whileai/simulations/py.typed +0 -0
  40. {whileai-0.54 → whileai-0.55}/whileai/simulations/run/__init__.py +0 -0
  41. {whileai-0.54 → whileai-0.55}/whileai/simulations/run/config.py +0 -0
  42. {whileai-0.54 → whileai-0.55}/whileai/simulations/run/engine.py +0 -0
  43. {whileai-0.54 → whileai-0.55}/whileai/simulations/run/rows.py +0 -0
  44. {whileai-0.54 → whileai-0.55}/whileai/simulations/run/spec.py +0 -0
  45. {whileai-0.54 → whileai-0.55}/whileai/simulations/schema.py +0 -0
  46. {whileai-0.54 → whileai-0.55}/whileai/simulations/schemas/row-v1.json +0 -0
  47. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/__init__.py +0 -0
  48. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/agreement.py +0 -0
  49. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/checklist.py +0 -0
  50. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/curriculum.py +0 -0
  51. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/delta.py +0 -0
  52. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/grade_llm.py +0 -0
  53. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/grounding.py +0 -0
  54. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/hack_scan.py +0 -0
  55. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/hygiene.py +0 -0
  56. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/judge_trust.py +0 -0
  57. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/labels.py +0 -0
  58. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/llm_judge.py +0 -0
  59. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/logprobs.py +0 -0
  60. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/markers.py +0 -0
  61. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/optimize.py +0 -0
  62. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/pairwise.py +0 -0
  63. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/passat.py +0 -0
  64. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/preflight.py +0 -0
  65. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/privileged.py +0 -0
  66. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/publish_gate.py +0 -0
  67. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/quality.py +0 -0
  68. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/reference.py +0 -0
  69. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/rubric.py +0 -0
  70. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/spec.py +0 -0
  71. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/stage.py +0 -0
  72. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/stats.py +0 -0
  73. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/style.py +0 -0
  74. {whileai-0.54 → whileai-0.55}/whileai/simulations/score/trace.py +0 -0
  75. {whileai-0.54 → whileai-0.55}/whileai/simulations/simulation.py +0 -0
  76. {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/__init__.py +0 -0
  77. {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/base.py +0 -0
  78. {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/code.py +0 -0
  79. {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/math.py +0 -0
  80. {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/structured.py +0 -0
  81. {whileai-0.54 → whileai-0.55}/whileai/simulations/verify/text.py +0 -0
  82. {whileai-0.54 → whileai-0.55}/whileai/simulations/world/__init__.py +0 -0
  83. {whileai-0.54 → whileai-0.55}/whileai/simulations/world/sandbox.py +0 -0
  84. {whileai-0.54 → whileai-0.55}/whileai.egg-info/SOURCES.txt +0 -0
  85. {whileai-0.54 → whileai-0.55}/whileai.egg-info/dependency_links.txt +0 -0
  86. {whileai-0.54 → whileai-0.55}/whileai.egg-info/entry_points.txt +0 -0
  87. {whileai-0.54 → whileai-0.55}/whileai.egg-info/requires.txt +0 -0
  88. {whileai-0.54 → whileai-0.55}/whileai.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whileai
3
- Version: 0.54
3
+ Version: 0.55
4
4
  Summary: While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt.
5
5
  Author: While
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whileai"
7
- version = "0.54"
7
+ version = "0.55"
8
8
  description = "While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -175,6 +175,7 @@ from .training import (
175
175
  TrainingRun,
176
176
  attach_delta,
177
177
  attach_holdout,
178
+ delete_model,
178
179
  delete_run,
179
180
  get_run,
180
181
  list_runs,
@@ -183,6 +184,7 @@ from .training import (
183
184
  serve,
184
185
  train,
185
186
  training_run,
187
+ unserve,
186
188
  )
187
189
  from .verify import Verifier, verifier
188
190
  from .world.sandbox import MockEnvironment
@@ -253,6 +255,7 @@ __all__ = [
253
255
  "dedupe_groups",
254
256
  "delete_dataset",
255
257
  "delete_empty_datasets",
258
+ "delete_model",
256
259
  "delete_run",
257
260
  "delta_report",
258
261
  "dimensions_from_traces",
@@ -382,6 +385,7 @@ __all__ = [
382
385
  "trim_unanimous_groups",
383
386
  "ungrounded_arguments",
384
387
  "unpublish",
388
+ "unserve",
385
389
  "update_dataset",
386
390
  "validate",
387
391
  "verifier",
@@ -734,6 +734,7 @@ export_dataset = export_training
734
734
  _PAIR_KEYS = (
735
735
  "tie",
736
736
  "pairwise",
737
+ "first_turn_differs",
737
738
  "chosen_score",
738
739
  "rejected_score",
739
740
  "margin",
@@ -853,6 +854,12 @@ def export_preference(
853
854
  length_note = length_confound_warning(chosen_longer, len(deltas))
854
855
  if length_note:
855
856
  report["warnings"] = [length_note]
857
+ identical = sum(1 for r in out_rows if r.get("first_turn_differs") is False)
858
+ if identical:
859
+ from .score.judging import first_turn_note
860
+
861
+ report["first_turn_identical"] = identical
862
+ report.setdefault("warnings", []).append(first_turn_note(identical, len(out_rows)))
856
863
  margins = [r["margin"] for r in out_rows if isinstance(r.get("margin"), (int, float))]
857
864
  if margins:
858
865
  report["mean_margin"] = round(sum(margins) / len(margins), 4)
@@ -10,7 +10,7 @@ import os
10
10
  import re
11
11
  import threading
12
12
  import time
13
- from collections.abc import Callable
13
+ from collections.abc import Callable, Mapping
14
14
  from typing import Any
15
15
  from urllib.parse import urlparse
16
16
 
@@ -531,6 +531,7 @@ def complete(
531
531
  timeout: float = 60,
532
532
  n: int = 1,
533
533
  logprobs: bool | str = False,
534
+ extra: Mapping[str, Any] | None = None,
534
535
  ) -> dict:
535
536
  """POST /chat/completions. Reuses a thread-local keep-alive connection.
536
537
 
@@ -577,6 +578,8 @@ def complete(
577
578
  if tools:
578
579
  payload["tools"] = _wire_tools(tools)
579
580
  payload.update(_request_extras(base_url, model))
581
+ if extra:
582
+ payload.update(dict(extra))
580
583
  headers = {"Content-Type": "application/json", "Connection": "keep-alive"}
581
584
  if key:
582
585
  headers["Authorization"] = f"Bearer {key}"
@@ -730,8 +733,21 @@ def _strip_tool_markup(text: str) -> str:
730
733
  return cleaned.strip()
731
734
 
732
735
 
736
+ _THINK_BLOCK = re.compile(r"<think>.*?</think>\s*", re.S | re.I)
737
+ _THINK_OPEN = re.compile(r"<think>.*\Z", re.S | re.I)
738
+
739
+
740
+ def _strip_think(text: str) -> str:
741
+ """Drop a thinking model's reasoning markup. A closed block goes whole;
742
+ an unclosed ``<think>`` (the token cap landed inside it) goes to the
743
+ end. What is left is the reply, which is what a grader, a marker and
744
+ the next turn's history should see (#264)."""
745
+ text = _THINK_BLOCK.sub("", text)
746
+ return _THINK_OPEN.sub("", text)
747
+
748
+
733
749
  def _spoken_text(reply: dict) -> str:
734
- return _strip_tool_markup(str(reply.get("content") or ""))
750
+ return _strip_tool_markup(_strip_think(str(reply.get("content") or "")))
735
751
 
736
752
 
737
753
  def _calls_from_reply(reply: dict) -> tuple[list[dict], dict]:
@@ -1391,9 +1407,23 @@ def local_model(
1391
1407
  timeout: float = 60,
1392
1408
  max_tokens: int | None = None,
1393
1409
  user_model: str | None = None,
1410
+ thinking: bool | None = None,
1394
1411
  ) -> Callable:
1412
+ """An agent that talks to an OpenAI-compatible endpoint (a served
1413
+ adapter, a local vLLM, any chat server) for ``simulate(agent=...)``.
1414
+
1415
+ ``thinking`` is for reasoning bases such as Qwen3: ``False`` sends
1416
+ ``chat_template_kwargs={"enable_thinking": False}`` so the reply is
1417
+ the answer, not the reasoning, the way the hosted Qwen path already
1418
+ does; ``True`` asks for it; ``None`` (the default) sends nothing and
1419
+ leaves the server's default. Either way ``<think>`` markup never
1420
+ reaches ``step["text"]`` or ``final_text``.
1421
+ """
1395
1422
  local = threading.local()
1396
1423
  plans = fault_plans if fault_plans is not None else {}
1424
+ extras: dict[str, Any] | None = (
1425
+ None if thinking is None else {"chat_template_kwargs": {"enable_thinking": bool(thinking)}}
1426
+ )
1397
1427
  # The simulated user's model. None means the agent's own model plays
1398
1428
  # the user (the default); a backend spec moves that role to another
1399
1429
  # model, with the key resolved for that endpoint.
@@ -1438,6 +1468,7 @@ def local_model(
1438
1468
  temperature=temperature,
1439
1469
  timeout=timeout,
1440
1470
  max_tokens=120,
1471
+ extra=extras,
1441
1472
  )
1442
1473
  opener_text = (_spoken_text(greet) or "").strip()
1443
1474
  if opener_text:
@@ -1477,6 +1508,7 @@ def local_model(
1477
1508
  timeout=timeout,
1478
1509
  max_tokens=reply_budget(max_tokens),
1479
1510
  logprobs=logprobs,
1511
+ extra=extras,
1480
1512
  )
1481
1513
  calls, assistant = _calls_from_reply(reply)
1482
1514
  # One agent turn, one set of sampling facts, on its first step.
@@ -40,6 +40,15 @@ _FAULT_ALIASES = {
40
40
  "entity already acted on": "already_done",
41
41
  }
42
42
  _CLEAN_FAULT = {"", "ok", "created", "deleted", "success", "exit_0", "no fault", "clean"}
43
+ #: Statuses a tool result can carry that mean the call failed. A ``status``
44
+ #: key with any other value (``paid``, ``open``, ``completed``) is the
45
+ #: tool's own vocabulary, not a fault (#261).
46
+ _KNOWN_FAULTS = (
47
+ _BAD_STATUS
48
+ | set(_FAULT_ALIASES)
49
+ | set(_FAULT_ALIASES.values())
50
+ | {"stale", "malformed", "already_done", "not_found", "deny", "garbled"}
51
+ )
43
52
  _HTTP_FAIL = re.compile(r"^[45]\d\d$")
44
53
  _INFRA_STUB = re.compile(
45
54
  r"^<agent error:|returned\s+[45]\d\d\b|^(https?://\S+\s+)?[45]\d\d(\s|$)", re.I
@@ -426,10 +435,10 @@ def _fault_from_result(result) -> str:
426
435
  if status in {"already_done", "already_acted_on"} or reason == "already_acted_on":
427
436
  return "already_done"
428
437
  name = normalize_fault_name(status)
429
- if name:
438
+ if name and (name in _KNOWN_FAULTS or status in _KNOWN_FAULTS):
430
439
  return name
431
440
  if _step_faulted(result):
432
- return normalize_fault_name(status) or "error"
441
+ return name or "error"
433
442
  return ""
434
443
 
435
444
 
@@ -563,6 +563,42 @@ def length_confound_warning(chosen_longer: int, n: int) -> str | None:
563
563
  return None
564
564
 
565
565
 
566
+ def _first_turn(row: dict) -> str:
567
+ """What the policy emitted first, read the way the hosted DPO trainer
568
+ reads it: the first tool step as its call, else the first assistant
569
+ text, else ``final_text``."""
570
+ steps = [s for s in (row.get("steps") or []) if isinstance(s, dict)]
571
+ for step in steps:
572
+ if step.get("tool"):
573
+ args = step.get("arguments")
574
+ if args is None:
575
+ args = step.get("args")
576
+ return json.dumps({"name": step["tool"], "arguments": args or {}}, sort_keys=True)
577
+ for step in steps:
578
+ if str(step.get("text") or "").strip():
579
+ return str(step["text"]).strip()
580
+ for message in row.get("messages") or []:
581
+ if isinstance(message, dict) and message.get("role") == "assistant":
582
+ text = str(message.get("content") or "").strip()
583
+ if text:
584
+ return text
585
+ return str(row.get("final_text") or "").strip()
586
+
587
+
588
+ def first_turn_note(identical: int, n: int) -> str:
589
+ """The warning for pairs whose first assistant turns read the same."""
590
+ if not identical:
591
+ return ""
592
+ left = n - identical
593
+ return (
594
+ f"{identical}/{n} pairs have identical first assistant turns (same opening tool call "
595
+ "or line); their contrast is later in the rollout. The hosted DPO trainer compares "
596
+ f"first turns only and will drop them, leaving {left} (it needs at least 8). Keep "
597
+ "[p for p in pairs if p['first_turn_differs']] to see what it will train on, or "
598
+ "export_preference(pairs) for a trainer that reads whole conversations."
599
+ )
600
+
601
+
566
602
  def build_preference_pairs(
567
603
  rows: Sequence[dict],
568
604
  *,
@@ -651,6 +687,7 @@ def build_preference_pairs(
651
687
  "rejected_model": r_model,
652
688
  "same_policy": (c_model == r_model) if c_model and r_model else None,
653
689
  "length_delta": reply_length(chosen) - reply_length(rejected),
690
+ "first_turn_differs": _first_turn(chosen) != _first_turn(rejected),
654
691
  "chosen_reason": str(chosen.get("reason") or ""),
655
692
  "rejected_reason": str(rejected.get("reason") or ""),
656
693
  "rejected_failure_class": rejected.get("failure_class"),
@@ -673,10 +710,13 @@ def build_preference_pairs(
673
710
  for p in pairs
674
711
  if p["chosen_score"] not in (0.0, 1.0) or p["rejected_score"] not in (0.0, 1.0)
675
712
  )
713
+ identical = sum(1 for p in pairs if not p["first_turn_differs"])
676
714
  warnings: list[str] = []
677
715
  length_note = length_confound_warning(chosen_longer, n)
678
716
  if length_note:
679
717
  warnings.append(length_note)
718
+ if identical:
719
+ warnings.append(first_turn_note(identical, n))
680
720
  if mixed_policy:
681
721
  warnings.append(
682
722
  f"{mixed_policy}/{n} pairs mix policies (chosen and rejected from different "
@@ -694,6 +734,8 @@ def build_preference_pairs(
694
734
  "min_margin": min_margin,
695
735
  "mean_margin": round(sum(p["margin"] for p in pairs) / n, 4) if n else None,
696
736
  "partial_score_pairs": partial,
737
+ "first_turn_identical": identical,
738
+ "trainer_pairs": n - identical,
697
739
  "same_policy_pairs": same_policy,
698
740
  "mixed_policy_pairs": mixed_policy,
699
741
  "eval_sourced": eval_pairs,
@@ -474,6 +474,11 @@ class TrainingRun:
474
474
  else:
475
475
  self.finish("done")
476
476
 
477
+ @property
478
+ def id(self) -> str:
479
+ """The run id, the handle ``get_run``, ``serve`` and ``delete_run`` take."""
480
+ return self.run_id
481
+
477
482
  def __repr__(self) -> str:
478
483
  return (
479
484
  f"TrainingRun({self.run_id!r}, {self.name!r}, status={self.status!r}, step={self.step})"
@@ -825,11 +830,40 @@ def models(*, api_key: str | None = None) -> list[dict[str, Any]]:
825
830
  """The account's hosted models: ``name``, ``baseModel``, ``adapter``,
826
831
  ``adapterRunId``, ``version``, ``endpoint`` (an OpenAI-compatible
827
832
  base URL; send the account key as the bearer and ``name`` as the
828
- model)."""
833
+ model).
834
+
835
+ A row here is a registry entry, not a running GPU: the endpoint
836
+ behind it idles to zero on its own and an unused model costs nothing.
837
+ The row stays until ``unserve(name)`` removes it; serving the same
838
+ name again bumps its ``version`` rather than adding a row."""
829
839
  out = _call("GET", "/models", api_key)
830
840
  return list(out.get("models") or []) if isinstance(out, dict) else []
831
841
 
832
842
 
843
+ def unserve(
844
+ name: str,
845
+ *,
846
+ api_key: str | None = None,
847
+ transport: Callable[..., Any] | None = None,
848
+ ) -> dict[str, Any]:
849
+ """Stop hosting ``name``: removes the model row from the account, so
850
+ ``models()`` no longer lists it and its endpoint stops answering for
851
+ that name. The inverse of ``serve``, the way ``delete_dataset`` is the
852
+ inverse of ``push``. The adapter weights and the training run stay;
853
+ ``serve`` the run again to bring it back (at version 1).
854
+ Returns ``{"name": ..., "deleted": True}``."""
855
+ call = transport or _call
856
+ key = str(name).strip().lower()
857
+ if not key:
858
+ raise ValueError("unserve: name is the hosted model's name, as models() lists it")
859
+ out = call("DELETE", f"/models/{key}", api_key)
860
+ return dict(out) if isinstance(out, dict) else {"name": key, "deleted": True}
861
+
862
+
863
+ #: Same call, the other spelling: symmetric with ``delete_dataset``.
864
+ delete_model = unserve
865
+
866
+
833
867
  def serve(
834
868
  name: str,
835
869
  run: TrainingRun | str | None = None,
@@ -842,18 +876,36 @@ def serve(
842
876
  row; ``endpoint`` is the OpenAI-compatible base URL and ``name`` the
843
877
  model id to send. Posting an existing name bumps ``version``.
844
878
 
845
- ``run`` is a ``TrainingRun`` or its id; the adapter and base model
846
- come from the run record unless ``base_model`` is given. No ``run``
847
- serves the bare base (``base_model`` required).
879
+ ``run`` is a ``TrainingRun``, the record ``get_run`` returns, or the
880
+ run id; the adapter and base model come from the run record unless
881
+ ``base_model`` is given. No ``run`` serves the bare base
882
+ (``base_model`` required). ``unserve`` is the inverse.
848
883
  """
849
884
  call = transport or _call
850
885
  adapter: str | None = None
851
886
  base = base_model
887
+ run_id: str | None
852
888
  if isinstance(run, TrainingRun):
853
889
  adapter = run.adapter
854
- run_id: str | None = run.run_id
890
+ run_id = run.run_id
891
+ elif isinstance(run, dict):
892
+ # The record ``get_run`` returns. Reading the id here is what lets
893
+ # train in one process and serve in the next compose (#262).
894
+ run_id = str(run.get("runId") or run.get("run_id") or run.get("id") or "").strip() or None
895
+ if not run_id:
896
+ raise TypeError(
897
+ "run is a dict with no runId; pass the record wai.get_run(run_id) returns, "
898
+ "a TrainingRun, or the run id string"
899
+ )
900
+ adapter = str(run.get("adapter") or "").strip() or None
901
+ base = base or run.get("baseModel") or run.get("base_model")
902
+ elif run is None or isinstance(run, str):
903
+ run_id = (str(run).strip() or None) if run else None
855
904
  else:
856
- run_id = str(run) if run else None
905
+ raise TypeError(
906
+ "run must be a TrainingRun, the record wai.get_run(run_id) returns, or the run "
907
+ f"id string; got {type(run).__name__}"
908
+ )
857
909
  if run_id and (adapter is None or base is None):
858
910
  meta = call("GET", f"/runs/{run_id}", api_key)
859
911
  meta = meta if isinstance(meta, dict) else {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whileai
3
- Version: 0.54
3
+ Version: 0.55
4
4
  Summary: While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt.
5
5
  Author: While
6
6
  License: Apache-2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes