agent-workflow-sdk 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 (100) hide show
  1. agent_workflow_sdk-0.1.0/.env.example +17 -0
  2. agent_workflow_sdk-0.1.0/.github/workflows/ci.yml +80 -0
  3. agent_workflow_sdk-0.1.0/.gitignore +13 -0
  4. agent_workflow_sdk-0.1.0/CHANGELOG.md +168 -0
  5. agent_workflow_sdk-0.1.0/CONTRIBUTING.md +67 -0
  6. agent_workflow_sdk-0.1.0/DESIGN.md +206 -0
  7. agent_workflow_sdk-0.1.0/LICENSE +201 -0
  8. agent_workflow_sdk-0.1.0/NOTICE +10 -0
  9. agent_workflow_sdk-0.1.0/PKG-INFO +566 -0
  10. agent_workflow_sdk-0.1.0/README.md +524 -0
  11. agent_workflow_sdk-0.1.0/SECURITY.md +37 -0
  12. agent_workflow_sdk-0.1.0/agentflow/__init__.py +199 -0
  13. agent_workflow_sdk-0.1.0/agentflow/backends/__init__.py +54 -0
  14. agent_workflow_sdk-0.1.0/agentflow/backends/_http.py +169 -0
  15. agent_workflow_sdk-0.1.0/agentflow/backends/anthropic.py +291 -0
  16. agent_workflow_sdk-0.1.0/agentflow/backends/base.py +327 -0
  17. agent_workflow_sdk-0.1.0/agentflow/backends/claude_code.py +181 -0
  18. agent_workflow_sdk-0.1.0/agentflow/backends/cli_exec.py +237 -0
  19. agent_workflow_sdk-0.1.0/agentflow/backends/codex.py +126 -0
  20. agent_workflow_sdk-0.1.0/agentflow/backends/kiro.py +523 -0
  21. agent_workflow_sdk-0.1.0/agentflow/backends/ollama.py +208 -0
  22. agent_workflow_sdk-0.1.0/agentflow/backends/openai.py +249 -0
  23. agent_workflow_sdk-0.1.0/agentflow/checkpoint/__init__.py +51 -0
  24. agent_workflow_sdk-0.1.0/agentflow/checkpoint/_serde.py +57 -0
  25. agent_workflow_sdk-0.1.0/agentflow/checkpoint/base.py +125 -0
  26. agent_workflow_sdk-0.1.0/agentflow/checkpoint/file.py +210 -0
  27. agent_workflow_sdk-0.1.0/agentflow/checkpoint/memory.py +91 -0
  28. agent_workflow_sdk-0.1.0/agentflow/checkpoint/postgres.py +232 -0
  29. agent_workflow_sdk-0.1.0/agentflow/checkpoint/redis.py +206 -0
  30. agent_workflow_sdk-0.1.0/agentflow/checkpoint/sqlite.py +261 -0
  31. agent_workflow_sdk-0.1.0/agentflow/compiled.py +313 -0
  32. agent_workflow_sdk-0.1.0/agentflow/controlplane/__init__.py +57 -0
  33. agent_workflow_sdk-0.1.0/agentflow/controlplane/memory.py +181 -0
  34. agent_workflow_sdk-0.1.0/agentflow/controlplane/postgres.py +297 -0
  35. agent_workflow_sdk-0.1.0/agentflow/controlplane/queue.py +89 -0
  36. agent_workflow_sdk-0.1.0/agentflow/controlplane/records.py +122 -0
  37. agent_workflow_sdk-0.1.0/agentflow/controlplane/registry.py +70 -0
  38. agent_workflow_sdk-0.1.0/agentflow/controlplane/worker.py +219 -0
  39. agent_workflow_sdk-0.1.0/agentflow/errors.py +186 -0
  40. agent_workflow_sdk-0.1.0/agentflow/events.py +226 -0
  41. agent_workflow_sdk-0.1.0/agentflow/graph.py +328 -0
  42. agent_workflow_sdk-0.1.0/agentflow/observability.py +126 -0
  43. agent_workflow_sdk-0.1.0/agentflow/otel.py +124 -0
  44. agent_workflow_sdk-0.1.0/agentflow/prebuilt/__init__.py +29 -0
  45. agent_workflow_sdk-0.1.0/agentflow/prebuilt/loop.py +133 -0
  46. agent_workflow_sdk-0.1.0/agentflow/prebuilt/resilience.py +120 -0
  47. agent_workflow_sdk-0.1.0/agentflow/prebuilt/tool_loop.py +185 -0
  48. agent_workflow_sdk-0.1.0/agentflow/prometheus.py +175 -0
  49. agent_workflow_sdk-0.1.0/agentflow/py.typed +0 -0
  50. agent_workflow_sdk-0.1.0/agentflow/redaction.py +99 -0
  51. agent_workflow_sdk-0.1.0/agentflow/runtime.py +356 -0
  52. agent_workflow_sdk-0.1.0/agentflow/state.py +234 -0
  53. agent_workflow_sdk-0.1.0/agentflow/store/__init__.py +40 -0
  54. agent_workflow_sdk-0.1.0/agentflow/store/_util.py +54 -0
  55. agent_workflow_sdk-0.1.0/agentflow/store/base.py +103 -0
  56. agent_workflow_sdk-0.1.0/agentflow/store/memory.py +109 -0
  57. agent_workflow_sdk-0.1.0/agentflow/store/postgres.py +210 -0
  58. agent_workflow_sdk-0.1.0/agentflow/telemetry.py +197 -0
  59. agent_workflow_sdk-0.1.0/examples/agents_demo.py +137 -0
  60. agent_workflow_sdk-0.1.0/examples/codebase_qa_ollama.py +336 -0
  61. agent_workflow_sdk-0.1.0/examples/hello_graph.py +63 -0
  62. agent_workflow_sdk-0.1.0/examples/hitl_permission.py +107 -0
  63. agent_workflow_sdk-0.1.0/examples/mixed_backends.py +186 -0
  64. agent_workflow_sdk-0.1.0/examples/optimize_loop.py +48 -0
  65. agent_workflow_sdk-0.1.0/examples/telemetry_demo.py +85 -0
  66. agent_workflow_sdk-0.1.0/examples/tool_loop_ollama.py +96 -0
  67. agent_workflow_sdk-0.1.0/pyproject.toml +107 -0
  68. agent_workflow_sdk-0.1.0/tests/conftest.py +48 -0
  69. agent_workflow_sdk-0.1.0/tests/test_anthropic_backend.py +210 -0
  70. agent_workflow_sdk-0.1.0/tests/test_backend_lifecycle.py +101 -0
  71. agent_workflow_sdk-0.1.0/tests/test_checkpoint_contract.py +166 -0
  72. agent_workflow_sdk-0.1.0/tests/test_cli_backends.py +202 -0
  73. agent_workflow_sdk-0.1.0/tests/test_concurrency.py +136 -0
  74. agent_workflow_sdk-0.1.0/tests/test_controlplane.py +316 -0
  75. agent_workflow_sdk-0.1.0/tests/test_graph_engine.py +245 -0
  76. agent_workflow_sdk-0.1.0/tests/test_hitl_permission.py +228 -0
  77. agent_workflow_sdk-0.1.0/tests/test_http_retry.py +169 -0
  78. agent_workflow_sdk-0.1.0/tests/test_input_validation.py +60 -0
  79. agent_workflow_sdk-0.1.0/tests/test_kiro_backend.py +732 -0
  80. agent_workflow_sdk-0.1.0/tests/test_live_backends.py +462 -0
  81. agent_workflow_sdk-0.1.0/tests/test_observability.py +110 -0
  82. agent_workflow_sdk-0.1.0/tests/test_ollama_backend.py +124 -0
  83. agent_workflow_sdk-0.1.0/tests/test_openai_backend.py +191 -0
  84. agent_workflow_sdk-0.1.0/tests/test_otel.py +119 -0
  85. agent_workflow_sdk-0.1.0/tests/test_permission_policy.py +74 -0
  86. agent_workflow_sdk-0.1.0/tests/test_prebuilt_loop.py +76 -0
  87. agent_workflow_sdk-0.1.0/tests/test_prometheus.py +129 -0
  88. agent_workflow_sdk-0.1.0/tests/test_provider_resilience.py +185 -0
  89. agent_workflow_sdk-0.1.0/tests/test_redaction.py +133 -0
  90. agent_workflow_sdk-0.1.0/tests/test_redis_checkpointer.py +187 -0
  91. agent_workflow_sdk-0.1.0/tests/test_resilience.py +170 -0
  92. agent_workflow_sdk-0.1.0/tests/test_run_timeout.py +101 -0
  93. agent_workflow_sdk-0.1.0/tests/test_sqlite_checkpointer.py +117 -0
  94. agent_workflow_sdk-0.1.0/tests/test_state_isolation.py +116 -0
  95. agent_workflow_sdk-0.1.0/tests/test_step_atomicity.py +108 -0
  96. agent_workflow_sdk-0.1.0/tests/test_store.py +112 -0
  97. agent_workflow_sdk-0.1.0/tests/test_subgraph.py +126 -0
  98. agent_workflow_sdk-0.1.0/tests/test_sync_facade.py +78 -0
  99. agent_workflow_sdk-0.1.0/tests/test_telemetry.py +169 -0
  100. agent_workflow_sdk-0.1.0/tests/test_tool_loop.py +195 -0
@@ -0,0 +1,17 @@
1
+ # Copy this file to .env and fill in your keys. .env is gitignored.
2
+ # Used by the live test suite (pytest -m live) and the backend examples.
3
+
4
+ # OpenAI (or any OpenAI-compatible endpoint)
5
+ OPENAI_API_KEY=sk-...
6
+ OPENAI_MODEL=gpt-4o-mini
7
+ # Optional: override for a compatible endpoint (Groq, Together, vLLM, ...)
8
+ # OPENAI_BASE_URL=https://api.openai.com/v1
9
+
10
+ # Anthropic (Messages API)
11
+ ANTHROPIC_API_KEY=sk-ant-...
12
+ ANTHROPIC_MODEL=claude-sonnet-4-20250514
13
+ # Only needed for an org-level (unscoped) key; workspace-scoped keys omit this.
14
+ # ANTHROPIC_WORKSPACE_ID=wrkspc_...
15
+
16
+ # Kiro agent id (a valid v3 session mode, e.g. vibe / spec / developer)
17
+ KIRO_AGENT=vibe
@@ -0,0 +1,80 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.11"
16
+ - name: Install (dev extras)
17
+ run: |
18
+ python -m pip install --upgrade pip
19
+ pip install -e '.[ollama,dev]'
20
+ - name: Ruff (lint + format check)
21
+ run: |
22
+ ruff check agentflow tests examples
23
+ ruff format --check agentflow tests examples
24
+ - name: mypy (type check)
25
+ run: mypy agentflow
26
+ - name: pip-audit (dependency CVE scan)
27
+ run: pip-audit || true # advisory; do not fail the build on transient DB issues
28
+
29
+ test:
30
+ runs-on: ubuntu-latest
31
+ strategy:
32
+ fail-fast: false
33
+ matrix:
34
+ python-version: ["3.11", "3.12"]
35
+
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+
39
+ - name: Set up Python ${{ matrix.python-version }}
40
+ uses: actions/setup-python@v5
41
+ with:
42
+ python-version: ${{ matrix.python-version }}
43
+
44
+ - name: Install (with dev + ollama extras)
45
+ run: |
46
+ python -m pip install --upgrade pip
47
+ pip install -e '.[ollama,dev]'
48
+
49
+ - name: Run offline test suite with coverage
50
+ # The default addopts (-m 'not live') skips backend smoke tests, so CI
51
+ # stays hermetic — no agent CLIs or network required.
52
+ run: pytest -q --cov --cov-report=term-missing --cov-report=xml
53
+
54
+ - name: Upload coverage report
55
+ uses: actions/upload-artifact@v4
56
+ with:
57
+ name: coverage-${{ matrix.python-version }}
58
+ path: coverage.xml
59
+ if-no-files-found: ignore
60
+
61
+ build:
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+ - uses: actions/setup-python@v5
66
+ with:
67
+ python-version: "3.11"
68
+ - name: Build wheel + sdist
69
+ run: |
70
+ python -m pip install --upgrade pip build
71
+ python -m build
72
+ - name: Check the built wheel ships py.typed
73
+ run: |
74
+ python - <<'PY'
75
+ import glob, zipfile, sys
76
+ wheel = glob.glob("dist/*.whl")[0]
77
+ names = zipfile.ZipFile(wheel).namelist()
78
+ assert "agentflow/py.typed" in names, "py.typed missing from wheel"
79
+ print("py.typed present in", wheel)
80
+ PY
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ telemetry/
5
+ .DS_Store
6
+ .runs/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .env
11
+ .coverage
12
+ coverage.xml
13
+ .pytest_cache/
@@ -0,0 +1,168 @@
1
+ # Changelog
2
+
3
+ All notable changes to `agent-workflow-sdk` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
+ project aims to follow [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-09-29
8
+
9
+ First public release: an async-first, LangGraph-style SDK for agent workflows
10
+ with pluggable agent and LLM backends.
11
+
12
+ ### Added
13
+
14
+ - Agent backends require an explicit `permission` policy — there is no
15
+ auto-approve default. Construct with `permission=AllowAll()` (trusted local
16
+ sandbox only), `DenyAll()`, `Interactive()`, or `ToolAllowlist(...)`.
17
+ - Control-plane health/monitoring surface: `RunQueue.stats()` returns a
18
+ `QueueStats` (queue depth by status plus `expired_leases`, the running runs
19
+ whose lease is past due and await re-claim), and `WorkerPool.health()` returns
20
+ a `PoolHealth` (`workers`/`alive`/`busy`/`idle`, with `healthy` True when every
21
+ worker loop is alive). `Worker.busy` reports whether a worker is mid-run. Plain
22
+ values for the caller to expose via `/healthz` / `/metrics` (no HTTP layer
23
+ imposed). Implemented on both `MemoryRunQueue` and `PostgresRunQueue`.
24
+ - `PrometheusHooks` (in the `prometheus` extra): a `Hooks` listener that records
25
+ run/node/step counters, a node-duration histogram, and backend-event counts as
26
+ Prometheus metrics, for operators who scrape Prometheus rather than run an OTel
27
+ collector. Uses a private `CollectorRegistry` by default; `exposition()`
28
+ renders the text format for a `/metrics` endpoint (the HTTP layer is the
29
+ caller's).
30
+ - `ToolAllowlist` permission policy for least-privilege tool scoping, with a
31
+ configurable fallback (`DenyAll` by default).
32
+ - Capability matrix in the README documenting which backends route tool
33
+ requests through `PermissionPolicy` (Kiro does; the one-shot CLI agents use
34
+ their own sandbox/flags).
35
+ - Sensitive-data controls for persistence: `RedactKeys` redactor (mask values
36
+ by key fragment). `JsonlTelemetry(redact=...)` and `FileCheckpointer(
37
+ redact=...)` apply it before writing. Both create files/dirs owner-only
38
+ (0o600/0o700) by default (`secure_permissions=`). Checkpoint redaction is
39
+ opt-in and documented as non-resumable (masked values are lost).
40
+ - `SqliteCheckpointer`: transactional durable checkpointer with atomic
41
+ per-`(thread, step)` revisions (WAL mode, upsert-with-revision-bump), safe
42
+ for concurrent/multi-process runners. `FileCheckpointer` is now documented as
43
+ single-writer/single-process.
44
+ - Provider resilience: typed `BackendTransportError` now carries `status` and
45
+ `headers`; a 429 that outlives retries raises `BackendRateLimitError` with
46
+ `retry_after`. HTTP LLM backends accept `max_concurrency` to bound in-flight
47
+ requests, and `compile(max_node_concurrency=...)` bounds how many nodes run
48
+ concurrently within a super-step (limits fan-out request pressure).
49
+ - State isolation: `compile(isolate_state="fanout"|"always"|"never")` (default
50
+ `"fanout"`) deep-copies a node's input on concurrent super-steps so an
51
+ in-place mutation of a shared nested container cannot race a sibling. Linear
52
+ graphs pay no copy cost.
53
+ - `tool_loop` now sets a terminal `status` on the final state: `"completed"`
54
+ (model answered tool-free) or `"tool_calls_unresolved"` (hit `max_turns` with
55
+ pending tool calls). Callers must check it rather than assume the last
56
+ message is a final answer.
57
+ - Tooling: Ruff (lint + format), mypy (type check, `agentflow` clean), and
58
+ pip-audit added to the `dev` extra and CI. Codebase formatted and typed to
59
+ zero findings.
60
+ - Release artifacts: `LICENSE` (Apache-2.0) and `NOTICE`, SPDX metadata,
61
+ shipped in the wheel; real
62
+ project URLs, `CONTRIBUTING.md` (with a release process), and `SECURITY.md`
63
+ (reporting + security-relevant design notes).
64
+ - `DESIGN.md`: architecture and the invariants/contracts a change must
65
+ preserve (dependency direction, execution model, checkpointer/store/control-
66
+ plane contracts, security posture, out-of-scope). Referenced from
67
+ `CONTRIBUTING.md` and the README layout.
68
+ - `OtelHooks` (in the `otel` extra): an OpenTelemetry `Hooks` exporter emitting
69
+ a span per run and per node, recording errors and backend events, with a
70
+ versioned attribute schema (`EVENT_SCHEMA_VERSION`).
71
+ - Backends are async context managers (`async with backend: ...`) so
72
+ `start()`/`close()` can't be skipped and `close()` runs on error. Kiro's
73
+ stdin writes now apply backpressure via `drain()`.
74
+ - Synchronous facade on `CompiledGraph`: `invoke_sync`, `resume_sync`, and
75
+ `stream_sync` wrap `asyncio.run` for non-async callers, and refuse to run
76
+ inside an existing event loop rather than deadlock.
77
+ - Control plane for managing runs outside the process that created them
78
+ (`agentflow.controlplane`). A `RunQueue` holds run requests; a `GraphRegistry`
79
+ maps a graph name to a compiled-graph factory (graphs are code, not data);
80
+ and a `Worker`/`WorkerPool` claims requests and executes them, so runs
81
+ survive a process restart and scale across workers. Run lifecycle:
82
+ `queued -> running -> succeeded | interrupted | failed | cancelled`, with
83
+ interrupted runs resumed via `enqueue_resume(run_id, value)` and cooperative
84
+ `request_cancel` (stops at a super-step boundary). Ships `MemoryRunQueue`
85
+ (single process) and `PostgresRunQueue` (in the `postgres` extra; `claim` uses
86
+ `FOR UPDATE SKIP LOCKED` for safe concurrent workers, with a lease/heartbeat
87
+ so a crashed worker's run is re-claimed). Also adds `Checkpointer.list_threads`
88
+ (+ `ThreadInfo`) to enumerate runs across all backends. An HTTP layer over
89
+ the queue is planned but not included.
90
+ - `Store` protocol for durable cross-thread memory (distinct from a
91
+ checkpointer, which persists one thread's execution state). Items are
92
+ addressed by a `namespace` tuple and a string `key`, hold any JSON value, and
93
+ support an optional per-item TTL. Methods: `get`, `put`, `delete`, `search`
94
+ (by namespace prefix) and `list_namespaces`. Ships `MemoryStore` (ephemeral)
95
+ and `PostgresStore` (in the `postgres` extra; `text[]` namespace column for
96
+ native prefix search, `jsonb` value, `timestamptz` expiry filtered from every
97
+ read). Lazily exported so the core stays dependency-free.
98
+ - `PostgresCheckpointer` (in the `postgres` extra, via `asyncpg`): durable
99
+ checkpoints in a Postgres table keyed by `(thread, step)` with a `revision`
100
+ column and `jsonb` payload. Transactional upsert bumps the revision
101
+ atomically; conditional writes check the stored revision under `FOR UPDATE`.
102
+ Postgres is the recommended production-durability default (a committed row
103
+ survives a crash without extra tuning). Lazily exported so the core stays
104
+ dependency-free.
105
+ - Optimistic concurrency for checkpointers: `Checkpoint` gains a `revision`
106
+ field (write count per `(thread, step)`, set by the store on read), and
107
+ `Checkpointer.put(cp, if_revision=...)` performs a compare-and-set — a
108
+ mismatch raises the new `CheckpointConflict`. `None` (default) stays an
109
+ unconditional upsert, so existing `put(cp)` calls are unchanged. This lets
110
+ two resumes racing the same super-step fail loudly instead of silently
111
+ clobbering each other. Implemented across the Memory, Sqlite, File, Redis and
112
+ Postgres backends.
113
+ - Retention on the `Checkpointer` protocol: `delete_thread(thread)` removes a
114
+ whole thread, and `prune(thread, before_step=..., older_than=...)` deletes
115
+ old checkpoints and returns the count removed. Implemented across all
116
+ backends.
117
+ - `RedisCheckpointer` (in the `redis` extra): durable checkpoints on a Redis
118
+ server for distributed / multi-process runners — JSON value per step plus a
119
+ per-thread sorted-set step index, atomic pipeline writes. Shared checkpoint
120
+ serialization extracted to `checkpoint/_serde.py`. Lazily exported so the
121
+ core stays redis-free. Note: Redis is not crash-durable without AOF/RDB
122
+ persistence configured; prefer `PostgresCheckpointer` when durability matters.
123
+
124
+ - HTTP retry/backoff for the httpx LLM backends (`OllamaBackend`,
125
+ `OpenAIBackend`, `AnthropicBackend`) via a shared `RetryPolicy` and
126
+ `open_stream` helper. Retries the connect/initial-response phase only (never
127
+ mid-stream), honors `Retry-After`, and retries transient statuses
128
+ (408/409/425/429/500/502/503/504) and transport errors. Configurable per
129
+ backend through the `retry=` argument.
130
+ - Persistent telemetry: `JsonlTelemetry` (durable JSON Lines per run) and
131
+ `MultiHooks` (compose several listeners); `Hooks.on_event` surfaces backend
132
+ events emitted via `ctx.emit` to telemetry.
133
+ - Coverage: `pytest-cov` dev dependency and `[tool.coverage]` config; opt-in
134
+ via `pytest --cov`. CI reports coverage.
135
+ - `CHANGELOG.md`.
136
+
137
+ Core engine (the foundation the above builds on):
138
+
139
+ - Graph engine: `Graph` builder (`add_node`, `add_edge`,
140
+ `add_conditional_edges`), `START`/`END` sentinels, compile-time validation
141
+ (reachability, unknown targets, dead ends), and a `CompiledGraph` runnable
142
+ with `invoke` / `stream` / `resume` / `get_state` / `history`.
143
+ - Typed state with per-key reducers (`last`, `append`, `add`, `merge`,
144
+ `union`) over a `TypedDict` schema; deterministic update merge.
145
+ - Super-step execution: concurrent fan-out, deterministic reducer folding,
146
+ all-or-nothing step semantics, and a `step_limit` guard.
147
+ - Whole-run timeout (`invoke(..., timeout=...)` → `RunTimeout`) with the last
148
+ checkpoint preserved for resume.
149
+ - Input validation at run start (undeclared/reserved channels rejected).
150
+ - Subgraph-as-node: `add_node(compiled_graph)` and `add_subgraph(...,
151
+ input_map=, output_map=)` with isolated checkpoint sub-threads.
152
+ - Checkpointing: `Checkpointer` protocol, `MemoryCheckpointer`, durable
153
+ `FileCheckpointer`; resume and time-travel.
154
+ - Human-in-the-loop: `ctx.interrupt(...)` suspends a run; `resume(value=...)`
155
+ continues it. `Interactive` permission policy drives real interrupts.
156
+ - Backends behind one event stream. Agent: `KiroBackend` (persistent ACP
157
+ session), `CodexBackend`, `ClaudeCodeBackend` (one-shot CLI per turn). LLM:
158
+ `OllamaBackend`, `OpenAIBackend` (any OpenAI-compatible endpoint),
159
+ `AnthropicBackend` (Messages API). Permission policies `AllowAll`/`DenyAll`/
160
+ `Interactive`.
161
+ - Prebuilt patterns: `iterate_until_converged`, `tool_loop` (graph-native LLM
162
+ function-calling), and `with_retry` / `with_timeout` node wrappers.
163
+ - Observability: `Hooks` lifecycle callbacks and `RunMetrics`.
164
+ - Packaging: `py.typed`, dependency-free core with optional `[ollama]` extra,
165
+ GitHub Actions CI (offline suite + wheel build), and a `live` pytest marker
166
+ separating opt-in real-backend smoke tests from the hermetic default run.
167
+
168
+ [0.1.0]: https://github.com/dankosorkin/agent-workflow-sdk/releases/tag/v0.1.0
@@ -0,0 +1,67 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in `agent-workflow-sdk`.
4
+
5
+ ## Development setup
6
+
7
+ ```bash
8
+ python -m venv .venv && source .venv/bin/activate
9
+ pip install -e '.[ollama,dev]'
10
+ ```
11
+
12
+ ## Before you open a PR
13
+
14
+ Run the same checks CI runs. All must pass:
15
+
16
+ ```bash
17
+ ruff check agentflow tests examples
18
+ ruff format --check agentflow tests examples
19
+ mypy agentflow
20
+ pytest -q # offline suite (hermetic)
21
+ ```
22
+
23
+ - Format with `ruff format` before committing.
24
+ - Add tests for new behavior. The suite deliberately tests invariants
25
+ (concurrency, atomicity, timeouts, interrupts), not just happy paths.
26
+ - Keep the core (`agentflow/` minus `backends/`) dependency-free. New provider
27
+ libraries go behind an optional extra.
28
+ - Update `CHANGELOG.md` under `[Unreleased]`.
29
+
30
+ ## Live backend tests
31
+
32
+ Backend smoke tests are opt-in and skipped by default. To run them you need the
33
+ relevant CLIs/servers and keys (see `.env.example`):
34
+
35
+ ```bash
36
+ pytest -m live
37
+ ```
38
+
39
+ Each live test self-skips when its backend is absent.
40
+
41
+ ## Architecture
42
+
43
+ Read `DESIGN.md` first — it records the invariants and contracts your change
44
+ must preserve. In short: the dependency direction is strict, state → engine →
45
+ backends, and the engine never imports a backend. Please preserve that
46
+ boundary.
47
+
48
+ ## Commit and PR conventions
49
+
50
+ - One logical change per PR; keep diffs reviewable.
51
+ - Reference the affected area in the commit subject (e.g. `runtime:`,
52
+ `backends/openai:`).
53
+ - Do not commit secrets. `.env` is gitignored; use `.env.example` as the
54
+ template.
55
+
56
+ ## Release process
57
+
58
+ 1. Move `[Unreleased]` entries in `CHANGELOG.md` under a new version heading;
59
+ bump `version` in `pyproject.toml` (SemVer).
60
+ 2. Ensure CI is green (lint, types, tests, build).
61
+ 3. Build and verify the artifacts: `python -m build` — confirm the wheel ships
62
+ `agentflow/py.typed` and the LICENSE.
63
+ 4. Tag `vX.Y.Z` and publish.
64
+
65
+ Pre-1.0 the public API may change between minor versions; breaking changes are
66
+ called out in the changelog.
67
+
@@ -0,0 +1,206 @@
1
+ # Design
2
+
3
+ This document records the architecture and the contracts that the rest of the
4
+ codebase depends on. The README shows how to use the SDK; this explains what
5
+ must stay true as it changes. If a change would break an invariant here, that
6
+ is a design decision, not a refactor.
7
+
8
+ The import root is `agentflow`. The distribution name is `agent-workflow-sdk`.
9
+
10
+ ## Dependency direction
11
+
12
+ The one hard rule: `state` to `engine` to `backends`, and never the reverse.
13
+
14
+ - The core (`agentflow/` minus `backends/`) is dependency-free — standard
15
+ library only. Importing `agentflow` must not pull in `httpx`, `redis`,
16
+ `asyncpg`, `prometheus_client`, or `opentelemetry`.
17
+ - The engine (`graph`, `runtime`, `compiled`, `state`) never imports a backend.
18
+ - A backend imports only `events` and `errors` from the core, never `runtime`
19
+ or `graph`.
20
+ - Every provider library lives behind an optional extra and is imported lazily,
21
+ so the class is only resolved when you actually reference it.
22
+
23
+ This boundary is what lets the core run with zero third-party packages and lets
24
+ a backend be swapped without touching workflow code.
25
+
26
+ ## State and reducers
27
+
28
+ State is a set of named channels declared as a `TypedDict` subclass of `State`.
29
+ Each channel has a reducer — a pure function `(current, update) -> new` — that
30
+ folds a node's partial update into the current value.
31
+
32
+ - A field annotated `Annotated[type, reducer]` uses that reducer. A bare field
33
+ uses `last` (last-value-wins), the default.
34
+ - Built-in reducers: `last`, `append`, `add`, `merge`, `union`. A reducer must
35
+ be pure and must treat a `None` current as its empty value (empty list, 0,
36
+ empty dict/set).
37
+ - Reducers are what make concurrent updates in one super-step merge
38
+ deterministically instead of racing on last-writer-wins.
39
+ - Channel names starting with `__` and the names in `RESERVED_NAMES`
40
+ (`__step`, `__next`) belong to the engine. A schema or a node update that
41
+ writes one is rejected.
42
+ - Nodes may only write declared channels. Writing an undeclared channel raises
43
+ rather than sitting silently in state — a typo fails fast.
44
+
45
+ `State` is pure data: no async, no I/O, no engine imports. It can be understood
46
+ and tested in isolation.
47
+
48
+ ## Execution model
49
+
50
+ A run advances in super-steps. Each super-step runs the current frontier of
51
+ nodes, folds their updates into state, computes the next frontier from the
52
+ edges, and checkpoints — then the next super-step begins.
53
+
54
+ Invariants that must hold:
55
+
56
+ - Super-steps are all-or-nothing. If any node in a step raises, the whole
57
+ step's updates are discarded and no checkpoint is written for that step. The
58
+ last good checkpoint is the previous step, so a resume never sees a partial
59
+ step.
60
+ - Updates within a step are folded in a deterministic node order, so
61
+ non-commutative reducers (`append`, `last`) are well-defined. Commutative
62
+ reducers (`add`, `union`) do not depend on it.
63
+ - `step_limit` bounds total super-steps to catch a graph that never reaches
64
+ `END`.
65
+ - A whole-run timeout cancels the in-flight step and raises `RunTimeout`; the
66
+ last completed step's checkpoint is intact, so a checkpointed run resumes.
67
+
68
+ ### State isolation
69
+
70
+ `compile(isolate_state=...)` controls defensive copying of a node's input:
71
+
72
+ - `"fanout"` (default) deep-copies a node's input only when more than one node
73
+ runs in the same super-step, so an in-place mutation of a shared nested
74
+ container cannot race a sibling. Linear graphs pay no copy cost.
75
+ - `"always"` copies on every node; `"never"` never copies (fastest, but the
76
+ caller promises nodes do not mutate shared input in place).
77
+
78
+ ## Backends
79
+
80
+ Two kinds of backend share one event vocabulary, so a node consumes either the
81
+ same way — a stream of events ending in `TurnEnd`.
82
+
83
+ - Agent backends (`AgentBackend`) drive a session that runs its own tools and
84
+ asks permission. Lifecycle is `start()` / `prompt(text)` / `close()`, also
85
+ usable as an async context manager. `KiroBackend` holds a long-lived
86
+ JSON-RPC (ACP) session; `CodexBackend` and `ClaudeCodeBackend` run a fresh
87
+ subprocess per turn and carry a resumable session id between turns.
88
+ - LLM backends (`LLMBackend`) are stateless: messages in, token stream out.
89
+ They never run tools themselves — a tool call is a request the graph
90
+ fulfils.
91
+
92
+ Contracts a backend must honor:
93
+
94
+ - Import only `events` and `errors` from the core.
95
+ - Subprocess backends use argv lists (never `shell=True`), so a prompt can
96
+ never be interpreted as a shell command.
97
+ - Transport failures raise typed errors (`BackendTransportError`, and
98
+ `BackendRateLimitError` for a 429 that outlives retries). Retries cover the
99
+ connect / initial-response phase only, never a partially received stream.
100
+
101
+ ## Permissions
102
+
103
+ Authorization is explicit. Agent backends require a `PermissionPolicy` — there
104
+ is no auto-approve default. `AllowAll` is for a trusted local sandbox only;
105
+ other policies are `DenyAll`, `Interactive`, and `ToolAllowlist`. Only Kiro
106
+ routes tool requests through the policy; the one-shot CLI agents rely on their
107
+ own sandbox flags (see the capability matrix in the README).
108
+
109
+ ## Checkpointing and durability
110
+
111
+ A `Checkpointer` persists the execution state of one thread so a run can
112
+ resume, be inspected, or fork (time-travel). It is keyed by `(thread, step)`.
113
+
114
+ Contract:
115
+
116
+ - `put(cp, if_revision=...)` — `Checkpoint` carries a `revision` (the write
117
+ count for its `(thread, step)`, set on read). Passing `if_revision` makes the
118
+ write a compare-and-set; a mismatch raises `CheckpointConflict`. `None`
119
+ (default) is an unconditional upsert, so plain `put(cp)` is unchanged. This
120
+ gives optimistic concurrency for two resumes racing the same super-step.
121
+ - `get` / `history` — read one step or all steps of a thread.
122
+ - `delete_thread` / `prune(before_step=, older_than=)` — retention.
123
+ - `list_threads` — enumerate threads with a `ThreadInfo` summary, the
124
+ foundation the control plane builds on.
125
+
126
+ Implementations: `MemoryCheckpointer`, `FileCheckpointer` (single-writer,
127
+ atomic per-step files), `SqliteCheckpointer` (transactional, WAL),
128
+ `RedisCheckpointer`, `PostgresCheckpointer`. Serialization is shared in
129
+ `checkpoint/_serde.py` so the persistent backends never drift.
130
+
131
+ Durability note: Postgres is the recommended production default — a committed
132
+ row survives a crash without extra tuning. Redis is not crash-durable unless
133
+ AOF/RDB persistence is configured.
134
+
135
+ ## Human-in-the-loop
136
+
137
+ A node calls `await ctx.interrupt(payload)` to suspend the run for a human. The
138
+ runtime writes an interrupted checkpoint and stops. `resume(thread, value=...)`
139
+ continues the run, and the same `interrupt` call returns `value`. Because the
140
+ frontier is persisted, this works across process restarts.
141
+
142
+ ## Store
143
+
144
+ A `Store` is the other axis from a checkpointer: durable key-value data shared
145
+ across threads (profiles, facts, long-term memory), addressed by a `namespace`
146
+ tuple and a string `key`, holding any JSON value, with an optional per-item
147
+ TTL. Expired items never surface from `get` or `search`. Implementations:
148
+ `MemoryStore` and `PostgresStore`.
149
+
150
+ Keep this distinct from a checkpointer: a checkpointer is one thread's
151
+ execution state; a store is cross-thread application data.
152
+
153
+ ## Control plane
154
+
155
+ The control plane manages runs from outside the process that created them. It
156
+ is a library layer, not a service — there is no HTTP server (that is planned as
157
+ a thin adapter over these methods).
158
+
159
+ - `GraphRegistry` maps a graph name to a compiled-graph factory. Graphs are
160
+ code and cannot be serialized, so a run request references a graph by name;
161
+ the enqueuer and its workers must share an equivalent registry and the same
162
+ checkpointer.
163
+ - `RunQueue` holds run requests. Lifecycle: `queued` to `running` to
164
+ `succeeded` / `interrupted` / `failed` / `cancelled`; an interrupted run
165
+ returns to `queued` via `enqueue_resume`. Cancellation is cooperative —
166
+ `request_cancel` stops a running graph at a super-step boundary, leaving a
167
+ consistent checkpoint.
168
+ - `Worker` / `WorkerPool` claim requests under a time-bounded lease and execute
169
+ them; a heartbeat renews the lease, and a crashed worker's run is re-claimed
170
+ once its lease expires. `PostgresRunQueue` claims via
171
+ `FOR UPDATE SKIP LOCKED`, so each run goes to exactly one worker under
172
+ concurrency.
173
+ - Monitoring: `RunQueue.stats()` and `WorkerPool.health()` return plain values
174
+ a caller can expose through its own `/healthz` and `/metrics`.
175
+
176
+ ## Observability
177
+
178
+ `Hooks` are lifecycle callbacks attached at `compile(hooks=...)`. They are
179
+ awaited but must never raise — an exception in a hook is swallowed so
180
+ instrumentation cannot break a run. `RunMetrics`, `JsonlTelemetry`, `OtelHooks`
181
+ (otel extra), and `PrometheusHooks` (prometheus extra) are all `Hooks`
182
+ listeners; compose several with `MultiHooks`.
183
+
184
+ ## Security posture
185
+
186
+ - Persistence is plaintext by default. Checkpoints and JSONL telemetry contain
187
+ prompts, model output, and tool arguments. `RedactKeys` (via `redact=`) masks
188
+ sensitive keys; files and directories are created owner-only (0o600/0o700)
189
+ where the filesystem supports it. Redacted checkpoints are not resumable to
190
+ the exact original state — masked values are lost.
191
+ - Postgres backends validate the table name with `str.isidentifier()` before
192
+ interpolating it into SQL.
193
+ - Treat model and tool output and fetched content as untrusted. The SDK does
194
+ not execute model output; nodes decide what to run.
195
+
196
+ See `SECURITY.md` for the reporting process and the operator-facing summary.
197
+
198
+ ## What is intentionally out of scope
199
+
200
+ - An HTTP/gRPC API over the control plane, and the auth/multi-tenancy that a
201
+ service layer implies. These are properties of a service, not this SDK, and
202
+ are deferred to a future `server` layer that adapts the existing queue
203
+ methods.
204
+ - Rate limiting and per-tenant quotas. `max_concurrency` /
205
+ `max_node_concurrency` bound pressure within one process; per-tenant
206
+ isolation is the caller's responsibility until the service layer exists.