whileai 0.59__tar.gz → 0.61__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 (94) hide show
  1. {whileai-0.59/whileai.egg-info → whileai-0.61}/PKG-INFO +35 -4
  2. {whileai-0.59 → whileai-0.61}/README.md +34 -3
  3. {whileai-0.59 → whileai-0.61}/pyproject.toml +5 -1
  4. {whileai-0.59 → whileai-0.61}/whileai/auth.py +95 -1
  5. {whileai-0.59 → whileai-0.61}/whileai/cli.py +21 -1
  6. whileai-0.61/whileai/init_evals.py +526 -0
  7. {whileai-0.59 → whileai-0.61}/whileai/simulations/__init__.py +10 -1
  8. {whileai-0.59 → whileai-0.61}/whileai/simulations/data.py +14 -1
  9. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/actionspace.py +14 -5
  10. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/adapters.py +25 -1
  11. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/agents.py +51 -4
  12. whileai-0.61/whileai/simulations/generate/anthropic_backend.py +405 -0
  13. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/generator.py +6 -3
  14. {whileai-0.59 → whileai-0.61}/whileai/simulations/run/config.py +5 -0
  15. {whileai-0.59 → whileai-0.61}/whileai/simulations/run/engine.py +209 -13
  16. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/agreement.py +64 -3
  17. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/grade_llm.py +1 -1
  18. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/hygiene.py +18 -1
  19. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/judge_trust.py +45 -9
  20. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/judging.py +16 -1
  21. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/llm_judge.py +7 -2
  22. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/passat.py +31 -2
  23. whileai-0.61/whileai/simulations/score/preflight.py +927 -0
  24. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/stats.py +26 -7
  25. {whileai-0.59 → whileai-0.61}/whileai/simulations/simulation.py +13 -1
  26. whileai-0.61/whileai/templates/__init__.py +1 -0
  27. whileai-0.61/whileai/templates/evals.py +455 -0
  28. {whileai-0.59 → whileai-0.61/whileai.egg-info}/PKG-INFO +35 -4
  29. {whileai-0.59 → whileai-0.61}/whileai.egg-info/SOURCES.txt +5 -1
  30. {whileai-0.59 → whileai-0.61}/whileai.egg-info/entry_points.txt +2 -0
  31. whileai-0.59/whileai/simulations/score/preflight.py +0 -365
  32. {whileai-0.59 → whileai-0.61}/LICENSE +0 -0
  33. {whileai-0.59 → whileai-0.61}/MANIFEST.in +0 -0
  34. {whileai-0.59 → whileai-0.61}/setup.cfg +0 -0
  35. {whileai-0.59 → whileai-0.61}/whileai/__init__.py +0 -0
  36. {whileai-0.59 → whileai-0.61}/whileai/_env.py +0 -0
  37. {whileai-0.59 → whileai-0.61}/whileai/ingest.py +0 -0
  38. {whileai-0.59 → whileai-0.61}/whileai/py.typed +0 -0
  39. {whileai-0.59 → whileai-0.61}/whileai/simulations/__main__.py +0 -0
  40. {whileai-0.59 → whileai-0.61}/whileai/simulations/environment.py +0 -0
  41. {whileai-0.59 → whileai-0.61}/whileai/simulations/export.py +0 -0
  42. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/__init__.py +0 -0
  43. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/coverage.py +0 -0
  44. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/diversity.py +0 -0
  45. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/embeddings.py +0 -0
  46. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/explore.py +0 -0
  47. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/offline_agent.py +0 -0
  48. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/scenarios.py +0 -0
  49. {whileai-0.59 → whileai-0.61}/whileai/simulations/generate/usage_meter.py +0 -0
  50. {whileai-0.59 → whileai-0.61}/whileai/simulations/ingest/__init__.py +0 -0
  51. {whileai-0.59 → whileai-0.61}/whileai/simulations/ingest/otel.py +0 -0
  52. {whileai-0.59 → whileai-0.61}/whileai/simulations/ingest/platform.py +0 -0
  53. {whileai-0.59 → whileai-0.61}/whileai/simulations/ingest/traces.py +0 -0
  54. {whileai-0.59 → whileai-0.61}/whileai/simulations/monitor.py +0 -0
  55. {whileai-0.59 → whileai-0.61}/whileai/simulations/py.typed +0 -0
  56. {whileai-0.59 → whileai-0.61}/whileai/simulations/run/__init__.py +0 -0
  57. {whileai-0.59 → whileai-0.61}/whileai/simulations/run/rows.py +0 -0
  58. {whileai-0.59 → whileai-0.61}/whileai/simulations/run/spec.py +0 -0
  59. {whileai-0.59 → whileai-0.61}/whileai/simulations/schema.py +0 -0
  60. {whileai-0.59 → whileai-0.61}/whileai/simulations/schemas/row-v1.json +0 -0
  61. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/__init__.py +0 -0
  62. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/audit.py +0 -0
  63. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/checklist.py +0 -0
  64. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/curriculum.py +0 -0
  65. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/delta.py +0 -0
  66. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/grading.py +0 -0
  67. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/grounding.py +0 -0
  68. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/hack_scan.py +0 -0
  69. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/labels.py +0 -0
  70. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/logprobs.py +0 -0
  71. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/markers.py +0 -0
  72. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/optimize.py +0 -0
  73. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/pairwise.py +0 -0
  74. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/privileged.py +0 -0
  75. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/publish_gate.py +0 -0
  76. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/quality.py +0 -0
  77. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/reference.py +0 -0
  78. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/rubric.py +0 -0
  79. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/spec.py +0 -0
  80. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/stage.py +0 -0
  81. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/style.py +0 -0
  82. {whileai-0.59 → whileai-0.61}/whileai/simulations/score/trace.py +0 -0
  83. {whileai-0.59 → whileai-0.61}/whileai/simulations/training.py +0 -0
  84. {whileai-0.59 → whileai-0.61}/whileai/simulations/verify/__init__.py +0 -0
  85. {whileai-0.59 → whileai-0.61}/whileai/simulations/verify/base.py +0 -0
  86. {whileai-0.59 → whileai-0.61}/whileai/simulations/verify/code.py +0 -0
  87. {whileai-0.59 → whileai-0.61}/whileai/simulations/verify/math.py +0 -0
  88. {whileai-0.59 → whileai-0.61}/whileai/simulations/verify/structured.py +0 -0
  89. {whileai-0.59 → whileai-0.61}/whileai/simulations/verify/text.py +0 -0
  90. {whileai-0.59 → whileai-0.61}/whileai/simulations/world/__init__.py +0 -0
  91. {whileai-0.59 → whileai-0.61}/whileai/simulations/world/sandbox.py +0 -0
  92. {whileai-0.59 → whileai-0.61}/whileai.egg-info/dependency_links.txt +0 -0
  93. {whileai-0.59 → whileai-0.61}/whileai.egg-info/requires.txt +0 -0
  94. {whileai-0.59 → whileai-0.61}/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.59
3
+ Version: 0.61
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
@@ -41,7 +41,9 @@ The While Python SDK. One package, two importable modules:
41
41
  - `whileai`: the platform client. OTLP trace ingest and trace-dataset listing against the token gate.
42
42
  - `whileai.simulations`: post-training data for an agent. Give it the agent's traces, or its tools and system prompt; it simulates the situations, the people, and the world, plays the agent through multi-turn tool-calling conversations, and returns rows for your grader.
43
43
 
44
- **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`. zp, ZeroProof and While all name this one product: the package is `whileai`, the import is `whileai.simulations`, keys start with `zp_`. A machine with the old package still picks up `~/.zeroproof/credentials.json`; set `WHILEAI_HOME` to a fresh directory to isolate a new account from it.
44
+ Have an agent and want a pass rate with an interval? Start at [docs/evals.md](docs/evals.md) (offline, seconds, `coverage_gap` names what your tests miss).
45
+
46
+ **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`. zp, ZeroProof and While all name this one product: the package is `whileai`, the import is `whileai.simulations`, keys start with `zp_`. `zp`, `wai` and `whileai` run the same CLI, so `zp login` and `whileai login` do the same thing (the help text says `whileai`). A machine with the old package still picks up `~/.zeroproof/credentials.json`; set `WHILEAI_HOME` to a fresh directory to isolate a new account from it.
45
47
 
46
48
  Releases of `whileai` before 0.3 were an unrelated encrypted agent-to-agent messaging client. That code was removed in 0.04; pin `whileai<0.3` if you still depend on it.
47
49
 
@@ -51,7 +53,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
51
53
 
52
54
  ![How a row gets made: the draw, the coverage grid, the search arms, the rollout, the split](docs/how-a-row-gets-made.svg)
53
55
 
54
- A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
56
+ A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [zeroproofai.com/docs/engine](https://zeroproofai.com/docs/engine).
55
57
 
56
58
  ## Overview
57
59
 
@@ -195,7 +197,9 @@ that reads the trajectory, run the asks `k` times each, and read pass@1
195
197
  with its interval. Offline first, then the hosted writer. The how-to is
196
198
  [docs/evals.md](docs/evals.md); the runnable version is
197
199
  [`recipes/02-measure/eval-your-agent`](recipes/02-measure/eval-your-agent),
198
- which ends at a CI gate, not a push.
200
+ which ends at a CI gate, not a push. `whileai init-evals` writes those
201
+ four files for you, wired to the tools, system prompt and callable it
202
+ finds in the project, and prints what it picked.
199
203
 
200
204
  ```python
201
205
  data = wai.simulate(
@@ -241,6 +245,33 @@ data = wai.simulate(
241
245
  )
242
246
  ```
243
247
 
248
+ A model spec names the backend and the model. Four are built in:
249
+
250
+ - `ollama:<model>`: a local Ollama server, no key.
251
+ - `vllm:<model>@<url>`: any vLLM or OpenAI-compatible endpoint you serve.
252
+ - `openai:<model>`: `OPENAI_API_KEY`, and `OPENAI_BASE_URL` for a
253
+ compatible endpoint that is not OpenAI's.
254
+ - `anthropic:<model>`: the Claude Messages API on `ANTHROPIC_API_KEY`
255
+ (`WHILEAI_ANTHROPIC_API_KEY` overrides it).
256
+
257
+ A spec works everywhere one is accepted: `agent=`, `simulator=` for the
258
+ situation writer, `user_model=` for the simulated person, and `spec=` for the
259
+ judge.
260
+
261
+ ```bash
262
+ export ANTHROPIC_API_KEY=...
263
+ ```
264
+
265
+ ```python
266
+ data = wai.simulate(
267
+ agent="anthropic:claude-haiku-4-5",
268
+ tools=my_tools,
269
+ system_prompt=my_system_prompt,
270
+ simulator="anthropic:claude-sonnet-5", # the writer, on the same key
271
+ output="rollout.jsonl",
272
+ )
273
+ ```
274
+
244
275
  ## Five calls
245
276
 
246
277
  Agent to gated dataset. Everything else in this README is one layer down.
@@ -5,7 +5,9 @@ The While Python SDK. One package, two importable modules:
5
5
  - `whileai`: the platform client. OTLP trace ingest and trace-dataset listing against the token gate.
6
6
  - `whileai.simulations`: post-training data for an agent. Give it the agent's traces, or its tools and system prompt; it simulates the situations, the people, and the world, plays the agent through multi-turn tool-calling conversations, and returns rows for your grader.
7
7
 
8
- **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`. zp, ZeroProof and While all name this one product: the package is `whileai`, the import is `whileai.simulations`, keys start with `zp_`. A machine with the old package still picks up `~/.zeroproof/credentials.json`; set `WHILEAI_HOME` to a fresh directory to isolate a new account from it.
8
+ Have an agent and want a pass rate with an interval? Start at [docs/evals.md](docs/evals.md) (offline, seconds, `coverage_gap` names what your tests miss).
9
+
10
+ **Renamed.** This SDK was `zeroproof` (ZeroProof is now While). `pip install zeroproof` still works: it installs `whileai`, and `import zeroproof` (or the older `zeroproof_simulations`) resolves to the same modules with a deprecation warning. `ZEROPROOF_*` environment variables and a saved `~/.zeroproof/credentials.json` are still read. Change the import when you can; new releases land under `whileai`. zp, ZeroProof and While all name this one product: the package is `whileai`, the import is `whileai.simulations`, keys start with `zp_`. `zp`, `wai` and `whileai` run the same CLI, so `zp login` and `whileai login` do the same thing (the help text says `whileai`). A machine with the old package still picks up `~/.zeroproof/credentials.json`; set `WHILEAI_HOME` to a fresh directory to isolate a new account from it.
9
11
 
10
12
  Releases of `whileai` before 0.3 were an unrelated encrypted agent-to-agent messaging client. That code was removed in 0.04; pin `whileai<0.3` if you still depend on it.
11
13
 
@@ -15,7 +17,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
15
17
 
16
18
  ![How a row gets made: the draw, the coverage grid, the search arms, the rollout, the split](docs/how-a-row-gets-made.svg)
17
19
 
18
- A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
20
+ A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [zeroproofai.com/docs/engine](https://zeroproofai.com/docs/engine).
19
21
 
20
22
  ## Overview
21
23
 
@@ -159,7 +161,9 @@ that reads the trajectory, run the asks `k` times each, and read pass@1
159
161
  with its interval. Offline first, then the hosted writer. The how-to is
160
162
  [docs/evals.md](docs/evals.md); the runnable version is
161
163
  [`recipes/02-measure/eval-your-agent`](recipes/02-measure/eval-your-agent),
162
- which ends at a CI gate, not a push.
164
+ which ends at a CI gate, not a push. `whileai init-evals` writes those
165
+ four files for you, wired to the tools, system prompt and callable it
166
+ finds in the project, and prints what it picked.
163
167
 
164
168
  ```python
165
169
  data = wai.simulate(
@@ -205,6 +209,33 @@ data = wai.simulate(
205
209
  )
206
210
  ```
207
211
 
212
+ A model spec names the backend and the model. Four are built in:
213
+
214
+ - `ollama:<model>`: a local Ollama server, no key.
215
+ - `vllm:<model>@<url>`: any vLLM or OpenAI-compatible endpoint you serve.
216
+ - `openai:<model>`: `OPENAI_API_KEY`, and `OPENAI_BASE_URL` for a
217
+ compatible endpoint that is not OpenAI's.
218
+ - `anthropic:<model>`: the Claude Messages API on `ANTHROPIC_API_KEY`
219
+ (`WHILEAI_ANTHROPIC_API_KEY` overrides it).
220
+
221
+ A spec works everywhere one is accepted: `agent=`, `simulator=` for the
222
+ situation writer, `user_model=` for the simulated person, and `spec=` for the
223
+ judge.
224
+
225
+ ```bash
226
+ export ANTHROPIC_API_KEY=...
227
+ ```
228
+
229
+ ```python
230
+ data = wai.simulate(
231
+ agent="anthropic:claude-haiku-4-5",
232
+ tools=my_tools,
233
+ system_prompt=my_system_prompt,
234
+ simulator="anthropic:claude-sonnet-5", # the writer, on the same key
235
+ output="rollout.jsonl",
236
+ )
237
+ ```
238
+
208
239
  ## Five calls
209
240
 
210
241
  Agent to gated dataset. Everything else in this README is one layer down.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whileai"
7
- version = "0.59"
7
+ version = "0.61"
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"}
@@ -38,6 +38,10 @@ Datasets = "https://huggingface.co/datasets/zero-proof-ai/agent-simulations"
38
38
 
39
39
  [project.scripts]
40
40
  whileai = "whileai.cli:main"
41
+ # same CLI under the two names people type: the product is "zp"
42
+ # (keys start with zp_) and "wai" is the short import alias.
43
+ zp = "whileai.cli:main"
44
+ wai = "whileai.cli:main"
41
45
  whileai-simulations = "whileai.simulations.score.quality:main"
42
46
 
43
47
  [project.optional-dependencies]
@@ -34,12 +34,59 @@ from pathlib import Path
34
34
  from whileai._env import env_name, getenv
35
35
 
36
36
  DEFAULT_API_URL = "https://api.zeroproofai.com"
37
+ SIGN_IN_URL = "https://www.zeroproofai.com/sign-in"
38
+ #: the trial allowance the gate hands out, used when the reply does not say
39
+ DEFAULT_TRIAL_INPUT_TOKENS = 25_000
40
+ #: Input tokens one hosted situation spends, measured on a 4-tool spec: a
41
+ #: 12-situation run of one cost 28,490, so round it to 2,000 a situation.
42
+ #: The trial allowance is small enough that the count is the first thing
43
+ #: anyone needs to know about it.
44
+ INPUT_TOKENS_PER_SITUATION = 2_000
37
45
 
38
46
 
39
47
  class LoginError(RuntimeError):
40
48
  pass
41
49
 
42
50
 
51
+ def trial_situations(daily_input_tokens: float | None = None) -> int:
52
+ """About how many hosted situations a trial day buys, in round numbers."""
53
+ tokens = float(daily_input_tokens or DEFAULT_TRIAL_INPUT_TOKENS)
54
+ return max(1, int(tokens / INPUT_TOKENS_PER_SITUATION))
55
+
56
+
57
+ def trial_note(daily_input_tokens: float | None = None) -> str:
58
+ """The two facts a trial key needs before its first hosted run: how
59
+ far the daily allowance goes, and the offline writer that has no
60
+ allowance at all."""
61
+ return (
62
+ f"That is about {trial_situations(daily_input_tokens)} hosted situations a day "
63
+ f"(a 4-tool spec spends around {INPUT_TOKENS_PER_SITUATION:,} input tokens per "
64
+ "situation); simulate(..., simulator=False) writes situations offline with no quota "
65
+ f"and no network; signing in once at {SIGN_IN_URL} lifts the limit."
66
+ )
67
+
68
+
69
+ def trial_prerun_note() -> str | None:
70
+ """The one line a trial key needs before a hosted run spends it, or ``None``.
71
+
72
+ Read from the tier the credentials file recorded at sign-up or at the
73
+ last ``whileai status`` / ``whileai login``, so a run can say this
74
+ without a network call. A key from the environment has no recorded
75
+ tier, so this says nothing rather than guess at one.
76
+ """
77
+ if getenv("API_KEY"):
78
+ return None
79
+ saved = _read(credentials_path()) or {}
80
+ if not saved.get("api_key") or str(saved.get("tier") or "") != "trial":
81
+ return None
82
+ tokens = int(float(saved.get("daily_input_tokens") or DEFAULT_TRIAL_INPUT_TOKENS))
83
+ return (
84
+ f"trial key: the hosted writer covers about {trial_situations(tokens)} situations a day "
85
+ f"({tokens // 1000}k input tokens); simulator=False writes them offline with no quota; "
86
+ f"sign in once at {SIGN_IN_URL} to lift it"
87
+ )
88
+
89
+
43
90
  def _api_url() -> str:
44
91
  return getenv("API_URL", DEFAULT_API_URL).rstrip("/")
45
92
 
@@ -86,6 +133,37 @@ def _read(path: Path) -> dict | None:
86
133
  return data if isinstance(data, dict) else None
87
134
 
88
135
 
136
+ def _tier_fields(payload: dict) -> dict:
137
+ """The tier facts worth keeping next to the key, from ``/signup`` or ``/me``."""
138
+ tier = str((payload or {}).get("tier") or "").strip()
139
+ if not tier:
140
+ return {}
141
+ fields: dict = {"tier": tier}
142
+ trial = (payload or {}).get("trial") or {}
143
+ if isinstance(trial, dict):
144
+ if trial.get("daily_input_tokens") is not None:
145
+ fields["daily_input_tokens"] = trial["daily_input_tokens"]
146
+ if trial.get("expires_at"):
147
+ fields["expires_at"] = str(trial["expires_at"])
148
+ return fields
149
+
150
+
151
+ def remember_account(payload: dict) -> None:
152
+ """Record a ``/signup`` or ``/me`` reply's tier in the credentials file.
153
+
154
+ Only for the key that file holds: a key from the environment may belong
155
+ to another account, and writing its tier here would mislabel this one.
156
+ """
157
+ fields = _tier_fields(payload)
158
+ saved = _read(credentials_path())
159
+ if not fields or not saved or not saved.get("api_key"):
160
+ return
161
+ if all(saved.get(key) == value for key, value in fields.items()):
162
+ return
163
+ saved.update(fields)
164
+ _write_private(credentials_path(), saved)
165
+
166
+
89
167
  def stored_api_key() -> str | None:
90
168
  """The key saved by ``whileai login``, or ``None``."""
91
169
  data = _read(credentials_path())
@@ -238,6 +316,10 @@ def login(
238
316
  )
239
317
  with contextlib.suppress(OSError):
240
318
  _pending_path().unlink()
319
+ # the tier the key carries, saved next to it, so a run can
320
+ # name a trial limit before it spends one
321
+ with contextlib.suppress(LoginError, OSError, ValueError):
322
+ remember_account(account(data["api_key"]))
241
323
  say(f"Logged in. Key saved to {credentials_path()}")
242
324
  return data["api_key"]
243
325
  error = data.get("error", "")
@@ -280,6 +362,9 @@ def signup(email: str, *, name: str | None = None, out: Callable[[str], None] |
280
362
  "user_id": data.get("user_id"),
281
363
  "email": data.get("email", email),
282
364
  "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
365
+ # so simulate() can warn about the trial before a hosted
366
+ # run spends it, without a call to /me
367
+ **_tier_fields(data),
283
368
  },
284
369
  )
285
370
  say(f"Account created for {data.get('email', email)}. Key saved to {credentials_path()}")
@@ -291,6 +376,7 @@ def signup(email: str, *, name: str | None = None, out: Callable[[str], None] |
291
376
  f"output tokens a day, {int(trial.get('storage_bytes', 104857600)) // 1048576} MB, "
292
377
  f"{trial.get('datasets', 10)} datasets, expires {str(trial.get('expires_at', ''))[:10]}."
293
378
  )
379
+ say(trial_note(trial.get("daily_input_tokens")))
294
380
  say(
295
381
  trial.get("lift")
296
382
  or "Sign in once at https://www.zeroproofai.com/sign-in with an email code to lift trial limits."
@@ -321,7 +407,12 @@ def logout() -> bool:
321
407
 
322
408
 
323
409
  def status() -> dict:
324
- """What the SDK would use right now, with the key masked."""
410
+ """What the SDK would use right now, with the key masked.
411
+
412
+ On a trial key ``trial_note`` says how many hosted situations the
413
+ daily allowance covers and names the offline writer that has none.
414
+ ``tier`` comes from ``GET /me``, so reading it costs one call.
415
+ """
325
416
  env = getenv("API_KEY")
326
417
  env_var = env_name("API_KEY")
327
418
  saved = _read(credentials_path()) or {}
@@ -350,8 +441,11 @@ def status() -> dict:
350
441
  out["tier"] = f"unknown ({err})"
351
442
  else:
352
443
  out["tier"] = me.get("tier")
444
+ if not env:
445
+ remember_account(me)
353
446
  if me.get("tier") == "trial":
354
447
  trial = me.get("trial") or {}
355
448
  out["trial_expires_at"] = trial.get("expires_at")
356
449
  out["lift"] = trial.get("lift")
450
+ out["trial_note"] = trial_note(trial.get("daily_input_tokens"))
357
451
  return out
@@ -1,4 +1,4 @@
1
- """``whileai`` command line: login, signup, logout, status."""
1
+ """``whileai`` command line: login, signup, logout, status, init-evals."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -7,6 +7,7 @@ import json
7
7
  import sys
8
8
 
9
9
  from . import auth
10
+ from .init_evals import add_arguments as init_evals_args
10
11
 
11
12
 
12
13
  def main(argv: list[str] | None = None) -> int:
@@ -33,6 +34,12 @@ def main(argv: list[str] | None = None) -> int:
33
34
  sub.add_parser("logout", help="delete the saved key")
34
35
  sub.add_parser("status", help="show which key the SDK will use")
35
36
 
37
+ p_init = sub.add_parser(
38
+ "init-evals",
39
+ help="write an eval harness (agent, judge, run, test) wired to this project",
40
+ )
41
+ init_evals_args(p_init)
42
+
36
43
  p_purge = sub.add_parser(
37
44
  "purge", help="delete an agent's traces, datasets and record, or empty datasets"
38
45
  )
@@ -72,6 +79,17 @@ def main(argv: list[str] | None = None) -> int:
72
79
  return 1
73
80
  return 0
74
81
 
82
+ if args.command == "init-evals":
83
+ from .init_evals import init_evals
84
+
85
+ return init_evals(
86
+ agent=args.agent,
87
+ tools=args.tools,
88
+ system_prompt=args.system_prompt,
89
+ out=args.dir,
90
+ force=args.force,
91
+ )
92
+
75
93
  if args.command == "logout":
76
94
  print("Logged out." if auth.logout() else "No saved key.")
77
95
  return 0
@@ -79,6 +97,8 @@ def main(argv: list[str] | None = None) -> int:
79
97
  if args.command == "status":
80
98
  shown = auth.status()
81
99
  print(json.dumps(shown, indent=2))
100
+ if shown.get("trial_note"):
101
+ print(shown["trial_note"])
82
102
  if not shown.get("configured"):
83
103
  print(
84
104
  "no API key configured: run `whileai login` or set WHILEAI_API_KEY",