agent-workflow-sdk 0.3.0__tar.gz → 0.4.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.3.0 → agent_workflow_sdk-0.4.0}/.github/workflows/docs.yml +11 -5
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/CHANGELOG.md +25 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/DESIGN.md +17 -7
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/PKG-INFO +5 -1
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/__init__.py +1 -1
- agent_workflow_sdk-0.4.0/agentflow/server/__init__.py +46 -0
- agent_workflow_sdk-0.4.0/agentflow/server/app.py +205 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/durability/control-plane.md +62 -1
- agent_workflow_sdk-0.4.0/docs/reference/api/server.md +23 -0
- agent_workflow_sdk-0.4.0/examples/serve_control_plane.py +89 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/mkdocs.yml +1 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/pyproject.toml +4 -1
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_otel.py +4 -2
- agent_workflow_sdk-0.4.0/tests/test_server.py +172 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/.env.example +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/.github/workflows/ci.yml +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/.github/workflows/release.yml +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/.gitignore +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/CONTRIBUTING.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/LICENSE +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/NOTICE +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/README.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/SECURITY.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/_pg.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/__init__.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/_http.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/anthropic.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/base.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/claude_code.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/cli_exec.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/codex.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/kiro.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/ollama.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/backends/openai.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/__init__.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/_serde.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/base.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/file.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/memory.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/postgres.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/redis.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/checkpoint/sqlite.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/compiled.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/controlplane/__init__.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/controlplane/memory.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/controlplane/postgres.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/controlplane/queue.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/controlplane/records.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/controlplane/registry.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/controlplane/worker.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/errors.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/events.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/graph.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/interrupts.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/observability.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/otel.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/__init__.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/effects.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/gate.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/idempotent.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/loop.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/resilience.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/tool_loop.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prebuilt/watch.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/prometheus.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/py.typed +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/redaction.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/runtime.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/state.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/store/__init__.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/store/_util.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/store/base.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/store/memory.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/store/postgres.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/agentflow/telemetry.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/backends/agent-backends.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/backends/llm-backends.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/backends/overview.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/backends/permissions.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/backends/retries.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/concepts/context.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/concepts/execution.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/concepts/nodes-and-edges.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/concepts/running.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/concepts/state.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/concepts/subgraphs.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/durability/checkpointing.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/durability/human-in-the-loop.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/durability/observability.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/durability/quality-gates.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/durability/store.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/get-started/installation.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/get-started/mental-model.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/get-started/overview.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/get-started/quickstart.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/index.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/patterns/long-running-loops.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/patterns/prebuilt.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/patterns/tutorial-qa-agent.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/backends.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/checkpoint.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/compiled.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/controlplane.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/errors.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/events.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/gate.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/graph.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/interrupts.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/observability.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/permissions.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/prebuilt.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/runtime.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/state.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/store.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api/watch.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/api.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/docs/reference/glossary.md +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/agents_demo.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/codebase_qa_ollama.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/hello_graph.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/hitl_permission.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/idempotent_effect.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/mixed_backends.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/optimize_loop.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/quality_gate.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/research_plan_implement_review.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/telemetry_demo.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/tool_loop_ollama.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/examples/watch_until_done.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/conftest.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_anthropic_backend.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_backend_lifecycle.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_checkpoint_contract.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_cli_backends.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_concurrency.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_controlplane.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_effects.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_gate.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_graph_engine.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_hitl_permission.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_http_retry.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_idempotent.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_input_validation.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_kiro_backend.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_live_backends.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_observability.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_ollama_backend.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_openai_backend.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_permission_policy.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_postgres_checkpointer.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_postgres_runqueue.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_postgres_store.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_prebuilt_loop.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_prebuilt_no_httpx.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_prometheus.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_provider_resilience.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_redaction.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_redis_checkpointer.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_resilience.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_run_timeout.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_sqlite_checkpointer.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_state_isolation.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_step_atomicity.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_store.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_subgraph.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_sync_facade.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_telemetry.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_tool_loop.py +0 -0
- {agent_workflow_sdk-0.3.0 → agent_workflow_sdk-0.4.0}/tests/test_watch.py +0 -0
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
name: Docs
|
|
2
2
|
|
|
3
|
-
# Build the mkdocs site and deploy it to GitHub Pages
|
|
4
|
-
# demand from the Actions tab.
|
|
5
|
-
#
|
|
3
|
+
# Build the mkdocs site and deploy it to GitHub Pages when docs change on main,
|
|
4
|
+
# or on demand from the Actions tab. Triggering on main (not a tag) matches the
|
|
5
|
+
# github-pages environment's branch protection, which allows the default branch
|
|
6
|
+
# — a tag push is rejected by that rule. Enable Pages once
|
|
7
|
+
# (Settings -> Pages -> Source: GitHub Actions) for the deploy step to publish.
|
|
6
8
|
on:
|
|
7
9
|
push:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
branches: [main]
|
|
11
|
+
paths:
|
|
12
|
+
- "docs/**"
|
|
13
|
+
- "mkdocs.yml"
|
|
14
|
+
- "agentflow/**"
|
|
15
|
+
- ".github/workflows/docs.yml"
|
|
10
16
|
workflow_dispatch:
|
|
11
17
|
|
|
12
18
|
# Allow one concurrent deploy; a newer run supersedes an in-flight one.
|
|
@@ -4,6 +4,31 @@ All notable changes to `agent-workflow-sdk` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project aims to follow [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-10-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Optional HTTP control-plane server (`server` extra): `agentflow.server.
|
|
12
|
+
create_app(queue)` returns a FastAPI ASGI app that maps 1:1 onto a
|
|
13
|
+
`RunQueue`, so runs can be enqueued and observed over the network. Routes:
|
|
14
|
+
`POST /runs`, `GET /runs/{id}`, `GET /runs`, `POST /runs/{id}/resume`,
|
|
15
|
+
`POST /runs/{id}/cancel`, `GET /stats`, `GET /healthz`. Request/response
|
|
16
|
+
bodies are Pydantic models, so FastAPI serves interactive OpenAPI docs at
|
|
17
|
+
`/docs`. `pool_health=` wires a `WorkerPool`'s health into `/healthz`. The
|
|
18
|
+
app adds no behavior of its own — each route forwards to one queue method.
|
|
19
|
+
See `examples/serve_control_plane.py` and the control-plane docs.
|
|
20
|
+
- `server` install extra: `pip install 'agent-workflow-sdk[server]'`
|
|
21
|
+
(`fastapi>=0.110`). The core remains dependency-free; serve the returned
|
|
22
|
+
ASGI app with any ASGI server (e.g. `uvicorn`), installed separately.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- The worker protocol (`claim` / `heartbeat` / `complete`) is deliberately not
|
|
27
|
+
exposed over HTTP; workers talk to the queue directly. The HTTP layer ships
|
|
28
|
+
no authentication, authorization, rate limiting, or multi-tenancy — those
|
|
29
|
+
stay out of scope and are the caller's responsibility (trust boundary /
|
|
30
|
+
reverse proxy). `DESIGN.md` updated accordingly.
|
|
31
|
+
|
|
7
32
|
## [0.3.0] - 2026-10-02
|
|
8
33
|
|
|
9
34
|
### Added
|
|
@@ -152,9 +152,11 @@ execution state; a store is cross-thread application data.
|
|
|
152
152
|
|
|
153
153
|
## Control plane
|
|
154
154
|
|
|
155
|
-
The control plane manages runs from outside the process that created them.
|
|
156
|
-
is a library layer, not a service
|
|
157
|
-
a thin adapter
|
|
155
|
+
The control plane manages runs from outside the process that created them. The
|
|
156
|
+
core is a library layer, not a service. An optional HTTP layer — the `server`
|
|
157
|
+
extra, `agentflow.server.create_app` — is a thin FastAPI adapter mapping 1:1
|
|
158
|
+
over the queue methods below; it adds no behavior of its own, and auth and
|
|
159
|
+
multi-tenancy remain out of scope (see below).
|
|
158
160
|
|
|
159
161
|
- `GraphRegistry` maps a graph name to a compiled-graph factory. Graphs are
|
|
160
162
|
code and cannot be serialized, so a run request references a graph by name;
|
|
@@ -172,6 +174,13 @@ a thin adapter over these methods).
|
|
|
172
174
|
concurrency.
|
|
173
175
|
- Monitoring: `RunQueue.stats()` and `WorkerPool.health()` return plain values
|
|
174
176
|
a caller can expose through its own `/healthz` and `/metrics`.
|
|
177
|
+
- HTTP (optional, `server` extra): `agentflow.server.create_app(queue)` returns
|
|
178
|
+
a FastAPI ASGI app exposing the enqueue-and-observe plane — `POST /runs`,
|
|
179
|
+
`GET /runs/{id}`, `GET /runs`, `POST /runs/{id}/resume`,
|
|
180
|
+
`POST /runs/{id}/cancel`, `GET /stats`, `GET /healthz`. The worker protocol
|
|
181
|
+
(`claim` / `heartbeat` / `complete`) is deliberately not exposed; workers
|
|
182
|
+
talk to the queue directly. Each route forwards to one queue method, so the
|
|
183
|
+
HTTP layer carries no state of its own.
|
|
175
184
|
|
|
176
185
|
## Observability
|
|
177
186
|
|
|
@@ -197,10 +206,11 @@ See `SECURITY.md` for the reporting process and the operator-facing summary.
|
|
|
197
206
|
|
|
198
207
|
## What is intentionally out of scope
|
|
199
208
|
|
|
200
|
-
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
- Authentication, authorization, and multi-tenancy over the HTTP layer. The
|
|
210
|
+
optional `server` extra adapts the queue to HTTP, but it ships no auth and no
|
|
211
|
+
per-tenant isolation — those are properties of a service, not this SDK. Run
|
|
212
|
+
it inside your own trust boundary, or behind a reverse proxy that supplies
|
|
213
|
+
them. A gRPC API is not provided.
|
|
204
214
|
- Rate limiting and per-tenant quotas. `max_concurrency` /
|
|
205
215
|
`max_node_concurrency` bound pressure within one process; per-tenant
|
|
206
216
|
isolation is the caller's responsibility until the service layer exists.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-workflow-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Async-first SDK for orchestrating local coding-agent CLIs (Claude Code, Codex, Kiro) and LLMs as durable, graph-based workflows.
|
|
5
5
|
Project-URL: Homepage, https://github.com/dankosorkin/agent-workflow-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/dankosorkin/agent-workflow-sdk
|
|
@@ -20,6 +20,8 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: fakeredis>=2.20; extra == 'dev'
|
|
23
|
+
Requires-Dist: fastapi>=0.110; extra == 'dev'
|
|
24
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
23
25
|
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
24
26
|
Requires-Dist: pip-audit>=2.7; extra == 'dev'
|
|
25
27
|
Requires-Dist: prometheus-client>=0.20; extra == 'dev'
|
|
@@ -41,6 +43,8 @@ Provides-Extra: prometheus
|
|
|
41
43
|
Requires-Dist: prometheus-client>=0.20; extra == 'prometheus'
|
|
42
44
|
Provides-Extra: redis
|
|
43
45
|
Requires-Dist: redis>=5; extra == 'redis'
|
|
46
|
+
Provides-Extra: server
|
|
47
|
+
Requires-Dist: fastapi>=0.110; extra == 'server'
|
|
44
48
|
Description-Content-Type: text/markdown
|
|
45
49
|
|
|
46
50
|
# agent-workflow-sdk
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright 2026 Daniel Sorkin
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""An HTTP adapter over the control-plane run queue (optional ``server`` extra).
|
|
16
|
+
|
|
17
|
+
This is the thin service layer DESIGN.md defers to: a FastAPI app that maps 1:1
|
|
18
|
+
onto :class:`~agentflow.controlplane.queue.RunQueue` methods, so a run can be
|
|
19
|
+
enqueued, inspected, resumed, or cancelled over HTTP from anywhere. It adds no
|
|
20
|
+
behavior the queue does not already have.
|
|
21
|
+
|
|
22
|
+
from agentflow import PostgresRunQueue
|
|
23
|
+
from agentflow.server import create_app
|
|
24
|
+
|
|
25
|
+
app = create_app(PostgresRunQueue(dsn)) # an ASGI app; serve with uvicorn
|
|
26
|
+
|
|
27
|
+
What it exposes is the *enqueue and observe* plane: ``POST /runs``, ``GET
|
|
28
|
+
/runs/{id}``, ``GET /runs``, ``POST /runs/{id}/resume``, ``POST
|
|
29
|
+
/runs/{id}/cancel``, ``GET /stats``, ``GET /healthz``. The worker protocol
|
|
30
|
+
(``claim`` / ``heartbeat`` / ``complete``) is deliberately *not* exposed —
|
|
31
|
+
those mutate run state and belong to workers, which talk to the queue directly
|
|
32
|
+
(in-process or via the same Postgres queue), not over this API.
|
|
33
|
+
|
|
34
|
+
Scope, stated plainly: there is no authentication, authorization, rate
|
|
35
|
+
limiting, or multi-tenancy here — DESIGN.md defers those as properties of a
|
|
36
|
+
service. Run this inside your own trust boundary, or behind a reverse proxy
|
|
37
|
+
that supplies them. It is an adapter, not a hardened public endpoint.
|
|
38
|
+
|
|
39
|
+
Requires the ``server`` extra: ``pip install 'agent-workflow-sdk[server]'``.
|
|
40
|
+
Serve the returned ASGI app with any ASGI server (e.g. ``uvicorn``), which you
|
|
41
|
+
install separately.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from agentflow.server.app import create_app
|
|
45
|
+
|
|
46
|
+
__all__ = ["create_app"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Copyright 2026 Daniel Sorkin
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""FastAPI app factory mapping HTTP routes onto a ``RunQueue``.
|
|
16
|
+
|
|
17
|
+
Each route is a direct adapter over one queue method; see the package docstring
|
|
18
|
+
for the exposed surface and the explicit non-goals (no auth, no worker
|
|
19
|
+
protocol). Response/request shapes are Pydantic models so the generated OpenAPI
|
|
20
|
+
docs (``/docs``) are accurate.
|
|
21
|
+
|
|
22
|
+
Note: this module intentionally does NOT use ``from __future__ import
|
|
23
|
+
annotations``. FastAPI resolves endpoint annotations with ``get_type_hints``,
|
|
24
|
+
which cannot re-evaluate a stringized ``Depends(get_queue)`` that references a
|
|
25
|
+
function-local dependency — real annotation objects are required.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
from typing import Annotated, Any
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from fastapi import Depends, FastAPI, HTTPException, Query
|
|
33
|
+
from pydantic import BaseModel, Field
|
|
34
|
+
except ModuleNotFoundError as exc: # pragma: no cover - import-time guard
|
|
35
|
+
raise ModuleNotFoundError(
|
|
36
|
+
"agentflow.server requires FastAPI. Install the extra: "
|
|
37
|
+
"pip install 'agent-workflow-sdk[server]'"
|
|
38
|
+
) from exc
|
|
39
|
+
|
|
40
|
+
from agentflow.controlplane.queue import RunQueue
|
|
41
|
+
from agentflow.controlplane.records import QueueStats, RunRecord
|
|
42
|
+
from agentflow.errors import RunNotFound
|
|
43
|
+
|
|
44
|
+
__all__ = ["create_app"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Wire models (what crosses the HTTP boundary)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EnqueueRequest(BaseModel):
|
|
53
|
+
"""Body for ``POST /runs`` — ask to run a registered graph."""
|
|
54
|
+
|
|
55
|
+
graph: str = Field(..., description="Name of a graph registered on the worker side.")
|
|
56
|
+
input: dict[str, Any] = Field(default_factory=dict, description="Initial state for the run.")
|
|
57
|
+
thread: str | None = Field(None, description="Checkpoint thread; defaults to the run id.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ResumeRequest(BaseModel):
|
|
61
|
+
"""Body for ``POST /runs/{run_id}/resume`` — answer an interrupted run."""
|
|
62
|
+
|
|
63
|
+
value: Any = Field(None, description="Value fed to the interrupted node on resume.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RunResponse(BaseModel):
|
|
67
|
+
"""One run's queue record."""
|
|
68
|
+
|
|
69
|
+
run_id: str
|
|
70
|
+
graph: str
|
|
71
|
+
thread: str
|
|
72
|
+
status: str
|
|
73
|
+
input: dict[str, Any] = Field(default_factory=dict)
|
|
74
|
+
resume_value: Any = None
|
|
75
|
+
error: str | None = None
|
|
76
|
+
attempt: int = 0
|
|
77
|
+
created_at: str = ""
|
|
78
|
+
updated_at: str = ""
|
|
79
|
+
lease_until: str | None = None
|
|
80
|
+
wake_at: str | None = None
|
|
81
|
+
cancel_requested: bool = False
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def of(cls, rec: RunRecord) -> "RunResponse":
|
|
85
|
+
return cls(
|
|
86
|
+
run_id=rec.run_id,
|
|
87
|
+
graph=rec.graph,
|
|
88
|
+
thread=rec.thread,
|
|
89
|
+
status=rec.status,
|
|
90
|
+
input=dict(rec.input),
|
|
91
|
+
resume_value=rec.resume_value,
|
|
92
|
+
error=rec.error,
|
|
93
|
+
attempt=rec.attempt,
|
|
94
|
+
created_at=rec.created_at,
|
|
95
|
+
updated_at=rec.updated_at,
|
|
96
|
+
lease_until=rec.lease_until,
|
|
97
|
+
wake_at=rec.wake_at,
|
|
98
|
+
cancel_requested=rec.cancel_requested,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CancelResponse(BaseModel):
|
|
103
|
+
cancelled: bool = Field(
|
|
104
|
+
..., description="True if the run existed and was not already terminal."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class StatsResponse(BaseModel):
|
|
109
|
+
total: int = 0
|
|
110
|
+
by_status: dict[str, int] = Field(default_factory=dict)
|
|
111
|
+
expired_leases: int = 0
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def of(cls, s: QueueStats) -> "StatsResponse":
|
|
115
|
+
return cls(total=s.total, by_status=dict(s.by_status), expired_leases=s.expired_leases)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# App factory
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def create_app(
|
|
124
|
+
queue: RunQueue,
|
|
125
|
+
*,
|
|
126
|
+
pool_health: Callable[[], Any] | None = None,
|
|
127
|
+
title: str = "AgentFlow control plane",
|
|
128
|
+
) -> FastAPI:
|
|
129
|
+
"""Build a FastAPI app over ``queue``.
|
|
130
|
+
|
|
131
|
+
``queue`` is any :class:`~agentflow.controlplane.queue.RunQueue` (Memory for
|
|
132
|
+
a single process, Postgres for a shared/durable one). ``pool_health``, if
|
|
133
|
+
given, is a zero-arg callable returning a
|
|
134
|
+
:class:`~agentflow.controlplane.records.PoolHealth` (typically
|
|
135
|
+
``pool.health``); ``GET /healthz`` reports it when present. The app adds no
|
|
136
|
+
behavior of its own — every route forwards to a queue method.
|
|
137
|
+
"""
|
|
138
|
+
app = FastAPI(title=title, version="1")
|
|
139
|
+
|
|
140
|
+
def get_queue() -> RunQueue:
|
|
141
|
+
return queue
|
|
142
|
+
|
|
143
|
+
# FastAPI dependency injected via the Annotated form (the modern idiom:
|
|
144
|
+
# keeps Depends() out of the parameter default, so no ruff B008). Inlined in
|
|
145
|
+
# each signature rather than aliased to a local var, because FastAPI
|
|
146
|
+
# resolves annotations by name and would not see a function-local alias.
|
|
147
|
+
@app.post("/runs", response_model=RunResponse, status_code=201, tags=["runs"])
|
|
148
|
+
async def enqueue_run(
|
|
149
|
+
body: EnqueueRequest, q: Annotated[RunQueue, Depends(get_queue)]
|
|
150
|
+
) -> RunResponse:
|
|
151
|
+
rec = await q.enqueue(body.graph, body.input, thread=body.thread)
|
|
152
|
+
return RunResponse.of(rec)
|
|
153
|
+
|
|
154
|
+
@app.get("/runs/{run_id}", response_model=RunResponse, tags=["runs"])
|
|
155
|
+
async def get_run(run_id: str, q: Annotated[RunQueue, Depends(get_queue)]) -> RunResponse:
|
|
156
|
+
rec = await q.get(run_id)
|
|
157
|
+
if rec is None:
|
|
158
|
+
raise HTTPException(status_code=404, detail=f"no run with id {run_id!r}")
|
|
159
|
+
return RunResponse.of(rec)
|
|
160
|
+
|
|
161
|
+
@app.get("/runs", response_model=list[RunResponse], tags=["runs"])
|
|
162
|
+
async def list_runs(
|
|
163
|
+
q: Annotated[RunQueue, Depends(get_queue)],
|
|
164
|
+
status: Annotated[str | None, Query()] = None,
|
|
165
|
+
graph: Annotated[str | None, Query()] = None,
|
|
166
|
+
limit: Annotated[int, Query(ge=1, le=1000)] = 100,
|
|
167
|
+
offset: Annotated[int, Query(ge=0)] = 0,
|
|
168
|
+
) -> list[RunResponse]:
|
|
169
|
+
recs = await q.list(status=status, graph=graph, limit=limit, offset=offset)
|
|
170
|
+
return [RunResponse.of(r) for r in recs]
|
|
171
|
+
|
|
172
|
+
@app.post("/runs/{run_id}/resume", response_model=RunResponse, tags=["runs"])
|
|
173
|
+
async def resume_run(
|
|
174
|
+
run_id: str, body: ResumeRequest, q: Annotated[RunQueue, Depends(get_queue)]
|
|
175
|
+
) -> RunResponse:
|
|
176
|
+
try:
|
|
177
|
+
rec = await q.enqueue_resume(run_id, body.value)
|
|
178
|
+
except RunNotFound:
|
|
179
|
+
raise HTTPException(status_code=404, detail=f"no run with id {run_id!r}") from None
|
|
180
|
+
return RunResponse.of(rec)
|
|
181
|
+
|
|
182
|
+
@app.post("/runs/{run_id}/cancel", response_model=CancelResponse, tags=["runs"])
|
|
183
|
+
async def cancel_run(run_id: str, q: Annotated[RunQueue, Depends(get_queue)]) -> CancelResponse:
|
|
184
|
+
cancelled = await q.request_cancel(run_id)
|
|
185
|
+
return CancelResponse(cancelled=cancelled)
|
|
186
|
+
|
|
187
|
+
@app.get("/stats", response_model=StatsResponse, tags=["monitoring"])
|
|
188
|
+
async def get_stats(q: Annotated[RunQueue, Depends(get_queue)]) -> StatsResponse:
|
|
189
|
+
return StatsResponse.of(await q.stats())
|
|
190
|
+
|
|
191
|
+
@app.get("/healthz", tags=["monitoring"])
|
|
192
|
+
async def healthz() -> dict[str, Any]:
|
|
193
|
+
body: dict[str, Any] = {"status": "ok"}
|
|
194
|
+
if pool_health is not None:
|
|
195
|
+
h = pool_health()
|
|
196
|
+
body["pool"] = {
|
|
197
|
+
"workers": h.workers,
|
|
198
|
+
"alive": h.alive,
|
|
199
|
+
"busy": h.busy,
|
|
200
|
+
"idle": h.idle,
|
|
201
|
+
"healthy": h.healthy,
|
|
202
|
+
}
|
|
203
|
+
return body
|
|
204
|
+
|
|
205
|
+
return app
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## What it is, and is not
|
|
6
6
|
|
|
7
|
-
The control plane is a library layer
|
|
7
|
+
The control plane is a library layer first. What you get is the durable machinery: a queue, a registry, and workers, all as plain objects you drive. An optional HTTP layer (the `server` extra) wraps the queue so you can drive it over the network — covered at the end of this chapter. There is still no auth or tenancy anywhere in it; those are properties of a service you put in front, and stay out of scope.
|
|
8
8
|
|
|
9
9
|
## The pieces
|
|
10
10
|
|
|
@@ -105,6 +105,67 @@ if not health.healthy:
|
|
|
105
105
|
|
|
106
106
|
A rising `expired_leases` is the signal that workers are dying or falling behind their heartbeat. `health.healthy` is False when any configured worker loop is not alive — a readiness signal you can expose directly.
|
|
107
107
|
|
|
108
|
+
## Over HTTP
|
|
109
|
+
|
|
110
|
+
Everything so far drives the queue from Python. The optional `server` extra wraps the same queue in a FastAPI app, so you can enqueue and observe runs over the network — from a different language, a web UI, or a `curl` in a shell.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install 'agent-workflow-sdk[server]'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`create_app` takes a `RunQueue` and returns an ASGI app. It adds no behavior of its own; each route forwards to one queue method.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from agentflow import PostgresRunQueue
|
|
120
|
+
from agentflow.server import create_app
|
|
121
|
+
|
|
122
|
+
queue = PostgresRunQueue(dsn)
|
|
123
|
+
app = create_app(queue) # an ASGI app
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Serve it with any ASGI server. `uvicorn` is a common choice and you install it yourself:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
uvicorn myapp:app --port 8000
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The routes map 1:1 onto the queue methods from earlier in this chapter:
|
|
133
|
+
|
|
134
|
+
- `POST /runs` enqueues a run. Body: `{"graph": "...", "input": {...}, "thread": "..."}` (thread optional). Returns `201` with the run record.
|
|
135
|
+
- `GET /runs/{id}` returns one run record, or `404` if there is no such run.
|
|
136
|
+
- `GET /runs` lists runs. Query params: `status`, `graph`, `limit` (1–1000), `offset`.
|
|
137
|
+
- `POST /runs/{id}/resume` re-queues an interrupted run. Body: `{"value": ...}`. `404` if there is no such run.
|
|
138
|
+
- `POST /runs/{id}/cancel` requests cancellation. Returns `{"cancelled": true}` unless the run was unknown or already terminal.
|
|
139
|
+
- `GET /stats` returns the `QueueStats` snapshot (`total`, `by_status`, `expired_leases`).
|
|
140
|
+
- `GET /healthz` returns `{"status": "ok"}`, plus a `pool` block when you pass `pool_health`.
|
|
141
|
+
|
|
142
|
+
Because the models are typed, FastAPI serves interactive OpenAPI docs at `/docs` and the raw schema at `/openapi.json` with no extra work.
|
|
143
|
+
|
|
144
|
+
A quick enqueue over HTTP:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
curl -X POST localhost:8000/runs \
|
|
148
|
+
-H 'content-type: application/json' \
|
|
149
|
+
-d '{"graph": "summarize", "input": {"text": "..."}}'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Wire the pool's health into `/healthz` by passing the callable:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
pool = WorkerPool(queue, registry, concurrency=4)
|
|
156
|
+
app = create_app(queue, pool_health=lambda: pool.health())
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### What it does not expose
|
|
160
|
+
|
|
161
|
+
The worker protocol — `claim`, `heartbeat`, `complete` — is deliberately absent from the HTTP surface. Those methods mutate run state and belong to workers, which talk to the queue directly (in-process, or via the same `PostgresRunQueue`), not over this API. The HTTP layer is the enqueue-and-observe plane only.
|
|
162
|
+
|
|
163
|
+
### Trust boundary
|
|
164
|
+
|
|
165
|
+
There is no authentication, authorization, rate limiting, or multi-tenancy in this app. It is an adapter over the queue, not a hardened public endpoint. Run it inside your own trust boundary, or behind a reverse proxy or gateway that supplies those. Anyone who can reach it can enqueue, cancel, and resume any run.
|
|
166
|
+
|
|
167
|
+
See the [HTTP server API reference](../reference/api/server.md) for the request and response models.
|
|
168
|
+
|
|
108
169
|
## When to use the control plane
|
|
109
170
|
|
|
110
171
|
- Use inline `invoke`/`stream` when the run belongs to the calling process — a request handler, a script, a test.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# HTTP server
|
|
2
|
+
|
|
3
|
+
The optional FastAPI adapter over a run queue. See the [Control plane over HTTP](../../durability/control-plane.md#over-http) guide.
|
|
4
|
+
|
|
5
|
+
Requires the `server` extra: `pip install 'agent-workflow-sdk[server]'`.
|
|
6
|
+
|
|
7
|
+
::: agentflow.server
|
|
8
|
+
options:
|
|
9
|
+
members:
|
|
10
|
+
- create_app
|
|
11
|
+
|
|
12
|
+
## Wire models
|
|
13
|
+
|
|
14
|
+
The request and response bodies that cross the HTTP boundary. These are Pydantic models, so they also drive the generated OpenAPI schema served at `/openapi.json` and the interactive docs at `/docs`.
|
|
15
|
+
|
|
16
|
+
::: agentflow.server.app
|
|
17
|
+
options:
|
|
18
|
+
members:
|
|
19
|
+
- EnqueueRequest
|
|
20
|
+
- ResumeRequest
|
|
21
|
+
- RunResponse
|
|
22
|
+
- CancelResponse
|
|
23
|
+
- StatsResponse
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Copyright 2026 Daniel Sorkin
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Drive the control plane over HTTP.
|
|
16
|
+
|
|
17
|
+
Run: python examples/serve_control_plane.py
|
|
18
|
+
Needs the server extra: pip install 'agent-workflow-sdk[server]'
|
|
19
|
+
|
|
20
|
+
``create_app(queue)`` returns a FastAPI ASGI app that maps 1:1 onto the run
|
|
21
|
+
queue: POST /runs enqueues, GET /runs[/{id}] observes, resume/cancel manage,
|
|
22
|
+
/stats and /healthz monitor. The app adds no behavior of its own — every route
|
|
23
|
+
forwards to one queue method.
|
|
24
|
+
|
|
25
|
+
To keep this offline and self-contained, it exercises the app through FastAPI's
|
|
26
|
+
TestClient (no socket, no running server). In a real deployment you would skip
|
|
27
|
+
the client and serve the module-level ``app`` with an ASGI server instead:
|
|
28
|
+
|
|
29
|
+
uvicorn examples.serve_control_plane:app --port 8000
|
|
30
|
+
|
|
31
|
+
and a worker (usually a separate process) would drain the same queue. Here the
|
|
32
|
+
queue is a MemoryRunQueue, so there is nothing to actually execute the run — we
|
|
33
|
+
only demonstrate the HTTP surface and the run lifecycle up to "queued".
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from agentflow import MemoryRunQueue, PoolHealth
|
|
39
|
+
from agentflow.server import create_app
|
|
40
|
+
|
|
41
|
+
# A module-level ASGI app, so `uvicorn examples.serve_control_plane:app` works.
|
|
42
|
+
# pool_health is optional; here it is a static stand-in for a WorkerPool's
|
|
43
|
+
# health, which /healthz surfaces as a readiness signal.
|
|
44
|
+
queue = MemoryRunQueue()
|
|
45
|
+
app = create_app(queue, pool_health=lambda: PoolHealth(workers=4, alive=4, busy=1))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> None:
|
|
49
|
+
try:
|
|
50
|
+
from fastapi.testclient import TestClient
|
|
51
|
+
except ModuleNotFoundError:
|
|
52
|
+
raise SystemExit(
|
|
53
|
+
"this example needs the server extra: pip install 'agent-workflow-sdk[server]'"
|
|
54
|
+
) from None
|
|
55
|
+
|
|
56
|
+
client = TestClient(app)
|
|
57
|
+
|
|
58
|
+
print("POST /runs")
|
|
59
|
+
created = client.post("/runs", json={"graph": "summarize", "input": {"text": "..."}})
|
|
60
|
+
run = created.json()
|
|
61
|
+
print(f" -> {created.status_code} {run['status']} id={run['run_id']}")
|
|
62
|
+
|
|
63
|
+
print("GET /runs/{id}")
|
|
64
|
+
got = client.get(f"/runs/{run['run_id']}").json()
|
|
65
|
+
print(f" -> status={got['status']} graph={got['graph']}")
|
|
66
|
+
|
|
67
|
+
print("GET /runs?graph=summarize")
|
|
68
|
+
listed = client.get("/runs", params={"graph": "summarize"}).json()
|
|
69
|
+
print(f" -> {len(listed)} run(s)")
|
|
70
|
+
|
|
71
|
+
print("GET /stats")
|
|
72
|
+
stats = client.get("/stats").json()
|
|
73
|
+
print(f" -> total={stats['total']} by_status={stats['by_status']}")
|
|
74
|
+
|
|
75
|
+
print("GET /healthz")
|
|
76
|
+
print(f" -> {client.get('/healthz').json()}")
|
|
77
|
+
|
|
78
|
+
print("POST /runs/{id}/cancel")
|
|
79
|
+
rid = run["run_id"]
|
|
80
|
+
cancelled = client.post(f"/runs/{rid}/cancel").json()
|
|
81
|
+
print(f" -> cancelled={cancelled['cancelled']}")
|
|
82
|
+
now = client.get(f"/runs/{rid}").json()["status"]
|
|
83
|
+
print(f" -> status now: {now}")
|
|
84
|
+
|
|
85
|
+
print("\ninteractive OpenAPI docs would be at /docs; schema at /openapi.json")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -144,6 +144,7 @@ nav:
|
|
|
144
144
|
- Checkpointing: reference/api/checkpoint.md
|
|
145
145
|
- Store: reference/api/store.md
|
|
146
146
|
- Control plane: reference/api/controlplane.md
|
|
147
|
+
- HTTP server: reference/api/server.md
|
|
147
148
|
- Observability: reference/api/observability.md
|
|
148
149
|
- Prebuilt patterns: reference/api/prebuilt.md
|
|
149
150
|
- Quality gates: reference/api/gate.md
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-workflow-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Async-first SDK for orchestrating local coding-agent CLIs (Claude Code, Codex, Kiro) and LLMs as durable, graph-based workflows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -31,6 +31,7 @@ otel = ["opentelemetry-api>=1.20", "opentelemetry-sdk>=1.20"]
|
|
|
31
31
|
redis = ["redis>=5"]
|
|
32
32
|
postgres = ["asyncpg>=0.29"]
|
|
33
33
|
prometheus = ["prometheus-client>=0.20"]
|
|
34
|
+
server = ["fastapi>=0.110"]
|
|
34
35
|
docs = ["mkdocs-material>=9.5", "mkdocstrings[python]>=0.26"]
|
|
35
36
|
dev = [
|
|
36
37
|
"pytest>=8",
|
|
@@ -41,6 +42,8 @@ dev = [
|
|
|
41
42
|
"pip-audit>=2.7",
|
|
42
43
|
"fakeredis>=2.20",
|
|
43
44
|
"prometheus-client>=0.20",
|
|
45
|
+
"fastapi>=0.110",
|
|
46
|
+
"httpx>=0.27",
|
|
44
47
|
]
|
|
45
48
|
|
|
46
49
|
[project.urls]
|
|
@@ -21,8 +21,10 @@ from typing import Annotated
|
|
|
21
21
|
import pytest
|
|
22
22
|
from agentflow import END, START, Graph, State, add
|
|
23
23
|
|
|
24
|
-
# Skip the whole module if the otel extra isn't installed.
|
|
25
|
-
|
|
24
|
+
# Skip the whole module if the otel extra isn't installed. Guard on
|
|
25
|
+
# opentelemetry.sdk, not the bare opentelemetry namespace: the API package can
|
|
26
|
+
# be present transitively while the SDK (the otel extra) is not.
|
|
27
|
+
otel = pytest.importorskip("opentelemetry.sdk")
|
|
26
28
|
|
|
27
29
|
from agentflow.otel import OtelHooks # noqa: E402
|
|
28
30
|
from opentelemetry.sdk.trace import TracerProvider # noqa: E402
|