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.
- agent_workflow_sdk-0.1.0/.env.example +17 -0
- agent_workflow_sdk-0.1.0/.github/workflows/ci.yml +80 -0
- agent_workflow_sdk-0.1.0/.gitignore +13 -0
- agent_workflow_sdk-0.1.0/CHANGELOG.md +168 -0
- agent_workflow_sdk-0.1.0/CONTRIBUTING.md +67 -0
- agent_workflow_sdk-0.1.0/DESIGN.md +206 -0
- agent_workflow_sdk-0.1.0/LICENSE +201 -0
- agent_workflow_sdk-0.1.0/NOTICE +10 -0
- agent_workflow_sdk-0.1.0/PKG-INFO +566 -0
- agent_workflow_sdk-0.1.0/README.md +524 -0
- agent_workflow_sdk-0.1.0/SECURITY.md +37 -0
- agent_workflow_sdk-0.1.0/agentflow/__init__.py +199 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/__init__.py +54 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/_http.py +169 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/anthropic.py +291 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/base.py +327 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/claude_code.py +181 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/cli_exec.py +237 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/codex.py +126 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/kiro.py +523 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/ollama.py +208 -0
- agent_workflow_sdk-0.1.0/agentflow/backends/openai.py +249 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/__init__.py +51 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/_serde.py +57 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/base.py +125 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/file.py +210 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/memory.py +91 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/postgres.py +232 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/redis.py +206 -0
- agent_workflow_sdk-0.1.0/agentflow/checkpoint/sqlite.py +261 -0
- agent_workflow_sdk-0.1.0/agentflow/compiled.py +313 -0
- agent_workflow_sdk-0.1.0/agentflow/controlplane/__init__.py +57 -0
- agent_workflow_sdk-0.1.0/agentflow/controlplane/memory.py +181 -0
- agent_workflow_sdk-0.1.0/agentflow/controlplane/postgres.py +297 -0
- agent_workflow_sdk-0.1.0/agentflow/controlplane/queue.py +89 -0
- agent_workflow_sdk-0.1.0/agentflow/controlplane/records.py +122 -0
- agent_workflow_sdk-0.1.0/agentflow/controlplane/registry.py +70 -0
- agent_workflow_sdk-0.1.0/agentflow/controlplane/worker.py +219 -0
- agent_workflow_sdk-0.1.0/agentflow/errors.py +186 -0
- agent_workflow_sdk-0.1.0/agentflow/events.py +226 -0
- agent_workflow_sdk-0.1.0/agentflow/graph.py +328 -0
- agent_workflow_sdk-0.1.0/agentflow/observability.py +126 -0
- agent_workflow_sdk-0.1.0/agentflow/otel.py +124 -0
- agent_workflow_sdk-0.1.0/agentflow/prebuilt/__init__.py +29 -0
- agent_workflow_sdk-0.1.0/agentflow/prebuilt/loop.py +133 -0
- agent_workflow_sdk-0.1.0/agentflow/prebuilt/resilience.py +120 -0
- agent_workflow_sdk-0.1.0/agentflow/prebuilt/tool_loop.py +185 -0
- agent_workflow_sdk-0.1.0/agentflow/prometheus.py +175 -0
- agent_workflow_sdk-0.1.0/agentflow/py.typed +0 -0
- agent_workflow_sdk-0.1.0/agentflow/redaction.py +99 -0
- agent_workflow_sdk-0.1.0/agentflow/runtime.py +356 -0
- agent_workflow_sdk-0.1.0/agentflow/state.py +234 -0
- agent_workflow_sdk-0.1.0/agentflow/store/__init__.py +40 -0
- agent_workflow_sdk-0.1.0/agentflow/store/_util.py +54 -0
- agent_workflow_sdk-0.1.0/agentflow/store/base.py +103 -0
- agent_workflow_sdk-0.1.0/agentflow/store/memory.py +109 -0
- agent_workflow_sdk-0.1.0/agentflow/store/postgres.py +210 -0
- agent_workflow_sdk-0.1.0/agentflow/telemetry.py +197 -0
- agent_workflow_sdk-0.1.0/examples/agents_demo.py +137 -0
- agent_workflow_sdk-0.1.0/examples/codebase_qa_ollama.py +336 -0
- agent_workflow_sdk-0.1.0/examples/hello_graph.py +63 -0
- agent_workflow_sdk-0.1.0/examples/hitl_permission.py +107 -0
- agent_workflow_sdk-0.1.0/examples/mixed_backends.py +186 -0
- agent_workflow_sdk-0.1.0/examples/optimize_loop.py +48 -0
- agent_workflow_sdk-0.1.0/examples/telemetry_demo.py +85 -0
- agent_workflow_sdk-0.1.0/examples/tool_loop_ollama.py +96 -0
- agent_workflow_sdk-0.1.0/pyproject.toml +107 -0
- agent_workflow_sdk-0.1.0/tests/conftest.py +48 -0
- agent_workflow_sdk-0.1.0/tests/test_anthropic_backend.py +210 -0
- agent_workflow_sdk-0.1.0/tests/test_backend_lifecycle.py +101 -0
- agent_workflow_sdk-0.1.0/tests/test_checkpoint_contract.py +166 -0
- agent_workflow_sdk-0.1.0/tests/test_cli_backends.py +202 -0
- agent_workflow_sdk-0.1.0/tests/test_concurrency.py +136 -0
- agent_workflow_sdk-0.1.0/tests/test_controlplane.py +316 -0
- agent_workflow_sdk-0.1.0/tests/test_graph_engine.py +245 -0
- agent_workflow_sdk-0.1.0/tests/test_hitl_permission.py +228 -0
- agent_workflow_sdk-0.1.0/tests/test_http_retry.py +169 -0
- agent_workflow_sdk-0.1.0/tests/test_input_validation.py +60 -0
- agent_workflow_sdk-0.1.0/tests/test_kiro_backend.py +732 -0
- agent_workflow_sdk-0.1.0/tests/test_live_backends.py +462 -0
- agent_workflow_sdk-0.1.0/tests/test_observability.py +110 -0
- agent_workflow_sdk-0.1.0/tests/test_ollama_backend.py +124 -0
- agent_workflow_sdk-0.1.0/tests/test_openai_backend.py +191 -0
- agent_workflow_sdk-0.1.0/tests/test_otel.py +119 -0
- agent_workflow_sdk-0.1.0/tests/test_permission_policy.py +74 -0
- agent_workflow_sdk-0.1.0/tests/test_prebuilt_loop.py +76 -0
- agent_workflow_sdk-0.1.0/tests/test_prometheus.py +129 -0
- agent_workflow_sdk-0.1.0/tests/test_provider_resilience.py +185 -0
- agent_workflow_sdk-0.1.0/tests/test_redaction.py +133 -0
- agent_workflow_sdk-0.1.0/tests/test_redis_checkpointer.py +187 -0
- agent_workflow_sdk-0.1.0/tests/test_resilience.py +170 -0
- agent_workflow_sdk-0.1.0/tests/test_run_timeout.py +101 -0
- agent_workflow_sdk-0.1.0/tests/test_sqlite_checkpointer.py +117 -0
- agent_workflow_sdk-0.1.0/tests/test_state_isolation.py +116 -0
- agent_workflow_sdk-0.1.0/tests/test_step_atomicity.py +108 -0
- agent_workflow_sdk-0.1.0/tests/test_store.py +112 -0
- agent_workflow_sdk-0.1.0/tests/test_subgraph.py +126 -0
- agent_workflow_sdk-0.1.0/tests/test_sync_facade.py +78 -0
- agent_workflow_sdk-0.1.0/tests/test_telemetry.py +169 -0
- 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,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.
|