ww-agentic-workflows 1.0.0.dev3__py3-none-any.whl
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.
- ww/__init__.py +18 -0
- ww/_bundled_extensions/ww/git/extension.py +1728 -0
- ww/action_execution.py +887 -0
- ww/actions/__init__.py +94 -0
- ww/actions/command.py +444 -0
- ww/actions/contracts.py +699 -0
- ww/actions/extension.py +197 -0
- ww/actions/mcp.py +84 -0
- ww/actions/prompt.py +74 -0
- ww/actions/skill.py +62 -0
- ww/actions/slash_command.py +63 -0
- ww/agents.py +151 -0
- ww/amendments.py +54 -0
- ww/artifacts.py +93 -0
- ww/assessments.py +181 -0
- ww/assets/__init__.py +2 -0
- ww/assets/agent_instructions.md +49 -0
- ww/assets/docs/examples.md +879 -0
- ww/assets/docs/features.md +4639 -0
- ww/assets/docs/specification.md +1876 -0
- ww/assets/noww_skill.md +11 -0
- ww/assets/workflows/catchall.yaml +26 -0
- ww/assets/workflows/onboarding.yaml +586 -0
- ww/assets/workflows/scriptize.yaml +130 -0
- ww/assets/ww-automate_skill.md +23 -0
- ww/assets/ww-deduce-feedback_skill.md +38 -0
- ww/assets/ww-feedback-rules_skill.md +48 -0
- ww/assets/ww-learn-project_skill.md +22 -0
- ww/assets/ww-refresh_skill.md +26 -0
- ww/assets/ww-rule_skill.md +83 -0
- ww/assets/ww-rules-from-artifacts_skill.md +22 -0
- ww/assets/ww-scriptize_skill.md +33 -0
- ww/assets/ww-setup_skill.md +94 -0
- ww/assets/ww-solve_skill.md +23 -0
- ww/assets/ww-suggest_skill.md +32 -0
- ww/assets/ww-wizard_skill.md +105 -0
- ww/assets/ww_skill.md +59 -0
- ww/assignments.py +283 -0
- ww/bootstrap.py +405 -0
- ww/builtin_workflows.py +215 -0
- ww/changes.py +225 -0
- ww/child_coordination.py +482 -0
- ww/children.py +106 -0
- ww/claude_permissions.py +115 -0
- ww/cli/__init__.py +7 -0
- ww/cli/__main__.py +6 -0
- ww/cli/audit.py +129 -0
- ww/cli/catalogs.py +131 -0
- ww/cli/discover.py +607 -0
- ww/cli/initialization.py +898 -0
- ww/cli/lookup.py +287 -0
- ww/cli/main.py +1768 -0
- ww/cli/parser.py +1200 -0
- ww/cli/prompts.py +217 -0
- ww/cli/updates.py +117 -0
- ww/completion_artifacts.py +156 -0
- ww/completion_inputs.py +39 -0
- ww/config/__init__.py +582 -0
- ww/config/actions.py +591 -0
- ww/config/composition.py +571 -0
- ww/config/rules.py +511 -0
- ww/config/steps.py +1220 -0
- ww/config/values.py +223 -0
- ww/config_files.py +191 -0
- ww/config_writes.py +264 -0
- ww/contracts.py +155 -0
- ww/control.py +41 -0
- ww/defaults.py +130 -0
- ww/design_docs.py +32 -0
- ww/discovery.py +104 -0
- ww/documents.py +217 -0
- ww/errors.py +18 -0
- ww/executable.py +43 -0
- ww/execution_models/__init__.py +64 -0
- ww/execution_models/construction.py +148 -0
- ww/execution_models/decoding.py +38 -0
- ww/execution_models/plan_codec.py +565 -0
- ww/execution_models/records.py +1206 -0
- ww/execution_models/runs.py +266 -0
- ww/extensions/__init__.py +40 -0
- ww/extensions/api.py +559 -0
- ww/extensions/registry.py +864 -0
- ww/extensions/store.py +78 -0
- ww/feedback.py +342 -0
- ww/handler_repairs.py +57 -0
- ww/hooks/__init__.py +40 -0
- ww/hooks/agents.py +380 -0
- ww/hooks/install.py +168 -0
- ww/hooks/notices.py +206 -0
- ww/hooks/records.py +209 -0
- ww/hooks/runtime.py +266 -0
- ww/hooks/transcripts.py +183 -0
- ww/inspect.py +896 -0
- ww/instructions/__init__.py +17 -0
- ww/instructions/builder.py +1682 -0
- ww/instructions/commands.py +335 -0
- ww/instructions/handoff.py +149 -0
- ww/instructions/models.py +686 -0
- ww/instructions/policy.py +219 -0
- ww/instructions/text.py +168 -0
- ww/interactions.py +187 -0
- ww/interpolation.py +37 -0
- ww/item_passes.py +167 -0
- ww/items.py +99 -0
- ww/locking.py +207 -0
- ww/metadata_publication.py +230 -0
- ww/onboarding.py +229 -0
- ww/open_work.py +236 -0
- ww/operations.py +193 -0
- ww/operator_ui/__init__.py +16 -0
- ww/operator_ui/page.html +351 -0
- ww/operator_ui/server.py +215 -0
- ww/operator_ui/session.py +389 -0
- ww/operator_ui/sheet.py +104 -0
- ww/operator_ui/view.py +109 -0
- ww/output.py +339 -0
- ww/output_adapters/__init__.py +12 -0
- ww/output_adapters/base.py +25 -0
- ww/output_adapters/json_adapter.py +37 -0
- ww/output_adapters/markdown.py +2293 -0
- ww/output_adapters/rule_pages.py +337 -0
- ww/output_adapters/terminal.py +21 -0
- ww/package_updates.py +167 -0
- ww/plan/__init__.py +38 -0
- ww/plan/actions.py +207 -0
- ww/plan/compiler.py +1492 -0
- ww/plan/constructs.py +456 -0
- ww/plan/models.py +665 -0
- ww/project_config.py +752 -0
- ww/recovery.py +401 -0
- ww/replanning.py +367 -0
- ww/results.py +77 -0
- ww/rule_checks.py +230 -0
- ww/rule_conversion.py +331 -0
- ww/rule_disputes.py +148 -0
- ww/rule_store.py +456 -0
- ww/rule_verification.py +714 -0
- ww/rule_views.py +447 -0
- ww/rule_writes.py +920 -0
- ww/run_coordination.py +158 -0
- ww/runtimes.py +105 -0
- ww/service.py +4405 -0
- ww/setup_apply.py +428 -0
- ww/step_values.py +20 -0
- ww/storage.py +447 -0
- ww/storage_adapters/__init__.py +36 -0
- ww/storage_adapters/base.py +540 -0
- ww/storage_adapters/filesystem.py +370 -0
- ww/storage_adapters/memory.py +195 -0
- ww/storage_adapters/project_metadata.py +69 -0
- ww/storage_adapters/task_document.py +484 -0
- ww/task_ids.py +114 -0
- ww/task_references.py +124 -0
- ww/transitions.py +1619 -0
- ww/updates.py +399 -0
- ww/upgrade.py +95 -0
- ww/validation.py +168 -0
- ww/variables.py +275 -0
- ww/workflow_config.py +854 -0
- ww/workflow_update.py +239 -0
- ww/workflow_validation.py +1260 -0
- ww/workspace.py +50 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
ww/workflow_config.py
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Normalized workflow-definition domain models.
|
|
3
|
+
|
|
4
|
+
These models describe configuration, not task progress. A later execution layer
|
|
5
|
+
consumes a compiled plan rather than independently deciding which hooks apply.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import TYPE_CHECKING, Literal, Protocol
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ww.actions import Commands, DefinedAction
|
|
18
|
+
|
|
19
|
+
from ww.contracts import (
|
|
20
|
+
HookFailure,
|
|
21
|
+
HookPhase,
|
|
22
|
+
HookScope,
|
|
23
|
+
ItemAssignment,
|
|
24
|
+
ItemOperation,
|
|
25
|
+
LoopAssignment,
|
|
26
|
+
StepRole,
|
|
27
|
+
)
|
|
28
|
+
from ww.operations import ChildLaunch, ChildWorkflowRun, WorkflowHandoff
|
|
29
|
+
from ww.workspace import Workdir
|
|
30
|
+
|
|
31
|
+
MetadataScope = Literal["task", "project"]
|
|
32
|
+
WorkflowConfigLevel = Literal["global", "project", "local"]
|
|
33
|
+
# A document also has the user scope: one file per user, in the user
|
|
34
|
+
# configuration directory, shared by every project.
|
|
35
|
+
DocumentScope = Literal["task", "project", "user"]
|
|
36
|
+
DOCUMENT_SCOPES = ("task", "project", "user")
|
|
37
|
+
# The project metadata namespace ww keeps its own state in, such as
|
|
38
|
+
# ``ww.setup.done``; no workflow may save into it.
|
|
39
|
+
WW_METADATA_NAMESPACE = "ww"
|
|
40
|
+
# The task ID in a task-scoped document ``path``.
|
|
41
|
+
TASK_ID_TOKEN = "{{ww.task.id}}"
|
|
42
|
+
# A saved metadata name, e.g. "pr.url" or "base-branch".
|
|
43
|
+
_SAVED_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_.-]*")
|
|
44
|
+
# A dotted saved metadata key, e.g. "github.owner"; "github..owner" does not
|
|
45
|
+
# match.
|
|
46
|
+
_SAVED_KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*")
|
|
47
|
+
# A document or item field name, e.g. "plan" or "due-date"; "2nd" does not
|
|
48
|
+
# match.
|
|
49
|
+
_FIELD_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*")
|
|
50
|
+
|
|
51
|
+
INIT_STEP_NAME = "init"
|
|
52
|
+
INIT_STEP_PROMPT = (
|
|
53
|
+
"Record the passed requirements for this task. Correct their grammar and "
|
|
54
|
+
"style while preserving their meaning. Do not analyze, reason about, or "
|
|
55
|
+
"plan the work; only save the requirements."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConfigurationLoader(Protocol):
|
|
60
|
+
"""A notation frontend that emits normalized workflow definitions."""
|
|
61
|
+
|
|
62
|
+
def __call__(self) -> WorkflowConfiguration:
|
|
63
|
+
"""Load definitions without assigning execution semantics."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class ProvidedVariable:
|
|
69
|
+
"""An input a handler asks its executor to provide."""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
description: str = ""
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, str]:
|
|
75
|
+
return {"name": self.name, "description": self.description}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class SavedMetadata:
|
|
80
|
+
"""A metadata value a handler must preserve."""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
key: str
|
|
84
|
+
description: str = ""
|
|
85
|
+
scope: MetadataScope = "task"
|
|
86
|
+
# An append key holds a list: each completion may add values, none is
|
|
87
|
+
# required, and repeats are dropped.
|
|
88
|
+
append: bool = False
|
|
89
|
+
# Read/roundtrip older plan snapshots with explicit stdout captures.
|
|
90
|
+
# New YAML saves infer the source from the handler and leave this unset.
|
|
91
|
+
source: str | None = None
|
|
92
|
+
|
|
93
|
+
def __post_init__(self) -> None:
|
|
94
|
+
if not isinstance(self.name, str) or not _SAVED_NAME.fullmatch(self.name):
|
|
95
|
+
raise ValueError(f"invalid saved metadata name: {self.name!r}")
|
|
96
|
+
if not isinstance(self.key, str) or not _SAVED_KEY.fullmatch(self.key):
|
|
97
|
+
raise ValueError(f"invalid saved metadata key: {self.key!r}")
|
|
98
|
+
if not isinstance(self.description, str):
|
|
99
|
+
raise ValueError("saved metadata description must be a string")
|
|
100
|
+
if self.scope not in {"task", "project"}:
|
|
101
|
+
raise ValueError(f"invalid saved metadata scope: {self.scope!r}")
|
|
102
|
+
if self.scope == "project" and (
|
|
103
|
+
self.key == WW_METADATA_NAMESPACE
|
|
104
|
+
or self.key.startswith(f"{WW_METADATA_NAMESPACE}.")
|
|
105
|
+
):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"project metadata under {WW_METADATA_NAMESPACE}. is ww's own "
|
|
108
|
+
"state, such as its onboarding; save under another path"
|
|
109
|
+
)
|
|
110
|
+
if self.source is not None and self.source != "stdout":
|
|
111
|
+
raise ValueError("saved metadata source must be stdout")
|
|
112
|
+
if type(self.append) is not bool:
|
|
113
|
+
raise ValueError("saved metadata append must be a bool")
|
|
114
|
+
|
|
115
|
+
def to_dict(self) -> dict[str, object]:
|
|
116
|
+
data: dict[str, object] = {
|
|
117
|
+
"name": self.name,
|
|
118
|
+
"key": self.key,
|
|
119
|
+
"description": self.description,
|
|
120
|
+
"scope": self.scope,
|
|
121
|
+
}
|
|
122
|
+
if self.source is not None:
|
|
123
|
+
data["source"] = self.source
|
|
124
|
+
if self.append:
|
|
125
|
+
data["append"] = True
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class DocumentDefinition:
|
|
131
|
+
"""A root-level durable document workflows read and update across runs.
|
|
132
|
+
|
|
133
|
+
The format is the workflow's business; ww only knows the document's
|
|
134
|
+
name, scope, file, and which step last updated it.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
name: str
|
|
138
|
+
description: str = ""
|
|
139
|
+
scope: DocumentScope = "task"
|
|
140
|
+
# An explicit file, relative to the project (or, for a task document, to
|
|
141
|
+
# the task's working directory when the run has one; for a user document,
|
|
142
|
+
# to the user configuration directory). ``{{ww.task.id}}`` is replaced in
|
|
143
|
+
# a task-scoped path. Omitted, the document lives under ``.ww``, or in
|
|
144
|
+
# the user configuration directory for the user scope.
|
|
145
|
+
path: str | None = None
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
if not isinstance(self.name, str) or not _FIELD_NAME.fullmatch(self.name):
|
|
149
|
+
raise ValueError(f"invalid document name: {self.name!r}")
|
|
150
|
+
if not isinstance(self.description, str):
|
|
151
|
+
raise ValueError("document description must be a string")
|
|
152
|
+
if self.scope not in DOCUMENT_SCOPES:
|
|
153
|
+
raise ValueError(f"invalid document scope: {self.scope!r}")
|
|
154
|
+
if self.path is not None:
|
|
155
|
+
if not isinstance(self.path, str) or not self.path.strip():
|
|
156
|
+
raise ValueError("document path must be a non-empty string")
|
|
157
|
+
parts = self.path.replace("\\", "/").split("/")
|
|
158
|
+
if self.path.startswith(("/", "\\")) or ".." in parts:
|
|
159
|
+
inside = (
|
|
160
|
+
"the user configuration directory"
|
|
161
|
+
if self.scope == "user"
|
|
162
|
+
else "the project"
|
|
163
|
+
)
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"document path must stay inside {inside}: {self.path!r}"
|
|
166
|
+
)
|
|
167
|
+
if self.scope != "task" and TASK_ID_TOKEN in self.path:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"a {self.scope} document path cannot use {{{{ww.task.id}}}}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def to_dict(self) -> dict[str, str]:
|
|
173
|
+
data = {"name": self.name, "description": self.description, "scope": self.scope}
|
|
174
|
+
if self.path is not None:
|
|
175
|
+
data["path"] = self.path
|
|
176
|
+
return data
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True)
|
|
180
|
+
class DocumentUpdate:
|
|
181
|
+
"""An agent-owned action's promise to create or update a document."""
|
|
182
|
+
|
|
183
|
+
name: str
|
|
184
|
+
instruction: str = ""
|
|
185
|
+
|
|
186
|
+
def __post_init__(self) -> None:
|
|
187
|
+
if not isinstance(self.name, str) or not _FIELD_NAME.fullmatch(self.name):
|
|
188
|
+
raise ValueError(f"invalid document update name: {self.name!r}")
|
|
189
|
+
if not isinstance(self.instruction, str):
|
|
190
|
+
raise ValueError("document update instruction must be a string")
|
|
191
|
+
|
|
192
|
+
def to_dict(self) -> dict[str, str]:
|
|
193
|
+
return {"name": self.name, "instruction": self.instruction}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(frozen=True)
|
|
197
|
+
class ChoiceDefinition:
|
|
198
|
+
"""One option an interactive step offers the operator."""
|
|
199
|
+
|
|
200
|
+
label: str
|
|
201
|
+
description: str = ""
|
|
202
|
+
|
|
203
|
+
def __post_init__(self) -> None:
|
|
204
|
+
if not isinstance(self.label, str) or not self.label.strip():
|
|
205
|
+
raise ValueError("choice label must be a non-empty string")
|
|
206
|
+
if not isinstance(self.description, str):
|
|
207
|
+
raise ValueError("choice description must be a string")
|
|
208
|
+
|
|
209
|
+
def to_dict(self) -> dict[str, str]:
|
|
210
|
+
return {"label": self.label, "description": self.description}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True)
|
|
214
|
+
class ModeDefinition:
|
|
215
|
+
"""A root mode: guidance delivered on the pages of the steps it covers.
|
|
216
|
+
|
|
217
|
+
A mode applies where it is selected (``start --mode`` or the workflow's
|
|
218
|
+
default modes). ``workflows`` and ``steps`` are its filters: a mode with
|
|
219
|
+
either one also applies automatically wherever it matches; ``None`` means
|
|
220
|
+
the key is absent. On a mode, as on a hook, ``[]`` admits every name.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
name: str
|
|
224
|
+
description: tuple[str, ...] = ()
|
|
225
|
+
workflows: NameFilter | None = None
|
|
226
|
+
steps: NameFilter | None = None
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def automatic(self) -> bool:
|
|
230
|
+
return self.workflows is not None or self.steps is not None
|
|
231
|
+
|
|
232
|
+
def applies_to(
|
|
233
|
+
self,
|
|
234
|
+
workflow_name: str,
|
|
235
|
+
step_name: str,
|
|
236
|
+
step_path: str,
|
|
237
|
+
precise_step_paths: frozenset[str] = frozenset(),
|
|
238
|
+
) -> bool:
|
|
239
|
+
"""Whether this mode applies automatically to one step of one workflow."""
|
|
240
|
+
return self.automatic and step_filter_matches(
|
|
241
|
+
self.workflows or ALL,
|
|
242
|
+
self.steps or ALL,
|
|
243
|
+
workflow_name,
|
|
244
|
+
step_name,
|
|
245
|
+
step_path,
|
|
246
|
+
precise_step_paths,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def to_dict(self) -> dict[str, object]:
|
|
250
|
+
data: dict[str, object] = {
|
|
251
|
+
"name": self.name,
|
|
252
|
+
"description": list(self.description),
|
|
253
|
+
}
|
|
254
|
+
if self.workflows is not None:
|
|
255
|
+
data["workflows"] = self.workflows.to_data()
|
|
256
|
+
if self.steps is not None:
|
|
257
|
+
data["steps"] = self.steps.to_data()
|
|
258
|
+
return data
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True)
|
|
262
|
+
class ProfileDefinition:
|
|
263
|
+
"""A root-level profile and its optional inline instruction."""
|
|
264
|
+
|
|
265
|
+
name: str
|
|
266
|
+
description: str | None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass(frozen=True)
|
|
270
|
+
class ItemFieldUpdate:
|
|
271
|
+
"""A promise to set a custom field on an item, by an agent or a command.
|
|
272
|
+
|
|
273
|
+
On a per-item stage the stage's item must carry the field when the stage
|
|
274
|
+
completes; on the collection step every collected item must.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
name: str
|
|
278
|
+
description: str = ""
|
|
279
|
+
|
|
280
|
+
def __post_init__(self) -> None:
|
|
281
|
+
if not isinstance(self.name, str) or not _FIELD_NAME.fullmatch(self.name):
|
|
282
|
+
raise ValueError(f"invalid item field name: {self.name!r}")
|
|
283
|
+
if not isinstance(self.description, str):
|
|
284
|
+
raise ValueError("item field description must be a string")
|
|
285
|
+
|
|
286
|
+
def to_dict(self) -> dict[str, str]:
|
|
287
|
+
return {"name": self.name, "description": self.description}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass(frozen=True)
|
|
291
|
+
class HandlerDefinition:
|
|
292
|
+
"""Shared policy around a typed action or implicit name reference."""
|
|
293
|
+
|
|
294
|
+
name: str
|
|
295
|
+
description: str = ""
|
|
296
|
+
action: DefinedAction | None = None
|
|
297
|
+
# Core structural operation. Unlike ``action``, it never enters the
|
|
298
|
+
# ordinary action registry.
|
|
299
|
+
operation: WorkflowHandoff | ChildWorkflowRun | None = None
|
|
300
|
+
provide: tuple[ProvidedVariable, ...] = ()
|
|
301
|
+
save_metadata: tuple[SavedMetadata, ...] = ()
|
|
302
|
+
update_document: tuple[DocumentUpdate, ...] = ()
|
|
303
|
+
update_item: tuple[ItemFieldUpdate, ...] = ()
|
|
304
|
+
outputs: tuple[str, ...] = ()
|
|
305
|
+
agent: str | None = None
|
|
306
|
+
model: str | None = None
|
|
307
|
+
reasoning: str | None = None
|
|
308
|
+
# The directory this handler or step works in; ``None`` leaves the
|
|
309
|
+
# choice to the enclosing step (for a step) or the task workspace.
|
|
310
|
+
workdir: Workdir | None = None
|
|
311
|
+
# ``args`` of a reference to an extension handler: the positional
|
|
312
|
+
# arguments it is run with, templates allowed.
|
|
313
|
+
extension_arguments: tuple[str, ...] = ()
|
|
314
|
+
on_failure: HookFailure | None = None
|
|
315
|
+
on_failure_instruction: str | None = None
|
|
316
|
+
# An ordered group of ww-owned actions, sharing a logical lifecycle.
|
|
317
|
+
handlers: tuple[HandlerDefinition, ...] = ()
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def is_reference(self) -> bool:
|
|
321
|
+
"""Whether this only names a root or extension handler to run.
|
|
322
|
+
|
|
323
|
+
With no action, description, or values of its own, a hook or handler
|
|
324
|
+
entry such as ``- update-readme: ~`` stands for the handler it names.
|
|
325
|
+
"""
|
|
326
|
+
return not (
|
|
327
|
+
self.action is not None
|
|
328
|
+
or self.handlers
|
|
329
|
+
or self.operation is not None
|
|
330
|
+
or self.description
|
|
331
|
+
or self.provide
|
|
332
|
+
or self.save_metadata
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
ALL_NAMES = "*"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@dataclass(frozen=True)
|
|
340
|
+
class NameFilter:
|
|
341
|
+
"""The names a ``workflows`` or ``steps`` filter admits.
|
|
342
|
+
|
|
343
|
+
``names`` is ``None`` when every name is admitted (``"*"``, or an omitted
|
|
344
|
+
key); otherwise only the names listed, and none when it is empty. The
|
|
345
|
+
configuration writes it as ``"*"`` or a list of names.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
names: tuple[str, ...] | None = None
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def of(cls, names: Iterable[str]) -> NameFilter:
|
|
352
|
+
return cls(tuple(names))
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def admits_all(self) -> bool:
|
|
356
|
+
return self.names is None
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def admits_none(self) -> bool:
|
|
360
|
+
return self.names == ()
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def listed(self) -> tuple[str, ...]:
|
|
364
|
+
"""The names listed; empty when the filter admits every name."""
|
|
365
|
+
return self.names or ()
|
|
366
|
+
|
|
367
|
+
def admits(self, name: str) -> bool:
|
|
368
|
+
return self.names is None or name in self.names
|
|
369
|
+
|
|
370
|
+
def to_data(self) -> str | list[str]:
|
|
371
|
+
"""``"*"`` for every name, else the list of names."""
|
|
372
|
+
return ALL_NAMES if self.names is None else list(self.names)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
ALL = NameFilter()
|
|
376
|
+
# Admits no name: a rule group with ``[]`` applies only where a step names it.
|
|
377
|
+
NO_NAMES = NameFilter(())
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def step_filter_matches(
|
|
381
|
+
workflows: NameFilter,
|
|
382
|
+
steps: NameFilter,
|
|
383
|
+
workflow_name: str,
|
|
384
|
+
step_name: str,
|
|
385
|
+
step_path: str,
|
|
386
|
+
precise_step_paths: frozenset[str] = frozenset(),
|
|
387
|
+
) -> bool:
|
|
388
|
+
"""Whether ``workflows``/``steps`` filters admit one step of one workflow.
|
|
389
|
+
|
|
390
|
+
A step selector that is a precise logical path matches that path, any
|
|
391
|
+
other matches the step's own name. Hooks and rule groups share this
|
|
392
|
+
matching so a filter means the same in both.
|
|
393
|
+
"""
|
|
394
|
+
logical_path = step_path.replace("/{item}", "").replace("/{child}", "")
|
|
395
|
+
step_matches = steps.names is None or any(
|
|
396
|
+
logical_path == selector
|
|
397
|
+
if selector in precise_step_paths
|
|
398
|
+
else step_name == selector
|
|
399
|
+
for selector in steps.names
|
|
400
|
+
)
|
|
401
|
+
return workflows.admits(workflow_name) and step_matches
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@dataclass(frozen=True)
|
|
405
|
+
class HookDefinition:
|
|
406
|
+
phase: HookPhase
|
|
407
|
+
handler: HandlerDefinition
|
|
408
|
+
# A hook's ``[]`` admits every name, like an omitted key.
|
|
409
|
+
workflows: NameFilter = ALL
|
|
410
|
+
steps: NameFilter = ALL
|
|
411
|
+
scope: HookScope = "global"
|
|
412
|
+
path: str = ""
|
|
413
|
+
# ``fix`` turns a failed ``before_complete`` hook into a rejected
|
|
414
|
+
# completion the step's worker fixes, instead of an operator stop.
|
|
415
|
+
on_failure: HookFailure = "operator"
|
|
416
|
+
|
|
417
|
+
def applies_to(
|
|
418
|
+
self,
|
|
419
|
+
workflow_name: str,
|
|
420
|
+
step_name: str,
|
|
421
|
+
step_path: str,
|
|
422
|
+
precise_step_paths: frozenset[str] = frozenset(),
|
|
423
|
+
) -> bool:
|
|
424
|
+
return step_filter_matches(
|
|
425
|
+
self.workflows,
|
|
426
|
+
self.steps,
|
|
427
|
+
workflow_name,
|
|
428
|
+
step_name,
|
|
429
|
+
step_path,
|
|
430
|
+
precise_step_paths,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def applies_in(
|
|
434
|
+
self,
|
|
435
|
+
workflow: WorkflowDefinition,
|
|
436
|
+
step_name: str,
|
|
437
|
+
step_path: str,
|
|
438
|
+
precise_step_paths: frozenset[str] = frozenset(),
|
|
439
|
+
) -> bool:
|
|
440
|
+
"""Whether this hook runs for one step of ``workflow``.
|
|
441
|
+
|
|
442
|
+
A hook filtered with ``workflows`` applies when the filter admits the
|
|
443
|
+
workflow's own name or the lane it takes its hooks from
|
|
444
|
+
(``hooks_from``), so a workflow with a lane keeps the hooks written
|
|
445
|
+
for itself.
|
|
446
|
+
"""
|
|
447
|
+
return any(
|
|
448
|
+
self.applies_to(name, step_name, step_path, precise_step_paths)
|
|
449
|
+
for name in dict.fromkeys((workflow.name, workflow.lane))
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclass(frozen=True)
|
|
454
|
+
class RuleHints:
|
|
455
|
+
"""The worker a rule asks to be judged by: agent, model, and reasoning.
|
|
456
|
+
|
|
457
|
+
Each field is ``None`` when the rule leaves it to its group or the step.
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
agent: str | None = None
|
|
461
|
+
model: str | None = None
|
|
462
|
+
reasoning: str | None = None
|
|
463
|
+
|
|
464
|
+
def overlay(self, other: RuleHints) -> RuleHints:
|
|
465
|
+
"""These hints with every field ``other`` sets replacing this one's."""
|
|
466
|
+
return RuleHints(
|
|
467
|
+
other.agent if other.agent is not None else self.agent,
|
|
468
|
+
other.model if other.model is not None else self.model,
|
|
469
|
+
other.reasoning if other.reasoning is not None else self.reasoning,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def to_dict(self) -> dict[str, str]:
|
|
473
|
+
return {
|
|
474
|
+
name: value
|
|
475
|
+
for name, value in (
|
|
476
|
+
("agent", self.agent),
|
|
477
|
+
("model", self.model),
|
|
478
|
+
("reasoning", self.reasoning),
|
|
479
|
+
)
|
|
480
|
+
if value is not None
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@dataclass(frozen=True)
|
|
485
|
+
class RuleDefinition:
|
|
486
|
+
"""One rule: text the step's agent follows, and optionally its check.
|
|
487
|
+
|
|
488
|
+
``id`` is ``<group>/<file stem>`` for a rule reached through a group and
|
|
489
|
+
``<step>/<ordinal>`` or ``<step>/<file stem>`` for a step's own entry.
|
|
490
|
+
``text_hash`` identifies the wording regardless of whitespace, so derived
|
|
491
|
+
knowledge about a rule follows its text rather than its file.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
id: str
|
|
495
|
+
text: str
|
|
496
|
+
summary: str
|
|
497
|
+
text_hash: str
|
|
498
|
+
paths: tuple[str, ...] = ()
|
|
499
|
+
check: Commands | None = None
|
|
500
|
+
max_fixes: int | None = None
|
|
501
|
+
hints: RuleHints = RuleHints()
|
|
502
|
+
# The rule file, or ``None`` for a rule written inline in YAML.
|
|
503
|
+
source: str | None = None
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@dataclass(frozen=True)
|
|
507
|
+
class RuleGroupRef:
|
|
508
|
+
"""A step's reference to a root rule group by name."""
|
|
509
|
+
|
|
510
|
+
name: str
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@dataclass(frozen=True)
|
|
514
|
+
class UnresolvedStepRule:
|
|
515
|
+
"""A bare string in a step's ``rules`` list, before the parser resolves it.
|
|
516
|
+
|
|
517
|
+
It becomes a group reference, the rules of a file or directory, or the
|
|
518
|
+
literal text of a rule; no normalized configuration keeps one.
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
value: str
|
|
522
|
+
step: str
|
|
523
|
+
ordinal: int
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
StepRule = RuleDefinition | RuleGroupRef | UnresolvedStepRule
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@dataclass(frozen=True)
|
|
530
|
+
class RuleGroup:
|
|
531
|
+
"""A named set of rules and where they apply on their own.
|
|
532
|
+
|
|
533
|
+
``workflows`` and ``steps`` filter like a global hook's, except that an
|
|
534
|
+
empty filter admits none: such a group applies only where a step names it.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
name: str
|
|
538
|
+
rules: tuple[RuleDefinition, ...] = ()
|
|
539
|
+
workflows: NameFilter = ALL
|
|
540
|
+
steps: NameFilter = ALL
|
|
541
|
+
hints: RuleHints = RuleHints()
|
|
542
|
+
# Where the group was declared: the YAML, or the extension that ships it.
|
|
543
|
+
origin: str = "configuration"
|
|
544
|
+
|
|
545
|
+
def applies_to(
|
|
546
|
+
self,
|
|
547
|
+
workflow_name: str,
|
|
548
|
+
step_name: str,
|
|
549
|
+
step_path: str,
|
|
550
|
+
precise_step_paths: frozenset[str] = frozenset(),
|
|
551
|
+
) -> bool:
|
|
552
|
+
return step_filter_matches(
|
|
553
|
+
self.workflows,
|
|
554
|
+
self.steps,
|
|
555
|
+
workflow_name,
|
|
556
|
+
step_name,
|
|
557
|
+
step_path,
|
|
558
|
+
precise_step_paths,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@dataclass(frozen=True)
|
|
563
|
+
class StepDefinition(HandlerDefinition):
|
|
564
|
+
# Who performs the step: ``manager`` in its own session, ``worker`` when
|
|
565
|
+
# delegated. ``None`` inherits along the step chain, like ``profile``.
|
|
566
|
+
role: StepRole | None = None
|
|
567
|
+
# ``False``: whoever performs the step spawns no subagents for anything.
|
|
568
|
+
# ``None`` inherits along the step chain, like ``profile``.
|
|
569
|
+
subagents: bool | None = None
|
|
570
|
+
# A conversation with the operator, held by the session that can talk to
|
|
571
|
+
# them; implies ``role: manager``.
|
|
572
|
+
interactive: bool = False
|
|
573
|
+
# Describe each operation and show concrete edits when enabled. ``None``
|
|
574
|
+
# inherits from the enclosing workflow or structural step.
|
|
575
|
+
explicit: bool | None = None
|
|
576
|
+
# Opt-in artifact source for feedback deduction after workflow completion.
|
|
577
|
+
learnable: bool = False
|
|
578
|
+
# The options the operator chooses from during an interactive step.
|
|
579
|
+
choices: tuple[ChoiceDefinition, ...] = ()
|
|
580
|
+
# The operator answers this per-item stage on the operator page.
|
|
581
|
+
ui: bool = False
|
|
582
|
+
profile: str | None = None
|
|
583
|
+
profile_description: str | None = None
|
|
584
|
+
hooks: tuple[HookDefinition, ...] = ()
|
|
585
|
+
# The step's own rules, and the root groups it names, in declaration order.
|
|
586
|
+
rules: tuple[StepRule, ...] = ()
|
|
587
|
+
# The compiler flattens nested steps while preserving their parent identity.
|
|
588
|
+
child_steps: tuple[StepDefinition, ...] = ()
|
|
589
|
+
loop_steps: tuple[StepDefinition, ...] = ()
|
|
590
|
+
max_rounds: int | None = None
|
|
591
|
+
loop_assignment: LoopAssignment | None = None
|
|
592
|
+
loop_break: str | None = None
|
|
593
|
+
loop_continue: str | None = None
|
|
594
|
+
# A step with ``items`` collects work items, then runs ``items.steps``
|
|
595
|
+
# once for every collected item.
|
|
596
|
+
items: ItemFlow | None = None
|
|
597
|
+
item_operation: ItemOperation | None = None
|
|
598
|
+
# ``start_child`` on the stage that runs a child: ww starts it itself.
|
|
599
|
+
child_launch: ChildLaunch | None = None
|
|
600
|
+
artifact: bool = True
|
|
601
|
+
# A step with ``children`` collects child tasks, then runs each with
|
|
602
|
+
# ``children.workflow``, or runs ``children.steps`` once per child.
|
|
603
|
+
children: ChildFlow | None = None
|
|
604
|
+
artifact_dependency: str | None = None
|
|
605
|
+
# An assessment is an agent prompt whose named outcome selects a conditional
|
|
606
|
+
# subtree. Empty outcomes use the compact positive/negative continuation.
|
|
607
|
+
assessment_question: str | None = None
|
|
608
|
+
assessment_outcomes: tuple[StepDefinition, ...] = ()
|
|
609
|
+
# An assessment outcome that ends the workflow instead of running steps.
|
|
610
|
+
stop_workflow: bool = False
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@dataclass(frozen=True)
|
|
614
|
+
class ChildFlow:
|
|
615
|
+
"""The child tasks owned by one ``children`` step.
|
|
616
|
+
|
|
617
|
+
The step's own action collects them with ``add-child``. Without
|
|
618
|
+
``steps`` ww then runs every child, one at a time, with ``workflow``, and
|
|
619
|
+
the parent continues after the last one completes. With ``steps`` the
|
|
620
|
+
parent runs those stages once per child, one child at a time; exactly one
|
|
621
|
+
of them runs the child task with ``workflow`` and waits for it.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
workflow: str
|
|
625
|
+
# Splitting guidance for the collecting agent, as ``items.description``.
|
|
626
|
+
description: str | None = None
|
|
627
|
+
# The parent's stages per child; the one running the child carries a
|
|
628
|
+
# ``ChildWorkflowRun`` operation. Empty in the simple form.
|
|
629
|
+
steps: tuple[StepDefinition, ...] = ()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@dataclass(frozen=True)
|
|
633
|
+
class ItemFlow:
|
|
634
|
+
"""The collection and per-item lifecycle owned by one ``items`` step.
|
|
635
|
+
|
|
636
|
+
``steps`` are the resolved per-item stages: the configured ones, or the
|
|
637
|
+
single built-in stage of a bare ``items: ~``. Empty ``steps`` collect
|
|
638
|
+
items without processing them. Item-flow worker settings are already
|
|
639
|
+
folded into each stage while parsing.
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
steps: tuple[StepDefinition, ...] = ()
|
|
643
|
+
description: str | None = None
|
|
644
|
+
assignment: ItemAssignment = "together"
|
|
645
|
+
# Collection-wide settings, exactly as this declaration wrote them.
|
|
646
|
+
# ``None`` means it did not set one: the first declaration of a workflow
|
|
647
|
+
# establishes them, so a later declaration that omits one must not be
|
|
648
|
+
# read as choosing the default. ``identity`` is folded into the unique
|
|
649
|
+
# pool only by ``effective_unique``, so an explicit ``unique`` stays
|
|
650
|
+
# distinguishable from an omitted one.
|
|
651
|
+
#
|
|
652
|
+
# ``persistent``: the items outlive the run: every run of the task reuses
|
|
653
|
+
# them, and the collection stage reconciles them instead of splitting anew.
|
|
654
|
+
persistent: bool | None = None
|
|
655
|
+
# The custom field a new item must carry, and the fields whose values
|
|
656
|
+
# form one pool in which each value may appear once across all items.
|
|
657
|
+
identity: str | None = None
|
|
658
|
+
unique: tuple[str, ...] | None = None
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def effective_unique(self) -> tuple[str, ...]:
|
|
662
|
+
"""The unique pool a run applies: ``identity`` first, then ``unique``."""
|
|
663
|
+
return tuple(
|
|
664
|
+
dict.fromkeys(
|
|
665
|
+
(*((self.identity,) if self.identity else ()), *(self.unique or ()))
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
@property
|
|
670
|
+
def collect_only(self) -> bool:
|
|
671
|
+
"""Whether this pass only collects or reconciles items.
|
|
672
|
+
|
|
673
|
+
Explicit ``steps: []`` (or ``steps: ~``) is the one way to be true; the
|
|
674
|
+
bare ``items: ~`` shorthand resolves to the built-in handle-item stage.
|
|
675
|
+
"""
|
|
676
|
+
return not self.steps
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@dataclass(frozen=True)
|
|
680
|
+
class WorkflowDefinition:
|
|
681
|
+
name: str
|
|
682
|
+
description: str = ""
|
|
683
|
+
steps: tuple[StepDefinition, ...] = ()
|
|
684
|
+
hooks: tuple[HookDefinition, ...] = ()
|
|
685
|
+
modes: tuple[str, ...] = ()
|
|
686
|
+
agent: str | None = None
|
|
687
|
+
model: str | None = None
|
|
688
|
+
reasoning: str | None = None
|
|
689
|
+
profile: str | None = None
|
|
690
|
+
profile_description: str | None = None
|
|
691
|
+
# The role every step of the workflow inherits unless it sets its own.
|
|
692
|
+
role: StepRole | None = None
|
|
693
|
+
# Whether the performers of its steps may spawn subagents, inherited.
|
|
694
|
+
subagents: bool | None = None
|
|
695
|
+
# Default visibility guidance for steps; individual steps can override it.
|
|
696
|
+
explicit: bool | None = None
|
|
697
|
+
# The runtime ``start`` uses for this workflow when ``--runtime`` is
|
|
698
|
+
# omitted; it outranks the project default, and the flag outranks it.
|
|
699
|
+
runtime: str | None = None
|
|
700
|
+
# A new start while this workflow's previous run is unfinished abandons
|
|
701
|
+
# that run instead of being refused.
|
|
702
|
+
restartable: bool = False
|
|
703
|
+
# The workflow this one copies, recorded for display; the definition is
|
|
704
|
+
# already complete, so nothing downstream resolves it again.
|
|
705
|
+
inherits: str | None = None
|
|
706
|
+
# A workflow to offer the operator once this one completes.
|
|
707
|
+
recommended_next_workflow: str | None = None
|
|
708
|
+
# The workflow whose global hooks this one runs with: a hook filtered to
|
|
709
|
+
# ``workflows: [task]`` applies here too when this names ``task``, so a
|
|
710
|
+
# workflow can take a lane's branch, worktree and commit handling.
|
|
711
|
+
hooks_from: str | None = None
|
|
712
|
+
# ``start`` refuses until ``hooks_from`` is set, for a workflow that must
|
|
713
|
+
# not run without the project's lane handling (a built-in that installs
|
|
714
|
+
# tools and writes configuration, for example).
|
|
715
|
+
needs_hooks_from: bool = False
|
|
716
|
+
|
|
717
|
+
@property
|
|
718
|
+
def lane(self) -> str:
|
|
719
|
+
"""The workflow whose project handling this one takes.
|
|
720
|
+
|
|
721
|
+
Its ``hooks_from`` when it has one, else itself: global hooks filtered
|
|
722
|
+
to the lane apply here, and extensions key their workflow settings
|
|
723
|
+
(a branch format, a base branch) by it.
|
|
724
|
+
"""
|
|
725
|
+
return self.hooks_from or self.name
|
|
726
|
+
|
|
727
|
+
@property
|
|
728
|
+
def hands_off(self) -> bool:
|
|
729
|
+
"""Whether the workflow ends by handing the task to another workflow.
|
|
730
|
+
|
|
731
|
+
The transition itself declares it: a ``handoff_to:`` step, or a
|
|
732
|
+
``handoff_to:`` hook on a step. Validation places it at the end.
|
|
733
|
+
"""
|
|
734
|
+
return any(
|
|
735
|
+
isinstance(step.operation, WorkflowHandoff)
|
|
736
|
+
or any(
|
|
737
|
+
isinstance(hook.handler.operation, WorkflowHandoff)
|
|
738
|
+
for hook in step.hooks
|
|
739
|
+
)
|
|
740
|
+
for step in step_tree(self.steps)
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
@dataclass(frozen=True)
|
|
745
|
+
class WorkflowProvenance:
|
|
746
|
+
"""The physical configured source and level that defined a workflow."""
|
|
747
|
+
|
|
748
|
+
source: str
|
|
749
|
+
level: WorkflowConfigLevel
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def binds_task_identity(workflow: WorkflowDefinition) -> bool:
|
|
753
|
+
"""Whether a workflow's first step supplies the task's external ID.
|
|
754
|
+
|
|
755
|
+
Such a workflow started without an ID, or run for a child added without
|
|
756
|
+
one, first executes that step as an identity request and binds the task
|
|
757
|
+
to the ID it provides.
|
|
758
|
+
"""
|
|
759
|
+
return bool(workflow.steps) and any(
|
|
760
|
+
value.name == "task_id" for value in workflow.steps[0].provide
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def delegation_requests(workflow: WorkflowDefinition) -> tuple[str, ...]:
|
|
765
|
+
"""Names of the steps in ``workflow`` that ask for a particular worker.
|
|
766
|
+
|
|
767
|
+
A declared ``agent``, ``model``, ``reasoning``, or ``profile`` is a request
|
|
768
|
+
for who should perform the work. Only the ``auto`` runtime can act on one,
|
|
769
|
+
because only it delegates; ``single`` keeps the request on the plan and
|
|
770
|
+
performs the step in the caller's own session. A workflow that carries
|
|
771
|
+
these is therefore written for ``auto``, and saying so lets ``discover``
|
|
772
|
+
point that out rather than leaving the reader to infer it.
|
|
773
|
+
"""
|
|
774
|
+
requested: list[str] = []
|
|
775
|
+
if _requests_worker(workflow):
|
|
776
|
+
requested.append(workflow.name)
|
|
777
|
+
for step in step_tree(workflow.steps):
|
|
778
|
+
if _requests_worker(step) and step.name not in requested:
|
|
779
|
+
requested.append(step.name)
|
|
780
|
+
return tuple(requested)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _requests_worker(definition: object) -> bool:
|
|
784
|
+
return any(
|
|
785
|
+
getattr(definition, field, None)
|
|
786
|
+
for field in ("agent", "model", "reasoning", "profile")
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def step_tree(steps: Iterable[StepDefinition]) -> Iterator[StepDefinition]:
|
|
791
|
+
"""Walk a step tree: nested steps, loop bodies, assessments, item and
|
|
792
|
+
per-child stages."""
|
|
793
|
+
for step in steps:
|
|
794
|
+
yield step
|
|
795
|
+
yield from step_tree(step.child_steps)
|
|
796
|
+
yield from step_tree(step.loop_steps)
|
|
797
|
+
yield from step_tree(step.assessment_outcomes)
|
|
798
|
+
if step.items is not None:
|
|
799
|
+
yield from step_tree(step.items.steps)
|
|
800
|
+
if step.children is not None:
|
|
801
|
+
yield from step_tree(step.children.steps)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@dataclass(frozen=True)
|
|
805
|
+
class WorkflowConfiguration:
|
|
806
|
+
modes: tuple[ModeDefinition, ...]
|
|
807
|
+
profiles: tuple[ProfileDefinition, ...]
|
|
808
|
+
handlers: tuple[HandlerDefinition, ...]
|
|
809
|
+
global_hooks: tuple[HookDefinition, ...]
|
|
810
|
+
workflows: tuple[WorkflowDefinition, ...]
|
|
811
|
+
documents: tuple[DocumentDefinition, ...] = ()
|
|
812
|
+
# Root rule groups, extension groups first, then YAML order.
|
|
813
|
+
rule_groups: tuple[RuleGroup, ...] = ()
|
|
814
|
+
# Effective YAML workflow definitions only. Built-ins and extension
|
|
815
|
+
# contributions have no configured source and are absent from this map.
|
|
816
|
+
workflow_provenance: Mapping[str, WorkflowProvenance] = field(
|
|
817
|
+
default_factory=lambda: MappingProxyType({})
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
def __post_init__(self) -> None:
|
|
821
|
+
object.__setattr__(
|
|
822
|
+
self,
|
|
823
|
+
"workflow_provenance",
|
|
824
|
+
MappingProxyType(dict(self.workflow_provenance)),
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
@property
|
|
828
|
+
def rule_groups_by_name(self) -> dict[str, RuleGroup]:
|
|
829
|
+
return {group.name: group for group in self.rule_groups}
|
|
830
|
+
|
|
831
|
+
@property
|
|
832
|
+
def documents_by_name(self) -> dict[str, DocumentDefinition]:
|
|
833
|
+
return {document.name: document for document in self.documents}
|
|
834
|
+
|
|
835
|
+
@property
|
|
836
|
+
def handlers_by_name(self) -> dict[str, HandlerDefinition]:
|
|
837
|
+
return {handler.name: handler for handler in self.handlers}
|
|
838
|
+
|
|
839
|
+
@property
|
|
840
|
+
def profiles_by_name(self) -> dict[str, ProfileDefinition]:
|
|
841
|
+
return {profile.name: profile for profile in self.profiles}
|
|
842
|
+
|
|
843
|
+
@property
|
|
844
|
+
def workflows_by_name(self) -> dict[str, WorkflowDefinition]:
|
|
845
|
+
return {workflow.name: workflow for workflow in self.workflows}
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def every_step(configuration: WorkflowConfiguration) -> Iterator[StepDefinition]:
|
|
849
|
+
"""Every step of every workflow and of every step-shaped root handler."""
|
|
850
|
+
for workflow in configuration.workflows:
|
|
851
|
+
yield from step_tree(workflow.steps)
|
|
852
|
+
for handler in configuration.handlers:
|
|
853
|
+
if isinstance(handler, StepDefinition):
|
|
854
|
+
yield from step_tree((handler,))
|