agentpause 0.3.0__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 (63) hide show
  1. {agentpause-0.3.0/src/agentpause.egg-info → agentpause-0.4.0}/PKG-INFO +175 -18
  2. {agentpause-0.3.0 → agentpause-0.4.0}/README.md +174 -17
  3. {agentpause-0.3.0 → agentpause-0.4.0}/pyproject.toml +1 -1
  4. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/__init__.py +11 -1
  5. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/openai_compat.py +47 -3
  6. agentpause-0.4.0/src/agentpause/attention.py +196 -0
  7. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/errors.py +15 -0
  8. agentpause-0.4.0/src/agentpause/forecast.py +183 -0
  9. agentpause-0.4.0/src/agentpause/llamacpp_kv.py +403 -0
  10. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/scheduler.py +34 -0
  11. agentpause-0.4.0/src/agentpause/state.py +281 -0
  12. {agentpause-0.3.0 → agentpause-0.4.0/src/agentpause.egg-info}/PKG-INFO +175 -18
  13. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/SOURCES.txt +7 -0
  14. agentpause-0.4.0/tests/test_attention.py +132 -0
  15. agentpause-0.4.0/tests/test_forecast.py +130 -0
  16. agentpause-0.4.0/tests/test_fork_migrate.py +210 -0
  17. agentpause-0.4.0/tests/test_llamacpp_kv.py +278 -0
  18. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_openai_compat.py +60 -0
  19. agentpause-0.3.0/src/agentpause/state.py +0 -146
  20. {agentpause-0.3.0 → agentpause-0.4.0}/LICENSE +0 -0
  21. {agentpause-0.3.0 → agentpause-0.4.0}/setup.cfg +0 -0
  22. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/__init__.py +0 -0
  23. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/anthropic.py +0 -0
  24. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/langgraph.py +0 -0
  25. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/litellm.py +0 -0
  26. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/breaker.py +0 -0
  27. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/coordinator.py +0 -0
  28. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/estimator.py +0 -0
  29. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/fallback.py +0 -0
  30. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/refill.py +0 -0
  31. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/regression.py +0 -0
  32. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/retry.py +0 -0
  33. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/risk.py +0 -0
  34. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/router.py +0 -0
  35. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/tools.py +0 -0
  36. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/dependency_links.txt +0 -0
  37. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/requires.txt +0 -0
  38. {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/top_level.txt +0 -0
  39. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_anthropic_adapter.py +0 -0
  40. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_async.py +0 -0
  41. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_breaker.py +0 -0
  42. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_coordinator.py +0 -0
  43. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_estimator.py +0 -0
  44. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_estimator_evolution.py +0 -0
  45. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_fallback.py +0 -0
  46. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_field_adoptions.py +0 -0
  47. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_io_tpm.py +0 -0
  48. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_langgraph_adapter.py +0 -0
  49. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_latency_budget.py +0 -0
  50. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_litellm_adapter.py +0 -0
  51. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_money_and_hooks.py +0 -0
  52. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_quantile.py +0 -0
  53. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_refill_wait.py +0 -0
  54. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_regime.py +0 -0
  55. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_regression.py +0 -0
  56. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_risk.py +0 -0
  57. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_robustness.py +0 -0
  58. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_router.py +0 -0
  59. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_scheduler.py +0 -0
  60. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_smart_context.py +0 -0
  61. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_state.py +0 -0
  62. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_tool_quota.py +0 -0
  63. {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_wait_and_rpm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentpause
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Predictive scheduling for autonomous LLM agents: suspend gracefully before rate limits, resume without losing work.
5
5
  Author: Andrea Lelio Ciampolillo
6
6
  License: MIT
@@ -79,12 +79,60 @@ A 20-step live stress test (2026-07-10) closed the loop: at the §8.6 wall the
79
79
  scheduler suspended, compacted the checkpoint offline, resumed slim, and
80
80
  completed 20/20 steps with zero 429s.
81
81
 
82
- > Status: **0.3.0**. Core scheduler, direct + LiteLLM + Anthropic adapters,
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,
83
103
  > LangGraph integration, multi-provider routing, multi-agent shared budgets,
84
- > feature-based cost/latency estimation, runnable examples, and a full test
85
- > suite are in place; the optional KV-cache plugin is next.
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
86
132
 
87
- ## Quick example
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:
88
136
 
89
137
  ```python
90
138
  from agentpause import PredictiveScheduler
@@ -102,9 +150,36 @@ with sched.session("task-1") as s: # resumes automatically if interrupted b
102
150
  s.complete() # task done: drop the checkpoint
103
151
  ```
104
152
 
105
- `backend` is any callable `messages -> (reply, tokens_used)`; `telemetry` is any
106
- callable `() -> remaining_tokens`. See `examples/quickstart.py` for a runnable demo
107
- (no API keys needed) that suspends mid-task and resumes on the next run.
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.
108
183
 
109
184
  ## Real providers via LiteLLM
110
185
 
@@ -134,6 +209,31 @@ Rate-limit headers by provider (defaults target the OpenAI-style names):
134
209
 
135
210
  Header names differ? Override them: `LiteLLMAdapter(model=..., remaining_header="...", requests_header="...", reset_header="...")`.
136
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
+
137
237
  ## Beyond tokens: RPM and wait-vs-suspend
138
238
 
139
239
  Telemetry can be richer than a token count. `adapter.budget` reports all
@@ -253,6 +353,63 @@ unlike tokens, does not refill).
253
353
  Runnable demo without any key: `python examples/fleet_quickstart.py`
254
354
  (routing switch + predictive WAIT under a shared window, in 30 lines of loop).
255
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
+
256
413
  ## Why
257
414
 
258
415
  Current agent frameworks persist state *reactively*: they checkpoint after a step
@@ -282,15 +439,11 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
282
439
  | `adapters.openai_compat.OpenAICompatAdapter` | direct HTTP adapter for OpenAI-compatible APIs (Groq, OpenAI, …) — no litellm dependency |
283
440
  | `adapters.anthropic.AnthropicAdapter` | direct adapter for the Anthropic Messages API, with `cache_control` prompt caching and measured `cache_read/write_tokens` |
284
441
  | `adapters.langgraph.AgentPauseGuard` | predictive gate for LangGraph nodes: `check()` interrupts the graph before the fatal call, `record()` trains the estimator |
285
-
286
- ## Install (from source, during development)
287
-
288
- ```bash
289
- git clone https://github.com/<user>/agentpause
290
- cd agentpause
291
- pip install -e ".[dev]"
292
- pytest
293
- ```
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 |
294
447
 
295
448
  ## Roadmap
296
449
 
@@ -303,8 +456,12 @@ pytest
303
456
  - [x] Predictive multi-provider routing (`BudgetRouter`)
304
457
  - [x] Shared budget across agents (`MultiAgentCoordinator`)
305
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`)
306
463
  - [ ] CrewAI / AutoGen / LlamaIndex adapters
307
- - [ ] Optional KV-cache plugin for llama.cpp / vLLM (true warm start)
464
+ - [ ] KV-cache plugin for vLLM
308
465
 
309
466
  ## License
310
467
 
@@ -56,12 +56,60 @@ A 20-step live stress test (2026-07-10) closed the loop: at the §8.6 wall the
56
56
  scheduler suspended, compacted the checkpoint offline, resumed slim, and
57
57
  completed 20/20 steps with zero 429s.
58
58
 
59
- > Status: **0.3.0**. Core scheduler, direct + LiteLLM + Anthropic adapters,
59
+ **Facts vs. voice** (`python scripts/quality_ab.py`, live 2026-07-20,
60
+ Groq `llama-3.1-8b-instant` — the voice probe rides along on the same run,
61
+ no extra flag needed): does shrinking a checkpoint also flatten the
62
+ *style* it was written in, not just the facts inside it? A persona with two
63
+ short verbal tics is planted in the same conversation as the six facts above:
64
+
65
+ | condition | facts recalled | voice tics preserved |
66
+ |---|---|---|
67
+ | A — full history | **6/6** | 1/2 |
68
+ | B — `compact()` (blind truncation) | 0/6 | 1/2 |
69
+ | C — `summarize_with()` (one summary call) | **6/6** | **0/2** |
70
+
71
+ Truncation and summarization fail in opposite, complementary ways: blind
72
+ truncation keeps short, verbatim phrases intact (only long messages get cut)
73
+ but loses facts scattered earlier in the history; summarization recovers the
74
+ facts but rewrites everything in its own voice, so a persona's way of talking
75
+ does not survive being summarized even when what it *said* does. If tone and
76
+ persona consistency matter for your agent, `compact()` and `summarize_with()`
77
+ are not interchangeable — pick per what you can't afford to lose.
78
+
79
+ > Status: **0.4.0**. Core scheduler, direct + LiteLLM + Anthropic adapters,
60
80
  > LangGraph integration, multi-provider routing, multi-agent shared budgets,
61
- > feature-based cost/latency estimation, runnable examples, and a full test
62
- > suite are in place; the optional KV-cache plugin is next.
81
+ > feature-based cost/latency estimation, task-completion forecast, checkpoint
82
+ > fork & cross-machine migration, an optional true-warm-start KV-cache plugin
83
+ > for llama.cpp, human attention as a budget, runnable examples, and a full
84
+ > test suite are all in place.
85
+
86
+ ## Install
87
+
88
+ ```bash
89
+ pip install agentpause
90
+ ```
91
+
92
+ Optional extras, only if you use them (the core has zero dependencies):
93
+
94
+ ```bash
95
+ pip install "agentpause[litellm]" # LiteLLM adapter (100+ providers)
96
+ pip install "agentpause[langgraph]" # LangGraph adapter
97
+ ```
98
+
99
+ Developing on the library itself (clone + editable install + tests):
100
+
101
+ ```bash
102
+ git clone https://github.com/Champoleello/agentpause
103
+ cd agentpause
104
+ pip install -e ".[dev]"
105
+ pytest
106
+ ```
107
+
108
+ ## Getting started
63
109
 
64
- ## Quick example
110
+ **Step 1 — the smallest possible loop.** `backend` is any callable
111
+ `messages -> (reply, tokens_used)`; `telemetry` is any callable
112
+ `() -> remaining_tokens`. That's the entire contract — no framework required:
65
113
 
66
114
  ```python
67
115
  from agentpause import PredictiveScheduler
@@ -79,9 +127,36 @@ with sched.session("task-1") as s: # resumes automatically if interrupted b
79
127
  s.complete() # task done: drop the checkpoint
80
128
  ```
81
129
 
82
- `backend` is any callable `messages -> (reply, tokens_used)`; `telemetry` is any
83
- callable `() -> remaining_tokens`. See `examples/quickstart.py` for a runnable demo
84
- (no API keys needed) that suspends mid-task and resumes on the next run.
130
+ Run it now, no API key needed: `python examples/quickstart.py` it suspends
131
+ mid-task on the first run and resumes cleanly the second time you run it.
132
+
133
+ **Step 2 — the same three calls, wired into YOUR agent loop.** Whatever
134
+ framework you use (a hand-rolled ReAct loop, LangGraph, CrewAI, a plain
135
+ `while` loop), the integration is always the same three calls in the same
136
+ three places:
137
+
138
+ 1. Before every LLM call, ask `should_suspend()` (or `session.next_action()`
139
+ for the richer `continue`/`wait`/`checkpoint` answer). This is a local
140
+ computation — no network call, no cost.
141
+ 2. If it says stop, call `checkpoint()` and exit the process. Nothing else
142
+ to do: state, messages, and idempotency keys are already on disk.
143
+ 3. Re-running the same code with the same session id resumes automatically
144
+ from the exact step — `with sched.session(...)` handles the restore.
145
+
146
+ **Step 3 — pick the adapter for your provider**, which supplies `backend` and
147
+ `telemetry` for you instead of writing them by hand:
148
+
149
+ | You use... | Adapter | Runnable example |
150
+ |---|---|---|
151
+ | LiteLLM (100+ providers) | `adapters.litellm.LiteLLMAdapter` | `examples/litellm_groq.py` |
152
+ | Direct HTTP, OpenAI-compatible (Groq, OpenAI, ...) | `adapters.openai_compat.OpenAICompatAdapter` | `scripts/validate_provider.py` |
153
+ | Direct Anthropic Messages API | `adapters.anthropic.AnthropicAdapter` | see "Real providers" below |
154
+ | LangGraph | `adapters.langgraph.AgentPauseGuard` | `examples/langgraph_quickstart.py` |
155
+ | A CrewAI/AutoGen/custom loop, no adapter yet | none — write `backend`/`telemetry` by hand as in Step 1 | `examples/quickstart.py` |
156
+
157
+ Details and full code for each adapter are in the sections right below; the
158
+ `## Components` table further down is the complete function/class reference
159
+ for everything the library exposes.
85
160
 
86
161
  ## Real providers via LiteLLM
87
162
 
@@ -111,6 +186,31 @@ Rate-limit headers by provider (defaults target the OpenAI-style names):
111
186
 
112
187
  Header names differ? Override them: `LiteLLMAdapter(model=..., remaining_header="...", requests_header="...", reset_header="...")`.
113
188
 
189
+ ### Self-hosted: ollama-gateway
190
+
191
+ Running Ollama locally behind
192
+ [martinobettucci/ollama-gateway](https://github.com/martinobettucci/ollama-gateway)
193
+ (a self-hosted auth + quota proxy for Ollama)? It emits the same
194
+ OpenAI/Groq-style `x-ratelimit-*` headers agentpause already reads, so it
195
+ works out of the box through the direct adapter — no litellm, no
196
+ special-casing. Confirmed live end-to-end (2026-07-20) against a real
197
+ gateway instance: `adapter.budget()` correctly reads `remaining_tokens`,
198
+ `remaining_requests` (decrementing per call), and `reset_seconds` — **the
199
+ one condition is that the API key has a quota configured** (token cap and/or
200
+ rate limit); a key with no quota set simply emits no rate-limit headers at
201
+ all, which agentpause reports as a clear `TelemetryError` rather than a
202
+ silent wrong answer:
203
+
204
+ ```python
205
+ from agentpause.adapters.openai_compat import OpenAICompatAdapter
206
+
207
+ adapter = OpenAICompatAdapter.for_model("ollama-gateway/llama3:8b")
208
+ # default base_url is http://127.0.0.1:8787/v1 (dev default); override for
209
+ # a custom port or a production HTTPS domain:
210
+ # OpenAICompatAdapter.for_model("ollama-gateway/llama3:8b",
211
+ # base_url="https://your-gateway-domain")
212
+ ```
213
+
114
214
  ## Beyond tokens: RPM and wait-vs-suspend
115
215
 
116
216
  Telemetry can be richer than a token count. `adapter.budget` reports all
@@ -230,6 +330,63 @@ unlike tokens, does not refill).
230
330
  Runnable demo without any key: `python examples/fleet_quickstart.py`
231
331
  (routing switch + predictive WAIT under a shared window, in 30 lines of loop).
232
332
 
333
+ ## Plan before you spend, fork your past, and survive a reboot (v0.4)
334
+
335
+ Four pieces that treat a suspended checkpoint as what it actually is: inert
336
+ data you can inspect, clone, move, or accelerate.
337
+
338
+ ```python
339
+ from agentpause import HumanAttentionBudget, StateStore
340
+
341
+ # 1. FORECAST: before committing to a long run, simulate the REMAINING
342
+ # steps against the live window (no network calls) and get an honest
343
+ # estimate of tokens, money, waits/suspensions and wall-clock time.
344
+ forecast = session.forecast(steps_remaining=12)
345
+ print(forecast) # flags context_wall / fits_time_budget if either fails
346
+
347
+ # 2. FORK: one suspended past, N independent futures. Clones are fully
348
+ # independent (deep-copied messages, own idempotency namespace) but
349
+ # inherit the parent's calibrated estimator (F9.3 symmetry).
350
+ store = StateStore(".agentpause")
351
+ store.fork("research-task", "research-task-cautious")
352
+ store.fork("research-task", "research-task-bold")
353
+
354
+ # 3. MIGRATE: the checkpoint directory IS the process image. Export it on
355
+ # machine A, ship the bundle over any transport, resume on machine B at
356
+ # the exact step.
357
+ bundle = store.export_bundle("research-task")
358
+ StateStore("/mnt/machine-b/.agentpause").import_bundle(bundle)
359
+
360
+ # 4. The human in the loop is a rate-limited resource too: N questions per
361
+ # rolling hour, with a manual override for "I'm away".
362
+ attention = HumanAttentionBudget(max_asks=3, window_s=3600)
363
+ if not attention.ready():
364
+ session.checkpoint() # an absent human means suspend, not spin
365
+ ```
366
+
367
+ For **self-hosted llama.cpp**, checkpoints can go further than logical state:
368
+ `KVStateStore` wraps any `StateStore` and additionally saves/restores the
369
+ model's KV-cache via `/slots` — a TRUE warm start (no re-prefill at all, not
370
+ just no re-work). It degrades gracefully and automatically to a plain logical
371
+ warm start whenever the accelerator can't be trusted: a model fingerprint
372
+ mismatch, a missing blob, or a resume after `import_bundle` on a different
373
+ machine (KV blobs are intentionally machine-local and never migrate).
374
+
375
+ ```python
376
+ from agentpause.llamacpp_kv import LlamaCppSlots, KVStateStore
377
+
378
+ kv_store = KVStateStore(StateStore(".agentpause"),
379
+ slots=LlamaCppSlots(), base_url="http://127.0.0.1:8080")
380
+ kv_store.save_with_kv(checkpoint) # KV blob first, logical commit second
381
+ cp, info = kv_store.load_with_kv("research-task")
382
+ print(info) # {"kv_restored": True, "n_restored": 4096} — or a graceful
383
+ # {"kv_restored": False, "reason": "model_mismatch" | "kv_file_missing"}
384
+ ```
385
+
386
+ Runnable demos without any key or server: `python examples/migrate_fork.py`
387
+ (fork + migration story) and `python examples/kv_llamacpp_demo.py` (KV
388
+ save/restore, model-change degradation, independent forked blobs, migration).
389
+
233
390
  ## Why
234
391
 
235
392
  Current agent frameworks persist state *reactively*: they checkpoint after a step
@@ -259,15 +416,11 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
259
416
  | `adapters.openai_compat.OpenAICompatAdapter` | direct HTTP adapter for OpenAI-compatible APIs (Groq, OpenAI, …) — no litellm dependency |
260
417
  | `adapters.anthropic.AnthropicAdapter` | direct adapter for the Anthropic Messages API, with `cache_control` prompt caching and measured `cache_read/write_tokens` |
261
418
  | `adapters.langgraph.AgentPauseGuard` | predictive gate for LangGraph nodes: `check()` interrupts the graph before the fatal call, `record()` trains the estimator |
262
-
263
- ## Install (from source, during development)
264
-
265
- ```bash
266
- git clone https://github.com/<user>/agentpause
267
- cd agentpause
268
- pip install -e ".[dev]"
269
- pytest
270
- ```
419
+ | `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` |
420
+ | `Checkpoint.fork()` / `StateStore.fork()` | one suspended past, N independent futures — deep-copied state, collision-free idempotency, inherited estimator calibration |
421
+ | `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 |
422
+ | `HumanAttentionBudget` | the person in the loop as a rate-limited `Budget`: N asks per rolling window + manual `available()`/`away(until)` override; composes with `decide()` |
423
+ | `llamacpp_kv.LlamaCppSlots` / `KVStateStore` | optional plugin: TRUE warm start for self-hosted llama.cpp via KV-cache save/restore, transactional, with automatic graceful degradation |
271
424
 
272
425
  ## Roadmap
273
426
 
@@ -280,8 +433,12 @@ pytest
280
433
  - [x] Predictive multi-provider routing (`BudgetRouter`)
281
434
  - [x] Shared budget across agents (`MultiAgentCoordinator`)
282
435
  - [x] Feature-based cost & latency estimator (`FeatureEstimator`)
436
+ - [x] Task-completion forecast (`session.forecast()`)
437
+ - [x] Checkpoint fork & cross-machine migration
438
+ - [x] Optional KV-cache plugin for llama.cpp (true warm start), incl. fork+KV
439
+ - [x] Human attention as a rate-limited budget (`HumanAttentionBudget`)
283
440
  - [ ] CrewAI / AutoGen / LlamaIndex adapters
284
- - [ ] Optional KV-cache plugin for llama.cpp / vLLM (true warm start)
441
+ - [ ] KV-cache plugin for vLLM
285
442
 
286
443
  ## License
287
444
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentpause"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Predictive scheduling for autonomous LLM agents: suspend gracefully before rate limits, resume without losing work."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -5,10 +5,12 @@ it without redoing work. The core works on any provider (cloud or local); true
5
5
  KV-cache warm start is an optional plugin for self-hosted runtimes.
6
6
  """
7
7
 
8
+ from .attention import HumanAttentionBudget
8
9
  from .errors import (
9
10
  AgentPauseError,
10
11
  BackendError,
11
12
  CheckpointError,
13
+ KVError,
12
14
  RateLimitHit,
13
15
  TelemetryError,
14
16
  )
@@ -16,6 +18,8 @@ from .breaker import CircuitBreaker, CircuitOpenError
16
18
  from .estimator import Estimator
17
19
  from .regression import FeatureEstimator
18
20
  from .fallback import FallbackBackend
21
+ from .forecast import Forecast, forecast_run
22
+ from .llamacpp_kv import KVStateStore, LlamaCppSlots
19
23
  from .refill import RegimeDetector
20
24
  from .retry import RetryPolicy
21
25
  from .risk import Budget, Decision, RiskModel, decide, should_checkpoint
@@ -25,7 +29,7 @@ from .state import Checkpoint, StateStore
25
29
  from .tools import ToolQuota
26
30
  from .scheduler import PredictiveScheduler, Session
27
31
 
28
- __version__ = "0.3.0"
32
+ __version__ = "0.4.0"
29
33
 
30
34
  __all__ = [
31
35
  "PredictiveScheduler",
@@ -35,6 +39,8 @@ __all__ = [
35
39
  "RiskModel",
36
40
  "Budget",
37
41
  "Decision",
42
+ "Forecast",
43
+ "forecast_run",
38
44
  "decide",
39
45
  "should_checkpoint",
40
46
  "Checkpoint",
@@ -47,10 +53,14 @@ __all__ = [
47
53
  "CircuitOpenError",
48
54
  "RegimeDetector",
49
55
  "ToolQuota",
56
+ "HumanAttentionBudget",
57
+ "LlamaCppSlots",
58
+ "KVStateStore",
50
59
  "AgentPauseError",
51
60
  "RateLimitHit",
52
61
  "TelemetryError",
53
62
  "CheckpointError",
54
63
  "BackendError",
64
+ "KVError",
55
65
  "__version__",
56
66
  ]
@@ -15,6 +15,30 @@ scheduler needs are always there.
15
15
 
16
16
  Works with any provider exposing the OpenAI chat API: pass ``base_url`` and
17
17
  the env var holding the key, or use :meth:`for_model` for known prefixes.
18
+
19
+ Note on ``KNOWN_PROVIDERS["ollama-gateway"]``: unlike the cloud providers
20
+ above (Groq, OpenAI — one true hostname each), ollama-gateway
21
+ (github.com/martinobettucci/ollama-gateway) is SELF-HOSTED: it's an
22
+ auth/quota proxy operators run in front of their own Ollama instance, so
23
+ there is no single fixed public URL. The registered entry
24
+ (``http://127.0.0.1:8787/v1``) is only a LOCAL DEV DEFAULT, not a promise
25
+ about where your deployment lives. If yours runs on a different port or
26
+ behind a real HTTPS domain in production, override it directly instead of
27
+ relying on ``for_model``'s default:
28
+
29
+ adapter = OpenAICompatAdapter(
30
+ model="llama3:8b",
31
+ base_url="https://your-gateway-domain",
32
+ api_key_env="OLLAMA_GATEWAY_API_KEY",
33
+ )
34
+
35
+ or pass ``base_url=`` through ``for_model`` itself, since it forwards
36
+ ``**kwargs`` to the constructor and constructor kwargs win over the
37
+ registry default:
38
+
39
+ adapter = OpenAICompatAdapter.for_model(
40
+ "ollama-gateway/llama3:8b", base_url="https://your-gateway-domain",
41
+ )
18
42
  """
19
43
 
20
44
  from __future__ import annotations
@@ -33,6 +57,12 @@ __all__ = ["OpenAICompatAdapter"]
33
57
  KNOWN_PROVIDERS = {
34
58
  "groq": ("https://api.groq.com/openai/v1", "GROQ_API_KEY"),
35
59
  "openai": ("https://api.openai.com/v1", "OPENAI_API_KEY"),
60
+ # Self-hosted, NOT a fixed cloud endpoint: this is a LOCAL DEV DEFAULT
61
+ # for github.com/martinobettucci/ollama-gateway (an auth/quota proxy in
62
+ # front of a local Ollama), not a promise about where your instance
63
+ # lives. Override base_url= for a custom port or a production HTTPS
64
+ # domain — see the module docstring and for_model() for how.
65
+ "ollama-gateway": ("http://127.0.0.1:8787/v1", "OLLAMA_GATEWAY_API_KEY"),
36
66
  }
37
67
 
38
68
 
@@ -98,13 +128,27 @@ class OpenAICompatAdapter:
98
128
  @classmethod
99
129
  def for_model(cls, model: str, **kwargs: Any) -> "OpenAICompatAdapter":
100
130
  """Build from a prefixed model string: ``groq/...``, ``openai/...``,
101
- or a bare OpenAI model name like ``gpt-4o-mini``."""
131
+ ``ollama-gateway/...``, or a bare OpenAI model name like ``gpt-4o-mini``.
132
+
133
+ ``base_url`` and/or ``api_key_env`` passed in ``**kwargs`` override
134
+ the registry default for the matched provider (``setdefault``, not a
135
+ hard-coded kwarg), so ``for_model`` never raises a duplicate-keyword
136
+ error when a caller supplies them. This matters most for self-hosted
137
+ entries like ``ollama-gateway``: the registry only holds a local dev
138
+ default (``http://127.0.0.1:8787/v1``), and a real deployment behind
139
+ a custom port or an HTTPS domain overrides it with
140
+ ``for_model("ollama-gateway/llama3:8b", base_url="https://...")``.
141
+ """
102
142
  prefix, _, rest = model.partition("/")
103
143
  if rest and prefix in KNOWN_PROVIDERS:
104
144
  base_url, env = KNOWN_PROVIDERS[prefix]
105
- return cls(rest, base_url=base_url, api_key_env=env, **kwargs)
145
+ kwargs.setdefault("base_url", base_url)
146
+ kwargs.setdefault("api_key_env", env)
147
+ return cls(rest, **kwargs)
106
148
  base_url, env = KNOWN_PROVIDERS["openai"]
107
- return cls(model, base_url=base_url, api_key_env=env, **kwargs)
149
+ kwargs.setdefault("base_url", base_url)
150
+ kwargs.setdefault("api_key_env", env)
151
+ return cls(model, **kwargs)
108
152
 
109
153
  # -- the callables the scheduler wants -------------------------------------
110
154