stateous 0.2.0__tar.gz → 0.2.2__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.
- {stateous-0.2.0 → stateous-0.2.2}/.gitignore +1 -0
- {stateous-0.2.0 → stateous-0.2.2}/PKG-INFO +19 -30
- stateous-0.2.2/README.md +46 -0
- stateous-0.2.2/docs/concepts/agents.md +153 -0
- stateous-0.2.2/docs/concepts/context-notes-memory.md +113 -0
- stateous-0.2.2/docs/concepts/guards.md +97 -0
- stateous-0.2.2/docs/concepts/model.md +253 -0
- stateous-0.2.2/docs/concepts/parallel.md +134 -0
- stateous-0.2.2/docs/concepts/pause-resume-cancel.md +142 -0
- stateous-0.2.2/docs/concepts/reliability.md +140 -0
- stateous-0.2.2/docs/concepts/tasks-and-routines.md +136 -0
- stateous-0.2.2/docs/concepts/timers.md +99 -0
- stateous-0.2.2/docs/concepts/two-sides.md +120 -0
- stateous-0.2.2/docs/getting-started.md +244 -0
- stateous-0.2.2/docs/how-to/change-a-workflow.md +87 -0
- stateous-0.2.2/docs/how-to/run-supervisors.md +154 -0
- stateous-0.2.2/docs/how-to/steer-tokens.md +162 -0
- stateous-0.2.2/docs/how-to/use-postgres.md +73 -0
- stateous-0.2.2/docs/how-to/write-a-routine.md +188 -0
- stateous-0.2.2/docs/how-to/write-a-workflow.md +153 -0
- stateous-0.2.2/docs/index.md +44 -0
- stateous-0.2.2/docs/inside/data.md +219 -0
- stateous-0.2.2/docs/inside/event-graph.md +73 -0
- stateous-0.2.2/docs/inside/workflows.md +537 -0
- stateous-0.2.2/docs/reference/cli.md +292 -0
- stateous-0.2.2/docs/reference/files.md +177 -0
- stateous-0.2.2/docs/reference/http.md +249 -0
- stateous-0.2.2/docs/reference/limits.md +40 -0
- stateous-0.2.2/docs/reference/python.md +46 -0
- {stateous-0.2.0 → stateous-0.2.2}/examples/sdlc-supervisor/coding.py +1 -1
- stateous-0.2.2/mkdocs.yml +114 -0
- {stateous-0.2.0 → stateous-0.2.2}/pyproject.toml +15 -4
- {stateous-0.2.0 → stateous-0.2.2}/scripts/check.py +10 -4
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/main.py +6 -3
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/project_file.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/serving.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/starter.py +4 -3
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/tokens.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/client/client.py +145 -18
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/compiler/codegen.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/compiler/compile.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/compiler/exclusive.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/compiler/load.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/compiler/plot.py +3 -4
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/compiler/validate.py +3 -3
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/api.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/engine.py +8 -8
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/store.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/context.py +61 -16
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/contract.py +58 -17
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/events.py +14 -2
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/guards.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/workflow.py +92 -58
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/supervisor/outbox.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/supervisor/remote.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/supervisor/supervisor.py +1 -1
- stateous-0.2.2/tests/docs/pages.py +122 -0
- stateous-0.2.2/tests/docs/test_concepts.py +225 -0
- stateous-0.2.2/tests/docs/test_how_to.py +87 -0
- stateous-0.2.2/tests/docs/test_inside.py +76 -0
- stateous-0.2.2/tests/docs/test_numbers.py +140 -0
- stateous-0.2.2/tests/docs/test_reference.py +212 -0
- stateous-0.2.2/tests/docs/test_site.py +258 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/e2e/test_example.py +62 -30
- {stateous-0.2.0 → stateous-0.2.2}/tests/examples/test_sdlc.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_api_parallel.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_api_refusals.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_api_tokens.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_client.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_parallel_flow.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_prepare_serve.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/tests/parallel.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_closed_output.py +28 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_compile_command.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_init_workflow.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_project_file.py +2 -2
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/compiler/test_exclusive.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_clock.py +16 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_parallel_steering.py +1 -1
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_context.py +1 -1
- stateous-0.2.0/README.md +0 -57
- stateous-0.2.0/docs/guide.md +0 -827
- stateous-0.2.0/tests/docs/test_guide.py +0 -339
- {stateous-0.2.0 → stateous-0.2.2}/LICENSE +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/NOTICE +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/examples/sdlc/build/sdlc.json +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/examples/sdlc/flow.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/examples/sdlc/schemas.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/examples/sdlc-supervisor/contract.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/examples/sdlc-supervisor/stateous.yaml +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/examples/sdlc-supervisor/testing.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/__main__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/errors.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/cli/running.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/client/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/compiler/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/graph.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/errors.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/migrations/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/migrations/env.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/migrations/script.py.mako +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/migrations/versions/0001_baseline.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/migrations/versions/0002_claims.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/migrations/versions/0003_children.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/control/store/tables.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/durations.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/graph_file.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/host.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/model/wire.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/py.typed +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/supervisor/__init__.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/supervisor/folder.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/src/stateous/supervisor/registry.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/conftest.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/examples/fake_phd.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/examples/test_coding.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/conftest.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/supervising.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_api_tasks.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_project_uv_sync.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_serve_command.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_supervise_cli.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_supervise_process.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_supervisor_flow.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/integration/test_token_commands.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/postgres/test_concurrency.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/postgres/test_connection.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/postgres/test_migrations.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/postgres/test_serving.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/postgres/test_time_limits.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_clock.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_configure_logging.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_errors.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_graceful_server.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_listen.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_show_children.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_startup.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_supervise_helpers.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_supervised_server.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/cli/test_token_lines.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/compiler/test_codegen.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/compiler/test_compile.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/compiler/test_load.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/compiler/test_plot.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/compiler/test_validate.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/support.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_case_race.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_claim_requests.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_deadlock.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_engine.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_graph.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_inputs.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_migrations.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_parallel.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_pause.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_serving.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_store.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/control/test_tasks.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_contract.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_durations.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_graph_file.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_guards.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_host.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_wire.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/model/test_workflow.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/fakes.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_folder.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_open.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_outbox.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_registry.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_remote.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_supervisor.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_supervisor_edges.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/supervisor/test_supervisor_outbox.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/test_layout.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/test_public_names.py +0 -0
- {stateous-0.2.0 → stateous-0.2.2}/tests/unit/test_test_databases.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: stateous
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A process as a graph of states. A control plane keeps every token durable.
|
|
5
5
|
Project-URL: Homepage, https://github.com/StateousLabs/stateous
|
|
6
6
|
Project-URL: Source, https://github.com/StateousLabs/stateous
|
|
7
7
|
Project-URL: Issues, https://github.com/StateousLabs/stateous/issues
|
|
8
|
-
Project-URL: Documentation, https://github.
|
|
8
|
+
Project-URL: Documentation, https://stateouslabs.github.io/stateous/
|
|
9
9
|
Author: The Stateous Authors
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
@@ -38,6 +38,12 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
# Stateous
|
|
40
40
|
|
|
41
|
+
[](https://github.com/StateousLabs/stateous/actions/workflows/ci.yml)
|
|
42
|
+
[](https://pypi.org/project/stateous/)
|
|
43
|
+
[](https://pypi.org/project/stateous/)
|
|
44
|
+
[](https://stateouslabs.github.io/stateous/)
|
|
45
|
+
[](https://github.com/StateousLabs/stateous/blob/main/LICENSE)
|
|
46
|
+
|
|
41
47
|
Stateous runs a process as a graph of states, and keeps every token of it durable.
|
|
42
48
|
|
|
43
49
|
You write the graph in Python. A control plane keeps every token and chooses each next state. A supervisor, on any computer, does the work and reports what happened. The supervisor never sees the graph. It knows only which states it serves, and which outcomes each state allows.
|
|
@@ -53,42 +59,25 @@ pip install stateous # or: uv add stateous
|
|
|
53
59
|
pip install "stateous[phd]" # also PHD, to run agents in routines
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
## The example
|
|
62
|
+
The control plane keeps its data in SQLite or in Postgres.
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
## A short example
|
|
61
65
|
|
|
62
66
|
```bash
|
|
63
|
-
#
|
|
64
|
-
stateous
|
|
65
|
-
stateous
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
stateous init supervisor http://cp.internal:8080 sdlc-supervisor # contract.py, stubs, stateous.yaml, pyproject.toml
|
|
69
|
-
stateous supervise sdlc-supervisor # runs the routines
|
|
70
|
-
|
|
71
|
-
# anywhere
|
|
72
|
-
stateous token start TICKET-1 '{"repo": "acme/api"}' --url http://cp.internal:8080
|
|
73
|
-
stateous token show TICKET-1 --url http://cp.internal:8080
|
|
67
|
+
stateous init workflow orders # a starter workflow that compiles
|
|
68
|
+
stateous serve orders/build/orders.json # the control plane
|
|
69
|
+
stateous init supervisor orders/flow.py orders-supervisor # contract.py and a stub for each state
|
|
70
|
+
stateous supervise orders-supervisor # runs your routines
|
|
71
|
+
stateous token start ORDER-1 '{"title": "first order"}' # starts a token
|
|
74
72
|
```
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
To start a workflow of your own, let Stateous write a starter that compiles:
|
|
74
|
+
Write the routines in `orders-supervisor/` before you run `supervise`. [`examples/sdlc/`](https://github.com/StateousLabs/stateous/tree/main/examples/sdlc) and [`examples/sdlc-supervisor/`](https://github.com/StateousLabs/stateous/tree/main/examples/sdlc-supervisor) are a larger example.
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
stateous init workflow orders # orders/flow.py, schemas.py, pyproject.toml, build/orders.json
|
|
82
|
-
stateous serve orders/build/orders.json
|
|
83
|
-
stateous init supervisor orders/flow.py orders-supervisor # from a local flow.py or graph file, too
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
To try the example on one computer, follow [the guide](https://github.com/StateousLabs/stateous/blob/main/docs/guide.md#try-it-on-one-computer).
|
|
76
|
+
Stateous has no authentication. Keep a control plane that listens on a wider address behind a VPN or a proxy.
|
|
87
77
|
|
|
88
|
-
##
|
|
78
|
+
## Documentation
|
|
89
79
|
|
|
90
|
-
|
|
91
|
-
- [`examples/`](https://github.com/StateousLabs/stateous/tree/main/examples): the example.
|
|
80
|
+
The documentation is at [stateouslabs.github.io/stateous](https://stateouslabs.github.io/stateous/). It explains the model, then tells you how to do the work, then gives the reference. To run the short example step by step, follow [Getting started](https://stateouslabs.github.io/stateous/getting-started/).
|
|
92
81
|
|
|
93
82
|
## Licence
|
|
94
83
|
|
stateous-0.2.2/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Stateous
|
|
2
|
+
|
|
3
|
+
[](https://github.com/StateousLabs/stateous/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/stateous/)
|
|
5
|
+
[](https://pypi.org/project/stateous/)
|
|
6
|
+
[](https://stateouslabs.github.io/stateous/)
|
|
7
|
+
[](https://github.com/StateousLabs/stateous/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
Stateous runs a process as a graph of states, and keeps every token of it durable.
|
|
10
|
+
|
|
11
|
+
You write the graph in Python. A control plane keeps every token and chooses each next state. A supervisor, on any computer, does the work and reports what happened. The supervisor never sees the graph. It knows only which states it serves, and which outcomes each state allows.
|
|
12
|
+
|
|
13
|
+
> **Status:** alpha. The API can change before version 1.0.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Stateous needs Python 3.12 or newer.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install stateous # or: uv add stateous
|
|
21
|
+
pip install "stateous[phd]" # also PHD, to run agents in routines
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The control plane keeps its data in SQLite or in Postgres.
|
|
25
|
+
|
|
26
|
+
## A short example
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
stateous init workflow orders # a starter workflow that compiles
|
|
30
|
+
stateous serve orders/build/orders.json # the control plane
|
|
31
|
+
stateous init supervisor orders/flow.py orders-supervisor # contract.py and a stub for each state
|
|
32
|
+
stateous supervise orders-supervisor # runs your routines
|
|
33
|
+
stateous token start ORDER-1 '{"title": "first order"}' # starts a token
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Write the routines in `orders-supervisor/` before you run `supervise`. [`examples/sdlc/`](https://github.com/StateousLabs/stateous/tree/main/examples/sdlc) and [`examples/sdlc-supervisor/`](https://github.com/StateousLabs/stateous/tree/main/examples/sdlc-supervisor) are a larger example.
|
|
37
|
+
|
|
38
|
+
Stateous has no authentication. Keep a control plane that listens on a wider address behind a VPN or a proxy.
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
The documentation is at [stateouslabs.github.io/stateous](https://stateouslabs.github.io/stateous/). It explains the model, then tells you how to do the work, then gives the reference. To run the short example step by step, follow [Getting started](https://stateouslabs.github.io/stateous/getting-started/).
|
|
43
|
+
|
|
44
|
+
## Licence
|
|
45
|
+
|
|
46
|
+
Apache License 2.0. See [`LICENSE`](https://github.com/StateousLabs/stateous/blob/main/LICENSE) and [`NOTICE`](https://github.com/StateousLabs/stateous/blob/main/NOTICE).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Agents, with PHD
|
|
2
|
+
|
|
3
|
+
## In one sentence
|
|
4
|
+
|
|
5
|
+
A routine runs a coding agent with the library PHD, and Stateous keeps the agent's task alive, its progress visible and its session safe.
|
|
6
|
+
|
|
7
|
+
## Why it exists
|
|
8
|
+
|
|
9
|
+
A coding agent can work for an hour, hang, or crash with its computer. Its answer must be one of the outcomes that the state allows. When it fails, the next try should continue its session, not start again.
|
|
10
|
+
|
|
11
|
+
Stateous and PHD share this work. PHD runs the agent and checks its answer. Stateous keeps the task, the retries and the session id. PHD knows nothing of workflows, and it does not report to Stateous.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
| Need | Who does it |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Run the agent | PHD, called by an `async def` routine. |
|
|
18
|
+
| Keep the task's lease alive while the agent runs | The supervisor. It sends heartbeats for each task whose routine still runs. |
|
|
19
|
+
| Stop the agent when the token leaves the state, or is cancelled | The supervisor cancels the routine. PHD's `async with … run(...)` block stops the run when it ends. |
|
|
20
|
+
| Choose the outcome | The agent. PHD's `output_schema` is `states.X.outcome_schema()`, so the agent's answer names one allowed event and its payload. |
|
|
21
|
+
| Check the outcome | PHD checks the answer against the schema. A mismatch is a failed run, so a failed attempt. `states.X.outcome(...)` makes the event, and the control plane checks it again. |
|
|
22
|
+
| See that an agent is stuck | PHD. `Spec.idle_timeout_s` stops a run that sends no event for that long. |
|
|
23
|
+
| Show progress | The routine gives PHD's events to `ctx.note(...)`. |
|
|
24
|
+
| Keep the session for the next attempt | The routine, with `ctx.memory`. |
|
|
25
|
+
| Retry and time out | Stateous, as for each routine. |
|
|
26
|
+
|
|
27
|
+
```mermaid
|
|
28
|
+
sequenceDiagram
|
|
29
|
+
participant S as Supervisor
|
|
30
|
+
participant R as Routine
|
|
31
|
+
participant P as PHD
|
|
32
|
+
participant H as Harness (the agent)
|
|
33
|
+
participant CP as Control plane
|
|
34
|
+
S->>R: code(ctx)
|
|
35
|
+
R->>P: Dispatcher(spec).run(inputs, session_id)
|
|
36
|
+
P->>H: start or resume the session
|
|
37
|
+
loop while the agent works
|
|
38
|
+
H-->>P: an event
|
|
39
|
+
P-->>R: event.summary, handle.session_id
|
|
40
|
+
R->>CP: ctx.memory["dev"] = session id (once)
|
|
41
|
+
R->>CP: ctx.note(event.summary)
|
|
42
|
+
end
|
|
43
|
+
H-->>P: the final answer
|
|
44
|
+
P-->>R: result.structured_output, checked
|
|
45
|
+
R-->>S: states.coding.outcome(...): an event
|
|
46
|
+
S->>CP: report the event
|
|
47
|
+
Note over S,H: Cancel: the token leaves the state. The next heartbeat says "lost".<br/>The supervisor cancels the routine, and PHD stops the run.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### The contract's helpers for agents
|
|
51
|
+
|
|
52
|
+
The contract gives each state two helpers:
|
|
53
|
+
|
|
54
|
+
- `states.X.outcome_schema()`: the events that state X can emit, with their payloads, as one JSON schema;
|
|
55
|
+
- `states.X.outcome(data)`: the event that `data` names, checked against the schema. It raises `OutcomeError` if `data` does not fit.
|
|
56
|
+
|
|
57
|
+
### Install PHD
|
|
58
|
+
|
|
59
|
+
PHD is on PyPI as `poly-harness-dispatcher`. Its import name is `phd`. Install it in the supervisor's environment with the `phd` extra of Stateous:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install "stateous[phd]" # or: uv add "stateous[phd]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
From a copy of the Stateous repository, use `uv add --editable path/to/stateous --extra phd`.
|
|
66
|
+
|
|
67
|
+
Without PHD, the supervisor cannot import a routine that imports `phd`, and it does not start. Then remove that routine, and report its state from outside, as [Try it on one computer](../how-to/run-supervisors.md#try-it-on-one-computer) shows for `coding`.
|
|
68
|
+
|
|
69
|
+
PHD needs a stack and credentials for each run. `phd.Stack.load("stacks/dev")` loads the agent's stack folder: its identity, skills and tools. The path is relative to the folder where the supervisor runs. `phd.Credentials.from_host("claude")` uses the login of the supervisor's computer. The PHD documentation tells how to write a stack.
|
|
70
|
+
|
|
71
|
+
### What the routine uses from PHD
|
|
72
|
+
|
|
73
|
+
- `Dispatcher(spec).run(inputs, session_id=...)` is an `async with` block. If the block ends before the run ends, PHD stops the run.
|
|
74
|
+
- `handle.session_id` is the harness's session id. It is `None` until the harness reports it.
|
|
75
|
+
- `Spec.idle_timeout_s` stops a run that sends no event for that long. The run's status is then `timed_out`, with the error kind `idle`.
|
|
76
|
+
- `Spec.timeout_s` is the longest time for one run. Its default is 1800 seconds. Set it when an agent must work longer.
|
|
77
|
+
- `event.summary` is one line for a `message`, `tool_call` or `tool_result` event. For other events, it is `""`.
|
|
78
|
+
- `result.error.kind` is one of `phd.ERROR_KINDS`. Use the kind, not the message.
|
|
79
|
+
- `Spec.cwd` must be a folder that exists. If it does not exist, PHD refuses the spec (`CwdNotFound`).
|
|
80
|
+
|
|
81
|
+
### Sessions, crashes and retries
|
|
82
|
+
|
|
83
|
+
The routine keeps the session id in `ctx.memory`, as soon as the harness reports it. The memory is saved at once, so a crash keeps it. The next attempt gives the session id to `run(...)`, and the agent continues its session.
|
|
84
|
+
|
|
85
|
+
With the error kind `"session"`, PHD cannot resume the session. The routine then deletes the session id from `ctx.memory`. So the retry starts a new session. With another kind, the retry resumes the same session.
|
|
86
|
+
|
|
87
|
+
The running agents of a supervisor are its child processes. PHD ties each agent to its caller, so the agents stop when the supervisor crashes. On Windows, a Job Object stops them. On Linux and macOS, a small reaper process stops the agent's process group. If PHD cannot start this protection, the run continues without it, and `result.warnings` says so. Then the agent can continue after a crash of the supervisor.
|
|
88
|
+
|
|
89
|
+
### Free-form payloads
|
|
90
|
+
|
|
91
|
+
Codex uses OpenAI's strict structured outputs. These cannot return an object that has no listed properties. So a payload model with a `dict[str, Any]` field cannot run on Codex. PHD refuses such a run before it starts (`UnsupportedFeature`). Claude and OpenCode accept it.
|
|
92
|
+
|
|
93
|
+
## Example
|
|
94
|
+
|
|
95
|
+
This is the routine of `coding` in the `sdlc` example:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# sdlc-supervisor/coding.py: an agent, run by PHD (see the docs, Concepts > Agents, with PHD)
|
|
99
|
+
import phd
|
|
100
|
+
|
|
101
|
+
from stateous import on_state
|
|
102
|
+
|
|
103
|
+
from contract import TicketContext, events, states
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@on_state(states.coding)
|
|
107
|
+
async def code(ctx: TicketContext) -> events.done | events.stuck:
|
|
108
|
+
spec = phd.Spec(
|
|
109
|
+
harness="claude",
|
|
110
|
+
model="sonnet",
|
|
111
|
+
stack=phd.Stack.load("stacks/dev"), # the agent: a PHD stack folder
|
|
112
|
+
cwd=ctx["repo"],
|
|
113
|
+
credentials=phd.Credentials.from_host("claude"), # the login of this computer
|
|
114
|
+
output_schema=states.coding.outcome_schema(),
|
|
115
|
+
idle_timeout_s=900, # no event for 15 minutes: stop
|
|
116
|
+
)
|
|
117
|
+
inputs = phd.Inputs(msg=f"Implement {ctx['repo']}")
|
|
118
|
+
# Leaving the block stops an unfinished run:
|
|
119
|
+
# an error, or the token left the state.
|
|
120
|
+
async with phd.Dispatcher(spec).run(inputs, session_id=ctx.memory.get("dev")) as handle:
|
|
121
|
+
async for ev in handle.events():
|
|
122
|
+
if handle.session_id and "dev" not in ctx.memory:
|
|
123
|
+
# known when the harness reports it; saved at once, so a crash keeps it
|
|
124
|
+
ctx.memory["dev"] = handle.session_id
|
|
125
|
+
ctx.note(ev.summary) # progress in the token's history; "" gives no note
|
|
126
|
+
result = await handle.wait()
|
|
127
|
+
if result.status != "succeeded":
|
|
128
|
+
if result.error and result.error.kind == "session" and "dev" in ctx.memory:
|
|
129
|
+
del ctx.memory["dev"] # the session cannot be resumed: the retry starts fresh
|
|
130
|
+
raise RuntimeError(result.error) # a failed attempt
|
|
131
|
+
return states.coding.outcome(result.structured_output)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- The agent's answer must name `done` or `stuck`, because `output_schema` is `states.coding.outcome_schema()`.
|
|
135
|
+
- A failed run raises an exception, so the attempt fails. `coding` has `retries=2`, so the agent gets two more attempts.
|
|
136
|
+
- The state's `timeout="6h"` covers all attempts.
|
|
137
|
+
|
|
138
|
+
## Guarantees
|
|
139
|
+
|
|
140
|
+
- Only an outcome that the state can emit is accepted. PHD checks it, and the control plane checks it again.
|
|
141
|
+
- The task's lease stays alive while the routine runs, for as long as the agent works.
|
|
142
|
+
- When the token leaves the state, or is cancelled, the supervisor cancels the routine at the next heartbeat, and PHD stops the run.
|
|
143
|
+
- The session id in `ctx.memory` stays after a crash, so the next attempt can resume the session.
|
|
144
|
+
- Not guaranteed: that an agent's steps are safe to repeat. After a crash, the agent can repeat its last step or two. Its instructions must make that safe.
|
|
145
|
+
- Not guaranteed: free-form payloads on Codex. A `dict[str, Any]` field fails before the run starts.
|
|
146
|
+
|
|
147
|
+
## See also
|
|
148
|
+
|
|
149
|
+
- [Context, notes and memory](context-notes-memory.md): `ctx.memory` and `ctx.note`.
|
|
150
|
+
- [Tasks and routines](tasks-and-routines.md): `async def` routines, cancel, and retries.
|
|
151
|
+
- [Reliability](reliability.md): what happens when the supervisor stops.
|
|
152
|
+
- [Write a routine](../how-to/write-a-routine.md): `outcome_schema()` and `outcome()`.
|
|
153
|
+
- [Limits](../reference/limits.md): the known limits, also on Codex.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Context, notes and memory
|
|
2
|
+
|
|
3
|
+
## In one sentence
|
|
4
|
+
|
|
5
|
+
A routine gets `ctx`, which holds the token's context, the routine's own memory, and a way to write notes to the token's history.
|
|
6
|
+
|
|
7
|
+
## Why it exists
|
|
8
|
+
|
|
9
|
+
A routine needs three kinds of data, and each one has different rules:
|
|
10
|
+
|
|
11
|
+
- **The context** is the business data of the token, for example the path of a checkout. The guards read it, so a change to it must agree with the outcome.
|
|
12
|
+
- **A note** tells a person what the routine does now, for example each step of an agent.
|
|
13
|
+
- **The memory** is bookkeeping for the routine, for example an agent's session id. It must stay after a crash, because the next attempt needs it.
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
These are the fields of `ctx`:
|
|
18
|
+
|
|
19
|
+
| Field | What it gives |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `ctx["key"]` | A value of the token's context. It raises `KeyError` for a key that is not set. |
|
|
22
|
+
| `ctx["key"] = value` | A change to the token's context. It is saved with the outcome, or not at all. |
|
|
23
|
+
| `ctx.get("key", default)` | The same as `ctx["key"]`, with a default value for a key that is not set. |
|
|
24
|
+
| `ctx.memory` | The routine's own memory, for this token and this state. Each change is saved at once. |
|
|
25
|
+
| `ctx.note(text)` | One line of progress in the token's history. It is sent at once. |
|
|
26
|
+
| `ctx.token` | The token's id. |
|
|
27
|
+
| `ctx.task` | The task's id. |
|
|
28
|
+
| `ctx.visit` | 1 the first time the token is in this state, 2 the next time, and so on. |
|
|
29
|
+
| `ctx.attempt` | 1, then 2 after a retry, and so on. |
|
|
30
|
+
| `ctx.entered_by` | The event that brought the token here: `.name` and `.payload`. In the first state, `.name` is `__started__`. |
|
|
31
|
+
| `ctx.schedule(event, after="10m")` | A timer. See [Timers and timeouts](timers.md). |
|
|
32
|
+
|
|
33
|
+
### The context
|
|
34
|
+
|
|
35
|
+
A value that you read from `ctx` is a copy. So `ctx["items"].append(2)` changes nothing. Write the new value with `ctx["items"] = ...`. Values must be JSON.
|
|
36
|
+
|
|
37
|
+
The supervisor sends the changes with the report. The control plane checks them against the context model. Changes that do not agree with the model fail the task. A failed attempt saves no change to the context.
|
|
38
|
+
|
|
39
|
+
### Notes
|
|
40
|
+
|
|
41
|
+
`ctx.note(text)` adds one line to the token's history. A note has up to 500 characters. A longer note raises `ValueError`. Empty text does nothing, so you can give each event of an agent to `ctx.note`.
|
|
42
|
+
|
|
43
|
+
A note is sent at once, through the supervisor's outbox. It does not wait for the outcome.
|
|
44
|
+
|
|
45
|
+
### The memory
|
|
46
|
+
|
|
47
|
+
`ctx.memory` is a small key-value store. Stateous keeps one for each token **and** state. Use it for bookkeeping, for example an agent's session id. Do not use it for the workflow's data.
|
|
48
|
+
|
|
49
|
+
| Property | Rule |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Scope | One token and one state. `TICKET-1` in `coding` and `TICKET-1` in `testing` have different memories. |
|
|
52
|
+
| Saving | At once, with `PUT /tasks/{id}/memory`, not with the outcome. So it stays after a crash. |
|
|
53
|
+
| Keys and values | A key has 1 to 128 characters. A value is JSON. |
|
|
54
|
+
| Delete a key | `del ctx.memory["dev"]`. A value of `None` raises `ValueError`. |
|
|
55
|
+
| Retries | The next attempt sees it, so it can resume a crashed agent session. |
|
|
56
|
+
| Later visits | The token sees it again when it comes back to the state. |
|
|
57
|
+
| Several agents in one state | The routine chooses the keys, for example `memory["dev"]` and `memory["review"]`. |
|
|
58
|
+
| A new session for each visit | Put the visit in the key: `memory[f"dev:{ctx.visit}"]`. |
|
|
59
|
+
| End | It is deleted when the token ends: completed, failed or cancelled. |
|
|
60
|
+
| History | The history records the key of each change, never the value: `memory set: dev (in coding)`. |
|
|
61
|
+
| Guards | They cannot read it. |
|
|
62
|
+
|
|
63
|
+
The context and the memory follow different rules:
|
|
64
|
+
|
|
65
|
+
| | The context | The memory |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Saved | With the outcome, or not at all | At once |
|
|
68
|
+
| After a failed attempt | The changes are lost | The changes stay |
|
|
69
|
+
| Seen by | The whole token: each state, and the guards | One state of one token |
|
|
70
|
+
| Checked against a model | Yes | No: any JSON value |
|
|
71
|
+
|
|
72
|
+
## Example
|
|
73
|
+
|
|
74
|
+
The routine of `coding` in `sdlc` runs an agent. It keeps the agent's session id in `ctx.memory`, and it writes each event of the agent as a note:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
async with phd.Dispatcher(spec).run(inputs, session_id=ctx.memory.get("dev")) as handle:
|
|
78
|
+
async for ev in handle.events():
|
|
79
|
+
if handle.session_id and "dev" not in ctx.memory:
|
|
80
|
+
# known when the harness reports it; saved at once, so a crash keeps it
|
|
81
|
+
ctx.memory["dev"] = handle.session_id
|
|
82
|
+
ctx.note(ev.summary) # progress in the token's history; "" gives no note
|
|
83
|
+
result = await handle.wait()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If the supervisor stops, the next attempt reads `ctx.memory.get("dev")`, and the agent continues its session. The whole routine is on [Agents, with PHD](agents.md).
|
|
87
|
+
|
|
88
|
+
A routine can also write the context. Here, the routine of `testing` keeps the last result for a later state:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
@on_state(states.testing)
|
|
92
|
+
def test(ctx: TicketContext) -> events.passed | events.failed:
|
|
93
|
+
result = run_tests(ctx["repo"])
|
|
94
|
+
ctx["last_failed"] = result.failed # saved with the event
|
|
95
|
+
return events.passed(result) if result.failed == 0 else events.failed(result)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For this, `Ticket` must have a field `last_failed`. Else the control plane refuses the change, and the task fails.
|
|
99
|
+
|
|
100
|
+
## Guarantees
|
|
101
|
+
|
|
102
|
+
- Changes to the context are saved with the outcome in one transaction, or not at all.
|
|
103
|
+
- Notes and memory writes are sent in order, before the report of the same task.
|
|
104
|
+
- A memory write is on disk in the supervisor's outbox before it is sent. A crash does not lose it, if the supervisor starts again before the task's lease ends.
|
|
105
|
+
- A note or a memory write for an attempt that is over is refused. An old attempt cannot write to a new one.
|
|
106
|
+
- Not guaranteed: that the memory stays after the token ends. It is deleted when the token ends.
|
|
107
|
+
|
|
108
|
+
## See also
|
|
109
|
+
|
|
110
|
+
- [The model](model.md): the words [context](model.md#context), [note](model.md#note) and [memory](model.md#memory).
|
|
111
|
+
- [Agents, with PHD](agents.md): the memory keeps an agent's session.
|
|
112
|
+
- [Reliability](reliability.md): the supervisor's outbox.
|
|
113
|
+
- [Write a routine](../how-to/write-a-routine.md): the routine that reads and writes `ctx`.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Guards
|
|
2
|
+
|
|
3
|
+
## In one sentence
|
|
4
|
+
|
|
5
|
+
A guard is a small condition in `when=` that chooses which transition an event takes, from the data of the token and the event.
|
|
6
|
+
|
|
7
|
+
## Why it exists
|
|
8
|
+
|
|
9
|
+
One outcome can lead to different places. For example, a failed test run goes back to `coding` two times, but the third failure ends the token. The routine must not know this rule, because it does not see the graph.
|
|
10
|
+
|
|
11
|
+
So the graph holds the rule, as a guard on the transition. Stateous checks the guards when you compile, so a token never has two ways to go.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
A guard is an expression in a small language. It is not Python, and nothing runs it as Python. It can use these items:
|
|
16
|
+
|
|
17
|
+
| Item | What it reads |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `ctx['key']` | A field of the token's context, with the changes of the routine. |
|
|
20
|
+
| `payload.field` | A field of the payload of the event. |
|
|
21
|
+
| `count('event')` | How many times the token accepted this event, this one included. |
|
|
22
|
+
| `<`, `<=`, `>`, `>=`, `==`, `!=`, `in`, `not in` | Comparisons. |
|
|
23
|
+
| `and`, `or`, `not` | Logic. |
|
|
24
|
+
| `1`, `-2.5`, `'text'`, `true`, `false`, `null`, `[1, 2]` | Numbers, strings, the three names, and lists of these. |
|
|
25
|
+
|
|
26
|
+
Some rules of the language:
|
|
27
|
+
|
|
28
|
+
- A guard reads the context as `ctx['key']`, never as `ctx.key`. It cannot read `ctx.memory`.
|
|
29
|
+
- On the right of `in` and `not in`, use a list, or a value from `ctx`, `payload` or `count`. A string is not permitted there.
|
|
30
|
+
- A value that is not set reads as `null`. A comparison of `null` with a number is false, not an error.
|
|
31
|
+
- `count('event')` counts the event over the whole life of the token, not for one visit.
|
|
32
|
+
- A guard on a transition from `START` reads only the context. A new token has no event yet, so it has no payload and no count.
|
|
33
|
+
- A guard on `joined` can also use `branches.all(...)` and `branches.any(...)`. See [Parallel and nested work](parallel.md).
|
|
34
|
+
|
|
35
|
+
When an event comes, the control plane looks at the transitions from the token's state on that event:
|
|
36
|
+
|
|
37
|
+
1. If one guard matches, the token takes that transition.
|
|
38
|
+
2. If no guard matches, the token takes the transition with `otherwise=True`.
|
|
39
|
+
3. If no guard matches and there is no `otherwise=True`, the control plane refuses the event. The token stays where it is.
|
|
40
|
+
|
|
41
|
+
### The overlap check
|
|
42
|
+
|
|
43
|
+
`stateous compile` makes sure that the guards on one state and one event never overlap. Two guards overlap when some data can make both of them true.
|
|
44
|
+
|
|
45
|
+
If two guards can overlap, compile stops with an error, and it gives an example of such data. Stateous never chooses between two matching guards at run time.
|
|
46
|
+
|
|
47
|
+
Compile also stops when it cannot prove that two guards never overlap. This occurs when a guard compares two values with each other, orders a list, or uses `branches`. Then make one transition the fallback with `otherwise=True`.
|
|
48
|
+
|
|
49
|
+
Next to transitions with guards, a transition without a guard must have `otherwise=True`. Compile stops if it does not.
|
|
50
|
+
|
|
51
|
+
## Example
|
|
52
|
+
|
|
53
|
+
In `sdlc`, the event `failed` has two transitions from `testing`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
flow.transition(testing, coding, on=failed, when="count('failed') < 3")
|
|
57
|
+
flow.transition(testing, END, on=failed, otherwise=True)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The first and second `failed` have a count of 1 and 2, so they go back to `coding`. The third has a count of 3, so the token ends.
|
|
61
|
+
|
|
62
|
+
A guard can also read the payload and the context. The `orders` starter ends a token when the review approves it:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
flow.transition(review, END, on=reviewed, when="payload.approved == true")
|
|
66
|
+
flow.transition(review, draft, on=reviewed, otherwise=True)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Now give the context of `orders` an `int` field `amount`. These two guards overlap:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
flow.transition(review, END, on=reviewed, when="ctx['amount'] > 100")
|
|
73
|
+
flow.transition(review, draft, on=reviewed, when="ctx['amount'] > 50")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`stateous compile` refuses them, and it gives an amount that makes both of them true:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
error: workflow 'orders' is not valid:
|
|
80
|
+
- 'reviewed' from 'review': the guards "ctx['amount'] > 100" and "ctx['amount'] > 50" can both be true, e.g. when ctx['amount'] = 101; make them exclusive, or make one the fallback (otherwise=True)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Change the second guard to `ctx['amount'] <= 100`, or make it the fallback.
|
|
84
|
+
|
|
85
|
+
## Guarantees
|
|
86
|
+
|
|
87
|
+
- An event takes at most one transition. Compile proves that the guards on one state and event never overlap.
|
|
88
|
+
- A guard has no side effects. It reads data, and it changes nothing.
|
|
89
|
+
- A guard reads only fields that its models have. Compile checks each `ctx['key']` against the context model, and each `payload.field` against the payload model.
|
|
90
|
+
- `count('event')` must name an event of the workflow, or `timeout`, `error` or `joined`.
|
|
91
|
+
- Not guaranteed: that an event finds a transition. With no `otherwise=True`, an event that no guard matches is refused.
|
|
92
|
+
|
|
93
|
+
## See also
|
|
94
|
+
|
|
95
|
+
- [The model](model.md): the words [transition](model.md#transition) and [guard](model.md#guard).
|
|
96
|
+
- [Timers and timeouts](timers.md): guards on the events `timeout` and `error`.
|
|
97
|
+
- [Write a workflow](../how-to/write-a-workflow.md): compile errors for guards that overlap.
|