power-loop 1.0.0__tar.gz → 2.1.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.
- power_loop-2.1.0/PKG-INFO +349 -0
- power_loop-2.1.0/README.md +276 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/__init__.py +17 -10
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/anthropic_factory.py +46 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/interface.py +7 -4
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/llm_factory.py +11 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/agent/sink.py +37 -27
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/agent/stateful_loop.py +516 -122
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/event_payloads.py +4 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contrib/metrics_sink.py +6 -1
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/core/events.py +14 -5
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/core/pipeline.py +48 -28
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/core/state.py +10 -1
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/blackboard.py +19 -13
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/budget.py +7 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/compact.py +20 -10
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/env.py +10 -3
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/notes.py +10 -9
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/retry.py +15 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/runtime_state.py +9 -6
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/session_store.py +59 -147
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/spec.py +9 -7
- power_loop-2.1.0/power_loop/runtime/store/__init__.py +58 -0
- power_loop-2.1.0/power_loop/runtime/store/backends/__init__.py +1 -0
- power_loop-2.1.0/power_loop/runtime/store/backends/mysql.py +138 -0
- power_loop-2.1.0/power_loop/runtime/store/backends/postgres.py +85 -0
- power_loop-2.1.0/power_loop/runtime/store/backends/sqlite.py +192 -0
- power_loop-2.1.0/power_loop/runtime/store/capabilities.py +27 -0
- power_loop-2.1.0/power_loop/runtime/store/db.py +64 -0
- power_loop-2.1.0/power_loop/runtime/store/dialect.py +368 -0
- power_loop-2.1.0/power_loop/runtime/store/factory.py +103 -0
- power_loop-2.1.0/power_loop/runtime/store/schema.py +335 -0
- power_loop-2.1.0/power_loop/runtime/store/store.py +1309 -0
- power_loop-2.1.0/power_loop/runtime/store/types.py +174 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/timers.py +19 -15
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/tools/__init__.py +25 -10
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/tools/blackboard.py +3 -3
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/tools/default_tools.py +327 -81
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/engine.py +23 -14
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/introspect.py +8 -8
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/journal.py +63 -50
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/result.py +4 -1
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/resume.py +15 -15
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/runner.py +64 -32
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/spec.py +145 -37
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/subprocess_executor.py +16 -1
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/tool.py +2 -2
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/worker.py +3 -3
- power_loop-2.1.0/power_loop.egg-info/PKG-INFO +349 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop.egg-info/SOURCES.txt +12 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop.egg-info/requires.txt +10 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/pyproject.toml +7 -0
- power_loop-1.0.0/PKG-INFO +0 -295
- power_loop-1.0.0/README.md +0 -230
- power_loop-1.0.0/power_loop.egg-info/PKG-INFO +0 -295
- {power_loop-1.0.0 → power_loop-2.1.0}/LICENSE +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/__init__.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/__init__.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/capabilities.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/_vendor/llm_client/multimodal.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/agent/__init__.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/agent/follow_up.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/agent/system_prompt.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/agent/types.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/__init__.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/errors.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/events.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/handlers.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/hook_contexts.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/hooks.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/messages.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/protocols.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contracts/tools.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contrib/__init__.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contrib/_redact.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contrib/jsonl_sink.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contrib/logging_sink.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contrib/mcp.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/contrib/otel_sink.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/core/agent_context.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/core/hooks.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/core/phase.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/core/runner.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/py.typed +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/cancellation.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/exec_backend.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/human_input.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/memory.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/provider.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/skills.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/structured.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/runtime/stub_provider.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/tools/default_manifest.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/tools/registry.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/tools/spawn_agent.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/__init__.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop/workflow/api.py +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop.egg-info/dependency_links.txt +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/power_loop.egg-info/top_level.txt +0 -0
- {power_loop-1.0.0 → power_loop-2.1.0}/setup.cfg +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: power-loop
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Embeddable agent execution kernel — LLM loop, hooks, events, tools, dynamic sub-agents.
|
|
5
|
+
Author-email: zhangran <zhangran24@126.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PL-play/power-loop
|
|
8
|
+
Project-URL: Repository, https://github.com/PL-play/power-loop
|
|
9
|
+
Project-URL: Changelog, https://github.com/PL-play/power-loop/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Roadmap, https://github.com/PL-play/power-loop/blob/main/ROADMAP.md
|
|
11
|
+
Keywords: agent,llm,openai,anthropic,tool-use,hooks
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: anthropic
|
|
25
|
+
Requires-Dist: anthropic>=0.42.0; extra == "anthropic"
|
|
26
|
+
Provides-Extra: openai
|
|
27
|
+
Requires-Dist: openai>=1.52.0; extra == "openai"
|
|
28
|
+
Provides-Extra: skills
|
|
29
|
+
Requires-Dist: pyyaml>=6.0; extra == "skills"
|
|
30
|
+
Provides-Extra: pdf
|
|
31
|
+
Requires-Dist: pypdf>=5.3.0; extra == "pdf"
|
|
32
|
+
Provides-Extra: prometheus
|
|
33
|
+
Requires-Dist: prometheus-client>=0.19; extra == "prometheus"
|
|
34
|
+
Provides-Extra: statsd
|
|
35
|
+
Requires-Dist: statsd>=4.0; extra == "statsd"
|
|
36
|
+
Provides-Extra: otel
|
|
37
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
|
|
38
|
+
Provides-Extra: mcp
|
|
39
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
40
|
+
Provides-Extra: postgres
|
|
41
|
+
Requires-Dist: asyncpg>=0.29; extra == "postgres"
|
|
42
|
+
Provides-Extra: mysql
|
|
43
|
+
Requires-Dist: aiomysql>=0.2; extra == "mysql"
|
|
44
|
+
Provides-Extra: all
|
|
45
|
+
Requires-Dist: anthropic>=0.42.0; extra == "all"
|
|
46
|
+
Requires-Dist: openai>=1.52.0; extra == "all"
|
|
47
|
+
Requires-Dist: pyyaml>=6.0; extra == "all"
|
|
48
|
+
Requires-Dist: pypdf>=5.3.0; extra == "all"
|
|
49
|
+
Requires-Dist: prometheus-client>=0.19; extra == "all"
|
|
50
|
+
Requires-Dist: statsd>=4.0; extra == "all"
|
|
51
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == "all"
|
|
52
|
+
Requires-Dist: mcp>=1.0; extra == "all"
|
|
53
|
+
Requires-Dist: asyncpg>=0.29; extra == "all"
|
|
54
|
+
Requires-Dist: aiomysql>=0.2; extra == "all"
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
57
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
58
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
59
|
+
Requires-Dist: hypothesis>=6.0.0; extra == "dev"
|
|
60
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
61
|
+
Requires-Dist: mypy>=1.10.0; extra == "dev"
|
|
62
|
+
Requires-Dist: anthropic>=0.42.0; extra == "dev"
|
|
63
|
+
Requires-Dist: openai>=1.52.0; extra == "dev"
|
|
64
|
+
Requires-Dist: pyyaml>=6.0; extra == "dev"
|
|
65
|
+
Requires-Dist: pypdf>=5.3.0; extra == "dev"
|
|
66
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
|
|
67
|
+
Requires-Dist: prometheus-client>=0.19; extra == "dev"
|
|
68
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == "dev"
|
|
69
|
+
Requires-Dist: mcp>=1.0; extra == "dev"
|
|
70
|
+
Requires-Dist: asyncpg>=0.29; extra == "dev"
|
|
71
|
+
Requires-Dist: aiomysql>=0.2; extra == "dev"
|
|
72
|
+
Dynamic: license-file
|
|
73
|
+
|
|
74
|
+
# power-loop
|
|
75
|
+
|
|
76
|
+
[](https://pypi.org/project/power-loop/)
|
|
77
|
+
[](https://pypi.org/project/power-loop/)
|
|
78
|
+
[](https://github.com/PL-play/power-loop/actions/workflows/ci.yml)
|
|
79
|
+
[](LICENSE)
|
|
80
|
+
|
|
81
|
+
**English** · [中文](README.zh.md) · [Docs](docs/en/index.md) · [Examples](examples/README.md) · [Changelog](CHANGELOG.md)
|
|
82
|
+
|
|
83
|
+
> **Loop engineering, not framework adoption.** power-loop is an embeddable **agent execution kernel**: you engineer the agent *loop* — hooks at every lifecycle point, pluggable storage, sandbox seams, compaction, deterministic workflows — instead of building your app *inside* a framework. The loop itself is a **lightweight, stateless handle** over a **pluggable store** (SQLite by default — zero infrastructure — or PostgreSQL/MySQL by DSN). Out of it you get durable multi-turn sessions, tool calling, sub-agents, crash-resumable multi-agent workflows, durable timers, and process-level sandboxing. No service to run, no graph DSL to learn.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from power_loop import StatefulAgentLoop, create_llm_service_from_env
|
|
87
|
+
|
|
88
|
+
# The loop is a thin, stateless handle over a store. Default = one SQLite file (zero infra);
|
|
89
|
+
# swap dsn= to "postgresql://…/app" or "mysql://…/app" and nothing else changes.
|
|
90
|
+
loop = StatefulAgentLoop(llm=create_llm_service_from_env(), dsn="app.db")
|
|
91
|
+
sid = await loop.new_session()
|
|
92
|
+
await loop.send("Remember my favorite color is teal.", session_id=sid)
|
|
93
|
+
print((await loop.send("What's my favorite color?", session_id=sid)).final_text)
|
|
94
|
+
# → "Your favorite color is teal." (durable; survives restarts)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The conversation is already durable, resumable, and tool-capable. And because the loop holds **no authoritative state**, a fresh process resumes it from nothing but a DSN + the session id:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# Cold start, another process — reconstruct the loop and continue. No state to serialize/carry.
|
|
101
|
+
loop = StatefulAgentLoop(llm=create_llm_service_from_env(), dsn="app.db")
|
|
102
|
+
print((await loop.send("And my second-favorite?", session_id=sid)).final_text)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install 'power-loop[openai]' # or [anthropic] · add [postgres] / [mysql] for those backends
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> **1.0 — stable.** The public API is frozen under SemVer (a breaking change requires a major bump), machine-enforced by a baseline guard in CI. The **core has zero runtime dependencies** (pure stdlib; verified by a CI job that imports it with nothing else installed) — LLM transports *and database drivers* are optional extras. See [Stability](#stability--semver) and the [honest caveats](#honest-scope) — a young, single-maintainer project says so plainly.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Start here
|
|
114
|
+
|
|
115
|
+
| You are… | Go to |
|
|
116
|
+
|---|---|
|
|
117
|
+
| 🚀 **New** — show me the 5-minute version | [Getting Started](docs/en/getting-started.md) |
|
|
118
|
+
| 🛠️ **Learning by building** | [Tutorials](docs/en/tutorials/index.md) — chatbot · tools · human-in-the-loop · multi-agent |
|
|
119
|
+
| 🧩 **Browsing runnable code** | [40 examples](examples/README.md) — `00_hello_world.py` → full chatbot |
|
|
120
|
+
| 📚 **Looking something up** | [User Guide](docs/en/user-guide/index.md) · [API reference](docs/en/api/index.md) |
|
|
121
|
+
| 🤔 **Deciding if it fits** | [How it compares](#how-it-compares) · [Honest scope](#honest-scope) |
|
|
122
|
+
|
|
123
|
+
**Find your way by goal:** persist & resume across restarts → [Sessions](docs/en/user-guide/sessions.md) · pick a backend (SQLite/PG/MySQL) → [Storage backends](docs/en/user-guide/storage-backends.md) · give it tools → [Tools](docs/en/user-guide/tools.md) / [Extending](docs/en/user-guide/extending-tools.md) · multi-agent → [Workflows](docs/en/user-guide/workflows.md) · sandbox untrusted code → [Sandboxing](docs/en/user-guide/sandboxing.md) · monitor → [Observability](docs/en/user-guide/observability.md) · scale → [Scaling](docs/en/user-guide/scaling.md) · survive crashes → [Pending recovery](docs/en/user-guide/sessions.md#pending-recovery).
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Why power-loop — "loop engineering"
|
|
128
|
+
|
|
129
|
+
Most "agent frameworks" ask you to build your app *inside* them. power-loop is the opposite: a **library you embed**. You keep your HTTP layer, your auth, your queues, your RAG, your UI, your deploy. It runs the agent loop — and lets you *engineer* it.
|
|
130
|
+
|
|
131
|
+
- 🪶 **Featherweight & zero-dependency.** No `pydantic`, no LangChain, no graph DSL. A compact, pure-stdlib core (~20k lines) whose public surface is essentially one class — and **zero runtime dependencies**. LLM transports *and* the Postgres/MySQL drivers are pulled in only by the extra you install.
|
|
132
|
+
- 🗄️ **Pluggable storage, zero-infra default.** Sessions, timers, sub-agent trees, workflow journals, the shared blackboard — one backend-neutral store written once against a tiny `Database`/`Dialect` port. The default is **one SQLite file** (copy the file, you've copied the state); point a DSN at **PostgreSQL or MySQL** when you want a real multi-writer server — same code, same conformance suite. Tables are auto-created, or **provisioned out-of-band** with a printed DDL script (see [Storage backends](docs/en/user-guide/storage-backends.md)).
|
|
133
|
+
- ♻️ **Stateless, resumable loops.** A `StatefulAgentLoop` carries no authoritative state — all of it lives in the store. So a loop is cheap to create and trivially **restored from a DSN + a session id** (ideal for web handlers, workers, cold starts). It self-caches each session's active window (a rebuildable accelerator that never changes what the model sees) to skip re-reads on hot paths.
|
|
134
|
+
- ⏱️ **Durable by default.** Crash mid-run and `resume()`. Agents schedule their own **durable timers** that survive restarts. Workflows **replay finished steps and re-run only the unfinished tail** after a process death. The store survives version upgrades (a portable, backend-neutral migration-version table) and can be **pruned, VACUUMed, and exported**.
|
|
135
|
+
- 🧩 **Composable from one loop to a fleet.** Start with `send()`. Add tools. Spawn sub-agents. Fan out a deterministic **workflow** (`sequence`/`parallel`/`foreach`/`branch`). Run each leaf in its **own process and DB** behind a sandbox. Same primitives all the way up.
|
|
136
|
+
- 🛡️ **Isolation seams where it counts.** Tool-level sandboxing via a `ShellBackend` (drop in gVisor/Docker for `bash`); process-level via a `WorkerLauncher` (wrap a whole sub-agent worker per leaf). power-loop stays sandbox-agnostic; you choose the policy.
|
|
137
|
+
- 🔬 **Built to be observed.** Typed events for every stream chunk, tool call, round, and **individual LLM call** — each `seq`-ordered + monotonic-clock stamped. Pluggable sinks behind extras: durable **JSONL** (with `replay`), **Prometheus/StatsD** metrics, an **OpenTelemetry** span tree. Per-run + per-session token accounting and hard per-run budgets.
|
|
138
|
+
- 🔌 **Open ecosystem.** Provider-agnostic (any OpenAI-compatible endpoint or native Anthropic, by env var). Bring any tool via the `ToolRegistry`, or connect a **Model Context Protocol** server with one adapter.
|
|
139
|
+
- ✅ **Real-tested.** A dedicated `tests/real/` suite runs the library — workflows, resume, sandboxed subprocess agents, structured output, compaction, a live MCP server — against a real model; the storage layer has a **backend-agnostic conformance suite** run against SQLite, PostgreSQL, and MySQL.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## What you get
|
|
144
|
+
|
|
145
|
+
| Capability | One-liner | Docs |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| **Stateful sessions** | Durable multi-turn memory + resume by id, in SQLite/PG/MySQL | [Sessions](docs/en/user-guide/sessions.md) |
|
|
148
|
+
| **Pluggable backends** | One store, `dsn=` picks SQLite (default) / PostgreSQL / MySQL; configurable schema provisioning | [Storage backends](docs/en/user-guide/storage-backends.md) |
|
|
149
|
+
| **Stateless / resumable loop** | Loop holds no state; reconstruct from `dsn` + `session_id`; cheap to create | [Sessions](docs/en/user-guide/sessions.md) |
|
|
150
|
+
| **Tool calling** | JSON-Schema-validated tools; built-in `bash`/file/search/skills presets | [Tools](docs/en/user-guide/tools.md) · [Extending](docs/en/user-guide/extending-tools.md) |
|
|
151
|
+
| **Sub-agents** | Delegate to a child loop via `AgentSpec` (own prompt/tools/model) | [Sub-agents](docs/en/user-guide/subagents.md) |
|
|
152
|
+
| **Dynamic workflows** | JSON DSL (`sequence`/`parallel`/`foreach`/`branch`) the LLM can author; deterministic engine | [Workflows](docs/en/user-guide/workflows.md) |
|
|
153
|
+
| **Workflow resume** | Journals each step; after a crash, replays completed steps and re-runs only the tail | [Workflows](docs/en/user-guide/workflows.md) |
|
|
154
|
+
| **Process sandboxing** | Each workflow leaf in its own OS process + own DB; wrap each in gVisor/Docker per leaf | [Sandboxing](docs/en/user-guide/sandboxing.md) |
|
|
155
|
+
| **Durable timers** | Agents schedule their own wake-ups; survive restarts; one-shot or recurring | [Timers](docs/en/user-guide/timers.md) |
|
|
156
|
+
| **Context compaction** | Auto-summarize old turns (never splits a tool-call pair); `recall_compacted` to pull originals back | [Compaction](docs/en/user-guide/compaction.md) |
|
|
157
|
+
| **Durability ops** | Portable migration-version table, retention/prune, VACUUM, `export_session`/`import_session`, graceful `aclose()` | [Sessions](docs/en/user-guide/sessions.md) |
|
|
158
|
+
| **Observability** | Typed `seq`-ordered events → durable JSONL + `replay`, Prometheus/StatsD metrics, OpenTelemetry spans | [Observability](docs/en/user-guide/observability.md) |
|
|
159
|
+
| **MCP tools** | Surface a Model Context Protocol server's tools as power-loop tools | [Extending](docs/en/user-guide/extending-tools.md) |
|
|
160
|
+
| **Hooks & events** | Veto/observe at every lifecycle point; strongly-typed event payloads | [Hooks](docs/en/user-guide/hooks.md) · [Events](docs/en/user-guide/events.md) |
|
|
161
|
+
| **Structured output** | `output_schema` → provider `response_format` → parsed & validated | [Structured](docs/en/user-guide/structured-output.md) |
|
|
162
|
+
| **Pluggable memory** | Cross-session recall via a `MemoryProvider` Protocol | [Memory](docs/en/user-guide/memory.md) |
|
|
163
|
+
| **Retry / cancel / budgets** | Provider-aware retry, a unified cancellation token, hard per-run token caps | [Retry & Cancel](docs/en/user-guide/retry-cancel.md) |
|
|
164
|
+
| **Stable error codes** | Every `PowerLoopError` carries a frozen machine-readable `code` — branch on `exc.code` | [API: error codes](docs/en/api/index.md#error-codes) |
|
|
165
|
+
| **Crash recovery** | `heal_pending` / `resume` / `abort_pending` for runs killed mid tool-call | [Pending recovery](docs/en/user-guide/sessions.md#pending-recovery) |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Highlights
|
|
170
|
+
|
|
171
|
+
### Pluggable storage — SQLite by default, PostgreSQL/MySQL by DSN
|
|
172
|
+
|
|
173
|
+
The whole store (sessions, messages, timers, compaction journals, sub-agent trees, the blackboard) is written **once** against a tiny async `Database` + `Dialect` port. Pick the backend with a DSN; the code above it never changes.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from power_loop import StatefulAgentLoop, SchemaPolicy
|
|
177
|
+
|
|
178
|
+
StatefulAgentLoop(llm=llm, dsn="app.db") # SQLite (zero infra, default)
|
|
179
|
+
StatefulAgentLoop(llm=llm, dsn="postgresql://u:p@host/app") # PostgreSQL → pip install 'power-loop[postgres]'
|
|
180
|
+
StatefulAgentLoop(llm=llm, dsn="mysql://u:p@host/app", table_prefix="pl_") # MySQL → pip install 'power-loop[mysql]'
|
|
181
|
+
|
|
182
|
+
# Schema provisioning is a policy. AUTO_CREATE (default) creates tables if missing; VERIFY only
|
|
183
|
+
# checks and, if the schema is absent, raises with the EXACT DDL to run as a privileged user.
|
|
184
|
+
StatefulAgentLoop(llm=llm, dsn="postgresql://readonly@host/app", schema=SchemaPolicy.VERIFY)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
SQLite is a single-writer file (zero infra, shard across processes). PostgreSQL/MySQL are real **multi-writer** servers — per-session sequence allocation is correct across processes via a `SELECT … FOR UPDATE` row lock. The same backend-agnostic **conformance suite** runs against all three. See [Storage backends](docs/en/user-guide/storage-backends.md) for the per-backend DDL and provisioning options.
|
|
188
|
+
|
|
189
|
+
### Stateless, resumable loops
|
|
190
|
+
|
|
191
|
+
A `StatefulAgentLoop` is a *handle*, not a session. It owns no conversation state — that all lives in the store — so it's cheap to create and you resume any session by id from a cold process:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# Web handler / worker: build a loop per request, resume the user's session, done.
|
|
195
|
+
loop = StatefulAgentLoop(llm=create_llm_service_from_env(), dsn=DSN)
|
|
196
|
+
await loop.prewarm(session_id) # optional: pre-load the active window
|
|
197
|
+
result = await loop.send(user_text, session_id=session_id)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Under the hood the loop keeps a per-session **active-window cache** — but it caches only the *durable* projection, validated by a monotonic `next_seq` token, so it's a pure accelerator: a cold loop with an empty cache produces byte-for-byte the same prompts (proven by a warm-vs-cold conformance test, including the recall/compaction/prompt-edit edge cases).
|
|
201
|
+
|
|
202
|
+
### Deterministic multi-agent workflows — that the model can author, and that survive a crash
|
|
203
|
+
|
|
204
|
+
Sub-agent delegation is *model-driven* ("go do this"). When you want **code-driven, deterministic** orchestration — fan out over a list, branch on a result, run a pipeline — describe it as a `WorkflowSpec` and let the engine interpret it. The only LLM calls are the leaves; `sequence`/`parallel`/`foreach`/`branch` are plain code.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from power_loop.workflow import create_workflow
|
|
208
|
+
|
|
209
|
+
spec = {
|
|
210
|
+
"name": "research", "input": "the Japanese tea ceremony",
|
|
211
|
+
"root": {"type": "sequence", "steps": [
|
|
212
|
+
{"type": "agent", "id": "plan",
|
|
213
|
+
"spec": {"name": "planner", "system_prompt": "Break the topic into 3 subtopics."},
|
|
214
|
+
"output_schema": {"name": "Plan", "schema": {"type": "object", "required": ["subtopics"],
|
|
215
|
+
"properties": {"subtopics": {"type": "array", "items": {"type": "string"}}}}}},
|
|
216
|
+
{"type": "foreach", "id": "research", "items_from": "plan.subtopics", "as": "t",
|
|
217
|
+
"parallel": True, "max_concurrency": 3,
|
|
218
|
+
"body": {"type": "agent", "id": "r",
|
|
219
|
+
"spec": {"name": "researcher", "system_prompt": "Write 2 sentences on {{t}}."},
|
|
220
|
+
"input": "Subtopic: {{t}}"}},
|
|
221
|
+
{"type": "agent", "id": "write",
|
|
222
|
+
"spec": {"name": "writer", "system_prompt": "Synthesize the notes."},
|
|
223
|
+
"inputs_from": ["research"]},
|
|
224
|
+
]},
|
|
225
|
+
}
|
|
226
|
+
result = await create_workflow(spec, parent_loop=loop).run()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Validated on creation (every problem reported at once — ideal for an LLM to repair). Run it **detached** and the parent agent is woken on completion via a durable timer. Crash halfway through the fan-out? `resume_run(loop, parent_sid, run_id)` replays the planner + finished researchers from the journal and re-runs only what's left. Register it as a tool and the agent builds and submits workflows itself.
|
|
230
|
+
|
|
231
|
+
### Run untrusted sub-agents in real sandboxes — without sandboxing the parent
|
|
232
|
+
|
|
233
|
+
The default executor runs leaves in-process. The **subprocess executor** runs each leaf in its own OS process against its own SQLite file (the one-writer-per-file rule holds trivially), and a `WorkerLauncher` wraps that process — per leaf, by inspecting its granted tools — in gVisor / Docker / firejail.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from power_loop.workflow import SubprocessExecutor, WorkerBootstrap, create_workflow
|
|
237
|
+
|
|
238
|
+
ex = SubprocessExecutor(
|
|
239
|
+
bootstrap=WorkerBootstrap(llm_from_env=True, tool_preset="core"),
|
|
240
|
+
launcher=my_gvisor_launcher, # wraps the worker command per leaf; fail-closed
|
|
241
|
+
timeout_s=120,
|
|
242
|
+
)
|
|
243
|
+
await create_workflow(spec, parent_loop=loop, executor=ex).run()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Durable, operable storage — the part most "agent libraries" skip
|
|
247
|
+
|
|
248
|
+
The store is the product, so it's built to run for the long haul:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
await store.export_session(sid) # full session → a JSON archive (incl. compacted turns)
|
|
252
|
+
await store.prune_compacted_messages(sid) # opt-in retention of folded-out originals
|
|
253
|
+
await store.vacuum(); await store.checkpoint() # reclaim disk (SQLite; no-op where N/A)
|
|
254
|
+
async with StatefulAgentLoop(...) as loop: # graceful aclose(): drain in-flight sends, then close
|
|
255
|
+
...
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
It survives upgrades — a portable `pl_schema_migrations` version table (not a SQLite-only `PRAGMA`) refuses a newer-than-code DB rather than corrupting it, and works identically on every backend.
|
|
259
|
+
|
|
260
|
+
### Observe everything, export anywhere
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from power_loop.contrib.jsonl_sink import attach_jsonl_sink, replay
|
|
264
|
+
from power_loop.contrib.metrics_sink import attach_metrics_sink, PrometheusBackend
|
|
265
|
+
|
|
266
|
+
attach_jsonl_sink(bus, "events.jsonl") # durable; replay("events.jsonl") later
|
|
267
|
+
attach_metrics_sink(bus, PrometheusBackend()) # power-loop[prometheus] · or StatsD, or OpenTelemetry spans
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Every event carries a process-wide `seq` and a monotonic clock, so streams totally-order and reconstruct. Sync subscribers run inline by default; opt into a bounded-queue background dispatcher when a sink might block.
|
|
271
|
+
|
|
272
|
+
### Connect a Model Context Protocol server
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from power_loop.contrib.mcp import StdioMCPClient, register_mcp_tools # power-loop[mcp]
|
|
276
|
+
|
|
277
|
+
client = await StdioMCPClient("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/data"]).connect()
|
|
278
|
+
await register_mcp_tools(registry, client, prefix="fs.") # MCP tools → power-loop ToolDefinitions
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The seam is a tiny `MCPToolSource` Protocol, so the `mcp` SDK is optional and any client works.
|
|
282
|
+
|
|
283
|
+
> More: hard token budgets, structured output, crash recovery, memory, the blackboard — see [`examples/`](examples/README.md) (40 runnable programs) and the [docs](docs/en/index.md).
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## How it compares
|
|
288
|
+
|
|
289
|
+
power-loop is a **kernel**, not a platform — that's the whole trade-off.
|
|
290
|
+
|
|
291
|
+
- **vs. LangChain / LangGraph / LlamaIndex / CrewAI / AutoGen** — those are batteries-included frameworks with large ecosystems (connectors, vector stores, integrations) and heavy dependency trees. power-loop deliberately ships **none of that**: a compact (~20k-line) pure-stdlib core with zero runtime dependencies, and you bring your own tools (or an MCP server). You get durable sessions across SQLite/PG/MySQL, crash-resumable workflows, and real sandbox seams out of the box; you do **not** get a bundled RAG stack or 100 connectors.
|
|
292
|
+
- **Choose power-loop** when you want to *embed* an agent in an existing app, keep your dependency surface tiny, pick your own database, and care about durability + isolation + a stable contract.
|
|
293
|
+
- **Choose a framework** when you want batteries included, a big integration catalog, and don't mind the weight.
|
|
294
|
+
|
|
295
|
+
Honestly: power-loop is **behind on ecosystem breadth** (integrations, community, age) and **ahead on embeddability, durability, storage flexibility, and a machine-guarded stable API**. Pick accordingly.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Install & configure
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
pip install 'power-loop[openai]' # any OpenAI-compatible endpoint
|
|
303
|
+
pip install 'power-loop[anthropic]' # native Anthropic Messages API
|
|
304
|
+
pip install 'power-loop[postgres]' # PostgreSQL backend (asyncpg)
|
|
305
|
+
pip install 'power-loop[mysql]' # MySQL backend (aiomysql)
|
|
306
|
+
pip install 'power-loop[all]' # transports + postgres + mysql + skills/pdf/observability/mcp
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Point it at any OpenAI-compatible endpoint (or `POWER_LOOP_PROVIDER=anthropic`):
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
POWER_LOOP_BASE_URL=https://api.openai.com/v1
|
|
313
|
+
POWER_LOOP_API_KEY=sk-...
|
|
314
|
+
POWER_LOOP_MODEL=gpt-4o-mini
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Python 3.10+. See [Getting Started](docs/en/getting-started.md). Optional extras: `postgres`, `mysql`, `skills`, `pdf`, `prometheus`, `statsd`, `otel`, `mcp`.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Stability & SemVer
|
|
322
|
+
|
|
323
|
+
As of **1.0**, the **STABLE** API (listed in `power_loop.STABLE_API`) is under SemVer: a breaking change requires a major bump (`2.0.0`), enforced by a frozen-baseline test in CI — including the flagship `StatefulAgentLoop` *and the LLM contract needed to construct it*. Error `.code` strings are frozen too.
|
|
324
|
+
|
|
325
|
+
| Tier | Meaning |
|
|
326
|
+
|---|---|
|
|
327
|
+
| **Stable** | Backward-compatible within a major version; in `power_loop.STABLE_API`. |
|
|
328
|
+
| **Provisional** | Re-exported from the top level (e.g. `open_store`, `SchemaPolicy`); may change in a future minor. |
|
|
329
|
+
| **Internal** | `power_loop.core.*`, `power_loop.runtime.store.*` internals; no compatibility promise. |
|
|
330
|
+
|
|
331
|
+
See the [API reference](docs/en/api/index.md).
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Honest scope
|
|
336
|
+
|
|
337
|
+
power-loop **orchestrates; it does not, by itself, isolate.** The built-in `bash`/file tools run in-process and inherit the host environment — convenient for trusted, local use, **not a security boundary**. For untrusted/model-authored commands, inject a sandbox via the `ShellBackend` seam (tool-level) or run leaves through `SubprocessExecutor` + `WorkerLauncher` (process-level). Keep secrets in your orchestrator. See [SECURITY.md](SECURITY.md).
|
|
338
|
+
|
|
339
|
+
**Single-writer-per-session.** Per-session ordering is an in-process `asyncio.Lock`; it gives no cross-process mutual exclusion. With **SQLite**, run one writer process per file (shard sessions across files). With **PostgreSQL/MySQL**, sequence allocation is multi-writer-safe (`SELECT … FOR UPDATE`), but the *pending-state machine* still assumes one writer drives a given session at a time (the dispatcher/queue layer above is yours). Concurrent first-boot of a fresh server schema should provision out-of-band (`SchemaPolicy.VERIFY`). See the [scaling guide](docs/en/user-guide/scaling.md).
|
|
340
|
+
|
|
341
|
+
**Maturity.** A 1.0 tag here is a confidence statement about the **API/durability contract** — not a claim of years of field-hardening. power-loop is young, primarily a single maintainer, with limited public production track record. The contract is machine-guarded and the project is MIT + forkable; weigh the bus factor for your use.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Project & links
|
|
346
|
+
|
|
347
|
+
- **Used by:** DeepTalk — the agent runtime for a 1-on-1 relationship-IM product's in-conversation agents. *(Using it in production? PR a line here.)*
|
|
348
|
+
- **Develop:** `pip install -e ".[dev]"` · `ruff check .` · `pytest -q --no-real` (drop `--no-real` for the live-LLM suite; set a `POWER_LOOP_TEST_PG_DSN` / `POWER_LOOP_TEST_MYSQL_DSN` to run the server-backend conformance suites).
|
|
349
|
+
- [Docs](docs/en/index.md) · [Architecture](docs/en/architecture.md) · [Storage backends](docs/en/user-guide/storage-backends.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · [License](LICENSE)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# power-loop
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/power-loop/)
|
|
4
|
+
[](https://pypi.org/project/power-loop/)
|
|
5
|
+
[](https://github.com/PL-play/power-loop/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**English** · [中文](README.zh.md) · [Docs](docs/en/index.md) · [Examples](examples/README.md) · [Changelog](CHANGELOG.md)
|
|
9
|
+
|
|
10
|
+
> **Loop engineering, not framework adoption.** power-loop is an embeddable **agent execution kernel**: you engineer the agent *loop* — hooks at every lifecycle point, pluggable storage, sandbox seams, compaction, deterministic workflows — instead of building your app *inside* a framework. The loop itself is a **lightweight, stateless handle** over a **pluggable store** (SQLite by default — zero infrastructure — or PostgreSQL/MySQL by DSN). Out of it you get durable multi-turn sessions, tool calling, sub-agents, crash-resumable multi-agent workflows, durable timers, and process-level sandboxing. No service to run, no graph DSL to learn.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from power_loop import StatefulAgentLoop, create_llm_service_from_env
|
|
14
|
+
|
|
15
|
+
# The loop is a thin, stateless handle over a store. Default = one SQLite file (zero infra);
|
|
16
|
+
# swap dsn= to "postgresql://…/app" or "mysql://…/app" and nothing else changes.
|
|
17
|
+
loop = StatefulAgentLoop(llm=create_llm_service_from_env(), dsn="app.db")
|
|
18
|
+
sid = await loop.new_session()
|
|
19
|
+
await loop.send("Remember my favorite color is teal.", session_id=sid)
|
|
20
|
+
print((await loop.send("What's my favorite color?", session_id=sid)).final_text)
|
|
21
|
+
# → "Your favorite color is teal." (durable; survives restarts)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The conversation is already durable, resumable, and tool-capable. And because the loop holds **no authoritative state**, a fresh process resumes it from nothing but a DSN + the session id:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# Cold start, another process — reconstruct the loop and continue. No state to serialize/carry.
|
|
28
|
+
loop = StatefulAgentLoop(llm=create_llm_service_from_env(), dsn="app.db")
|
|
29
|
+
print((await loop.send("And my second-favorite?", session_id=sid)).final_text)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install 'power-loop[openai]' # or [anthropic] · add [postgres] / [mysql] for those backends
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> **1.0 — stable.** The public API is frozen under SemVer (a breaking change requires a major bump), machine-enforced by a baseline guard in CI. The **core has zero runtime dependencies** (pure stdlib; verified by a CI job that imports it with nothing else installed) — LLM transports *and database drivers* are optional extras. See [Stability](#stability--semver) and the [honest caveats](#honest-scope) — a young, single-maintainer project says so plainly.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Start here
|
|
41
|
+
|
|
42
|
+
| You are… | Go to |
|
|
43
|
+
|---|---|
|
|
44
|
+
| 🚀 **New** — show me the 5-minute version | [Getting Started](docs/en/getting-started.md) |
|
|
45
|
+
| 🛠️ **Learning by building** | [Tutorials](docs/en/tutorials/index.md) — chatbot · tools · human-in-the-loop · multi-agent |
|
|
46
|
+
| 🧩 **Browsing runnable code** | [40 examples](examples/README.md) — `00_hello_world.py` → full chatbot |
|
|
47
|
+
| 📚 **Looking something up** | [User Guide](docs/en/user-guide/index.md) · [API reference](docs/en/api/index.md) |
|
|
48
|
+
| 🤔 **Deciding if it fits** | [How it compares](#how-it-compares) · [Honest scope](#honest-scope) |
|
|
49
|
+
|
|
50
|
+
**Find your way by goal:** persist & resume across restarts → [Sessions](docs/en/user-guide/sessions.md) · pick a backend (SQLite/PG/MySQL) → [Storage backends](docs/en/user-guide/storage-backends.md) · give it tools → [Tools](docs/en/user-guide/tools.md) / [Extending](docs/en/user-guide/extending-tools.md) · multi-agent → [Workflows](docs/en/user-guide/workflows.md) · sandbox untrusted code → [Sandboxing](docs/en/user-guide/sandboxing.md) · monitor → [Observability](docs/en/user-guide/observability.md) · scale → [Scaling](docs/en/user-guide/scaling.md) · survive crashes → [Pending recovery](docs/en/user-guide/sessions.md#pending-recovery).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why power-loop — "loop engineering"
|
|
55
|
+
|
|
56
|
+
Most "agent frameworks" ask you to build your app *inside* them. power-loop is the opposite: a **library you embed**. You keep your HTTP layer, your auth, your queues, your RAG, your UI, your deploy. It runs the agent loop — and lets you *engineer* it.
|
|
57
|
+
|
|
58
|
+
- 🪶 **Featherweight & zero-dependency.** No `pydantic`, no LangChain, no graph DSL. A compact, pure-stdlib core (~20k lines) whose public surface is essentially one class — and **zero runtime dependencies**. LLM transports *and* the Postgres/MySQL drivers are pulled in only by the extra you install.
|
|
59
|
+
- 🗄️ **Pluggable storage, zero-infra default.** Sessions, timers, sub-agent trees, workflow journals, the shared blackboard — one backend-neutral store written once against a tiny `Database`/`Dialect` port. The default is **one SQLite file** (copy the file, you've copied the state); point a DSN at **PostgreSQL or MySQL** when you want a real multi-writer server — same code, same conformance suite. Tables are auto-created, or **provisioned out-of-band** with a printed DDL script (see [Storage backends](docs/en/user-guide/storage-backends.md)).
|
|
60
|
+
- ♻️ **Stateless, resumable loops.** A `StatefulAgentLoop` carries no authoritative state — all of it lives in the store. So a loop is cheap to create and trivially **restored from a DSN + a session id** (ideal for web handlers, workers, cold starts). It self-caches each session's active window (a rebuildable accelerator that never changes what the model sees) to skip re-reads on hot paths.
|
|
61
|
+
- ⏱️ **Durable by default.** Crash mid-run and `resume()`. Agents schedule their own **durable timers** that survive restarts. Workflows **replay finished steps and re-run only the unfinished tail** after a process death. The store survives version upgrades (a portable, backend-neutral migration-version table) and can be **pruned, VACUUMed, and exported**.
|
|
62
|
+
- 🧩 **Composable from one loop to a fleet.** Start with `send()`. Add tools. Spawn sub-agents. Fan out a deterministic **workflow** (`sequence`/`parallel`/`foreach`/`branch`). Run each leaf in its **own process and DB** behind a sandbox. Same primitives all the way up.
|
|
63
|
+
- 🛡️ **Isolation seams where it counts.** Tool-level sandboxing via a `ShellBackend` (drop in gVisor/Docker for `bash`); process-level via a `WorkerLauncher` (wrap a whole sub-agent worker per leaf). power-loop stays sandbox-agnostic; you choose the policy.
|
|
64
|
+
- 🔬 **Built to be observed.** Typed events for every stream chunk, tool call, round, and **individual LLM call** — each `seq`-ordered + monotonic-clock stamped. Pluggable sinks behind extras: durable **JSONL** (with `replay`), **Prometheus/StatsD** metrics, an **OpenTelemetry** span tree. Per-run + per-session token accounting and hard per-run budgets.
|
|
65
|
+
- 🔌 **Open ecosystem.** Provider-agnostic (any OpenAI-compatible endpoint or native Anthropic, by env var). Bring any tool via the `ToolRegistry`, or connect a **Model Context Protocol** server with one adapter.
|
|
66
|
+
- ✅ **Real-tested.** A dedicated `tests/real/` suite runs the library — workflows, resume, sandboxed subprocess agents, structured output, compaction, a live MCP server — against a real model; the storage layer has a **backend-agnostic conformance suite** run against SQLite, PostgreSQL, and MySQL.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## What you get
|
|
71
|
+
|
|
72
|
+
| Capability | One-liner | Docs |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| **Stateful sessions** | Durable multi-turn memory + resume by id, in SQLite/PG/MySQL | [Sessions](docs/en/user-guide/sessions.md) |
|
|
75
|
+
| **Pluggable backends** | One store, `dsn=` picks SQLite (default) / PostgreSQL / MySQL; configurable schema provisioning | [Storage backends](docs/en/user-guide/storage-backends.md) |
|
|
76
|
+
| **Stateless / resumable loop** | Loop holds no state; reconstruct from `dsn` + `session_id`; cheap to create | [Sessions](docs/en/user-guide/sessions.md) |
|
|
77
|
+
| **Tool calling** | JSON-Schema-validated tools; built-in `bash`/file/search/skills presets | [Tools](docs/en/user-guide/tools.md) · [Extending](docs/en/user-guide/extending-tools.md) |
|
|
78
|
+
| **Sub-agents** | Delegate to a child loop via `AgentSpec` (own prompt/tools/model) | [Sub-agents](docs/en/user-guide/subagents.md) |
|
|
79
|
+
| **Dynamic workflows** | JSON DSL (`sequence`/`parallel`/`foreach`/`branch`) the LLM can author; deterministic engine | [Workflows](docs/en/user-guide/workflows.md) |
|
|
80
|
+
| **Workflow resume** | Journals each step; after a crash, replays completed steps and re-runs only the tail | [Workflows](docs/en/user-guide/workflows.md) |
|
|
81
|
+
| **Process sandboxing** | Each workflow leaf in its own OS process + own DB; wrap each in gVisor/Docker per leaf | [Sandboxing](docs/en/user-guide/sandboxing.md) |
|
|
82
|
+
| **Durable timers** | Agents schedule their own wake-ups; survive restarts; one-shot or recurring | [Timers](docs/en/user-guide/timers.md) |
|
|
83
|
+
| **Context compaction** | Auto-summarize old turns (never splits a tool-call pair); `recall_compacted` to pull originals back | [Compaction](docs/en/user-guide/compaction.md) |
|
|
84
|
+
| **Durability ops** | Portable migration-version table, retention/prune, VACUUM, `export_session`/`import_session`, graceful `aclose()` | [Sessions](docs/en/user-guide/sessions.md) |
|
|
85
|
+
| **Observability** | Typed `seq`-ordered events → durable JSONL + `replay`, Prometheus/StatsD metrics, OpenTelemetry spans | [Observability](docs/en/user-guide/observability.md) |
|
|
86
|
+
| **MCP tools** | Surface a Model Context Protocol server's tools as power-loop tools | [Extending](docs/en/user-guide/extending-tools.md) |
|
|
87
|
+
| **Hooks & events** | Veto/observe at every lifecycle point; strongly-typed event payloads | [Hooks](docs/en/user-guide/hooks.md) · [Events](docs/en/user-guide/events.md) |
|
|
88
|
+
| **Structured output** | `output_schema` → provider `response_format` → parsed & validated | [Structured](docs/en/user-guide/structured-output.md) |
|
|
89
|
+
| **Pluggable memory** | Cross-session recall via a `MemoryProvider` Protocol | [Memory](docs/en/user-guide/memory.md) |
|
|
90
|
+
| **Retry / cancel / budgets** | Provider-aware retry, a unified cancellation token, hard per-run token caps | [Retry & Cancel](docs/en/user-guide/retry-cancel.md) |
|
|
91
|
+
| **Stable error codes** | Every `PowerLoopError` carries a frozen machine-readable `code` — branch on `exc.code` | [API: error codes](docs/en/api/index.md#error-codes) |
|
|
92
|
+
| **Crash recovery** | `heal_pending` / `resume` / `abort_pending` for runs killed mid tool-call | [Pending recovery](docs/en/user-guide/sessions.md#pending-recovery) |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Highlights
|
|
97
|
+
|
|
98
|
+
### Pluggable storage — SQLite by default, PostgreSQL/MySQL by DSN
|
|
99
|
+
|
|
100
|
+
The whole store (sessions, messages, timers, compaction journals, sub-agent trees, the blackboard) is written **once** against a tiny async `Database` + `Dialect` port. Pick the backend with a DSN; the code above it never changes.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from power_loop import StatefulAgentLoop, SchemaPolicy
|
|
104
|
+
|
|
105
|
+
StatefulAgentLoop(llm=llm, dsn="app.db") # SQLite (zero infra, default)
|
|
106
|
+
StatefulAgentLoop(llm=llm, dsn="postgresql://u:p@host/app") # PostgreSQL → pip install 'power-loop[postgres]'
|
|
107
|
+
StatefulAgentLoop(llm=llm, dsn="mysql://u:p@host/app", table_prefix="pl_") # MySQL → pip install 'power-loop[mysql]'
|
|
108
|
+
|
|
109
|
+
# Schema provisioning is a policy. AUTO_CREATE (default) creates tables if missing; VERIFY only
|
|
110
|
+
# checks and, if the schema is absent, raises with the EXACT DDL to run as a privileged user.
|
|
111
|
+
StatefulAgentLoop(llm=llm, dsn="postgresql://readonly@host/app", schema=SchemaPolicy.VERIFY)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
SQLite is a single-writer file (zero infra, shard across processes). PostgreSQL/MySQL are real **multi-writer** servers — per-session sequence allocation is correct across processes via a `SELECT … FOR UPDATE` row lock. The same backend-agnostic **conformance suite** runs against all three. See [Storage backends](docs/en/user-guide/storage-backends.md) for the per-backend DDL and provisioning options.
|
|
115
|
+
|
|
116
|
+
### Stateless, resumable loops
|
|
117
|
+
|
|
118
|
+
A `StatefulAgentLoop` is a *handle*, not a session. It owns no conversation state — that all lives in the store — so it's cheap to create and you resume any session by id from a cold process:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Web handler / worker: build a loop per request, resume the user's session, done.
|
|
122
|
+
loop = StatefulAgentLoop(llm=create_llm_service_from_env(), dsn=DSN)
|
|
123
|
+
await loop.prewarm(session_id) # optional: pre-load the active window
|
|
124
|
+
result = await loop.send(user_text, session_id=session_id)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Under the hood the loop keeps a per-session **active-window cache** — but it caches only the *durable* projection, validated by a monotonic `next_seq` token, so it's a pure accelerator: a cold loop with an empty cache produces byte-for-byte the same prompts (proven by a warm-vs-cold conformance test, including the recall/compaction/prompt-edit edge cases).
|
|
128
|
+
|
|
129
|
+
### Deterministic multi-agent workflows — that the model can author, and that survive a crash
|
|
130
|
+
|
|
131
|
+
Sub-agent delegation is *model-driven* ("go do this"). When you want **code-driven, deterministic** orchestration — fan out over a list, branch on a result, run a pipeline — describe it as a `WorkflowSpec` and let the engine interpret it. The only LLM calls are the leaves; `sequence`/`parallel`/`foreach`/`branch` are plain code.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from power_loop.workflow import create_workflow
|
|
135
|
+
|
|
136
|
+
spec = {
|
|
137
|
+
"name": "research", "input": "the Japanese tea ceremony",
|
|
138
|
+
"root": {"type": "sequence", "steps": [
|
|
139
|
+
{"type": "agent", "id": "plan",
|
|
140
|
+
"spec": {"name": "planner", "system_prompt": "Break the topic into 3 subtopics."},
|
|
141
|
+
"output_schema": {"name": "Plan", "schema": {"type": "object", "required": ["subtopics"],
|
|
142
|
+
"properties": {"subtopics": {"type": "array", "items": {"type": "string"}}}}}},
|
|
143
|
+
{"type": "foreach", "id": "research", "items_from": "plan.subtopics", "as": "t",
|
|
144
|
+
"parallel": True, "max_concurrency": 3,
|
|
145
|
+
"body": {"type": "agent", "id": "r",
|
|
146
|
+
"spec": {"name": "researcher", "system_prompt": "Write 2 sentences on {{t}}."},
|
|
147
|
+
"input": "Subtopic: {{t}}"}},
|
|
148
|
+
{"type": "agent", "id": "write",
|
|
149
|
+
"spec": {"name": "writer", "system_prompt": "Synthesize the notes."},
|
|
150
|
+
"inputs_from": ["research"]},
|
|
151
|
+
]},
|
|
152
|
+
}
|
|
153
|
+
result = await create_workflow(spec, parent_loop=loop).run()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Validated on creation (every problem reported at once — ideal for an LLM to repair). Run it **detached** and the parent agent is woken on completion via a durable timer. Crash halfway through the fan-out? `resume_run(loop, parent_sid, run_id)` replays the planner + finished researchers from the journal and re-runs only what's left. Register it as a tool and the agent builds and submits workflows itself.
|
|
157
|
+
|
|
158
|
+
### Run untrusted sub-agents in real sandboxes — without sandboxing the parent
|
|
159
|
+
|
|
160
|
+
The default executor runs leaves in-process. The **subprocess executor** runs each leaf in its own OS process against its own SQLite file (the one-writer-per-file rule holds trivially), and a `WorkerLauncher` wraps that process — per leaf, by inspecting its granted tools — in gVisor / Docker / firejail.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from power_loop.workflow import SubprocessExecutor, WorkerBootstrap, create_workflow
|
|
164
|
+
|
|
165
|
+
ex = SubprocessExecutor(
|
|
166
|
+
bootstrap=WorkerBootstrap(llm_from_env=True, tool_preset="core"),
|
|
167
|
+
launcher=my_gvisor_launcher, # wraps the worker command per leaf; fail-closed
|
|
168
|
+
timeout_s=120,
|
|
169
|
+
)
|
|
170
|
+
await create_workflow(spec, parent_loop=loop, executor=ex).run()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Durable, operable storage — the part most "agent libraries" skip
|
|
174
|
+
|
|
175
|
+
The store is the product, so it's built to run for the long haul:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
await store.export_session(sid) # full session → a JSON archive (incl. compacted turns)
|
|
179
|
+
await store.prune_compacted_messages(sid) # opt-in retention of folded-out originals
|
|
180
|
+
await store.vacuum(); await store.checkpoint() # reclaim disk (SQLite; no-op where N/A)
|
|
181
|
+
async with StatefulAgentLoop(...) as loop: # graceful aclose(): drain in-flight sends, then close
|
|
182
|
+
...
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
It survives upgrades — a portable `pl_schema_migrations` version table (not a SQLite-only `PRAGMA`) refuses a newer-than-code DB rather than corrupting it, and works identically on every backend.
|
|
186
|
+
|
|
187
|
+
### Observe everything, export anywhere
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from power_loop.contrib.jsonl_sink import attach_jsonl_sink, replay
|
|
191
|
+
from power_loop.contrib.metrics_sink import attach_metrics_sink, PrometheusBackend
|
|
192
|
+
|
|
193
|
+
attach_jsonl_sink(bus, "events.jsonl") # durable; replay("events.jsonl") later
|
|
194
|
+
attach_metrics_sink(bus, PrometheusBackend()) # power-loop[prometheus] · or StatsD, or OpenTelemetry spans
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Every event carries a process-wide `seq` and a monotonic clock, so streams totally-order and reconstruct. Sync subscribers run inline by default; opt into a bounded-queue background dispatcher when a sink might block.
|
|
198
|
+
|
|
199
|
+
### Connect a Model Context Protocol server
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from power_loop.contrib.mcp import StdioMCPClient, register_mcp_tools # power-loop[mcp]
|
|
203
|
+
|
|
204
|
+
client = await StdioMCPClient("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/data"]).connect()
|
|
205
|
+
await register_mcp_tools(registry, client, prefix="fs.") # MCP tools → power-loop ToolDefinitions
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The seam is a tiny `MCPToolSource` Protocol, so the `mcp` SDK is optional and any client works.
|
|
209
|
+
|
|
210
|
+
> More: hard token budgets, structured output, crash recovery, memory, the blackboard — see [`examples/`](examples/README.md) (40 runnable programs) and the [docs](docs/en/index.md).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## How it compares
|
|
215
|
+
|
|
216
|
+
power-loop is a **kernel**, not a platform — that's the whole trade-off.
|
|
217
|
+
|
|
218
|
+
- **vs. LangChain / LangGraph / LlamaIndex / CrewAI / AutoGen** — those are batteries-included frameworks with large ecosystems (connectors, vector stores, integrations) and heavy dependency trees. power-loop deliberately ships **none of that**: a compact (~20k-line) pure-stdlib core with zero runtime dependencies, and you bring your own tools (or an MCP server). You get durable sessions across SQLite/PG/MySQL, crash-resumable workflows, and real sandbox seams out of the box; you do **not** get a bundled RAG stack or 100 connectors.
|
|
219
|
+
- **Choose power-loop** when you want to *embed* an agent in an existing app, keep your dependency surface tiny, pick your own database, and care about durability + isolation + a stable contract.
|
|
220
|
+
- **Choose a framework** when you want batteries included, a big integration catalog, and don't mind the weight.
|
|
221
|
+
|
|
222
|
+
Honestly: power-loop is **behind on ecosystem breadth** (integrations, community, age) and **ahead on embeddability, durability, storage flexibility, and a machine-guarded stable API**. Pick accordingly.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Install & configure
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pip install 'power-loop[openai]' # any OpenAI-compatible endpoint
|
|
230
|
+
pip install 'power-loop[anthropic]' # native Anthropic Messages API
|
|
231
|
+
pip install 'power-loop[postgres]' # PostgreSQL backend (asyncpg)
|
|
232
|
+
pip install 'power-loop[mysql]' # MySQL backend (aiomysql)
|
|
233
|
+
pip install 'power-loop[all]' # transports + postgres + mysql + skills/pdf/observability/mcp
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Point it at any OpenAI-compatible endpoint (or `POWER_LOOP_PROVIDER=anthropic`):
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
POWER_LOOP_BASE_URL=https://api.openai.com/v1
|
|
240
|
+
POWER_LOOP_API_KEY=sk-...
|
|
241
|
+
POWER_LOOP_MODEL=gpt-4o-mini
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Python 3.10+. See [Getting Started](docs/en/getting-started.md). Optional extras: `postgres`, `mysql`, `skills`, `pdf`, `prometheus`, `statsd`, `otel`, `mcp`.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Stability & SemVer
|
|
249
|
+
|
|
250
|
+
As of **1.0**, the **STABLE** API (listed in `power_loop.STABLE_API`) is under SemVer: a breaking change requires a major bump (`2.0.0`), enforced by a frozen-baseline test in CI — including the flagship `StatefulAgentLoop` *and the LLM contract needed to construct it*. Error `.code` strings are frozen too.
|
|
251
|
+
|
|
252
|
+
| Tier | Meaning |
|
|
253
|
+
|---|---|
|
|
254
|
+
| **Stable** | Backward-compatible within a major version; in `power_loop.STABLE_API`. |
|
|
255
|
+
| **Provisional** | Re-exported from the top level (e.g. `open_store`, `SchemaPolicy`); may change in a future minor. |
|
|
256
|
+
| **Internal** | `power_loop.core.*`, `power_loop.runtime.store.*` internals; no compatibility promise. |
|
|
257
|
+
|
|
258
|
+
See the [API reference](docs/en/api/index.md).
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Honest scope
|
|
263
|
+
|
|
264
|
+
power-loop **orchestrates; it does not, by itself, isolate.** The built-in `bash`/file tools run in-process and inherit the host environment — convenient for trusted, local use, **not a security boundary**. For untrusted/model-authored commands, inject a sandbox via the `ShellBackend` seam (tool-level) or run leaves through `SubprocessExecutor` + `WorkerLauncher` (process-level). Keep secrets in your orchestrator. See [SECURITY.md](SECURITY.md).
|
|
265
|
+
|
|
266
|
+
**Single-writer-per-session.** Per-session ordering is an in-process `asyncio.Lock`; it gives no cross-process mutual exclusion. With **SQLite**, run one writer process per file (shard sessions across files). With **PostgreSQL/MySQL**, sequence allocation is multi-writer-safe (`SELECT … FOR UPDATE`), but the *pending-state machine* still assumes one writer drives a given session at a time (the dispatcher/queue layer above is yours). Concurrent first-boot of a fresh server schema should provision out-of-band (`SchemaPolicy.VERIFY`). See the [scaling guide](docs/en/user-guide/scaling.md).
|
|
267
|
+
|
|
268
|
+
**Maturity.** A 1.0 tag here is a confidence statement about the **API/durability contract** — not a claim of years of field-hardening. power-loop is young, primarily a single maintainer, with limited public production track record. The contract is machine-guarded and the project is MIT + forkable; weigh the bus factor for your use.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Project & links
|
|
273
|
+
|
|
274
|
+
- **Used by:** DeepTalk — the agent runtime for a 1-on-1 relationship-IM product's in-conversation agents. *(Using it in production? PR a line here.)*
|
|
275
|
+
- **Develop:** `pip install -e ".[dev]"` · `ruff check .` · `pytest -q --no-real` (drop `--no-real` for the live-LLM suite; set a `POWER_LOOP_TEST_PG_DSN` / `POWER_LOOP_TEST_MYSQL_DSN` to run the server-backend conformance suites).
|
|
276
|
+
- [Docs](docs/en/index.md) · [Architecture](docs/en/architecture.md) · [Storage backends](docs/en/user-guide/storage-backends.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · [License](LICENSE)
|