agentpause 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentpause-0.4.0/src/agentpause.egg-info → agentpause-0.5.0}/PKG-INFO +197 -8
- {agentpause-0.4.0 → agentpause-0.5.0}/README.md +194 -7
- {agentpause-0.4.0 → agentpause-0.5.0}/pyproject.toml +2 -1
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/__init__.py +3 -1
- agentpause-0.5.0/src/agentpause/adapters/local_resources.py +1429 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/errors.py +21 -0
- agentpause-0.5.0/src/agentpause/fleet.py +214 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/llamacpp_kv.py +186 -6
- {agentpause-0.4.0 → agentpause-0.5.0/src/agentpause.egg-info}/PKG-INFO +197 -8
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/SOURCES.txt +4 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/requires.txt +3 -0
- agentpause-0.5.0/tests/test_fleet.py +164 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_llamacpp_kv.py +261 -1
- agentpause-0.5.0/tests/test_local_resources.py +1177 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/LICENSE +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/setup.cfg +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/__init__.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/anthropic.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/langgraph.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/litellm.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/openai_compat.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/attention.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/breaker.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/coordinator.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/estimator.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/fallback.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/forecast.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/refill.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/regression.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/retry.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/risk.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/router.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/scheduler.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/state.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/tools.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/dependency_links.txt +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/top_level.txt +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_anthropic_adapter.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_async.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_attention.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_breaker.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_coordinator.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_estimator.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_estimator_evolution.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_fallback.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_field_adoptions.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_forecast.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_fork_migrate.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_io_tpm.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_langgraph_adapter.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_latency_budget.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_litellm_adapter.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_money_and_hooks.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_openai_compat.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_quantile.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_refill_wait.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_regime.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_regression.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_risk.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_robustness.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_router.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_scheduler.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_smart_context.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_state.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_tool_quota.py +0 -0
- {agentpause-0.4.0 → agentpause-0.5.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.5.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
|
|
@@ -19,6 +19,8 @@ Provides-Extra: litellm
|
|
|
19
19
|
Requires-Dist: litellm>=1.40; extra == "litellm"
|
|
20
20
|
Provides-Extra: langgraph
|
|
21
21
|
Requires-Dist: langgraph>=0.2; extra == "langgraph"
|
|
22
|
+
Provides-Extra: gpu
|
|
23
|
+
Requires-Dist: nvidia-ml-py>=12; extra == "gpu"
|
|
22
24
|
Dynamic: license-file
|
|
23
25
|
|
|
24
26
|
# agentpause
|
|
@@ -28,7 +30,10 @@ Dynamic: license-file
|
|
|
28
30
|
|
|
29
31
|
The core works on any provider — cloud (OpenAI, Anthropic, Groq) or local — because
|
|
30
32
|
it only serializes application-level state. True KV-cache warm start is an optional
|
|
31
|
-
plugin for self-hosted runtimes (llama.cpp, vLLM).
|
|
33
|
+
plugin for self-hosted runtimes (llama.cpp, vLLM). Predictive suspension itself still
|
|
34
|
+
needs a real budget signal to act on — on a cloud provider that's rate-limit headers;
|
|
35
|
+
for a self-hosted server there are none, see "Local mode: what's actually being
|
|
36
|
+
controlled" below for what to wire in instead.
|
|
32
37
|
|
|
33
38
|
## Measured results (from the accompanying research)
|
|
34
39
|
|
|
@@ -308,7 +313,60 @@ Install with `pip install -e ".[langgraph]"`; see `examples/langgraph_quickstart
|
|
|
308
313
|
## Scaling up: many providers, many agents, smarter estimates (v0.3)
|
|
309
314
|
|
|
310
315
|
The same predictive idea — read the budget first, act before the error —
|
|
311
|
-
extends
|
|
316
|
+
extends to routing across providers and sharing one window across agents.
|
|
317
|
+
|
|
318
|
+
### Plug and play: `AgentFleet`
|
|
319
|
+
|
|
320
|
+
Combining `BudgetRouter` + `MultiAgentCoordinator` + an estimator by hand is
|
|
321
|
+
six manual pieces for every single call any agent makes: build the router,
|
|
322
|
+
build the coordinator around `router.budget`, register each agent, build a
|
|
323
|
+
separate estimator per agent, compute `estimated`/`sigma` from it yourself,
|
|
324
|
+
call `coordinator.request(...)`, check `.action == "continue"`, call
|
|
325
|
+
`router.backend(...)` yourself, call `coordinator.complete(...)`, call
|
|
326
|
+
`est.record(...)`. `AgentFleet` collapses that to one constructor call plus
|
|
327
|
+
one `.call(...)` per step:
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from agentpause import AgentFleet
|
|
331
|
+
from agentpause.adapters.openai_compat import OpenAICompatAdapter
|
|
332
|
+
from agentpause.adapters.anthropic import AnthropicAdapter
|
|
333
|
+
|
|
334
|
+
fleet = AgentFleet(
|
|
335
|
+
[("groq", OpenAICompatAdapter.for_model("groq/llama-3.1-8b-instant")),
|
|
336
|
+
("claude", AnthropicAdapter("claude-haiku-4-5"))],
|
|
337
|
+
agents=[("researcher", 1), ("summarizer", 0)], # (agent_id, priority)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
decision, reply, used = fleet.call("researcher", messages)
|
|
341
|
+
if decision.action == "continue":
|
|
342
|
+
... # reply/used are already there; the shared pool is already reconciled
|
|
343
|
+
elif decision.action == "wait":
|
|
344
|
+
... # decision.wait_seconds suggests how long
|
|
345
|
+
else:
|
|
346
|
+
... # checkpoint: this agent's own StateStore, same as single-agent usage
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
What you must provide: the list of `providers` (same shape `BudgetRouter`
|
|
350
|
+
already accepts), and agent ids if you want explicit priorities. What's
|
|
351
|
+
automatic: the router and coordinator built together on the same clock, a
|
|
352
|
+
fresh and ISOLATED estimator per agent (one agent's learned history never
|
|
353
|
+
leaks into another's), and the whole request → call → reconcile → record
|
|
354
|
+
sequence behind one method. What this does **not** add: checkpoint/resume
|
|
355
|
+
for fleets — `Session`'s predictive decision is computed from its own private
|
|
356
|
+
estimator and there's no seam (by design — the core stays dependency-free,
|
|
357
|
+
`MultiAgentCoordinator`/`BudgetRouter` are deliberately separate,
|
|
358
|
+
adapter-level constructs) to redirect it through the coordinator instead. If
|
|
359
|
+
you need per-agent resumability, pair `AgentFleet` with your own `StateStore`
|
|
360
|
+
per `agent_id`, exactly like single-agent usage does elsewhere in this
|
|
361
|
+
README.
|
|
362
|
+
|
|
363
|
+
Runnable, no key required: `python examples/fleet_facade_quickstart.py`.
|
|
364
|
+
|
|
365
|
+
### Under the hood: `BudgetRouter` + `MultiAgentCoordinator` + `FeatureEstimator` on their own
|
|
366
|
+
|
|
367
|
+
Reach for these directly instead of the facade when you want fine control —
|
|
368
|
+
a custom routing `key=`, direct access to `coordinator.arbitrate()` for a
|
|
369
|
+
batch of simultaneous requests, or a hand-built estimator per agent:
|
|
312
370
|
|
|
313
371
|
```python
|
|
314
372
|
from agentpause import BudgetRouter, MultiAgentCoordinator, FeatureEstimator
|
|
@@ -326,7 +384,9 @@ router = BudgetRouter(
|
|
|
326
384
|
# 2. Share ONE rate-limit window across a fleet: every granted call
|
|
327
385
|
# reserves its predicted cost (estimate + k·σ) from the shared pool,
|
|
328
386
|
# so agents can't overcommit the window together. Contention is
|
|
329
|
-
# arbitrated by priority, then longest-waiting
|
|
387
|
+
# arbitrated by priority, then longest-waiting — but ONLY in arbitrate()'s
|
|
388
|
+
# batch call; the streaming request()/complete() pair below has no
|
|
389
|
+
# priority ordering of its own, first-asked is first-served.
|
|
330
390
|
coord = MultiAgentCoordinator(telemetry=router.budget)
|
|
331
391
|
coord.register("researcher", priority=1)
|
|
332
392
|
coord.register("summarizer")
|
|
@@ -343,7 +403,8 @@ if d.action == "continue":
|
|
|
343
403
|
```
|
|
344
404
|
|
|
345
405
|
`FeatureEstimator` is a drop-in for the default estimator
|
|
346
|
-
(`PredictiveScheduler(estimator=FeatureEstimator())
|
|
406
|
+
(`PredictiveScheduler(estimator=FeatureEstimator())`, or
|
|
407
|
+
`AgentFleet(..., estimator_factory=FeatureEstimator)`): a dependency-free
|
|
347
408
|
ridge regression over features you declare (tool, model, temperature, …)
|
|
348
409
|
that also learns per-step **latency**, feeding the optional time budget
|
|
349
410
|
(`PredictiveScheduler(time_budget_s=...)`) — if the predicted step can't
|
|
@@ -351,7 +412,8 @@ finish before the deadline, the answer is checkpoint, never wait (time,
|
|
|
351
412
|
unlike tokens, does not refill).
|
|
352
413
|
|
|
353
414
|
Runnable demo without any key: `python examples/fleet_quickstart.py`
|
|
354
|
-
(
|
|
415
|
+
(the same three pieces, composed by hand instead of through the facade, for
|
|
416
|
+
anyone who wants to see or override one piece at a time).
|
|
355
417
|
|
|
356
418
|
## Plan before you spend, fork your past, and survive a reboot (v0.4)
|
|
357
419
|
|
|
@@ -399,17 +461,136 @@ machine (KV blobs are intentionally machine-local and never migrate).
|
|
|
399
461
|
from agentpause.llamacpp_kv import LlamaCppSlots, KVStateStore
|
|
400
462
|
|
|
401
463
|
kv_store = KVStateStore(StateStore(".agentpause"),
|
|
402
|
-
slots=LlamaCppSlots(), base_url="http://127.0.0.1:8080"
|
|
464
|
+
slots=LlamaCppSlots(), base_url="http://127.0.0.1:8080",
|
|
465
|
+
kv_dir="./kv_cache") # MUST equal --slot-save-path, see below
|
|
403
466
|
kv_store.save_with_kv(checkpoint) # KV blob first, logical commit second
|
|
404
467
|
cp, info = kv_store.load_with_kv("research-task")
|
|
405
468
|
print(info) # {"kv_restored": True, "n_restored": 4096} — or a graceful
|
|
406
469
|
# {"kv_restored": False, "reason": "model_mismatch" | "kv_file_missing"}
|
|
407
470
|
```
|
|
408
471
|
|
|
472
|
+
`kv_dir` must be the exact same directory the server was started with via
|
|
473
|
+
`--slot-save-path` — a llama-server does not expose that path anywhere in
|
|
474
|
+
`/props` or `/slots`, so this is the one thing agentpause cannot discover on
|
|
475
|
+
its own and must be told. Getting it wrong used to surface only much later,
|
|
476
|
+
at the next restore, as `reason="kv_file_missing"`; `save_with_kv` now checks
|
|
477
|
+
that the blob actually landed in `kv_dir` right after the save call returns
|
|
478
|
+
and raises a clear `KVError` immediately if it didn't, naming the likely
|
|
479
|
+
`kv_dir`/`--slot-save-path` mismatch — caught on the very first save, not
|
|
480
|
+
discovered sessions later.
|
|
481
|
+
|
|
409
482
|
Runnable demos without any key or server: `python examples/migrate_fork.py`
|
|
410
483
|
(fork + migration story) and `python examples/kv_llamacpp_demo.py` (KV
|
|
411
484
|
save/restore, model-change degradation, independent forked blobs, migration).
|
|
412
485
|
|
|
486
|
+
## Local mode: what's actually being controlled
|
|
487
|
+
|
|
488
|
+
A rate-limit budget on a cloud provider comes from real response headers
|
|
489
|
+
(`x-ratelimit-remaining-tokens` and friends — see the tables above). A
|
|
490
|
+
self-hosted `llama-server` sends none of those headers, because there is no
|
|
491
|
+
traffic limit to respect: nobody is throttling requests-per-minute against a
|
|
492
|
+
process running on your own machine.
|
|
493
|
+
|
|
494
|
+
That has a direct consequence for what `should_suspend()` does locally.
|
|
495
|
+
**Without explicitly configuring `time_budget_s`, `money_budget`, or one of
|
|
496
|
+
the local adapters below, `should_suspend()` has nothing real to evaluate on
|
|
497
|
+
a self-hosted server and will never fire** — unlike on a cloud provider, the
|
|
498
|
+
library will not stop spontaneously for a resource limit, because nothing
|
|
499
|
+
told it one exists. A hand-picked `fallback_remaining=N` papers over this
|
|
500
|
+
with a number that looks real but never depletes. What keeps working with no
|
|
501
|
+
extra configuration either way: manual `checkpoint()` calls, and everything
|
|
502
|
+
the KV-cache section above describes — a warm start stays exactly as
|
|
503
|
+
valuable whether or not a budget signal is also wired in.
|
|
504
|
+
|
|
505
|
+
### Plug and play: `LocalResourceBudget`
|
|
506
|
+
|
|
507
|
+
`agentpause.adapters.local_resources` has five independently useful pieces
|
|
508
|
+
for self-hosted llama.cpp (below). Wiring all five by hand for the common
|
|
509
|
+
case — one server, one GPU or none — is six manual steps, two of which need
|
|
510
|
+
a hand calibration most people won't think to do correctly (a
|
|
511
|
+
`bytes_per_token` for the GPU budget, an `estimated_kv_save_s` guess for the
|
|
512
|
+
time budget). `LocalResourceBudget` reduces that to one constructor call
|
|
513
|
+
plus one `.kv_store(...)` call, and self-calibrates the rest from your first
|
|
514
|
+
real checkpoint save — no synthetic probe, no hand measurement:
|
|
515
|
+
|
|
516
|
+
```python
|
|
517
|
+
from agentpause import PredictiveScheduler, StateStore
|
|
518
|
+
from agentpause.adapters.local_resources import LocalResourceBudget
|
|
519
|
+
|
|
520
|
+
# kv_dir is the ONE thing this can't discover on its own: it MUST be the
|
|
521
|
+
# exact same directory the server was started with via --slot-save-path
|
|
522
|
+
# (llama-server --slot-save-path ./kv_cache ... <-> kv_dir="./kv_cache").
|
|
523
|
+
local = LocalResourceBudget(kv_dir="./kv_cache")
|
|
524
|
+
|
|
525
|
+
sched = PredictiveScheduler(backend=my_llm_call, telemetry=local)
|
|
526
|
+
kv_store = local.kv_store(StateStore(".agentpause"))
|
|
527
|
+
|
|
528
|
+
# normal use from here: kv_store.save_with_kv(checkpoint) whenever you need
|
|
529
|
+
# to suspend. VRAM, the KV-save time reserve, and the disk-space guard
|
|
530
|
+
# threshold all calibrate themselves from that FIRST real save — nothing
|
|
531
|
+
# to measure by hand, nothing to guess.
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
What you must provide: `kv_dir` (see above), and `base_url` only if your
|
|
535
|
+
server isn't the default `http://127.0.0.1:8080`. What's automatic: the
|
|
536
|
+
configured context size (read from `GET /props` the instant you construct
|
|
537
|
+
it — an unreachable server fails loudly right here, not three steps into a
|
|
538
|
+
run), whether an NVIDIA GPU is present at all (no GPU is treated as
|
|
539
|
+
completely ordinary, never an error), and — from the first real
|
|
540
|
+
`save_with_kv()` call onward — `bytes_per_token`, the measured KV-save
|
|
541
|
+
duration, and a disk-space guard threshold sized from the real blob you just
|
|
542
|
+
wrote (`max(500 MB, 5× that blob's size)`), unless you asked for a specific
|
|
543
|
+
threshold yourself.
|
|
544
|
+
|
|
545
|
+
Runnable, no real server or GPU required:
|
|
546
|
+
`python examples/local_resource_budget_quickstart.py`.
|
|
547
|
+
|
|
548
|
+
### Under the hood: the five pieces on their own
|
|
549
|
+
|
|
550
|
+
Reach for these directly instead of the facade when you want fine control
|
|
551
|
+
over one axis — a custom `used_field=`, a hand-picked `bytes_per_token`, an
|
|
552
|
+
explicit `estimated_kv_save_s`:
|
|
553
|
+
|
|
554
|
+
- `LlamaCppContextBudget` — reads the ACTUAL configured context size and the
|
|
555
|
+
ACTUAL tokens used in a slot's KV cache from a real server (`GET /props` +
|
|
556
|
+
`GET /slots`); never sets `reset_seconds` (a local context window has
|
|
557
|
+
nothing that refills it on its own).
|
|
558
|
+
- `GPUMemoryBudget` — reads REAL free VRAM straight from the hardware via
|
|
559
|
+
NVML (`pynvml`, PyPI package `nvidia-ml-py`); refuses to guess a token
|
|
560
|
+
count without an explicit `bytes_per_token`.
|
|
561
|
+
- `KVAwareTimeBudget` — wraps another telemetry callable and reserves
|
|
562
|
+
wall-clock time up front for saving the KV-cache blob before a deadline
|
|
563
|
+
actually hits.
|
|
564
|
+
- `CompositeLocalBudget` — composes several telemetry callables (e.g. the
|
|
565
|
+
two above) into one, reporting whichever resource is closest to running
|
|
566
|
+
out, not an average of them.
|
|
567
|
+
- `estimate_local_price_per_1k_tokens` / `estimate_hourly_cost_from_power` /
|
|
568
|
+
`price_per_1k_tokens_from_estimator` — derive a real `price_per_1k_tokens`
|
|
569
|
+
for the scheduler's existing monetary constraint from local throughput and
|
|
570
|
+
a power/rental cost, instead of a provider invoice.
|
|
571
|
+
|
|
572
|
+
Two honest caveats, not glossed over (they apply whether you use the facade
|
|
573
|
+
or these pieces directly, since the facade is built on top of them):
|
|
574
|
+
|
|
575
|
+
- **`LlamaCppContextBudget`'s `used_field`**: CONFIRMED live (2026-07-21,
|
|
576
|
+
against a real llama-server serving Qwen3-8B-GGUF Q4_K_M) to be
|
|
577
|
+
`n_prompt_tokens` — not documented in the official llama.cpp server
|
|
578
|
+
README's own example payload at the time this was written, found by
|
|
579
|
+
actually diffing a slot's `GET /slots` entry before and after a real
|
|
580
|
+
completion. The default extractor tries it first, with older guesses kept
|
|
581
|
+
as a fallback chain for other llama.cpp-ecosystem forks/proxies that may
|
|
582
|
+
shape this differently. Verify against your own server if you're not
|
|
583
|
+
running mainline llama.cpp, and pass `used_field=` explicitly if it
|
|
584
|
+
doesn't match.
|
|
585
|
+
- **`GPUMemoryBudget` is NVIDIA-only, today**: it reads VRAM via NVML.
|
|
586
|
+
AMD/ROCm GPUs expose a different interface (`rocm-smi`/`amdsmi`) that this
|
|
587
|
+
module does not implement — bring your own `reader_fn=` on ROCm hardware.
|
|
588
|
+
|
|
589
|
+
Runnable demo, no real server or GPU required:
|
|
590
|
+
`python examples/local_resources_quickstart.py` (the same five signals,
|
|
591
|
+
composed by hand instead of through the facade, for anyone who wants to see
|
|
592
|
+
or override one piece at a time).
|
|
593
|
+
|
|
413
594
|
## Why
|
|
414
595
|
|
|
415
596
|
Current agent frameworks persist state *reactively*: they checkpoint after a step
|
|
@@ -433,6 +614,7 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
|
433
614
|
| `StateStore` / `Checkpoint` | atomic logical checkpointing with idempotency keys — works on any provider; `compact()` / `summarize_with()` shrink a suspended checkpoint offline |
|
|
434
615
|
| `BudgetRouter` | predictive multi-provider routing: reads every provider's budget first, routes to the most headroom, cools down 429'd providers |
|
|
435
616
|
| `MultiAgentCoordinator` | one shared rate-limit window across many agents: granted calls reserve their predicted cost; `arbitrate()` resolves contention by priority + fairness |
|
|
617
|
+
| `AgentFleet` | plug-and-play facade over the two rows above plus a per-agent estimator: one constructor call + one `.call(agent_id, messages)` per step replaces the request/call/complete/record dance |
|
|
436
618
|
| `ToolQuota` | client-side sliding window for rate-limited tools that expose no headers |
|
|
437
619
|
| `CircuitBreaker` / `FallbackBackend` | reactive safety nets: fail fast on a broken provider, try the next one in order |
|
|
438
620
|
| `adapters.litellm.LiteLLMAdapter` | backend + telemetry for any LiteLLM-supported provider (headers → budget, stale reading → 1-token ping) |
|
|
@@ -444,6 +626,12 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
|
444
626
|
| `StateStore.export_bundle()` / `import_bundle()` | the checkpoint as a versioned, json-portable process image: suspend on one machine, resume on another at the exact step |
|
|
445
627
|
| `HumanAttentionBudget` | the person in the loop as a rate-limited `Budget`: N asks per rolling window + manual `available()`/`away(until)` override; composes with `decide()` |
|
|
446
628
|
| `llamacpp_kv.LlamaCppSlots` / `KVStateStore` | optional plugin: TRUE warm start for self-hosted llama.cpp via KV-cache save/restore, transactional, with automatic graceful degradation |
|
|
629
|
+
| `adapters.local_resources.LlamaCppContextBudget` | real local-context `Budget` from a llama.cpp server's actual `--ctx-size` and current KV-cache usage (`/props` + `/slots`); never a rate limit, never a `reset_seconds` |
|
|
630
|
+
| `adapters.local_resources.GPUMemoryBudget` | real free-VRAM `Budget` read from NVML (`pynvml`); NVIDIA-only, needs an explicit `bytes_per_token` to report tokens |
|
|
631
|
+
| `adapters.local_resources.KVAwareTimeBudget` | wraps any telemetry callable and takes over `remaining_seconds` itself, reserving time up front for a KV-cache blob save |
|
|
632
|
+
| `adapters.local_resources.CompositeLocalBudget` | composes several telemetry callables into one, most-restrictive `Budget` — see "Local mode" above |
|
|
633
|
+
| `adapters.local_resources.estimate_local_price_per_1k_tokens` / `estimate_hourly_cost_from_power` / `price_per_1k_tokens_from_estimator` | derive a real `price_per_1k_tokens` for a local deployment from throughput and power/rental cost, instead of a provider invoice |
|
|
634
|
+
| `adapters.local_resources.LocalResourceBudget` | plug-and-play facade over the four rows above plus the KV disk guard: one constructor call + `.kv_store(...)`, self-calibrates VRAM/time/disk numbers from your first real checkpoint save |
|
|
447
635
|
|
|
448
636
|
## Roadmap
|
|
449
637
|
|
|
@@ -454,12 +642,13 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
|
454
642
|
- [x] LangGraph adapter (interrupt + checkpointer)
|
|
455
643
|
- [x] Direct adapters (OpenAI-compatible, Anthropic with prompt caching)
|
|
456
644
|
- [x] Predictive multi-provider routing (`BudgetRouter`)
|
|
457
|
-
- [x] Shared budget across agents (`MultiAgentCoordinator`)
|
|
645
|
+
- [x] Shared budget across agents (`MultiAgentCoordinator`), with a one-call plug-and-play facade (`AgentFleet`)
|
|
458
646
|
- [x] Feature-based cost & latency estimator (`FeatureEstimator`)
|
|
459
647
|
- [x] Task-completion forecast (`session.forecast()`)
|
|
460
648
|
- [x] Checkpoint fork & cross-machine migration
|
|
461
649
|
- [x] Optional KV-cache plugin for llama.cpp (true warm start), incl. fork+KV
|
|
462
650
|
- [x] Human attention as a rate-limited budget (`HumanAttentionBudget`)
|
|
651
|
+
- [x] Real local-resource budgets for self-hosted llama.cpp: context (`LlamaCppContextBudget`), GPU VRAM (`GPUMemoryBudget`), KV-aware time (`KVAwareTimeBudget`), derived local price, composed via `CompositeLocalBudget`, and a one-call plug-and-play facade (`LocalResourceBudget`) that self-calibrates all of the above from a real checkpoint save
|
|
463
652
|
- [ ] CrewAI / AutoGen / LlamaIndex adapters
|
|
464
653
|
- [ ] KV-cache plugin for vLLM
|
|
465
654
|
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
The core works on any provider — cloud (OpenAI, Anthropic, Groq) or local — because
|
|
7
7
|
it only serializes application-level state. True KV-cache warm start is an optional
|
|
8
|
-
plugin for self-hosted runtimes (llama.cpp, vLLM).
|
|
8
|
+
plugin for self-hosted runtimes (llama.cpp, vLLM). Predictive suspension itself still
|
|
9
|
+
needs a real budget signal to act on — on a cloud provider that's rate-limit headers;
|
|
10
|
+
for a self-hosted server there are none, see "Local mode: what's actually being
|
|
11
|
+
controlled" below for what to wire in instead.
|
|
9
12
|
|
|
10
13
|
## Measured results (from the accompanying research)
|
|
11
14
|
|
|
@@ -285,7 +288,60 @@ Install with `pip install -e ".[langgraph]"`; see `examples/langgraph_quickstart
|
|
|
285
288
|
## Scaling up: many providers, many agents, smarter estimates (v0.3)
|
|
286
289
|
|
|
287
290
|
The same predictive idea — read the budget first, act before the error —
|
|
288
|
-
extends
|
|
291
|
+
extends to routing across providers and sharing one window across agents.
|
|
292
|
+
|
|
293
|
+
### Plug and play: `AgentFleet`
|
|
294
|
+
|
|
295
|
+
Combining `BudgetRouter` + `MultiAgentCoordinator` + an estimator by hand is
|
|
296
|
+
six manual pieces for every single call any agent makes: build the router,
|
|
297
|
+
build the coordinator around `router.budget`, register each agent, build a
|
|
298
|
+
separate estimator per agent, compute `estimated`/`sigma` from it yourself,
|
|
299
|
+
call `coordinator.request(...)`, check `.action == "continue"`, call
|
|
300
|
+
`router.backend(...)` yourself, call `coordinator.complete(...)`, call
|
|
301
|
+
`est.record(...)`. `AgentFleet` collapses that to one constructor call plus
|
|
302
|
+
one `.call(...)` per step:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from agentpause import AgentFleet
|
|
306
|
+
from agentpause.adapters.openai_compat import OpenAICompatAdapter
|
|
307
|
+
from agentpause.adapters.anthropic import AnthropicAdapter
|
|
308
|
+
|
|
309
|
+
fleet = AgentFleet(
|
|
310
|
+
[("groq", OpenAICompatAdapter.for_model("groq/llama-3.1-8b-instant")),
|
|
311
|
+
("claude", AnthropicAdapter("claude-haiku-4-5"))],
|
|
312
|
+
agents=[("researcher", 1), ("summarizer", 0)], # (agent_id, priority)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
decision, reply, used = fleet.call("researcher", messages)
|
|
316
|
+
if decision.action == "continue":
|
|
317
|
+
... # reply/used are already there; the shared pool is already reconciled
|
|
318
|
+
elif decision.action == "wait":
|
|
319
|
+
... # decision.wait_seconds suggests how long
|
|
320
|
+
else:
|
|
321
|
+
... # checkpoint: this agent's own StateStore, same as single-agent usage
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
What you must provide: the list of `providers` (same shape `BudgetRouter`
|
|
325
|
+
already accepts), and agent ids if you want explicit priorities. What's
|
|
326
|
+
automatic: the router and coordinator built together on the same clock, a
|
|
327
|
+
fresh and ISOLATED estimator per agent (one agent's learned history never
|
|
328
|
+
leaks into another's), and the whole request → call → reconcile → record
|
|
329
|
+
sequence behind one method. What this does **not** add: checkpoint/resume
|
|
330
|
+
for fleets — `Session`'s predictive decision is computed from its own private
|
|
331
|
+
estimator and there's no seam (by design — the core stays dependency-free,
|
|
332
|
+
`MultiAgentCoordinator`/`BudgetRouter` are deliberately separate,
|
|
333
|
+
adapter-level constructs) to redirect it through the coordinator instead. If
|
|
334
|
+
you need per-agent resumability, pair `AgentFleet` with your own `StateStore`
|
|
335
|
+
per `agent_id`, exactly like single-agent usage does elsewhere in this
|
|
336
|
+
README.
|
|
337
|
+
|
|
338
|
+
Runnable, no key required: `python examples/fleet_facade_quickstart.py`.
|
|
339
|
+
|
|
340
|
+
### Under the hood: `BudgetRouter` + `MultiAgentCoordinator` + `FeatureEstimator` on their own
|
|
341
|
+
|
|
342
|
+
Reach for these directly instead of the facade when you want fine control —
|
|
343
|
+
a custom routing `key=`, direct access to `coordinator.arbitrate()` for a
|
|
344
|
+
batch of simultaneous requests, or a hand-built estimator per agent:
|
|
289
345
|
|
|
290
346
|
```python
|
|
291
347
|
from agentpause import BudgetRouter, MultiAgentCoordinator, FeatureEstimator
|
|
@@ -303,7 +359,9 @@ router = BudgetRouter(
|
|
|
303
359
|
# 2. Share ONE rate-limit window across a fleet: every granted call
|
|
304
360
|
# reserves its predicted cost (estimate + k·σ) from the shared pool,
|
|
305
361
|
# so agents can't overcommit the window together. Contention is
|
|
306
|
-
# arbitrated by priority, then longest-waiting
|
|
362
|
+
# arbitrated by priority, then longest-waiting — but ONLY in arbitrate()'s
|
|
363
|
+
# batch call; the streaming request()/complete() pair below has no
|
|
364
|
+
# priority ordering of its own, first-asked is first-served.
|
|
307
365
|
coord = MultiAgentCoordinator(telemetry=router.budget)
|
|
308
366
|
coord.register("researcher", priority=1)
|
|
309
367
|
coord.register("summarizer")
|
|
@@ -320,7 +378,8 @@ if d.action == "continue":
|
|
|
320
378
|
```
|
|
321
379
|
|
|
322
380
|
`FeatureEstimator` is a drop-in for the default estimator
|
|
323
|
-
(`PredictiveScheduler(estimator=FeatureEstimator())
|
|
381
|
+
(`PredictiveScheduler(estimator=FeatureEstimator())`, or
|
|
382
|
+
`AgentFleet(..., estimator_factory=FeatureEstimator)`): a dependency-free
|
|
324
383
|
ridge regression over features you declare (tool, model, temperature, …)
|
|
325
384
|
that also learns per-step **latency**, feeding the optional time budget
|
|
326
385
|
(`PredictiveScheduler(time_budget_s=...)`) — if the predicted step can't
|
|
@@ -328,7 +387,8 @@ finish before the deadline, the answer is checkpoint, never wait (time,
|
|
|
328
387
|
unlike tokens, does not refill).
|
|
329
388
|
|
|
330
389
|
Runnable demo without any key: `python examples/fleet_quickstart.py`
|
|
331
|
-
(
|
|
390
|
+
(the same three pieces, composed by hand instead of through the facade, for
|
|
391
|
+
anyone who wants to see or override one piece at a time).
|
|
332
392
|
|
|
333
393
|
## Plan before you spend, fork your past, and survive a reboot (v0.4)
|
|
334
394
|
|
|
@@ -376,17 +436,136 @@ machine (KV blobs are intentionally machine-local and never migrate).
|
|
|
376
436
|
from agentpause.llamacpp_kv import LlamaCppSlots, KVStateStore
|
|
377
437
|
|
|
378
438
|
kv_store = KVStateStore(StateStore(".agentpause"),
|
|
379
|
-
slots=LlamaCppSlots(), base_url="http://127.0.0.1:8080"
|
|
439
|
+
slots=LlamaCppSlots(), base_url="http://127.0.0.1:8080",
|
|
440
|
+
kv_dir="./kv_cache") # MUST equal --slot-save-path, see below
|
|
380
441
|
kv_store.save_with_kv(checkpoint) # KV blob first, logical commit second
|
|
381
442
|
cp, info = kv_store.load_with_kv("research-task")
|
|
382
443
|
print(info) # {"kv_restored": True, "n_restored": 4096} — or a graceful
|
|
383
444
|
# {"kv_restored": False, "reason": "model_mismatch" | "kv_file_missing"}
|
|
384
445
|
```
|
|
385
446
|
|
|
447
|
+
`kv_dir` must be the exact same directory the server was started with via
|
|
448
|
+
`--slot-save-path` — a llama-server does not expose that path anywhere in
|
|
449
|
+
`/props` or `/slots`, so this is the one thing agentpause cannot discover on
|
|
450
|
+
its own and must be told. Getting it wrong used to surface only much later,
|
|
451
|
+
at the next restore, as `reason="kv_file_missing"`; `save_with_kv` now checks
|
|
452
|
+
that the blob actually landed in `kv_dir` right after the save call returns
|
|
453
|
+
and raises a clear `KVError` immediately if it didn't, naming the likely
|
|
454
|
+
`kv_dir`/`--slot-save-path` mismatch — caught on the very first save, not
|
|
455
|
+
discovered sessions later.
|
|
456
|
+
|
|
386
457
|
Runnable demos without any key or server: `python examples/migrate_fork.py`
|
|
387
458
|
(fork + migration story) and `python examples/kv_llamacpp_demo.py` (KV
|
|
388
459
|
save/restore, model-change degradation, independent forked blobs, migration).
|
|
389
460
|
|
|
461
|
+
## Local mode: what's actually being controlled
|
|
462
|
+
|
|
463
|
+
A rate-limit budget on a cloud provider comes from real response headers
|
|
464
|
+
(`x-ratelimit-remaining-tokens` and friends — see the tables above). A
|
|
465
|
+
self-hosted `llama-server` sends none of those headers, because there is no
|
|
466
|
+
traffic limit to respect: nobody is throttling requests-per-minute against a
|
|
467
|
+
process running on your own machine.
|
|
468
|
+
|
|
469
|
+
That has a direct consequence for what `should_suspend()` does locally.
|
|
470
|
+
**Without explicitly configuring `time_budget_s`, `money_budget`, or one of
|
|
471
|
+
the local adapters below, `should_suspend()` has nothing real to evaluate on
|
|
472
|
+
a self-hosted server and will never fire** — unlike on a cloud provider, the
|
|
473
|
+
library will not stop spontaneously for a resource limit, because nothing
|
|
474
|
+
told it one exists. A hand-picked `fallback_remaining=N` papers over this
|
|
475
|
+
with a number that looks real but never depletes. What keeps working with no
|
|
476
|
+
extra configuration either way: manual `checkpoint()` calls, and everything
|
|
477
|
+
the KV-cache section above describes — a warm start stays exactly as
|
|
478
|
+
valuable whether or not a budget signal is also wired in.
|
|
479
|
+
|
|
480
|
+
### Plug and play: `LocalResourceBudget`
|
|
481
|
+
|
|
482
|
+
`agentpause.adapters.local_resources` has five independently useful pieces
|
|
483
|
+
for self-hosted llama.cpp (below). Wiring all five by hand for the common
|
|
484
|
+
case — one server, one GPU or none — is six manual steps, two of which need
|
|
485
|
+
a hand calibration most people won't think to do correctly (a
|
|
486
|
+
`bytes_per_token` for the GPU budget, an `estimated_kv_save_s` guess for the
|
|
487
|
+
time budget). `LocalResourceBudget` reduces that to one constructor call
|
|
488
|
+
plus one `.kv_store(...)` call, and self-calibrates the rest from your first
|
|
489
|
+
real checkpoint save — no synthetic probe, no hand measurement:
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
from agentpause import PredictiveScheduler, StateStore
|
|
493
|
+
from agentpause.adapters.local_resources import LocalResourceBudget
|
|
494
|
+
|
|
495
|
+
# kv_dir is the ONE thing this can't discover on its own: it MUST be the
|
|
496
|
+
# exact same directory the server was started with via --slot-save-path
|
|
497
|
+
# (llama-server --slot-save-path ./kv_cache ... <-> kv_dir="./kv_cache").
|
|
498
|
+
local = LocalResourceBudget(kv_dir="./kv_cache")
|
|
499
|
+
|
|
500
|
+
sched = PredictiveScheduler(backend=my_llm_call, telemetry=local)
|
|
501
|
+
kv_store = local.kv_store(StateStore(".agentpause"))
|
|
502
|
+
|
|
503
|
+
# normal use from here: kv_store.save_with_kv(checkpoint) whenever you need
|
|
504
|
+
# to suspend. VRAM, the KV-save time reserve, and the disk-space guard
|
|
505
|
+
# threshold all calibrate themselves from that FIRST real save — nothing
|
|
506
|
+
# to measure by hand, nothing to guess.
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
What you must provide: `kv_dir` (see above), and `base_url` only if your
|
|
510
|
+
server isn't the default `http://127.0.0.1:8080`. What's automatic: the
|
|
511
|
+
configured context size (read from `GET /props` the instant you construct
|
|
512
|
+
it — an unreachable server fails loudly right here, not three steps into a
|
|
513
|
+
run), whether an NVIDIA GPU is present at all (no GPU is treated as
|
|
514
|
+
completely ordinary, never an error), and — from the first real
|
|
515
|
+
`save_with_kv()` call onward — `bytes_per_token`, the measured KV-save
|
|
516
|
+
duration, and a disk-space guard threshold sized from the real blob you just
|
|
517
|
+
wrote (`max(500 MB, 5× that blob's size)`), unless you asked for a specific
|
|
518
|
+
threshold yourself.
|
|
519
|
+
|
|
520
|
+
Runnable, no real server or GPU required:
|
|
521
|
+
`python examples/local_resource_budget_quickstart.py`.
|
|
522
|
+
|
|
523
|
+
### Under the hood: the five pieces on their own
|
|
524
|
+
|
|
525
|
+
Reach for these directly instead of the facade when you want fine control
|
|
526
|
+
over one axis — a custom `used_field=`, a hand-picked `bytes_per_token`, an
|
|
527
|
+
explicit `estimated_kv_save_s`:
|
|
528
|
+
|
|
529
|
+
- `LlamaCppContextBudget` — reads the ACTUAL configured context size and the
|
|
530
|
+
ACTUAL tokens used in a slot's KV cache from a real server (`GET /props` +
|
|
531
|
+
`GET /slots`); never sets `reset_seconds` (a local context window has
|
|
532
|
+
nothing that refills it on its own).
|
|
533
|
+
- `GPUMemoryBudget` — reads REAL free VRAM straight from the hardware via
|
|
534
|
+
NVML (`pynvml`, PyPI package `nvidia-ml-py`); refuses to guess a token
|
|
535
|
+
count without an explicit `bytes_per_token`.
|
|
536
|
+
- `KVAwareTimeBudget` — wraps another telemetry callable and reserves
|
|
537
|
+
wall-clock time up front for saving the KV-cache blob before a deadline
|
|
538
|
+
actually hits.
|
|
539
|
+
- `CompositeLocalBudget` — composes several telemetry callables (e.g. the
|
|
540
|
+
two above) into one, reporting whichever resource is closest to running
|
|
541
|
+
out, not an average of them.
|
|
542
|
+
- `estimate_local_price_per_1k_tokens` / `estimate_hourly_cost_from_power` /
|
|
543
|
+
`price_per_1k_tokens_from_estimator` — derive a real `price_per_1k_tokens`
|
|
544
|
+
for the scheduler's existing monetary constraint from local throughput and
|
|
545
|
+
a power/rental cost, instead of a provider invoice.
|
|
546
|
+
|
|
547
|
+
Two honest caveats, not glossed over (they apply whether you use the facade
|
|
548
|
+
or these pieces directly, since the facade is built on top of them):
|
|
549
|
+
|
|
550
|
+
- **`LlamaCppContextBudget`'s `used_field`**: CONFIRMED live (2026-07-21,
|
|
551
|
+
against a real llama-server serving Qwen3-8B-GGUF Q4_K_M) to be
|
|
552
|
+
`n_prompt_tokens` — not documented in the official llama.cpp server
|
|
553
|
+
README's own example payload at the time this was written, found by
|
|
554
|
+
actually diffing a slot's `GET /slots` entry before and after a real
|
|
555
|
+
completion. The default extractor tries it first, with older guesses kept
|
|
556
|
+
as a fallback chain for other llama.cpp-ecosystem forks/proxies that may
|
|
557
|
+
shape this differently. Verify against your own server if you're not
|
|
558
|
+
running mainline llama.cpp, and pass `used_field=` explicitly if it
|
|
559
|
+
doesn't match.
|
|
560
|
+
- **`GPUMemoryBudget` is NVIDIA-only, today**: it reads VRAM via NVML.
|
|
561
|
+
AMD/ROCm GPUs expose a different interface (`rocm-smi`/`amdsmi`) that this
|
|
562
|
+
module does not implement — bring your own `reader_fn=` on ROCm hardware.
|
|
563
|
+
|
|
564
|
+
Runnable demo, no real server or GPU required:
|
|
565
|
+
`python examples/local_resources_quickstart.py` (the same five signals,
|
|
566
|
+
composed by hand instead of through the facade, for anyone who wants to see
|
|
567
|
+
or override one piece at a time).
|
|
568
|
+
|
|
390
569
|
## Why
|
|
391
570
|
|
|
392
571
|
Current agent frameworks persist state *reactively*: they checkpoint after a step
|
|
@@ -410,6 +589,7 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
|
410
589
|
| `StateStore` / `Checkpoint` | atomic logical checkpointing with idempotency keys — works on any provider; `compact()` / `summarize_with()` shrink a suspended checkpoint offline |
|
|
411
590
|
| `BudgetRouter` | predictive multi-provider routing: reads every provider's budget first, routes to the most headroom, cools down 429'd providers |
|
|
412
591
|
| `MultiAgentCoordinator` | one shared rate-limit window across many agents: granted calls reserve their predicted cost; `arbitrate()` resolves contention by priority + fairness |
|
|
592
|
+
| `AgentFleet` | plug-and-play facade over the two rows above plus a per-agent estimator: one constructor call + one `.call(agent_id, messages)` per step replaces the request/call/complete/record dance |
|
|
413
593
|
| `ToolQuota` | client-side sliding window for rate-limited tools that expose no headers |
|
|
414
594
|
| `CircuitBreaker` / `FallbackBackend` | reactive safety nets: fail fast on a broken provider, try the next one in order |
|
|
415
595
|
| `adapters.litellm.LiteLLMAdapter` | backend + telemetry for any LiteLLM-supported provider (headers → budget, stale reading → 1-token ping) |
|
|
@@ -421,6 +601,12 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
|
421
601
|
| `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
602
|
| `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
603
|
| `llamacpp_kv.LlamaCppSlots` / `KVStateStore` | optional plugin: TRUE warm start for self-hosted llama.cpp via KV-cache save/restore, transactional, with automatic graceful degradation |
|
|
604
|
+
| `adapters.local_resources.LlamaCppContextBudget` | real local-context `Budget` from a llama.cpp server's actual `--ctx-size` and current KV-cache usage (`/props` + `/slots`); never a rate limit, never a `reset_seconds` |
|
|
605
|
+
| `adapters.local_resources.GPUMemoryBudget` | real free-VRAM `Budget` read from NVML (`pynvml`); NVIDIA-only, needs an explicit `bytes_per_token` to report tokens |
|
|
606
|
+
| `adapters.local_resources.KVAwareTimeBudget` | wraps any telemetry callable and takes over `remaining_seconds` itself, reserving time up front for a KV-cache blob save |
|
|
607
|
+
| `adapters.local_resources.CompositeLocalBudget` | composes several telemetry callables into one, most-restrictive `Budget` — see "Local mode" above |
|
|
608
|
+
| `adapters.local_resources.estimate_local_price_per_1k_tokens` / `estimate_hourly_cost_from_power` / `price_per_1k_tokens_from_estimator` | derive a real `price_per_1k_tokens` for a local deployment from throughput and power/rental cost, instead of a provider invoice |
|
|
609
|
+
| `adapters.local_resources.LocalResourceBudget` | plug-and-play facade over the four rows above plus the KV disk guard: one constructor call + `.kv_store(...)`, self-calibrates VRAM/time/disk numbers from your first real checkpoint save |
|
|
424
610
|
|
|
425
611
|
## Roadmap
|
|
426
612
|
|
|
@@ -431,12 +617,13 @@ Resource-Aware Predictive Scheduler for Autonomous LLM Agents"*.
|
|
|
431
617
|
- [x] LangGraph adapter (interrupt + checkpointer)
|
|
432
618
|
- [x] Direct adapters (OpenAI-compatible, Anthropic with prompt caching)
|
|
433
619
|
- [x] Predictive multi-provider routing (`BudgetRouter`)
|
|
434
|
-
- [x] Shared budget across agents (`MultiAgentCoordinator`)
|
|
620
|
+
- [x] Shared budget across agents (`MultiAgentCoordinator`), with a one-call plug-and-play facade (`AgentFleet`)
|
|
435
621
|
- [x] Feature-based cost & latency estimator (`FeatureEstimator`)
|
|
436
622
|
- [x] Task-completion forecast (`session.forecast()`)
|
|
437
623
|
- [x] Checkpoint fork & cross-machine migration
|
|
438
624
|
- [x] Optional KV-cache plugin for llama.cpp (true warm start), incl. fork+KV
|
|
439
625
|
- [x] Human attention as a rate-limited budget (`HumanAttentionBudget`)
|
|
626
|
+
- [x] Real local-resource budgets for self-hosted llama.cpp: context (`LlamaCppContextBudget`), GPU VRAM (`GPUMemoryBudget`), KV-aware time (`KVAwareTimeBudget`), derived local price, composed via `CompositeLocalBudget`, and a one-call plug-and-play facade (`LocalResourceBudget`) that self-calibrates all of the above from a real checkpoint save
|
|
440
627
|
- [ ] CrewAI / AutoGen / LlamaIndex adapters
|
|
441
628
|
- [ ] KV-cache plugin for vLLM
|
|
442
629
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentpause"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.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"
|
|
@@ -24,6 +24,7 @@ dependencies = []
|
|
|
24
24
|
dev = ["pytest>=7.0"]
|
|
25
25
|
litellm = ["litellm>=1.40"]
|
|
26
26
|
langgraph = ["langgraph>=0.2"]
|
|
27
|
+
gpu = ["nvidia-ml-py>=12"]
|
|
27
28
|
|
|
28
29
|
[tool.setuptools.packages.find]
|
|
29
30
|
where = ["src"]
|
|
@@ -25,11 +25,12 @@ from .retry import RetryPolicy
|
|
|
25
25
|
from .risk import Budget, Decision, RiskModel, decide, should_checkpoint
|
|
26
26
|
from .router import BudgetRouter
|
|
27
27
|
from .coordinator import MultiAgentCoordinator
|
|
28
|
+
from .fleet import AgentFleet
|
|
28
29
|
from .state import Checkpoint, StateStore
|
|
29
30
|
from .tools import ToolQuota
|
|
30
31
|
from .scheduler import PredictiveScheduler, Session
|
|
31
32
|
|
|
32
|
-
__version__ = "0.
|
|
33
|
+
__version__ = "0.5.0"
|
|
33
34
|
|
|
34
35
|
__all__ = [
|
|
35
36
|
"PredictiveScheduler",
|
|
@@ -49,6 +50,7 @@ __all__ = [
|
|
|
49
50
|
"FallbackBackend",
|
|
50
51
|
"BudgetRouter",
|
|
51
52
|
"MultiAgentCoordinator",
|
|
53
|
+
"AgentFleet",
|
|
52
54
|
"CircuitBreaker",
|
|
53
55
|
"CircuitOpenError",
|
|
54
56
|
"RegimeDetector",
|