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/compiler.py
ADDED
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Compile normalized workflow definitions into a flat, inspectable plan."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass, replace
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ww.actions import (
|
|
13
|
+
Commands,
|
|
14
|
+
DefinedAction,
|
|
15
|
+
PlannedAction,
|
|
16
|
+
Prompt,
|
|
17
|
+
actions,
|
|
18
|
+
)
|
|
19
|
+
from ww.config.rules import rule_source
|
|
20
|
+
from ww.contracts import (
|
|
21
|
+
ExecutionKind,
|
|
22
|
+
HookPhase,
|
|
23
|
+
PlanItemKind,
|
|
24
|
+
PlanItemOwner,
|
|
25
|
+
PlanItemPhase,
|
|
26
|
+
StepRole,
|
|
27
|
+
)
|
|
28
|
+
from ww.control import child_workflow, workflow_transition
|
|
29
|
+
from ww.discovery import AgentDiscovery
|
|
30
|
+
from ww.errors import ConfigurationError
|
|
31
|
+
from ww.extensions import ExtensionRegistry, is_extension_reference
|
|
32
|
+
from ww.interpolation import dependencies, interpolate
|
|
33
|
+
from ww.operations import LoopBoundary, PlanOperation, WorkflowHandoff
|
|
34
|
+
from ww.project_config import ProjectConfig
|
|
35
|
+
from ww.variables import (
|
|
36
|
+
CHILD_FIELD_PREFIX,
|
|
37
|
+
CHILD_VALUE_NAMES,
|
|
38
|
+
CHILD_VALUE_PREFIX,
|
|
39
|
+
CHOICES,
|
|
40
|
+
CORE_VARIABLE_NAMES,
|
|
41
|
+
DOCUMENTS_PREFIX,
|
|
42
|
+
child_value_name,
|
|
43
|
+
compile_variable_values,
|
|
44
|
+
)
|
|
45
|
+
from ww.workflow_config import (
|
|
46
|
+
INIT_STEP_NAME,
|
|
47
|
+
HandlerDefinition,
|
|
48
|
+
HookDefinition,
|
|
49
|
+
ProvidedVariable,
|
|
50
|
+
RuleDefinition,
|
|
51
|
+
RuleGroupRef,
|
|
52
|
+
StepDefinition,
|
|
53
|
+
WorkflowConfiguration,
|
|
54
|
+
WorkflowDefinition,
|
|
55
|
+
binds_task_identity,
|
|
56
|
+
)
|
|
57
|
+
from ww.workflow_validation import implicit_init_step, validate_configuration
|
|
58
|
+
from ww.workspace import Workdir, relative_workspace
|
|
59
|
+
|
|
60
|
+
from .actions import ActionResolver
|
|
61
|
+
from .constructs import (
|
|
62
|
+
EMPTY_ITEM_ANNOTATIONS,
|
|
63
|
+
ConstructPlannerRegistry,
|
|
64
|
+
ItemAnnotations,
|
|
65
|
+
LeafRequest,
|
|
66
|
+
LoopBoundaryRequest,
|
|
67
|
+
PlanningContext,
|
|
68
|
+
PlanningScope,
|
|
69
|
+
builtin_construct_planners,
|
|
70
|
+
normalize_construct,
|
|
71
|
+
step_annotations,
|
|
72
|
+
)
|
|
73
|
+
from .models import (
|
|
74
|
+
PlanItem,
|
|
75
|
+
PlannedCheck,
|
|
76
|
+
PlannedMode,
|
|
77
|
+
PlannedRule,
|
|
78
|
+
WorkflowPlan,
|
|
79
|
+
number_step_paths,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class ExecutionHints:
|
|
85
|
+
"""The worker shape a step requests: agent, model, reasoning, and profile.
|
|
86
|
+
|
|
87
|
+
Every field is inherited along the same chain, workflow, then each
|
|
88
|
+
enclosing step, then the step itself, so a loop wrapper or a parent step
|
|
89
|
+
can set the profile for its whole body. ``workdir`` follows the same
|
|
90
|
+
chain, but only steps declare it: a hook chooses its own directory.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
agent: str
|
|
94
|
+
model: str = "auto"
|
|
95
|
+
reasoning: str = "auto"
|
|
96
|
+
profile: str | None = None
|
|
97
|
+
profile_description: str | None = None
|
|
98
|
+
workdir: Workdir = "task"
|
|
99
|
+
# Who performs the steps below: inherited like the profile.
|
|
100
|
+
role: StepRole = "worker"
|
|
101
|
+
# Whether their performer may spawn subagents, inherited the same way.
|
|
102
|
+
subagents: bool = True
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def builtin(cls, agent: str, settings: dict[str, str]) -> ExecutionHints:
|
|
106
|
+
"""The worker shape of a built-in step from its model/reasoning request."""
|
|
107
|
+
return cls(agent, settings["model"], settings["reasoning"])
|
|
108
|
+
|
|
109
|
+
def overlay(self, value: HandlerDefinition | WorkflowDefinition) -> ExecutionHints:
|
|
110
|
+
agent = value.agent if value.agent is not None else self.agent
|
|
111
|
+
model = self.model
|
|
112
|
+
reasoning = self.reasoning
|
|
113
|
+
if value.model is not None:
|
|
114
|
+
if value.reasoning is None and value.model != model:
|
|
115
|
+
reasoning = "auto"
|
|
116
|
+
model = value.model
|
|
117
|
+
if value.reasoning is not None:
|
|
118
|
+
reasoning = value.reasoning
|
|
119
|
+
profile, profile_description = self.profile, self.profile_description
|
|
120
|
+
workdir = self.workdir
|
|
121
|
+
# Hooks, handlers, and modes cannot declare profiles.
|
|
122
|
+
if isinstance(value, StepDefinition | WorkflowDefinition):
|
|
123
|
+
if value.profile is not None:
|
|
124
|
+
profile = value.profile
|
|
125
|
+
if value.profile_description is not None:
|
|
126
|
+
profile_description = value.profile_description
|
|
127
|
+
if isinstance(value, StepDefinition) and value.workdir is not None:
|
|
128
|
+
workdir = value.workdir
|
|
129
|
+
role, subagents = self.role, self.subagents
|
|
130
|
+
if isinstance(value, StepDefinition | WorkflowDefinition):
|
|
131
|
+
if value.role:
|
|
132
|
+
role = value.role
|
|
133
|
+
if value.subagents is not None:
|
|
134
|
+
subagents = value.subagents
|
|
135
|
+
return ExecutionHints(
|
|
136
|
+
agent,
|
|
137
|
+
model,
|
|
138
|
+
reasoning,
|
|
139
|
+
profile,
|
|
140
|
+
profile_description,
|
|
141
|
+
workdir,
|
|
142
|
+
role,
|
|
143
|
+
subagents,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class PlanCompilationOptions:
|
|
149
|
+
"""Context-dependent choices applied while producing the final plan.
|
|
150
|
+
|
|
151
|
+
``task_id`` makes that ID authoritative and removes it from agent-provided
|
|
152
|
+
values. ``completed_bootstrap_step`` omits an identity action that already
|
|
153
|
+
ran before the durable task was created, while retaining normal start hooks.
|
|
154
|
+
Keeping both transformations here gives ``plan`` and ``start`` one plan
|
|
155
|
+
interpretation instead of service-only rewriting.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
task_id: str | None = None
|
|
159
|
+
completed_bootstrap_step: str | None = None
|
|
160
|
+
# The configured project the run works in; extension items working there
|
|
161
|
+
# freeze that project's settings instead of the root's.
|
|
162
|
+
project: str | None = None
|
|
163
|
+
# The modes selected for the run; ``None`` takes the workflow's defaults.
|
|
164
|
+
modes: tuple[str, ...] | None = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class _CompilerPlanningContext(PlanningContext):
|
|
169
|
+
"""Compiler-private implementation of the construct planning primitives."""
|
|
170
|
+
|
|
171
|
+
scope: PlanningScope
|
|
172
|
+
default_max_rounds: int
|
|
173
|
+
_compile: Callable[[tuple[StepDefinition, ...], PlanningScope], tuple[str, ...]]
|
|
174
|
+
_compile_region: Callable[
|
|
175
|
+
[tuple[StepDefinition, ...], PlanningScope, ItemAnnotations], tuple[str, ...]
|
|
176
|
+
]
|
|
177
|
+
_emit_leaf: Callable[[LeafRequest], tuple[str, ...]]
|
|
178
|
+
_emit_boundary: Callable[[LoopBoundaryRequest], None]
|
|
179
|
+
|
|
180
|
+
def derive_scope(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
parent: str,
|
|
184
|
+
parent_ancestors: tuple[str, ...],
|
|
185
|
+
item_template: bool | None = None,
|
|
186
|
+
) -> PlanningScope:
|
|
187
|
+
return replace(
|
|
188
|
+
self.scope,
|
|
189
|
+
parent=parent,
|
|
190
|
+
ancestors=parent_ancestors,
|
|
191
|
+
item_template=(
|
|
192
|
+
self.scope.item_template if item_template is None else item_template
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def compile_steps(
|
|
197
|
+
self, steps: tuple[StepDefinition, ...], scope: PlanningScope
|
|
198
|
+
) -> tuple[str, ...]:
|
|
199
|
+
return self._compile(steps, scope)
|
|
200
|
+
|
|
201
|
+
def compile_scoped_region(
|
|
202
|
+
self,
|
|
203
|
+
steps: tuple[StepDefinition, ...],
|
|
204
|
+
scope: PlanningScope,
|
|
205
|
+
annotations: ItemAnnotations,
|
|
206
|
+
) -> tuple[str, ...]:
|
|
207
|
+
return self._compile_region(steps, scope, annotations)
|
|
208
|
+
|
|
209
|
+
def emit_leaf(self, request: LeafRequest) -> tuple[str, ...]:
|
|
210
|
+
return self._emit_leaf(request)
|
|
211
|
+
|
|
212
|
+
def emit_loop_boundary(self, request: LoopBoundaryRequest) -> None:
|
|
213
|
+
self._emit_boundary(request)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class WorkflowPlanCompiler:
|
|
217
|
+
"""Resolve a selected workflow into exactly the actions it will perform."""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
configuration: WorkflowConfiguration,
|
|
222
|
+
root: Path,
|
|
223
|
+
agent: str,
|
|
224
|
+
task_id: str | None = None,
|
|
225
|
+
extensions: ExtensionRegistry | None = None,
|
|
226
|
+
options: PlanCompilationOptions | None = None,
|
|
227
|
+
project_config: ProjectConfig | None = None,
|
|
228
|
+
construct_planners: ConstructPlannerRegistry | None = None,
|
|
229
|
+
construct_normalizer: Callable[[StepDefinition], object] = normalize_construct,
|
|
230
|
+
) -> None:
|
|
231
|
+
self.configuration = validate_configuration(configuration, extensions)
|
|
232
|
+
self.root = root
|
|
233
|
+
self.agent = agent
|
|
234
|
+
if options is not None and task_id is not None and options.task_id != task_id:
|
|
235
|
+
raise ValueError("task_id and compilation options disagree")
|
|
236
|
+
self.options = options or PlanCompilationOptions(task_id=task_id)
|
|
237
|
+
self.task_id = self.options.task_id
|
|
238
|
+
self.extensions = extensions
|
|
239
|
+
self.project_config = project_config or ProjectConfig()
|
|
240
|
+
self.discovery = AgentDiscovery(root)
|
|
241
|
+
self.available = self.discovery.available(agent)
|
|
242
|
+
self.builtins = compile_variable_values(
|
|
243
|
+
tuple(item.name for item in self.configuration.workflows), self.task_id
|
|
244
|
+
)
|
|
245
|
+
self.actions = ActionResolver(
|
|
246
|
+
self.configuration,
|
|
247
|
+
self.agent,
|
|
248
|
+
self.extensions,
|
|
249
|
+
self.available,
|
|
250
|
+
self.builtins,
|
|
251
|
+
self.options.project,
|
|
252
|
+
)
|
|
253
|
+
self.construct_planners = construct_planners or builtin_construct_planners()
|
|
254
|
+
self.construct_normalizer = construct_normalizer
|
|
255
|
+
# Step paths whose agent item took the step's ``fix`` hooks as checks;
|
|
256
|
+
# those hooks are then not compiled as hook items as well.
|
|
257
|
+
self._checked_steps: set[tuple[str, str]] = set()
|
|
258
|
+
# The run's selected modes, resolved when a workflow is compiled.
|
|
259
|
+
self._selected_modes: tuple[PlannedMode, ...] = ()
|
|
260
|
+
|
|
261
|
+
@cached_property
|
|
262
|
+
def _namespace_variables(self) -> tuple[str, ...]:
|
|
263
|
+
"""Values configured extensions provide under ``{{ww.<namespace>.*}}``."""
|
|
264
|
+
if self.extensions is None:
|
|
265
|
+
return ()
|
|
266
|
+
return self.extensions.namespace_variables()
|
|
267
|
+
|
|
268
|
+
@cached_property
|
|
269
|
+
def _child_variables(self) -> tuple[str, ...]:
|
|
270
|
+
"""The exact ``{{ww.child.*}}`` names a per-child stage may read.
|
|
271
|
+
|
|
272
|
+
``{{ww.child.field.<name>}}`` is open-ended and admitted by prefix.
|
|
273
|
+
Every extension namespace value is also offered for the child, as
|
|
274
|
+
``{{ww.child.<namespace>.<name>}}``.
|
|
275
|
+
"""
|
|
276
|
+
return (
|
|
277
|
+
*CHILD_VALUE_NAMES,
|
|
278
|
+
*(child_value_name(name) for name in self._namespace_variables),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _check_child_values(
|
|
282
|
+
self,
|
|
283
|
+
handler: HandlerDefinition,
|
|
284
|
+
names: tuple[str, ...],
|
|
285
|
+
step_path: str,
|
|
286
|
+
annotations: ItemAnnotations,
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Only a per-child stage reads ``{{ww.child.*}}``, and only real names.
|
|
289
|
+
|
|
290
|
+
The child's extension values (``{{ww.child.git.branch}}``) exist once
|
|
291
|
+
its task does, so a stage before the one that runs the child reads
|
|
292
|
+
only its record: ``id``, ``text``, ``project``, and ``field.*``.
|
|
293
|
+
"""
|
|
294
|
+
wanted = [name for name in names if name.startswith(CHILD_VALUE_PREFIX)]
|
|
295
|
+
if not wanted:
|
|
296
|
+
return
|
|
297
|
+
child_stage = annotations.child_stage
|
|
298
|
+
if child_stage is None:
|
|
299
|
+
raise ConfigurationError(
|
|
300
|
+
f"handler {handler.name!r} reads {{{{{wanted[0]}}}}}, which only "
|
|
301
|
+
"a stage under children.steps can read"
|
|
302
|
+
)
|
|
303
|
+
unknown = [
|
|
304
|
+
name
|
|
305
|
+
for name in wanted
|
|
306
|
+
if name not in self._child_variables
|
|
307
|
+
and not name.startswith(CHILD_FIELD_PREFIX)
|
|
308
|
+
]
|
|
309
|
+
if unknown:
|
|
310
|
+
raise ConfigurationError(
|
|
311
|
+
f"handler {handler.name!r} references unknown child value(s): "
|
|
312
|
+
+ ", ".join(sorted(unknown))
|
|
313
|
+
)
|
|
314
|
+
stage = step_path.removeprefix(f"{child_stage}/{{child}}/").split("/")[0]
|
|
315
|
+
extension = [
|
|
316
|
+
name
|
|
317
|
+
for name in wanted
|
|
318
|
+
if name not in CHILD_VALUE_NAMES and not name.startswith(CHILD_FIELD_PREFIX)
|
|
319
|
+
]
|
|
320
|
+
if extension and stage in annotations.child_before_run:
|
|
321
|
+
raise ConfigurationError(
|
|
322
|
+
f"handler {handler.name!r} in per-child stage {stage!r} reads "
|
|
323
|
+
f"{{{{{extension[0]}}}}}, but that stage runs before the child "
|
|
324
|
+
"task exists; read child extension values only in the stages "
|
|
325
|
+
"after the one that runs the child (ww.child.id, text, project, "
|
|
326
|
+
"and field.* are readable in every stage)"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def compile(self, workflow_name: str) -> WorkflowPlan:
|
|
330
|
+
try:
|
|
331
|
+
workflow = self.configuration.workflows_by_name[workflow_name]
|
|
332
|
+
except KeyError as error:
|
|
333
|
+
raise ConfigurationError(f"workflow not found: {workflow_name}") from error
|
|
334
|
+
selected = (
|
|
335
|
+
self.options.modes if self.options.modes is not None else workflow.modes
|
|
336
|
+
)
|
|
337
|
+
self._selected_modes = self._resolve_modes(selected)
|
|
338
|
+
effective_steps = (implicit_init_step(), *workflow.steps)
|
|
339
|
+
items: list[PlanItem] = []
|
|
340
|
+
workflow_hints = ExecutionHints(self.agent).overlay(workflow)
|
|
341
|
+
start_values = self._append_hooks(
|
|
342
|
+
items,
|
|
343
|
+
workflow,
|
|
344
|
+
effective_steps[0],
|
|
345
|
+
INIT_STEP_NAME,
|
|
346
|
+
None,
|
|
347
|
+
"before_start_workflow",
|
|
348
|
+
(),
|
|
349
|
+
boundary_hints=workflow_hints,
|
|
350
|
+
)
|
|
351
|
+
available_values = self._compile_steps(
|
|
352
|
+
items,
|
|
353
|
+
workflow,
|
|
354
|
+
effective_steps,
|
|
355
|
+
None,
|
|
356
|
+
start_values,
|
|
357
|
+
parent_hints=workflow_hints,
|
|
358
|
+
)
|
|
359
|
+
# Validation leaves a handoff workflow's transition as its last item.
|
|
360
|
+
transition = (
|
|
361
|
+
items.pop()
|
|
362
|
+
if workflow.hands_off and workflow_transition(items[-1]) is not None
|
|
363
|
+
else None
|
|
364
|
+
)
|
|
365
|
+
terminal_step = effective_steps[-1]
|
|
366
|
+
self._append_hooks(
|
|
367
|
+
items,
|
|
368
|
+
workflow,
|
|
369
|
+
terminal_step,
|
|
370
|
+
terminal_step.name,
|
|
371
|
+
None,
|
|
372
|
+
"before_complete_workflow",
|
|
373
|
+
(),
|
|
374
|
+
available_values,
|
|
375
|
+
boundary_hints=workflow_hints,
|
|
376
|
+
)
|
|
377
|
+
if transition is not None:
|
|
378
|
+
items.append(transition)
|
|
379
|
+
if not workflow.hands_off:
|
|
380
|
+
self._append_handler(
|
|
381
|
+
items,
|
|
382
|
+
workflow,
|
|
383
|
+
terminal_step,
|
|
384
|
+
terminal_step.name,
|
|
385
|
+
None,
|
|
386
|
+
"before_complete_workflow",
|
|
387
|
+
"internal",
|
|
388
|
+
HandlerDefinition(
|
|
389
|
+
"update-workflow-summary",
|
|
390
|
+
description=(
|
|
391
|
+
"Provide a concise summary of this workflow run's "
|
|
392
|
+
"goal and result."
|
|
393
|
+
),
|
|
394
|
+
action=DefinedAction(
|
|
395
|
+
"prompt",
|
|
396
|
+
Prompt(
|
|
397
|
+
"Provide a concise summary of this workflow run's "
|
|
398
|
+
"goal and result."
|
|
399
|
+
),
|
|
400
|
+
),
|
|
401
|
+
provide=(
|
|
402
|
+
ProvidedVariable(
|
|
403
|
+
"summary",
|
|
404
|
+
"A short goal/result summary for the task workflow ledger.",
|
|
405
|
+
),
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
(),
|
|
409
|
+
summary=True,
|
|
410
|
+
boundary_hints=ExecutionHints.builtin(
|
|
411
|
+
self.agent, self.project_config.builtin_settings("workflow_summary")
|
|
412
|
+
),
|
|
413
|
+
annotations=step_annotations(terminal_step),
|
|
414
|
+
)
|
|
415
|
+
items = self._mark_child_identity(items)
|
|
416
|
+
plan = WorkflowPlan(
|
|
417
|
+
workflow=workflow.name,
|
|
418
|
+
workflow_description=workflow.description,
|
|
419
|
+
agent=self.agent,
|
|
420
|
+
task_id=self.task_id,
|
|
421
|
+
modes=selected,
|
|
422
|
+
handoff=workflow.hands_off,
|
|
423
|
+
items=number_step_paths(tuple(items)),
|
|
424
|
+
documents=self.configuration.documents,
|
|
425
|
+
recommended_next_workflow=workflow.recommended_next_workflow,
|
|
426
|
+
hooks_from=workflow.hooks_from,
|
|
427
|
+
)
|
|
428
|
+
return self._apply_options(plan)
|
|
429
|
+
|
|
430
|
+
def _mark_child_identity(self, items: list[PlanItem]) -> list[PlanItem]:
|
|
431
|
+
"""Flag the children collection when the child workflow binds its ID."""
|
|
432
|
+
target = next(
|
|
433
|
+
(
|
|
434
|
+
coordinator.workflow
|
|
435
|
+
for coordinator in map(child_workflow, items)
|
|
436
|
+
if coordinator is not None
|
|
437
|
+
),
|
|
438
|
+
None,
|
|
439
|
+
)
|
|
440
|
+
if target is None:
|
|
441
|
+
return items
|
|
442
|
+
child = self.configuration.workflows_by_name.get(target)
|
|
443
|
+
if child is None or not binds_task_identity(child):
|
|
444
|
+
return items
|
|
445
|
+
return [
|
|
446
|
+
replace(item, child_identity=True)
|
|
447
|
+
if item.child_operation == "collect"
|
|
448
|
+
else item
|
|
449
|
+
for item in items
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
def _apply_options(self, plan: WorkflowPlan) -> WorkflowPlan:
|
|
453
|
+
completed = self.options.completed_bootstrap_step
|
|
454
|
+
if completed is not None:
|
|
455
|
+
if self.options.task_id is None:
|
|
456
|
+
raise ConfigurationError(
|
|
457
|
+
"a completed bootstrap step requires an authoritative task ID"
|
|
458
|
+
)
|
|
459
|
+
if not any(
|
|
460
|
+
item.step == completed and item.phase == "step" for item in plan.items
|
|
461
|
+
):
|
|
462
|
+
raise ConfigurationError(
|
|
463
|
+
f"completed bootstrap step not found in plan: {completed}"
|
|
464
|
+
)
|
|
465
|
+
retained = tuple(
|
|
466
|
+
item
|
|
467
|
+
for item in plan.items
|
|
468
|
+
if completed is None
|
|
469
|
+
or item.step != completed
|
|
470
|
+
or item.phase == "before_start_workflow"
|
|
471
|
+
)
|
|
472
|
+
if self.options.task_id is not None:
|
|
473
|
+
without_task_id = []
|
|
474
|
+
for item in retained:
|
|
475
|
+
provide = tuple(
|
|
476
|
+
value for value in item.provide if value.name != "task_id"
|
|
477
|
+
)
|
|
478
|
+
without_task_id.append(
|
|
479
|
+
replace(
|
|
480
|
+
item,
|
|
481
|
+
provide=provide,
|
|
482
|
+
requires_agent_input=(
|
|
483
|
+
item.execution == "automatic" and bool(provide)
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
retained = tuple(without_task_id)
|
|
488
|
+
return replace(
|
|
489
|
+
plan,
|
|
490
|
+
items=tuple(
|
|
491
|
+
replace(item, position=index) for index, item in enumerate(retained, 1)
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def _compile_steps(
|
|
496
|
+
self,
|
|
497
|
+
items: list[PlanItem],
|
|
498
|
+
workflow: WorkflowDefinition,
|
|
499
|
+
steps: tuple[StepDefinition, ...],
|
|
500
|
+
parent: str | None,
|
|
501
|
+
available_values: tuple[str, ...],
|
|
502
|
+
item_template: bool = False,
|
|
503
|
+
parent_ancestors: tuple[str, ...] = (),
|
|
504
|
+
parent_hints: ExecutionHints | None = None,
|
|
505
|
+
annotations: ItemAnnotations = EMPTY_ITEM_ANNOTATIONS,
|
|
506
|
+
) -> tuple[str, ...]:
|
|
507
|
+
"""Flatten a recursive step tree while retaining each logical lifecycle."""
|
|
508
|
+
values = available_values
|
|
509
|
+
for step in steps:
|
|
510
|
+
inherited_hints = parent_hints or ExecutionHints(self.agent).overlay(
|
|
511
|
+
workflow
|
|
512
|
+
)
|
|
513
|
+
step_hints = inherited_hints.overlay(step)
|
|
514
|
+
action_hints = step_hints
|
|
515
|
+
if step.name == INIT_STEP_NAME:
|
|
516
|
+
settings = self.project_config.builtin_settings("init")
|
|
517
|
+
action_hints = ExecutionHints.builtin(self.agent, settings)
|
|
518
|
+
path = f"{parent}/{step.name}" if parent else step.name
|
|
519
|
+
ancestors = (*parent_ancestors, parent) if parent else ()
|
|
520
|
+
scope = PlanningScope(
|
|
521
|
+
workflow,
|
|
522
|
+
step,
|
|
523
|
+
path,
|
|
524
|
+
parent,
|
|
525
|
+
ancestors,
|
|
526
|
+
values,
|
|
527
|
+
item_template,
|
|
528
|
+
annotations,
|
|
529
|
+
)
|
|
530
|
+
values = (
|
|
531
|
+
*values,
|
|
532
|
+
*self._append_hooks(
|
|
533
|
+
items,
|
|
534
|
+
workflow,
|
|
535
|
+
step,
|
|
536
|
+
path,
|
|
537
|
+
parent,
|
|
538
|
+
"before_start",
|
|
539
|
+
values,
|
|
540
|
+
item_template=item_template,
|
|
541
|
+
ancestors=ancestors,
|
|
542
|
+
boundary_hints=step_hints,
|
|
543
|
+
annotations=annotations,
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
scope = replace(scope, available_values=values)
|
|
547
|
+
|
|
548
|
+
def compile_nested(
|
|
549
|
+
nested_steps: tuple[StepDefinition, ...],
|
|
550
|
+
nested_scope: PlanningScope,
|
|
551
|
+
current_hints: ExecutionHints = step_hints,
|
|
552
|
+
) -> tuple[str, ...]:
|
|
553
|
+
return self._compile_steps(
|
|
554
|
+
items,
|
|
555
|
+
workflow,
|
|
556
|
+
nested_steps,
|
|
557
|
+
nested_scope.parent,
|
|
558
|
+
nested_scope.available_values,
|
|
559
|
+
nested_scope.item_template,
|
|
560
|
+
parent_ancestors=nested_scope.ancestors,
|
|
561
|
+
parent_hints=current_hints,
|
|
562
|
+
annotations=nested_scope.annotations,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def compile_region(
|
|
566
|
+
nested_steps: tuple[StepDefinition, ...],
|
|
567
|
+
nested_scope: PlanningScope,
|
|
568
|
+
region_annotations: ItemAnnotations,
|
|
569
|
+
) -> tuple[str, ...]:
|
|
570
|
+
start = len(items)
|
|
571
|
+
# A region's own tags replace the scope's, except that an
|
|
572
|
+
# outcome inside a per-item stage still belongs to its pass.
|
|
573
|
+
result = compile_nested(
|
|
574
|
+
nested_steps,
|
|
575
|
+
replace(
|
|
576
|
+
nested_scope,
|
|
577
|
+
annotations=replace(
|
|
578
|
+
region_annotations,
|
|
579
|
+
item_pass=nested_scope.annotations.item_pass,
|
|
580
|
+
),
|
|
581
|
+
),
|
|
582
|
+
)
|
|
583
|
+
for index in range(start, len(items)):
|
|
584
|
+
items[index] = _with_annotations(items[index], region_annotations)
|
|
585
|
+
return result
|
|
586
|
+
|
|
587
|
+
def emit_leaf(
|
|
588
|
+
request: LeafRequest,
|
|
589
|
+
current_step: StepDefinition = step,
|
|
590
|
+
current_path: str = path,
|
|
591
|
+
current_scope: PlanningScope = scope,
|
|
592
|
+
current_ancestors: tuple[str, ...] = ancestors,
|
|
593
|
+
current_action_hints: ExecutionHints = action_hints,
|
|
594
|
+
current_item_template: bool = item_template,
|
|
595
|
+
current_annotations: ItemAnnotations = annotations,
|
|
596
|
+
) -> tuple[str, ...]:
|
|
597
|
+
return self._append_handler(
|
|
598
|
+
items,
|
|
599
|
+
workflow,
|
|
600
|
+
current_step,
|
|
601
|
+
current_path,
|
|
602
|
+
parent,
|
|
603
|
+
"step",
|
|
604
|
+
"step",
|
|
605
|
+
request.reference,
|
|
606
|
+
current_scope.available_values,
|
|
607
|
+
item_template=current_item_template,
|
|
608
|
+
ancestors=current_ancestors,
|
|
609
|
+
boundary_hints=current_action_hints,
|
|
610
|
+
action_override=request.action,
|
|
611
|
+
annotations=_merge_annotations(
|
|
612
|
+
current_annotations, request.annotations
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
def emit_boundary(
|
|
617
|
+
request: LoopBoundaryRequest,
|
|
618
|
+
current_step: StepDefinition = step,
|
|
619
|
+
current_path: str = path,
|
|
620
|
+
current_ancestors: tuple[str, ...] = ancestors,
|
|
621
|
+
current_annotations: ItemAnnotations = annotations,
|
|
622
|
+
current_item_template: bool = item_template,
|
|
623
|
+
) -> None:
|
|
624
|
+
self._append_loop_control(
|
|
625
|
+
items,
|
|
626
|
+
workflow,
|
|
627
|
+
current_step,
|
|
628
|
+
current_path,
|
|
629
|
+
parent,
|
|
630
|
+
current_ancestors,
|
|
631
|
+
request,
|
|
632
|
+
current_annotations,
|
|
633
|
+
current_item_template,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
context = _CompilerPlanningContext(
|
|
637
|
+
scope,
|
|
638
|
+
self.project_config.limits.rounds,
|
|
639
|
+
compile_nested,
|
|
640
|
+
compile_region,
|
|
641
|
+
emit_leaf,
|
|
642
|
+
emit_boundary,
|
|
643
|
+
)
|
|
644
|
+
expansion = self.construct_planners.expand(
|
|
645
|
+
self.construct_normalizer(step), context
|
|
646
|
+
)
|
|
647
|
+
expanded_values = (
|
|
648
|
+
expansion.available_values
|
|
649
|
+
if expansion.available_values is not None
|
|
650
|
+
else (*values, *expansion.outputs)
|
|
651
|
+
)
|
|
652
|
+
available_after = (
|
|
653
|
+
*expanded_values,
|
|
654
|
+
*(item.name for item in step.provide),
|
|
655
|
+
)
|
|
656
|
+
available_after = (
|
|
657
|
+
*available_after,
|
|
658
|
+
*self._append_hooks(
|
|
659
|
+
items,
|
|
660
|
+
workflow,
|
|
661
|
+
step,
|
|
662
|
+
path,
|
|
663
|
+
parent,
|
|
664
|
+
"before_complete",
|
|
665
|
+
(),
|
|
666
|
+
available_after,
|
|
667
|
+
item_template=item_template,
|
|
668
|
+
ancestors=ancestors,
|
|
669
|
+
boundary_hints=step_hints,
|
|
670
|
+
annotations=annotations,
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
available_after = (
|
|
674
|
+
*available_after,
|
|
675
|
+
*self._append_hooks(
|
|
676
|
+
items,
|
|
677
|
+
workflow,
|
|
678
|
+
step,
|
|
679
|
+
path,
|
|
680
|
+
parent,
|
|
681
|
+
"after_complete",
|
|
682
|
+
(),
|
|
683
|
+
available_after,
|
|
684
|
+
item_template=item_template,
|
|
685
|
+
ancestors=ancestors,
|
|
686
|
+
boundary_hints=step_hints,
|
|
687
|
+
annotations=annotations,
|
|
688
|
+
),
|
|
689
|
+
)
|
|
690
|
+
if step.rules and (workflow.name, path) not in self._checked_steps:
|
|
691
|
+
raise ConfigurationError(
|
|
692
|
+
f"step {step.name!r} in workflow {workflow.name!r} declares "
|
|
693
|
+
"rules, but has no agent work of its own to deliver them to"
|
|
694
|
+
)
|
|
695
|
+
values = available_after
|
|
696
|
+
return values
|
|
697
|
+
|
|
698
|
+
def _append_loop_control(
|
|
699
|
+
self,
|
|
700
|
+
items: list[PlanItem],
|
|
701
|
+
workflow: WorkflowDefinition,
|
|
702
|
+
step: StepDefinition,
|
|
703
|
+
path: str,
|
|
704
|
+
parent: str | None,
|
|
705
|
+
ancestors: tuple[str, ...],
|
|
706
|
+
request: LoopBoundaryRequest,
|
|
707
|
+
annotations: ItemAnnotations,
|
|
708
|
+
item_template: bool = False,
|
|
709
|
+
) -> None:
|
|
710
|
+
"""Add a manager-owned boundary around an otherwise ordinary step tree.
|
|
711
|
+
|
|
712
|
+
Inside repeated stages the boundary is a template like the stages,
|
|
713
|
+
so each item or child gets its own loop.
|
|
714
|
+
"""
|
|
715
|
+
suffix = request.operation
|
|
716
|
+
description = step.description or f"Run the {step.name!r} loop."
|
|
717
|
+
items.append(
|
|
718
|
+
PlanItem(
|
|
719
|
+
id=f"{workflow.name}:{path}:loop:{suffix}:1",
|
|
720
|
+
position=len(items) + 1,
|
|
721
|
+
name=step.name,
|
|
722
|
+
description=description,
|
|
723
|
+
operation=LoopBoundary(path, request.operation, request.max_times),
|
|
724
|
+
owner="ww",
|
|
725
|
+
execution="loop_control",
|
|
726
|
+
requires_agent_input=False,
|
|
727
|
+
workflow=workflow.name,
|
|
728
|
+
step=path,
|
|
729
|
+
parent=parent,
|
|
730
|
+
phase="step",
|
|
731
|
+
source="step",
|
|
732
|
+
registered_handler=None,
|
|
733
|
+
artifact=request.artifact,
|
|
734
|
+
item_template=item_template,
|
|
735
|
+
item_pass=annotations.item_pass,
|
|
736
|
+
child_stage=annotations.child_stage,
|
|
737
|
+
ancestors=ancestors,
|
|
738
|
+
loop_break=None,
|
|
739
|
+
loop_continue=None,
|
|
740
|
+
assessment_question=annotations.assessment_question,
|
|
741
|
+
assessment_outcomes=annotations.assessment_outcomes,
|
|
742
|
+
assessment_stops=annotations.assessment_stops,
|
|
743
|
+
assessment_parent=annotations.assessment_parent,
|
|
744
|
+
assessment_outcome=annotations.assessment_outcome,
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
def _append_hooks(
|
|
749
|
+
self,
|
|
750
|
+
items: list[PlanItem],
|
|
751
|
+
workflow: WorkflowDefinition,
|
|
752
|
+
step: StepDefinition,
|
|
753
|
+
step_path: str,
|
|
754
|
+
parent: str | None,
|
|
755
|
+
phase: HookPhase,
|
|
756
|
+
before_variables: tuple[str, ...],
|
|
757
|
+
after_variables: tuple[str, ...] = (),
|
|
758
|
+
item_template: bool = False,
|
|
759
|
+
ancestors: tuple[str, ...] = (),
|
|
760
|
+
boundary_hints: ExecutionHints | None = None,
|
|
761
|
+
annotations: ItemAnnotations = EMPTY_ITEM_ANNOTATIONS,
|
|
762
|
+
) -> tuple[str, ...]:
|
|
763
|
+
available = (
|
|
764
|
+
before_variables
|
|
765
|
+
if phase in {"before_start_workflow", "before_start"}
|
|
766
|
+
else after_variables
|
|
767
|
+
)
|
|
768
|
+
hooks = (
|
|
769
|
+
*(hook for hook in self.configuration.global_hooks if hook.phase == phase),
|
|
770
|
+
*(hook for hook in workflow.hooks if hook.phase == phase),
|
|
771
|
+
*(hook for hook in step.hooks if hook.phase == phase),
|
|
772
|
+
)
|
|
773
|
+
hook_annotations = _merge_annotations(annotations, step_annotations(step))
|
|
774
|
+
produced: list[str] = []
|
|
775
|
+
for hook in self._expanded_hooks(hooks):
|
|
776
|
+
applies = hook.applies_in(
|
|
777
|
+
workflow,
|
|
778
|
+
step.name,
|
|
779
|
+
step_path,
|
|
780
|
+
_logical_step_paths(workflow.steps),
|
|
781
|
+
)
|
|
782
|
+
if (
|
|
783
|
+
applies
|
|
784
|
+
and hook.on_failure == "fix"
|
|
785
|
+
and (workflow.name, step_path) in self._checked_steps
|
|
786
|
+
):
|
|
787
|
+
# Compiled into the step's checks instead.
|
|
788
|
+
continue
|
|
789
|
+
if applies:
|
|
790
|
+
output_names = self._append_handler(
|
|
791
|
+
items,
|
|
792
|
+
workflow,
|
|
793
|
+
step,
|
|
794
|
+
step_path,
|
|
795
|
+
parent,
|
|
796
|
+
phase,
|
|
797
|
+
hook.scope,
|
|
798
|
+
replace(hook.handler, on_failure=hook.on_failure),
|
|
799
|
+
(*available, *produced),
|
|
800
|
+
item_template=item_template,
|
|
801
|
+
ancestors=ancestors,
|
|
802
|
+
boundary_hints=boundary_hints,
|
|
803
|
+
annotations=hook_annotations,
|
|
804
|
+
)
|
|
805
|
+
produced.extend(output_names)
|
|
806
|
+
return tuple(produced)
|
|
807
|
+
|
|
808
|
+
def _append_handler(
|
|
809
|
+
self,
|
|
810
|
+
items: list[PlanItem],
|
|
811
|
+
workflow: WorkflowDefinition,
|
|
812
|
+
step: StepDefinition,
|
|
813
|
+
step_path: str,
|
|
814
|
+
parent: str | None,
|
|
815
|
+
phase: PlanItemPhase,
|
|
816
|
+
source: str,
|
|
817
|
+
reference: HandlerDefinition,
|
|
818
|
+
available_variables: tuple[str, ...],
|
|
819
|
+
summary: bool = False,
|
|
820
|
+
item_template: bool = False,
|
|
821
|
+
ancestors: tuple[str, ...] = (),
|
|
822
|
+
boundary_hints: ExecutionHints | None = None,
|
|
823
|
+
action_override: DefinedAction | None = None,
|
|
824
|
+
annotations: ItemAnnotations = EMPTY_ITEM_ANNOTATIONS,
|
|
825
|
+
) -> tuple[str, ...]:
|
|
826
|
+
handler, registered_name, definition = self.actions._handler(
|
|
827
|
+
reference,
|
|
828
|
+
resolve_reference=(
|
|
829
|
+
phase != "step" or is_extension_reference(reference.name)
|
|
830
|
+
),
|
|
831
|
+
)
|
|
832
|
+
if handler.handlers:
|
|
833
|
+
produced: list[str] = []
|
|
834
|
+
for member in self.actions.automatic_members(handler):
|
|
835
|
+
member_hints = (
|
|
836
|
+
boundary_hints
|
|
837
|
+
or ExecutionHints(self.agent).overlay(workflow).overlay(step)
|
|
838
|
+
).overlay(member)
|
|
839
|
+
produced.extend(
|
|
840
|
+
self._append_handler(
|
|
841
|
+
items,
|
|
842
|
+
workflow,
|
|
843
|
+
step,
|
|
844
|
+
step_path,
|
|
845
|
+
parent,
|
|
846
|
+
phase,
|
|
847
|
+
source,
|
|
848
|
+
member,
|
|
849
|
+
(*available_variables, *produced),
|
|
850
|
+
summary=summary,
|
|
851
|
+
item_template=item_template,
|
|
852
|
+
ancestors=ancestors,
|
|
853
|
+
boundary_hints=member_hints,
|
|
854
|
+
annotations=annotations,
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
return tuple(produced)
|
|
858
|
+
hints = boundary_hints or ExecutionHints(self.agent).overlay(workflow).overlay(
|
|
859
|
+
step
|
|
860
|
+
)
|
|
861
|
+
if phase != "step" and definition is not None:
|
|
862
|
+
hints = hints.overlay(definition)
|
|
863
|
+
if phase != "step":
|
|
864
|
+
hints = hints.overlay(reference)
|
|
865
|
+
profile, profile_instruction, profile_path = (
|
|
866
|
+
self._resolve_profile(hints.profile, hints.profile_description)
|
|
867
|
+
if source == "step" and step.name != INIT_STEP_NAME
|
|
868
|
+
else (None, None, None)
|
|
869
|
+
)
|
|
870
|
+
# A step inherits its directory along the step chain; a hook works in
|
|
871
|
+
# the one it or the handler it names declares, else the task's.
|
|
872
|
+
workdir: Workdir = (
|
|
873
|
+
hints.workdir
|
|
874
|
+
if phase == "step"
|
|
875
|
+
else reference.workdir
|
|
876
|
+
or (definition.workdir if definition is not None else None)
|
|
877
|
+
or "task"
|
|
878
|
+
)
|
|
879
|
+
# A hook of a step the manager performs is the manager's too.
|
|
880
|
+
role: StepRole = (
|
|
881
|
+
"manager"
|
|
882
|
+
if hints.role == "manager" or (phase == "step" and step.interactive)
|
|
883
|
+
else "worker"
|
|
884
|
+
)
|
|
885
|
+
local = phase == "step" and role == "manager"
|
|
886
|
+
if local:
|
|
887
|
+
# An explicitly local step, or a conversation with the operator
|
|
888
|
+
# that only the talking session can hold, has no worker shape;
|
|
889
|
+
# whether its performer may spawn subagents still holds.
|
|
890
|
+
hints = ExecutionHints(self.agent, subagents=hints.subagents)
|
|
891
|
+
profile, profile_instruction, profile_path = (None, None, None)
|
|
892
|
+
operation = handler.operation
|
|
893
|
+
action: DefinedAction | None = None
|
|
894
|
+
owner: PlanItemOwner
|
|
895
|
+
execution: ExecutionKind
|
|
896
|
+
if operation is not None:
|
|
897
|
+
if action_override is not None:
|
|
898
|
+
raise ConfigurationError(
|
|
899
|
+
"a plan item cannot combine action and core operation"
|
|
900
|
+
)
|
|
901
|
+
kind, owner, execution = (
|
|
902
|
+
operation.kind,
|
|
903
|
+
operation.owner,
|
|
904
|
+
operation.execution,
|
|
905
|
+
)
|
|
906
|
+
else:
|
|
907
|
+
kind, owner, action = self.actions._resolve(handler)
|
|
908
|
+
if action_override is not None:
|
|
909
|
+
kind = action_override.identifier
|
|
910
|
+
owner = actions.get(kind).owner
|
|
911
|
+
action = action_override
|
|
912
|
+
execution = _execution_kind(kind)
|
|
913
|
+
if (
|
|
914
|
+
phase == "step"
|
|
915
|
+
and (step.loop_break is not None or step.loop_continue is not None)
|
|
916
|
+
and owner != "agent"
|
|
917
|
+
):
|
|
918
|
+
raise ConfigurationError(
|
|
919
|
+
f"step {step.name!r} uses break/continue but is not agent-owned"
|
|
920
|
+
)
|
|
921
|
+
if phase == "step" and step.role is not None and owner != "agent":
|
|
922
|
+
raise ConfigurationError(
|
|
923
|
+
f"step {step.name!r} sets role, but ww runs it: role applies to "
|
|
924
|
+
"agent steps only"
|
|
925
|
+
)
|
|
926
|
+
if handler.save_metadata and owner != "agent" and kind != "cli":
|
|
927
|
+
raise ConfigurationError(
|
|
928
|
+
f"handler {handler.name!r} can save metadata only when agent-owned "
|
|
929
|
+
"or using a shell or argv handler"
|
|
930
|
+
)
|
|
931
|
+
if handler.update_document and owner != "agent":
|
|
932
|
+
raise ConfigurationError(
|
|
933
|
+
f"handler {handler.name!r} can update documents only when agent-owned"
|
|
934
|
+
)
|
|
935
|
+
if handler.update_item and owner != "agent" and kind != "cli":
|
|
936
|
+
raise ConfigurationError(
|
|
937
|
+
f"handler {handler.name!r} can save item fields only when "
|
|
938
|
+
"agent-owned or using a shell or argv handler"
|
|
939
|
+
)
|
|
940
|
+
allowed = {
|
|
941
|
+
*CORE_VARIABLE_NAMES,
|
|
942
|
+
*available_variables,
|
|
943
|
+
*(item.name for item in handler.provide),
|
|
944
|
+
*(
|
|
945
|
+
f"{DOCUMENTS_PREFIX}{document.name}"
|
|
946
|
+
for document in self.configuration.documents
|
|
947
|
+
),
|
|
948
|
+
*self._namespace_variables,
|
|
949
|
+
*(self._child_variables if annotations.child_stage is not None else ()),
|
|
950
|
+
}
|
|
951
|
+
choice_value = json.dumps(
|
|
952
|
+
[choice.label for choice in step.choices], ensure_ascii=False
|
|
953
|
+
)
|
|
954
|
+
provided = tuple(
|
|
955
|
+
replace(
|
|
956
|
+
value,
|
|
957
|
+
description=interpolate(value.description, {CHOICES: choice_value}),
|
|
958
|
+
)
|
|
959
|
+
for value in handler.provide
|
|
960
|
+
)
|
|
961
|
+
if isinstance(operation, WorkflowHandoff):
|
|
962
|
+
operation = WorkflowHandoff(
|
|
963
|
+
self.actions._interpolate(operation.target, allowed)
|
|
964
|
+
)
|
|
965
|
+
compiled_operation: PlanOperation
|
|
966
|
+
if operation is not None:
|
|
967
|
+
compiled_operation = operation
|
|
968
|
+
else:
|
|
969
|
+
assert action is not None
|
|
970
|
+
planned_payload = self.actions.plan_action(action, allowed, workdir)
|
|
971
|
+
compiled_operation = PlannedAction(kind, planned_payload)
|
|
972
|
+
if (
|
|
973
|
+
handler.on_failure == "fix" or handler.on_failure_instruction is not None
|
|
974
|
+
) and kind != "cli":
|
|
975
|
+
raise ConfigurationError(
|
|
976
|
+
f"handler {handler.name!r}: on_failure repair requires "
|
|
977
|
+
"a command handler using shell/argv"
|
|
978
|
+
)
|
|
979
|
+
failure_instruction = (
|
|
980
|
+
self.actions._interpolate(handler.on_failure_instruction, allowed)
|
|
981
|
+
if handler.on_failure_instruction is not None
|
|
982
|
+
else None
|
|
983
|
+
)
|
|
984
|
+
description = (
|
|
985
|
+
self.actions._interpolate(handler.description, allowed)
|
|
986
|
+
if handler.description
|
|
987
|
+
else ""
|
|
988
|
+
)
|
|
989
|
+
dependency_names = tuple(
|
|
990
|
+
dict.fromkeys(
|
|
991
|
+
name
|
|
992
|
+
for value in (
|
|
993
|
+
*(
|
|
994
|
+
actions.get(action.identifier).templates(action.payload)
|
|
995
|
+
if action is not None
|
|
996
|
+
else ()
|
|
997
|
+
),
|
|
998
|
+
*(
|
|
999
|
+
(operation.target,)
|
|
1000
|
+
if isinstance(operation, WorkflowHandoff)
|
|
1001
|
+
else ()
|
|
1002
|
+
),
|
|
1003
|
+
handler.description,
|
|
1004
|
+
handler.on_failure_instruction,
|
|
1005
|
+
)
|
|
1006
|
+
if value is not None
|
|
1007
|
+
for name in dependencies(value)
|
|
1008
|
+
)
|
|
1009
|
+
)
|
|
1010
|
+
self._check_child_values(handler, dependency_names, step_path, annotations)
|
|
1011
|
+
rules: tuple[PlannedRule, ...] = ()
|
|
1012
|
+
checks: tuple[PlannedCheck, ...] = ()
|
|
1013
|
+
modes: tuple[PlannedMode, ...] = ()
|
|
1014
|
+
if (
|
|
1015
|
+
phase == "step"
|
|
1016
|
+
and owner == "agent"
|
|
1017
|
+
and not summary
|
|
1018
|
+
and step.name != INIT_STEP_NAME
|
|
1019
|
+
):
|
|
1020
|
+
rules, checks = self._step_rules(
|
|
1021
|
+
workflow, step, step_path, allowed, workdir
|
|
1022
|
+
)
|
|
1023
|
+
modes = self._step_modes(workflow, step, step_path)
|
|
1024
|
+
self._checked_steps.add((workflow.name, step_path))
|
|
1025
|
+
ordinal = (
|
|
1026
|
+
sum(
|
|
1027
|
+
1
|
|
1028
|
+
for item in items
|
|
1029
|
+
if item.workflow == workflow.name
|
|
1030
|
+
and item.step == step_path
|
|
1031
|
+
and item.phase == phase
|
|
1032
|
+
and item.source == source
|
|
1033
|
+
)
|
|
1034
|
+
+ 1
|
|
1035
|
+
)
|
|
1036
|
+
item_id = f"{workflow.name}:{step_path}:{phase}:{source}:{ordinal}"
|
|
1037
|
+
items.append(
|
|
1038
|
+
PlanItem(
|
|
1039
|
+
id=item_id,
|
|
1040
|
+
position=len(items) + 1,
|
|
1041
|
+
name=handler.name,
|
|
1042
|
+
description=description,
|
|
1043
|
+
operation=compiled_operation,
|
|
1044
|
+
on_failure=handler.on_failure or "operator",
|
|
1045
|
+
on_failure_instruction=failure_instruction,
|
|
1046
|
+
max_handler_fixes=self.project_config.limits.fixes,
|
|
1047
|
+
owner=owner,
|
|
1048
|
+
execution=execution,
|
|
1049
|
+
requires_agent_input=execution == "automatic" and bool(provided),
|
|
1050
|
+
workflow=workflow.name,
|
|
1051
|
+
step=step_path,
|
|
1052
|
+
parent=parent,
|
|
1053
|
+
phase=phase,
|
|
1054
|
+
source=source,
|
|
1055
|
+
registered_handler=registered_name,
|
|
1056
|
+
provide=provided,
|
|
1057
|
+
save_metadata=handler.save_metadata,
|
|
1058
|
+
update_document=handler.update_document,
|
|
1059
|
+
update_item=handler.update_item,
|
|
1060
|
+
outputs=handler.outputs,
|
|
1061
|
+
dependencies=dependency_names,
|
|
1062
|
+
requested_agent=hints.agent if not local else None,
|
|
1063
|
+
requested_model=hints.model if not local else None,
|
|
1064
|
+
requested_reasoning=hints.reasoning if not local else None,
|
|
1065
|
+
role=role,
|
|
1066
|
+
subagents=hints.subagents,
|
|
1067
|
+
interactive=step.interactive and phase == "step",
|
|
1068
|
+
explicit=step.explicit is True and phase == "step",
|
|
1069
|
+
learnable=step.learnable and phase == "step",
|
|
1070
|
+
choices=step.choices if phase == "step" else (),
|
|
1071
|
+
ui=step.ui and phase == "step",
|
|
1072
|
+
# Explicit retained aliases for plan schemas <= 3.
|
|
1073
|
+
model=hints.model if not local else None,
|
|
1074
|
+
reasoning=hints.reasoning if not local else None,
|
|
1075
|
+
profile=profile,
|
|
1076
|
+
profile_instruction=profile_instruction,
|
|
1077
|
+
profile_path=profile_path,
|
|
1078
|
+
workdir=workdir,
|
|
1079
|
+
summary=summary,
|
|
1080
|
+
item_operation=annotations.item_operation,
|
|
1081
|
+
item_template=item_template,
|
|
1082
|
+
item_pass=annotations.item_pass,
|
|
1083
|
+
item_collect_only=annotations.item_collect_only,
|
|
1084
|
+
item_assignment=annotations.item_assignment or "per_step",
|
|
1085
|
+
loop_id=annotations.loop_id,
|
|
1086
|
+
loop_assignment=annotations.loop_assignment,
|
|
1087
|
+
split_instruction=annotations.split_instruction,
|
|
1088
|
+
shared_items=annotations.shared_items,
|
|
1089
|
+
item_identity=annotations.item_identity,
|
|
1090
|
+
item_unique=annotations.item_unique,
|
|
1091
|
+
artifact=step.artifact,
|
|
1092
|
+
child_operation=annotations.child_operation,
|
|
1093
|
+
child_stage=annotations.child_stage,
|
|
1094
|
+
ancestors=ancestors,
|
|
1095
|
+
artifact_dependency=(
|
|
1096
|
+
_artifact_dependency_path(
|
|
1097
|
+
step.artifact_dependency, ancestors, items
|
|
1098
|
+
)
|
|
1099
|
+
if phase == "step" and step.artifact_dependency is not None
|
|
1100
|
+
else None
|
|
1101
|
+
),
|
|
1102
|
+
loop_break=step.loop_break if phase == "step" else None,
|
|
1103
|
+
loop_continue=step.loop_continue if phase == "step" else None,
|
|
1104
|
+
assessment_question=annotations.assessment_question,
|
|
1105
|
+
assessment_outcomes=annotations.assessment_outcomes,
|
|
1106
|
+
assessment_stops=annotations.assessment_stops,
|
|
1107
|
+
assessment_parent=annotations.assessment_parent,
|
|
1108
|
+
assessment_outcome=annotations.assessment_outcome,
|
|
1109
|
+
rules=rules,
|
|
1110
|
+
checks=checks,
|
|
1111
|
+
modes=modes,
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
return (
|
|
1115
|
+
*(item.name for item in handler.provide),
|
|
1116
|
+
*handler.outputs,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
def _resolve_modes(self, names: tuple[str, ...]) -> tuple[PlannedMode, ...]:
|
|
1120
|
+
"""The selected modes by name, with their descriptions frozen."""
|
|
1121
|
+
catalog = {mode.name: mode for mode in self.configuration.modes}
|
|
1122
|
+
resolved = []
|
|
1123
|
+
for name in dict.fromkeys(names):
|
|
1124
|
+
mode = catalog.get(name)
|
|
1125
|
+
if (
|
|
1126
|
+
mode is None
|
|
1127
|
+
and self.extensions is not None
|
|
1128
|
+
and is_extension_reference(name)
|
|
1129
|
+
):
|
|
1130
|
+
mode = self.extensions.mode(name)
|
|
1131
|
+
if mode is None:
|
|
1132
|
+
raise ConfigurationError(f"unknown mode(s): {name}")
|
|
1133
|
+
resolved.append(PlannedMode(mode.name, mode.description))
|
|
1134
|
+
return tuple(resolved)
|
|
1135
|
+
|
|
1136
|
+
def _step_modes(
|
|
1137
|
+
self, workflow: WorkflowDefinition, step: StepDefinition, step_path: str
|
|
1138
|
+
) -> tuple[PlannedMode, ...]:
|
|
1139
|
+
"""The modes delivered to one agent step.
|
|
1140
|
+
|
|
1141
|
+
The run's selected modes come first, in their order, then every
|
|
1142
|
+
automatic mode whose filters admit the step, in declaration order; a
|
|
1143
|
+
mode both selected and automatic is listed once, as selected.
|
|
1144
|
+
"""
|
|
1145
|
+
precise = _logical_step_paths(workflow.steps)
|
|
1146
|
+
selected = {mode.name for mode in self._selected_modes}
|
|
1147
|
+
automatic = tuple(
|
|
1148
|
+
PlannedMode(mode.name, mode.description, automatic=True)
|
|
1149
|
+
for mode in self.configuration.modes
|
|
1150
|
+
if mode.name not in selected
|
|
1151
|
+
and mode.applies_to(workflow.name, step.name, step_path, precise)
|
|
1152
|
+
)
|
|
1153
|
+
return (*self._selected_modes, *automatic)
|
|
1154
|
+
|
|
1155
|
+
def _step_rules(
|
|
1156
|
+
self,
|
|
1157
|
+
workflow: WorkflowDefinition,
|
|
1158
|
+
step: StepDefinition,
|
|
1159
|
+
step_path: str,
|
|
1160
|
+
allowed: set[str],
|
|
1161
|
+
workdir: Workdir,
|
|
1162
|
+
) -> tuple[tuple[PlannedRule, ...], tuple[PlannedCheck, ...]]:
|
|
1163
|
+
"""The rules delivered to one agent step and the checks ww runs for it.
|
|
1164
|
+
|
|
1165
|
+
Root groups whose filters admit the step come first, in declaration
|
|
1166
|
+
order, then the step's own entries; a group the step names applies
|
|
1167
|
+
regardless of its filters. The first occurrence of a rule ID wins.
|
|
1168
|
+
Rule checks come in rule order, followed by the step's ``fix`` hooks.
|
|
1169
|
+
"""
|
|
1170
|
+
precise = _logical_step_paths(workflow.steps)
|
|
1171
|
+
groups = self.configuration.rule_groups_by_name
|
|
1172
|
+
collected: list[RuleDefinition] = [
|
|
1173
|
+
rule
|
|
1174
|
+
for group in self.configuration.rule_groups
|
|
1175
|
+
if group.applies_to(workflow.name, step.name, step_path, precise)
|
|
1176
|
+
for rule in group.rules
|
|
1177
|
+
]
|
|
1178
|
+
for entry in step.rules:
|
|
1179
|
+
if isinstance(entry, RuleGroupRef):
|
|
1180
|
+
collected.extend(groups[entry.name].rules)
|
|
1181
|
+
elif isinstance(entry, RuleDefinition):
|
|
1182
|
+
collected.append(entry)
|
|
1183
|
+
else: # pragma: no cover - the parser resolves every entry
|
|
1184
|
+
raise AssertionError(f"unresolved step rule {entry.value!r}")
|
|
1185
|
+
unique: dict[str, RuleDefinition] = {}
|
|
1186
|
+
for rule in collected:
|
|
1187
|
+
unique.setdefault(rule.id, rule)
|
|
1188
|
+
default_fixes = self.project_config.limits.fixes
|
|
1189
|
+
rules = tuple(
|
|
1190
|
+
PlannedRule(
|
|
1191
|
+
id=rule.id,
|
|
1192
|
+
summary=rule.summary,
|
|
1193
|
+
text=rule.text,
|
|
1194
|
+
text_hash=rule.text_hash,
|
|
1195
|
+
paths=rule.paths,
|
|
1196
|
+
has_command=rule.check is not None,
|
|
1197
|
+
max_fixes=rule.max_fixes or default_fixes,
|
|
1198
|
+
hints=rule.hints,
|
|
1199
|
+
source=rule_source(rule.source, self.root),
|
|
1200
|
+
)
|
|
1201
|
+
for rule in unique.values()
|
|
1202
|
+
)
|
|
1203
|
+
checks = [
|
|
1204
|
+
PlannedCheck(
|
|
1205
|
+
id=rule.id,
|
|
1206
|
+
source="rule",
|
|
1207
|
+
summary=rule.summary,
|
|
1208
|
+
command=self._plan_check(rule.check, allowed, workdir, rule.id),
|
|
1209
|
+
paths=rule.paths,
|
|
1210
|
+
max_fixes=rule.max_fixes or default_fixes,
|
|
1211
|
+
)
|
|
1212
|
+
for rule in unique.values()
|
|
1213
|
+
if rule.check is not None
|
|
1214
|
+
]
|
|
1215
|
+
taken = {check.id for check in checks}
|
|
1216
|
+
for hook in self._fix_hooks(workflow, step, step_path, precise):
|
|
1217
|
+
handler, _, _ = self.actions._handler(hook.handler, resolve_reference=True)
|
|
1218
|
+
action = handler.action
|
|
1219
|
+
if action is None or action.identifier != "cli" or handler.provide:
|
|
1220
|
+
raise ConfigurationError(
|
|
1221
|
+
f"{hook.path or 'hook'}: on_failure: fix requires a command "
|
|
1222
|
+
"handler that asks the agent for no values"
|
|
1223
|
+
)
|
|
1224
|
+
check_id = _unique_check_id(f"{step.name}/{_hook_label(handler)}", taken)
|
|
1225
|
+
taken.add(check_id)
|
|
1226
|
+
assert isinstance(action.payload, Commands)
|
|
1227
|
+
checks.append(
|
|
1228
|
+
PlannedCheck(
|
|
1229
|
+
id=check_id,
|
|
1230
|
+
source="hook",
|
|
1231
|
+
summary=handler.description or handler.name,
|
|
1232
|
+
command=self._plan_check(
|
|
1233
|
+
action.payload, allowed, workdir, check_id
|
|
1234
|
+
),
|
|
1235
|
+
max_fixes=default_fixes,
|
|
1236
|
+
on_failure_instruction=(
|
|
1237
|
+
self.actions._interpolate(
|
|
1238
|
+
handler.on_failure_instruction, allowed
|
|
1239
|
+
)
|
|
1240
|
+
if handler.on_failure_instruction is not None
|
|
1241
|
+
else None
|
|
1242
|
+
),
|
|
1243
|
+
)
|
|
1244
|
+
)
|
|
1245
|
+
return rules, tuple(checks)
|
|
1246
|
+
|
|
1247
|
+
def _expanded_hooks(
|
|
1248
|
+
self, hooks: tuple[HookDefinition, ...]
|
|
1249
|
+
) -> tuple[HookDefinition, ...]:
|
|
1250
|
+
result = []
|
|
1251
|
+
for hook in hooks:
|
|
1252
|
+
catalog = self.configuration.handlers_by_name.get(hook.handler.name)
|
|
1253
|
+
if not hook.handler.handlers and not (
|
|
1254
|
+
hook.handler.is_reference and catalog is not None and catalog.handlers
|
|
1255
|
+
):
|
|
1256
|
+
result.append(hook)
|
|
1257
|
+
continue
|
|
1258
|
+
handler, _, _ = self.actions._handler(hook.handler, resolve_reference=True)
|
|
1259
|
+
if not handler.handlers:
|
|
1260
|
+
result.append(hook)
|
|
1261
|
+
continue
|
|
1262
|
+
handler = replace(
|
|
1263
|
+
handler,
|
|
1264
|
+
on_failure=hook.on_failure,
|
|
1265
|
+
on_failure_instruction=hook.handler.on_failure_instruction
|
|
1266
|
+
or handler.on_failure_instruction,
|
|
1267
|
+
)
|
|
1268
|
+
result.extend(
|
|
1269
|
+
replace(
|
|
1270
|
+
hook, handler=member, on_failure=member.on_failure or "operator"
|
|
1271
|
+
)
|
|
1272
|
+
for member in self.actions.automatic_members(handler)
|
|
1273
|
+
)
|
|
1274
|
+
return tuple(result)
|
|
1275
|
+
|
|
1276
|
+
def _fix_hooks(
|
|
1277
|
+
self,
|
|
1278
|
+
workflow: WorkflowDefinition,
|
|
1279
|
+
step: StepDefinition,
|
|
1280
|
+
step_path: str,
|
|
1281
|
+
precise: frozenset[str],
|
|
1282
|
+
) -> tuple[HookDefinition, ...]:
|
|
1283
|
+
"""The ``before_complete`` hooks with ``on_failure: fix`` for one step."""
|
|
1284
|
+
return tuple(
|
|
1285
|
+
hook
|
|
1286
|
+
for hook in self._expanded_hooks(
|
|
1287
|
+
(
|
|
1288
|
+
*self.configuration.global_hooks,
|
|
1289
|
+
*workflow.hooks,
|
|
1290
|
+
*step.hooks,
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1293
|
+
if hook.phase == "before_complete"
|
|
1294
|
+
and hook.on_failure == "fix"
|
|
1295
|
+
and hook.applies_in(workflow, step.name, step_path, precise)
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
def _plan_check(
|
|
1299
|
+
self, command: Commands, allowed: set[str], workdir: Workdir, check_id: str
|
|
1300
|
+
) -> Commands:
|
|
1301
|
+
try:
|
|
1302
|
+
planned = self.actions.plan_action(
|
|
1303
|
+
DefinedAction("cli", command), allowed, workdir
|
|
1304
|
+
)
|
|
1305
|
+
except ConfigurationError as error:
|
|
1306
|
+
raise ConfigurationError(f"check {check_id!r}: {error}") from error
|
|
1307
|
+
assert isinstance(planned, Commands)
|
|
1308
|
+
return planned
|
|
1309
|
+
|
|
1310
|
+
def _resolve_profile(
|
|
1311
|
+
self, name: str | None, description: str | None
|
|
1312
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
1313
|
+
"""Return the profile name, its configured text, and its file.
|
|
1314
|
+
|
|
1315
|
+
A project-local profile file is recorded relative to the project root,
|
|
1316
|
+
so a plan compiled on one filesystem prints the right path on another.
|
|
1317
|
+
"""
|
|
1318
|
+
if name is not None:
|
|
1319
|
+
profile_path = self.discovery.profile(self.agent, name)
|
|
1320
|
+
if profile_path is not None:
|
|
1321
|
+
return name, None, relative_workspace(self.root, profile_path)
|
|
1322
|
+
configured = self.configuration.profiles_by_name.get(name) if name else None
|
|
1323
|
+
text = description or (configured.description if configured else None)
|
|
1324
|
+
if text:
|
|
1325
|
+
return name, text, None
|
|
1326
|
+
if name:
|
|
1327
|
+
return name, f"Use the `{name}` profile.", None
|
|
1328
|
+
return None, None, None
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def compile_workflow_plan(
|
|
1332
|
+
configuration: WorkflowConfiguration,
|
|
1333
|
+
root: Path,
|
|
1334
|
+
workflow: str,
|
|
1335
|
+
agent: str,
|
|
1336
|
+
task_id: str | None = None,
|
|
1337
|
+
extensions: ExtensionRegistry | None = None,
|
|
1338
|
+
options: PlanCompilationOptions | None = None,
|
|
1339
|
+
project_config: ProjectConfig | None = None,
|
|
1340
|
+
) -> WorkflowPlan:
|
|
1341
|
+
return WorkflowPlanCompiler(
|
|
1342
|
+
configuration, root, agent, task_id, extensions, options, project_config
|
|
1343
|
+
).compile(workflow)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def _execution_kind(kind: PlanItemKind) -> ExecutionKind:
|
|
1347
|
+
return actions.get(kind).execution
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _merge_annotations(
|
|
1351
|
+
inherited: ItemAnnotations, emitted: ItemAnnotations
|
|
1352
|
+
) -> ItemAnnotations:
|
|
1353
|
+
"""Apply a construct's local choices without losing scoped outcome tags."""
|
|
1354
|
+
return ItemAnnotations(
|
|
1355
|
+
item_operation=(
|
|
1356
|
+
emitted.item_operation
|
|
1357
|
+
if emitted.item_operation is not None
|
|
1358
|
+
else inherited.item_operation
|
|
1359
|
+
),
|
|
1360
|
+
child_operation=(
|
|
1361
|
+
emitted.child_operation
|
|
1362
|
+
if emitted.child_operation is not None
|
|
1363
|
+
else inherited.child_operation
|
|
1364
|
+
),
|
|
1365
|
+
assessment_question=(
|
|
1366
|
+
emitted.assessment_question
|
|
1367
|
+
if emitted.assessment_question is not None
|
|
1368
|
+
else inherited.assessment_question
|
|
1369
|
+
),
|
|
1370
|
+
assessment_outcomes=(
|
|
1371
|
+
emitted.assessment_outcomes
|
|
1372
|
+
if emitted.assessment_outcomes
|
|
1373
|
+
else inherited.assessment_outcomes
|
|
1374
|
+
),
|
|
1375
|
+
assessment_stops=(
|
|
1376
|
+
emitted.assessment_stops
|
|
1377
|
+
if emitted.assessment_outcomes
|
|
1378
|
+
else inherited.assessment_stops
|
|
1379
|
+
),
|
|
1380
|
+
assessment_parent=(
|
|
1381
|
+
emitted.assessment_parent
|
|
1382
|
+
if emitted.assessment_parent is not None
|
|
1383
|
+
else inherited.assessment_parent
|
|
1384
|
+
),
|
|
1385
|
+
assessment_outcome=(
|
|
1386
|
+
emitted.assessment_outcome
|
|
1387
|
+
if emitted.assessment_outcome is not None
|
|
1388
|
+
else inherited.assessment_outcome
|
|
1389
|
+
),
|
|
1390
|
+
item_assignment=(
|
|
1391
|
+
emitted.item_assignment
|
|
1392
|
+
if emitted.item_assignment is not None
|
|
1393
|
+
else inherited.item_assignment
|
|
1394
|
+
),
|
|
1395
|
+
item_pass=(
|
|
1396
|
+
emitted.item_pass if emitted.item_pass is not None else inherited.item_pass
|
|
1397
|
+
),
|
|
1398
|
+
item_collect_only=emitted.item_collect_only,
|
|
1399
|
+
loop_id=emitted.loop_id if emitted.loop_id is not None else inherited.loop_id,
|
|
1400
|
+
child_stage=(
|
|
1401
|
+
emitted.child_stage
|
|
1402
|
+
if emitted.child_stage is not None
|
|
1403
|
+
else inherited.child_stage
|
|
1404
|
+
),
|
|
1405
|
+
child_before_run=(
|
|
1406
|
+
emitted.child_before_run
|
|
1407
|
+
if emitted.child_stage is not None
|
|
1408
|
+
else inherited.child_before_run
|
|
1409
|
+
),
|
|
1410
|
+
loop_assignment=(
|
|
1411
|
+
emitted.loop_assignment
|
|
1412
|
+
if emitted.loop_assignment is not None
|
|
1413
|
+
else inherited.loop_assignment
|
|
1414
|
+
),
|
|
1415
|
+
split_instruction=emitted.split_instruction,
|
|
1416
|
+
shared_items=emitted.shared_items,
|
|
1417
|
+
item_identity=emitted.item_identity,
|
|
1418
|
+
item_unique=emitted.item_unique,
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
def _with_annotations(item: PlanItem, annotations: ItemAnnotations) -> PlanItem:
|
|
1423
|
+
"""``item`` with the assessment annotations of its scoped region."""
|
|
1424
|
+
return replace(
|
|
1425
|
+
item,
|
|
1426
|
+
assessment_parent=annotations.assessment_parent,
|
|
1427
|
+
assessment_outcome=annotations.assessment_outcome,
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def _artifact_dependency_path(
|
|
1432
|
+
name: str, ancestors: tuple[str, ...], items: list[PlanItem]
|
|
1433
|
+
) -> str:
|
|
1434
|
+
"""Return the plan path of the nearest earlier step named ``name``.
|
|
1435
|
+
|
|
1436
|
+
Validation has already chosen the step: an earlier sibling, else an
|
|
1437
|
+
earlier step of the nearest enclosing level. The same search over the
|
|
1438
|
+
already-compiled items gives its path, skipping a loop that is still
|
|
1439
|
+
running around the dependent step. A group emits no item of its own, so
|
|
1440
|
+
it is found as the ancestor of an earlier item, never as one enclosing
|
|
1441
|
+
the dependent step.
|
|
1442
|
+
"""
|
|
1443
|
+
earlier = {item.step: item for item in items if item.phase == "step"}
|
|
1444
|
+
groups = {
|
|
1445
|
+
ancestor
|
|
1446
|
+
for item in items
|
|
1447
|
+
if item.phase == "step"
|
|
1448
|
+
for ancestor in item.ancestors
|
|
1449
|
+
if ancestor not in ancestors
|
|
1450
|
+
}
|
|
1451
|
+
for container in (*reversed(ancestors), None):
|
|
1452
|
+
path = f"{container}/{name}" if container else name
|
|
1453
|
+
found = earlier.get(path)
|
|
1454
|
+
if found is not None and not (path in ancestors and found.kind == "loop"):
|
|
1455
|
+
return path
|
|
1456
|
+
if found is None and path in groups:
|
|
1457
|
+
return path
|
|
1458
|
+
return name
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
def _logical_step_paths(
|
|
1462
|
+
steps: tuple[StepDefinition, ...], parent: str | None = None
|
|
1463
|
+
) -> frozenset[str]:
|
|
1464
|
+
paths: set[str] = set()
|
|
1465
|
+
for step in steps:
|
|
1466
|
+
path = f"{parent}/{step.name}" if parent else step.name
|
|
1467
|
+
paths.add(path)
|
|
1468
|
+
item_steps = step.items.steps if step.items is not None else ()
|
|
1469
|
+
child_stages = step.children.steps if step.children is not None else ()
|
|
1470
|
+
for nested in (step.child_steps, step.loop_steps, item_steps, child_stages):
|
|
1471
|
+
paths.update(_logical_step_paths(nested, path))
|
|
1472
|
+
return frozenset(paths)
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _hook_label(handler: HandlerDefinition) -> str:
|
|
1476
|
+
"""A readable check name for a hook: its name, or the program it runs."""
|
|
1477
|
+
if not handler.name.startswith("inline-"):
|
|
1478
|
+
return handler.name
|
|
1479
|
+
assert handler.action is not None and isinstance(handler.action.payload, Commands)
|
|
1480
|
+
command = handler.action.payload.commands[0]
|
|
1481
|
+
if command.shell is not None:
|
|
1482
|
+
return "shell"
|
|
1483
|
+
return Path(command.argv[0]).name or "command"
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def _unique_check_id(candidate: str, taken: set[str]) -> str:
|
|
1487
|
+
if candidate not in taken:
|
|
1488
|
+
return candidate
|
|
1489
|
+
ordinal = 2
|
|
1490
|
+
while f"{candidate}-{ordinal}" in taken:
|
|
1491
|
+
ordinal += 1
|
|
1492
|
+
return f"{candidate}-{ordinal}"
|