halflife-ai 0.3.0__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.
- halflife_ai-0.3.0/LICENSE +17 -0
- halflife_ai-0.3.0/PKG-INFO +239 -0
- halflife_ai-0.3.0/README.md +216 -0
- halflife_ai-0.3.0/pyproject.toml +38 -0
- halflife_ai-0.3.0/setup.cfg +4 -0
- halflife_ai-0.3.0/src/halflife/__init__.py +45 -0
- halflife_ai-0.3.0/src/halflife/__main__.py +3 -0
- halflife_ai-0.3.0/src/halflife/agents/__init__.py +93 -0
- halflife_ai-0.3.0/src/halflife/agents/_http.py +39 -0
- halflife_ai-0.3.0/src/halflife/agents/anthropic_api.py +82 -0
- halflife_ai-0.3.0/src/halflife/agents/openai_compat.py +72 -0
- halflife_ai-0.3.0/src/halflife/cli.py +441 -0
- halflife_ai-0.3.0/src/halflife/decorator.py +94 -0
- halflife_ai-0.3.0/src/halflife/demo.py +157 -0
- halflife_ai-0.3.0/src/halflife/history.py +95 -0
- halflife_ai-0.3.0/src/halflife/mcp.py +276 -0
- halflife_ai-0.3.0/src/halflife/remediation.py +175 -0
- halflife_ai-0.3.0/src/halflife/report.py +536 -0
- halflife_ai-0.3.0/src/halflife/results.py +30 -0
- halflife_ai-0.3.0/src/halflife/rules.py +289 -0
- halflife_ai-0.3.0/src/halflife/runner.py +188 -0
- halflife_ai-0.3.0/src/halflife/sandbox.py +151 -0
- halflife_ai-0.3.0/src/halflife/scaffold.py +95 -0
- halflife_ai-0.3.0/src/halflife/stats.py +153 -0
- halflife_ai-0.3.0/src/halflife/suggest.py +350 -0
- halflife_ai-0.3.0/src/halflife/suite.py +53 -0
- halflife_ai-0.3.0/src/halflife/suites/__init__.py +3 -0
- halflife_ai-0.3.0/src/halflife/suites/_money.py +14 -0
- halflife_ai-0.3.0/src/halflife/suites/claims_intake.py +336 -0
- halflife_ai-0.3.0/src/halflife/suites/invoice_matching.py +290 -0
- halflife_ai-0.3.0/src/halflife/task.py +105 -0
- halflife_ai-0.3.0/src/halflife/traces.py +170 -0
- halflife_ai-0.3.0/src/halflife_ai.egg-info/PKG-INFO +239 -0
- halflife_ai-0.3.0/src/halflife_ai.egg-info/SOURCES.txt +42 -0
- halflife_ai-0.3.0/src/halflife_ai.egg-info/dependency_links.txt +1 -0
- halflife_ai-0.3.0/src/halflife_ai.egg-info/entry_points.txt +2 -0
- halflife_ai-0.3.0/src/halflife_ai.egg-info/requires.txt +9 -0
- halflife_ai-0.3.0/src/halflife_ai.egg-info/top_level.txt +1 -0
- halflife_ai-0.3.0/tests/test_daily.py +58 -0
- halflife_ai-0.3.0/tests/test_halflife.py +136 -0
- halflife_ai-0.3.0/tests/test_mcp.py +63 -0
- halflife_ai-0.3.0/tests/test_model_agents.py +101 -0
- halflife_ai-0.3.0/tests/test_rules_audit.py +113 -0
- halflife_ai-0.3.0/tests/test_suggest.py +94 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Evan Prawda
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: halflife-ai
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Measure how long a task you can trust an AI agent with.
|
|
5
|
+
Author-email: Evan Prawda <evanprawda92@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/ev-prawda92/halflife
|
|
8
|
+
Keywords: ai,agents,evaluation,benchmark,reliability,llm
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: anthropic
|
|
17
|
+
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
18
|
+
Provides-Extra: openai
|
|
19
|
+
Requires-Dist: openai>=1.40; extra == "openai"
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# halflife
|
|
25
|
+
|
|
26
|
+
**Your agent works on 5-step tasks. Does it work on 50?**
|
|
27
|
+
|
|
28
|
+
[](https://github.com/ev-prawda92/halflife/actions/workflows/tests.yml)
|
|
29
|
+
[](https://pypi.org/project/halflife-ai/)
|
|
30
|
+
[](pyproject.toml)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
halflife measures your agent's **reliability half-life**: the task length at which it succeeds half the time. Then it tells you where runs broke, what to change, and re-runs exactly the cases that failed once you've changed it.
|
|
34
|
+
|
|
35
|
+
Agents fail quietly as tasks get longer: they lose a running total from step 4 by step 30, skip the last item, pay a duplicate they saw twenty steps ago. Unit tests and single-turn evals don't catch it. halflife does, in your terminal, your tests, your CI, and your coding agent.
|
|
36
|
+
|
|
37
|
+
<p align="center"><img src="docs/terminal.png" width="760" alt="halflife terminal output: success by length, half-life 32.7 steps, failure modes, and fixes"></p>
|
|
38
|
+
|
|
39
|
+
> The numbers above come from `halflife demo`, a simulated agent built to show the tool. Run it on your own agent for real numbers.
|
|
40
|
+
|
|
41
|
+
## Start in one minute
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install halflife-ai
|
|
45
|
+
halflife demo
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then pick the way in that fits what you have.
|
|
49
|
+
|
|
50
|
+
### You build with Claude Code, Cursor or Codex → let your coding agent use it
|
|
51
|
+
|
|
52
|
+
Most agents today are built with a coding agent. Give it halflife once and it checks every agent it builds for you:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
claude mcp add halflife -- halflife mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then ask it in plain words: *"check how reliable my agent is on long tasks and fix what breaks."* It runs the benchmark or audits your logs, reads where runs broke and why, proposes a change for you to approve, and replays exactly the failing cases to prove the fix worked. A Claude skill and setup for other editors are in [`integrations/`](integrations/).
|
|
59
|
+
|
|
60
|
+
### You have agent logs → audit them
|
|
61
|
+
|
|
62
|
+
No setup, any framework, any model. Point it at your logs (JSON Lines of tool calls) and let halflife find the rules your agent follows:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
halflife audit agent_logs.jsonl --suggest-rules
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It mines patterns from your own runs (a lookup before every action, one notification per claim, every listed item handled), writes them to `rules_suggested.py` with how often each held, and audits against them. Rules that held in every run guard future runs. Rules that broke in a few runs point straight at the runs that went wrong. The file describes what your agent *usually* does, so read it, delete anything that's coincidence, and keep what's policy.
|
|
69
|
+
|
|
70
|
+
Or write the rules yourself:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from halflife.rules import at_most_once, covers, eventually, requires
|
|
74
|
+
|
|
75
|
+
RULES = [
|
|
76
|
+
covers("list_claims", field="claims", actions=["finalize_claim", "flag_for_review"], key="claim_id"),
|
|
77
|
+
requires("finalize_claim", before="get_claim", match=["claim_id"]),
|
|
78
|
+
eventually("finalize_claim", then="notify_member", match=["claim_id"]),
|
|
79
|
+
at_most_once("notify_member", key=["claim_id"]),
|
|
80
|
+
]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
halflife audit agent_logs.jsonl --rules rules.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You get the half-life measured in tool calls, which rules break and how often, and where. No answer key needed. Try it on the sample: `halflife audit examples/traces/claims.jsonl --suggest-rules`.
|
|
88
|
+
|
|
89
|
+
### You have agent code → benchmark it
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from halflife import benchmark
|
|
93
|
+
|
|
94
|
+
@benchmark(suite="claims-intake")
|
|
95
|
+
def my_agent(task):
|
|
96
|
+
return agent.run(task)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
halflife run my_agent.py --quick
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Your agent runs against a realistic simulated system with a hidden answer key, on tasks from 5 to 40 steps, several times each. Or scaffold everything with `halflife init`.
|
|
104
|
+
|
|
105
|
+
### You want to compare models → bench them
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
halflife bench --model ollama:qwen2.5:7b --model anthropic:claude-sonnet-5-5 --quick
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Built-in agents for Ollama, LM Studio, vLLM, OpenAI, OpenRouter, Together, Groq, Anthropic, and any OpenAI-compatible server. No code.
|
|
112
|
+
|
|
113
|
+
## Every run tells you what to do next
|
|
114
|
+
|
|
115
|
+
<p align="center"><img src="docs/curve.png" width="760" alt="Success rate falls with task length; fitted curve crosses 50% at 32.7 steps"></p>
|
|
116
|
+
|
|
117
|
+
- **Half-life with a 95% interval.** Only lengths you actually tested count: if you tested up to 20 steps, it says "≥ 20", never a made-up number.
|
|
118
|
+
- **Decay shape.** Do errors compound as tasks grow, or happen early? The fix differs.
|
|
119
|
+
- **Where runs broke.** The first failure in each run, grouped: lost track of state, skipped a step, acted without a check, looped, broke a rule.
|
|
120
|
+
- **What to fix first.** Ranked changes, structural fixes before prompt tweaks, each with a failing run as evidence.
|
|
121
|
+
- **Should it act on its own?** `--workflow-length 26` predicts success at your real workflow length and recommends act autonomously, act with approval, suggest only, or shadow mode.
|
|
122
|
+
- **Change since last run.** Every run compares itself with the previous one: `half-life 32.7 → 28.1 steps (−14%, worse)`.
|
|
123
|
+
- **An eval pack.** The exact failing tasks, saved. After a fix: `halflife run my_agent.py --replay .halflife/<run>.evals.json`.
|
|
124
|
+
- **An HTML report** to share, and JSON for your pipeline.
|
|
125
|
+
|
|
126
|
+
## Make it a habit
|
|
127
|
+
|
|
128
|
+
In your tests:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import halflife
|
|
132
|
+
|
|
133
|
+
def test_agent_is_reliable():
|
|
134
|
+
halflife.assert_reliable(my_agent, min_half_life=15)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
In CI (`halflife init --ci` writes the workflow):
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
halflife run my_agent.py --min-half-life 20
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In your README: `halflife badge` writes 
|
|
144
|
+
|
|
145
|
+
## Plug in any agent
|
|
146
|
+
|
|
147
|
+
Your function gets a `task`:
|
|
148
|
+
|
|
149
|
+
| | |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `task.instructions` | the goal and the policy |
|
|
152
|
+
| `task.call(name, args)` | run a tool; errors come back as `{"error": ...}` so your agent can recover |
|
|
153
|
+
| `task.openai_tools()` | tool definitions for OpenAI-compatible APIs (Ollama, vLLM, OpenRouter…) |
|
|
154
|
+
| `task.anthropic_tools()` | tool definitions for the Anthropic API |
|
|
155
|
+
| `task.max_tool_calls` | the budget; going over ends the run, which catches loops |
|
|
156
|
+
|
|
157
|
+
Full examples: [`examples/claude_agent.py`](examples/claude_agent.py), [`examples/openai_agent.py`](examples/openai_agent.py).
|
|
158
|
+
|
|
159
|
+
<details>
|
|
160
|
+
<summary><strong>Providers for <code>halflife bench</code></strong></summary>
|
|
161
|
+
|
|
162
|
+
| Provider | Key | Notes |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `ollama` | none | local; set `OLLAMA_HOST` if not localhost |
|
|
165
|
+
| `lmstudio`, `vllm` | none | local; `--base-url` if not the default port |
|
|
166
|
+
| `compatible` | `HALFLIFE_API_KEY` (optional) | any OpenAI-compatible server, with `--base-url` |
|
|
167
|
+
| `openai` | `OPENAI_API_KEY` | |
|
|
168
|
+
| `openrouter` | `OPENROUTER_API_KEY` | one key for most open and closed models |
|
|
169
|
+
| `together`, `groq` | `TOGETHER_API_KEY`, `GROQ_API_KEY` | |
|
|
170
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | prompt caching on by default |
|
|
171
|
+
|
|
172
|
+
**Ollama:** start it with a large context window, or long tasks fail for the wrong reason, because the default window silently drops older messages:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
OLLAMA_CONTEXT_LENGTH=32768 ollama serve
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
</details>
|
|
179
|
+
|
|
180
|
+
## Rules
|
|
181
|
+
|
|
182
|
+
Invariants checked on every run's tool calls. They need no answer key, so they work on any agent and on production logs.
|
|
183
|
+
|
|
184
|
+
| Rule | Fails when |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `requires(A, before=B, match=[...])` | A happens without an earlier matching B |
|
|
187
|
+
| `at_most_once(A, key=[...])` | A happens twice for the same key |
|
|
188
|
+
| `eventually(A, then=B, match=[...])` | A is never followed by a matching B |
|
|
189
|
+
| `covers(S, field, actions, key)` | an item listed by S never reaches one of the actions |
|
|
190
|
+
| `never(A, when=...)` | A happens with forbidden arguments |
|
|
191
|
+
| `no_loops(n)` | the same call repeats more than n times in a row |
|
|
192
|
+
| `recovers_from_errors()` | the agent resends a call that just failed |
|
|
193
|
+
| `@rule` | anything you can write as a function of the steps |
|
|
194
|
+
|
|
195
|
+
Use them in audits, or add them to benchmarks: `halflife run my_agent.py --rules rules.py`.
|
|
196
|
+
|
|
197
|
+
## Built-in suites
|
|
198
|
+
|
|
199
|
+
| Suite | Domain | What gets harder with length |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `claims-intake` | Health plan claims: eligibility, network, coverage, prior auth, deductibles | Members recur across claims; the deductible shown is from the start of the batch, so state must be carried |
|
|
202
|
+
| `invoice-matching` | Accounts payable: blocked vendors, duplicates, price and quantity checks | Duplicates repeat earlier invoices; receipts are consumed by earlier approvals |
|
|
203
|
+
|
|
204
|
+
Every suite ships with a test proving that an agent following the written policy exactly scores 100%, so a failure is the agent's, not the answer key's. Write your own suite in about 40 lines: [`examples/custom_suite.py`](examples/custom_suite.py). `halflife show --suite claims-intake --length 10 --answers` prints exactly what an agent sees, plus the answer key.
|
|
205
|
+
|
|
206
|
+
## How the number works
|
|
207
|
+
|
|
208
|
+
Success at length L is modeled as `2^(−(L/h)^k)`. h (half-life) and k (decay shape) are fit by maximum likelihood over every run, with a bootstrap 95% interval. The observed 50% crossing is reported alongside as a model-free check. Full definitions, file formats, and reporting rules are in [SPEC.md](SPEC.md), so any tool can produce comparable numbers.
|
|
209
|
+
|
|
210
|
+
**Related work.** Measuring capability by the length of task completed half the time builds on METR's time-horizon research on frontier models. halflife applies the idea to your own agent on your own workflows, explains why runs fail, and makes it a daily development check.
|
|
211
|
+
|
|
212
|
+
## Commands
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
halflife run FILE[:function] [--quick] [--suite S] [--lengths 5,10,20,40] [--tasks N] [--runs N] [--rules FILE] [--workflow-length W] [--min-half-life H] [--replay PACK] [--save-traces FILE] [--workers N] [--out DIR]
|
|
216
|
+
halflife bench --model PROVIDER:MODEL [--model ...] [same options]
|
|
217
|
+
halflife audit TRACES.jsonl [--rules FILE] [--workflow-length W] [--min-half-life H]
|
|
218
|
+
halflife init [--suite S] [--ci]
|
|
219
|
+
halflife badge [RESULT.json]
|
|
220
|
+
halflife compare RESULT.json [RESULT.json ...]
|
|
221
|
+
halflife show [--suite S] [--length N] [--answers] [--json]
|
|
222
|
+
halflife mcp
|
|
223
|
+
halflife demo [--suite S] [--quick]
|
|
224
|
+
halflife suites
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Roadmap
|
|
228
|
+
|
|
229
|
+
- Generate test environments automatically from an MCP server or OpenAPI spec, so any agent gets a benchmark with no hand-written suite
|
|
230
|
+
- Read OpenTelemetry GenAI traces directly in `halflife audit`
|
|
231
|
+
- Adapters for popular agent frameworks
|
|
232
|
+
- TypeScript SDK
|
|
233
|
+
- More suites: support, IT ops, procurement, coding
|
|
234
|
+
|
|
235
|
+
Ideas and suites welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
Apache 2.0
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# halflife
|
|
2
|
+
|
|
3
|
+
**Your agent works on 5-step tasks. Does it work on 50?**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/ev-prawda92/halflife/actions/workflows/tests.yml)
|
|
6
|
+
[](https://pypi.org/project/halflife-ai/)
|
|
7
|
+
[](pyproject.toml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
halflife measures your agent's **reliability half-life**: the task length at which it succeeds half the time. Then it tells you where runs broke, what to change, and re-runs exactly the cases that failed once you've changed it.
|
|
11
|
+
|
|
12
|
+
Agents fail quietly as tasks get longer: they lose a running total from step 4 by step 30, skip the last item, pay a duplicate they saw twenty steps ago. Unit tests and single-turn evals don't catch it. halflife does, in your terminal, your tests, your CI, and your coding agent.
|
|
13
|
+
|
|
14
|
+
<p align="center"><img src="docs/terminal.png" width="760" alt="halflife terminal output: success by length, half-life 32.7 steps, failure modes, and fixes"></p>
|
|
15
|
+
|
|
16
|
+
> The numbers above come from `halflife demo`, a simulated agent built to show the tool. Run it on your own agent for real numbers.
|
|
17
|
+
|
|
18
|
+
## Start in one minute
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
pip install halflife-ai
|
|
22
|
+
halflife demo
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then pick the way in that fits what you have.
|
|
26
|
+
|
|
27
|
+
### You build with Claude Code, Cursor or Codex → let your coding agent use it
|
|
28
|
+
|
|
29
|
+
Most agents today are built with a coding agent. Give it halflife once and it checks every agent it builds for you:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
claude mcp add halflife -- halflife mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then ask it in plain words: *"check how reliable my agent is on long tasks and fix what breaks."* It runs the benchmark or audits your logs, reads where runs broke and why, proposes a change for you to approve, and replays exactly the failing cases to prove the fix worked. A Claude skill and setup for other editors are in [`integrations/`](integrations/).
|
|
36
|
+
|
|
37
|
+
### You have agent logs → audit them
|
|
38
|
+
|
|
39
|
+
No setup, any framework, any model. Point it at your logs (JSON Lines of tool calls) and let halflife find the rules your agent follows:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
halflife audit agent_logs.jsonl --suggest-rules
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It mines patterns from your own runs (a lookup before every action, one notification per claim, every listed item handled), writes them to `rules_suggested.py` with how often each held, and audits against them. Rules that held in every run guard future runs. Rules that broke in a few runs point straight at the runs that went wrong. The file describes what your agent *usually* does, so read it, delete anything that's coincidence, and keep what's policy.
|
|
46
|
+
|
|
47
|
+
Or write the rules yourself:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from halflife.rules import at_most_once, covers, eventually, requires
|
|
51
|
+
|
|
52
|
+
RULES = [
|
|
53
|
+
covers("list_claims", field="claims", actions=["finalize_claim", "flag_for_review"], key="claim_id"),
|
|
54
|
+
requires("finalize_claim", before="get_claim", match=["claim_id"]),
|
|
55
|
+
eventually("finalize_claim", then="notify_member", match=["claim_id"]),
|
|
56
|
+
at_most_once("notify_member", key=["claim_id"]),
|
|
57
|
+
]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
halflife audit agent_logs.jsonl --rules rules.py
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You get the half-life measured in tool calls, which rules break and how often, and where. No answer key needed. Try it on the sample: `halflife audit examples/traces/claims.jsonl --suggest-rules`.
|
|
65
|
+
|
|
66
|
+
### You have agent code → benchmark it
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from halflife import benchmark
|
|
70
|
+
|
|
71
|
+
@benchmark(suite="claims-intake")
|
|
72
|
+
def my_agent(task):
|
|
73
|
+
return agent.run(task)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
halflife run my_agent.py --quick
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Your agent runs against a realistic simulated system with a hidden answer key, on tasks from 5 to 40 steps, several times each. Or scaffold everything with `halflife init`.
|
|
81
|
+
|
|
82
|
+
### You want to compare models → bench them
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
halflife bench --model ollama:qwen2.5:7b --model anthropic:claude-sonnet-5-5 --quick
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Built-in agents for Ollama, LM Studio, vLLM, OpenAI, OpenRouter, Together, Groq, Anthropic, and any OpenAI-compatible server. No code.
|
|
89
|
+
|
|
90
|
+
## Every run tells you what to do next
|
|
91
|
+
|
|
92
|
+
<p align="center"><img src="docs/curve.png" width="760" alt="Success rate falls with task length; fitted curve crosses 50% at 32.7 steps"></p>
|
|
93
|
+
|
|
94
|
+
- **Half-life with a 95% interval.** Only lengths you actually tested count: if you tested up to 20 steps, it says "≥ 20", never a made-up number.
|
|
95
|
+
- **Decay shape.** Do errors compound as tasks grow, or happen early? The fix differs.
|
|
96
|
+
- **Where runs broke.** The first failure in each run, grouped: lost track of state, skipped a step, acted without a check, looped, broke a rule.
|
|
97
|
+
- **What to fix first.** Ranked changes, structural fixes before prompt tweaks, each with a failing run as evidence.
|
|
98
|
+
- **Should it act on its own?** `--workflow-length 26` predicts success at your real workflow length and recommends act autonomously, act with approval, suggest only, or shadow mode.
|
|
99
|
+
- **Change since last run.** Every run compares itself with the previous one: `half-life 32.7 → 28.1 steps (−14%, worse)`.
|
|
100
|
+
- **An eval pack.** The exact failing tasks, saved. After a fix: `halflife run my_agent.py --replay .halflife/<run>.evals.json`.
|
|
101
|
+
- **An HTML report** to share, and JSON for your pipeline.
|
|
102
|
+
|
|
103
|
+
## Make it a habit
|
|
104
|
+
|
|
105
|
+
In your tests:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import halflife
|
|
109
|
+
|
|
110
|
+
def test_agent_is_reliable():
|
|
111
|
+
halflife.assert_reliable(my_agent, min_half_life=15)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
In CI (`halflife init --ci` writes the workflow):
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
halflife run my_agent.py --min-half-life 20
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
In your README: `halflife badge` writes 
|
|
121
|
+
|
|
122
|
+
## Plug in any agent
|
|
123
|
+
|
|
124
|
+
Your function gets a `task`:
|
|
125
|
+
|
|
126
|
+
| | |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `task.instructions` | the goal and the policy |
|
|
129
|
+
| `task.call(name, args)` | run a tool; errors come back as `{"error": ...}` so your agent can recover |
|
|
130
|
+
| `task.openai_tools()` | tool definitions for OpenAI-compatible APIs (Ollama, vLLM, OpenRouter…) |
|
|
131
|
+
| `task.anthropic_tools()` | tool definitions for the Anthropic API |
|
|
132
|
+
| `task.max_tool_calls` | the budget; going over ends the run, which catches loops |
|
|
133
|
+
|
|
134
|
+
Full examples: [`examples/claude_agent.py`](examples/claude_agent.py), [`examples/openai_agent.py`](examples/openai_agent.py).
|
|
135
|
+
|
|
136
|
+
<details>
|
|
137
|
+
<summary><strong>Providers for <code>halflife bench</code></strong></summary>
|
|
138
|
+
|
|
139
|
+
| Provider | Key | Notes |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `ollama` | none | local; set `OLLAMA_HOST` if not localhost |
|
|
142
|
+
| `lmstudio`, `vllm` | none | local; `--base-url` if not the default port |
|
|
143
|
+
| `compatible` | `HALFLIFE_API_KEY` (optional) | any OpenAI-compatible server, with `--base-url` |
|
|
144
|
+
| `openai` | `OPENAI_API_KEY` | |
|
|
145
|
+
| `openrouter` | `OPENROUTER_API_KEY` | one key for most open and closed models |
|
|
146
|
+
| `together`, `groq` | `TOGETHER_API_KEY`, `GROQ_API_KEY` | |
|
|
147
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | prompt caching on by default |
|
|
148
|
+
|
|
149
|
+
**Ollama:** start it with a large context window, or long tasks fail for the wrong reason, because the default window silently drops older messages:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
OLLAMA_CONTEXT_LENGTH=32768 ollama serve
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
</details>
|
|
156
|
+
|
|
157
|
+
## Rules
|
|
158
|
+
|
|
159
|
+
Invariants checked on every run's tool calls. They need no answer key, so they work on any agent and on production logs.
|
|
160
|
+
|
|
161
|
+
| Rule | Fails when |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `requires(A, before=B, match=[...])` | A happens without an earlier matching B |
|
|
164
|
+
| `at_most_once(A, key=[...])` | A happens twice for the same key |
|
|
165
|
+
| `eventually(A, then=B, match=[...])` | A is never followed by a matching B |
|
|
166
|
+
| `covers(S, field, actions, key)` | an item listed by S never reaches one of the actions |
|
|
167
|
+
| `never(A, when=...)` | A happens with forbidden arguments |
|
|
168
|
+
| `no_loops(n)` | the same call repeats more than n times in a row |
|
|
169
|
+
| `recovers_from_errors()` | the agent resends a call that just failed |
|
|
170
|
+
| `@rule` | anything you can write as a function of the steps |
|
|
171
|
+
|
|
172
|
+
Use them in audits, or add them to benchmarks: `halflife run my_agent.py --rules rules.py`.
|
|
173
|
+
|
|
174
|
+
## Built-in suites
|
|
175
|
+
|
|
176
|
+
| Suite | Domain | What gets harder with length |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `claims-intake` | Health plan claims: eligibility, network, coverage, prior auth, deductibles | Members recur across claims; the deductible shown is from the start of the batch, so state must be carried |
|
|
179
|
+
| `invoice-matching` | Accounts payable: blocked vendors, duplicates, price and quantity checks | Duplicates repeat earlier invoices; receipts are consumed by earlier approvals |
|
|
180
|
+
|
|
181
|
+
Every suite ships with a test proving that an agent following the written policy exactly scores 100%, so a failure is the agent's, not the answer key's. Write your own suite in about 40 lines: [`examples/custom_suite.py`](examples/custom_suite.py). `halflife show --suite claims-intake --length 10 --answers` prints exactly what an agent sees, plus the answer key.
|
|
182
|
+
|
|
183
|
+
## How the number works
|
|
184
|
+
|
|
185
|
+
Success at length L is modeled as `2^(−(L/h)^k)`. h (half-life) and k (decay shape) are fit by maximum likelihood over every run, with a bootstrap 95% interval. The observed 50% crossing is reported alongside as a model-free check. Full definitions, file formats, and reporting rules are in [SPEC.md](SPEC.md), so any tool can produce comparable numbers.
|
|
186
|
+
|
|
187
|
+
**Related work.** Measuring capability by the length of task completed half the time builds on METR's time-horizon research on frontier models. halflife applies the idea to your own agent on your own workflows, explains why runs fail, and makes it a daily development check.
|
|
188
|
+
|
|
189
|
+
## Commands
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
halflife run FILE[:function] [--quick] [--suite S] [--lengths 5,10,20,40] [--tasks N] [--runs N] [--rules FILE] [--workflow-length W] [--min-half-life H] [--replay PACK] [--save-traces FILE] [--workers N] [--out DIR]
|
|
193
|
+
halflife bench --model PROVIDER:MODEL [--model ...] [same options]
|
|
194
|
+
halflife audit TRACES.jsonl [--rules FILE] [--workflow-length W] [--min-half-life H]
|
|
195
|
+
halflife init [--suite S] [--ci]
|
|
196
|
+
halflife badge [RESULT.json]
|
|
197
|
+
halflife compare RESULT.json [RESULT.json ...]
|
|
198
|
+
halflife show [--suite S] [--length N] [--answers] [--json]
|
|
199
|
+
halflife mcp
|
|
200
|
+
halflife demo [--suite S] [--quick]
|
|
201
|
+
halflife suites
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Roadmap
|
|
205
|
+
|
|
206
|
+
- Generate test environments automatically from an MCP server or OpenAPI spec, so any agent gets a benchmark with no hand-written suite
|
|
207
|
+
- Read OpenTelemetry GenAI traces directly in `halflife audit`
|
|
208
|
+
- Adapters for popular agent frameworks
|
|
209
|
+
- TypeScript SDK
|
|
210
|
+
- More suites: support, IT ops, procurement, coding
|
|
211
|
+
|
|
212
|
+
Ideas and suites welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
Apache 2.0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "halflife-ai"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Measure how long a task you can trust an AI agent with."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Evan Prawda", email = "evanprawda92@gmail.com" }]
|
|
13
|
+
keywords = ["ai", "agents", "evaluation", "benchmark", "reliability", "llm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
18
|
+
"Topic :: Software Development :: Testing",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
anthropic = ["anthropic>=0.40"]
|
|
24
|
+
openai = ["openai>=1.40"]
|
|
25
|
+
dev = ["pytest>=7"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
halflife = "halflife.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/ev-prawda92/halflife"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
pythonpath = ["tests"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""halflife: measure how long a task you can trust an AI agent with.
|
|
2
|
+
|
|
3
|
+
from halflife import benchmark
|
|
4
|
+
|
|
5
|
+
@benchmark(suite="claims-intake")
|
|
6
|
+
def my_agent(task):
|
|
7
|
+
return agent.run(task)
|
|
8
|
+
|
|
9
|
+
Then run `halflife run my_agent.py`, or `my_agent.evaluate()` from Python.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.3.0"
|
|
13
|
+
|
|
14
|
+
from .decorator import assert_reliable, benchmark
|
|
15
|
+
from .report import Report
|
|
16
|
+
from .runner import build_task, evaluate, grade
|
|
17
|
+
from .sandbox import Sandbox, ToolBudgetExceeded, ToolError, param, tool
|
|
18
|
+
from .suite import get_suite, list_suites, suite
|
|
19
|
+
from .task import Checkpoint, Constraint, Task, TaskSpec
|
|
20
|
+
from .traces import TraceWriter, audit
|
|
21
|
+
from . import rules
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"benchmark",
|
|
25
|
+
"assert_reliable",
|
|
26
|
+
"evaluate",
|
|
27
|
+
"Report",
|
|
28
|
+
"Task",
|
|
29
|
+
"TaskSpec",
|
|
30
|
+
"Checkpoint",
|
|
31
|
+
"Constraint",
|
|
32
|
+
"Sandbox",
|
|
33
|
+
"tool",
|
|
34
|
+
"param",
|
|
35
|
+
"ToolError",
|
|
36
|
+
"ToolBudgetExceeded",
|
|
37
|
+
"suite",
|
|
38
|
+
"get_suite",
|
|
39
|
+
"list_suites",
|
|
40
|
+
"build_task",
|
|
41
|
+
"grade",
|
|
42
|
+
"rules",
|
|
43
|
+
"audit",
|
|
44
|
+
"TraceWriter",
|
|
45
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Built-in agents for real models, so you can benchmark a model with no code.
|
|
2
|
+
|
|
3
|
+
halflife bench --model ollama:qwen2.5:7b
|
|
4
|
+
halflife bench --model anthropic:claude-sonnet-5-5
|
|
5
|
+
halflife bench --model openai:gpt-4.1
|
|
6
|
+
halflife bench --model openrouter:meta-llama/llama-3.3-70b-instruct
|
|
7
|
+
halflife bench --model vllm:my-model --base-url http://gpu-box:8000/v1
|
|
8
|
+
|
|
9
|
+
Providers and where they read their key:
|
|
10
|
+
ollama local, no key OLLAMA_HOST (default http://localhost:11434)
|
|
11
|
+
lmstudio local, no key http://localhost:1234/v1
|
|
12
|
+
vllm local, no key http://localhost:8000/v1
|
|
13
|
+
openai OPENAI_API_KEY
|
|
14
|
+
openrouter OPENROUTER_API_KEY
|
|
15
|
+
together TOGETHER_API_KEY
|
|
16
|
+
groq GROQ_API_KEY
|
|
17
|
+
anthropic ANTHROPIC_API_KEY
|
|
18
|
+
compatible any OpenAI-compatible server; pass --base-url (key from HALFLIFE_API_KEY)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from ._http import ModelAPIError
|
|
27
|
+
from .anthropic_api import AnthropicAgent
|
|
28
|
+
from .openai_compat import OpenAICompatibleAgent
|
|
29
|
+
|
|
30
|
+
PROVIDERS = {
|
|
31
|
+
"ollama": (None, None),
|
|
32
|
+
"lmstudio": ("http://localhost:1234/v1", None),
|
|
33
|
+
"vllm": ("http://localhost:8000/v1", None),
|
|
34
|
+
"openai": ("https://api.openai.com/v1", "OPENAI_API_KEY"),
|
|
35
|
+
"openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
|
|
36
|
+
"together": ("https://api.together.xyz/v1", "TOGETHER_API_KEY"),
|
|
37
|
+
"groq": ("https://api.groq.com/openai/v1", "GROQ_API_KEY"),
|
|
38
|
+
"compatible": (None, "HALFLIFE_API_KEY"),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def from_spec(spec: str, base_url: Optional[str] = None, temperature: float = 0.0):
|
|
43
|
+
"""Build an agent from 'provider:model', e.g. 'ollama:qwen2.5:7b'."""
|
|
44
|
+
provider, sep, model = spec.partition(":")
|
|
45
|
+
if not sep or not model:
|
|
46
|
+
raise ValueError(f"model must look like provider:model, e.g. ollama:qwen2.5:7b (got '{spec}')")
|
|
47
|
+
name = spec
|
|
48
|
+
if provider == "anthropic":
|
|
49
|
+
return AnthropicAgent(model, base_url=base_url or "https://api.anthropic.com", name=name)
|
|
50
|
+
if provider not in PROVIDERS:
|
|
51
|
+
raise ValueError(f"unknown provider '{provider}'. Known: anthropic, {', '.join(PROVIDERS)}")
|
|
52
|
+
default_url, key_var = PROVIDERS[provider]
|
|
53
|
+
if provider == "ollama":
|
|
54
|
+
default_url = os.environ.get("OLLAMA_HOST", "http://localhost:11434").rstrip("/") + "/v1"
|
|
55
|
+
if not default_url.startswith("http"):
|
|
56
|
+
default_url = "http://" + default_url
|
|
57
|
+
url = base_url or default_url
|
|
58
|
+
if not url:
|
|
59
|
+
raise ValueError(f"provider '{provider}' needs --base-url")
|
|
60
|
+
key = os.environ.get(key_var) if key_var else None
|
|
61
|
+
if key_var and not key and provider not in ("compatible",):
|
|
62
|
+
raise ModelAPIError(f"{key_var} is not set")
|
|
63
|
+
return OpenAICompatibleAgent(model, url, api_key=key, temperature=temperature, name=name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def preflight(agent, timeout: float = 4.0) -> Optional[str]:
|
|
67
|
+
"""Return a plain-English problem if the model server can't be reached, else None."""
|
|
68
|
+
import urllib.error
|
|
69
|
+
import urllib.request
|
|
70
|
+
|
|
71
|
+
if isinstance(agent, AnthropicAgent):
|
|
72
|
+
return None if agent.api_key else "ANTHROPIC_API_KEY is not set"
|
|
73
|
+
url = agent.base_url.rstrip("/") + "/models"
|
|
74
|
+
headers = {"authorization": f"Bearer {agent.api_key}"} if agent.api_key else {}
|
|
75
|
+
try:
|
|
76
|
+
urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=timeout)
|
|
77
|
+
return None
|
|
78
|
+
except urllib.error.HTTPError as exc:
|
|
79
|
+
if exc.code in (401, 403):
|
|
80
|
+
return f"the server at {agent.base_url} rejected the API key ({exc.code})"
|
|
81
|
+
return None
|
|
82
|
+
except (urllib.error.URLError, OSError):
|
|
83
|
+
host = agent.base_url.split("/v1")[0]
|
|
84
|
+
if "11434" in host:
|
|
85
|
+
return (
|
|
86
|
+
f"can't reach Ollama at {host}. Is it installed and running? Start it with:\n"
|
|
87
|
+
" OLLAMA_CONTEXT_LENGTH=32768 ollama serve\n"
|
|
88
|
+
" and pull the model first, e.g.: ollama pull qwen2.5:7b"
|
|
89
|
+
)
|
|
90
|
+
return f"can't reach the model server at {host}. Is it running?"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = ["preflight", "AnthropicAgent", "OpenAICompatibleAgent", "ModelAPIError", "from_spec", "PROVIDERS"]
|