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.
Files changed (61) hide show
  1. agent_saga-0.1.0/.claude/settings.local.json +61 -0
  2. agent_saga-0.1.0/.github/workflows/bench.yml +106 -0
  3. agent_saga-0.1.0/.gitignore +8 -0
  4. agent_saga-0.1.0/.vscode/settings.json +7 -0
  5. agent_saga-0.1.0/CONTRIBUTING.md +58 -0
  6. agent_saga-0.1.0/LICENSE +661 -0
  7. agent_saga-0.1.0/PKG-INFO +243 -0
  8. agent_saga-0.1.0/README.md +192 -0
  9. agent_saga-0.1.0/SECURITY.md +47 -0
  10. agent_saga-0.1.0/agent_saga/__init__.py +127 -0
  11. agent_saga-0.1.0/agent_saga/adapters/__init__.py +11 -0
  12. agent_saga-0.1.0/agent_saga/adapters/_common.py +53 -0
  13. agent_saga-0.1.0/agent_saga/adapters/autogen.py +96 -0
  14. agent_saga-0.1.0/agent_saga/adapters/crewai.py +131 -0
  15. agent_saga-0.1.0/agent_saga/adapters/langgraph.py +172 -0
  16. agent_saga-0.1.0/agent_saga/adapters/llamaindex.py +104 -0
  17. agent_saga-0.1.0/agent_saga/adapters/openai_agents.py +120 -0
  18. agent_saga-0.1.0/agent_saga/cli.py +99 -0
  19. agent_saga-0.1.0/agent_saga/connectors/__init__.py +27 -0
  20. agent_saga-0.1.0/agent_saga/connectors/_secrets.py +139 -0
  21. agent_saga-0.1.0/agent_saga/connectors/postgres.py +404 -0
  22. agent_saga-0.1.0/agent_saga/connectors/salesforce.py +188 -0
  23. agent_saga-0.1.0/agent_saga/connectors/stripe.py +137 -0
  24. agent_saga-0.1.0/agent_saga/context.py +384 -0
  25. agent_saga-0.1.0/agent_saga/decorator.py +147 -0
  26. agent_saga-0.1.0/agent_saga/durable.py +258 -0
  27. agent_saga-0.1.0/agent_saga/encryption.py +152 -0
  28. agent_saga-0.1.0/agent_saga/gate.py +147 -0
  29. agent_saga-0.1.0/agent_saga/gc.py +205 -0
  30. agent_saga-0.1.0/agent_saga/locks.py +90 -0
  31. agent_saga-0.1.0/agent_saga/recovery.py +378 -0
  32. agent_saga-0.1.0/agent_saga/registry.py +62 -0
  33. agent_saga-0.1.0/agent_saga/semantics.py +136 -0
  34. agent_saga-0.1.0/agent_saga/snapshot.py +185 -0
  35. agent_saga-0.1.0/agent_saga/ui/__init__.py +5 -0
  36. agent_saga-0.1.0/agent_saga/ui/__main__.py +11 -0
  37. agent_saga-0.1.0/agent_saga/ui/reader.py +367 -0
  38. agent_saga-0.1.0/agent_saga/ui/server.py +149 -0
  39. agent_saga-0.1.0/agent_saga/ui/templates/dashboard.html +438 -0
  40. agent_saga-0.1.0/agent_saga/wal.py +266 -0
  41. agent_saga-0.1.0/bench/bench_core.py +248 -0
  42. agent_saga-0.1.0/bench/results.json +128 -0
  43. agent_saga-0.1.0/bench/summarize.py +69 -0
  44. agent_saga-0.1.0/examples/demo.py +221 -0
  45. agent_saga-0.1.0/launch/show-hn.md +174 -0
  46. agent_saga-0.1.0/pyproject.toml +56 -0
  47. agent_saga-0.1.0/site/index.html +506 -0
  48. agent_saga-0.1.0/tests/conftest.py +13 -0
  49. agent_saga-0.1.0/tests/crash_worker.py +71 -0
  50. agent_saga-0.1.0/tests/test_adapters_crewai_openai.py +303 -0
  51. agent_saga-0.1.0/tests/test_adapters_llamaindex_autogen.py +232 -0
  52. agent_saga-0.1.0/tests/test_connectors.py +670 -0
  53. agent_saga-0.1.0/tests/test_core.py +596 -0
  54. agent_saga-0.1.0/tests/test_durable.py +246 -0
  55. agent_saga-0.1.0/tests/test_encryption.py +183 -0
  56. agent_saga-0.1.0/tests/test_gc.py +174 -0
  57. agent_saga-0.1.0/tests/test_langgraph_adapter.py +303 -0
  58. agent_saga-0.1.0/tests/test_locks.py +69 -0
  59. agent_saga-0.1.0/tests/test_recovery.py +360 -0
  60. agent_saga-0.1.0/tests/test_snapshot.py +309 -0
  61. 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,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ bench/results-*.json
5
+ *.jsonl
6
+ *.wal
7
+ .claims/
8
+ .venv/
@@ -0,0 +1,7 @@
1
+ {
2
+ "python.testing.pytestArgs": [
3
+ "agent_saga"
4
+ ],
5
+ "python.testing.unittestEnabled": false,
6
+ "python.testing.pytestEnabled": true
7
+ }
@@ -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.