agent-saga 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.
- agent_saga-0.1.0/.claude/settings.local.json +61 -0
- agent_saga-0.1.0/.github/workflows/bench.yml +106 -0
- agent_saga-0.1.0/.gitignore +8 -0
- agent_saga-0.1.0/.vscode/settings.json +7 -0
- agent_saga-0.1.0/CONTRIBUTING.md +58 -0
- agent_saga-0.1.0/LICENSE +661 -0
- agent_saga-0.1.0/PKG-INFO +243 -0
- agent_saga-0.1.0/README.md +192 -0
- agent_saga-0.1.0/SECURITY.md +47 -0
- agent_saga-0.1.0/agent_saga/__init__.py +127 -0
- agent_saga-0.1.0/agent_saga/adapters/__init__.py +11 -0
- agent_saga-0.1.0/agent_saga/adapters/_common.py +53 -0
- agent_saga-0.1.0/agent_saga/adapters/autogen.py +96 -0
- agent_saga-0.1.0/agent_saga/adapters/crewai.py +131 -0
- agent_saga-0.1.0/agent_saga/adapters/langgraph.py +172 -0
- agent_saga-0.1.0/agent_saga/adapters/llamaindex.py +104 -0
- agent_saga-0.1.0/agent_saga/adapters/openai_agents.py +120 -0
- agent_saga-0.1.0/agent_saga/cli.py +99 -0
- agent_saga-0.1.0/agent_saga/connectors/__init__.py +27 -0
- agent_saga-0.1.0/agent_saga/connectors/_secrets.py +139 -0
- agent_saga-0.1.0/agent_saga/connectors/postgres.py +404 -0
- agent_saga-0.1.0/agent_saga/connectors/salesforce.py +188 -0
- agent_saga-0.1.0/agent_saga/connectors/stripe.py +137 -0
- agent_saga-0.1.0/agent_saga/context.py +384 -0
- agent_saga-0.1.0/agent_saga/decorator.py +147 -0
- agent_saga-0.1.0/agent_saga/durable.py +258 -0
- agent_saga-0.1.0/agent_saga/encryption.py +152 -0
- agent_saga-0.1.0/agent_saga/gate.py +147 -0
- agent_saga-0.1.0/agent_saga/gc.py +205 -0
- agent_saga-0.1.0/agent_saga/locks.py +90 -0
- agent_saga-0.1.0/agent_saga/recovery.py +378 -0
- agent_saga-0.1.0/agent_saga/registry.py +62 -0
- agent_saga-0.1.0/agent_saga/semantics.py +136 -0
- agent_saga-0.1.0/agent_saga/snapshot.py +185 -0
- agent_saga-0.1.0/agent_saga/ui/__init__.py +5 -0
- agent_saga-0.1.0/agent_saga/ui/__main__.py +11 -0
- agent_saga-0.1.0/agent_saga/ui/reader.py +367 -0
- agent_saga-0.1.0/agent_saga/ui/server.py +149 -0
- agent_saga-0.1.0/agent_saga/ui/templates/dashboard.html +438 -0
- agent_saga-0.1.0/agent_saga/wal.py +266 -0
- agent_saga-0.1.0/bench/bench_core.py +248 -0
- agent_saga-0.1.0/bench/results.json +128 -0
- agent_saga-0.1.0/bench/summarize.py +69 -0
- agent_saga-0.1.0/examples/demo.py +221 -0
- agent_saga-0.1.0/launch/show-hn.md +174 -0
- agent_saga-0.1.0/pyproject.toml +56 -0
- agent_saga-0.1.0/site/index.html +506 -0
- agent_saga-0.1.0/tests/conftest.py +13 -0
- agent_saga-0.1.0/tests/crash_worker.py +71 -0
- agent_saga-0.1.0/tests/test_adapters_crewai_openai.py +303 -0
- agent_saga-0.1.0/tests/test_adapters_llamaindex_autogen.py +232 -0
- agent_saga-0.1.0/tests/test_connectors.py +670 -0
- agent_saga-0.1.0/tests/test_core.py +596 -0
- agent_saga-0.1.0/tests/test_durable.py +246 -0
- agent_saga-0.1.0/tests/test_encryption.py +183 -0
- agent_saga-0.1.0/tests/test_gc.py +174 -0
- agent_saga-0.1.0/tests/test_langgraph_adapter.py +303 -0
- agent_saga-0.1.0/tests/test_locks.py +69 -0
- agent_saga-0.1.0/tests/test_recovery.py +360 -0
- agent_saga-0.1.0/tests/test_snapshot.py +309 -0
- agent_saga-0.1.0/tests/test_ui.py +386 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"PowerShell(python --version)",
|
|
5
|
+
"PowerShell(python -c \"import sys; print\\(sys.executable\\)\")",
|
|
6
|
+
"PowerShell(cd \"D:\\\\AI Error\"; \\(Get-Content bench/results.json -Raw | ConvertFrom-Json\\).concurrency | Select-Object concurrency, total_ops, fsync_barriers, amortization, ops_per_sec | Format-Table -AutoSize)",
|
|
7
|
+
"PowerShell(cd \"D:\\\\AI Error\"; python bench/bench_core.py --samples 10000 2>&1 | Select-Object -Last 26; \\(Get-Content bench/results.json -Raw | ConvertFrom-Json\\).concurrency | Select-Object concurrency, total_ops, fsyncs, group_commit_factor, ops_per_sec, p99_ms | Format-Table -AutoSize)",
|
|
8
|
+
"PowerShell(cd \"D:\\\\AI Error\"; python -m pytest -q 2>&1 | Select-Object -Last 8; python examples/demo.py 2>&1 | Select-String -Pattern \"charge \\\\$2,500|allowed|BLOCK|no-large\" | Select-Object -First 5)",
|
|
9
|
+
"Bash(python -m pytest -q)",
|
|
10
|
+
"Bash(python -c ' *)",
|
|
11
|
+
"PowerShell(cd \"D:\\\\AI Error\"; python -m pytest -q 2>&1 | Select-Object -Last 4; echo \"EXIT: $LASTEXITCODE\")",
|
|
12
|
+
"PowerShell(cd \"D:\\\\AI Error\"; python -m pytest -q 2>&1 | Select-Object -Last 3; echo \"EXIT: $LASTEXITCODE\")",
|
|
13
|
+
"PowerShell(cd \"D:\\\\AI Error\"; python -m pytest tests/test_ui.py -q 2>&1 | Select-Object -Last 3; echo \"EXIT: $LASTEXITCODE\")",
|
|
14
|
+
"PowerShell(cd \"D:\\\\AI Error\"; $env:PYTHONTRACEMALLOC=25; python -m pytest tests/test_ui.py::test_http_endpoints_serve_dashboard_and_api -W error 2>&1 | Select-String -Pattern \"test_ui.py:|http|client|socket\\\\.py|urlopen\" | Select-Object -First 12; $env:PYTHONTRACEMALLOC=\"\")",
|
|
15
|
+
"Bash(python -m agent_saga.cli ui --wal-path ./demo-agent-saga.wal --port 8137)",
|
|
16
|
+
"Bash(curl -s http://127.0.0.1:8137/api/sagas)",
|
|
17
|
+
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('sagas:', d['total']\\); [print\\(' ', s['status'], s['step_count'],'steps', round\\(s['total_latency_ms'],2\\),'ms'\\) for s in d['sagas']]\")",
|
|
18
|
+
"Bash(netstat -ano)",
|
|
19
|
+
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('sagas:', d['total']\\); [print\\(' ', s['status'], '|', s['step_count'],'steps |', round\\(s['total_latency_ms'] or 0,2\\),'ms'\\) for s in d['sagas']]\")",
|
|
20
|
+
"Bash(rm -f demo-agent-saga.wal)",
|
|
21
|
+
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('sagas:', d['total']\\); [print\\(' ', s['status'].ljust\\(12\\), '|', s['step_count'],'steps'\\) for s in d['sagas']]\")",
|
|
22
|
+
"Bash(taskkill //F //FI \"WINDOWTITLE eq *\" //IM python.exe)",
|
|
23
|
+
"PowerShell($c = Get-NetTCPConnection -LocalPort 8137 -State Listen -ErrorAction SilentlyContinue; if \\($c\\) { Stop-Process -Id $c.OwningProcess -Force; \"stopped PID $\\($c.OwningProcess\\)\" } else { \"no listener on 8137\" })",
|
|
24
|
+
"PowerShell(cd \"D:\\\\AI Error\"; python -m pytest -q 2>&1 | Select-Object -Last 6; echo \"EXIT: $LASTEXITCODE\")",
|
|
25
|
+
"Bash(python -m agent_saga.ui --wal-path ./demo-agent-saga.wal --port 8138)",
|
|
26
|
+
"Read(//tmp/**)",
|
|
27
|
+
"Bash(curl -s http://127.0.0.1:8138/api/sagas)",
|
|
28
|
+
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('sagas:', d['total']\\)\")",
|
|
29
|
+
"PowerShell($c = Get-NetTCPConnection -LocalPort 8138 -State Listen -ErrorAction SilentlyContinue; if \\($c\\) { Stop-Process -Id $c.OwningProcess -Force; \"stopped $\\($c.OwningProcess\\)\" }; cd \"D:\\\\AI Error\"; python -m pytest -q 2>&1 | Select-Object -Last 2)",
|
|
30
|
+
"Bash(git --no-pager log --oneline -8)",
|
|
31
|
+
"Bash(curl -fsSL https://www.gnu.org/licenses/agpl-3.0.txt -o LICENSE)",
|
|
32
|
+
"Bash(curl -fsSL --ssl-no-revoke https://www.gnu.org/licenses/agpl-3.0.txt -o LICENSE)",
|
|
33
|
+
"Bash(git add *)",
|
|
34
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@agentrollback.io commit -q -m 'License under AGPL-3.0-only with a commercial exemption *)",
|
|
35
|
+
"Bash(git --no-pager log --oneline -3)",
|
|
36
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@avertis.systems commit -q -m 'Attribute the project to Avertis Systems in package metadata *)",
|
|
37
|
+
"Bash(git --no-pager log --oneline -1)",
|
|
38
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@avertis.systems commit -q -m 'Snapshot-based REVERSIBLE steps *)",
|
|
39
|
+
"Bash(git push *)",
|
|
40
|
+
"Bash(git rev-list *)",
|
|
41
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@avertis.systems commit -q -m 'Durable-target snapshots and CrewAI / OpenAI Agents SDK adapters *)",
|
|
42
|
+
"Bash(curl -fsSL --ssl-no-revoke \"https://api.github.com/repos/thomasjgeorge23/AI-Error/actions/runs?per_page=5\")",
|
|
43
|
+
"Bash(python -c 'import sys,json; d=json.load\\(sys.stdin\\); print\\('\\\\''total:'\\\\'', d.get\\('\\\\''total_count'\\\\''\\)\\); *)",
|
|
44
|
+
"Bash(curl -s --ssl-no-revoke \"https://api.github.com/repos/thomasjgeorge23/AI-Error/actions/runs?per_page=3\")",
|
|
45
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\" --ssl-no-revoke \"https://api.github.com/repos/thomasjgeorge23/AI-Error\")",
|
|
46
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@sagaops.dev commit -q -m 'Rename to SagaOps; snapshot GC; fix 3 audit safety bugs; OSS infra *)",
|
|
47
|
+
"Bash(python -m pip install --quiet \"cryptography>=41.0\")",
|
|
48
|
+
"Bash(python -c \"from cryptography.fernet import Fernet; print\\('cryptography OK'\\)\")",
|
|
49
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@sagaops.dev commit -q -m 'BYOK WAL encryption, injectable recovery lock, full-CRUD Postgres *)",
|
|
50
|
+
"Skill(artifact-design)",
|
|
51
|
+
"Skill(artifact-design:*)",
|
|
52
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@sagaops.dev commit -q -m 'Launch materials: sagaops.dev landing page and Show HN kit *)",
|
|
53
|
+
"Bash(git -c user.name=Thomas -c user.email=dev@sagaops.dev commit -q -m 'LlamaIndex + AutoGen adapters, and debugger UI token auth *)",
|
|
54
|
+
"Bash(git ls-remote *)",
|
|
55
|
+
"Bash(python -)",
|
|
56
|
+
"Bash(python -m pip install --quiet --upgrade build twine)",
|
|
57
|
+
"Bash(rm -rf dist build *.egg-info)",
|
|
58
|
+
"Bash(python -m build)"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
name: benchmark
|
|
2
|
+
|
|
3
|
+
# The durable-path p99 is the number a bank's architect asks about first.
|
|
4
|
+
# It cannot be measured on a developer laptop -- NTFS fsync tails on Windows
|
|
5
|
+
# varied 22-33ms p99 and 40-202ms max across identical runs. This workflow
|
|
6
|
+
# produces the number we are willing to publish.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
pull_request:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
inputs:
|
|
14
|
+
samples:
|
|
15
|
+
description: Samples per profile
|
|
16
|
+
default: "10000"
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
test:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
- run: pip install pytest langchain-core
|
|
27
|
+
- run: python -m pytest -q
|
|
28
|
+
|
|
29
|
+
bench:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
needs: test
|
|
32
|
+
strategy:
|
|
33
|
+
fail-fast: false
|
|
34
|
+
matrix:
|
|
35
|
+
# Three runs of each so we report median-of-p99 rather than whichever
|
|
36
|
+
# single run happened to look best. One sample of a tail is not a
|
|
37
|
+
# measurement.
|
|
38
|
+
run: [1, 2, 3]
|
|
39
|
+
gc: [enabled, disabled]
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- uses: actions/setup-python@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: "3.12"
|
|
45
|
+
|
|
46
|
+
- name: Record hardware and filesystem
|
|
47
|
+
run: |
|
|
48
|
+
echo "## runner" >> $GITHUB_STEP_SUMMARY
|
|
49
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
50
|
+
lscpu | grep -E 'Model name|^CPU\(s\)|MHz' >> $GITHUB_STEP_SUMMARY || true
|
|
51
|
+
df -hT . >> $GITHUB_STEP_SUMMARY
|
|
52
|
+
# fsync cost is a property of the device, not of our code. Capture it
|
|
53
|
+
# so the durable-path number can be read in context.
|
|
54
|
+
lsblk -d -o NAME,ROTA,MODEL >> $GITHUB_STEP_SUMMARY || true
|
|
55
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
56
|
+
|
|
57
|
+
- name: Baseline fsync cost of this runner
|
|
58
|
+
run: |
|
|
59
|
+
python - <<'PY'
|
|
60
|
+
import os, statistics, tempfile, time
|
|
61
|
+
with tempfile.TemporaryDirectory() as d:
|
|
62
|
+
p = os.path.join(d, "probe")
|
|
63
|
+
fh = open(p, "w")
|
|
64
|
+
lat = []
|
|
65
|
+
for _ in range(2000):
|
|
66
|
+
fh.write("x" * 200 + "\n"); fh.flush()
|
|
67
|
+
t0 = time.perf_counter_ns(); os.fsync(fh.fileno())
|
|
68
|
+
lat.append(time.perf_counter_ns() - t0)
|
|
69
|
+
fh.close()
|
|
70
|
+
s = sorted(lat)
|
|
71
|
+
print(f"raw fsync p50={s[1000]/1e6:.4f}ms p95={s[1900]/1e6:.4f}ms "
|
|
72
|
+
f"p99={s[1980]/1e6:.4f}ms max={s[-1]/1e6:.3f}ms")
|
|
73
|
+
PY
|
|
74
|
+
|
|
75
|
+
- name: Run benchmark
|
|
76
|
+
run: |
|
|
77
|
+
python bench/bench_core.py \
|
|
78
|
+
--samples ${{ github.event.inputs.samples || 10000 }} \
|
|
79
|
+
--label ${{ matrix.gc }}-${{ matrix.run }} \
|
|
80
|
+
${{ matrix.gc == 'disabled' && '--no-gc' || '' }} \
|
|
81
|
+
| tee bench-output.txt
|
|
82
|
+
{
|
|
83
|
+
echo "## bench (gc ${{ matrix.gc }}, run ${{ matrix.run }})"
|
|
84
|
+
echo '```'
|
|
85
|
+
cat bench-output.txt
|
|
86
|
+
echo '```'
|
|
87
|
+
} >> $GITHUB_STEP_SUMMARY
|
|
88
|
+
|
|
89
|
+
- uses: actions/upload-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
name: results-${{ matrix.gc }}-${{ matrix.run }}
|
|
92
|
+
path: bench/results-*.json
|
|
93
|
+
|
|
94
|
+
report:
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
needs: bench
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
- uses: actions/setup-python@v5
|
|
100
|
+
with:
|
|
101
|
+
python-version: "3.12"
|
|
102
|
+
- uses: actions/download-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
path: artifacts
|
|
105
|
+
- name: Median-of-runs summary
|
|
106
|
+
run: python bench/summarize.py artifacts >> $GITHUB_STEP_SUMMARY
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Contributing to agent-saga
|
|
2
|
+
|
|
3
|
+
Thanks for considering a contribution. This library guards the transaction path
|
|
4
|
+
of autonomous agents, so the bar is correctness first, then clarity, then speed.
|
|
5
|
+
|
|
6
|
+
## Development setup
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
git clone <repo> && cd <repo>
|
|
10
|
+
python -m pip install -e ".[dev]"
|
|
11
|
+
python -m pytest -q
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The core suite runs with **only `pytest`** — no network, no live database, no
|
|
15
|
+
real credentials. Connector and adapter tests use in-process fakes; the handful
|
|
16
|
+
of real-SDK integration tests `importorskip` their framework and are skipped
|
|
17
|
+
when it is absent. A contribution should keep the base suite dependency-free.
|
|
18
|
+
|
|
19
|
+
## Principles this codebase holds to
|
|
20
|
+
|
|
21
|
+
- **"Undo" is typed.** Every effect is `REVERSIBLE`, `COMPENSABLE`, or
|
|
22
|
+
`IRREVERSIBLE`. If you cannot say which, you do not yet understand the effect.
|
|
23
|
+
- **Fail closed, and say so.** When something cannot be undone, it is reported
|
|
24
|
+
(`ORPHANED`, `NEEDS_HUMAN`), never silently dropped. Tests assert on what the
|
|
25
|
+
system *refuses* to do as much as what it does.
|
|
26
|
+
- **Write-ahead, then act.** Durable intent is recorded before a side effect,
|
|
27
|
+
not after.
|
|
28
|
+
- **Credentials are references, never values.** Anything that could put a secret
|
|
29
|
+
in the WAL must go through a `*_ref` and `resolve_credential`.
|
|
30
|
+
- **Comments explain *why*, at the altitude of the surrounding code.** Skip
|
|
31
|
+
narration of what the line plainly does.
|
|
32
|
+
|
|
33
|
+
## Adding a connector
|
|
34
|
+
|
|
35
|
+
A connector is the real product surface, so it carries the most responsibility:
|
|
36
|
+
|
|
37
|
+
1. Pick the correct semantics and justify it in the module docstring (a shared
|
|
38
|
+
database row is `COMPENSABLE`, not `REVERSIBLE` — see `connectors/postgres.py`
|
|
39
|
+
for why).
|
|
40
|
+
2. Register the compensation handler by **name** via `@compensator`, with
|
|
41
|
+
JSON-serializable kwargs, so `saga-recoveryd` can run it after a crash.
|
|
42
|
+
3. Derive the inverse at runtime from the forward result, and guard against
|
|
43
|
+
clobbering a concurrent external change.
|
|
44
|
+
4. Run `assert_no_secrets` on the compensation kwargs.
|
|
45
|
+
5. Test rollback, the `UNKNOWN` (timed-out) outcome, and the concurrency guard —
|
|
46
|
+
all against a fake, no live system.
|
|
47
|
+
|
|
48
|
+
## Pull requests
|
|
49
|
+
|
|
50
|
+
- Keep the diff focused; one concern per PR.
|
|
51
|
+
- Add or update tests for the behavior you change. New behavior without a test
|
|
52
|
+
that would fail without it will be asked for one.
|
|
53
|
+
- Match the surrounding style; do not reformat unrelated code.
|
|
54
|
+
- Note any effect on the durability/latency story in the description.
|
|
55
|
+
|
|
56
|
+
## Reporting security issues
|
|
57
|
+
|
|
58
|
+
Not here — see [SECURITY.md](SECURITY.md) for the private channel.
|