agentpause 0.2.1__tar.gz → 0.4.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.
Files changed (67) hide show
  1. agentpause-0.4.0/PKG-INFO +468 -0
  2. agentpause-0.4.0/README.md +445 -0
  3. {agentpause-0.2.1 → agentpause-0.4.0}/pyproject.toml +1 -1
  4. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/__init__.py +19 -1
  5. agentpause-0.4.0/src/agentpause/adapters/anthropic.py +118 -0
  6. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/adapters/langgraph.py +11 -2
  7. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/adapters/litellm.py +18 -2
  8. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/adapters/openai_compat.py +125 -16
  9. agentpause-0.4.0/src/agentpause/attention.py +196 -0
  10. agentpause-0.4.0/src/agentpause/coordinator.py +185 -0
  11. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/errors.py +15 -0
  12. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/estimator.py +65 -7
  13. agentpause-0.4.0/src/agentpause/forecast.py +183 -0
  14. agentpause-0.4.0/src/agentpause/llamacpp_kv.py +403 -0
  15. agentpause-0.4.0/src/agentpause/regression.py +235 -0
  16. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/risk.py +46 -9
  17. agentpause-0.4.0/src/agentpause/router.py +185 -0
  18. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/scheduler.py +117 -6
  19. agentpause-0.4.0/src/agentpause/state.py +281 -0
  20. agentpause-0.4.0/src/agentpause/tools.py +77 -0
  21. agentpause-0.4.0/src/agentpause.egg-info/PKG-INFO +468 -0
  22. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause.egg-info/SOURCES.txt +21 -0
  23. agentpause-0.4.0/tests/test_anthropic_adapter.py +118 -0
  24. agentpause-0.4.0/tests/test_attention.py +132 -0
  25. agentpause-0.4.0/tests/test_coordinator.py +121 -0
  26. agentpause-0.4.0/tests/test_estimator_evolution.py +78 -0
  27. agentpause-0.4.0/tests/test_field_adoptions.py +96 -0
  28. agentpause-0.4.0/tests/test_forecast.py +130 -0
  29. agentpause-0.4.0/tests/test_fork_migrate.py +210 -0
  30. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_io_tpm.py +1 -1
  31. agentpause-0.4.0/tests/test_latency_budget.py +77 -0
  32. agentpause-0.4.0/tests/test_llamacpp_kv.py +278 -0
  33. agentpause-0.4.0/tests/test_openai_compat.py +232 -0
  34. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_quantile.py +19 -0
  35. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_refill_wait.py +11 -2
  36. agentpause-0.4.0/tests/test_regression.py +109 -0
  37. agentpause-0.4.0/tests/test_router.py +140 -0
  38. agentpause-0.4.0/tests/test_smart_context.py +109 -0
  39. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_state.py +26 -0
  40. agentpause-0.4.0/tests/test_tool_quota.py +74 -0
  41. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_wait_and_rpm.py +3 -3
  42. agentpause-0.2.1/PKG-INFO +0 -228
  43. agentpause-0.2.1/README.md +0 -205
  44. agentpause-0.2.1/src/agentpause/state.py +0 -99
  45. agentpause-0.2.1/src/agentpause.egg-info/PKG-INFO +0 -228
  46. agentpause-0.2.1/tests/test_openai_compat.py +0 -98
  47. {agentpause-0.2.1 → agentpause-0.4.0}/LICENSE +0 -0
  48. {agentpause-0.2.1 → agentpause-0.4.0}/setup.cfg +0 -0
  49. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/adapters/__init__.py +0 -0
  50. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/breaker.py +0 -0
  51. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/fallback.py +0 -0
  52. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/refill.py +0 -0
  53. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause/retry.py +0 -0
  54. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause.egg-info/dependency_links.txt +0 -0
  55. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause.egg-info/requires.txt +0 -0
  56. {agentpause-0.2.1 → agentpause-0.4.0}/src/agentpause.egg-info/top_level.txt +0 -0
  57. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_async.py +0 -0
  58. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_breaker.py +0 -0
  59. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_estimator.py +0 -0
  60. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_fallback.py +0 -0
  61. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_langgraph_adapter.py +0 -0
  62. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_litellm_adapter.py +0 -0
  63. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_money_and_hooks.py +0 -0
  64. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_regime.py +0 -0
  65. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_risk.py +0 -0
  66. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_robustness.py +0 -0
  67. {agentpause-0.2.1 → agentpause-0.4.0}/tests/test_scheduler.py +0 -0
@@ -0,0 +1,468 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentpause
3
+ Version: 0.4.0
4
+ Summary: Predictive scheduling for autonomous LLM agents: suspend gracefully before rate limits, resume without losing work.
5
+ Author: Andrea Lelio Ciampolillo
6
+ License: MIT
7
+ Keywords: llm,agents,rate-limiting,checkpointing,scheduler
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.0; extra == "dev"
18
+ Provides-Extra: litellm
19
+ Requires-Dist: litellm>=1.40; extra == "litellm"
20
+ Provides-Extra: langgraph
21
+ Requires-Dist: langgraph>=0.2; extra == "langgraph"
22
+ Dynamic: license-file
23
+
24
+ # agentpause
25
+
26
+ **Predictive scheduling for autonomous LLM agents.** Suspend an agent gracefully
27
+ *before* it hits a provider rate limit, and resume it without redoing work.
28
+
29
+ The core works on any provider — cloud (OpenAI, Anthropic, Groq) or local — because
30
+ it only serializes application-level state. True KV-cache warm start is an optional
31
+ plugin for self-hosted runtimes (llama.cpp, vLLM).
32
+
33
+ ## Measured results (from the accompanying research)
34
+
35
+ | Experiment | Result |
36
+ |---|---|
37
+ | Simulation, 300 runs/config | reactive baseline crashes up to 100% → predictive **0%** (k=4), token waste −80% |
38
+ | Real provider (Groq free, 6K TPM) | multi-window task completed with **zero 429 errors** |
39
+ | vs LangGraph's MemorySaver (200 runs) | LangGraph: 7.8 × 429/run, 9,239 tokens wasted; predictive: **0 and 0** |
40
+ | End-to-end A/B (thinking models, 4B/8B) | recovery **54×–93× faster**; total task time −19% |
41
+
42
+ Reproduce them yourself with a free key: `python scripts/benchmark_groq.py`.
43
+ Live run (2026-07-08, Groq free tier, 12 steps, ~1k-token context,
44
+ refill-aware chunked waiting, leveled windows):
45
+
46
+ | | reactive baseline | agentpause |
47
+ |---|---|---|
48
+ | 429 errors suffered | 7 | **0** |
49
+ | steps redone | 7 | **0** |
50
+ | tokens re-sent (waste) | 12,840 | **0** |
51
+ | telemetry overhead (pings) | 0 | 259 |
52
+ | wall-clock | 82 s | 86 s |
53
+
54
+ Zero errors and zero waste at wall-clock parity: the sensor costs ~2% of
55
+ what crashes waste (259 vs 12,840 tokens) — and waste is money on paid tiers.
56
+
57
+ **Context slimming vs. answer quality** (`python scripts/quality_ab.py`,
58
+ live 2026-07-10, Groq `llama-3.1-8b-instant`): six facts planted early in a
59
+ verbose conversation, same final quiz under three histories.
60
+
61
+ | condition | prompt chars | facts recalled |
62
+ |---|---|---|
63
+ | A — full history | 8,984 | **6/6** |
64
+ | B — `compact()` (blind truncation) | 4,370 | **0/6** — and the model *invented* plausible replacements |
65
+ | C — `summarize_with()` (one summary call) | 3,608 | **6/6** |
66
+
67
+ A 2.2× larger run (`--big`, 19,145 chars — near the free tier's whole TPM
68
+ window, the physical ceiling for a single call) repeated the pattern:
69
+ A 6/6, B 0/6, C 6/6 at a third of the prompt.
70
+
71
+ The instructive failure is B's, and it is *erratic*: in one run it answered
72
+ confidently with fabricated values (fake codename, fake budget, fake city);
73
+ in the larger run it honestly declined. You cannot know in advance which
74
+ failure you get — and the hallucinating one is the dangerous one. Blind
75
+ truncation is an emergency exit (§8.6 overflow, no LLM available), not a
76
+ strategy; semantic summarization recalled everything at a fraction of the
77
+ prompt, earning its one extra call.
78
+ A 20-step live stress test (2026-07-10) closed the loop: at the §8.6 wall the
79
+ scheduler suspended, compacted the checkpoint offline, resumed slim, and
80
+ completed 20/20 steps with zero 429s.
81
+
82
+ **Facts vs. voice** (`python scripts/quality_ab.py`, live 2026-07-20,
83
+ Groq `llama-3.1-8b-instant` — the voice probe rides along on the same run,
84
+ no extra flag needed): does shrinking a checkpoint also flatten the
85
+ *style* it was written in, not just the facts inside it? A persona with two
86
+ short verbal tics is planted in the same conversation as the six facts above:
87
+
88
+ | condition | facts recalled | voice tics preserved |
89
+ |---|---|---|
90
+ | A — full history | **6/6** | 1/2 |
91
+ | B — `compact()` (blind truncation) | 0/6 | 1/2 |
92
+ | C — `summarize_with()` (one summary call) | **6/6** | **0/2** |
93
+
94
+ Truncation and summarization fail in opposite, complementary ways: blind
95
+ truncation keeps short, verbatim phrases intact (only long messages get cut)
96
+ but loses facts scattered earlier in the history; summarization recovers the
97
+ facts but rewrites everything in its own voice, so a persona's way of talking
98
+ does not survive being summarized even when what it *said* does. If tone and
99
+ persona consistency matter for your agent, `compact()` and `summarize_with()`
100
+ are not interchangeable — pick per what you can't afford to lose.
101
+
102
+ > Status: **0.4.0**. Core scheduler, direct + LiteLLM + Anthropic adapters,
103
+ > LangGraph integration, multi-provider routing, multi-agent shared budgets,
104
+ > feature-based cost/latency estimation, task-completion forecast, checkpoint
105
+ > fork & cross-machine migration, an optional true-warm-start KV-cache plugin
106
+ > for llama.cpp, human attention as a budget, runnable examples, and a full
107
+ > test suite are all in place.
108
+
109
+ ## Install
110
+
111
+ ```bash
112
+ pip install agentpause
113
+ ```
114
+
115
+ Optional extras, only if you use them (the core has zero dependencies):
116
+
117
+ ```bash
118
+ pip install "agentpause[litellm]" # LiteLLM adapter (100+ providers)
119
+ pip install "agentpause[langgraph]" # LangGraph adapter
120
+ ```
121
+
122
+ Developing on the library itself (clone + editable install + tests):
123
+
124
+ ```bash
125
+ git clone https://github.com/Champoleello/agentpause
126
+ cd agentpause
127
+ pip install -e ".[dev]"
128
+ pytest
129
+ ```
130
+
131
+ ## Getting started
132
+
133
+ **Step 1 — the smallest possible loop.** `backend` is any callable
134
+ `messages -> (reply, tokens_used)`; `telemetry` is any callable
135
+ `() -> remaining_tokens`. That's the entire contract — no framework required:
136
+
137
+ ```python
138
+ from agentpause import PredictiveScheduler
139
+
140
+ sched = PredictiveScheduler(backend=my_llm_call, telemetry=my_rate_limit_reader)
141
+
142
+ with sched.session("task-1") as s: # resumes automatically if interrupted before
143
+ for question in questions[s.step:]: # skip steps finished before a suspend
144
+ s.add_user(question)
145
+ if s.should_suspend(): # predictive check, *before* the call
146
+ s.checkpoint()
147
+ break # stop cleanly; rerun to resume
148
+ reply = s.call()
149
+ else:
150
+ s.complete() # task done: drop the checkpoint
151
+ ```
152
+
153
+ Run it now, no API key needed: `python examples/quickstart.py` — it suspends
154
+ mid-task on the first run and resumes cleanly the second time you run it.
155
+
156
+ **Step 2 — the same three calls, wired into YOUR agent loop.** Whatever
157
+ framework you use (a hand-rolled ReAct loop, LangGraph, CrewAI, a plain
158
+ `while` loop), the integration is always the same three calls in the same
159
+ three places:
160
+
161
+ 1. Before every LLM call, ask `should_suspend()` (or `session.next_action()`
162
+ for the richer `continue`/`wait`/`checkpoint` answer). This is a local
163
+ computation — no network call, no cost.
164
+ 2. If it says stop, call `checkpoint()` and exit the process. Nothing else
165
+ to do: state, messages, and idempotency keys are already on disk.
166
+ 3. Re-running the same code with the same session id resumes automatically
167
+ from the exact step — `with sched.session(...)` handles the restore.
168
+
169
+ **Step 3 — pick the adapter for your provider**, which supplies `backend` and
170
+ `telemetry` for you instead of writing them by hand:
171
+
172
+ | You use... | Adapter | Runnable example |
173
+ |---|---|---|
174
+ | LiteLLM (100+ providers) | `adapters.litellm.LiteLLMAdapter` | `examples/litellm_groq.py` |
175
+ | Direct HTTP, OpenAI-compatible (Groq, OpenAI, ...) | `adapters.openai_compat.OpenAICompatAdapter` | `scripts/validate_provider.py` |
176
+ | Direct Anthropic Messages API | `adapters.anthropic.AnthropicAdapter` | see "Real providers" below |
177
+ | LangGraph | `adapters.langgraph.AgentPauseGuard` | `examples/langgraph_quickstart.py` |
178
+ | A CrewAI/AutoGen/custom loop, no adapter yet | none — write `backend`/`telemetry` by hand as in Step 1 | `examples/quickstart.py` |
179
+
180
+ Details and full code for each adapter are in the sections right below; the
181
+ `## Components` table further down is the complete function/class reference
182
+ for everything the library exposes.
183
+
184
+ ## Real providers via LiteLLM
185
+
186
+ The LiteLLM adapter supplies both callables for 100+ providers (OpenAI, Groq,
187
+ Anthropic, local servers, ...), reading the budget from each response's
188
+ rate-limit headers and refreshing stale readings with a tiny telemetry ping:
189
+
190
+ ```python
191
+ from agentpause import PredictiveScheduler
192
+ from agentpause.adapters.litellm import LiteLLMAdapter
193
+
194
+ adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
195
+ sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.telemetry)
196
+ ```
197
+
198
+ Install with `pip install -e ".[litellm]"`; see `examples/litellm_groq.py`.
199
+ To validate against your own provider (frontier models included):
200
+ `python scripts/validate_provider.py gpt-4o-mini`.
201
+
202
+ Rate-limit headers by provider (defaults target the OpenAI-style names):
203
+
204
+ | Provider | remaining tokens | remaining requests | reset |
205
+ |---|---|---|---|
206
+ | OpenAI | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`1s`, `6m0s`) |
207
+ | Groq | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`7.66s`, `2m59.56s`) |
208
+ | Anthropic | `anthropic-ratelimit-tokens-remaining` | `anthropic-ratelimit-requests-remaining` | RFC 3339 timestamp — set `remaining_header=` etc. |
209
+
210
+ Header names differ? Override them: `LiteLLMAdapter(model=..., remaining_header="...", requests_header="...", reset_header="...")`.
211
+
212
+ ### Self-hosted: ollama-gateway
213
+
214
+ Running Ollama locally behind
215
+ [martinobettucci/ollama-gateway](https://github.com/martinobettucci/ollama-gateway)
216
+ (a self-hosted auth + quota proxy for Ollama)? It emits the same
217
+ OpenAI/Groq-style `x-ratelimit-*` headers agentpause already reads, so it
218
+ works out of the box through the direct adapter — no litellm, no
219
+ special-casing. Confirmed live end-to-end (2026-07-20) against a real
220
+ gateway instance: `adapter.budget()` correctly reads `remaining_tokens`,
221
+ `remaining_requests` (decrementing per call), and `reset_seconds` — **the
222
+ one condition is that the API key has a quota configured** (token cap and/or
223
+ rate limit); a key with no quota set simply emits no rate-limit headers at
224
+ all, which agentpause reports as a clear `TelemetryError` rather than a
225
+ silent wrong answer:
226
+
227
+ ```python
228
+ from agentpause.adapters.openai_compat import OpenAICompatAdapter
229
+
230
+ adapter = OpenAICompatAdapter.for_model("ollama-gateway/llama3:8b")
231
+ # default base_url is http://127.0.0.1:8787/v1 (dev default); override for
232
+ # a custom port or a production HTTPS domain:
233
+ # OpenAICompatAdapter.for_model("ollama-gateway/llama3:8b",
234
+ # base_url="https://your-gateway-domain")
235
+ ```
236
+
237
+ ## Beyond tokens: RPM and wait-vs-suspend
238
+
239
+ Telemetry can be richer than a token count. `adapter.budget` reports all
240
+ three dimensions providers expose — remaining tokens (TPM), remaining
241
+ requests (RPM), and seconds until the window resets — and unlocks the
242
+ three-valued decision:
243
+
244
+ ```python
245
+ sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.budget)
246
+
247
+ d = session.next_action() # "continue" | "wait" | "checkpoint"
248
+ ```
249
+
250
+ `wait` fires when the budget does not fit **but** the window resets within
251
+ `wait_threshold_s` (default 15 s): a short in-place pause is cheaper than a
252
+ full suspend/resume cycle. Exhausted requests (RPM = 0) block the call even
253
+ with plenty of tokens left. Plain-int telemetry keeps working unchanged.
254
+
255
+ ## Async
256
+
257
+ Every entry point has an async twin — same rules, same guarantees, never
258
+ blocks the event loop:
259
+
260
+ ```python
261
+ adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
262
+ sched = PredictiveScheduler(backend=None, async_backend=adapter.abackend,
263
+ telemetry=adapter.telemetry)
264
+ reply = await session.acall() # retry/backoff via asyncio.sleep
265
+ await guard.acheck(state["messages"]) # async LangGraph nodes
266
+ ```
267
+
268
+ For sharper input estimates, wire the per-model tokenizer:
269
+ `PredictiveScheduler(..., count_tokens=adapter.count_tokens)` (falls back to
270
+ the ~4 chars/token heuristic if the tokenizer is unavailable).
271
+
272
+ ## When prediction fails anyway
273
+
274
+ Estimates are statistical — a 429 can still slip through. agentpause survives
275
+ it instead of crashing:
276
+
277
+ - **typed errors**: everything derives from `AgentPauseError`
278
+ (`RateLimitHit`, `TelemetryError`, `CheckpointError`, `BackendError`);
279
+ - **retry with backoff**: unexpected 429s are retried (provider `retry-after`
280
+ honored, else exponential backoff — see `RetryPolicy`);
281
+ - **hits are feedback**: each one bumps `safety_k` up (capped at `k_max`),
282
+ so the scheduler grows more cautious on workloads it underestimates;
283
+ - **clean failure**: a failed call leaves the session state untouched —
284
+ no phantom steps, resumes stay consistent.
285
+
286
+ ## LangGraph integration
287
+
288
+ `AgentPauseGuard` adds the predictive gate to any LangGraph node — LangGraph
289
+ persists reactively (after nodes), the guard pauses *before* the call that
290
+ would hit the rate limit, via LangGraph's own `interrupt()` + checkpointer:
291
+
292
+ ```python
293
+ from agentpause.adapters.langgraph import AgentPauseGuard
294
+
295
+ guard = AgentPauseGuard(telemetry=adapter.telemetry) # e.g. from LiteLLMAdapter
296
+
297
+ def agent_node(state):
298
+ guard.check(state["messages"]) # pauses the graph here if needed
299
+ reply = llm.invoke(state["messages"])
300
+ guard.record(state["messages"], reply.usage_metadata["total_tokens"])
301
+ ...
302
+ ```
303
+
304
+ Resume the paused thread with `graph.invoke(Command(resume=True), config)`.
305
+ On resume the guard re-reads telemetry fresh — never from the checkpoint.
306
+ Install with `pip install -e ".[langgraph]"`; see `examples/langgraph_quickstart.py`.
307
+
308
+ ## Scaling up: many providers, many agents, smarter estimates (v0.3)
309
+
310
+ The same predictive idea — read the budget first, act before the error —
311
+ extends in three directions, and they compose:
312
+
313
+ ```python
314
+ from agentpause import BudgetRouter, MultiAgentCoordinator, FeatureEstimator
315
+ from agentpause.adapters.openai_compat import OpenAICompatAdapter
316
+ from agentpause.adapters.anthropic import AnthropicAdapter
317
+
318
+ # 1. Route each call to the provider with the most headroom (predictive
319
+ # fallback: switch BEFORE the 429, not after). Providers in cooldown
320
+ # after a real 429 are skipped until their window resets.
321
+ router = BudgetRouter(
322
+ ("groq", OpenAICompatAdapter.for_model("groq/llama-3.1-8b-instant")),
323
+ ("claude", AnthropicAdapter("claude-haiku-4-5")),
324
+ )
325
+
326
+ # 2. Share ONE rate-limit window across a fleet: every granted call
327
+ # reserves its predicted cost (estimate + k·σ) from the shared pool,
328
+ # so agents can't overcommit the window together. Contention is
329
+ # arbitrated by priority, then longest-waiting.
330
+ coord = MultiAgentCoordinator(telemetry=router.budget)
331
+ coord.register("researcher", priority=1)
332
+ coord.register("summarizer")
333
+
334
+ est = FeatureEstimator() # 3. learn cost from features,
335
+ est.set_context(tool="web_search") # not just context size
336
+
337
+ d = coord.request("researcher", estimated=est.estimate(1200),
338
+ sigma=est.sigma(fallback_estimate=1200))
339
+ if d.action == "continue":
340
+ reply, used = router.backend(messages) # router picks the provider
341
+ coord.complete("researcher", actual_tokens=used)
342
+ est.record(1200, used)
343
+ ```
344
+
345
+ `FeatureEstimator` is a drop-in for the default estimator
346
+ (`PredictiveScheduler(estimator=FeatureEstimator())`): a dependency-free
347
+ ridge regression over features you declare (tool, model, temperature, …)
348
+ that also learns per-step **latency**, feeding the optional time budget
349
+ (`PredictiveScheduler(time_budget_s=...)`) — if the predicted step can't
350
+ finish before the deadline, the answer is checkpoint, never wait (time,
351
+ unlike tokens, does not refill).
352
+
353
+ Runnable demo without any key: `python examples/fleet_quickstart.py`
354
+ (routing switch + predictive WAIT under a shared window, in 30 lines of loop).
355
+
356
+ ## Plan before you spend, fork your past, and survive a reboot (v0.4)
357
+
358
+ Four pieces that treat a suspended checkpoint as what it actually is: inert
359
+ data you can inspect, clone, move, or accelerate.
360
+
361
+ ```python
362
+ from agentpause import HumanAttentionBudget, StateStore
363
+
364
+ # 1. FORECAST: before committing to a long run, simulate the REMAINING
365
+ # steps against the live window (no network calls) and get an honest
366
+ # estimate of tokens, money, waits/suspensions and wall-clock time.
367
+ forecast = session.forecast(steps_remaining=12)
368
+ print(forecast) # flags context_wall / fits_time_budget if either fails
369
+
370
+ # 2. FORK: one suspended past, N independent futures. Clones are fully
371
+ # independent (deep-copied messages, own idempotency namespace) but
372
+ # inherit the parent's calibrated estimator (F9.3 symmetry).
373
+ store = StateStore(".agentpause")
374
+ store.fork("research-task", "research-task-cautious")
375
+ store.fork("research-task", "research-task-bold")
376
+
377
+ # 3. MIGRATE: the checkpoint directory IS the process image. Export it on
378
+ # machine A, ship the bundle over any transport, resume on machine B at
379
+ # the exact step.
380
+ bundle = store.export_bundle("research-task")
381
+ StateStore("/mnt/machine-b/.agentpause").import_bundle(bundle)
382
+
383
+ # 4. The human in the loop is a rate-limited resource too: N questions per
384
+ # rolling hour, with a manual override for "I'm away".
385
+ attention = HumanAttentionBudget(max_asks=3, window_s=3600)
386
+ if not attention.ready():
387
+ session.checkpoint() # an absent human means suspend, not spin
388
+ ```
389
+
390
+ For **self-hosted llama.cpp**, checkpoints can go further than logical state:
391
+ `KVStateStore` wraps any `StateStore` and additionally saves/restores the
392
+ model's KV-cache via `/slots` — a TRUE warm start (no re-prefill at all, not
393
+ just no re-work). It degrades gracefully and automatically to a plain logical
394
+ warm start whenever the accelerator can't be trusted: a model fingerprint
395
+ mismatch, a missing blob, or a resume after `import_bundle` on a different
396
+ machine (KV blobs are intentionally machine-local and never migrate).
397
+
398
+ ```python
399
+ from agentpause.llamacpp_kv import LlamaCppSlots, KVStateStore
400
+
401
+ kv_store = KVStateStore(StateStore(".agentpause"),
402
+ slots=LlamaCppSlots(), base_url="http://127.0.0.1:8080")
403
+ kv_store.save_with_kv(checkpoint) # KV blob first, logical commit second
404
+ cp, info = kv_store.load_with_kv("research-task")
405
+ print(info) # {"kv_restored": True, "n_restored": 4096} — or a graceful
406
+ # {"kv_restored": False, "reason": "model_mismatch" | "kv_file_missing"}
407
+ ```
408
+
409
+ Runnable demos without any key or server: `python examples/migrate_fork.py`
410
+ (fork + migration story) and `python examples/kv_llamacpp_demo.py` (KV
411
+ save/restore, model-change degradation, independent forked blobs, migration).
412
+
413
+ ## Why
414
+
415
+ Current agent frameworks persist state *reactively*: they checkpoint after a step
416
+ completes and crash when the provider returns HTTP 429. `agentpause` adds a
417
+ *predictive* layer that estimates the next step's cost, compares it against the
418
+ remaining rate-limit budget, and suspends cleanly before the error — then resumes
419
+ from the exact step.
420
+
421
+ This library is the engineering counterpart of the research preprint *"A
422
+ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
423
+
424
+ ## Components
425
+
426
+ | Module | Role |
427
+ |--------|------|
428
+ | `PredictiveScheduler` / `Session` | the high-level API: `session()`, `should_suspend()`, `call()`, `checkpoint()` |
429
+ | `Estimator` | predicts next-step token cost with a moving-average error correction (ε) and tracks σ |
430
+ | `FeatureEstimator` | drop-in replacement that learns cost *and latency* from declared features (tool, model, …) via dependency-free ridge regression |
431
+ | `should_checkpoint` / `RiskModel` | the suspension rule (`remaining < estimated + k·σ`) and a diagnostic risk score |
432
+ | `Budget` / `decide` | multi-dimensional telemetry (TPM, RPM, reset time, deadline) and the three-valued rule: continue / wait / checkpoint |
433
+ | `StateStore` / `Checkpoint` | atomic logical checkpointing with idempotency keys — works on any provider; `compact()` / `summarize_with()` shrink a suspended checkpoint offline |
434
+ | `BudgetRouter` | predictive multi-provider routing: reads every provider's budget first, routes to the most headroom, cools down 429'd providers |
435
+ | `MultiAgentCoordinator` | one shared rate-limit window across many agents: granted calls reserve their predicted cost; `arbitrate()` resolves contention by priority + fairness |
436
+ | `ToolQuota` | client-side sliding window for rate-limited tools that expose no headers |
437
+ | `CircuitBreaker` / `FallbackBackend` | reactive safety nets: fail fast on a broken provider, try the next one in order |
438
+ | `adapters.litellm.LiteLLMAdapter` | backend + telemetry for any LiteLLM-supported provider (headers → budget, stale reading → 1-token ping) |
439
+ | `adapters.openai_compat.OpenAICompatAdapter` | direct HTTP adapter for OpenAI-compatible APIs (Groq, OpenAI, …) — no litellm dependency |
440
+ | `adapters.anthropic.AnthropicAdapter` | direct adapter for the Anthropic Messages API, with `cache_control` prompt caching and measured `cache_read/write_tokens` |
441
+ | `adapters.langgraph.AgentPauseGuard` | predictive gate for LangGraph nodes: `check()` interrupts the graph before the fatal call, `record()` trains the estimator |
442
+ | `Session.forecast()` | pure, no-network simulation of the remaining steps: predicted tokens, money, waits/suspensions, wall-clock time; flags `context_wall` and `fits_time_budget` |
443
+ | `Checkpoint.fork()` / `StateStore.fork()` | one suspended past, N independent futures — deep-copied state, collision-free idempotency, inherited estimator calibration |
444
+ | `StateStore.export_bundle()` / `import_bundle()` | the checkpoint as a versioned, json-portable process image: suspend on one machine, resume on another at the exact step |
445
+ | `HumanAttentionBudget` | the person in the loop as a rate-limited `Budget`: N asks per rolling window + manual `available()`/`away(until)` override; composes with `decide()` |
446
+ | `llamacpp_kv.LlamaCppSlots` / `KVStateStore` | optional plugin: TRUE warm start for self-hosted llama.cpp via KV-cache save/restore, transactional, with automatic graceful degradation |
447
+
448
+ ## Roadmap
449
+
450
+ - [x] Core components + test suite
451
+ - [x] `PredictiveScheduler` high-level API (`session()`, `should_suspend()`, `call()`)
452
+ - [x] Runnable quickstart example (no keys)
453
+ - [x] LiteLLM adapter (works with any provider)
454
+ - [x] LangGraph adapter (interrupt + checkpointer)
455
+ - [x] Direct adapters (OpenAI-compatible, Anthropic with prompt caching)
456
+ - [x] Predictive multi-provider routing (`BudgetRouter`)
457
+ - [x] Shared budget across agents (`MultiAgentCoordinator`)
458
+ - [x] Feature-based cost & latency estimator (`FeatureEstimator`)
459
+ - [x] Task-completion forecast (`session.forecast()`)
460
+ - [x] Checkpoint fork & cross-machine migration
461
+ - [x] Optional KV-cache plugin for llama.cpp (true warm start), incl. fork+KV
462
+ - [x] Human attention as a rate-limited budget (`HumanAttentionBudget`)
463
+ - [ ] CrewAI / AutoGen / LlamaIndex adapters
464
+ - [ ] KV-cache plugin for vLLM
465
+
466
+ ## License
467
+
468
+ MIT