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/plan/models.py
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Immutable plan data and serialization."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from typing import TypeVar, get_args
|
|
8
|
+
|
|
9
|
+
from ww.actions import Commands, PlannedAction, actions
|
|
10
|
+
from ww.contracts import (
|
|
11
|
+
CheckSource,
|
|
12
|
+
ChildOperation,
|
|
13
|
+
ExecutionKind,
|
|
14
|
+
ItemAssignment,
|
|
15
|
+
ItemOperation,
|
|
16
|
+
LoopAssignment,
|
|
17
|
+
PlanItemKind,
|
|
18
|
+
PlanItemOwner,
|
|
19
|
+
PlanItemPhase,
|
|
20
|
+
StepRole,
|
|
21
|
+
)
|
|
22
|
+
from ww.operations import LoopBoundary, PlanOperation, encode_operation
|
|
23
|
+
from ww.validation import is_positive_int
|
|
24
|
+
from ww.workflow_config import (
|
|
25
|
+
ChoiceDefinition,
|
|
26
|
+
DocumentDefinition,
|
|
27
|
+
DocumentUpdate,
|
|
28
|
+
ItemFieldUpdate,
|
|
29
|
+
ProvidedVariable,
|
|
30
|
+
RuleHints,
|
|
31
|
+
SavedMetadata,
|
|
32
|
+
)
|
|
33
|
+
from ww.workspace import WORKDIRS, Workdir
|
|
34
|
+
|
|
35
|
+
PayloadT = TypeVar("PayloadT")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class PlannedMode:
|
|
40
|
+
"""A mode frozen into the plan for one agent step.
|
|
41
|
+
|
|
42
|
+
``automatic`` marks a mode the step gets from the mode's own filters
|
|
43
|
+
rather than from the run's selected modes. The description is frozen so
|
|
44
|
+
a run keeps delivering the guidance it started with.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
description: tuple[str, ...] = ()
|
|
49
|
+
automatic: bool = False
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, object]:
|
|
52
|
+
return {
|
|
53
|
+
"name": self.name,
|
|
54
|
+
"description": list(self.description),
|
|
55
|
+
"automatic": self.automatic,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, value: object, path: str) -> PlannedMode:
|
|
60
|
+
"""Read a mode written by :meth:`to_dict`, rejecting any other shape."""
|
|
61
|
+
keys = {"name", "description", "automatic"}
|
|
62
|
+
if not isinstance(value, dict) or set(value) != keys:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"{path} must be an object of name, description, automatic"
|
|
65
|
+
)
|
|
66
|
+
name, description, automatic = (
|
|
67
|
+
value["name"],
|
|
68
|
+
value["description"],
|
|
69
|
+
value["automatic"],
|
|
70
|
+
)
|
|
71
|
+
if (
|
|
72
|
+
not isinstance(name, str)
|
|
73
|
+
or not isinstance(description, list)
|
|
74
|
+
or not all(isinstance(line, str) for line in description)
|
|
75
|
+
or not isinstance(automatic, bool)
|
|
76
|
+
):
|
|
77
|
+
raise ValueError(f"{path} has an invalid name, description or automatic")
|
|
78
|
+
return cls(name, tuple(description), automatic)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class PlannedRule:
|
|
83
|
+
"""A rule frozen into the plan for one agent step.
|
|
84
|
+
|
|
85
|
+
The text and its hash are frozen so a run keeps delivering the wording it
|
|
86
|
+
started with; ``has_command`` says whether ww checks it mechanically.
|
|
87
|
+
``source`` is the rule file, relative to the project root when it lies
|
|
88
|
+
inside it, or ``None`` for a rule written in the step's YAML.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
id: str
|
|
92
|
+
summary: str
|
|
93
|
+
text: str
|
|
94
|
+
text_hash: str
|
|
95
|
+
paths: tuple[str, ...] = ()
|
|
96
|
+
has_command: bool = False
|
|
97
|
+
max_fixes: int = 1
|
|
98
|
+
hints: RuleHints = RuleHints()
|
|
99
|
+
source: str | None = None
|
|
100
|
+
|
|
101
|
+
def to_dict(self) -> dict[str, object]:
|
|
102
|
+
data: dict[str, object] = {
|
|
103
|
+
"id": self.id,
|
|
104
|
+
"summary": self.summary,
|
|
105
|
+
"text": self.text,
|
|
106
|
+
"text_hash": self.text_hash,
|
|
107
|
+
"paths": list(self.paths),
|
|
108
|
+
"has_command": self.has_command,
|
|
109
|
+
"max_fixes": self.max_fixes,
|
|
110
|
+
}
|
|
111
|
+
if self.hints.to_dict():
|
|
112
|
+
data["hints"] = self.hints.to_dict()
|
|
113
|
+
if self.source is not None:
|
|
114
|
+
data["source"] = self.source
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class PlannedCheck:
|
|
120
|
+
"""A command ww runs when the step completes; a failure sends it back.
|
|
121
|
+
|
|
122
|
+
``command`` is already planned like any cli handler's: build-time values
|
|
123
|
+
are substituted and runtime ones are left for execution. ``summary`` is
|
|
124
|
+
the rule's first sentence, or the hook's handler name.
|
|
125
|
+
|
|
126
|
+
A ``derived`` check is one the operator approved into the rule-automation
|
|
127
|
+
store; it is never compiled into a plan but resolved when the step
|
|
128
|
+
begins, and ``covers`` names the rules of the step it checks, so a check
|
|
129
|
+
shared by several rules runs once.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
id: str
|
|
133
|
+
source: CheckSource
|
|
134
|
+
summary: str
|
|
135
|
+
command: Commands
|
|
136
|
+
paths: tuple[str, ...] = ()
|
|
137
|
+
max_fixes: int = 1
|
|
138
|
+
covers: tuple[str, ...] = ()
|
|
139
|
+
on_failure_instruction: str | None = None
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
if self.source not in {"rule", "hook", "derived"}:
|
|
143
|
+
raise ValueError(f"invalid check source: {self.source!r}")
|
|
144
|
+
if not is_positive_int(self.max_fixes):
|
|
145
|
+
raise ValueError("check max_fixes must be a positive integer")
|
|
146
|
+
if bool(self.covers) != (self.source == "derived"):
|
|
147
|
+
raise ValueError("exactly a derived check names the rules it covers")
|
|
148
|
+
|
|
149
|
+
def to_dict(self) -> dict[str, object]:
|
|
150
|
+
data: dict[str, object] = {
|
|
151
|
+
"id": self.id,
|
|
152
|
+
"source": self.source,
|
|
153
|
+
"summary": self.summary,
|
|
154
|
+
"command": actions.get("cli").encode(self.command),
|
|
155
|
+
"paths": list(self.paths),
|
|
156
|
+
"max_fixes": self.max_fixes,
|
|
157
|
+
}
|
|
158
|
+
if self.covers:
|
|
159
|
+
data["covers"] = list(self.covers)
|
|
160
|
+
if self.on_failure_instruction is not None:
|
|
161
|
+
data["on_failure_instruction"] = self.on_failure_instruction
|
|
162
|
+
return data
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class VerificationTarget:
|
|
167
|
+
"""What a ww-generated verification item verifies.
|
|
168
|
+
|
|
169
|
+
The item judges, or proposes a check for, the rules without a command of
|
|
170
|
+
the agent item ``item_id``. Each distinct set of worker hints among those
|
|
171
|
+
rules gets its own verification item; ``ordinal`` numbers them in the
|
|
172
|
+
order ww created them and ``hints`` is the set this one runs with.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
item_id: str
|
|
176
|
+
ordinal: int
|
|
177
|
+
hints: RuleHints = RuleHints()
|
|
178
|
+
|
|
179
|
+
def __post_init__(self) -> None:
|
|
180
|
+
if not is_positive_int(self.ordinal):
|
|
181
|
+
raise ValueError("verification ordinal must be a positive integer")
|
|
182
|
+
|
|
183
|
+
def to_dict(self) -> dict[str, object]:
|
|
184
|
+
data: dict[str, object] = {"item_id": self.item_id, "ordinal": self.ordinal}
|
|
185
|
+
if self.hints.to_dict():
|
|
186
|
+
data["hints"] = self.hints.to_dict()
|
|
187
|
+
return data
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class PlanItem:
|
|
192
|
+
id: str
|
|
193
|
+
position: int
|
|
194
|
+
name: str
|
|
195
|
+
description: str
|
|
196
|
+
operation: PlanOperation
|
|
197
|
+
owner: PlanItemOwner
|
|
198
|
+
execution: ExecutionKind
|
|
199
|
+
requires_agent_input: bool
|
|
200
|
+
workflow: str
|
|
201
|
+
step: str
|
|
202
|
+
parent: str | None
|
|
203
|
+
phase: PlanItemPhase
|
|
204
|
+
source: str
|
|
205
|
+
registered_handler: str | None
|
|
206
|
+
provide: tuple[ProvidedVariable, ...] = ()
|
|
207
|
+
save_metadata: tuple[SavedMetadata, ...] = ()
|
|
208
|
+
# Documents this agent-owned item creates or edits in place.
|
|
209
|
+
update_document: tuple[DocumentUpdate, ...] = ()
|
|
210
|
+
# Custom item fields this agent-owned item must set on its item, or on
|
|
211
|
+
# every item when it is the collection.
|
|
212
|
+
update_item: tuple[ItemFieldUpdate, ...] = ()
|
|
213
|
+
outputs: tuple[str, ...] = ()
|
|
214
|
+
dependencies: tuple[str, ...] = ()
|
|
215
|
+
requested_agent: str | None = None
|
|
216
|
+
requested_model: str | None = None
|
|
217
|
+
requested_reasoning: str | None = None
|
|
218
|
+
# Who performs the item: the manager in its own session, or a worker.
|
|
219
|
+
role: StepRole = "worker"
|
|
220
|
+
# ``False``: its performer spawns no subagents for anything.
|
|
221
|
+
subagents: bool = True
|
|
222
|
+
# A conversation with the operator; performed by the session that can
|
|
223
|
+
# talk to them, so ``role`` is ``manager`` as well.
|
|
224
|
+
interactive: bool = False
|
|
225
|
+
explicit: bool = False
|
|
226
|
+
learnable: bool = False
|
|
227
|
+
choices: tuple[ChoiceDefinition, ...] = ()
|
|
228
|
+
# A per-item stage the operator answers on the operator page.
|
|
229
|
+
ui: bool = False
|
|
230
|
+
model: str | None = None
|
|
231
|
+
reasoning: str | None = None
|
|
232
|
+
profile: str | None = None
|
|
233
|
+
profile_instruction: str | None = None
|
|
234
|
+
# A project-local profile file, relative to the project root; the
|
|
235
|
+
# instruction prints it absolute for the current filesystem.
|
|
236
|
+
profile_path: str | None = None
|
|
237
|
+
# The directory this item works in: the task workspace, the project's
|
|
238
|
+
# own directory, or the project root.
|
|
239
|
+
workdir: Workdir = "task"
|
|
240
|
+
summary: bool = False
|
|
241
|
+
item_operation: ItemOperation | None = None
|
|
242
|
+
item_template: bool = False
|
|
243
|
+
item_id: str | None = None
|
|
244
|
+
# The stable identity of the ``items`` declaration this item belongs to: its
|
|
245
|
+
# logical step path. Carried by the collection item and by every per-item
|
|
246
|
+
# stage and hook (templates and their concrete copies), and by nothing
|
|
247
|
+
# else. It is plan data, independent of display names and work-item IDs,
|
|
248
|
+
# so a later pass can expand exactly its own templates. Not
|
|
249
|
+
# ``child_stage``, which belongs to ``children``.
|
|
250
|
+
item_pass: str | None = None
|
|
251
|
+
# On a collection item: this pass only collects or reconciles items and
|
|
252
|
+
# has no per-item stages (explicit ``items: {steps: []}``).
|
|
253
|
+
item_collect_only: bool = False
|
|
254
|
+
item_assignment: ItemAssignment = "per_step"
|
|
255
|
+
# Set on every body step and hook of the nearest enclosing ``loop``.
|
|
256
|
+
loop_id: str | None = None
|
|
257
|
+
loop_assignment: LoopAssignment | None = None
|
|
258
|
+
split_instruction: str | None = None
|
|
259
|
+
# On a collection item: the items outlive the run and are reconciled.
|
|
260
|
+
shared_items: bool = False
|
|
261
|
+
# On a collection item: the field a new item must carry, and the fields
|
|
262
|
+
# whose values may each appear once across all items.
|
|
263
|
+
item_identity: str | None = None
|
|
264
|
+
item_unique: tuple[str, ...] = ()
|
|
265
|
+
artifact: bool = True
|
|
266
|
+
child_operation: ChildOperation | None = None
|
|
267
|
+
# On a children collection: each child binds its own external ID through
|
|
268
|
+
# the first step of the child workflow, so ``add-child`` takes no ``--id``.
|
|
269
|
+
child_identity: bool = False
|
|
270
|
+
# On every per-child stage (``children.steps``) and its hooks: the path
|
|
271
|
+
# of the ``children`` step. Its templates carry ``item_template`` until
|
|
272
|
+
# the children are collected.
|
|
273
|
+
child_stage: str | None = None
|
|
274
|
+
# On a concrete per-child stage: the one-based position of its child in
|
|
275
|
+
# the run's children, which are append-only and never reordered.
|
|
276
|
+
child_number: int | None = None
|
|
277
|
+
# Ordered logical container paths above ``parent``. Hierarchy is explicit
|
|
278
|
+
# plan data; consumers must not reconstruct it by parsing ``step``.
|
|
279
|
+
ancestors: tuple[str, ...] = ()
|
|
280
|
+
# One-based declaration ordinal for every path in ``(*ancestors, step)``.
|
|
281
|
+
# Artifact storage uses these values to produce stable, ordered paths.
|
|
282
|
+
step_ordinals: tuple[int, ...] = ()
|
|
283
|
+
artifact_dependency: str | None = None
|
|
284
|
+
loop_break: str | None = None
|
|
285
|
+
loop_continue: str | None = None
|
|
286
|
+
assessment_question: str | None = None
|
|
287
|
+
assessment_outcomes: tuple[str, ...] = ()
|
|
288
|
+
# The outcomes that end the workflow; they emit no items of their own.
|
|
289
|
+
assessment_stops: tuple[str, ...] = ()
|
|
290
|
+
assessment_parent: str | None = None
|
|
291
|
+
assessment_outcome: str | None = None
|
|
292
|
+
# Rules delivered on this agent step's page, and the checks ww runs when
|
|
293
|
+
# it completes.
|
|
294
|
+
rules: tuple[PlannedRule, ...] = ()
|
|
295
|
+
checks: tuple[PlannedCheck, ...] = ()
|
|
296
|
+
# The modes delivered on this agent step's page: the run's selected
|
|
297
|
+
# modes, then the automatic modes whose filters admit the step.
|
|
298
|
+
modes: tuple[PlannedMode, ...] = ()
|
|
299
|
+
# Set on a verification item ww inserts before an agent step whose rules
|
|
300
|
+
# without a command need a verifier; never compiled from configuration.
|
|
301
|
+
verifies: VerificationTarget | None = None
|
|
302
|
+
on_failure: str = "operator"
|
|
303
|
+
on_failure_instruction: str | None = None
|
|
304
|
+
max_handler_fixes: int = 3
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def kind(self) -> PlanItemKind:
|
|
308
|
+
return self.operation.kind
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def breaks_children(self) -> bool:
|
|
312
|
+
"""Whether this step's ``break`` ends the per-child stages.
|
|
313
|
+
|
|
314
|
+
A ``break`` ends the nearest enclosing construct: a loop inside the
|
|
315
|
+
per-child stage, else the children, whose remaining ones are skipped.
|
|
316
|
+
"""
|
|
317
|
+
if self.loop_break is None or self.child_stage is None:
|
|
318
|
+
return False
|
|
319
|
+
return self.loop_id is None or not self.loop_id.startswith(
|
|
320
|
+
f"{self.child_stage}/"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def hands_over(self) -> bool:
|
|
325
|
+
"""Whether completing this item must leave a summary for the next step.
|
|
326
|
+
|
|
327
|
+
Only an ordinary agent step does: hooks, ``init`` (its artifact is the
|
|
328
|
+
requirements), the built-in workflow summary, and verification items
|
|
329
|
+
are exempt.
|
|
330
|
+
"""
|
|
331
|
+
return (
|
|
332
|
+
self.phase == "step"
|
|
333
|
+
and self.owner == "agent"
|
|
334
|
+
and not self.summary
|
|
335
|
+
and self.step != "init"
|
|
336
|
+
and self.verifies is None
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def payload_as(self, payload_type: type[PayloadT]) -> PayloadT:
|
|
340
|
+
if not isinstance(self.operation, PlannedAction):
|
|
341
|
+
raise ValueError(f"plan operation {self.kind!r} has no action payload")
|
|
342
|
+
payload = self.operation.payload
|
|
343
|
+
if not isinstance(payload, payload_type):
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"plan action {self.kind!r} does not contain "
|
|
346
|
+
f"{payload_type.__name__} data"
|
|
347
|
+
)
|
|
348
|
+
return payload
|
|
349
|
+
|
|
350
|
+
def __post_init__(self) -> None:
|
|
351
|
+
if self.on_failure not in {"operator", "fix"}:
|
|
352
|
+
raise ValueError("invalid handler failure policy")
|
|
353
|
+
if not is_positive_int(self.max_handler_fixes):
|
|
354
|
+
raise ValueError("handler fix limit must be positive")
|
|
355
|
+
if (self.on_failure == "fix" or self.on_failure_instruction is not None) and (
|
|
356
|
+
self.kind != "cli" or self.owner != "ww"
|
|
357
|
+
):
|
|
358
|
+
raise ValueError("handler repair requires an automatic command")
|
|
359
|
+
if self.phase not in {
|
|
360
|
+
"before_start_workflow",
|
|
361
|
+
"before_start",
|
|
362
|
+
"step",
|
|
363
|
+
"before_complete",
|
|
364
|
+
"after_complete",
|
|
365
|
+
"before_complete_workflow",
|
|
366
|
+
}:
|
|
367
|
+
raise ValueError(f"invalid plan item phase: {self.phase!r}")
|
|
368
|
+
if self.item_operation not in {
|
|
369
|
+
None,
|
|
370
|
+
"collect",
|
|
371
|
+
"process_item",
|
|
372
|
+
"resolve_item",
|
|
373
|
+
"report_item",
|
|
374
|
+
"handle_item",
|
|
375
|
+
}:
|
|
376
|
+
raise ValueError(f"invalid item operation: {self.item_operation!r}")
|
|
377
|
+
if self.item_assignment not in get_args(ItemAssignment):
|
|
378
|
+
raise ValueError(f"invalid item assignment: {self.item_assignment!r}")
|
|
379
|
+
if self.loop_assignment is not None and self.loop_assignment not in get_args(
|
|
380
|
+
LoopAssignment
|
|
381
|
+
):
|
|
382
|
+
raise ValueError(f"invalid loop assignment: {self.loop_assignment!r}")
|
|
383
|
+
if (self.loop_id is None) != (self.loop_assignment is None):
|
|
384
|
+
raise ValueError("loop ID and loop assignment must be set together")
|
|
385
|
+
if self.workdir not in WORKDIRS:
|
|
386
|
+
raise ValueError(f"invalid workdir: {self.workdir!r}")
|
|
387
|
+
if self.child_operation not in {None, "collect"}:
|
|
388
|
+
raise ValueError(f"invalid child operation: {self.child_operation!r}")
|
|
389
|
+
if self.child_number is not None and (
|
|
390
|
+
self.child_stage is None or not is_positive_int(self.child_number)
|
|
391
|
+
):
|
|
392
|
+
raise ValueError("a child number belongs to a per-child stage")
|
|
393
|
+
if self.step_ordinals and (
|
|
394
|
+
len(self.step_ordinals) != len(self.ancestors) + 1
|
|
395
|
+
or not all(is_positive_int(value) for value in self.step_ordinals)
|
|
396
|
+
):
|
|
397
|
+
raise ValueError("step ordinals must match the positive step hierarchy")
|
|
398
|
+
if isinstance(self.operation, PlannedAction):
|
|
399
|
+
contract = actions.get(self.kind) if actions.contains(self.kind) else None
|
|
400
|
+
if contract is not None and (
|
|
401
|
+
self.owner != contract.owner or self.execution != contract.execution
|
|
402
|
+
):
|
|
403
|
+
raise ValueError(
|
|
404
|
+
f"plan item kind {self.kind!r} conflicts with owner/execution"
|
|
405
|
+
)
|
|
406
|
+
if contract is not None and not isinstance(
|
|
407
|
+
self.operation.payload, contract.planned_type
|
|
408
|
+
):
|
|
409
|
+
raise ValueError(
|
|
410
|
+
f"plan item kind {self.kind!r} has the wrong action payload type"
|
|
411
|
+
)
|
|
412
|
+
elif (self.owner, self.execution) != (
|
|
413
|
+
self.operation.owner,
|
|
414
|
+
self.operation.execution,
|
|
415
|
+
):
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"{self.kind} items must be {self.operation.owner}-owned "
|
|
418
|
+
f"{self.operation.execution} operations"
|
|
419
|
+
)
|
|
420
|
+
# ``validate`` accepts the pre-planning definition. Planned payloads
|
|
421
|
+
# may deliberately have a distinct type, and their strict boundary is
|
|
422
|
+
# the action's decoder (used for persisted snapshots).
|
|
423
|
+
if isinstance(self.operation, LoopBoundary) and (
|
|
424
|
+
self.loop_break is not None or self.loop_continue is not None
|
|
425
|
+
):
|
|
426
|
+
raise ValueError("loop control items cannot define a worker break gate")
|
|
427
|
+
if self.loop_break is not None and (
|
|
428
|
+
self.owner != "agent" or self.phase != "step"
|
|
429
|
+
):
|
|
430
|
+
raise ValueError("loop break gates require agent-owned step work")
|
|
431
|
+
if self.loop_continue is not None and (
|
|
432
|
+
self.owner != "agent" or self.phase != "step"
|
|
433
|
+
):
|
|
434
|
+
raise ValueError("loop continue gates require agent-owned step work")
|
|
435
|
+
expected_agent_input = self.execution == "automatic" and bool(self.provide)
|
|
436
|
+
if self.requires_agent_input != expected_agent_input:
|
|
437
|
+
raise ValueError(
|
|
438
|
+
"requires_agent_input must match an automatic action with provided "
|
|
439
|
+
"values"
|
|
440
|
+
)
|
|
441
|
+
if (self.rules or self.checks) and self.owner != "agent":
|
|
442
|
+
raise ValueError("only agent-owned plan items carry rules and checks")
|
|
443
|
+
if self.modes and self.owner != "agent":
|
|
444
|
+
raise ValueError("only agent-owned plan items carry modes")
|
|
445
|
+
if self.verifies is not None and (
|
|
446
|
+
self.owner != "agent" or self.rules or self.checks or self.provide
|
|
447
|
+
):
|
|
448
|
+
raise ValueError(
|
|
449
|
+
"a verification item is agent-owned and carries no rules, checks, "
|
|
450
|
+
"or provided values"
|
|
451
|
+
)
|
|
452
|
+
if self.save_metadata and self.owner != "agent" and self.kind != "cli":
|
|
453
|
+
raise ValueError("only agent-owned or CLI plan items can save metadata")
|
|
454
|
+
if self.update_document and self.owner != "agent":
|
|
455
|
+
raise ValueError("only agent-owned plan items can update documents")
|
|
456
|
+
if self.update_item and self.owner != "agent" and self.kind != "cli":
|
|
457
|
+
raise ValueError("only agent-owned or CLI plan items can save item fields")
|
|
458
|
+
metadata_names = [item.name for item in self.save_metadata]
|
|
459
|
+
metadata_keys = [(item.scope, item.key) for item in self.save_metadata]
|
|
460
|
+
if len(metadata_names) != len(set(metadata_names)):
|
|
461
|
+
raise ValueError("plan item has duplicate saved metadata names")
|
|
462
|
+
if len(metadata_keys) != len(set(metadata_keys)):
|
|
463
|
+
raise ValueError("plan item has duplicate saved metadata keys")
|
|
464
|
+
for index, (scope, key) in enumerate(metadata_keys):
|
|
465
|
+
if any(
|
|
466
|
+
scope == other_scope
|
|
467
|
+
and (key.startswith(f"{other}.") or other.startswith(f"{key}."))
|
|
468
|
+
for other_scope, other in metadata_keys[index + 1 :]
|
|
469
|
+
):
|
|
470
|
+
raise ValueError("plan item has overlapping saved metadata keys")
|
|
471
|
+
|
|
472
|
+
def to_dict(self) -> dict[str, object]:
|
|
473
|
+
"""The persisted form."""
|
|
474
|
+
data = self._to_dict()
|
|
475
|
+
# Pass identity is written only where it applies.
|
|
476
|
+
if self.item_pass is not None:
|
|
477
|
+
data["item_pass"] = self.item_pass
|
|
478
|
+
if self.item_collect_only:
|
|
479
|
+
data["item_collect_only"] = True
|
|
480
|
+
if self.on_failure != "operator":
|
|
481
|
+
data["on_failure"] = self.on_failure
|
|
482
|
+
data["max_handler_fixes"] = self.max_handler_fixes
|
|
483
|
+
if self.on_failure_instruction is not None:
|
|
484
|
+
data["on_failure_instruction"] = self.on_failure_instruction
|
|
485
|
+
# Loop membership is written only where it applies, so plans saved
|
|
486
|
+
# before it existed re-serialize byte for byte and keep their digest.
|
|
487
|
+
if self.loop_id is None:
|
|
488
|
+
del data["loop_id"], data["loop_assignment"]
|
|
489
|
+
if self.profile_path is None:
|
|
490
|
+
del data["profile_path"]
|
|
491
|
+
if self.workdir == "task":
|
|
492
|
+
del data["workdir"]
|
|
493
|
+
if not self.update_document:
|
|
494
|
+
del data["update_document"]
|
|
495
|
+
if not self.interactive:
|
|
496
|
+
del data["interactive"]
|
|
497
|
+
if not self.explicit:
|
|
498
|
+
del data["explicit"]
|
|
499
|
+
if not self.learnable:
|
|
500
|
+
del data["learnable"]
|
|
501
|
+
if not self.choices:
|
|
502
|
+
del data["choices"]
|
|
503
|
+
if not self.ui:
|
|
504
|
+
del data["ui"]
|
|
505
|
+
if not self.shared_items:
|
|
506
|
+
del data["shared_items"]
|
|
507
|
+
if not self.update_item:
|
|
508
|
+
del data["update_item"]
|
|
509
|
+
if self.item_identity is None and not self.item_unique:
|
|
510
|
+
del data["item_identity"], data["item_unique"]
|
|
511
|
+
if not self.rules:
|
|
512
|
+
del data["rules"]
|
|
513
|
+
if not self.checks:
|
|
514
|
+
del data["checks"]
|
|
515
|
+
if not self.modes:
|
|
516
|
+
del data["modes"]
|
|
517
|
+
if self.verifies is None:
|
|
518
|
+
del data["verifies"]
|
|
519
|
+
if self.child_stage is None:
|
|
520
|
+
del data["child_stage"]
|
|
521
|
+
if self.child_number is None:
|
|
522
|
+
del data["child_number"]
|
|
523
|
+
return data
|
|
524
|
+
|
|
525
|
+
def _to_dict(self) -> dict[str, object]:
|
|
526
|
+
return {
|
|
527
|
+
"id": self.id,
|
|
528
|
+
"position": self.position,
|
|
529
|
+
"name": self.name,
|
|
530
|
+
"description": self.description,
|
|
531
|
+
"operation": encode_operation(self.operation),
|
|
532
|
+
"owner": self.owner,
|
|
533
|
+
"execution": self.execution,
|
|
534
|
+
"requires_agent_input": self.requires_agent_input,
|
|
535
|
+
"workflow": self.workflow,
|
|
536
|
+
"step": self.step,
|
|
537
|
+
"parent": self.parent,
|
|
538
|
+
"phase": self.phase,
|
|
539
|
+
"source": self.source,
|
|
540
|
+
"registered_handler": self.registered_handler,
|
|
541
|
+
"provide": [item.to_dict() for item in self.provide],
|
|
542
|
+
"save_metadata": [item.to_dict() for item in self.save_metadata],
|
|
543
|
+
"update_document": [item.to_dict() for item in self.update_document],
|
|
544
|
+
"outputs": list(self.outputs),
|
|
545
|
+
"dependencies": list(self.dependencies),
|
|
546
|
+
"requested_agent": self.requested_agent,
|
|
547
|
+
"requested_model": self.requested_model,
|
|
548
|
+
"requested_reasoning": self.requested_reasoning,
|
|
549
|
+
"role": self.role,
|
|
550
|
+
"subagents": self.subagents,
|
|
551
|
+
"interactive": self.interactive,
|
|
552
|
+
"explicit": self.explicit,
|
|
553
|
+
"learnable": self.learnable,
|
|
554
|
+
"choices": [choice.to_dict() for choice in self.choices],
|
|
555
|
+
"ui": self.ui,
|
|
556
|
+
"model": self.model,
|
|
557
|
+
"reasoning": self.reasoning,
|
|
558
|
+
"profile": self.profile,
|
|
559
|
+
"profile_instruction": self.profile_instruction,
|
|
560
|
+
"profile_path": self.profile_path,
|
|
561
|
+
"workdir": self.workdir,
|
|
562
|
+
"summary": self.summary,
|
|
563
|
+
"item_operation": self.item_operation,
|
|
564
|
+
"item_template": self.item_template,
|
|
565
|
+
"item_id": self.item_id,
|
|
566
|
+
"item_assignment": self.item_assignment,
|
|
567
|
+
"loop_id": self.loop_id,
|
|
568
|
+
"loop_assignment": self.loop_assignment,
|
|
569
|
+
"split_instruction": self.split_instruction,
|
|
570
|
+
"shared_items": self.shared_items,
|
|
571
|
+
"update_item": [item.to_dict() for item in self.update_item],
|
|
572
|
+
"item_identity": self.item_identity,
|
|
573
|
+
"item_unique": list(self.item_unique),
|
|
574
|
+
"artifact": self.artifact,
|
|
575
|
+
"child_operation": self.child_operation,
|
|
576
|
+
"child_identity": self.child_identity,
|
|
577
|
+
"child_stage": self.child_stage,
|
|
578
|
+
"child_number": self.child_number,
|
|
579
|
+
"ancestors": list(self.ancestors),
|
|
580
|
+
"step_ordinals": list(self.step_ordinals),
|
|
581
|
+
"artifact_dependency": self.artifact_dependency,
|
|
582
|
+
"loop_break": self.loop_break,
|
|
583
|
+
"loop_continue": self.loop_continue,
|
|
584
|
+
"assessment_question": self.assessment_question,
|
|
585
|
+
"assessment_outcomes": list(self.assessment_outcomes),
|
|
586
|
+
"assessment_parent": self.assessment_parent,
|
|
587
|
+
"assessment_outcome": self.assessment_outcome,
|
|
588
|
+
# Written only when set: a plan without stops has no such key.
|
|
589
|
+
**(
|
|
590
|
+
{"assessment_stops": list(self.assessment_stops)}
|
|
591
|
+
if self.assessment_stops
|
|
592
|
+
else {}
|
|
593
|
+
),
|
|
594
|
+
"rules": [rule.to_dict() for rule in self.rules],
|
|
595
|
+
"checks": [check.to_dict() for check in self.checks],
|
|
596
|
+
"modes": [mode.to_dict() for mode in self.modes],
|
|
597
|
+
"verifies": self.verifies.to_dict() if self.verifies else None,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def number_step_paths(items: tuple[PlanItem, ...]) -> tuple[PlanItem, ...]:
|
|
602
|
+
"""Attach each plan item's declaration ordinal at every hierarchy level."""
|
|
603
|
+
children: dict[str | None, list[str]] = {}
|
|
604
|
+
ordinals: dict[str, int] = {}
|
|
605
|
+
for item in items:
|
|
606
|
+
chain = (*item.ancestors, item.step)
|
|
607
|
+
parent: str | None = None
|
|
608
|
+
for path in chain:
|
|
609
|
+
siblings = children.setdefault(parent, [])
|
|
610
|
+
if path not in siblings:
|
|
611
|
+
siblings.append(path)
|
|
612
|
+
ordinals[path] = len(siblings)
|
|
613
|
+
parent = path
|
|
614
|
+
return tuple(
|
|
615
|
+
replace(
|
|
616
|
+
item,
|
|
617
|
+
step_ordinals=tuple(
|
|
618
|
+
ordinals[path] for path in (*item.ancestors, item.step)
|
|
619
|
+
),
|
|
620
|
+
)
|
|
621
|
+
for item in items
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@dataclass(frozen=True)
|
|
626
|
+
class WorkflowPlan:
|
|
627
|
+
workflow: str
|
|
628
|
+
workflow_description: str
|
|
629
|
+
agent: str
|
|
630
|
+
task_id: str | None
|
|
631
|
+
modes: tuple[str, ...]
|
|
632
|
+
handoff: bool
|
|
633
|
+
items: tuple[PlanItem, ...]
|
|
634
|
+
# The root documents, frozen with the plan so a run resolves their paths
|
|
635
|
+
# without reading the configuration again.
|
|
636
|
+
documents: tuple[DocumentDefinition, ...] = ()
|
|
637
|
+
# Offered to the operator when the run completes; frozen with the plan so
|
|
638
|
+
# a later configuration change does not alter a finished run's page.
|
|
639
|
+
recommended_next_workflow: str | None = None
|
|
640
|
+
# The workflow whose project handling the run takes (``hooks_from``),
|
|
641
|
+
# frozen so extensions key their settings by the lane the run started on.
|
|
642
|
+
hooks_from: str | None = None
|
|
643
|
+
|
|
644
|
+
@property
|
|
645
|
+
def lane(self) -> str:
|
|
646
|
+
"""The workflow extensions key their settings by: the lane, else this."""
|
|
647
|
+
return self.hooks_from or self.workflow
|
|
648
|
+
|
|
649
|
+
def to_dict(self) -> dict[str, object]:
|
|
650
|
+
data: dict[str, object] = {
|
|
651
|
+
"workflow": self.workflow,
|
|
652
|
+
"workflow_description": self.workflow_description,
|
|
653
|
+
"agent": self.agent,
|
|
654
|
+
"task_id": self.task_id,
|
|
655
|
+
"modes": list(self.modes),
|
|
656
|
+
"handoff": self.handoff,
|
|
657
|
+
"items": [item.to_dict() for item in self.items],
|
|
658
|
+
}
|
|
659
|
+
if self.documents:
|
|
660
|
+
data["documents"] = [document.to_dict() for document in self.documents]
|
|
661
|
+
if self.recommended_next_workflow is not None:
|
|
662
|
+
data["recommended_next_workflow"] = self.recommended_next_workflow
|
|
663
|
+
if self.hooks_from is not None:
|
|
664
|
+
data["hooks_from"] = self.hooks_from
|
|
665
|
+
return data
|