pyattacker 0.3.0__tar.gz → 0.3.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.
Files changed (124) hide show
  1. {pyattacker-0.3.0 → pyattacker-0.3.2}/CHANGELOG.md +57 -6
  2. {pyattacker-0.3.0 → pyattacker-0.3.2}/PKG-INFO +43 -21
  3. {pyattacker-0.3.0 → pyattacker-0.3.2}/README.md +42 -20
  4. {pyattacker-0.3.0 → pyattacker-0.3.2}/README.zh-CN.md +31 -13
  5. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/cli.md +30 -13
  6. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/design.md +51 -44
  7. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/reference.md +83 -31
  8. pyattacker-0.3.2/docs/suites.md +240 -0
  9. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/tutorial.md +31 -23
  10. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/zh-CN/cli.md +26 -9
  11. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/zh-CN/design.md +38 -25
  12. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/zh-CN/reference.md +76 -30
  13. pyattacker-0.3.2/docs/zh-CN/suites.md +128 -0
  14. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/zh-CN/tutorial.md +28 -19
  15. pyattacker-0.3.2/examples/live_metrics.py +72 -0
  16. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/llm_eval/README.md +1 -1
  17. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/llm_eval/README.zh-CN.md +1 -1
  18. pyattacker-0.3.2/examples/suites/experiment.json +14 -0
  19. pyattacker-0.3.2/examples/suites/suite.json +12 -0
  20. {pyattacker-0.3.0 → pyattacker-0.3.2}/pyproject.toml +1 -1
  21. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/__init__.py +8 -1
  22. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/cli.py +79 -18
  23. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/declarative.py +11 -3
  24. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/errors.py +5 -2
  25. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/export.py +46 -5
  26. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/handoff.py +23 -20
  27. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/monitor.py +44 -4
  28. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/pipeline.py +14 -5
  29. pyattacker-0.3.2/src/pyattacker/reported_metrics.py +73 -0
  30. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/runner.py +94 -8
  31. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/server.py +111 -14
  32. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/store/base.py +15 -4
  33. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/store/memory.py +25 -0
  34. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/store/sqlite.py +70 -5
  35. pyattacker-0.3.2/src/pyattacker/store/suite.py +955 -0
  36. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/store/writebehind.py +18 -2
  37. pyattacker-0.3.2/src/pyattacker/suite.py +340 -0
  38. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/task.py +20 -0
  39. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_backward.py +1 -1
  40. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_docs_examples.py +1 -1
  41. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_docs_i18n.py +1 -0
  42. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_export.py +2 -2
  43. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_handoff.py +1 -1
  44. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_monitor.py +1 -1
  45. pyattacker-0.3.2/tests/test_reported_metrics.py +193 -0
  46. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_server.py +14 -10
  47. pyattacker-0.3.2/tests/test_suite.py +822 -0
  48. {pyattacker-0.3.0 → pyattacker-0.3.2}/uv.lock +1 -1
  49. {pyattacker-0.3.0 → pyattacker-0.3.2}/.github/workflows/ci.yml +0 -0
  50. {pyattacker-0.3.0 → pyattacker-0.3.2}/.github/workflows/release.yml +0 -0
  51. {pyattacker-0.3.0 → pyattacker-0.3.2}/.gitignore +0 -0
  52. {pyattacker-0.3.0 → pyattacker-0.3.2}/LICENSE +0 -0
  53. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/benchmark.md +0 -0
  54. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/releasing.md +0 -0
  55. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/zh-CN/benchmark.md +0 -0
  56. {pyattacker-0.3.0 → pyattacker-0.3.2}/docs/zh-CN/releasing.md +0 -0
  57. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/data.jsonl +0 -0
  58. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/llm_eval/__init__.py +0 -0
  59. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/llm_eval/backend.py +0 -0
  60. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/llm_eval/demo.py +0 -0
  61. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/llm_eval/pipelines.py +0 -0
  62. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_package/README.md +0 -0
  63. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_package/README.zh-CN.md +0 -0
  64. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_package/pa_demo_plugin/__init__.py +0 -0
  65. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_package/pa_demo_plugin/algorithms.py +0 -0
  66. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_package/pa_demo_plugin/codecs.py +0 -0
  67. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_package/pa_demo_plugin/tasks.py +0 -0
  68. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_package/pyproject.toml +0 -0
  69. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/plugin_tasks.yaml +0 -0
  70. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/qa_eval.yaml +0 -0
  71. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/quickstart.py +0 -0
  72. {pyattacker-0.3.0 → pyattacker-0.3.2}/examples/sharded.py +0 -0
  73. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/__main__.py +0 -0
  74. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/algorithm.py +0 -0
  75. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/artifact.py +0 -0
  76. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/backends.py +0 -0
  77. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/benchmark/__init__.py +0 -0
  78. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/benchmark/clock.py +0 -0
  79. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/benchmark/harness.py +0 -0
  80. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/benchmark/metrics.py +0 -0
  81. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/benchmark/provider.py +0 -0
  82. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/benchmark/report.py +0 -0
  83. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/benchmark/scenario.py +0 -0
  84. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/history.py +0 -0
  85. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/merge.py +0 -0
  86. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/plugins.py +0 -0
  87. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/resource.py +0 -0
  88. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/scheduler.py +0 -0
  89. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/shard.py +0 -0
  90. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/store/__init__.py +0 -0
  91. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/store/visits.py +0 -0
  92. {pyattacker-0.3.0 → pyattacker-0.3.2}/src/pyattacker/tasks/__init__.py +0 -0
  93. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/helpers.py +0 -0
  94. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_algorithms.py +0 -0
  95. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_artifact.py +0 -0
  96. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_backends.py +0 -0
  97. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_benchmark_cli.py +0 -0
  98. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_benchmark_clock.py +0 -0
  99. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_benchmark_harness.py +0 -0
  100. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_benchmark_provider.py +0 -0
  101. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_benchmark_report.py +0 -0
  102. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_benchmark_scenario.py +0 -0
  103. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_cli.py +0 -0
  104. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_declarative.py +0 -0
  105. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_docs_facts.py +0 -0
  106. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_errors.py +0 -0
  107. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_lease_safety.py +0 -0
  108. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_m2.py +0 -0
  109. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_optional_yaml.py +0 -0
  110. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_packaging.py +0 -0
  111. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_pipeline.py +0 -0
  112. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_plugins.py +0 -0
  113. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_resume_cursor.py +0 -0
  114. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_resume_identity.py +0 -0
  115. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_retry_policy.py +0 -0
  116. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_runner.py +0 -0
  117. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_scheduler.py +0 -0
  118. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_shard.py +0 -0
  119. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_store.py +0 -0
  120. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_subprocess_lifecycle.py +0 -0
  121. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_task_overrides.py +0 -0
  122. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_tasks.py +0 -0
  123. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_tutorial.py +0 -0
  124. {pyattacker-0.3.0 → pyattacker-0.3.2}/tests/test_worker_liveness.py +0 -0
@@ -4,6 +4,57 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [0.3.2] — 2026-09-21
10
+
11
+ ### Changed
12
+
13
+ * **Handoffs and forward/backward jumps are supported public features.** `Handoff.to`,
14
+ `Handoff.end`, `Handoff.rewind`, `Handoff.retry_all` and their `control` declarations now
15
+ follow the project's regular versioning policy, without a separate experimental tier.
16
+ Code documentation and both language editions use this status consistently, including
17
+ the companion payload-history guide. Explicit declarations, finite backward budgets,
18
+ atomic store capabilities, execution semantics and persisted identities remain unchanged.
19
+ Documentation links use the new headings; legacy anchors preserve published bookmarks.
20
+ Earlier release entries below describe their status at the time of release.
21
+
22
+ ### Added
23
+
24
+ * **Experiment suites** (#68): reference existing configurations and schedule their inputs
25
+ round-robin through one Runner with shared global concurrency and explicit local pool bindings.
26
+ Stable Suite/member identities preserve original local keys and isolate checkpoints and metrics.
27
+ * Optional `by_experiment` output directories alongside default `combined` storage, with a
28
+ persistent catalog, source-exhaustion state, selective resume and bounded child connections.
29
+ Directory-aware report/watch/serve/export provide cumulative and durable invocation views,
30
+ preserving historical failures, skips and execution timing after resume. Recovery finalizes
31
+ abandoned catalog runs as interrupted, freezing their elapsed time. Unselected member
32
+ databases remain untouched. Pool environment placeholders allow credential rotation;
33
+ reopening validates the persisted artifact backend.
34
+ * `export --rows results`, `--by-experiment`, member-scoped reported metrics, and task context
35
+ experiment/output-directory fields. Split export filenames follow the selected row kind.
36
+ Single-store exports atomically replace completed files; admitted specs repair migrated repeats.
37
+ * English and Chinese Suite guides and a runnable JSON example. Existing standalone path/ID
38
+ semantics remain unchanged; Suite mode explicitly rejects independent process shards/custom stores.
39
+
40
+ ## [0.3.1] — 2026-09-19
41
+
42
+ ### Added
43
+
44
+ * **Application-reported live metrics.** `Runner.report_metric()` and `TaskContext.report_metric()`
45
+ publish latest values scoped to a run or pipeline. Applications can observe committed pipeline
46
+ completions with `on_pipeline_finished(runner, record, artifact)` and compute their own accuracy or
47
+ other cross-pipeline measures. SQLite and in-memory stores persist the reports; `/metrics`, the HTML
48
+ dashboard, and terminal `watch` display them. See `examples/live_metrics.py` for a restart-aware
49
+ accuracy monitor. Metric calculation remains application-owned.
50
+
51
+ ### Fixed
52
+
53
+ * **Consistent monitoring scope.** With no explicit run ID, the dashboard, JSON endpoints and terminal
54
+ `watch` all select the latest started run. The dashboard uses that same ID for statistics, metrics,
55
+ pipelines and events, including when only pipeline-scoped values were reported. `run_id=all` requests
56
+ aggregate operational statistics without mixing in a single run's application metrics.
57
+
7
58
  ## [0.3.0] — 2026-09-19
8
59
 
9
60
  ### Added
@@ -27,7 +78,7 @@ All notable changes to this project are documented here. The format follows
27
78
  keeps the append-only history, and a store that has committed its first revisit records a `base` → `visits-v1`
28
79
  feature level (`store.feature_level()`, `StoreFeatureUnsupported`) so lineage-unaware writers are refused
29
80
  rather than silently mutating the wrong occurrence. See
30
- [reference → advanced: backward traversal](docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits).
81
+ [reference → advanced: backward traversal](docs/reference.md#backward-traversal-rewind-retry-all-visits).
31
82
 
32
83
  * **Advanced feature: handoffs — a task can skip ahead, on the record (opt-in, experimental).** A task may
33
84
  return `Handoff.to(target, value)` to continue at a declared later station, or `Handoff.end(value)` to
@@ -50,9 +101,9 @@ All notable changes to this project are documented here. The format follows
50
101
  commit them atomically; both built-in backends can, and a store that cannot is refused up front with a
51
102
  `ConfigError` rather than silently writing a non-durable jump. Marked *experimental until 1.0*;
52
103
  the forward declaration excludes backward targets; joins and cross-pipeline jumps are not included. See
53
- [`docs/design.md` §4.8](docs/design.md#48-advanced-handoffs--declared-forward-jumps-opt-in-experimental),
54
- the [API reference](docs/reference.md#advanced-handoffs-opt-in) and
55
- [tutorial step 15](docs/tutorial.md#step-15--advanced-skipping-stations-handoffs).
104
+ [`docs/design.md` §4.8](docs/design.md#48-handoffs--declared-forward-jumps-opt-in),
105
+ the [API reference](docs/reference.md#handoffs-opt-in) and
106
+ [tutorial step 15](docs/tutorial.md#step-15--skipping-stations-handoffs).
56
107
 
57
108
  ### Fixed
58
109
 
@@ -188,9 +239,9 @@ All notable changes to this project are documented here. The format follows
188
239
 
189
240
  * **The backward-traversal guide is merged into the tutorial and the reference.** `docs/backward.md` was a
190
241
  seventh document that a reader had to find before they could use the feature; its usage now lives where
191
- the rest of the API does. [Tutorial step 16](docs/tutorial.md#step-16--advanced-regenerating-with-rewind-and-retry-all)
242
+ the rest of the API does. [Tutorial step 16](docs/tutorial.md#step-16--regenerating-with-rewind-and-retry-all)
192
243
  keeps the rewind/retry-all walkthrough and a new step 17 builds an optional `HistoryArtifact` payload,
193
- while [reference → advanced: backward traversal](docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits)
244
+ while [reference → advanced: backward traversal](docs/reference.md#backward-traversal-rewind-retry-all-visits)
194
245
  gains the `Handoff.rewind`/`Handoff.retry_all` signatures, the backward `control` keys, the visit and
195
246
  occurrence model, the budget and recovery rules, and the inspection surface; `HistoryArtifact` is
196
247
  documented next to the codecs it belongs to, and the store capability and compatibility/backup rules moved
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyattacker
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Artifact-centric, resumable async task orchestration: pipeline / task / resource pool.
5
5
  Project-URL: Homepage, https://github.com/Hazer-BJTU/pyattacker
6
6
  Project-URL: Repository, https://github.com/Hazer-BJTU/pyattacker
@@ -93,7 +93,7 @@ Five concepts, and that is the whole vocabulary:
93
93
  | Concept | Meaning | In one line |
94
94
  |---|---|---|
95
95
  | **artifact** | the persisted state of a task | content-addressed, **persisted as soon as it is produced** → checkpoint granularity = task |
96
- | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([advanced](#advanced-handoffs-opt-in)) |
96
+ | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([handoffs](#handoffs-opt-in)) |
97
97
  | **pipeline** | the unit of completion | `fetch \| ask \| judge \| metrics` chained linearly, semantically independent of each other |
98
98
  | **resource** | a leasable external capability | one endpoint / one key; once pooled, it can be published and subscribed to concurrency-safely |
99
99
  | **algorithm** | the policy for acquiring resources | `wait`, `backoff`, `least_busy`, `failover`, `sticky`, `quota_aware`, `immediate` — orthogonal to "retry on failure" |
@@ -202,7 +202,7 @@ runner.run(template.map(rows), resume=True) # or pyattacker resume -c config.y
202
202
  * Failed pipelines → continue from **the first task that produced no artifact**: **if task C died, only task C
203
203
  reruns when task B's checkpoint is durable**;
204
204
  * The seed artifact is persisted too → recovery **does not depend on the original dataset file**;
205
- * A pipeline that **handed off** ([advanced](#advanced-handoffs-opt-in)) resumes at the station it jumped to,
205
+ * A pipeline that **handed off** ([handoffs](#handoffs-opt-in)) resumes at the station it jumped to,
206
206
  with the entry state the ledger recorded — the task that handed off is not re-run;
207
207
  * Changed a task's source code (`spec_digest` includes source digests) → treated as a new pipeline, so old
208
208
  results are not incorrectly reused. Factory parameters, fanout children, retry/algorithm policies and
@@ -213,10 +213,13 @@ rerun and old explicit keys conflict. Finish old runs with the old package, then
213
213
  [resume identity and idempotency reference](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#resume-identity)
214
214
  for migration guidance, dynamic functions and external configuration.
215
215
 
216
- ## Advanced: Handoffs (Opt-In)
216
+ <a id="advanced-handoffs-opt-in"></a>
217
217
 
218
- **Advanced tier: opt-in, changes the execution model, not needed for ordinary pipelines, experimental until
219
- 1.0.** A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
218
+ ## Handoffs (Opt-In)
219
+
220
+ **Handoffs, forward jumps and rewinds are supported control-flow features, enabled explicitly through `control`.**
221
+
222
+ A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
220
223
  failure or hiding the branch inside one step. It **returns** a directive — `Handoff.to(target, value)` to
221
224
  continue at a declared later station, `Handoff.end(value)` to finish the pipeline right there:
222
225
 
@@ -284,8 +287,8 @@ with Runner(store=":memory:", concurrency=4) as runner:
284
287
  * **It is a durable checkpoint.** The jump is committed atomically (source task, attempt, entry artifact,
285
288
  ledger row and cursor together), so a killed process resumes **at the target** with the recorded entry
286
289
  state and does not re-run the source task. On a control-enabled pipeline `n_tasks_done` is a *position*,
287
- not a progress count: skipped stations have no task rows. `report`/`watch` count commits in the selected
288
- scope (one run when filtered, all history otherwise); `/pipelines.handoffs` counts active execution records and `handoffs_historical` counts all ledger
290
+ not a progress count: skipped stations have no task rows. `watch` counts commits in the selected
291
+ run by default (`--run-id all` selects all history); `/pipelines.handoffs` counts active execution records and `handoffs_historical` counts all ledger
289
292
  records. A resume can use an earlier run's active handoff while recording no new handoffs. Pipeline
290
293
  exports include ledger identity and the watermark to distinguish the scopes.
291
294
  * **Opt-in and inert.** Without the `control` block nothing changes — not one row, not one counter, and not a
@@ -296,16 +299,16 @@ with Runner(store=":memory:", concurrency=4) as runner:
296
299
  payloads provide explicit snapshots and restoration; ordinary dictionaries remain author-controlled.
297
300
  Visits and exact artifact occurrences retain history and make recovery safe. The APIs, budgets and
298
301
  recovery boundaries are in
299
- [reference → advanced: backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits),
302
+ [reference → backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#backward-traversal-rewind-retry-all-visits),
300
303
  with working programs in
301
- [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--advanced-regenerating-with-rewind-and-retry-all).
304
+ [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--regenerating-with-rewind-and-retry-all).
302
305
 
303
- The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-handoffs-opt-in)),
306
+ The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#handoffs-opt-in)),
304
307
  one declaration — `control={"edges": {...}}` for forward jumps, `control.rewind` / `control.retry_all` /
305
308
  `control.max_handoffs` for backward traversal — and one optional store capability; a custom store that cannot
306
309
  commit a handoff atomically is refused up front instead of writing a jump that would not survive a crash.
307
- Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--advanced-skipping-stations-handoffs).
308
- Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-advanced-handoffs--declared-forward-jumps-opt-in-experimental).
310
+ Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--skipping-stations-handoffs).
311
+ Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-handoffs--declared-forward-jumps-opt-in).
309
312
 
310
313
  When a pipeline restarts from the seed, previous task rows and chain artifacts are reset atomically with the cursor/watermark;
311
314
  previous handoffs stay in its history but no longer act as
@@ -427,13 +430,15 @@ keeps everything in the store.
427
430
 
428
431
  ```bash
429
432
  uv run pyattacker serve runs/qa.db # http://127.0.0.1:8787
430
- # / dashboard /stats /events /pipelines /resources /errors (JSON)
433
+ # / dashboard /stats /metrics /events /pipelines /resources /errors (JSON)
431
434
  ```
432
435
 
433
436
  It opens a fresh read-only connection per request, so it runs happily beside a live run. It has **no
434
437
  authentication** and binds to loopback: there is no artifact route, but it does expose what your run recorded —
435
438
  event `data` and stored error messages — so treat it as a debug view over your own data and do not put it on a
436
439
  public interface without your own proxy in front.
440
+ Applications can publish live values with `runner.report_metric("accuracy", value, display="percent")`;
441
+ the framework displays them without calculating the metric. See [`examples/live_metrics.py`](examples/live_metrics.py).
437
442
 
438
443
  **Branching inside a step** — `fanout(a, b)` runs several tasks on the same input concurrently and
439
444
  returns `{task_name: value}`. Retry granularity becomes the group, which is the honest price of not
@@ -474,8 +479,8 @@ what they do not say.
474
479
 
475
480
  | Document | What is in it |
476
481
  |---|---|
477
- | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation and the advanced handoff/backward tiers — rewinds, retry-all and payload history included; each one executed by the test suite |
478
- | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including the advanced backward-traversal tier and `HistoryArtifact` |
482
+ | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation, handoffs and backward traversal — rewinds, retry-all and payload history included; each one executed by the test suite |
483
+ | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including backward traversal and `HistoryArtifact` |
479
484
  | [`docs/cli.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/cli.md) | every subcommand, every flag, exit codes, config reference |
480
485
  | [`docs/design.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md) | conceptual model, the six invariants, the lease contract, data model, tradeoffs |
481
486
  | [`docs/benchmark.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/benchmark.md) | the algorithm benchmark: what the scenarios assume, what the metrics mean, how to read the table |
@@ -508,7 +513,7 @@ These are design decisions, not missing features:
508
513
  * **Semantic reduction** — accuracy, pass@k, F1 and any cross-pipeline aggregation. Export the artifacts and
509
514
  compute it outside, or write a sink pipeline out of the primitives.
510
515
  * **DAG orchestration** — a pipeline is a linear chain; branch inside a task with `fanout`. The one
511
- qualification is the opt-in [handoff](#advanced-handoffs-opt-in): it changes the traversal of
516
+ qualification is the opt-in [handoff](#handoffs-opt-in): it changes the traversal of
512
517
  the chain along declared edges, never its topology (no joins, no second entry point, no cross-pipeline
513
518
  jumps).
514
519
  * **A serving gateway** — the only HTTP surface is the read-only debug endpoint above.
@@ -519,11 +524,25 @@ every known tradeoff with its reason.
519
524
 
520
525
  ## Status
521
526
 
522
- **0.3.0 — advanced control flow (opt-in), and the documentation in Chinese.** Tasks can now
523
- [hand off](#advanced-handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
527
+ **0.3.2 — experiment suites and supported control flow.** Independent experiments can share resource
528
+ pools and global concurrency, with unified run/resume/report commands and optional per-experiment output
529
+ directories. Durable run histories preserve earlier outcomes after resume. Handoffs, forward jumps,
530
+ rewinds and retry-all are supported public features, enabled through explicit `control` declarations.
531
+ See the [Suite guide](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/suites.md) and
532
+ [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21).
533
+
534
+ **0.3.1 — application-reported live metrics.** Applications can publish values such as accuracy to the
535
+ read-only monitoring dashboard through `Runner.report_metric()` or `TaskContext.report_metric()`. A completion
536
+ callback receives committed pipeline results; the application owns the calculation. The dashboard and
537
+ `watch` now follow one run consistently by default. See [`examples/live_metrics.py`](examples/live_metrics.py).
538
+
539
+ **0.3.0 — control flow (opt-in), and the documentation in Chinese.** Tasks can now
540
+ [hand off](#handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
524
541
  early, recorded in a durable ledger that recovery resumes from. It is opt-in and inert — a pipeline without a
525
- `control` block writes no new rows and keeps a byte-identical `spec_digest` — and marked experimental until
526
- 1.0. Backward traversal now adds declared rewind and retry-all with visits and optional payload history. The
542
+ `control` block writes no new rows and keeps a byte-identical `spec_digest`. Handoffs and backward traversal
543
+ were experimental in 0.3.0 and became supported features in 0.3.2
544
+ (see [0.3.2 release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21)). Backward traversal adds declared rewind and retry-all with visits
545
+ and optional payload history. The
527
546
  whole documentation set also ships in Simplified Chinese ([`README.zh-CN.md`](README.zh-CN.md),
528
547
  [`docs/zh-CN/`](https://github.com/Hazer-BJTU/pyattacker/tree/main/docs/zh-CN)), kept in sync by CI. One
529
548
  rename to know about when upgrading: `MergedReport.events_total` is now `source_events_total`, because that
@@ -580,3 +599,6 @@ extracted and executed by `tests/test_tutorial.py`, so documentation that rots f
580
599
 
581
600
  MIT — see [LICENSE](https://github.com/Hazer-BJTU/pyattacker/blob/main/LICENSE).
582
601
 
602
+ ## Experiment suites
603
+
604
+ Combine existing configuration files under one Runner with shared resource pools and total concurrency. Optional per-experiment directories separate checkpoints, results and file artifacts. Suite reports include prior successes across resumes. See the [Suite guide](docs/suites.md) and [runnable configuration](examples/suites/suite.json).
@@ -68,7 +68,7 @@ Five concepts, and that is the whole vocabulary:
68
68
  | Concept | Meaning | In one line |
69
69
  |---|---|---|
70
70
  | **artifact** | the persisted state of a task | content-addressed, **persisted as soon as it is produced** → checkpoint granularity = task |
71
- | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([advanced](#advanced-handoffs-opt-in)) |
71
+ | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([handoffs](#handoffs-opt-in)) |
72
72
  | **pipeline** | the unit of completion | `fetch \| ask \| judge \| metrics` chained linearly, semantically independent of each other |
73
73
  | **resource** | a leasable external capability | one endpoint / one key; once pooled, it can be published and subscribed to concurrency-safely |
74
74
  | **algorithm** | the policy for acquiring resources | `wait`, `backoff`, `least_busy`, `failover`, `sticky`, `quota_aware`, `immediate` — orthogonal to "retry on failure" |
@@ -177,7 +177,7 @@ runner.run(template.map(rows), resume=True) # or pyattacker resume -c config.y
177
177
  * Failed pipelines → continue from **the first task that produced no artifact**: **if task C died, only task C
178
178
  reruns when task B's checkpoint is durable**;
179
179
  * The seed artifact is persisted too → recovery **does not depend on the original dataset file**;
180
- * A pipeline that **handed off** ([advanced](#advanced-handoffs-opt-in)) resumes at the station it jumped to,
180
+ * A pipeline that **handed off** ([handoffs](#handoffs-opt-in)) resumes at the station it jumped to,
181
181
  with the entry state the ledger recorded — the task that handed off is not re-run;
182
182
  * Changed a task's source code (`spec_digest` includes source digests) → treated as a new pipeline, so old
183
183
  results are not incorrectly reused. Factory parameters, fanout children, retry/algorithm policies and
@@ -188,10 +188,13 @@ rerun and old explicit keys conflict. Finish old runs with the old package, then
188
188
  [resume identity and idempotency reference](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#resume-identity)
189
189
  for migration guidance, dynamic functions and external configuration.
190
190
 
191
- ## Advanced: Handoffs (Opt-In)
191
+ <a id="advanced-handoffs-opt-in"></a>
192
192
 
193
- **Advanced tier: opt-in, changes the execution model, not needed for ordinary pipelines, experimental until
194
- 1.0.** A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
193
+ ## Handoffs (Opt-In)
194
+
195
+ **Handoffs, forward jumps and rewinds are supported control-flow features, enabled explicitly through `control`.**
196
+
197
+ A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
195
198
  failure or hiding the branch inside one step. It **returns** a directive — `Handoff.to(target, value)` to
196
199
  continue at a declared later station, `Handoff.end(value)` to finish the pipeline right there:
197
200
 
@@ -259,8 +262,8 @@ with Runner(store=":memory:", concurrency=4) as runner:
259
262
  * **It is a durable checkpoint.** The jump is committed atomically (source task, attempt, entry artifact,
260
263
  ledger row and cursor together), so a killed process resumes **at the target** with the recorded entry
261
264
  state and does not re-run the source task. On a control-enabled pipeline `n_tasks_done` is a *position*,
262
- not a progress count: skipped stations have no task rows. `report`/`watch` count commits in the selected
263
- scope (one run when filtered, all history otherwise); `/pipelines.handoffs` counts active execution records and `handoffs_historical` counts all ledger
265
+ not a progress count: skipped stations have no task rows. `watch` counts commits in the selected
266
+ run by default (`--run-id all` selects all history); `/pipelines.handoffs` counts active execution records and `handoffs_historical` counts all ledger
264
267
  records. A resume can use an earlier run's active handoff while recording no new handoffs. Pipeline
265
268
  exports include ledger identity and the watermark to distinguish the scopes.
266
269
  * **Opt-in and inert.** Without the `control` block nothing changes — not one row, not one counter, and not a
@@ -271,16 +274,16 @@ with Runner(store=":memory:", concurrency=4) as runner:
271
274
  payloads provide explicit snapshots and restoration; ordinary dictionaries remain author-controlled.
272
275
  Visits and exact artifact occurrences retain history and make recovery safe. The APIs, budgets and
273
276
  recovery boundaries are in
274
- [reference → advanced: backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits),
277
+ [reference → backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#backward-traversal-rewind-retry-all-visits),
275
278
  with working programs in
276
- [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--advanced-regenerating-with-rewind-and-retry-all).
279
+ [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--regenerating-with-rewind-and-retry-all).
277
280
 
278
- The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-handoffs-opt-in)),
281
+ The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#handoffs-opt-in)),
279
282
  one declaration — `control={"edges": {...}}` for forward jumps, `control.rewind` / `control.retry_all` /
280
283
  `control.max_handoffs` for backward traversal — and one optional store capability; a custom store that cannot
281
284
  commit a handoff atomically is refused up front instead of writing a jump that would not survive a crash.
282
- Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--advanced-skipping-stations-handoffs).
283
- Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-advanced-handoffs--declared-forward-jumps-opt-in-experimental).
285
+ Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--skipping-stations-handoffs).
286
+ Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-handoffs--declared-forward-jumps-opt-in).
284
287
 
285
288
  When a pipeline restarts from the seed, previous task rows and chain artifacts are reset atomically with the cursor/watermark;
286
289
  previous handoffs stay in its history but no longer act as
@@ -402,13 +405,15 @@ keeps everything in the store.
402
405
 
403
406
  ```bash
404
407
  uv run pyattacker serve runs/qa.db # http://127.0.0.1:8787
405
- # / dashboard /stats /events /pipelines /resources /errors (JSON)
408
+ # / dashboard /stats /metrics /events /pipelines /resources /errors (JSON)
406
409
  ```
407
410
 
408
411
  It opens a fresh read-only connection per request, so it runs happily beside a live run. It has **no
409
412
  authentication** and binds to loopback: there is no artifact route, but it does expose what your run recorded —
410
413
  event `data` and stored error messages — so treat it as a debug view over your own data and do not put it on a
411
414
  public interface without your own proxy in front.
415
+ Applications can publish live values with `runner.report_metric("accuracy", value, display="percent")`;
416
+ the framework displays them without calculating the metric. See [`examples/live_metrics.py`](examples/live_metrics.py).
412
417
 
413
418
  **Branching inside a step** — `fanout(a, b)` runs several tasks on the same input concurrently and
414
419
  returns `{task_name: value}`. Retry granularity becomes the group, which is the honest price of not
@@ -449,8 +454,8 @@ what they do not say.
449
454
 
450
455
  | Document | What is in it |
451
456
  |---|---|
452
- | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation and the advanced handoff/backward tiers — rewinds, retry-all and payload history included; each one executed by the test suite |
453
- | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including the advanced backward-traversal tier and `HistoryArtifact` |
457
+ | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation, handoffs and backward traversal — rewinds, retry-all and payload history included; each one executed by the test suite |
458
+ | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including backward traversal and `HistoryArtifact` |
454
459
  | [`docs/cli.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/cli.md) | every subcommand, every flag, exit codes, config reference |
455
460
  | [`docs/design.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md) | conceptual model, the six invariants, the lease contract, data model, tradeoffs |
456
461
  | [`docs/benchmark.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/benchmark.md) | the algorithm benchmark: what the scenarios assume, what the metrics mean, how to read the table |
@@ -483,7 +488,7 @@ These are design decisions, not missing features:
483
488
  * **Semantic reduction** — accuracy, pass@k, F1 and any cross-pipeline aggregation. Export the artifacts and
484
489
  compute it outside, or write a sink pipeline out of the primitives.
485
490
  * **DAG orchestration** — a pipeline is a linear chain; branch inside a task with `fanout`. The one
486
- qualification is the opt-in [handoff](#advanced-handoffs-opt-in): it changes the traversal of
491
+ qualification is the opt-in [handoff](#handoffs-opt-in): it changes the traversal of
487
492
  the chain along declared edges, never its topology (no joins, no second entry point, no cross-pipeline
488
493
  jumps).
489
494
  * **A serving gateway** — the only HTTP surface is the read-only debug endpoint above.
@@ -494,11 +499,25 @@ every known tradeoff with its reason.
494
499
 
495
500
  ## Status
496
501
 
497
- **0.3.0 — advanced control flow (opt-in), and the documentation in Chinese.** Tasks can now
498
- [hand off](#advanced-handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
502
+ **0.3.2 — experiment suites and supported control flow.** Independent experiments can share resource
503
+ pools and global concurrency, with unified run/resume/report commands and optional per-experiment output
504
+ directories. Durable run histories preserve earlier outcomes after resume. Handoffs, forward jumps,
505
+ rewinds and retry-all are supported public features, enabled through explicit `control` declarations.
506
+ See the [Suite guide](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/suites.md) and
507
+ [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21).
508
+
509
+ **0.3.1 — application-reported live metrics.** Applications can publish values such as accuracy to the
510
+ read-only monitoring dashboard through `Runner.report_metric()` or `TaskContext.report_metric()`. A completion
511
+ callback receives committed pipeline results; the application owns the calculation. The dashboard and
512
+ `watch` now follow one run consistently by default. See [`examples/live_metrics.py`](examples/live_metrics.py).
513
+
514
+ **0.3.0 — control flow (opt-in), and the documentation in Chinese.** Tasks can now
515
+ [hand off](#handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
499
516
  early, recorded in a durable ledger that recovery resumes from. It is opt-in and inert — a pipeline without a
500
- `control` block writes no new rows and keeps a byte-identical `spec_digest` — and marked experimental until
501
- 1.0. Backward traversal now adds declared rewind and retry-all with visits and optional payload history. The
517
+ `control` block writes no new rows and keeps a byte-identical `spec_digest`. Handoffs and backward traversal
518
+ were experimental in 0.3.0 and became supported features in 0.3.2
519
+ (see [0.3.2 release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21)). Backward traversal adds declared rewind and retry-all with visits
520
+ and optional payload history. The
502
521
  whole documentation set also ships in Simplified Chinese ([`README.zh-CN.md`](README.zh-CN.md),
503
522
  [`docs/zh-CN/`](https://github.com/Hazer-BJTU/pyattacker/tree/main/docs/zh-CN)), kept in sync by CI. One
504
523
  rename to know about when upgrading: `MergedReport.events_total` is now `source_events_total`, because that
@@ -555,3 +574,6 @@ extracted and executed by `tests/test_tutorial.py`, so documentation that rots f
555
574
 
556
575
  MIT — see [LICENSE](https://github.com/Hazer-BJTU/pyattacker/blob/main/LICENSE).
557
576
 
577
+ ## Experiment suites
578
+
579
+ Combine existing configuration files under one Runner with shared resource pools and total concurrency. Optional per-experiment directories separate checkpoints, results and file artifacts. Suite reports include prior successes across resumes. See the [Suite guide](docs/suites.md) and [runnable configuration](examples/suites/suite.json).
@@ -51,7 +51,7 @@ uv run pyattacker demo # zero-config smoke test: 50 simulated pipelines, r
51
51
  | 概念 | 含义 | 一句话 |
52
52
  |---|---|---|
53
53
  | **artifact(产物)** | 任务持久化后的状态 | 内容寻址,**一产生就落盘** → 检查点粒度 = 任务 |
54
- | **task(任务)** | 调度的最小单位 | 一个一元函数 `(artifact) -> artifact`,同步异步都行——也可以 `-> artifact \| Handoff` 来跳过后续步骤([进阶](README.zh-CN.md#进阶交接可选启用)) |
54
+ | **task(任务)** | 调度的最小单位 | 一个一元函数 `(artifact) -> artifact`,同步异步都行——也可以 `-> artifact \| Handoff` 来跳过后续步骤([交接](README.zh-CN.md#交接可选启用)) |
55
55
  | **pipeline(流水线)** | 完成和恢复的单位 | `fetch \| ask \| judge \| metrics` 线性串起来,各步语义独立 |
56
56
  | **resource(资源)** | 可租用的外部能力 | 一个端点 / 一把 key;放进资源池后可以并发安全地发布和订阅 |
57
57
  | **algorithm(算法)** | 获取资源的策略 | `wait`、`backoff`、`least_busy`、`failover`、`sticky`、`quota_aware`、`immediate`——和"失败了怎么重试"是两回事 |
@@ -147,14 +147,16 @@ runner.run(template.map(rows), resume=True) # or pyattacker resume -c config.y
147
147
  * 任务和输入都没变、已经成功的流水线 → 直接跳过。
148
148
  * 失败的流水线 → 从**第一个没产出 artifact 的任务**接着跑:任务 C 挂了,只要 B 的检查点已经落盘,就只重跑 C。
149
149
  * 种子数据也持久化了 → 续跑**不需要原始数据集文件**。
150
- * 走过交接的流水线([进阶](README.zh-CN.md#进阶交接可选启用))会在跳转目标处恢复,用账本里记好的入口状态——发起跳转的那个任务不重跑。
150
+ * 走过交接的流水线([交接](README.zh-CN.md#交接可选启用))会在跳转目标处恢复,用账本里记好的入口状态——发起跳转的那个任务不重跑。
151
151
  * 改了任务源码(`spec_digest` 包含源码摘要)→ 视为全新流水线,不会拿旧结果瞎复用。工厂参数、fanout 子任务、重试/算法策略、显式的 `config`/`version` 都算在内。手动指定 key 的话,定义或输入变了会直接拒绝。
152
152
 
153
153
  **升级已有存储:** fingerprint v2 改了默认 ID 和分片分配方式。旧任务可能会重跑,旧的手动 key 会冲突。建议用旧版本把手上的跑完,再换新存储。迁移指引、动态函数和外部配置详见 [恢复同一性与幂等性参考](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/reference.md#恢复同一性)。
154
154
 
155
- ## 进阶:交接(可选启用)
155
+ <a id="进阶交接可选启用"></a>
156
156
 
157
- **这是进阶功能:需要手动开启,会改变执行模型,普通流水线用不上,1.0 之前算实验性。**
157
+ ## 交接(可选启用)
158
+
159
+ **交接、正向跳转和回退都是正式的控制流特性,通过 `control` 显式声明后启用。**
158
160
 
159
161
  任务可以判断"后面的步骤不用跑了",然后*明确说出来*,而不是硬编一个失败或者把分支藏在某一步里面。它**返回**一条指令——`Handoff.to(target, value)` 表示跳到指定的后续步骤继续跑,`Handoff.end(value)` 表示直接收尾:
160
162
 
@@ -215,13 +217,13 @@ with Runner(store=":memory:", concurrency=4) as runner:
215
217
 
216
218
  * **边是显式声明的,不是猜的。** 走了没声明的边,或者一个没有 `control` 块的流水线里出现了 `Handoff`,都是致命配置错误——不重试,也不会静默跳转。目标步骤必须在来源步骤之后;最后一步发起 `end` 会被拒绝(因为没意义)。
217
219
  * **交接是一种处置结果,不是失败。** 它是个返回值,所以重试策略根本看不到它,任务里写的 `except Exception:` 也吞不掉它。租约的归还和成功时完全一样,被取消或超时的尝试压根走不到返回这一步。
218
- * **它是持久化的检查点。** 跳转是原子提交的(源任务、尝试、入口产物、账本记录、游标一起提交),进程被杀了会在目标步骤恢复,带着记录好的入口状态,不会重跑源任务。开了控制流的流水线里,`n_tasks_done` 是个*位置*,不是进度计数——被跳过的步骤没有任务记录。`report`/`watch` 统计选定范围内的提交数(过滤时是单次运行,否则是全部历史);`/pipelines.handoffs` 统计活跃执行记录,`handoffs_historical` 统计全部账本记录。续跑时可以用之前某次运行的活跃交接,同时不记录新的交接。导出的流水线数据里会带上账本身份和水位标记,用来区分这些范围。
220
+ * **它是持久化的检查点。** 跳转是原子提交的(源任务、尝试、入口产物、账本记录、游标一起提交),进程被杀了会在目标步骤恢复,带着记录好的入口状态,不会重跑源任务。开了控制流的流水线里,`n_tasks_done` 是个*位置*,不是进度计数——被跳过的步骤没有任务记录。`watch` 默认统计选定运行的提交数(`--run-id all` 查看全部历史);`/pipelines.handoffs` 统计活跃执行记录,`handoffs_historical` 统计全部账本记录。续跑时可以用之前某次运行的活跃交接,同时不记录新的交接。导出的流水线数据里会带上账本身份和水位标记,用来区分这些范围。
219
221
  * **不开就完全没影响。** 没有 `control` 块的流水线,一行数据、一个计数器、`spec_digest` 的一个字节都不会变。
220
- * **往回跳需要单独声明。** `Handoff.rewind(target, value)` 把作者指定的状态送回更早的步骤;`Handoff.retry_all()` 从最初的种子重新开始。需要声明 `control.rewind` / `control.retry_all`,还要设一个有限的 `control.max_handoffs`。可选的 `HistoryArtifact` 载荷可以做显式快照和恢复;普通字典的控制权完全在作者手里。访问记录(visits)和精确的产物出现记录会保留历史,保证恢复安全。API、预算和恢复边界见 [reference → 进阶:反向遍历](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/reference.md#进阶反向遍历rewindretry-allvisits),可运行的示例见 [tutorial 第 16–17 步](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/tutorial.md#第-16-步--高级用回退和全部重试重新生成)。
222
+ * **往回跳需要单独声明。** `Handoff.rewind(target, value)` 把作者指定的状态送回更早的步骤;`Handoff.retry_all()` 从最初的种子重新开始。需要声明 `control.rewind` / `control.retry_all`,还要设一个有限的 `control.max_handoffs`。可选的 `HistoryArtifact` 载荷可以做显式快照和恢复;普通字典的控制权完全在作者手里。访问记录(visits)和精确的产物出现记录会保留历史,保证恢复安全。API、预算和恢复边界见 [reference → 反向遍历](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/reference.md#反向遍历rewindretry-allvisits),可运行的示例见 [tutorial 第 16–17 步](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/tutorial.md#第-16-步--用回退和全部重试重新生成)。
221
223
 
222
- API 就是一个类([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/reference.md#进阶交接可选启用))、一条声明——正向跳转用 `control={"edges": {...}}`,反向遍历用 `control.rewind` / `control.retry_all` / `control.max_handoffs`——再加一项可选的存储能力。无法原子提交交接的自定义存储会直接被拒绝,不会让你写一个崩了就丢的跳转。
224
+ API 就是一个类([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/reference.md#交接可选启用))、一条声明——正向跳转用 `control={"edges": {...}}`,反向遍历用 `control.rewind` / `control.retry_all` / `control.max_handoffs`——再加一项可选的存储能力。无法原子提交交接的自定义存储会直接被拒绝,不会让你写一个崩了就丢的跳转。
223
225
 
224
- 上手示例:[教程第 15 步](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/tutorial.md#第-15-步--高级跳过站点交接)。模型和规则:[设计 §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/design.md#48-进阶交接--声明式正向跳转可选启用实验性)。
226
+ 上手示例:[教程第 15 步](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/tutorial.md#第-15-步--跳过站点交接)。模型和规则:[设计 §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/design.md#48-交接--声明式正向跳转可选启用)。
225
227
 
226
228
  流水线从种子重新开始时,之前的任务记录和链上产物会和游标/水位一起原子重置。之前的交接还留在历史里,但不再当检查点用。之后的续跑只看当前这轮执行的交接,完成时选一个最终产物。支持交接的自定义存储必须把持久化的 `handoff_floor` 水位和游标一起存,还要实现原子的 `reset_pipeline(record)`。详见 [恢复契约](docs/zh-CN/reference.md#表与读取器)。
227
229
 
@@ -322,10 +324,12 @@ uv run pyattacker run -c examples/qa_eval.yaml --artifact-backend file:///data/b
322
324
 
323
325
  ```bash
324
326
  uv run pyattacker serve runs/qa.db # http://127.0.0.1:8787
325
- # / dashboard /stats /events /pipelines /resources /errors (JSON)
327
+ # / dashboard /stats /metrics /events /pipelines /resources /errors (JSON)
326
328
  ```
327
329
 
328
330
  每个请求都新开一个只读连接,所以可以和正在跑的进程并存。**没有认证**,只绑回环地址——它没有 artifact 路由,但会暴露这次运行记下来的东西:事件的 `data`、存下来的错误信息。当调试工具用就好,别裸暴露到公网。
331
+ 应用可用 `runner.report_metric("accuracy", value, display="percent")` 汇报实时指标;框架只展示,
332
+ 不计算指标。参见 [`examples/live_metrics.py`](examples/live_metrics.py)。
329
333
 
330
334
  **步骤内分支**——`fanout(a, b)` 拿同一份输入并发跑多个任务,返回 `{task_name: value}`。重试粒度变成整个组,这是不把流水线做成 DAG 的代价。组是 Runner 看到的唯一规格,所以子任务们对 `resource`、`algorithm`、`timeout_s` 达成一致时,这些参数从子任务继承(此时 `timeout_s` 限制整个组)。
331
335
 
@@ -349,8 +353,8 @@ uv run pyattacker bench --algorithms wait,backoff --seeds 5 --json runs/bench.js
349
353
 
350
354
  | 文档 | 内容 |
351
355
  |---|---|
352
- | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/tutorial.md) | 17 个可运行步骤,从"一个任务"到分片评测和高级交接/反向遍历——回退、全量重试、载荷历史都有;每个步骤都有测试执行 |
353
- | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/reference.md) | 所有公开类和函数:签名、参数、示例——包括高级反向遍历和 `HistoryArtifact` |
356
+ | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/tutorial.md) | 17 个可运行步骤,从"一个任务"到分片评测、交接和反向遍历——回退、全量重试、载荷历史都有;每个步骤都有测试执行 |
357
+ | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/reference.md) | 所有公开类和函数:签名、参数、示例——包括反向遍历和 `HistoryArtifact` |
354
358
  | [`docs/cli.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/cli.md) | 每个子命令、每个参数、退出码、配置参考 |
355
359
  | [`docs/design.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/design.md) | 概念模型、六条不变量、租约契约、数据模型、设计权衡 |
356
360
  | [`docs/benchmark.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/benchmark.md) | 算法基准测试:场景假设了什么、指标怎么看、表格怎么读 |
@@ -381,7 +385,7 @@ uv run pyattacker run -c examples/qa_eval.yaml --limit 40
381
385
 
382
386
  * **网络请求**——openai/anthropic 的调用你自己写。内核从不碰 socket。
383
387
  * **指标计算**——准确率、pass@k、F1,以及任何跨流水线的汇总计算。把产物导出来在外面算,或者用原语自己拼一条汇总流水线。
384
- * **DAG 编排**——流水线就是一条直线链,要分支就在任务内部用 `fanout`。唯一例外是可选的[交接](README.zh-CN.md#进阶交接可选启用):它只是沿着声明的边改变链的遍历方式,拓扑不变(没有汇合节点、没有第二个入口、没有跨流水线跳转)。
388
+ * **DAG 编排**——流水线就是一条直线链,要分支就在任务内部用 `fanout`。唯一例外是可选的[交接](README.zh-CN.md#交接可选启用):它只是沿着声明的边改变链的遍历方式,拓扑不变(没有汇合节点、没有第二个入口、没有跨流水线跳转)。
385
389
  * **服务网关**——唯一的 HTTP 接口就是上面那个只读调试面板。
386
390
  * **分布式调度**——横向扩展用 `--shard`,多进程就是上限。
387
391
 
@@ -389,7 +393,17 @@ uv run pyattacker run -c examples/qa_eval.yaml --limit 40
389
393
 
390
394
  ## 当前状态
391
395
 
392
- **0.3.0——高级控制流(可选),以及中文文档。** 任务现在可以做[交接](README.zh-CN.md#进阶交接可选启用):返回一个 `Handoff` 跳过声明的后续步骤或提前收尾,记录在持久化账本里,续跑时从账本接着走。需要手动开启,不开完全没影响——没有 `control` 块的流水线不写新数据,`spec_digest` 逐字节不变。1.0 之前标记为实验性。反向遍历现在支持声明式回退和全量重试,带 visits 和可选的载荷历史。整套文档也提供了简体中文版([`README.zh-CN.md`](README.zh-CN.md)、[`docs/zh-CN/`](https://github.com/Hazer-BJTU/pyattacker/tree/main/docs/zh-CN)),由 CI 保持同步。升级时有一个改名要知道:`MergedReport.events_total` 现在叫 `source_events_total`,因为它是合并报告里**唯一**不去重、按源库原始累加的计数(旧名字仍可作为废弃别名使用)。
396
+ **0.3.2——组合实验与正式控制流特性。** 独立子实验可共享资源池和总并发,统一运行、恢复和查看结果,
397
+ 并可按实验分目录存储。持久化的运行历史在恢复后仍保留此前的结果。交接、正向跳转、回退和全量重试
398
+ 作为正式公共特性维护,通过 `control` 显式声明后启用。
399
+ 详见 [Suite 指南](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/suites.md) 和
400
+ [发布说明](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21)。
401
+
402
+ **0.3.1——应用汇报的实时指标。** 应用可用 `Runner.report_metric()` 或 `TaskContext.report_metric()`
403
+ 把准确率等数值汇报到只读监控面板。完成回调提供已提交的流水线结果,指标计算仍由应用负责。
404
+ 面板和 `watch` 默认统一跟随同一个运行。参见 [`examples/live_metrics.py`](examples/live_metrics.py)。
405
+
406
+ **0.3.0——控制流(可选),以及中文文档。** 任务现在可以做[交接](README.zh-CN.md#交接可选启用):返回一个 `Handoff` 跳过声明的后续步骤或提前收尾,记录在持久化账本里,续跑时从账本接着走。需要手动开启,不开完全没影响——没有 `control` 块的流水线不写新数据,`spec_digest` 逐字节不变。交接与反向遍历在 0.3.0 发布时属于实验性特性,在 0.3.2 中提升为正式特性(见 [0.3.2 发布说明](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21))。反向遍历现在支持声明式回退和全量重试,带 visits 和可选的载荷历史。整套文档也提供了简体中文版([`README.zh-CN.md`](README.zh-CN.md)、[`docs/zh-CN/`](https://github.com/Hazer-BJTU/pyattacker/tree/main/docs/zh-CN)),由 CI 保持同步。升级时有一个改名要知道:`MergedReport.events_total` 现在叫 `source_events_total`,因为它是合并报告里**唯一**不去重、按源库原始累加的计数(旧名字仍可作为废弃别名使用)。
393
407
 
394
408
  **0.2.0——基准测试、更严格的身份校验、三处正确性修复。** 新增 `pyattacker bench`:一个模拟接口方的世界,用一组指标对比各获取算法,而不是一个加权分数([`docs/benchmark.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/zh-CN/benchmark.md))。新增 `Retrying.decide`:把重试决策暴露为策略上的方法。新增分页的整类读取(`pyattacker.store.iter_*`,由可选的 `PagedStore` 扩展提供),导出大存储不再需要全量加载。0.1.x 已经实现了 M0–M4 计划的全部内容:内核、持久化和任务级恢复、重试和错误分类、带 7 种获取算法的资源池、延迟延续和 write-behind 批处理、分片和合并报告、三种格式五种导出形状、入口点插件、外部产物后端、fan-out 辅助函数、HTTP 监控端点。
395
409
 
@@ -415,3 +429,7 @@ uv build # sdist + wheel
415
429
  ## 许可证
416
430
 
417
431
  MIT——见 [LICENSE](https://github.com/Hazer-BJTU/pyattacker/blob/main/LICENSE)。
432
+
433
+ ## 实验集合
434
+
435
+ 把已有配置文件组合到同一个 Runner,共享资源池和总并发。可选的按子实验分目录布局分别保存检查点、结果和文件产物;Suite 报告包含恢复前已完成的结果。参见 [Suite 指南](docs/zh-CN/suites.md) 和 [可运行配置](examples/suites/suite.json)。