agentpause 0.3.0__tar.gz → 0.5.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 (70) hide show
  1. agentpause-0.5.0/PKG-INFO +657 -0
  2. agentpause-0.5.0/README.md +632 -0
  3. {agentpause-0.3.0 → agentpause-0.5.0}/pyproject.toml +2 -1
  4. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/__init__.py +13 -1
  5. agentpause-0.5.0/src/agentpause/adapters/local_resources.py +1429 -0
  6. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/adapters/openai_compat.py +47 -3
  7. agentpause-0.5.0/src/agentpause/attention.py +196 -0
  8. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/errors.py +36 -0
  9. agentpause-0.5.0/src/agentpause/fleet.py +214 -0
  10. agentpause-0.5.0/src/agentpause/forecast.py +183 -0
  11. agentpause-0.5.0/src/agentpause/llamacpp_kv.py +583 -0
  12. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/scheduler.py +34 -0
  13. agentpause-0.5.0/src/agentpause/state.py +281 -0
  14. agentpause-0.5.0/src/agentpause.egg-info/PKG-INFO +657 -0
  15. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause.egg-info/SOURCES.txt +11 -0
  16. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause.egg-info/requires.txt +3 -0
  17. agentpause-0.5.0/tests/test_attention.py +132 -0
  18. agentpause-0.5.0/tests/test_fleet.py +164 -0
  19. agentpause-0.5.0/tests/test_forecast.py +130 -0
  20. agentpause-0.5.0/tests/test_fork_migrate.py +210 -0
  21. agentpause-0.5.0/tests/test_llamacpp_kv.py +538 -0
  22. agentpause-0.5.0/tests/test_local_resources.py +1177 -0
  23. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_openai_compat.py +60 -0
  24. agentpause-0.3.0/PKG-INFO +0 -311
  25. agentpause-0.3.0/README.md +0 -288
  26. agentpause-0.3.0/src/agentpause/state.py +0 -146
  27. agentpause-0.3.0/src/agentpause.egg-info/PKG-INFO +0 -311
  28. {agentpause-0.3.0 → agentpause-0.5.0}/LICENSE +0 -0
  29. {agentpause-0.3.0 → agentpause-0.5.0}/setup.cfg +0 -0
  30. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/adapters/__init__.py +0 -0
  31. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/adapters/anthropic.py +0 -0
  32. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/adapters/langgraph.py +0 -0
  33. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/adapters/litellm.py +0 -0
  34. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/breaker.py +0 -0
  35. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/coordinator.py +0 -0
  36. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/estimator.py +0 -0
  37. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/fallback.py +0 -0
  38. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/refill.py +0 -0
  39. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/regression.py +0 -0
  40. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/retry.py +0 -0
  41. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/risk.py +0 -0
  42. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/router.py +0 -0
  43. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause/tools.py +0 -0
  44. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause.egg-info/dependency_links.txt +0 -0
  45. {agentpause-0.3.0 → agentpause-0.5.0}/src/agentpause.egg-info/top_level.txt +0 -0
  46. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_anthropic_adapter.py +0 -0
  47. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_async.py +0 -0
  48. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_breaker.py +0 -0
  49. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_coordinator.py +0 -0
  50. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_estimator.py +0 -0
  51. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_estimator_evolution.py +0 -0
  52. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_fallback.py +0 -0
  53. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_field_adoptions.py +0 -0
  54. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_io_tpm.py +0 -0
  55. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_langgraph_adapter.py +0 -0
  56. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_latency_budget.py +0 -0
  57. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_litellm_adapter.py +0 -0
  58. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_money_and_hooks.py +0 -0
  59. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_quantile.py +0 -0
  60. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_refill_wait.py +0 -0
  61. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_regime.py +0 -0
  62. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_regression.py +0 -0
  63. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_risk.py +0 -0
  64. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_robustness.py +0 -0
  65. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_router.py +0 -0
  66. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_scheduler.py +0 -0
  67. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_smart_context.py +0 -0
  68. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_state.py +0 -0
  69. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_tool_quota.py +0 -0
  70. {agentpause-0.3.0 → agentpause-0.5.0}/tests/test_wait_and_rpm.py +0 -0
@@ -0,0 +1,657 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentpause
3
+ Version: 0.5.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
+ Provides-Extra: gpu
23
+ Requires-Dist: nvidia-ml-py>=12; extra == "gpu"
24
+ Dynamic: license-file
25
+
26
+ # agentpause
27
+
28
+ **Predictive scheduling for autonomous LLM agents.** Suspend an agent gracefully
29
+ *before* it hits a provider rate limit, and resume it without redoing work.
30
+
31
+ The core works on any provider — cloud (OpenAI, Anthropic, Groq) or local — because
32
+ it only serializes application-level state. True KV-cache warm start is an optional
33
+ plugin for self-hosted runtimes (llama.cpp, vLLM). Predictive suspension itself still
34
+ needs a real budget signal to act on — on a cloud provider that's rate-limit headers;
35
+ for a self-hosted server there are none, see "Local mode: what's actually being
36
+ controlled" below for what to wire in instead.
37
+
38
+ ## Measured results (from the accompanying research)
39
+
40
+ | Experiment | Result |
41
+ |---|---|
42
+ | Simulation, 300 runs/config | reactive baseline crashes up to 100% → predictive **0%** (k=4), token waste −80% |
43
+ | Real provider (Groq free, 6K TPM) | multi-window task completed with **zero 429 errors** |
44
+ | vs LangGraph's MemorySaver (200 runs) | LangGraph: 7.8 × 429/run, 9,239 tokens wasted; predictive: **0 and 0** |
45
+ | End-to-end A/B (thinking models, 4B/8B) | recovery **54×–93× faster**; total task time −19% |
46
+
47
+ Reproduce them yourself with a free key: `python scripts/benchmark_groq.py`.
48
+ Live run (2026-07-08, Groq free tier, 12 steps, ~1k-token context,
49
+ refill-aware chunked waiting, leveled windows):
50
+
51
+ | | reactive baseline | agentpause |
52
+ |---|---|---|
53
+ | 429 errors suffered | 7 | **0** |
54
+ | steps redone | 7 | **0** |
55
+ | tokens re-sent (waste) | 12,840 | **0** |
56
+ | telemetry overhead (pings) | 0 | 259 |
57
+ | wall-clock | 82 s | 86 s |
58
+
59
+ Zero errors and zero waste at wall-clock parity: the sensor costs ~2% of
60
+ what crashes waste (259 vs 12,840 tokens) — and waste is money on paid tiers.
61
+
62
+ **Context slimming vs. answer quality** (`python scripts/quality_ab.py`,
63
+ live 2026-07-10, Groq `llama-3.1-8b-instant`): six facts planted early in a
64
+ verbose conversation, same final quiz under three histories.
65
+
66
+ | condition | prompt chars | facts recalled |
67
+ |---|---|---|
68
+ | A — full history | 8,984 | **6/6** |
69
+ | B — `compact()` (blind truncation) | 4,370 | **0/6** — and the model *invented* plausible replacements |
70
+ | C — `summarize_with()` (one summary call) | 3,608 | **6/6** |
71
+
72
+ A 2.2× larger run (`--big`, 19,145 chars — near the free tier's whole TPM
73
+ window, the physical ceiling for a single call) repeated the pattern:
74
+ A 6/6, B 0/6, C 6/6 at a third of the prompt.
75
+
76
+ The instructive failure is B's, and it is *erratic*: in one run it answered
77
+ confidently with fabricated values (fake codename, fake budget, fake city);
78
+ in the larger run it honestly declined. You cannot know in advance which
79
+ failure you get — and the hallucinating one is the dangerous one. Blind
80
+ truncation is an emergency exit (§8.6 overflow, no LLM available), not a
81
+ strategy; semantic summarization recalled everything at a fraction of the
82
+ prompt, earning its one extra call.
83
+ A 20-step live stress test (2026-07-10) closed the loop: at the §8.6 wall the
84
+ scheduler suspended, compacted the checkpoint offline, resumed slim, and
85
+ completed 20/20 steps with zero 429s.
86
+
87
+ **Facts vs. voice** (`python scripts/quality_ab.py`, live 2026-07-20,
88
+ Groq `llama-3.1-8b-instant` — the voice probe rides along on the same run,
89
+ no extra flag needed): does shrinking a checkpoint also flatten the
90
+ *style* it was written in, not just the facts inside it? A persona with two
91
+ short verbal tics is planted in the same conversation as the six facts above:
92
+
93
+ | condition | facts recalled | voice tics preserved |
94
+ |---|---|---|
95
+ | A — full history | **6/6** | 1/2 |
96
+ | B — `compact()` (blind truncation) | 0/6 | 1/2 |
97
+ | C — `summarize_with()` (one summary call) | **6/6** | **0/2** |
98
+
99
+ Truncation and summarization fail in opposite, complementary ways: blind
100
+ truncation keeps short, verbatim phrases intact (only long messages get cut)
101
+ but loses facts scattered earlier in the history; summarization recovers the
102
+ facts but rewrites everything in its own voice, so a persona's way of talking
103
+ does not survive being summarized even when what it *said* does. If tone and
104
+ persona consistency matter for your agent, `compact()` and `summarize_with()`
105
+ are not interchangeable — pick per what you can't afford to lose.
106
+
107
+ > Status: **0.4.0**. Core scheduler, direct + LiteLLM + Anthropic adapters,
108
+ > LangGraph integration, multi-provider routing, multi-agent shared budgets,
109
+ > feature-based cost/latency estimation, task-completion forecast, checkpoint
110
+ > fork & cross-machine migration, an optional true-warm-start KV-cache plugin
111
+ > for llama.cpp, human attention as a budget, runnable examples, and a full
112
+ > test suite are all in place.
113
+
114
+ ## Install
115
+
116
+ ```bash
117
+ pip install agentpause
118
+ ```
119
+
120
+ Optional extras, only if you use them (the core has zero dependencies):
121
+
122
+ ```bash
123
+ pip install "agentpause[litellm]" # LiteLLM adapter (100+ providers)
124
+ pip install "agentpause[langgraph]" # LangGraph adapter
125
+ ```
126
+
127
+ Developing on the library itself (clone + editable install + tests):
128
+
129
+ ```bash
130
+ git clone https://github.com/Champoleello/agentpause
131
+ cd agentpause
132
+ pip install -e ".[dev]"
133
+ pytest
134
+ ```
135
+
136
+ ## Getting started
137
+
138
+ **Step 1 — the smallest possible loop.** `backend` is any callable
139
+ `messages -> (reply, tokens_used)`; `telemetry` is any callable
140
+ `() -> remaining_tokens`. That's the entire contract — no framework required:
141
+
142
+ ```python
143
+ from agentpause import PredictiveScheduler
144
+
145
+ sched = PredictiveScheduler(backend=my_llm_call, telemetry=my_rate_limit_reader)
146
+
147
+ with sched.session("task-1") as s: # resumes automatically if interrupted before
148
+ for question in questions[s.step:]: # skip steps finished before a suspend
149
+ s.add_user(question)
150
+ if s.should_suspend(): # predictive check, *before* the call
151
+ s.checkpoint()
152
+ break # stop cleanly; rerun to resume
153
+ reply = s.call()
154
+ else:
155
+ s.complete() # task done: drop the checkpoint
156
+ ```
157
+
158
+ Run it now, no API key needed: `python examples/quickstart.py` — it suspends
159
+ mid-task on the first run and resumes cleanly the second time you run it.
160
+
161
+ **Step 2 — the same three calls, wired into YOUR agent loop.** Whatever
162
+ framework you use (a hand-rolled ReAct loop, LangGraph, CrewAI, a plain
163
+ `while` loop), the integration is always the same three calls in the same
164
+ three places:
165
+
166
+ 1. Before every LLM call, ask `should_suspend()` (or `session.next_action()`
167
+ for the richer `continue`/`wait`/`checkpoint` answer). This is a local
168
+ computation — no network call, no cost.
169
+ 2. If it says stop, call `checkpoint()` and exit the process. Nothing else
170
+ to do: state, messages, and idempotency keys are already on disk.
171
+ 3. Re-running the same code with the same session id resumes automatically
172
+ from the exact step — `with sched.session(...)` handles the restore.
173
+
174
+ **Step 3 — pick the adapter for your provider**, which supplies `backend` and
175
+ `telemetry` for you instead of writing them by hand:
176
+
177
+ | You use... | Adapter | Runnable example |
178
+ |---|---|---|
179
+ | LiteLLM (100+ providers) | `adapters.litellm.LiteLLMAdapter` | `examples/litellm_groq.py` |
180
+ | Direct HTTP, OpenAI-compatible (Groq, OpenAI, ...) | `adapters.openai_compat.OpenAICompatAdapter` | `scripts/validate_provider.py` |
181
+ | Direct Anthropic Messages API | `adapters.anthropic.AnthropicAdapter` | see "Real providers" below |
182
+ | LangGraph | `adapters.langgraph.AgentPauseGuard` | `examples/langgraph_quickstart.py` |
183
+ | A CrewAI/AutoGen/custom loop, no adapter yet | none — write `backend`/`telemetry` by hand as in Step 1 | `examples/quickstart.py` |
184
+
185
+ Details and full code for each adapter are in the sections right below; the
186
+ `## Components` table further down is the complete function/class reference
187
+ for everything the library exposes.
188
+
189
+ ## Real providers via LiteLLM
190
+
191
+ The LiteLLM adapter supplies both callables for 100+ providers (OpenAI, Groq,
192
+ Anthropic, local servers, ...), reading the budget from each response's
193
+ rate-limit headers and refreshing stale readings with a tiny telemetry ping:
194
+
195
+ ```python
196
+ from agentpause import PredictiveScheduler
197
+ from agentpause.adapters.litellm import LiteLLMAdapter
198
+
199
+ adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
200
+ sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.telemetry)
201
+ ```
202
+
203
+ Install with `pip install -e ".[litellm]"`; see `examples/litellm_groq.py`.
204
+ To validate against your own provider (frontier models included):
205
+ `python scripts/validate_provider.py gpt-4o-mini`.
206
+
207
+ Rate-limit headers by provider (defaults target the OpenAI-style names):
208
+
209
+ | Provider | remaining tokens | remaining requests | reset |
210
+ |---|---|---|---|
211
+ | OpenAI | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`1s`, `6m0s`) |
212
+ | Groq | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`7.66s`, `2m59.56s`) |
213
+ | Anthropic | `anthropic-ratelimit-tokens-remaining` | `anthropic-ratelimit-requests-remaining` | RFC 3339 timestamp — set `remaining_header=` etc. |
214
+
215
+ Header names differ? Override them: `LiteLLMAdapter(model=..., remaining_header="...", requests_header="...", reset_header="...")`.
216
+
217
+ ### Self-hosted: ollama-gateway
218
+
219
+ Running Ollama locally behind
220
+ [martinobettucci/ollama-gateway](https://github.com/martinobettucci/ollama-gateway)
221
+ (a self-hosted auth + quota proxy for Ollama)? It emits the same
222
+ OpenAI/Groq-style `x-ratelimit-*` headers agentpause already reads, so it
223
+ works out of the box through the direct adapter — no litellm, no
224
+ special-casing. Confirmed live end-to-end (2026-07-20) against a real
225
+ gateway instance: `adapter.budget()` correctly reads `remaining_tokens`,
226
+ `remaining_requests` (decrementing per call), and `reset_seconds` — **the
227
+ one condition is that the API key has a quota configured** (token cap and/or
228
+ rate limit); a key with no quota set simply emits no rate-limit headers at
229
+ all, which agentpause reports as a clear `TelemetryError` rather than a
230
+ silent wrong answer:
231
+
232
+ ```python
233
+ from agentpause.adapters.openai_compat import OpenAICompatAdapter
234
+
235
+ adapter = OpenAICompatAdapter.for_model("ollama-gateway/llama3:8b")
236
+ # default base_url is http://127.0.0.1:8787/v1 (dev default); override for
237
+ # a custom port or a production HTTPS domain:
238
+ # OpenAICompatAdapter.for_model("ollama-gateway/llama3:8b",
239
+ # base_url="https://your-gateway-domain")
240
+ ```
241
+
242
+ ## Beyond tokens: RPM and wait-vs-suspend
243
+
244
+ Telemetry can be richer than a token count. `adapter.budget` reports all
245
+ three dimensions providers expose — remaining tokens (TPM), remaining
246
+ requests (RPM), and seconds until the window resets — and unlocks the
247
+ three-valued decision:
248
+
249
+ ```python
250
+ sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.budget)
251
+
252
+ d = session.next_action() # "continue" | "wait" | "checkpoint"
253
+ ```
254
+
255
+ `wait` fires when the budget does not fit **but** the window resets within
256
+ `wait_threshold_s` (default 15 s): a short in-place pause is cheaper than a
257
+ full suspend/resume cycle. Exhausted requests (RPM = 0) block the call even
258
+ with plenty of tokens left. Plain-int telemetry keeps working unchanged.
259
+
260
+ ## Async
261
+
262
+ Every entry point has an async twin — same rules, same guarantees, never
263
+ blocks the event loop:
264
+
265
+ ```python
266
+ adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
267
+ sched = PredictiveScheduler(backend=None, async_backend=adapter.abackend,
268
+ telemetry=adapter.telemetry)
269
+ reply = await session.acall() # retry/backoff via asyncio.sleep
270
+ await guard.acheck(state["messages"]) # async LangGraph nodes
271
+ ```
272
+
273
+ For sharper input estimates, wire the per-model tokenizer:
274
+ `PredictiveScheduler(..., count_tokens=adapter.count_tokens)` (falls back to
275
+ the ~4 chars/token heuristic if the tokenizer is unavailable).
276
+
277
+ ## When prediction fails anyway
278
+
279
+ Estimates are statistical — a 429 can still slip through. agentpause survives
280
+ it instead of crashing:
281
+
282
+ - **typed errors**: everything derives from `AgentPauseError`
283
+ (`RateLimitHit`, `TelemetryError`, `CheckpointError`, `BackendError`);
284
+ - **retry with backoff**: unexpected 429s are retried (provider `retry-after`
285
+ honored, else exponential backoff — see `RetryPolicy`);
286
+ - **hits are feedback**: each one bumps `safety_k` up (capped at `k_max`),
287
+ so the scheduler grows more cautious on workloads it underestimates;
288
+ - **clean failure**: a failed call leaves the session state untouched —
289
+ no phantom steps, resumes stay consistent.
290
+
291
+ ## LangGraph integration
292
+
293
+ `AgentPauseGuard` adds the predictive gate to any LangGraph node — LangGraph
294
+ persists reactively (after nodes), the guard pauses *before* the call that
295
+ would hit the rate limit, via LangGraph's own `interrupt()` + checkpointer:
296
+
297
+ ```python
298
+ from agentpause.adapters.langgraph import AgentPauseGuard
299
+
300
+ guard = AgentPauseGuard(telemetry=adapter.telemetry) # e.g. from LiteLLMAdapter
301
+
302
+ def agent_node(state):
303
+ guard.check(state["messages"]) # pauses the graph here if needed
304
+ reply = llm.invoke(state["messages"])
305
+ guard.record(state["messages"], reply.usage_metadata["total_tokens"])
306
+ ...
307
+ ```
308
+
309
+ Resume the paused thread with `graph.invoke(Command(resume=True), config)`.
310
+ On resume the guard re-reads telemetry fresh — never from the checkpoint.
311
+ Install with `pip install -e ".[langgraph]"`; see `examples/langgraph_quickstart.py`.
312
+
313
+ ## Scaling up: many providers, many agents, smarter estimates (v0.3)
314
+
315
+ The same predictive idea — read the budget first, act before the error —
316
+ extends to routing across providers and sharing one window across agents.
317
+
318
+ ### Plug and play: `AgentFleet`
319
+
320
+ Combining `BudgetRouter` + `MultiAgentCoordinator` + an estimator by hand is
321
+ six manual pieces for every single call any agent makes: build the router,
322
+ build the coordinator around `router.budget`, register each agent, build a
323
+ separate estimator per agent, compute `estimated`/`sigma` from it yourself,
324
+ call `coordinator.request(...)`, check `.action == "continue"`, call
325
+ `router.backend(...)` yourself, call `coordinator.complete(...)`, call
326
+ `est.record(...)`. `AgentFleet` collapses that to one constructor call plus
327
+ one `.call(...)` per step:
328
+
329
+ ```python
330
+ from agentpause import AgentFleet
331
+ from agentpause.adapters.openai_compat import OpenAICompatAdapter
332
+ from agentpause.adapters.anthropic import AnthropicAdapter
333
+
334
+ fleet = AgentFleet(
335
+ [("groq", OpenAICompatAdapter.for_model("groq/llama-3.1-8b-instant")),
336
+ ("claude", AnthropicAdapter("claude-haiku-4-5"))],
337
+ agents=[("researcher", 1), ("summarizer", 0)], # (agent_id, priority)
338
+ )
339
+
340
+ decision, reply, used = fleet.call("researcher", messages)
341
+ if decision.action == "continue":
342
+ ... # reply/used are already there; the shared pool is already reconciled
343
+ elif decision.action == "wait":
344
+ ... # decision.wait_seconds suggests how long
345
+ else:
346
+ ... # checkpoint: this agent's own StateStore, same as single-agent usage
347
+ ```
348
+
349
+ What you must provide: the list of `providers` (same shape `BudgetRouter`
350
+ already accepts), and agent ids if you want explicit priorities. What's
351
+ automatic: the router and coordinator built together on the same clock, a
352
+ fresh and ISOLATED estimator per agent (one agent's learned history never
353
+ leaks into another's), and the whole request → call → reconcile → record
354
+ sequence behind one method. What this does **not** add: checkpoint/resume
355
+ for fleets — `Session`'s predictive decision is computed from its own private
356
+ estimator and there's no seam (by design — the core stays dependency-free,
357
+ `MultiAgentCoordinator`/`BudgetRouter` are deliberately separate,
358
+ adapter-level constructs) to redirect it through the coordinator instead. If
359
+ you need per-agent resumability, pair `AgentFleet` with your own `StateStore`
360
+ per `agent_id`, exactly like single-agent usage does elsewhere in this
361
+ README.
362
+
363
+ Runnable, no key required: `python examples/fleet_facade_quickstart.py`.
364
+
365
+ ### Under the hood: `BudgetRouter` + `MultiAgentCoordinator` + `FeatureEstimator` on their own
366
+
367
+ Reach for these directly instead of the facade when you want fine control —
368
+ a custom routing `key=`, direct access to `coordinator.arbitrate()` for a
369
+ batch of simultaneous requests, or a hand-built estimator per agent:
370
+
371
+ ```python
372
+ from agentpause import BudgetRouter, MultiAgentCoordinator, FeatureEstimator
373
+ from agentpause.adapters.openai_compat import OpenAICompatAdapter
374
+ from agentpause.adapters.anthropic import AnthropicAdapter
375
+
376
+ # 1. Route each call to the provider with the most headroom (predictive
377
+ # fallback: switch BEFORE the 429, not after). Providers in cooldown
378
+ # after a real 429 are skipped until their window resets.
379
+ router = BudgetRouter(
380
+ ("groq", OpenAICompatAdapter.for_model("groq/llama-3.1-8b-instant")),
381
+ ("claude", AnthropicAdapter("claude-haiku-4-5")),
382
+ )
383
+
384
+ # 2. Share ONE rate-limit window across a fleet: every granted call
385
+ # reserves its predicted cost (estimate + k·σ) from the shared pool,
386
+ # so agents can't overcommit the window together. Contention is
387
+ # arbitrated by priority, then longest-waiting — but ONLY in arbitrate()'s
388
+ # batch call; the streaming request()/complete() pair below has no
389
+ # priority ordering of its own, first-asked is first-served.
390
+ coord = MultiAgentCoordinator(telemetry=router.budget)
391
+ coord.register("researcher", priority=1)
392
+ coord.register("summarizer")
393
+
394
+ est = FeatureEstimator() # 3. learn cost from features,
395
+ est.set_context(tool="web_search") # not just context size
396
+
397
+ d = coord.request("researcher", estimated=est.estimate(1200),
398
+ sigma=est.sigma(fallback_estimate=1200))
399
+ if d.action == "continue":
400
+ reply, used = router.backend(messages) # router picks the provider
401
+ coord.complete("researcher", actual_tokens=used)
402
+ est.record(1200, used)
403
+ ```
404
+
405
+ `FeatureEstimator` is a drop-in for the default estimator
406
+ (`PredictiveScheduler(estimator=FeatureEstimator())`, or
407
+ `AgentFleet(..., estimator_factory=FeatureEstimator)`): a dependency-free
408
+ ridge regression over features you declare (tool, model, temperature, …)
409
+ that also learns per-step **latency**, feeding the optional time budget
410
+ (`PredictiveScheduler(time_budget_s=...)`) — if the predicted step can't
411
+ finish before the deadline, the answer is checkpoint, never wait (time,
412
+ unlike tokens, does not refill).
413
+
414
+ Runnable demo without any key: `python examples/fleet_quickstart.py`
415
+ (the same three pieces, composed by hand instead of through the facade, for
416
+ anyone who wants to see or override one piece at a time).
417
+
418
+ ## Plan before you spend, fork your past, and survive a reboot (v0.4)
419
+
420
+ Four pieces that treat a suspended checkpoint as what it actually is: inert
421
+ data you can inspect, clone, move, or accelerate.
422
+
423
+ ```python
424
+ from agentpause import HumanAttentionBudget, StateStore
425
+
426
+ # 1. FORECAST: before committing to a long run, simulate the REMAINING
427
+ # steps against the live window (no network calls) and get an honest
428
+ # estimate of tokens, money, waits/suspensions and wall-clock time.
429
+ forecast = session.forecast(steps_remaining=12)
430
+ print(forecast) # flags context_wall / fits_time_budget if either fails
431
+
432
+ # 2. FORK: one suspended past, N independent futures. Clones are fully
433
+ # independent (deep-copied messages, own idempotency namespace) but
434
+ # inherit the parent's calibrated estimator (F9.3 symmetry).
435
+ store = StateStore(".agentpause")
436
+ store.fork("research-task", "research-task-cautious")
437
+ store.fork("research-task", "research-task-bold")
438
+
439
+ # 3. MIGRATE: the checkpoint directory IS the process image. Export it on
440
+ # machine A, ship the bundle over any transport, resume on machine B at
441
+ # the exact step.
442
+ bundle = store.export_bundle("research-task")
443
+ StateStore("/mnt/machine-b/.agentpause").import_bundle(bundle)
444
+
445
+ # 4. The human in the loop is a rate-limited resource too: N questions per
446
+ # rolling hour, with a manual override for "I'm away".
447
+ attention = HumanAttentionBudget(max_asks=3, window_s=3600)
448
+ if not attention.ready():
449
+ session.checkpoint() # an absent human means suspend, not spin
450
+ ```
451
+
452
+ For **self-hosted llama.cpp**, checkpoints can go further than logical state:
453
+ `KVStateStore` wraps any `StateStore` and additionally saves/restores the
454
+ model's KV-cache via `/slots` — a TRUE warm start (no re-prefill at all, not
455
+ just no re-work). It degrades gracefully and automatically to a plain logical
456
+ warm start whenever the accelerator can't be trusted: a model fingerprint
457
+ mismatch, a missing blob, or a resume after `import_bundle` on a different
458
+ machine (KV blobs are intentionally machine-local and never migrate).
459
+
460
+ ```python
461
+ from agentpause.llamacpp_kv import LlamaCppSlots, KVStateStore
462
+
463
+ kv_store = KVStateStore(StateStore(".agentpause"),
464
+ slots=LlamaCppSlots(), base_url="http://127.0.0.1:8080",
465
+ kv_dir="./kv_cache") # MUST equal --slot-save-path, see below
466
+ kv_store.save_with_kv(checkpoint) # KV blob first, logical commit second
467
+ cp, info = kv_store.load_with_kv("research-task")
468
+ print(info) # {"kv_restored": True, "n_restored": 4096} — or a graceful
469
+ # {"kv_restored": False, "reason": "model_mismatch" | "kv_file_missing"}
470
+ ```
471
+
472
+ `kv_dir` must be the exact same directory the server was started with via
473
+ `--slot-save-path` — a llama-server does not expose that path anywhere in
474
+ `/props` or `/slots`, so this is the one thing agentpause cannot discover on
475
+ its own and must be told. Getting it wrong used to surface only much later,
476
+ at the next restore, as `reason="kv_file_missing"`; `save_with_kv` now checks
477
+ that the blob actually landed in `kv_dir` right after the save call returns
478
+ and raises a clear `KVError` immediately if it didn't, naming the likely
479
+ `kv_dir`/`--slot-save-path` mismatch — caught on the very first save, not
480
+ discovered sessions later.
481
+
482
+ Runnable demos without any key or server: `python examples/migrate_fork.py`
483
+ (fork + migration story) and `python examples/kv_llamacpp_demo.py` (KV
484
+ save/restore, model-change degradation, independent forked blobs, migration).
485
+
486
+ ## Local mode: what's actually being controlled
487
+
488
+ A rate-limit budget on a cloud provider comes from real response headers
489
+ (`x-ratelimit-remaining-tokens` and friends — see the tables above). A
490
+ self-hosted `llama-server` sends none of those headers, because there is no
491
+ traffic limit to respect: nobody is throttling requests-per-minute against a
492
+ process running on your own machine.
493
+
494
+ That has a direct consequence for what `should_suspend()` does locally.
495
+ **Without explicitly configuring `time_budget_s`, `money_budget`, or one of
496
+ the local adapters below, `should_suspend()` has nothing real to evaluate on
497
+ a self-hosted server and will never fire** — unlike on a cloud provider, the
498
+ library will not stop spontaneously for a resource limit, because nothing
499
+ told it one exists. A hand-picked `fallback_remaining=N` papers over this
500
+ with a number that looks real but never depletes. What keeps working with no
501
+ extra configuration either way: manual `checkpoint()` calls, and everything
502
+ the KV-cache section above describes — a warm start stays exactly as
503
+ valuable whether or not a budget signal is also wired in.
504
+
505
+ ### Plug and play: `LocalResourceBudget`
506
+
507
+ `agentpause.adapters.local_resources` has five independently useful pieces
508
+ for self-hosted llama.cpp (below). Wiring all five by hand for the common
509
+ case — one server, one GPU or none — is six manual steps, two of which need
510
+ a hand calibration most people won't think to do correctly (a
511
+ `bytes_per_token` for the GPU budget, an `estimated_kv_save_s` guess for the
512
+ time budget). `LocalResourceBudget` reduces that to one constructor call
513
+ plus one `.kv_store(...)` call, and self-calibrates the rest from your first
514
+ real checkpoint save — no synthetic probe, no hand measurement:
515
+
516
+ ```python
517
+ from agentpause import PredictiveScheduler, StateStore
518
+ from agentpause.adapters.local_resources import LocalResourceBudget
519
+
520
+ # kv_dir is the ONE thing this can't discover on its own: it MUST be the
521
+ # exact same directory the server was started with via --slot-save-path
522
+ # (llama-server --slot-save-path ./kv_cache ... <-> kv_dir="./kv_cache").
523
+ local = LocalResourceBudget(kv_dir="./kv_cache")
524
+
525
+ sched = PredictiveScheduler(backend=my_llm_call, telemetry=local)
526
+ kv_store = local.kv_store(StateStore(".agentpause"))
527
+
528
+ # normal use from here: kv_store.save_with_kv(checkpoint) whenever you need
529
+ # to suspend. VRAM, the KV-save time reserve, and the disk-space guard
530
+ # threshold all calibrate themselves from that FIRST real save — nothing
531
+ # to measure by hand, nothing to guess.
532
+ ```
533
+
534
+ What you must provide: `kv_dir` (see above), and `base_url` only if your
535
+ server isn't the default `http://127.0.0.1:8080`. What's automatic: the
536
+ configured context size (read from `GET /props` the instant you construct
537
+ it — an unreachable server fails loudly right here, not three steps into a
538
+ run), whether an NVIDIA GPU is present at all (no GPU is treated as
539
+ completely ordinary, never an error), and — from the first real
540
+ `save_with_kv()` call onward — `bytes_per_token`, the measured KV-save
541
+ duration, and a disk-space guard threshold sized from the real blob you just
542
+ wrote (`max(500 MB, 5× that blob's size)`), unless you asked for a specific
543
+ threshold yourself.
544
+
545
+ Runnable, no real server or GPU required:
546
+ `python examples/local_resource_budget_quickstart.py`.
547
+
548
+ ### Under the hood: the five pieces on their own
549
+
550
+ Reach for these directly instead of the facade when you want fine control
551
+ over one axis — a custom `used_field=`, a hand-picked `bytes_per_token`, an
552
+ explicit `estimated_kv_save_s`:
553
+
554
+ - `LlamaCppContextBudget` — reads the ACTUAL configured context size and the
555
+ ACTUAL tokens used in a slot's KV cache from a real server (`GET /props` +
556
+ `GET /slots`); never sets `reset_seconds` (a local context window has
557
+ nothing that refills it on its own).
558
+ - `GPUMemoryBudget` — reads REAL free VRAM straight from the hardware via
559
+ NVML (`pynvml`, PyPI package `nvidia-ml-py`); refuses to guess a token
560
+ count without an explicit `bytes_per_token`.
561
+ - `KVAwareTimeBudget` — wraps another telemetry callable and reserves
562
+ wall-clock time up front for saving the KV-cache blob before a deadline
563
+ actually hits.
564
+ - `CompositeLocalBudget` — composes several telemetry callables (e.g. the
565
+ two above) into one, reporting whichever resource is closest to running
566
+ out, not an average of them.
567
+ - `estimate_local_price_per_1k_tokens` / `estimate_hourly_cost_from_power` /
568
+ `price_per_1k_tokens_from_estimator` — derive a real `price_per_1k_tokens`
569
+ for the scheduler's existing monetary constraint from local throughput and
570
+ a power/rental cost, instead of a provider invoice.
571
+
572
+ Two honest caveats, not glossed over (they apply whether you use the facade
573
+ or these pieces directly, since the facade is built on top of them):
574
+
575
+ - **`LlamaCppContextBudget`'s `used_field`**: CONFIRMED live (2026-07-21,
576
+ against a real llama-server serving Qwen3-8B-GGUF Q4_K_M) to be
577
+ `n_prompt_tokens` — not documented in the official llama.cpp server
578
+ README's own example payload at the time this was written, found by
579
+ actually diffing a slot's `GET /slots` entry before and after a real
580
+ completion. The default extractor tries it first, with older guesses kept
581
+ as a fallback chain for other llama.cpp-ecosystem forks/proxies that may
582
+ shape this differently. Verify against your own server if you're not
583
+ running mainline llama.cpp, and pass `used_field=` explicitly if it
584
+ doesn't match.
585
+ - **`GPUMemoryBudget` is NVIDIA-only, today**: it reads VRAM via NVML.
586
+ AMD/ROCm GPUs expose a different interface (`rocm-smi`/`amdsmi`) that this
587
+ module does not implement — bring your own `reader_fn=` on ROCm hardware.
588
+
589
+ Runnable demo, no real server or GPU required:
590
+ `python examples/local_resources_quickstart.py` (the same five signals,
591
+ composed by hand instead of through the facade, for anyone who wants to see
592
+ or override one piece at a time).
593
+
594
+ ## Why
595
+
596
+ Current agent frameworks persist state *reactively*: they checkpoint after a step
597
+ completes and crash when the provider returns HTTP 429. `agentpause` adds a
598
+ *predictive* layer that estimates the next step's cost, compares it against the
599
+ remaining rate-limit budget, and suspends cleanly before the error — then resumes
600
+ from the exact step.
601
+
602
+ This library is the engineering counterpart of the research preprint *"A
603
+ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
604
+
605
+ ## Components
606
+
607
+ | Module | Role |
608
+ |--------|------|
609
+ | `PredictiveScheduler` / `Session` | the high-level API: `session()`, `should_suspend()`, `call()`, `checkpoint()` |
610
+ | `Estimator` | predicts next-step token cost with a moving-average error correction (ε) and tracks σ |
611
+ | `FeatureEstimator` | drop-in replacement that learns cost *and latency* from declared features (tool, model, …) via dependency-free ridge regression |
612
+ | `should_checkpoint` / `RiskModel` | the suspension rule (`remaining < estimated + k·σ`) and a diagnostic risk score |
613
+ | `Budget` / `decide` | multi-dimensional telemetry (TPM, RPM, reset time, deadline) and the three-valued rule: continue / wait / checkpoint |
614
+ | `StateStore` / `Checkpoint` | atomic logical checkpointing with idempotency keys — works on any provider; `compact()` / `summarize_with()` shrink a suspended checkpoint offline |
615
+ | `BudgetRouter` | predictive multi-provider routing: reads every provider's budget first, routes to the most headroom, cools down 429'd providers |
616
+ | `MultiAgentCoordinator` | one shared rate-limit window across many agents: granted calls reserve their predicted cost; `arbitrate()` resolves contention by priority + fairness |
617
+ | `AgentFleet` | plug-and-play facade over the two rows above plus a per-agent estimator: one constructor call + one `.call(agent_id, messages)` per step replaces the request/call/complete/record dance |
618
+ | `ToolQuota` | client-side sliding window for rate-limited tools that expose no headers |
619
+ | `CircuitBreaker` / `FallbackBackend` | reactive safety nets: fail fast on a broken provider, try the next one in order |
620
+ | `adapters.litellm.LiteLLMAdapter` | backend + telemetry for any LiteLLM-supported provider (headers → budget, stale reading → 1-token ping) |
621
+ | `adapters.openai_compat.OpenAICompatAdapter` | direct HTTP adapter for OpenAI-compatible APIs (Groq, OpenAI, …) — no litellm dependency |
622
+ | `adapters.anthropic.AnthropicAdapter` | direct adapter for the Anthropic Messages API, with `cache_control` prompt caching and measured `cache_read/write_tokens` |
623
+ | `adapters.langgraph.AgentPauseGuard` | predictive gate for LangGraph nodes: `check()` interrupts the graph before the fatal call, `record()` trains the estimator |
624
+ | `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` |
625
+ | `Checkpoint.fork()` / `StateStore.fork()` | one suspended past, N independent futures — deep-copied state, collision-free idempotency, inherited estimator calibration |
626
+ | `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 |
627
+ | `HumanAttentionBudget` | the person in the loop as a rate-limited `Budget`: N asks per rolling window + manual `available()`/`away(until)` override; composes with `decide()` |
628
+ | `llamacpp_kv.LlamaCppSlots` / `KVStateStore` | optional plugin: TRUE warm start for self-hosted llama.cpp via KV-cache save/restore, transactional, with automatic graceful degradation |
629
+ | `adapters.local_resources.LlamaCppContextBudget` | real local-context `Budget` from a llama.cpp server's actual `--ctx-size` and current KV-cache usage (`/props` + `/slots`); never a rate limit, never a `reset_seconds` |
630
+ | `adapters.local_resources.GPUMemoryBudget` | real free-VRAM `Budget` read from NVML (`pynvml`); NVIDIA-only, needs an explicit `bytes_per_token` to report tokens |
631
+ | `adapters.local_resources.KVAwareTimeBudget` | wraps any telemetry callable and takes over `remaining_seconds` itself, reserving time up front for a KV-cache blob save |
632
+ | `adapters.local_resources.CompositeLocalBudget` | composes several telemetry callables into one, most-restrictive `Budget` — see "Local mode" above |
633
+ | `adapters.local_resources.estimate_local_price_per_1k_tokens` / `estimate_hourly_cost_from_power` / `price_per_1k_tokens_from_estimator` | derive a real `price_per_1k_tokens` for a local deployment from throughput and power/rental cost, instead of a provider invoice |
634
+ | `adapters.local_resources.LocalResourceBudget` | plug-and-play facade over the four rows above plus the KV disk guard: one constructor call + `.kv_store(...)`, self-calibrates VRAM/time/disk numbers from your first real checkpoint save |
635
+
636
+ ## Roadmap
637
+
638
+ - [x] Core components + test suite
639
+ - [x] `PredictiveScheduler` high-level API (`session()`, `should_suspend()`, `call()`)
640
+ - [x] Runnable quickstart example (no keys)
641
+ - [x] LiteLLM adapter (works with any provider)
642
+ - [x] LangGraph adapter (interrupt + checkpointer)
643
+ - [x] Direct adapters (OpenAI-compatible, Anthropic with prompt caching)
644
+ - [x] Predictive multi-provider routing (`BudgetRouter`)
645
+ - [x] Shared budget across agents (`MultiAgentCoordinator`), with a one-call plug-and-play facade (`AgentFleet`)
646
+ - [x] Feature-based cost & latency estimator (`FeatureEstimator`)
647
+ - [x] Task-completion forecast (`session.forecast()`)
648
+ - [x] Checkpoint fork & cross-machine migration
649
+ - [x] Optional KV-cache plugin for llama.cpp (true warm start), incl. fork+KV
650
+ - [x] Human attention as a rate-limited budget (`HumanAttentionBudget`)
651
+ - [x] Real local-resource budgets for self-hosted llama.cpp: context (`LlamaCppContextBudget`), GPU VRAM (`GPUMemoryBudget`), KV-aware time (`KVAwareTimeBudget`), derived local price, composed via `CompositeLocalBudget`, and a one-call plug-and-play facade (`LocalResourceBudget`) that self-calibrates all of the above from a real checkpoint save
652
+ - [ ] CrewAI / AutoGen / LlamaIndex adapters
653
+ - [ ] KV-cache plugin for vLLM
654
+
655
+ ## License
656
+
657
+ MIT