keelrun 0.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.
- keelrun-0.1.0/PKG-INFO +10 -0
- keelrun-0.1.0/README.md +92 -0
- keelrun-0.1.0/pyproject.toml +40 -0
- keelrun-0.1.0/setup.cfg +4 -0
- keelrun-0.1.0/src/keel/__init__.py +26 -0
- keelrun-0.1.0/src/keel/__main__.py +7 -0
- keelrun-0.1.0/src/keel/_backend.py +149 -0
- keelrun-0.1.0/src/keel/_defaults.py +116 -0
- keelrun-0.1.0/src/keel/_discovery.py +368 -0
- keelrun-0.1.0/src/keel/_errors.py +31 -0
- keelrun-0.1.0/src/keel/_flow.py +327 -0
- keelrun-0.1.0/src/keel/_hook.py +156 -0
- keelrun-0.1.0/src/keel/_policy.py +136 -0
- keelrun-0.1.0/src/keel/_record.py +266 -0
- keelrun-0.1.0/src/keel/_run.py +131 -0
- keelrun-0.1.0/src/keel/_runtime.py +62 -0
- keelrun-0.1.0/src/keel/_sim.py +268 -0
- keelrun-0.1.0/src/keel/_targets.py +214 -0
- keelrun-0.1.0/src/keel/_wrap.py +157 -0
- keelrun-0.1.0/src/keel/adapters/__init__.py +178 -0
- keelrun-0.1.0/src/keel/adapters/_http.py +527 -0
- keelrun-0.1.0/src/keel/adapters/_llm_policy.py +290 -0
- keelrun-0.1.0/src/keel/adapters/_pack.py +53 -0
- keelrun-0.1.0/src/keel/adapters/aiohttp_pack.py +437 -0
- keelrun-0.1.0/src/keel/adapters/boto3_pack.py +360 -0
- keelrun-0.1.0/src/keel/adapters/httpx_pack.py +653 -0
- keelrun-0.1.0/src/keel/adapters/psycopg_pack.py +317 -0
- keelrun-0.1.0/src/keel/adapters/requests_pack.py +390 -0
- keelrun-0.1.0/src/keel/adapters/urllib3_pack.py +342 -0
- keelrun-0.1.0/src/keel/bootstrap.py +210 -0
- keelrun-0.1.0/src/keel/packs/__init__.py +111 -0
- keelrun-0.1.0/src/keel/packs/_framework.py +66 -0
- keelrun-0.1.0/src/keel/packs/_provider.py +95 -0
- keelrun-0.1.0/src/keel/packs/adk_pack.py +356 -0
- keelrun-0.1.0/src/keel/packs/anthropic_pack.py +41 -0
- keelrun-0.1.0/src/keel/packs/crewai_pack.py +243 -0
- keelrun-0.1.0/src/keel/packs/google_genai_pack.py +76 -0
- keelrun-0.1.0/src/keel/packs/langgraph_pack.py +590 -0
- keelrun-0.1.0/src/keel/packs/llm.py +145 -0
- keelrun-0.1.0/src/keel/packs/mcp_pack.py +485 -0
- keelrun-0.1.0/src/keel/packs/openai_agents_pack.py +208 -0
- keelrun-0.1.0/src/keel/packs/openai_pack.py +41 -0
- keelrun-0.1.0/src/keel/packs/pydantic_ai_pack.py +192 -0
- keelrun-0.1.0/src/keel/packs/tool.py +307 -0
- keelrun-0.1.0/src/keel/testing.py +181 -0
- keelrun-0.1.0/src/keel_core_stub/__init__.py +744 -0
- keelrun-0.1.0/src/keelrun.egg-info/PKG-INFO +10 -0
- keelrun-0.1.0/src/keelrun.egg-info/SOURCES.txt +90 -0
- keelrun-0.1.0/src/keelrun.egg-info/dependency_links.txt +1 -0
- keelrun-0.1.0/src/keelrun.egg-info/entry_points.txt +2 -0
- keelrun-0.1.0/src/keelrun.egg-info/requires.txt +5 -0
- keelrun-0.1.0/src/keelrun.egg-info/top_level.txt +2 -0
- keelrun-0.1.0/tests/test_adapters_aiohttp.py +357 -0
- keelrun-0.1.0/tests/test_adapters_boto3.py +354 -0
- keelrun-0.1.0/tests/test_adapters_disable.py +159 -0
- keelrun-0.1.0/tests/test_adapters_http.py +249 -0
- keelrun-0.1.0/tests/test_adapters_httpx.py +281 -0
- keelrun-0.1.0/tests/test_adapters_psycopg.py +293 -0
- keelrun-0.1.0/tests/test_adapters_requests.py +213 -0
- keelrun-0.1.0/tests/test_adapters_urllib3.py +233 -0
- keelrun-0.1.0/tests/test_demos.py +90 -0
- keelrun-0.1.0/tests/test_discovery.py +311 -0
- keelrun-0.1.0/tests/test_flaky_demo.py +45 -0
- keelrun-0.1.0/tests/test_flows.py +727 -0
- keelrun-0.1.0/tests/test_hook.py +90 -0
- keelrun-0.1.0/tests/test_journal_policy.py +85 -0
- keelrun-0.1.0/tests/test_layer_resolution.py +201 -0
- keelrun-0.1.0/tests/test_llm_budget_fallback.py +192 -0
- keelrun-0.1.0/tests/test_llm_policy.py +155 -0
- keelrun-0.1.0/tests/test_native_adapters.py +203 -0
- keelrun-0.1.0/tests/test_packs_adk.py +335 -0
- keelrun-0.1.0/tests/test_packs_crewai.py +309 -0
- keelrun-0.1.0/tests/test_packs_langgraph.py +581 -0
- keelrun-0.1.0/tests/test_packs_llm.py +337 -0
- keelrun-0.1.0/tests/test_packs_llm_e2e.py +183 -0
- keelrun-0.1.0/tests/test_packs_mcp.py +500 -0
- keelrun-0.1.0/tests/test_packs_openai_agents.py +249 -0
- keelrun-0.1.0/tests/test_packs_pydantic_ai.py +241 -0
- keelrun-0.1.0/tests/test_packs_tool.py +303 -0
- keelrun-0.1.0/tests/test_persistent_cache.py +79 -0
- keelrun-0.1.0/tests/test_policy.py +92 -0
- keelrun-0.1.0/tests/test_record.py +175 -0
- keelrun-0.1.0/tests/test_record_capture.py +73 -0
- keelrun-0.1.0/tests/test_resume_demo.py +165 -0
- keelrun-0.1.0/tests/test_run.py +237 -0
- keelrun-0.1.0/tests/test_sim.py +234 -0
- keelrun-0.1.0/tests/test_stub_validation.py +224 -0
- keelrun-0.1.0/tests/test_targets.py +219 -0
- keelrun-0.1.0/tests/test_testing.py +147 -0
- keelrun-0.1.0/tests/test_vendored_stub_parity.py +30 -0
- keelrun-0.1.0/tests/test_wrap.py +163 -0
- keelrun-0.1.0/tests/test_wrap_native.py +138 -0
keelrun-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keelrun
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Keel — resilience as a zero-config Python front end (Tier 1 bootstrap, import hook, discovery).
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: keelrun-core==0.1.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: httpx<0.29,>=0.27; extra == "dev"
|
|
10
|
+
Requires-Dist: requests<2.33,>=2.31; extra == "dev"
|
keelrun-0.1.0/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# keel (Python front end)
|
|
2
|
+
|
|
3
|
+
Production-grade resilience for any Python program, with **zero code changes**.
|
|
4
|
+
`keel run app.py` intercepts your outbound calls (httpx, requests, aiohttp,
|
|
5
|
+
urllib3, boto3, psycopg, LLM SDKs via `llm:` packs, `tool:`/`mcp:` targets,
|
|
6
|
+
agent-framework packs, and `py:` function targets) and applies retries, backoff,
|
|
7
|
+
timeouts, circuit breakers, rate limits, and an optional dev-mode response
|
|
8
|
+
cache — all declared in one `keel.toml`, enforced by the native Rust core
|
|
9
|
+
inside your process. No daemon, no port, no login.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
$ cd python/keel && pip install -e . && keelrun-py-run app.py
|
|
13
|
+
keel ▸ wrapped 14 call sites (httpx ×9, openai ×4) with production defaults — `keel init` to customize
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(Not yet published to any registry; run from source. Published name will be
|
|
17
|
+
`keelrun` — see `docs/naming-decision.md`.)
|
|
18
|
+
|
|
19
|
+
Uninstalling Keel removes the behavior and nothing else: your code runs
|
|
20
|
+
identically (minus resilience). No imports, no context objects, no base classes.
|
|
21
|
+
|
|
22
|
+
## Backends
|
|
23
|
+
|
|
24
|
+
Keel resolves a backend at startup (`KEEL_BACKEND=auto|native|stub`):
|
|
25
|
+
|
|
26
|
+
- **native** (`keel_core`) — the PyO3 module bundling the Rust core. Required
|
|
27
|
+
for the persistent dev cache and for Tier 2 durable flows. Built from
|
|
28
|
+
`crates/keel-py` (`maturin develop`); prebuilt wheels are not published yet.
|
|
29
|
+
- **stub** — a pure-Python core (the conformance reference). Tier 1 semantics
|
|
30
|
+
only; no journal, so no persistent cache and no flows.
|
|
31
|
+
|
|
32
|
+
`auto` (the default) uses the native core when importable, else the stub.
|
|
33
|
+
|
|
34
|
+
## Tier 2 — durable flows (Level 2)
|
|
35
|
+
|
|
36
|
+
Designate an entrypoint in `keel.toml` and `keel run` executes it as a durable
|
|
37
|
+
flow: every intercepted call inside is journaled, and a rerun after a crash
|
|
38
|
+
substitutes already-completed steps from the journal instead of re-firing them.
|
|
39
|
+
|
|
40
|
+
```toml
|
|
41
|
+
[flows]
|
|
42
|
+
entrypoints = ["py:pipeline:main"] # module `pipeline`, function `main`
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Crash it mid-run (`kill -9`), re-run the same command, and it resumes from where
|
|
46
|
+
it stopped. `keel flows` shows resumable/completed flows; `keel trace <flow>`
|
|
47
|
+
shows the step ledger.
|
|
48
|
+
|
|
49
|
+
### v0.1 limitations (precise, never silent)
|
|
50
|
+
|
|
51
|
+
Durability is a promise; a silent downgrade would be a Level 0 surprise. So this
|
|
52
|
+
is a hard, actionable error rather than a quiet fallback:
|
|
53
|
+
|
|
54
|
+
- **A journal is required.** Tier 2 replay lives in `.keel/journal.db`. If the
|
|
55
|
+
native core can't attach one (e.g. `KEEL_JOURNAL=""` or an unwritable dir), a
|
|
56
|
+
designated flow fails at startup with a config-level **KEEL-E005**
|
|
57
|
+
(unsupported-configuration) naming the cause and fix — it does not run
|
|
58
|
+
un-journaled. (The stub backend, which has no
|
|
59
|
+
journal at all, reports the same class of error, pointing you at the native
|
|
60
|
+
core.)
|
|
61
|
+
|
|
62
|
+
This is enforced before any effect fires.
|
|
63
|
+
|
|
64
|
+
### Async flow bodies
|
|
65
|
+
|
|
66
|
+
An `async def` flow entrypoint is supported: `keel run` drives it with
|
|
67
|
+
`asyncio.run`, and its `await`ed intercepted calls route through the **same**
|
|
68
|
+
open flow handle a synchronous flow's calls use — journaled and replayed
|
|
69
|
+
identically, with full Tier 1 resilience per step. Concurrent awaited effects
|
|
70
|
+
inside one flow (`asyncio.gather`) are admitted — and therefore journaled — in
|
|
71
|
+
the order their calls *reach* the flow handle, never in completion order, so
|
|
72
|
+
replay always reproduces the same step sequence. Keep fan-out order
|
|
73
|
+
deterministic (await sequentially, or fan out in a fixed, data-independent
|
|
74
|
+
order): if the runtime reaches the handle in a different order on resume
|
|
75
|
+
(racing tasks whose scheduling differs run-to-run), that is nondeterminism,
|
|
76
|
+
handled per `flows.on_nondeterminism` like any other divergence.
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
Every Keel error carries a stable `KEEL-E0NN` code (see `keel explain <code>`),
|
|
81
|
+
a human first line (what / why / next), and — for machines — a `--json` twin on
|
|
82
|
+
the CLI. On terminal failure the original exception propagates unchanged, with a
|
|
83
|
+
`keel_outcome` attachment for those who look.
|
|
84
|
+
|
|
85
|
+
## Testing
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
python3 -m unittest discover python/keel # front-end suite (stub; native legs run if keel_core is built)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Native-only tests (flows, persistent cache, native adapters) skip cleanly when
|
|
92
|
+
`keel_core` is not built.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# Published as `keelrun` (docs/naming-decision.md); the IMPORT name stays
|
|
7
|
+
# `keel` (see [tool.setuptools.packages.find] below) — only the PyPI
|
|
8
|
+
# distribution name changes.
|
|
9
|
+
name = "keelrun"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
description = "Keel — resilience as a zero-config Python front end (Tier 1 bootstrap, import hook, discovery)."
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
# The native core wheel (import name `keel_core`, distribution name
|
|
15
|
+
# `keelrun-core`), version-locked to this front end. When it is absent at
|
|
16
|
+
# runtime (exotic platform, failed install), the vendored pure-Python stub
|
|
17
|
+
# (`keel_core_stub`, shipped in this wheel and byte-identical to
|
|
18
|
+
# python/keel-core-stub — checked by scripts/check-release-metadata.sh) takes
|
|
19
|
+
# over, so there is never a compile. Beyond that, runtime is stdlib-only:
|
|
20
|
+
# adapters activate only when their library is already present in the
|
|
21
|
+
# environment, and never import an absent one.
|
|
22
|
+
dependencies = ["keelrun-core==0.1.0"]
|
|
23
|
+
|
|
24
|
+
# Test-only dependencies (the adapter contract-test farm). Install with
|
|
25
|
+
# `pip install -e '.[dev]'` or `uv pip install httpx requests`. The library
|
|
26
|
+
# versions the packs certify are pinned in each pack's `_PINNED`; these ranges
|
|
27
|
+
# track them. Development for Task 10 used httpx 0.28.1 + requests 2.32.5 on
|
|
28
|
+
# CPython 3.14 (system interpreter).
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["httpx>=0.27,<0.29", "requests>=2.31,<2.33"]
|
|
31
|
+
|
|
32
|
+
# Internal run entry. The public `keel run` CLI (Task 8) dispatches here for
|
|
33
|
+
# Python entrypoints; `keelrun-py-run app.py [args...]` runs a script directly.
|
|
34
|
+
[project.scripts]
|
|
35
|
+
keelrun-py-run = "keel._run:main_run_entry"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
# `keel*` also picks up the vendored `keel_core_stub` fallback (see above).
|
|
40
|
+
include = ["keel*"]
|
keelrun-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Keel — the Python front end (Tier 1: resilience, zero code changes).
|
|
2
|
+
|
|
3
|
+
`keel run app.py` installs, before user code loads: (1) a `sys.meta_path`
|
|
4
|
+
import hook that wraps functions matching `py:` policy targets, and (2)
|
|
5
|
+
per-call discovery recording to `.keel/discovery.db`. The resilience
|
|
6
|
+
semantics live in the backend (`keel_core` native module when importable,
|
|
7
|
+
else the in-repo pure-Python `keel_core_stub`); this package is the thin,
|
|
8
|
+
stdlib-only front end that drives it.
|
|
9
|
+
|
|
10
|
+
DX invariants (docs/dx-spec.md §3) this package must uphold:
|
|
11
|
+
1. zero code changes in user code;
|
|
12
|
+
2. uninstall = remove the package (no lock-in): see `uninstall_keel`;
|
|
13
|
+
4. silence on success (one stderr banner at startup, then quiet);
|
|
14
|
+
5. never swallow errors — the original exception propagates unchanged,
|
|
15
|
+
with a `keel_outcome` attachment for those who look;
|
|
16
|
+
and KEEL_DISABLE=1 makes a run byte-identical to one without Keel.
|
|
17
|
+
|
|
18
|
+
The public entry points are `keel.bootstrap.install_keel` /
|
|
19
|
+
`uninstall_keel` and the `keel._run` runner used by `python -m keel run`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
__all__ = ["__version__"]
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Backend selection, isolated behind one seam (architecture §5.1, "the swap").
|
|
2
|
+
|
|
3
|
+
A backend exposes `configure(policy)`, `execute(request, effect)`, and
|
|
4
|
+
`report()` — the four-operation core surface the stub defines (advance_clock
|
|
5
|
+
is test-only and unused here). Selection:
|
|
6
|
+
|
|
7
|
+
* native (`keel_core`) — the eventual PyO3 module (Task 6/14); probed by
|
|
8
|
+
import and required to expose `configure`/`execute`. It may not exist
|
|
9
|
+
yet, so a failed import is normal.
|
|
10
|
+
* stub (`keel_core_stub.KeelCoreStub`) — the in-repo pure-Python core.
|
|
11
|
+
|
|
12
|
+
`KEEL_BACKEND` overrides selection:
|
|
13
|
+
* `stub` → force the stub, never probe native
|
|
14
|
+
* `native` → require `keel_core`; raise KEEL-E040 if not loadable
|
|
15
|
+
* `auto` / unset → native if loadable, else the stub
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Mapping, Protocol, runtime_checkable
|
|
24
|
+
|
|
25
|
+
from ._errors import KeelError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class Backend(Protocol):
|
|
30
|
+
def configure(self, policy: dict[str, Any]) -> None: ...
|
|
31
|
+
|
|
32
|
+
def execute(self, request: dict[str, Any], effect: Any) -> dict[str, Any]: ...
|
|
33
|
+
|
|
34
|
+
def report(self) -> dict[str, Any]: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _NativeBackend:
|
|
38
|
+
"""Front-end adapter over the native ``keel_core`` core, adding one thing the
|
|
39
|
+
PyO3 surface does not expose: ``layer(target, key)``, resolved from the
|
|
40
|
+
configured policy exactly as the stub's ``_layer`` / Node's
|
|
41
|
+
``NativeBackend.layer`` do. The adapter packs read ``backend.layer`` to honor
|
|
42
|
+
``idempotency.header`` and gate cache-body buffering; without this the knob is
|
|
43
|
+
dead under native (a Python/Node parity break). Every other attribute
|
|
44
|
+
(``execute``/``execute_async``/``report``/``persistent``/the flow surface)
|
|
45
|
+
delegates straight through, so the swap is transparent."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, core: Any) -> None:
|
|
48
|
+
self._core = core
|
|
49
|
+
self._policy: dict[str, Any] = {}
|
|
50
|
+
|
|
51
|
+
def configure(self, policy: dict[str, Any]) -> None:
|
|
52
|
+
self._policy = policy if isinstance(policy, dict) else {}
|
|
53
|
+
self._core.configure(policy)
|
|
54
|
+
|
|
55
|
+
def layer(self, target: str, key: str) -> Any:
|
|
56
|
+
t = self._policy.get("target")
|
|
57
|
+
if isinstance(t, dict) and isinstance(t.get(target), dict) and key in t[target]:
|
|
58
|
+
return t[target][key]
|
|
59
|
+
defaults = self._policy.get("defaults")
|
|
60
|
+
if not isinstance(defaults, dict):
|
|
61
|
+
return None
|
|
62
|
+
if target.startswith("llm:"):
|
|
63
|
+
llm = defaults.get("llm")
|
|
64
|
+
if isinstance(llm, dict) and key in llm:
|
|
65
|
+
return llm[key]
|
|
66
|
+
outbound = defaults.get("outbound")
|
|
67
|
+
return outbound.get(key) if isinstance(outbound, dict) else None
|
|
68
|
+
|
|
69
|
+
def __getattr__(self, name: str) -> Any:
|
|
70
|
+
# Delegate everything else to the native core (execute, execute_async,
|
|
71
|
+
# report, persistent, enter_flow/exit_flow/journal_*, advance_clock, …).
|
|
72
|
+
return getattr(self._core, name)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _journal_path(cwd: str | Path | None, env: Mapping[str, str]) -> str | None:
|
|
76
|
+
"""Where the native core attaches its journal (persistent dev cache + Tier 2).
|
|
77
|
+
`KEEL_JOURNAL` overrides the path; an explicit empty value disables it. This
|
|
78
|
+
is the *construction-time* default: keel.toml's `journal` key replaces it at
|
|
79
|
+
configure time unless KEEL_JOURNAL is set, in which case the env wins
|
|
80
|
+
(see ``bootstrap.apply_journal_env_override``)."""
|
|
81
|
+
override = env.get("KEEL_JOURNAL")
|
|
82
|
+
if override is not None:
|
|
83
|
+
return override or None # empty string ⇒ no journal
|
|
84
|
+
base = Path(cwd) if cwd is not None else Path.cwd()
|
|
85
|
+
return str(base / ".keel" / "journal.db")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _try_load_native(journal_path: str | None) -> Backend | None:
|
|
89
|
+
try:
|
|
90
|
+
mod = importlib.import_module("keel_core")
|
|
91
|
+
except ImportError:
|
|
92
|
+
return None
|
|
93
|
+
ctor = getattr(mod, "KeelCore", None) or getattr(mod, "Core", None)
|
|
94
|
+
if ctor is None:
|
|
95
|
+
return None
|
|
96
|
+
inst = None
|
|
97
|
+
if journal_path:
|
|
98
|
+
try:
|
|
99
|
+
inst = ctor(journal_path=journal_path) # attaches the persistent journal
|
|
100
|
+
except Exception:
|
|
101
|
+
inst = None # journal open failed — fall back to an in-memory native core
|
|
102
|
+
if inst is None:
|
|
103
|
+
inst = ctor()
|
|
104
|
+
if callable(getattr(inst, "configure", None)) and callable(getattr(inst, "execute", None)):
|
|
105
|
+
# The native KeelCore exposes configure/execute/execute_async/report/
|
|
106
|
+
# persistent; wrap it only to add `layer(target, key)` (idempotency.header
|
|
107
|
+
# + cache-ttl gate parity), delegating everything else.
|
|
108
|
+
return _NativeBackend(inst) # type: ignore[return-value]
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_backend(
|
|
113
|
+
preferred: str | None = None,
|
|
114
|
+
*,
|
|
115
|
+
cwd: str | Path | None = None,
|
|
116
|
+
env: Mapping[str, str] | None = None,
|
|
117
|
+
) -> Backend:
|
|
118
|
+
"""Resolve the runtime backend per `KEEL_BACKEND` (or `preferred`). Under a
|
|
119
|
+
native backend, a journal is attached at `<cwd>/.keel/journal.db` (created on
|
|
120
|
+
demand) so the dev cache's `scope=persistent` replays across runs."""
|
|
121
|
+
environ = env if env is not None else os.environ
|
|
122
|
+
choice = (preferred if preferred is not None else environ.get("KEEL_BACKEND", "auto")) or "auto"
|
|
123
|
+
if choice not in ("auto", "native", "stub"):
|
|
124
|
+
# A user env-var mistake, not a Keel bug — E001 (config-level), not E040.
|
|
125
|
+
raise KeelError(
|
|
126
|
+
"KEEL-E001",
|
|
127
|
+
f"KEEL_BACKEND must be one of auto, native, or stub (got {choice!r}); "
|
|
128
|
+
"unset it or correct the value",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if choice != "stub":
|
|
132
|
+
native = _try_load_native(_journal_path(cwd, environ))
|
|
133
|
+
if native is not None:
|
|
134
|
+
return native
|
|
135
|
+
if choice == "native":
|
|
136
|
+
# A missing build/install is a user-environment problem, not a Keel
|
|
137
|
+
# bug, and not a malformed policy: the configuration is valid, the
|
|
138
|
+
# capability is absent — E005 (unsupported-configuration) with a
|
|
139
|
+
# concrete next step, never E040 ("file an issue").
|
|
140
|
+
raise KeelError(
|
|
141
|
+
"KEEL-E005",
|
|
142
|
+
"KEEL_BACKEND=native requires the keel_core native module, which is not "
|
|
143
|
+
"installed; build it (`maturin develop -m crates/keel-py/Cargo.toml`) or "
|
|
144
|
+
"unset KEEL_BACKEND to use the pure-Python stub",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
from keel_core_stub import KeelCoreStub
|
|
148
|
+
|
|
149
|
+
return KeelCoreStub()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Level 0 embedded smart-defaults pack (DX spec §1) + the policy-merge helper.
|
|
2
|
+
|
|
3
|
+
The policy applied when no keel.toml is present. It MIRRORS
|
|
4
|
+
contracts/defaults.toml verbatim (that file is the frozen source of truth);
|
|
5
|
+
we embed rather than read it so the installed package is self-contained
|
|
6
|
+
(stdlib-only, works offline). Drift against the contract is caught by the
|
|
7
|
+
parity test, which parses contracts/defaults.toml with `tomllib` and asserts
|
|
8
|
+
deep equality when the repo file is present.
|
|
9
|
+
|
|
10
|
+
`apply_pack_defaults` layers these defaults (and any provider-pack fragments)
|
|
11
|
+
UNDER the user's keel.toml — the Python twin of `applyPackDefaults` in
|
|
12
|
+
node/keel/src/defaults.mjs; with no pack fragments it is identical to the Node
|
|
13
|
+
twin, and both front ends must agree (merge parity is a cross-language
|
|
14
|
+
contract).
|
|
15
|
+
|
|
16
|
+
The Level 0 hard rules — never change success-path semantics; never retry
|
|
17
|
+
non-idempotent calls; do nothing if a call can't be wrapped safely — are
|
|
18
|
+
BEHAVIOR, not config, and are enforced in the front end / backend, not here.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from copy import deepcopy
|
|
24
|
+
from typing import Any, Iterable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def outbound_defaults() -> dict[str, Any]:
|
|
28
|
+
"""`[defaults.outbound]` — any intercepted network call."""
|
|
29
|
+
return {
|
|
30
|
+
"timeout": "30s",
|
|
31
|
+
"retry": {
|
|
32
|
+
"attempts": 3,
|
|
33
|
+
"schedule": "exp(200ms, x2, max 30s, jitter)",
|
|
34
|
+
"on": ["conn", "timeout", "429", "5xx"],
|
|
35
|
+
},
|
|
36
|
+
"breaker": {"failures": 5, "cooldown": "15s"},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def llm_defaults() -> dict[str, Any]:
|
|
41
|
+
"""`[defaults.llm]` — any `llm:*` target; the generic LLM pack layer.
|
|
42
|
+
|
|
43
|
+
Retry-After-aware retry (retry on conn/timeout/429/5xx; the core waits
|
|
44
|
+
`max(schedule_wait, retry_after)`) plus the dev-loop response cache
|
|
45
|
+
(`cache = { mode = "dev" }`, resolved to a concrete ttl off-prod and
|
|
46
|
+
dropped in prod by `keel.packs.llm.resolve_dev_cache`).
|
|
47
|
+
"""
|
|
48
|
+
return {
|
|
49
|
+
"timeout": "120s",
|
|
50
|
+
"retry": {
|
|
51
|
+
"attempts": 6,
|
|
52
|
+
"schedule": "exp(500ms, x2, max 60s, jitter)",
|
|
53
|
+
"on": ["conn", "timeout", "429", "5xx"],
|
|
54
|
+
},
|
|
55
|
+
"breaker": {"failures": 5, "cooldown": "30s"},
|
|
56
|
+
"cache": {"mode": "dev"},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def level0_defaults() -> dict[str, Any]:
|
|
61
|
+
"""The embedded Level 0 policy, as the dict the backend's `configure`
|
|
62
|
+
expects (identical shape to keel.toml parsed to JSON)."""
|
|
63
|
+
return {"defaults": {"outbound": outbound_defaults(), "llm": llm_defaults()}}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _table(v: Any) -> dict[str, Any]:
|
|
67
|
+
return v if isinstance(v, dict) else {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def apply_pack_defaults(
|
|
71
|
+
policy: dict[str, Any],
|
|
72
|
+
pack_fragments: Iterable[dict[str, Any]] = (),
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
"""Merge Level 0 defaults, then provider-pack fragments, then the user
|
|
75
|
+
policy — precedence ``defaults < packs < user`` — filling in any
|
|
76
|
+
``defaults.outbound`` / ``defaults.llm`` key a higher layer did not set,
|
|
77
|
+
while a higher layer replaces a key it DOES set wholesale (the
|
|
78
|
+
per-layer-wholesale semantics of the defaults.toml header, matching the
|
|
79
|
+
engine's own per-key layer resolution).
|
|
80
|
+
|
|
81
|
+
Target tables are left untouched — the engine resolves target →
|
|
82
|
+
defaults.llm → defaults.outbound precedence per key at execute time.
|
|
83
|
+
|
|
84
|
+
Provider packs (``keel.packs.openai_pack`` / ``anthropic_pack``) currently
|
|
85
|
+
emit the generic ``[defaults.llm]`` layer, so folding their fragments is the
|
|
86
|
+
identity over the embedded defaults; the parameter is the seam by which a
|
|
87
|
+
future per-provider refinement would take effect.
|
|
88
|
+
|
|
89
|
+
Returns a NEW policy; the input is never mutated. Idempotent on
|
|
90
|
+
``level0_defaults()``. Mirrors Node's ``applyPackDefaults`` (empty
|
|
91
|
+
``pack_fragments`` → identical result).
|
|
92
|
+
"""
|
|
93
|
+
out = deepcopy(policy) if isinstance(policy, dict) else {}
|
|
94
|
+
user_defaults = _table(out.get("defaults"))
|
|
95
|
+
|
|
96
|
+
pack_outbound: dict[str, Any] = {}
|
|
97
|
+
pack_llm: dict[str, Any] = {}
|
|
98
|
+
for frag in pack_fragments:
|
|
99
|
+
frag_defaults = _table(frag.get("defaults")) if isinstance(frag, dict) else {}
|
|
100
|
+
pack_outbound.update(_table(frag_defaults.get("outbound")))
|
|
101
|
+
pack_llm.update(_table(frag_defaults.get("llm")))
|
|
102
|
+
|
|
103
|
+
out["defaults"] = {
|
|
104
|
+
**user_defaults,
|
|
105
|
+
"outbound": {
|
|
106
|
+
**outbound_defaults(),
|
|
107
|
+
**pack_outbound,
|
|
108
|
+
**_table(user_defaults.get("outbound")),
|
|
109
|
+
},
|
|
110
|
+
"llm": {
|
|
111
|
+
**llm_defaults(),
|
|
112
|
+
**pack_llm,
|
|
113
|
+
**_table(user_defaults.get("llm")),
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
return out
|