agentpause 0.2.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.2.0/LICENSE +21 -0
- agentpause-0.2.0/PKG-INFO +213 -0
- agentpause-0.2.0/README.md +190 -0
- agentpause-0.2.0/pyproject.toml +33 -0
- agentpause-0.2.0/setup.cfg +4 -0
- agentpause-0.2.0/src/agentpause/__init__.py +46 -0
- agentpause-0.2.0/src/agentpause/adapters/__init__.py +8 -0
- agentpause-0.2.0/src/agentpause/adapters/langgraph.py +205 -0
- agentpause-0.2.0/src/agentpause/adapters/litellm.py +338 -0
- agentpause-0.2.0/src/agentpause/breaker.py +88 -0
- agentpause-0.2.0/src/agentpause/errors.py +66 -0
- agentpause-0.2.0/src/agentpause/estimator.py +106 -0
- agentpause-0.2.0/src/agentpause/fallback.py +74 -0
- agentpause-0.2.0/src/agentpause/retry.py +57 -0
- agentpause-0.2.0/src/agentpause/risk.py +138 -0
- agentpause-0.2.0/src/agentpause/scheduler.py +336 -0
- agentpause-0.2.0/src/agentpause/state.py +99 -0
- agentpause-0.2.0/src/agentpause.egg-info/PKG-INFO +213 -0
- agentpause-0.2.0/src/agentpause.egg-info/SOURCES.txt +34 -0
- agentpause-0.2.0/src/agentpause.egg-info/dependency_links.txt +1 -0
- agentpause-0.2.0/src/agentpause.egg-info/requires.txt +9 -0
- agentpause-0.2.0/src/agentpause.egg-info/top_level.txt +1 -0
- agentpause-0.2.0/tests/test_async.py +198 -0
- agentpause-0.2.0/tests/test_breaker.py +121 -0
- agentpause-0.2.0/tests/test_estimator.py +63 -0
- agentpause-0.2.0/tests/test_fallback.py +92 -0
- agentpause-0.2.0/tests/test_io_tpm.py +128 -0
- agentpause-0.2.0/tests/test_langgraph_adapter.py +170 -0
- agentpause-0.2.0/tests/test_litellm_adapter.py +160 -0
- agentpause-0.2.0/tests/test_money_and_hooks.py +138 -0
- agentpause-0.2.0/tests/test_quantile.py +65 -0
- agentpause-0.2.0/tests/test_risk.py +45 -0
- agentpause-0.2.0/tests/test_robustness.py +242 -0
- agentpause-0.2.0/tests/test_scheduler.py +120 -0
- agentpause-0.2.0/tests/test_state.py +55 -0
- agentpause-0.2.0/tests/test_wait_and_rpm.py +201 -0
agentpause-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrea Ciampolillo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentpause
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Predictive scheduling for autonomous LLM agents: suspend gracefully before rate limits, resume without losing work.
|
|
5
|
+
Author: Andrea Lelio Ciampolillo
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: llm,agents,rate-limiting,checkpointing,scheduler
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Provides-Extra: litellm
|
|
19
|
+
Requires-Dist: litellm>=1.40; extra == "litellm"
|
|
20
|
+
Provides-Extra: langgraph
|
|
21
|
+
Requires-Dist: langgraph>=0.2; extra == "langgraph"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# agentpause
|
|
25
|
+
|
|
26
|
+
**Predictive scheduling for autonomous LLM agents.** Suspend an agent gracefully
|
|
27
|
+
*before* it hits a provider rate limit, and resume it without redoing work.
|
|
28
|
+
|
|
29
|
+
The core works on any provider — cloud (OpenAI, Anthropic, Groq) or local — because
|
|
30
|
+
it only serializes application-level state. True KV-cache warm start is an optional
|
|
31
|
+
plugin for self-hosted runtimes (llama.cpp, vLLM).
|
|
32
|
+
|
|
33
|
+
## Measured results (from the accompanying research)
|
|
34
|
+
|
|
35
|
+
| Experiment | Result |
|
|
36
|
+
|---|---|
|
|
37
|
+
| Simulation, 300 runs/config | reactive baseline crashes up to 100% → predictive **0%** (k=4), token waste −80% |
|
|
38
|
+
| Real provider (Groq free, 6K TPM) | multi-window task completed with **zero 429 errors** |
|
|
39
|
+
| vs LangGraph's MemorySaver (200 runs) | LangGraph: 7.8 × 429/run, 9,239 tokens wasted; predictive: **0 and 0** |
|
|
40
|
+
| End-to-end A/B (thinking models, 4B/8B) | recovery **54×–93× faster**; total task time −19% |
|
|
41
|
+
|
|
42
|
+
Reproduce them yourself with a free key: `python scripts/benchmark_groq.py`.
|
|
43
|
+
|
|
44
|
+
> Status: **early alpha (0.1)**. Core components, the high-level
|
|
45
|
+
> `PredictiveScheduler` API, the LiteLLM adapter (any provider), the LangGraph
|
|
46
|
+
> adapter, runnable examples, and a full test suite are in place; the optional
|
|
47
|
+
> KV-cache plugin is next.
|
|
48
|
+
|
|
49
|
+
## Quick example
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from agentpause import PredictiveScheduler
|
|
53
|
+
|
|
54
|
+
sched = PredictiveScheduler(backend=my_llm_call, telemetry=my_rate_limit_reader)
|
|
55
|
+
|
|
56
|
+
with sched.session("task-1") as s: # resumes automatically if interrupted before
|
|
57
|
+
for question in questions[s.step:]: # skip steps finished before a suspend
|
|
58
|
+
s.add_user(question)
|
|
59
|
+
if s.should_suspend(): # predictive check, *before* the call
|
|
60
|
+
s.checkpoint()
|
|
61
|
+
break # stop cleanly; rerun to resume
|
|
62
|
+
reply = s.call()
|
|
63
|
+
else:
|
|
64
|
+
s.complete() # task done: drop the checkpoint
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`backend` is any callable `messages -> (reply, tokens_used)`; `telemetry` is any
|
|
68
|
+
callable `() -> remaining_tokens`. See `examples/quickstart.py` for a runnable demo
|
|
69
|
+
(no API keys needed) that suspends mid-task and resumes on the next run.
|
|
70
|
+
|
|
71
|
+
## Real providers via LiteLLM
|
|
72
|
+
|
|
73
|
+
The LiteLLM adapter supplies both callables for 100+ providers (OpenAI, Groq,
|
|
74
|
+
Anthropic, local servers, ...), reading the budget from each response's
|
|
75
|
+
rate-limit headers and refreshing stale readings with a tiny telemetry ping:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from agentpause import PredictiveScheduler
|
|
79
|
+
from agentpause.adapters.litellm import LiteLLMAdapter
|
|
80
|
+
|
|
81
|
+
adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
|
|
82
|
+
sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.telemetry)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Install with `pip install -e ".[litellm]"`; see `examples/litellm_groq.py`.
|
|
86
|
+
To validate against your own provider (frontier models included):
|
|
87
|
+
`python scripts/validate_provider.py gpt-4o-mini`.
|
|
88
|
+
|
|
89
|
+
Rate-limit headers by provider (defaults target the OpenAI-style names):
|
|
90
|
+
|
|
91
|
+
| Provider | remaining tokens | remaining requests | reset |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| OpenAI | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`1s`, `6m0s`) |
|
|
94
|
+
| Groq | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`7.66s`, `2m59.56s`) |
|
|
95
|
+
| Anthropic | `anthropic-ratelimit-tokens-remaining` | `anthropic-ratelimit-requests-remaining` | RFC 3339 timestamp — set `remaining_header=` etc. |
|
|
96
|
+
|
|
97
|
+
Header names differ? Override them: `LiteLLMAdapter(model=..., remaining_header="...", requests_header="...", reset_header="...")`.
|
|
98
|
+
|
|
99
|
+
## Beyond tokens: RPM and wait-vs-suspend
|
|
100
|
+
|
|
101
|
+
Telemetry can be richer than a token count. `adapter.budget` reports all
|
|
102
|
+
three dimensions providers expose — remaining tokens (TPM), remaining
|
|
103
|
+
requests (RPM), and seconds until the window resets — and unlocks the
|
|
104
|
+
three-valued decision:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.budget)
|
|
108
|
+
|
|
109
|
+
d = session.next_action() # "continue" | "wait" | "checkpoint"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`wait` fires when the budget does not fit **but** the window resets within
|
|
113
|
+
`wait_threshold_s` (default 15 s): a short in-place pause is cheaper than a
|
|
114
|
+
full suspend/resume cycle. Exhausted requests (RPM = 0) block the call even
|
|
115
|
+
with plenty of tokens left. Plain-int telemetry keeps working unchanged.
|
|
116
|
+
|
|
117
|
+
## Async
|
|
118
|
+
|
|
119
|
+
Every entry point has an async twin — same rules, same guarantees, never
|
|
120
|
+
blocks the event loop:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
|
|
124
|
+
sched = PredictiveScheduler(backend=None, async_backend=adapter.abackend,
|
|
125
|
+
telemetry=adapter.telemetry)
|
|
126
|
+
reply = await session.acall() # retry/backoff via asyncio.sleep
|
|
127
|
+
await guard.acheck(state["messages"]) # async LangGraph nodes
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For sharper input estimates, wire the per-model tokenizer:
|
|
131
|
+
`PredictiveScheduler(..., count_tokens=adapter.count_tokens)` (falls back to
|
|
132
|
+
the ~4 chars/token heuristic if the tokenizer is unavailable).
|
|
133
|
+
|
|
134
|
+
## When prediction fails anyway
|
|
135
|
+
|
|
136
|
+
Estimates are statistical — a 429 can still slip through. agentpause survives
|
|
137
|
+
it instead of crashing:
|
|
138
|
+
|
|
139
|
+
- **typed errors**: everything derives from `AgentPauseError`
|
|
140
|
+
(`RateLimitHit`, `TelemetryError`, `CheckpointError`, `BackendError`);
|
|
141
|
+
- **retry with backoff**: unexpected 429s are retried (provider `retry-after`
|
|
142
|
+
honored, else exponential backoff — see `RetryPolicy`);
|
|
143
|
+
- **hits are feedback**: each one bumps `safety_k` up (capped at `k_max`),
|
|
144
|
+
so the scheduler grows more cautious on workloads it underestimates;
|
|
145
|
+
- **clean failure**: a failed call leaves the session state untouched —
|
|
146
|
+
no phantom steps, resumes stay consistent.
|
|
147
|
+
|
|
148
|
+
## LangGraph integration
|
|
149
|
+
|
|
150
|
+
`AgentPauseGuard` adds the predictive gate to any LangGraph node — LangGraph
|
|
151
|
+
persists reactively (after nodes), the guard pauses *before* the call that
|
|
152
|
+
would hit the rate limit, via LangGraph's own `interrupt()` + checkpointer:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from agentpause.adapters.langgraph import AgentPauseGuard
|
|
156
|
+
|
|
157
|
+
guard = AgentPauseGuard(telemetry=adapter.telemetry) # e.g. from LiteLLMAdapter
|
|
158
|
+
|
|
159
|
+
def agent_node(state):
|
|
160
|
+
guard.check(state["messages"]) # pauses the graph here if needed
|
|
161
|
+
reply = llm.invoke(state["messages"])
|
|
162
|
+
guard.record(state["messages"], reply.usage_metadata["total_tokens"])
|
|
163
|
+
...
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Resume the paused thread with `graph.invoke(Command(resume=True), config)`.
|
|
167
|
+
On resume the guard re-reads telemetry fresh — never from the checkpoint.
|
|
168
|
+
Install with `pip install -e ".[langgraph]"`; see `examples/langgraph_quickstart.py`.
|
|
169
|
+
|
|
170
|
+
## Why
|
|
171
|
+
|
|
172
|
+
Current agent frameworks persist state *reactively*: they checkpoint after a step
|
|
173
|
+
completes and crash when the provider returns HTTP 429. `agentpause` adds a
|
|
174
|
+
*predictive* layer that estimates the next step's cost, compares it against the
|
|
175
|
+
remaining rate-limit budget, and suspends cleanly before the error — then resumes
|
|
176
|
+
from the exact step.
|
|
177
|
+
|
|
178
|
+
This library is the engineering counterpart of the research preprint *"A
|
|
179
|
+
Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
180
|
+
|
|
181
|
+
## Components (v0.1)
|
|
182
|
+
|
|
183
|
+
| Module | Role |
|
|
184
|
+
|--------|------|
|
|
185
|
+
| `PredictiveScheduler` / `Session` | the high-level API: `session()`, `should_suspend()`, `call()`, `checkpoint()` |
|
|
186
|
+
| `Estimator` | predicts next-step token cost with a moving-average error correction (ε) and tracks σ |
|
|
187
|
+
| `should_checkpoint` / `RiskModel` | the suspension rule (`remaining < estimated + k·σ`) and a diagnostic risk score |
|
|
188
|
+
| `Budget` / `decide` | three-dimensional telemetry (TPM, RPM, reset time) and the three-valued rule: continue / wait / checkpoint |
|
|
189
|
+
| `StateStore` / `Checkpoint` | atomic logical checkpointing with idempotency keys — works on any provider |
|
|
190
|
+
| `adapters.litellm.LiteLLMAdapter` | backend + telemetry for any LiteLLM-supported provider (headers → budget, stale reading → 1-token ping) |
|
|
191
|
+
| `adapters.langgraph.AgentPauseGuard` | predictive gate for LangGraph nodes: `check()` interrupts the graph before the fatal call, `record()` trains the estimator |
|
|
192
|
+
|
|
193
|
+
## Install (from source, during development)
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
git clone https://github.com/<user>/agentpause
|
|
197
|
+
cd agentpause
|
|
198
|
+
pip install -e ".[dev]"
|
|
199
|
+
pytest
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Roadmap
|
|
203
|
+
|
|
204
|
+
- [x] Core components + test suite
|
|
205
|
+
- [x] `PredictiveScheduler` high-level API (`session()`, `should_suspend()`, `call()`)
|
|
206
|
+
- [x] Runnable quickstart example (no keys)
|
|
207
|
+
- [x] LiteLLM adapter (works with any provider)
|
|
208
|
+
- [x] LangGraph adapter (interrupt + checkpointer)
|
|
209
|
+
- [ ] Optional KV-cache plugin for llama.cpp / vLLM (true warm start)
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# agentpause
|
|
2
|
+
|
|
3
|
+
**Predictive scheduling for autonomous LLM agents.** Suspend an agent gracefully
|
|
4
|
+
*before* it hits a provider rate limit, and resume it without redoing work.
|
|
5
|
+
|
|
6
|
+
The core works on any provider — cloud (OpenAI, Anthropic, Groq) or local — because
|
|
7
|
+
it only serializes application-level state. True KV-cache warm start is an optional
|
|
8
|
+
plugin for self-hosted runtimes (llama.cpp, vLLM).
|
|
9
|
+
|
|
10
|
+
## Measured results (from the accompanying research)
|
|
11
|
+
|
|
12
|
+
| Experiment | Result |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Simulation, 300 runs/config | reactive baseline crashes up to 100% → predictive **0%** (k=4), token waste −80% |
|
|
15
|
+
| Real provider (Groq free, 6K TPM) | multi-window task completed with **zero 429 errors** |
|
|
16
|
+
| vs LangGraph's MemorySaver (200 runs) | LangGraph: 7.8 × 429/run, 9,239 tokens wasted; predictive: **0 and 0** |
|
|
17
|
+
| End-to-end A/B (thinking models, 4B/8B) | recovery **54×–93× faster**; total task time −19% |
|
|
18
|
+
|
|
19
|
+
Reproduce them yourself with a free key: `python scripts/benchmark_groq.py`.
|
|
20
|
+
|
|
21
|
+
> Status: **early alpha (0.1)**. Core components, the high-level
|
|
22
|
+
> `PredictiveScheduler` API, the LiteLLM adapter (any provider), the LangGraph
|
|
23
|
+
> adapter, runnable examples, and a full test suite are in place; the optional
|
|
24
|
+
> KV-cache plugin is next.
|
|
25
|
+
|
|
26
|
+
## Quick example
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from agentpause import PredictiveScheduler
|
|
30
|
+
|
|
31
|
+
sched = PredictiveScheduler(backend=my_llm_call, telemetry=my_rate_limit_reader)
|
|
32
|
+
|
|
33
|
+
with sched.session("task-1") as s: # resumes automatically if interrupted before
|
|
34
|
+
for question in questions[s.step:]: # skip steps finished before a suspend
|
|
35
|
+
s.add_user(question)
|
|
36
|
+
if s.should_suspend(): # predictive check, *before* the call
|
|
37
|
+
s.checkpoint()
|
|
38
|
+
break # stop cleanly; rerun to resume
|
|
39
|
+
reply = s.call()
|
|
40
|
+
else:
|
|
41
|
+
s.complete() # task done: drop the checkpoint
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`backend` is any callable `messages -> (reply, tokens_used)`; `telemetry` is any
|
|
45
|
+
callable `() -> remaining_tokens`. See `examples/quickstart.py` for a runnable demo
|
|
46
|
+
(no API keys needed) that suspends mid-task and resumes on the next run.
|
|
47
|
+
|
|
48
|
+
## Real providers via LiteLLM
|
|
49
|
+
|
|
50
|
+
The LiteLLM adapter supplies both callables for 100+ providers (OpenAI, Groq,
|
|
51
|
+
Anthropic, local servers, ...), reading the budget from each response's
|
|
52
|
+
rate-limit headers and refreshing stale readings with a tiny telemetry ping:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from agentpause import PredictiveScheduler
|
|
56
|
+
from agentpause.adapters.litellm import LiteLLMAdapter
|
|
57
|
+
|
|
58
|
+
adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
|
|
59
|
+
sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.telemetry)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Install with `pip install -e ".[litellm]"`; see `examples/litellm_groq.py`.
|
|
63
|
+
To validate against your own provider (frontier models included):
|
|
64
|
+
`python scripts/validate_provider.py gpt-4o-mini`.
|
|
65
|
+
|
|
66
|
+
Rate-limit headers by provider (defaults target the OpenAI-style names):
|
|
67
|
+
|
|
68
|
+
| Provider | remaining tokens | remaining requests | reset |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| OpenAI | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`1s`, `6m0s`) |
|
|
71
|
+
| Groq | `x-ratelimit-remaining-tokens` | `x-ratelimit-remaining-requests` | `x-ratelimit-reset-tokens` (`7.66s`, `2m59.56s`) |
|
|
72
|
+
| Anthropic | `anthropic-ratelimit-tokens-remaining` | `anthropic-ratelimit-requests-remaining` | RFC 3339 timestamp — set `remaining_header=` etc. |
|
|
73
|
+
|
|
74
|
+
Header names differ? Override them: `LiteLLMAdapter(model=..., remaining_header="...", requests_header="...", reset_header="...")`.
|
|
75
|
+
|
|
76
|
+
## Beyond tokens: RPM and wait-vs-suspend
|
|
77
|
+
|
|
78
|
+
Telemetry can be richer than a token count. `adapter.budget` reports all
|
|
79
|
+
three dimensions providers expose — remaining tokens (TPM), remaining
|
|
80
|
+
requests (RPM), and seconds until the window resets — and unlocks the
|
|
81
|
+
three-valued decision:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
sched = PredictiveScheduler(backend=adapter.backend, telemetry=adapter.budget)
|
|
85
|
+
|
|
86
|
+
d = session.next_action() # "continue" | "wait" | "checkpoint"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`wait` fires when the budget does not fit **but** the window resets within
|
|
90
|
+
`wait_threshold_s` (default 15 s): a short in-place pause is cheaper than a
|
|
91
|
+
full suspend/resume cycle. Exhausted requests (RPM = 0) block the call even
|
|
92
|
+
with plenty of tokens left. Plain-int telemetry keeps working unchanged.
|
|
93
|
+
|
|
94
|
+
## Async
|
|
95
|
+
|
|
96
|
+
Every entry point has an async twin — same rules, same guarantees, never
|
|
97
|
+
blocks the event loop:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
adapter = LiteLLMAdapter(model="groq/llama-3.1-8b-instant")
|
|
101
|
+
sched = PredictiveScheduler(backend=None, async_backend=adapter.abackend,
|
|
102
|
+
telemetry=adapter.telemetry)
|
|
103
|
+
reply = await session.acall() # retry/backoff via asyncio.sleep
|
|
104
|
+
await guard.acheck(state["messages"]) # async LangGraph nodes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For sharper input estimates, wire the per-model tokenizer:
|
|
108
|
+
`PredictiveScheduler(..., count_tokens=adapter.count_tokens)` (falls back to
|
|
109
|
+
the ~4 chars/token heuristic if the tokenizer is unavailable).
|
|
110
|
+
|
|
111
|
+
## When prediction fails anyway
|
|
112
|
+
|
|
113
|
+
Estimates are statistical — a 429 can still slip through. agentpause survives
|
|
114
|
+
it instead of crashing:
|
|
115
|
+
|
|
116
|
+
- **typed errors**: everything derives from `AgentPauseError`
|
|
117
|
+
(`RateLimitHit`, `TelemetryError`, `CheckpointError`, `BackendError`);
|
|
118
|
+
- **retry with backoff**: unexpected 429s are retried (provider `retry-after`
|
|
119
|
+
honored, else exponential backoff — see `RetryPolicy`);
|
|
120
|
+
- **hits are feedback**: each one bumps `safety_k` up (capped at `k_max`),
|
|
121
|
+
so the scheduler grows more cautious on workloads it underestimates;
|
|
122
|
+
- **clean failure**: a failed call leaves the session state untouched —
|
|
123
|
+
no phantom steps, resumes stay consistent.
|
|
124
|
+
|
|
125
|
+
## LangGraph integration
|
|
126
|
+
|
|
127
|
+
`AgentPauseGuard` adds the predictive gate to any LangGraph node — LangGraph
|
|
128
|
+
persists reactively (after nodes), the guard pauses *before* the call that
|
|
129
|
+
would hit the rate limit, via LangGraph's own `interrupt()` + checkpointer:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from agentpause.adapters.langgraph import AgentPauseGuard
|
|
133
|
+
|
|
134
|
+
guard = AgentPauseGuard(telemetry=adapter.telemetry) # e.g. from LiteLLMAdapter
|
|
135
|
+
|
|
136
|
+
def agent_node(state):
|
|
137
|
+
guard.check(state["messages"]) # pauses the graph here if needed
|
|
138
|
+
reply = llm.invoke(state["messages"])
|
|
139
|
+
guard.record(state["messages"], reply.usage_metadata["total_tokens"])
|
|
140
|
+
...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Resume the paused thread with `graph.invoke(Command(resume=True), config)`.
|
|
144
|
+
On resume the guard re-reads telemetry fresh — never from the checkpoint.
|
|
145
|
+
Install with `pip install -e ".[langgraph]"`; see `examples/langgraph_quickstart.py`.
|
|
146
|
+
|
|
147
|
+
## Why
|
|
148
|
+
|
|
149
|
+
Current agent frameworks persist state *reactively*: they checkpoint after a step
|
|
150
|
+
completes and crash when the provider returns HTTP 429. `agentpause` adds a
|
|
151
|
+
*predictive* layer that estimates the next step's cost, compares it against the
|
|
152
|
+
remaining rate-limit budget, and suspends cleanly before the error — then resumes
|
|
153
|
+
from the exact step.
|
|
154
|
+
|
|
155
|
+
This library is the engineering counterpart of the research preprint *"A
|
|
156
|
+
Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
157
|
+
|
|
158
|
+
## Components (v0.1)
|
|
159
|
+
|
|
160
|
+
| Module | Role |
|
|
161
|
+
|--------|------|
|
|
162
|
+
| `PredictiveScheduler` / `Session` | the high-level API: `session()`, `should_suspend()`, `call()`, `checkpoint()` |
|
|
163
|
+
| `Estimator` | predicts next-step token cost with a moving-average error correction (ε) and tracks σ |
|
|
164
|
+
| `should_checkpoint` / `RiskModel` | the suspension rule (`remaining < estimated + k·σ`) and a diagnostic risk score |
|
|
165
|
+
| `Budget` / `decide` | three-dimensional telemetry (TPM, RPM, reset time) and the three-valued rule: continue / wait / checkpoint |
|
|
166
|
+
| `StateStore` / `Checkpoint` | atomic logical checkpointing with idempotency keys — works on any provider |
|
|
167
|
+
| `adapters.litellm.LiteLLMAdapter` | backend + telemetry for any LiteLLM-supported provider (headers → budget, stale reading → 1-token ping) |
|
|
168
|
+
| `adapters.langgraph.AgentPauseGuard` | predictive gate for LangGraph nodes: `check()` interrupts the graph before the fatal call, `record()` trains the estimator |
|
|
169
|
+
|
|
170
|
+
## Install (from source, during development)
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
git clone https://github.com/<user>/agentpause
|
|
174
|
+
cd agentpause
|
|
175
|
+
pip install -e ".[dev]"
|
|
176
|
+
pytest
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Roadmap
|
|
180
|
+
|
|
181
|
+
- [x] Core components + test suite
|
|
182
|
+
- [x] `PredictiveScheduler` high-level API (`session()`, `should_suspend()`, `call()`)
|
|
183
|
+
- [x] Runnable quickstart example (no keys)
|
|
184
|
+
- [x] LiteLLM adapter (works with any provider)
|
|
185
|
+
- [x] LangGraph adapter (interrupt + checkpointer)
|
|
186
|
+
- [ ] Optional KV-cache plugin for llama.cpp / vLLM (true warm start)
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentpause"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Predictive scheduling for autonomous LLM agents: suspend gracefully before rate limits, resume without losing work."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Andrea Lelio Ciampolillo" }]
|
|
13
|
+
keywords = ["llm", "agents", "rate-limiting", "checkpointing", "scheduler"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest>=7.0"]
|
|
25
|
+
litellm = ["litellm>=1.40"]
|
|
26
|
+
langgraph = ["langgraph>=0.2"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
pythonpath = ["src", "."]
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""agentpause — predictive scheduling for autonomous LLM agents.
|
|
2
|
+
|
|
3
|
+
Suspend an agent gracefully *before* it hits a provider rate limit, and resume
|
|
4
|
+
it without redoing work. The core works on any provider (cloud or local); true
|
|
5
|
+
KV-cache warm start is an optional plugin for self-hosted runtimes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .errors import (
|
|
9
|
+
AgentPauseError,
|
|
10
|
+
BackendError,
|
|
11
|
+
CheckpointError,
|
|
12
|
+
RateLimitHit,
|
|
13
|
+
TelemetryError,
|
|
14
|
+
)
|
|
15
|
+
from .breaker import CircuitBreaker, CircuitOpenError
|
|
16
|
+
from .estimator import Estimator
|
|
17
|
+
from .fallback import FallbackBackend
|
|
18
|
+
from .retry import RetryPolicy
|
|
19
|
+
from .risk import Budget, Decision, RiskModel, decide, should_checkpoint
|
|
20
|
+
from .state import Checkpoint, StateStore
|
|
21
|
+
from .scheduler import PredictiveScheduler, Session
|
|
22
|
+
|
|
23
|
+
__version__ = "0.2.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"PredictiveScheduler",
|
|
27
|
+
"Session",
|
|
28
|
+
"Estimator",
|
|
29
|
+
"RiskModel",
|
|
30
|
+
"Budget",
|
|
31
|
+
"Decision",
|
|
32
|
+
"decide",
|
|
33
|
+
"should_checkpoint",
|
|
34
|
+
"Checkpoint",
|
|
35
|
+
"StateStore",
|
|
36
|
+
"RetryPolicy",
|
|
37
|
+
"FallbackBackend",
|
|
38
|
+
"CircuitBreaker",
|
|
39
|
+
"CircuitOpenError",
|
|
40
|
+
"AgentPauseError",
|
|
41
|
+
"RateLimitHit",
|
|
42
|
+
"TelemetryError",
|
|
43
|
+
"CheckpointError",
|
|
44
|
+
"BackendError",
|
|
45
|
+
"__version__",
|
|
46
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Provider adapters: turn real LLM providers into the two callables
|
|
2
|
+
(backend, telemetry) that :class:`agentpause.PredictiveScheduler` expects.
|
|
3
|
+
|
|
4
|
+
Adapters are optional — the core has zero dependencies. Import them
|
|
5
|
+
explicitly, e.g.::
|
|
6
|
+
|
|
7
|
+
from agentpause.adapters.litellm import LiteLLMAdapter
|
|
8
|
+
"""
|