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.
- {agentpause-0.3.0/src/agentpause.egg-info → agentpause-0.4.0}/PKG-INFO +175 -18
- {agentpause-0.3.0 → agentpause-0.4.0}/README.md +174 -17
- {agentpause-0.3.0 → agentpause-0.4.0}/pyproject.toml +1 -1
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/__init__.py +11 -1
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/openai_compat.py +47 -3
- agentpause-0.4.0/src/agentpause/attention.py +196 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/errors.py +15 -0
- agentpause-0.4.0/src/agentpause/forecast.py +183 -0
- agentpause-0.4.0/src/agentpause/llamacpp_kv.py +403 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/scheduler.py +34 -0
- agentpause-0.4.0/src/agentpause/state.py +281 -0
- {agentpause-0.3.0 → agentpause-0.4.0/src/agentpause.egg-info}/PKG-INFO +175 -18
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/SOURCES.txt +7 -0
- agentpause-0.4.0/tests/test_attention.py +132 -0
- agentpause-0.4.0/tests/test_forecast.py +130 -0
- agentpause-0.4.0/tests/test_fork_migrate.py +210 -0
- agentpause-0.4.0/tests/test_llamacpp_kv.py +278 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_openai_compat.py +60 -0
- agentpause-0.3.0/src/agentpause/state.py +0 -146
- {agentpause-0.3.0 → agentpause-0.4.0}/LICENSE +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/setup.cfg +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/__init__.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/anthropic.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/langgraph.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/adapters/litellm.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/breaker.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/coordinator.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/estimator.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/fallback.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/refill.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/regression.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/retry.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/risk.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/router.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause/tools.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/dependency_links.txt +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/requires.txt +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/src/agentpause.egg-info/top_level.txt +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_anthropic_adapter.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_async.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_breaker.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_coordinator.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_estimator.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_estimator_evolution.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_fallback.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_field_adoptions.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_io_tpm.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_langgraph_adapter.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_latency_budget.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_litellm_adapter.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_money_and_hooks.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_quantile.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_refill_wait.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_regime.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_regression.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_risk.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_robustness.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_router.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_scheduler.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_smart_context.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_state.py +0 -0
- {agentpause-0.3.0 → agentpause-0.4.0}/tests/test_tool_quota.py +0 -0
- {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
|
+
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
|
-
|
|
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,
|
|
85
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
- [ ]
|
|
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
|
-
|
|
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,
|
|
62
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
- [ ]
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|