stateous 0.2.2__tar.gz → 0.3.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.
- {stateous-0.2.2 → stateous-0.3.0}/.gitignore +2 -0
- {stateous-0.2.2 → stateous-0.3.0}/PKG-INFO +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/agents.md +2 -2
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/context-notes-memory.md +1 -1
- stateous-0.3.0/docs/concepts/data.md +115 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/guards.md +11 -3
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/model.md +6 -48
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/parallel.md +4 -2
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/pause-resume-cancel.md +4 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/reliability.md +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/tasks-and-routines.md +3 -3
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/two-sides.md +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/docs/getting-started.md +94 -25
- {stateous-0.2.2 → stateous-0.3.0}/docs/how-to/steer-tokens.md +8 -2
- {stateous-0.2.2 → stateous-0.3.0}/docs/how-to/write-a-routine.md +8 -2
- {stateous-0.2.2 → stateous-0.3.0}/docs/how-to/write-a-workflow.md +7 -3
- {stateous-0.2.2 → stateous-0.3.0}/docs/index.md +10 -7
- {stateous-0.2.2 → stateous-0.3.0}/docs/inside/data.md +16 -12
- stateous-0.3.0/docs/inside/words.md +39 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/inside/workflows.md +2 -2
- {stateous-0.2.2 → stateous-0.3.0}/docs/reference/cli.md +4 -2
- {stateous-0.2.2 → stateous-0.3.0}/docs/reference/files.md +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/docs/reference/http.md +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/docs/reference/python.md +6 -0
- {stateous-0.2.2 → stateous-0.3.0}/mkdocs.yml +2 -0
- {stateous-0.2.2 → stateous-0.3.0}/pyproject.toml +19 -1
- {stateous-0.2.2 → stateous-0.3.0}/scripts/check.py +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/__init__.py +3 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/starter.py +21 -24
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/tokens.py +23 -3
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/compiler/codegen.py +2 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/compiler/compile.py +13 -4
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/compiler/exclusive.py +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/compiler/validate.py +20 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/api.py +3 -2
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/engine.py +34 -13
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/graph.py +28 -5
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/store.py +5 -15
- stateous-0.3.0/src/stateous/model/branches.py +32 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/context.py +16 -3
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/graph_file.py +24 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/guards.py +29 -15
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/workflow.py +58 -6
- {stateous-0.2.2 → stateous-0.3.0}/tests/docs/test_concepts.py +47 -10
- {stateous-0.2.2 → stateous-0.3.0}/tests/docs/test_numbers.py +1 -1
- {stateous-0.2.2 → stateous-0.3.0}/tests/docs/test_site.py +2 -2
- {stateous-0.2.2 → stateous-0.3.0}/tests/e2e/test_example.py +14 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_api_parallel.py +3 -1
- stateous-0.3.0/tests/unit/cli/test_command_failures.py +191 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_init_workflow.py +4 -4
- stateous-0.3.0/tests/unit/cli/test_running_paths.py +259 -0
- stateous-0.3.0/tests/unit/cli/test_serving_refusals.py +71 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_show_children.py +1 -1
- stateous-0.3.0/tests/unit/cli/test_show_context.py +91 -0
- stateous-0.3.0/tests/unit/client/test_client_requests.py +90 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/compiler/test_codegen.py +63 -0
- stateous-0.3.0/tests/unit/compiler/test_exclusive_domains.py +112 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/compiler/test_load.py +8 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/compiler/test_validate.py +37 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/support.py +18 -1
- stateous-0.3.0/tests/unit/control/test_api_edges.py +70 -0
- stateous-0.3.0/tests/unit/control/test_engine_edges.py +255 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_graph.py +8 -0
- stateous-0.3.0/tests/unit/control/test_into_engine.py +187 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_parallel.py +6 -6
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_serving.py +2 -16
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_store.py +0 -16
- stateous-0.3.0/tests/unit/control/test_store_edges.py +74 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_tasks.py +2 -1
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_context.py +17 -3
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_contract.py +4 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_graph_file.py +14 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_guards.py +43 -8
- stateous-0.3.0/tests/unit/model/test_into.py +192 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/fakes.py +25 -1
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_folder.py +17 -0
- stateous-0.3.0/tests/unit/supervisor/test_outbox_errors.py +39 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_remote.py +1 -22
- stateous-0.3.0/tests/unit/supervisor/test_remote_answers.py +129 -0
- stateous-0.3.0/tests/unit/supervisor/test_supervisor_faults.py +542 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/test_public_names.py +2 -0
- {stateous-0.2.2 → stateous-0.3.0}/LICENSE +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/NOTICE +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/README.md +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/concepts/timers.md +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/how-to/change-a-workflow.md +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/how-to/run-supervisors.md +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/how-to/use-postgres.md +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/inside/event-graph.md +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/docs/reference/limits.md +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/examples/sdlc/build/sdlc.json +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/examples/sdlc/flow.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/examples/sdlc/schemas.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/examples/sdlc-supervisor/coding.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/examples/sdlc-supervisor/contract.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/examples/sdlc-supervisor/stateous.yaml +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/examples/sdlc-supervisor/testing.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/__main__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/errors.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/main.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/project_file.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/running.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/cli/serving.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/client/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/client/client.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/compiler/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/compiler/load.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/compiler/plot.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/errors.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/migrations/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/migrations/env.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/migrations/script.py.mako +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/migrations/versions/0001_baseline.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/migrations/versions/0002_claims.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/migrations/versions/0003_children.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/control/store/tables.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/contract.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/durations.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/events.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/host.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/model/wire.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/py.typed +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/supervisor/__init__.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/supervisor/folder.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/supervisor/outbox.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/supervisor/registry.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/supervisor/remote.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/src/stateous/supervisor/supervisor.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/conftest.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/docs/pages.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/docs/test_how_to.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/docs/test_inside.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/docs/test_reference.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/examples/fake_phd.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/examples/test_coding.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/examples/test_sdlc.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/conftest.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/supervising.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_api_refusals.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_api_tasks.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_api_tokens.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_client.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_parallel_flow.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_prepare_serve.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_project_uv_sync.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_serve_command.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_supervise_cli.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_supervise_process.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_supervisor_flow.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/integration/test_token_commands.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/parallel.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/postgres/test_concurrency.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/postgres/test_connection.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/postgres/test_migrations.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/postgres/test_serving.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/postgres/test_time_limits.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_clock.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_closed_output.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_compile_command.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_configure_logging.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_errors.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_graceful_server.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_listen.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_project_file.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_startup.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_supervise_helpers.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_supervised_server.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/cli/test_token_lines.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/compiler/test_compile.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/compiler/test_exclusive.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/compiler/test_plot.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_case_race.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_claim_requests.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_clock.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_deadlock.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_engine.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_inputs.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_migrations.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_parallel_steering.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/control/test_pause.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_durations.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_host.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_wire.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/model/test_workflow.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_open.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_outbox.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_registry.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_supervisor.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_supervisor_edges.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/supervisor/test_supervisor_outbox.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/test_layout.py +0 -0
- {stateous-0.2.2 → stateous-0.3.0}/tests/unit/test_test_databases.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: stateous
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -6,7 +6,7 @@ A routine runs a coding agent with the library PHD, and Stateous keeps the agent
|
|
|
6
6
|
|
|
7
7
|
## Why it exists
|
|
8
8
|
|
|
9
|
-
A coding agent can work for an hour, hang, or crash with its computer. Its answer must
|
|
9
|
+
A coding agent can work for an hour, hang, or crash with its computer. Its answer must name one of the events that the state allows. When it fails, the next try should continue its session, not start again.
|
|
10
10
|
|
|
11
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
12
|
|
|
@@ -137,7 +137,7 @@ async def code(ctx: TicketContext) -> events.done | events.stuck:
|
|
|
137
137
|
|
|
138
138
|
## Guarantees
|
|
139
139
|
|
|
140
|
-
- Only an
|
|
140
|
+
- Only an event that the state can emit is accepted. PHD checks it, and the control plane checks it again.
|
|
141
141
|
- The task's lease stays alive while the routine runs, for as long as the agent works.
|
|
142
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
143
|
- The session id in `ctx.memory` stays after a crash, so the next attempt can resume the session.
|
|
@@ -27,7 +27,7 @@ These are the fields of `ctx`:
|
|
|
27
27
|
| `ctx.task` | The task's id. |
|
|
28
28
|
| `ctx.visit` | 1 the first time the token is in this state, 2 the next time, and so on. |
|
|
29
29
|
| `ctx.attempt` | 1, then 2 after a retry, and so on. |
|
|
30
|
-
| `ctx.entered_by` | The event that brought the token here
|
|
30
|
+
| `ctx.entered_by` | The name of the event that brought the token here, as `ctx.entered_by.name`. In the first state, it is `__started__`. A routine does not get the payload: an event whose payload the token keeps declares `into=`. See [Where data goes](data.md). |
|
|
31
31
|
| `ctx.schedule(event, after="10m")` | A timer. See [Timers and timeouts](timers.md). |
|
|
32
32
|
|
|
33
33
|
### The context
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Where data goes
|
|
2
|
+
|
|
3
|
+
## In one sentence
|
|
4
|
+
|
|
5
|
+
A routine reads and writes only the context: an event says why a token moved, and `into=` keeps its payload in the context.
|
|
6
|
+
|
|
7
|
+
## Why it exists
|
|
8
|
+
|
|
9
|
+
A token carries data from state to state. Without one rule, data can go to many places, and you must choose each time.
|
|
10
|
+
|
|
11
|
+
Stateous has two rules, and the API follows them:
|
|
12
|
+
|
|
13
|
+
1. **Data.** A routine reads and writes only the context. An event's name and payload say *why* the token moved. Guards and the history read the payload. If the token must keep a payload, the event declares `into=`.
|
|
14
|
+
2. **Decisions.** Routines decide, and guards only route. A routine returns the event that names its decision. A guard only chooses a transition, from data that is already there.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
| Place | What it is for | Who reads it | When it is saved |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| The context, `ctx["key"]` | The data of the token: all that later states need. | Each routine of the token, and the guards. | With the event, or not at all. |
|
|
21
|
+
| An event's payload | Why the token moved, for example a `Verdict`. | The guards, and the history. Not the routines. | With the event, in the history. |
|
|
22
|
+
| `into="key"` on an event | Keeps the event's payload in the context, as `ctx["key"]`. | Each later routine, through the context. | With the event, before the guards run. |
|
|
23
|
+
| `ctx.memory` | The routine's own bookkeeping, for example an agent's session id. | The routine of the same state, on a retry or a later visit. | At once. |
|
|
24
|
+
| `ctx.note(text)` | One line of progress, for people. | People, in the history. | At once. |
|
|
25
|
+
| `inputs=` on a parallel state | Data that goes down, from the parent's context to each child's context. | The routines of the child. | When the child starts. |
|
|
26
|
+
| `into="key"` on a parallel state | Data that comes back up: the `Branches` of the join, with each child's context. | Each later routine of the parent. | When the join is met. |
|
|
27
|
+
|
|
28
|
+
Some facts:
|
|
29
|
+
|
|
30
|
+
- A routine knows which event brought the token in: `ctx.entered_by.name`, for example `timeout`. It does not get that event's payload. For the payload, declare `into=`.
|
|
31
|
+
- `into=` works for each source of the event: a routine's report, `stateous token fire`, a timer and a resume.
|
|
32
|
+
- If a routine writes the same key that the event's `into=` names, the payload wins.
|
|
33
|
+
- `stateous compile` checks `into=`. The field must be in the context model, and its type must take the payload model, for example `Verdict` or `Verdict | None`.
|
|
34
|
+
- `stateous token show` prints the context, so you can see what each routine wrote.
|
|
35
|
+
|
|
36
|
+
## Example
|
|
37
|
+
|
|
38
|
+
This is a `triage` workflow for support tickets. `classify` decides if a ticket is urgent. `escalate` pages an engineer. `answer` writes the reply.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# triage/schemas.py
|
|
42
|
+
from pydantic import BaseModel
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Ticket(BaseModel):
|
|
46
|
+
title: str
|
|
47
|
+
body: str
|
|
48
|
+
on_call: str = "" # escalate writes it
|
|
49
|
+
reply: str = "" # answer writes it
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The rule "the body has the word 'down'" is a decision. So a routine makes it, and returns `urgent` or `normal`. A guard does not search the text.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# triage-supervisor/classify.py
|
|
56
|
+
import re
|
|
57
|
+
|
|
58
|
+
from stateous import on_state
|
|
59
|
+
|
|
60
|
+
from contract import TicketContext, events, states
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@on_state(states.classify)
|
|
64
|
+
def classify(ctx: TicketContext) -> events.urgent | events.normal:
|
|
65
|
+
if re.search(r"\bdown\b", ctx["body"].lower()):
|
|
66
|
+
return events.urgent()
|
|
67
|
+
return events.normal()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`escalate` writes the engineer's name to the context. `answer`, a later state, reads it there:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# triage-supervisor/escalate.py
|
|
74
|
+
from stateous import on_state
|
|
75
|
+
|
|
76
|
+
from contract import TicketContext, events, states
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@on_state(states.escalate)
|
|
80
|
+
def escalate(ctx: TicketContext) -> events.paged:
|
|
81
|
+
ctx["on_call"] = "Ada"
|
|
82
|
+
ctx.note("paged Ada")
|
|
83
|
+
return events.paged()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# triage-supervisor/answer.py
|
|
88
|
+
from stateous import on_state
|
|
89
|
+
|
|
90
|
+
from contract import TicketContext, events, states
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@on_state(states.answer)
|
|
94
|
+
def answer(ctx: TicketContext) -> events.replied:
|
|
95
|
+
if ctx["on_call"]:
|
|
96
|
+
ctx["reply"] = f"This is urgent. {ctx['on_call']} works on it now."
|
|
97
|
+
else:
|
|
98
|
+
ctx["reply"] = "Thank you. We reply soon."
|
|
99
|
+
return events.replied()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`stateous token show TICKET-1` then prints the context with `on_call` and `reply`.
|
|
103
|
+
|
|
104
|
+
## Guarantees
|
|
105
|
+
|
|
106
|
+
- A routine's context changes and an event's `into=` are saved in one transaction with the event, or not at all.
|
|
107
|
+
- A routine never sees a payload. So it cannot depend on data that the token does not keep.
|
|
108
|
+
- `stateous compile` refuses an `into=` whose field is not in the context model. It also refuses a field whose type does not take the payload.
|
|
109
|
+
|
|
110
|
+
## See also
|
|
111
|
+
|
|
112
|
+
- [Context, notes and memory](context-notes-memory.md): each field of `ctx`.
|
|
113
|
+
- [Guards](guards.md): what a guard can read.
|
|
114
|
+
- [Parallel and nested work](parallel.md): `inputs=` and `into=` on a parallel state.
|
|
115
|
+
- [The model](model.md): the words context, payload and event.
|
|
@@ -6,10 +6,12 @@ A guard is a small condition in `when=` that chooses which transition an event t
|
|
|
6
6
|
|
|
7
7
|
## Why it exists
|
|
8
8
|
|
|
9
|
-
One
|
|
9
|
+
One event 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
10
|
|
|
11
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
12
|
|
|
13
|
+
Routines decide, and guards only route. A decision about the work, such as "the body of the ticket has the word 'down'", is the work of a routine. The routine returns the event that names the decision, for example `urgent` or `normal`. A guard only chooses a transition from data that is already there: a count, a field of the payload, a field of the context. So the guard language has no text search and no arithmetic. See [Where data goes](data.md).
|
|
14
|
+
|
|
13
15
|
## How it works
|
|
14
16
|
|
|
15
17
|
A guard is an expression in a small language. It is not Python, and nothing runs it as Python. It can use these items:
|
|
@@ -38,6 +40,14 @@ When an event comes, the control plane looks at the transitions from the token's
|
|
|
38
40
|
2. If no guard matches, the token takes the transition with `otherwise=True`.
|
|
39
41
|
3. If no guard matches and there is no `otherwise=True`, the control plane refuses the event. The token stays where it is.
|
|
40
42
|
|
|
43
|
+
**The rule of `otherwise=True`.** This is the one place where the docs give this rule:
|
|
44
|
+
|
|
45
|
+
- A transition that is the only one from its state on its event needs no guard and no `otherwise=True`.
|
|
46
|
+
- When a state has two or more transitions on one event, each has a guard, except at most one. That one has no guard, and it must have `otherwise=True`: it is the fallback.
|
|
47
|
+
- The fallback is optional. With no fallback, an event that no guard matches is refused.
|
|
48
|
+
|
|
49
|
+
Compile stops if a transition breaks this rule.
|
|
50
|
+
|
|
41
51
|
### The overlap check
|
|
42
52
|
|
|
43
53
|
`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.
|
|
@@ -46,8 +56,6 @@ If two guards can overlap, compile stops with an error, and it gives an example
|
|
|
46
56
|
|
|
47
57
|
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
58
|
|
|
49
|
-
Next to transitions with guards, a transition without a guard must have `otherwise=True`. Compile stops if it does not.
|
|
50
|
-
|
|
51
59
|
## Example
|
|
52
60
|
|
|
53
61
|
In `sdlc`, the event `failed` has two transitions from `testing`:
|
|
@@ -10,7 +10,7 @@ A long process has many steps, and each step can take minutes or days. A step ca
|
|
|
10
10
|
|
|
11
11
|
Stateous puts the process in one graph. The graph tells all the places where a run can be, and all the ways to go on. A run never moves in a way that the graph does not show.
|
|
12
12
|
|
|
13
|
-
This page defines each word
|
|
13
|
+
This page defines each word that you need to build with Stateous. Every other page uses each word in the same sense. The words of how Stateous works inside, such as lease and outbox, are on [The words inside](../inside/words.md).
|
|
14
14
|
|
|
15
15
|
## How it works
|
|
16
16
|
|
|
@@ -44,7 +44,7 @@ Two states are Stateous's own. `START` is where each token begins, and `END` is
|
|
|
44
44
|
|
|
45
45
|
### event
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
What happened in a state, for example `passed` or `failed`. Only an event moves a token. Its name and its payload say why the token moved.
|
|
48
48
|
|
|
49
49
|
Stateous has three events of its own: `timeout`, `error` and `joined`. Your transitions use them by name. You cannot declare them, and you cannot fire them.
|
|
50
50
|
|
|
@@ -62,25 +62,20 @@ The status is `active`, `paused`, `completed`, `failed` or `cancelled`. A token
|
|
|
62
62
|
|
|
63
63
|
### context
|
|
64
64
|
|
|
65
|
-
The business data of a token, for example the path of a checkout. A pydantic model gives its fields. A routine reads it as `ctx["key"]`, and can write it.
|
|
65
|
+
The business data of a token, for example the path of a checkout. A pydantic model gives its fields. A routine reads it as `ctx["key"]`, and can write it. It is the only data that a routine reads and writes. See [Where data goes](data.md).
|
|
66
66
|
|
|
67
67
|
### payload
|
|
68
68
|
|
|
69
|
-
The data of one event, for example the numbers of tests that passed and failed. A pydantic model gives its fields. An event without a payload model has no payload.
|
|
69
|
+
The data of one event, for example the numbers of tests that passed and failed. A pydantic model gives its fields. An event without a payload model has no payload. Guards and the history read the payload. To keep it for later states, the event declares `into="key"`, and the context keeps it as `ctx["key"]`.
|
|
70
70
|
|
|
71
71
|
### task
|
|
72
72
|
|
|
73
73
|
"Do state S for token T". The control plane queues one task each time a token enters a state. A supervisor claims the task and runs its routine.
|
|
74
74
|
|
|
75
|
-
|
|
76
75
|
### visit
|
|
77
76
|
|
|
78
77
|
One stay of a token in one state. The first time that a token enters a state is visit 1, the next time is visit 2. A routine reads it as `ctx.visit`. The history shows it, for example `entered coding (visit 2)`.
|
|
79
78
|
|
|
80
|
-
### claim
|
|
81
|
-
|
|
82
|
-
A supervisor's request for a task of the states that it serves. The control plane answers with a grant: one queued task, its token, its visit, its attempt and its context. The task is then claimed.
|
|
83
|
-
|
|
84
79
|
### routine
|
|
85
80
|
|
|
86
81
|
Your function that does the work of one state. It gets the token's context as `ctx`, and it returns an event. It is in the supervisor's folder, and it is marked with `@on_state(states.X)`.
|
|
@@ -97,45 +92,22 @@ The process that keeps every token, and chooses each next state. You start it wi
|
|
|
97
92
|
|
|
98
93
|
The compiled workflow, as JSON, for example `sdlc/build/sdlc.json`. `stateous compile` writes it, and `stateous serve` serves it.
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
### checksum
|
|
102
|
-
|
|
103
|
-
The hash of the whole graph file, for example `sha256:...`. Each change to the graph file changes it. A supervisor's `contract.py` is for one checksum.
|
|
104
|
-
|
|
105
|
-
### structure
|
|
106
|
-
|
|
107
|
-
The hash of the graph file without the `timeout=` and `retries=` of its states. A database serves one structure at a time. See [Change a workflow](../how-to/change-a-workflow.md).
|
|
108
|
-
|
|
109
95
|
### contract
|
|
110
96
|
|
|
111
97
|
The file `contract.py` in the supervisor's folder. `stateous init supervisor` writes it from the graph. It gives the states, the events that each state can emit, and the models. It is all that a supervisor knows of the workflow.
|
|
112
98
|
|
|
113
|
-
### ledger
|
|
114
|
-
|
|
115
|
-
The history of each token. Each change adds one row, and the rows are never changed. `stateous token show` prints the ledger of a token.
|
|
116
|
-
|
|
117
99
|
### guard
|
|
118
100
|
|
|
119
|
-
A condition on a transition, written in `when=`. It reads the context, the payload and counts of events. See [Guards](guards.md).
|
|
101
|
+
A condition on a transition, written in `when=`. It reads the context, the payload and counts of events. A guard only routes: a routine makes the decisions. See [Guards](guards.md).
|
|
120
102
|
|
|
121
103
|
### timer
|
|
122
104
|
|
|
123
105
|
An event that the clock fires later, if the token is still in the same state. A state's `timeout=` is a timer too. See [Timers and timeouts](timers.md).
|
|
124
106
|
|
|
125
|
-
### lease
|
|
126
|
-
|
|
127
|
-
The time for which a supervisor holds a claimed task. A heartbeat renews it. See [Tasks and routines](tasks-and-routines.md).
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
### heartbeat
|
|
131
|
-
|
|
132
|
-
The message that a supervisor sends every 30 seconds for all the tasks that it holds. It renews their leases. Its answer names each task that the supervisor does not hold now, for example because the token left the state.
|
|
133
|
-
|
|
134
107
|
### attempt
|
|
135
108
|
|
|
136
109
|
One try of a task. The first claim is attempt 1. When an attempt fails and the state has retries left, the next claim is attempt 2.
|
|
137
110
|
|
|
138
|
-
|
|
139
111
|
### retries
|
|
140
112
|
|
|
141
113
|
The number of more attempts that a state gives after a failed attempt: `retries=2` gives three attempts in all. After the last failed attempt, the token gets the event `error`.
|
|
@@ -156,24 +128,10 @@ A state whose work is other workflows, which run at the same time. It has no tas
|
|
|
156
128
|
|
|
157
129
|
A token that a parallel state starts, one for each workflow of the state. Its parent is the token in the parallel state.
|
|
158
130
|
|
|
159
|
-
|
|
160
131
|
### join
|
|
161
132
|
|
|
162
133
|
The rule that tells when a parallel state is done. `join="all"` waits for each child token to end. `join="any"` continues when the first child token ends. When the join is met, the control plane fires `joined` on the parent.
|
|
163
134
|
|
|
164
|
-
### outbox
|
|
165
|
-
|
|
166
|
-
The supervisor's local file of messages that the control plane did not answer yet: `.stateous/outbox.db`. See [Reliability](reliability.md).
|
|
167
|
-
|
|
168
|
-
### idempotency key
|
|
169
|
-
|
|
170
|
-
A key that you give with an event from outside. A second event with the same key applies nothing, and gets the answer "duplicate". See [Reliability](reliability.md).
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
### held event
|
|
174
|
-
|
|
175
|
-
An event that comes to a paused token. The control plane keeps it, and applies it on resume, in the order of arrival. The history shows `done held while paused`. See [Pause, resume and cancel](pause-resume-cancel.md).
|
|
176
|
-
|
|
177
135
|
## Example
|
|
178
136
|
|
|
179
137
|
The `sdlc` workflow has two states. In `coding`, an agent writes code. In `testing`, a routine runs the tests.
|
|
@@ -240,7 +198,7 @@ stateDiagram-v2
|
|
|
240
198
|
- A token is in one state at a time. Only an event moves it, and only along a transition of the graph.
|
|
241
199
|
- An event that the token's state cannot take is refused. The token stays where it is, and the ledger records the refusal.
|
|
242
200
|
- The ledger keeps each change. Stateous never changes or deletes a row of the ledger.
|
|
243
|
-
- Not guaranteed: that your routine does the right work. Stateous makes sure only that its
|
|
201
|
+
- Not guaranteed: that your routine does the right work. Stateous makes sure only that its event is one that the graph allows.
|
|
244
202
|
|
|
245
203
|
## See also
|
|
246
204
|
|
|
@@ -47,9 +47,11 @@ When the join is met, the control plane fires the event `joined` on the parent.
|
|
|
47
47
|
- `token`: the child's id;
|
|
48
48
|
- `status`: `completed` or `failed`;
|
|
49
49
|
- `final`: the state that the child was in when it ended;
|
|
50
|
-
- `
|
|
50
|
+
- `context`: the child's context when it ended.
|
|
51
51
|
|
|
52
|
-
A guard on `joined` can use `branches.all(...)` and `branches.any(...)`. In the parentheses, use `workflow`, `token`, `status`, `final` and `
|
|
52
|
+
A guard on `joined` can use `branches.all(...)` and `branches.any(...)`. In the parentheses, use `workflow`, `token`, `status`, `final` and `context.field`. `stateous compile` checks that a child workflow of the state has the field. The guard can also use `ctx` and `count`.
|
|
53
|
+
|
|
54
|
+
To keep the results of the children for later states, give the parallel state `into="reviews"`. The parent's context then keeps the payload of `joined` as `ctx["reviews"]`. Its type in the context model is `Branches` (from `stateous`), or `Branches | None`. So data goes down to the children with `inputs=`, and comes back with `into=`, always through the context. See [Where data goes](data.md).
|
|
53
55
|
|
|
54
56
|
`stateous compile` cannot prove that two guards with `branches` never overlap. So give the guard to one transition, and make the other the fallback with `otherwise=True`. If no transition takes `joined`, the parent fails.
|
|
55
57
|
|
|
@@ -103,6 +103,10 @@ error: the control plane at http://127.0.0.1:8080 refused (409): token 'TICKET-3
|
|
|
103
103
|
|
|
104
104
|
```text
|
|
105
105
|
TICKET-3: testing, cancelled
|
|
106
|
+
context:
|
|
107
|
+
{
|
|
108
|
+
"repo": "acme/api"
|
|
109
|
+
}
|
|
106
110
|
22:39:47 #1 started
|
|
107
111
|
22:39:47 #2 __started__ in START
|
|
108
112
|
22:39:47 #3 entered coding
|
|
@@ -130,7 +130,7 @@ build-box-2 took task 1 (attempt 2)
|
|
|
130
130
|
|
|
131
131
|
## See also
|
|
132
132
|
|
|
133
|
-
- [The
|
|
133
|
+
- [The words inside](../inside/words.md): [ledger](../inside/words.md#ledger), [lease](../inside/words.md#lease), [outbox](../inside/words.md#outbox) and [idempotency key](../inside/words.md#idempotency-key).
|
|
134
134
|
- [Tasks and routines](tasks-and-routines.md): claims, heartbeats and attempts.
|
|
135
135
|
- [Context, notes and memory](context-notes-memory.md): the routine's memory.
|
|
136
136
|
- [Pause, resume and cancel](pause-resume-cancel.md).
|
|
@@ -20,7 +20,7 @@ A task goes through these steps:
|
|
|
20
20
|
4. **Heartbeat.** While the routine runs, the supervisor sends a heartbeat every 30 seconds. One heartbeat names all of its running tasks, and it renews their leases.
|
|
21
21
|
5. **Report.** The routine returns, and the supervisor reports the outcome. An event moves the token, and the task is done.
|
|
22
22
|
|
|
23
|
-
The supervisor reports one of three kinds
|
|
23
|
+
The supervisor reports the **outcome** of the task. An outcome is one of three kinds:
|
|
24
24
|
|
|
25
25
|
| Outcome | When | What the control plane does |
|
|
26
26
|
|---|---|---|
|
|
@@ -128,9 +128,9 @@ build-box-2 took task 1 (attempt 2)
|
|
|
128
128
|
|
|
129
129
|
## See also
|
|
130
130
|
|
|
131
|
-
- [The model](model.md): the words [task](model.md#task), [routine](model.md#routine), [lease](
|
|
131
|
+
- [The model](model.md): the words [task](model.md#task), [routine](model.md#routine), [lease](../inside/words.md#lease) and [attempt](model.md#attempt).
|
|
132
132
|
- [The two sides](two-sides.md): what crosses the network.
|
|
133
133
|
- [Timers and timeouts](timers.md): a state's `timeout=` covers all attempts.
|
|
134
134
|
- [Context, notes and memory](context-notes-memory.md): every field of `ctx`.
|
|
135
135
|
- [Reliability](reliability.md): leases, the outbox, and what happens when a supervisor stops.
|
|
136
|
-
- [Write a routine](../how-to/write-a-routine.md): mark a routine, return an
|
|
136
|
+
- [Write a routine](../how-to/write-a-routine.md): mark a routine, return an event, and fail.
|
|
@@ -12,7 +12,7 @@ So Stateous has two sides. The control plane decides, and the supervisors do the
|
|
|
12
12
|
|
|
13
13
|
The supervisor never sees the graph, for two reasons:
|
|
14
14
|
|
|
15
|
-
- A routine cannot depend on the next state. It can only return an
|
|
15
|
+
- A routine cannot depend on the next state. It can only return an event, so you can change the graph without a change to the routines.
|
|
16
16
|
- A supervisor has only the information that its work needs. The rest of the process stays on the control plane.
|
|
17
17
|
|
|
18
18
|
## How it works
|
|
@@ -36,48 +36,99 @@ Next steps:
|
|
|
36
36
|
|
|
37
37
|
On Windows, the paths in these lines have `\` in place of `/`.
|
|
38
38
|
|
|
39
|
-
This is `orders/flow.py
|
|
39
|
+
This is `orders/flow.py`, as `init` writes it:
|
|
40
40
|
|
|
41
41
|
```python
|
|
42
42
|
# orders/flow.py: the workflow. `stateous compile orders/flow.py` checks it and writes
|
|
43
43
|
# the graph file, orders/build/orders.json.
|
|
44
44
|
from stateous import END, START, Workflow
|
|
45
45
|
|
|
46
|
+
from schemas import Job, Result
|
|
47
|
+
|
|
48
|
+
# The workflow. Each token has a context: a Job (see schemas.py). The context is
|
|
49
|
+
# the data of a token. A routine reads and writes only the context.
|
|
50
|
+
flow = Workflow("orders", context=Job)
|
|
51
|
+
|
|
52
|
+
# The states. A token is in one state at a time. A routine does the work of a state.
|
|
53
|
+
work = flow.state("work")
|
|
54
|
+
|
|
55
|
+
# The events. An event moves a token. Its payload says why the token moved.
|
|
56
|
+
# `into="result"` keeps the payload in the context, as ctx["result"].
|
|
57
|
+
done = flow.event("done", payload=Result, into="result")
|
|
58
|
+
|
|
59
|
+
# The transitions: where each event takes a token. START and END begin and end each token.
|
|
60
|
+
# A routine decides which event to return. A guard (`when=`) only chooses a transition.
|
|
61
|
+
# See https://stateouslabs.github.io/stateous/concepts/data/
|
|
62
|
+
flow.transition(START, work)
|
|
63
|
+
flow.transition(work, END, on=done)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The starter has one state, `work`, and one event, `done`. Its comments give two rules:
|
|
67
|
+
|
|
68
|
+
- **Where data goes.** The context is the data of a token. A routine reads and writes only the context. An event's payload says why the token moved. `into=` keeps a payload in the context. See [Where data goes](concepts/data.md).
|
|
69
|
+
- **Who decides.** A routine decides, and returns the event that names its decision. A guard only chooses a transition.
|
|
70
|
+
|
|
71
|
+
## Make it yours
|
|
72
|
+
|
|
73
|
+
Now change the starter into `orders`. Replace `orders/schemas.py` with this:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# orders/schemas.py (your version)
|
|
77
|
+
from pydantic import BaseModel
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Verdict(BaseModel):
|
|
81
|
+
"""The payload of the event `reviewed`."""
|
|
82
|
+
|
|
83
|
+
approved: bool
|
|
84
|
+
note: str = ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Item(BaseModel):
|
|
88
|
+
"""The context of each token of `orders`."""
|
|
89
|
+
|
|
90
|
+
title: str
|
|
91
|
+
verdict: Verdict | None = None # `reviewed` keeps its payload here
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Replace `orders/flow.py` with this:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# orders/flow.py (your version)
|
|
98
|
+
from stateous import END, START, Workflow
|
|
99
|
+
|
|
46
100
|
from schemas import Item, Verdict
|
|
47
101
|
|
|
48
|
-
# The workflow and its name. Each token has a context: an Item (see schemas.py).
|
|
49
102
|
flow = Workflow("orders", context=Item)
|
|
50
103
|
|
|
51
|
-
# The states. A token is in one state at a time. A routine does the work of a state.
|
|
52
|
-
# `timeout=` and `retries=` are optional. See
|
|
53
|
-
# https://stateouslabs.github.io/stateous/how-to/write-a-workflow/
|
|
54
104
|
draft = flow.state("draft")
|
|
55
105
|
review = flow.state("review")
|
|
56
106
|
|
|
57
|
-
# The events: the outcomes that move a token. A payload is optional.
|
|
58
107
|
submitted = flow.event("submitted") # no payload
|
|
59
|
-
reviewed = flow.event("reviewed", payload=Verdict)
|
|
108
|
+
reviewed = flow.event("reviewed", payload=Verdict, into="verdict")
|
|
60
109
|
|
|
61
|
-
# The transitions: where each event takes a token. START and END begin and end each token.
|
|
62
110
|
flow.transition(START, draft) # a new token goes to `draft` at once
|
|
63
111
|
flow.transition(draft, review, on=submitted)
|
|
64
|
-
#
|
|
112
|
+
# The routine of `review` decides. The guard only routes on its Verdict.
|
|
65
113
|
flow.transition(review, END, on=reviewed, when="payload.approved == true")
|
|
66
|
-
# `otherwise=True` marks the transition to use when no guard matches.
|
|
67
114
|
flow.transition(review, draft, on=reviewed, otherwise=True)
|
|
68
115
|
```
|
|
69
116
|
|
|
117
|
+
Then compile it again:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
stateous compile orders/flow.py
|
|
121
|
+
```
|
|
122
|
+
|
|
70
123
|
What each part does:
|
|
71
124
|
|
|
72
125
|
- `Workflow("orders", context=Item)` makes the workflow. A **token** is one run of the workflow. Each token has a **context**: here, an `Item`.
|
|
73
126
|
- `flow.state(...)` adds a **state**. A token is in one state at a time. When a token enters a state, the control plane makes a **task** for it.
|
|
74
|
-
- `flow.event(...)` adds an **event
|
|
127
|
+
- `flow.event(...)` adds an **event**. Only an event moves a token. An event can have a **payload**, for example a `Verdict`. With `into="verdict"`, the context keeps the `Verdict` as `ctx["verdict"]`.
|
|
75
128
|
- `flow.transition(...)` tells where an event takes a token. `START` is where each token begins, and `END` is where it ends.
|
|
76
129
|
- `when=` is a **guard**: a condition on the payload or the context. Here, an approved `Verdict` ends the token.
|
|
77
130
|
- `otherwise=True` marks the transition that applies when no guard matches. Here, a `Verdict` that does not approve sends the token back to `draft`.
|
|
78
131
|
|
|
79
|
-
`orders/schemas.py` holds the two models. `Item` has one field, `title`. `Verdict` has the fields `approved` and `note`.
|
|
80
|
-
|
|
81
132
|
This is the graph of `orders`:
|
|
82
133
|
|
|
83
134
|
```mermaid
|
|
@@ -88,7 +139,7 @@ stateDiagram-v2
|
|
|
88
139
|
review --> draft: reviewed (otherwise)
|
|
89
140
|
```
|
|
90
141
|
|
|
91
|
-
`
|
|
142
|
+
`stateous compile` writes the graph file `orders/build/orders.json`. After each change to `flow.py` or `schemas.py`, compile again.
|
|
92
143
|
|
|
93
144
|
## Terminal 1: serve the workflow
|
|
94
145
|
|
|
@@ -161,10 +212,18 @@ stateous token start ORDER-1 '{"title": "first order"}'
|
|
|
161
212
|
stateous token show ORDER-1
|
|
162
213
|
```
|
|
163
214
|
|
|
164
|
-
`token start` prints `ORDER-1: draft, active`. One or two seconds later, `token show` prints the token and its history:
|
|
215
|
+
`token start` prints `ORDER-1: draft, active`. One or two seconds later, `token show` prints the token, its context and its history:
|
|
165
216
|
|
|
166
217
|
```text
|
|
167
218
|
ORDER-1: review, completed
|
|
219
|
+
context:
|
|
220
|
+
{
|
|
221
|
+
"title": "first order",
|
|
222
|
+
"verdict": {
|
|
223
|
+
"approved": true,
|
|
224
|
+
"note": ""
|
|
225
|
+
}
|
|
226
|
+
}
|
|
168
227
|
22:23:31 #1 started
|
|
169
228
|
22:23:31 #2 __started__ in START
|
|
170
229
|
22:23:31 #3 entered draft
|
|
@@ -175,9 +234,10 @@ ORDER-1: review, completed
|
|
|
175
234
|
22:23:32 #8 entered review
|
|
176
235
|
22:23:32 #9 task 2 queued for review
|
|
177
236
|
22:23:32 #10 build-box-1 took task 2 (attempt 1)
|
|
178
|
-
22:23:32 #11
|
|
179
|
-
22:23:32 #12
|
|
180
|
-
22:23:32 #13
|
|
237
|
+
22:23:32 #11 context changed: verdict
|
|
238
|
+
22:23:32 #12 reviewed in review: {"approved": true, "note": ""}
|
|
239
|
+
22:23:32 #13 task 2 done
|
|
240
|
+
22:23:32 #14 completed
|
|
181
241
|
```
|
|
182
242
|
|
|
183
243
|
The first line gives the last state and the status. `build-box-1` is the name of the supervisor: by default, the host name of its computer.
|
|
@@ -193,6 +253,14 @@ The review sends `ORDER-2` back to `draft`. There, `draft` returns `None`, so th
|
|
|
193
253
|
|
|
194
254
|
```text
|
|
195
255
|
ORDER-2: draft (visit 2), active
|
|
256
|
+
context:
|
|
257
|
+
{
|
|
258
|
+
"title": "reject me",
|
|
259
|
+
"verdict": {
|
|
260
|
+
"approved": false,
|
|
261
|
+
"note": ""
|
|
262
|
+
}
|
|
263
|
+
}
|
|
196
264
|
22:23:40 #1 started
|
|
197
265
|
22:23:40 #2 __started__ in START
|
|
198
266
|
22:23:40 #3 entered draft
|
|
@@ -203,13 +271,14 @@ ORDER-2: draft (visit 2), active
|
|
|
203
271
|
22:23:40 #8 entered review
|
|
204
272
|
22:23:40 #9 task 4 queued for review
|
|
205
273
|
22:23:40 #10 build-box-1 took task 4 (attempt 1)
|
|
206
|
-
22:23:40 #11
|
|
207
|
-
22:23:40 #12
|
|
208
|
-
22:23:40 #13
|
|
209
|
-
22:23:40 #14
|
|
210
|
-
22:23:40 #15
|
|
211
|
-
22:23:40 #16
|
|
212
|
-
22:23:40 #17
|
|
274
|
+
22:23:40 #11 context changed: verdict
|
|
275
|
+
22:23:40 #12 reviewed in review: {"approved": false, "note": ""}
|
|
276
|
+
22:23:40 #13 task 4 done
|
|
277
|
+
22:23:40 #14 entered draft (visit 2)
|
|
278
|
+
22:23:40 #15 task 5 queued for draft
|
|
279
|
+
22:23:40 #16 build-box-1 took task 5 (attempt 1)
|
|
280
|
+
22:23:40 #17 the review sent this order back: a person must change it
|
|
281
|
+
22:23:40 #18 task 5 waits: the work goes on outside
|
|
213
282
|
```
|
|
214
283
|
|
|
215
284
|
The token stays in `draft` until a person acts. For example, `stateous token cancel ORDER-2` cancels it. [Steer tokens](how-to/steer-tokens.md) tells what else a person can do.
|
|
@@ -47,16 +47,22 @@ The duplicate shows the token as it is now. A key stays used for the life of the
|
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
stateous token show TICKET-1
|
|
50
|
+
stateous token show TICKET-1 --json
|
|
50
51
|
stateous token show
|
|
51
52
|
stateous token show --graph
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
- With an id, `show` prints the token's line, its children, and its history. The times are local.
|
|
55
|
+
- With an id, `show` prints the token's line, its context, its children, and its history. The times are local.
|
|
56
|
+
- With an id and `--json`, it prints the same as one JSON object, for scripts.
|
|
55
57
|
- With no id, it lists the tokens, at most 100, the most recently moved first.
|
|
56
58
|
- `--graph` draws the workflow as a Mermaid diagram.
|
|
57
59
|
|
|
58
60
|
```text
|
|
59
61
|
TICKET-1: testing, completed
|
|
62
|
+
context:
|
|
63
|
+
{
|
|
64
|
+
"repo": "acme/api"
|
|
65
|
+
}
|
|
60
66
|
23:43:39 #1 started
|
|
61
67
|
23:43:39 #2 __started__ in START
|
|
62
68
|
23:43:39 #3 entered coding
|
|
@@ -157,6 +163,6 @@ The `Idempotency-Key` header is the key. A payload goes in the body, as `{"paylo
|
|
|
157
163
|
- [Pause, resume and cancel](../concepts/pause-resume-cancel.md): held events, and the order of replay.
|
|
158
164
|
- [Reliability](../concepts/reliability.md): idempotency keys.
|
|
159
165
|
- [Tasks and routines](../concepts/tasks-and-routines.md): tasks that wait.
|
|
160
|
-
- [The model](../concepts/model.md): the words [token](../concepts/model.md#token) and [idempotency key](../
|
|
166
|
+
- [The model](../concepts/model.md): the words [token](../concepts/model.md#token) and [idempotency key](../inside/words.md#idempotency-key).
|
|
161
167
|
- [The commands](../reference/cli.md): every `token` command and its options.
|
|
162
168
|
- [The HTTP API](../reference/http.md): every route, and the refusal codes.
|