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.
Files changed (66) hide show
  1. {agentpause-0.4.0/src/agentpause.egg-info → agentpause-0.5.0}/PKG-INFO +197 -8
  2. {agentpause-0.4.0 → agentpause-0.5.0}/README.md +194 -7
  3. {agentpause-0.4.0 → agentpause-0.5.0}/pyproject.toml +2 -1
  4. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/__init__.py +3 -1
  5. agentpause-0.5.0/src/agentpause/adapters/local_resources.py +1429 -0
  6. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/errors.py +21 -0
  7. agentpause-0.5.0/src/agentpause/fleet.py +214 -0
  8. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/llamacpp_kv.py +186 -6
  9. {agentpause-0.4.0 → agentpause-0.5.0/src/agentpause.egg-info}/PKG-INFO +197 -8
  10. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/SOURCES.txt +4 -0
  11. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/requires.txt +3 -0
  12. agentpause-0.5.0/tests/test_fleet.py +164 -0
  13. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_llamacpp_kv.py +261 -1
  14. agentpause-0.5.0/tests/test_local_resources.py +1177 -0
  15. {agentpause-0.4.0 → agentpause-0.5.0}/LICENSE +0 -0
  16. {agentpause-0.4.0 → agentpause-0.5.0}/setup.cfg +0 -0
  17. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/__init__.py +0 -0
  18. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/anthropic.py +0 -0
  19. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/langgraph.py +0 -0
  20. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/litellm.py +0 -0
  21. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/adapters/openai_compat.py +0 -0
  22. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/attention.py +0 -0
  23. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/breaker.py +0 -0
  24. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/coordinator.py +0 -0
  25. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/estimator.py +0 -0
  26. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/fallback.py +0 -0
  27. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/forecast.py +0 -0
  28. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/refill.py +0 -0
  29. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/regression.py +0 -0
  30. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/retry.py +0 -0
  31. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/risk.py +0 -0
  32. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/router.py +0 -0
  33. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/scheduler.py +0 -0
  34. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/state.py +0 -0
  35. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause/tools.py +0 -0
  36. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/dependency_links.txt +0 -0
  37. {agentpause-0.4.0 → agentpause-0.5.0}/src/agentpause.egg-info/top_level.txt +0 -0
  38. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_anthropic_adapter.py +0 -0
  39. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_async.py +0 -0
  40. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_attention.py +0 -0
  41. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_breaker.py +0 -0
  42. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_coordinator.py +0 -0
  43. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_estimator.py +0 -0
  44. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_estimator_evolution.py +0 -0
  45. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_fallback.py +0 -0
  46. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_field_adoptions.py +0 -0
  47. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_forecast.py +0 -0
  48. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_fork_migrate.py +0 -0
  49. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_io_tpm.py +0 -0
  50. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_langgraph_adapter.py +0 -0
  51. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_latency_budget.py +0 -0
  52. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_litellm_adapter.py +0 -0
  53. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_money_and_hooks.py +0 -0
  54. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_openai_compat.py +0 -0
  55. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_quantile.py +0 -0
  56. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_refill_wait.py +0 -0
  57. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_regime.py +0 -0
  58. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_regression.py +0 -0
  59. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_risk.py +0 -0
  60. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_robustness.py +0 -0
  61. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_router.py +0 -0
  62. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_scheduler.py +0 -0
  63. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_smart_context.py +0 -0
  64. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_state.py +0 -0
  65. {agentpause-0.4.0 → agentpause-0.5.0}/tests/test_tool_quota.py +0 -0
  66. {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.4.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 in three directions, and they compose:
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())`): a dependency-free
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
- (routing switch + predictive WAIT under a shared window, in 30 lines of loop).
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 in three directions, and they compose:
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())`): a dependency-free
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
- (routing switch + predictive WAIT under a shared window, in 30 lines of loop).
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.4.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.4.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",