power-loop 1.0.0__tar.gz → 2.0.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 (102) hide show
  1. power_loop-2.0.0/PKG-INFO +349 -0
  2. power_loop-2.0.0/README.md +276 -0
  3. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/__init__.py +17 -10
  4. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/anthropic_factory.py +46 -0
  5. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/interface.py +7 -4
  6. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/llm_factory.py +11 -0
  7. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/agent/sink.py +37 -27
  8. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/agent/stateful_loop.py +352 -112
  9. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/event_payloads.py +4 -0
  10. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contrib/metrics_sink.py +6 -1
  11. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/core/events.py +14 -5
  12. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/core/pipeline.py +48 -28
  13. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/core/state.py +10 -1
  14. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/blackboard.py +19 -13
  15. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/budget.py +7 -0
  16. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/compact.py +20 -10
  17. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/env.py +10 -3
  18. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/notes.py +10 -9
  19. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/retry.py +15 -0
  20. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/runtime_state.py +9 -6
  21. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/session_store.py +59 -147
  22. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/spec.py +9 -7
  23. power_loop-2.0.0/power_loop/runtime/store/__init__.py +58 -0
  24. power_loop-2.0.0/power_loop/runtime/store/backends/__init__.py +1 -0
  25. power_loop-2.0.0/power_loop/runtime/store/backends/mysql.py +135 -0
  26. power_loop-2.0.0/power_loop/runtime/store/backends/postgres.py +85 -0
  27. power_loop-2.0.0/power_loop/runtime/store/backends/sqlite.py +146 -0
  28. power_loop-2.0.0/power_loop/runtime/store/capabilities.py +27 -0
  29. power_loop-2.0.0/power_loop/runtime/store/db.py +64 -0
  30. power_loop-2.0.0/power_loop/runtime/store/dialect.py +366 -0
  31. power_loop-2.0.0/power_loop/runtime/store/factory.py +59 -0
  32. power_loop-2.0.0/power_loop/runtime/store/schema.py +169 -0
  33. power_loop-2.0.0/power_loop/runtime/store/store.py +1212 -0
  34. power_loop-2.0.0/power_loop/runtime/store/types.py +174 -0
  35. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/timers.py +19 -15
  36. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/tools/__init__.py +25 -10
  37. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/tools/blackboard.py +3 -3
  38. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/tools/default_tools.py +241 -79
  39. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/engine.py +23 -14
  40. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/introspect.py +8 -8
  41. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/journal.py +22 -22
  42. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/result.py +4 -1
  43. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/resume.py +14 -14
  44. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/runner.py +58 -30
  45. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/spec.py +145 -37
  46. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/subprocess_executor.py +16 -1
  47. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/tool.py +2 -2
  48. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/worker.py +3 -3
  49. power_loop-2.0.0/power_loop.egg-info/PKG-INFO +349 -0
  50. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop.egg-info/SOURCES.txt +12 -0
  51. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop.egg-info/requires.txt +10 -0
  52. {power_loop-1.0.0 → power_loop-2.0.0}/pyproject.toml +7 -0
  53. power_loop-1.0.0/PKG-INFO +0 -295
  54. power_loop-1.0.0/README.md +0 -230
  55. power_loop-1.0.0/power_loop.egg-info/PKG-INFO +0 -295
  56. {power_loop-1.0.0 → power_loop-2.0.0}/LICENSE +0 -0
  57. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/__init__.py +0 -0
  58. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/__init__.py +0 -0
  59. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  60. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  61. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  62. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  63. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/agent/__init__.py +0 -0
  64. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/agent/follow_up.py +0 -0
  65. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/agent/system_prompt.py +0 -0
  66. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/agent/types.py +0 -0
  67. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/__init__.py +0 -0
  68. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/errors.py +0 -0
  69. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/events.py +0 -0
  70. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/handlers.py +0 -0
  71. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/hook_contexts.py +0 -0
  72. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/hooks.py +0 -0
  73. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/messages.py +0 -0
  74. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/protocols.py +0 -0
  75. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contracts/tools.py +0 -0
  76. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contrib/__init__.py +0 -0
  77. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contrib/_redact.py +0 -0
  78. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contrib/jsonl_sink.py +0 -0
  79. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contrib/logging_sink.py +0 -0
  80. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contrib/mcp.py +0 -0
  81. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/contrib/otel_sink.py +0 -0
  82. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/core/agent_context.py +0 -0
  83. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/core/hooks.py +0 -0
  84. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/core/phase.py +0 -0
  85. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/core/runner.py +0 -0
  86. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/py.typed +0 -0
  87. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/cancellation.py +0 -0
  88. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/exec_backend.py +0 -0
  89. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/human_input.py +0 -0
  90. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/memory.py +0 -0
  91. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/provider.py +0 -0
  92. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/skills.py +0 -0
  93. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/structured.py +0 -0
  94. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/runtime/stub_provider.py +0 -0
  95. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/tools/default_manifest.py +0 -0
  96. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/tools/registry.py +0 -0
  97. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/tools/spawn_agent.py +0 -0
  98. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/__init__.py +0 -0
  99. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop/workflow/api.py +0 -0
  100. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop.egg-info/dependency_links.txt +0 -0
  101. {power_loop-1.0.0 → power_loop-2.0.0}/power_loop.egg-info/top_level.txt +0 -0
  102. {power_loop-1.0.0 → power_loop-2.0.0}/setup.cfg +0 -0
@@ -0,0 +1,349 @@
1
+ Metadata-Version: 2.4
2
+ Name: power-loop
3
+ Version: 2.0.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
+ [![PyPI](https://img.shields.io/pypi/v/power-loop.svg)](https://pypi.org/project/power-loop/)
77
+ [![Python](https://img.shields.io/pypi/pyversions/power-loop.svg)](https://pypi.org/project/power-loop/)
78
+ [![CI](https://github.com/PL-play/power-loop/actions/workflows/ci.yml/badge.svg)](https://github.com/PL-play/power-loop/actions/workflows/ci.yml)
79
+ [![License](https://img.shields.io/badge/license-see%20LICENSE-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/power-loop.svg)](https://pypi.org/project/power-loop/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/power-loop.svg)](https://pypi.org/project/power-loop/)
5
+ [![CI](https://github.com/PL-play/power-loop/actions/workflows/ci.yml/badge.svg)](https://github.com/PL-play/power-loop/actions/workflows/ci.yml)
6
+ [![License](https://img.shields.io/badge/license-see%20LICENSE-blue.svg)](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)