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/config/steps.py
ADDED
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Recursive step parsing and reusable handler catalog parsing."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import Any, cast, get_args
|
|
8
|
+
|
|
9
|
+
from ww.actions import (
|
|
10
|
+
DefinedAction,
|
|
11
|
+
DefinitionOverrideContext,
|
|
12
|
+
Prompt,
|
|
13
|
+
actions,
|
|
14
|
+
)
|
|
15
|
+
from ww.contracts import ItemAssignment, ItemOperation, LoopAssignment, StepRole
|
|
16
|
+
from ww.errors import ConfigurationError
|
|
17
|
+
from ww.items import FIELD_NAME
|
|
18
|
+
from ww.operations import (
|
|
19
|
+
CHILD_LAUNCH_SETTINGS,
|
|
20
|
+
ChildLaunch,
|
|
21
|
+
ChildWorkflowRun,
|
|
22
|
+
WorkflowHandoff,
|
|
23
|
+
)
|
|
24
|
+
from ww.validation import is_positive_int
|
|
25
|
+
from ww.workflow_config import (
|
|
26
|
+
ChildFlow,
|
|
27
|
+
ChoiceDefinition,
|
|
28
|
+
HandlerDefinition,
|
|
29
|
+
HookDefinition,
|
|
30
|
+
ItemFlow,
|
|
31
|
+
StepDefinition,
|
|
32
|
+
StepRule,
|
|
33
|
+
step_tree,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from .actions import (
|
|
37
|
+
_bare_extension_reference,
|
|
38
|
+
_handler_keys,
|
|
39
|
+
_parse_handler,
|
|
40
|
+
_parse_hooks,
|
|
41
|
+
extension_reference,
|
|
42
|
+
)
|
|
43
|
+
from .rules import parse_step_rules
|
|
44
|
+
from .values import (
|
|
45
|
+
_NAME,
|
|
46
|
+
_description,
|
|
47
|
+
_mapping,
|
|
48
|
+
_named_entry,
|
|
49
|
+
_nonempty_string,
|
|
50
|
+
_only,
|
|
51
|
+
_optional_agent,
|
|
52
|
+
_optional_string,
|
|
53
|
+
_profile,
|
|
54
|
+
_role,
|
|
55
|
+
_subagents,
|
|
56
|
+
_unique,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
STEP_ONLY_KEYS: set[str] = {
|
|
60
|
+
"hooks",
|
|
61
|
+
"steps",
|
|
62
|
+
"loop",
|
|
63
|
+
"max_rounds",
|
|
64
|
+
"assignment",
|
|
65
|
+
"break",
|
|
66
|
+
"continue",
|
|
67
|
+
"role",
|
|
68
|
+
"subagents",
|
|
69
|
+
"interactive",
|
|
70
|
+
"explicit",
|
|
71
|
+
"learnable",
|
|
72
|
+
"choices",
|
|
73
|
+
"profile",
|
|
74
|
+
"items",
|
|
75
|
+
"item_phase",
|
|
76
|
+
"start_child",
|
|
77
|
+
"artifact_from",
|
|
78
|
+
"artifact",
|
|
79
|
+
"children",
|
|
80
|
+
"handler",
|
|
81
|
+
"question",
|
|
82
|
+
"outcomes",
|
|
83
|
+
"positive",
|
|
84
|
+
"negative",
|
|
85
|
+
"mixed",
|
|
86
|
+
"rules",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
CHILD_FLOW_KEYS = {"description", "workflow", "steps", "assignment"}
|
|
90
|
+
ITEM_FLOW_KEYS = {
|
|
91
|
+
"description",
|
|
92
|
+
"steps",
|
|
93
|
+
"assignment",
|
|
94
|
+
"persistent",
|
|
95
|
+
"analyze",
|
|
96
|
+
"resolve",
|
|
97
|
+
"report",
|
|
98
|
+
"variables",
|
|
99
|
+
"saves",
|
|
100
|
+
"interactive",
|
|
101
|
+
"choices",
|
|
102
|
+
"identity",
|
|
103
|
+
"unique",
|
|
104
|
+
"agent",
|
|
105
|
+
"model",
|
|
106
|
+
"reasoning",
|
|
107
|
+
"profile",
|
|
108
|
+
"role",
|
|
109
|
+
"subagents",
|
|
110
|
+
}
|
|
111
|
+
_ITEM_FLOW_SETTINGS = ("agent", "model", "reasoning", "profile", "role", "subagents")
|
|
112
|
+
BUILTIN_ITEM_STEP_NAME = "handle-item"
|
|
113
|
+
BUILTIN_ITEM_STEP_PROMPT = (
|
|
114
|
+
"Handle this item end to end: analyze it, resolve it, and report the outcome."
|
|
115
|
+
)
|
|
116
|
+
# Under ``items``, guidance for one phase of the built-in stage.
|
|
117
|
+
_ITEM_PHASE_GUIDANCE = (
|
|
118
|
+
("analyze", "When analyzing it"),
|
|
119
|
+
("resolve", "When resolving it"),
|
|
120
|
+
("report", "When reporting the outcome"),
|
|
121
|
+
)
|
|
122
|
+
# ``item_phase`` on a per-item stage, and the item operation it marks.
|
|
123
|
+
ITEM_PHASES: dict[str, ItemOperation] = {
|
|
124
|
+
"analyze": "process_item",
|
|
125
|
+
"resolve": "resolve_item",
|
|
126
|
+
"report": "report_item",
|
|
127
|
+
}
|
|
128
|
+
# ``interactive`` takes true (a conversation) or ``page`` (the operator page).
|
|
129
|
+
INTERACTIVE_PAGE = "page"
|
|
130
|
+
LOOP_ASSIGNMENTS: tuple[LoopAssignment, ...] = ("per_round", "per_step")
|
|
131
|
+
CHILD_ASSIGNMENTS = ("per_step",)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_handlers(data: Any) -> tuple[HandlerDefinition, ...]:
|
|
135
|
+
if data is None:
|
|
136
|
+
return ()
|
|
137
|
+
if not isinstance(data, list):
|
|
138
|
+
raise ConfigurationError("handlers must be a list")
|
|
139
|
+
# A catalog entry may be a complete step tree, not just an executable
|
|
140
|
+
# action. Parse entries in order so a handler can reuse an earlier one in
|
|
141
|
+
# the same way that workflow steps do.
|
|
142
|
+
result: list[HandlerDefinition] = []
|
|
143
|
+
known: dict[str, HandlerDefinition] = {}
|
|
144
|
+
for index, item in enumerate(data):
|
|
145
|
+
path = f"handlers[{index}]"
|
|
146
|
+
raw_mapping = _mapping(item, path)
|
|
147
|
+
# Unlike an action-only handler, a reusable step may use its
|
|
148
|
+
# named-entry value for the container definition itself:
|
|
149
|
+
# ``- review: {loop: [...]}``.
|
|
150
|
+
if "name" not in raw_mapping and raw_mapping:
|
|
151
|
+
name, value = next(iter(raw_mapping.items()))
|
|
152
|
+
if isinstance(value, dict):
|
|
153
|
+
if not isinstance(name, str) or not name.strip():
|
|
154
|
+
raise ConfigurationError(
|
|
155
|
+
f"{path} shorthand name must be a non-empty string"
|
|
156
|
+
)
|
|
157
|
+
remaining = dict(raw_mapping)
|
|
158
|
+
del remaining[name]
|
|
159
|
+
overlap = set(remaining).intersection(value)
|
|
160
|
+
if overlap:
|
|
161
|
+
raise ConfigurationError(
|
|
162
|
+
f"{path} repeats key(s): {', '.join(sorted(overlap))}"
|
|
163
|
+
)
|
|
164
|
+
mapping = {"name": name, **value, **remaining}
|
|
165
|
+
else:
|
|
166
|
+
mapping = _named_entry(raw_mapping, path)
|
|
167
|
+
else:
|
|
168
|
+
mapping = _named_entry(raw_mapping, path)
|
|
169
|
+
handler = (
|
|
170
|
+
_parse_step(mapping, path, known)
|
|
171
|
+
if STEP_ONLY_KEYS & set(mapping)
|
|
172
|
+
else _parse_handler(mapping, path)
|
|
173
|
+
)
|
|
174
|
+
result.append(handler)
|
|
175
|
+
known[handler.name] = handler
|
|
176
|
+
return tuple(result)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Keys that give a step content of its own; a step with none of them and a
|
|
180
|
+
# root handler of the same name refers to that handler.
|
|
181
|
+
_STEP_CONTENT_KEYS = frozenset(
|
|
182
|
+
{
|
|
183
|
+
"description",
|
|
184
|
+
"handler",
|
|
185
|
+
"kind",
|
|
186
|
+
"mcp",
|
|
187
|
+
"argv",
|
|
188
|
+
"shell",
|
|
189
|
+
"args",
|
|
190
|
+
"env",
|
|
191
|
+
"assert",
|
|
192
|
+
"idempotent",
|
|
193
|
+
"action",
|
|
194
|
+
"variables",
|
|
195
|
+
"saves",
|
|
196
|
+
"handlers",
|
|
197
|
+
"steps",
|
|
198
|
+
"loop",
|
|
199
|
+
"items",
|
|
200
|
+
"children",
|
|
201
|
+
"explicit",
|
|
202
|
+
"handoff_to",
|
|
203
|
+
"question",
|
|
204
|
+
"outcomes",
|
|
205
|
+
"positive",
|
|
206
|
+
"negative",
|
|
207
|
+
"mixed",
|
|
208
|
+
"item_phase",
|
|
209
|
+
"start_child",
|
|
210
|
+
"rules",
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _parse_choices(data: Any, path: str) -> tuple[ChoiceDefinition, ...]:
|
|
216
|
+
"""Parse ``choices``: named entries whose key is the label shown as is."""
|
|
217
|
+
if data is None:
|
|
218
|
+
return ()
|
|
219
|
+
if not isinstance(data, list) or not data:
|
|
220
|
+
raise ConfigurationError(f"{path}.choices must be a non-empty list")
|
|
221
|
+
result = []
|
|
222
|
+
for index, item in enumerate(data):
|
|
223
|
+
item_path = f"{path}.choices[{index}]"
|
|
224
|
+
mapping = _named_entry(_mapping(item, item_path), item_path)
|
|
225
|
+
_only(mapping, {"name", "description"}, item_path)
|
|
226
|
+
label = mapping.get("name")
|
|
227
|
+
if not isinstance(label, str) or not label.strip():
|
|
228
|
+
raise ConfigurationError(f"{item_path} label must be a non-empty string")
|
|
229
|
+
result.append(
|
|
230
|
+
ChoiceDefinition(
|
|
231
|
+
label.strip(), _description(mapping.get("description"), item_path)
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
_unique((choice.label for choice in result), f"choice label in {path}")
|
|
235
|
+
return tuple(result)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _is_bare_reference(mapping: dict[str, Any]) -> bool:
|
|
239
|
+
return isinstance(mapping.get("name"), str) and all(
|
|
240
|
+
mapping.get(key) is None for key in _STEP_CONTENT_KEYS
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _parse_step(
|
|
245
|
+
data: Any,
|
|
246
|
+
path: str,
|
|
247
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
248
|
+
*,
|
|
249
|
+
item_stage: bool = False,
|
|
250
|
+
) -> StepDefinition:
|
|
251
|
+
"""Parse one step entry, its nested steps included."""
|
|
252
|
+
mapping = _step_mapping(data, path, handlers_by_name)
|
|
253
|
+
base = (
|
|
254
|
+
extension_reference(mapping, path)
|
|
255
|
+
if _bare_extension_reference(mapping)
|
|
256
|
+
else _parse_handler(
|
|
257
|
+
mapping, path, transition=True, allowed_extra=STEP_ONLY_KEYS
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
question, outcomes, base = _parse_assessment(mapping, path, base, handlers_by_name)
|
|
261
|
+
base, referenced = _resolve_handler(mapping, path, base, handlers_by_name)
|
|
262
|
+
if base.handlers and (base.action is not None or base.operation is not None):
|
|
263
|
+
raise ConfigurationError(f"{path} cannot combine handlers with an action")
|
|
264
|
+
children = _parse_nested_steps(mapping, "steps", path, handlers_by_name)
|
|
265
|
+
if "steps" not in mapping and referenced is not None:
|
|
266
|
+
children = referenced.child_steps
|
|
267
|
+
loop_steps, max_rounds, loop_assignment = _parse_loop(
|
|
268
|
+
mapping, path, handlers_by_name, referenced
|
|
269
|
+
)
|
|
270
|
+
loop_break, loop_continue = _parse_loop_controls(mapping, path, referenced)
|
|
271
|
+
_check_loop_wrapper(mapping, path, base, loop_steps)
|
|
272
|
+
item_operation = _parse_item_phase(mapping, path)
|
|
273
|
+
child_launch = _parse_child_launch(mapping, path)
|
|
274
|
+
child_flow = _parse_child_flow(mapping, path, handlers_by_name, referenced)
|
|
275
|
+
if "children" in mapping and base.operation is not None:
|
|
276
|
+
raise ConfigurationError(
|
|
277
|
+
f"{path} cannot combine children with handoff_to; "
|
|
278
|
+
"name the child workflow under children.workflow"
|
|
279
|
+
)
|
|
280
|
+
artifact, artifact_from = _parse_artifact(mapping, path, referenced)
|
|
281
|
+
role, subagents, profile = _parse_performer(mapping, path, referenced)
|
|
282
|
+
interactive, ui, choices = _parse_interactive(mapping, path, referenced, item_stage)
|
|
283
|
+
explicit = mapping.get(
|
|
284
|
+
"explicit", referenced.explicit if referenced is not None else None
|
|
285
|
+
)
|
|
286
|
+
if "explicit" in mapping and not isinstance(explicit, bool):
|
|
287
|
+
raise ConfigurationError(f"{path}.explicit must be true or false")
|
|
288
|
+
if interactive and role == "worker":
|
|
289
|
+
raise ConfigurationError(
|
|
290
|
+
f"{path} is interactive, so the manager holds the conversation; "
|
|
291
|
+
"role: worker contradicts it"
|
|
292
|
+
)
|
|
293
|
+
learnable = mapping.get("learnable", referenced.learnable if referenced else False)
|
|
294
|
+
if not isinstance(learnable, bool):
|
|
295
|
+
raise ConfigurationError(f"{path}.learnable must be true or false")
|
|
296
|
+
if learnable and not artifact:
|
|
297
|
+
raise ConfigurationError(f"{path}.learnable requires artifact: true")
|
|
298
|
+
hooks = _parse_step_hooks(mapping, path, referenced)
|
|
299
|
+
rules = _parse_rules(mapping, path, base.name, referenced)
|
|
300
|
+
items = (
|
|
301
|
+
_parse_items(mapping, path, handlers_by_name, profile, role, subagents)
|
|
302
|
+
if "items" in mapping
|
|
303
|
+
else referenced.items
|
|
304
|
+
if referenced is not None
|
|
305
|
+
else None
|
|
306
|
+
)
|
|
307
|
+
containers = sum(
|
|
308
|
+
bool(value)
|
|
309
|
+
for value in (children, loop_steps, items is not None, child_flow is not None)
|
|
310
|
+
)
|
|
311
|
+
if base.handlers and containers:
|
|
312
|
+
raise ConfigurationError(
|
|
313
|
+
f"{path} cannot combine handlers with steps, loop, items, or children"
|
|
314
|
+
)
|
|
315
|
+
if containers > 1:
|
|
316
|
+
raise ConfigurationError(
|
|
317
|
+
f"{path} cannot combine steps, loop, items, and children"
|
|
318
|
+
)
|
|
319
|
+
if items is not None and item_operation is not None:
|
|
320
|
+
raise ConfigurationError(f"{path} cannot combine items with item_phase")
|
|
321
|
+
if item_operation is None and referenced is not None:
|
|
322
|
+
item_operation = referenced.item_operation
|
|
323
|
+
return StepDefinition(
|
|
324
|
+
name=base.name,
|
|
325
|
+
description=base.description,
|
|
326
|
+
action=base.action,
|
|
327
|
+
operation=base.operation,
|
|
328
|
+
provide=base.provide,
|
|
329
|
+
save_metadata=base.save_metadata,
|
|
330
|
+
update_document=base.update_document,
|
|
331
|
+
update_item=base.update_item,
|
|
332
|
+
outputs=base.outputs,
|
|
333
|
+
agent=base.agent,
|
|
334
|
+
model=base.model,
|
|
335
|
+
reasoning=base.reasoning,
|
|
336
|
+
workdir=base.workdir,
|
|
337
|
+
extension_arguments=base.extension_arguments,
|
|
338
|
+
on_failure=base.on_failure,
|
|
339
|
+
on_failure_instruction=base.on_failure_instruction,
|
|
340
|
+
handlers=base.handlers,
|
|
341
|
+
role=role,
|
|
342
|
+
subagents=subagents,
|
|
343
|
+
interactive=interactive,
|
|
344
|
+
explicit=explicit,
|
|
345
|
+
learnable=learnable,
|
|
346
|
+
choices=choices,
|
|
347
|
+
ui=ui,
|
|
348
|
+
profile=profile["profile"],
|
|
349
|
+
profile_description=profile["profile_description"],
|
|
350
|
+
hooks=hooks,
|
|
351
|
+
rules=rules,
|
|
352
|
+
child_steps=children,
|
|
353
|
+
loop_steps=loop_steps,
|
|
354
|
+
max_rounds=max_rounds,
|
|
355
|
+
loop_assignment=loop_assignment,
|
|
356
|
+
loop_break=loop_break,
|
|
357
|
+
loop_continue=loop_continue,
|
|
358
|
+
items=items,
|
|
359
|
+
item_operation=item_operation,
|
|
360
|
+
child_launch=child_launch,
|
|
361
|
+
artifact=artifact,
|
|
362
|
+
children=child_flow,
|
|
363
|
+
artifact_dependency=artifact_from,
|
|
364
|
+
assessment_question=question,
|
|
365
|
+
assessment_outcomes=outcomes,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _step_mapping(
|
|
370
|
+
data: Any, path: str, handlers_by_name: dict[str, HandlerDefinition]
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
"""Normalize a step entry to one mapping of its declared keys."""
|
|
373
|
+
raw = _mapping(data, path)
|
|
374
|
+
# ``assess`` is deliberately a construct rather than a conventional step
|
|
375
|
+
# name: its compact spelling is a question and its long spelling owns
|
|
376
|
+
# outcome substeps.
|
|
377
|
+
mapping: dict[str, Any]
|
|
378
|
+
if "assess" in raw:
|
|
379
|
+
value = raw["assess"]
|
|
380
|
+
rest = {key: item for key, item in raw.items() if key != "assess"}
|
|
381
|
+
if isinstance(value, str):
|
|
382
|
+
if rest:
|
|
383
|
+
raise ConfigurationError(
|
|
384
|
+
f"{path}.assess compact form cannot have other keys"
|
|
385
|
+
)
|
|
386
|
+
mapping = {"name": "assess", "question": value}
|
|
387
|
+
elif isinstance(value, dict):
|
|
388
|
+
overlap = set(rest).intersection(value)
|
|
389
|
+
if overlap:
|
|
390
|
+
raise ConfigurationError(
|
|
391
|
+
f"{path}.assess repeats key(s): {', '.join(sorted(overlap))}"
|
|
392
|
+
)
|
|
393
|
+
mapping = {"name": "assess", **value, **rest}
|
|
394
|
+
else:
|
|
395
|
+
raise ConfigurationError(
|
|
396
|
+
f"{path}.assess must be a question string or mapping"
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
399
|
+
mapping = _named_entry(raw, path)
|
|
400
|
+
if _is_bare_reference(mapping) and mapping["name"] in handlers_by_name:
|
|
401
|
+
# ``- fetch_requirements: ~`` with a root handler of that name means
|
|
402
|
+
# the handler, exactly as a bare hook entry does; settings such as
|
|
403
|
+
# ``profile`` or ``model`` on the step still override the copy.
|
|
404
|
+
mapping = {**mapping, "handler": mapping["name"]}
|
|
405
|
+
allowed = _handler_keys() | STEP_ONLY_KEYS | {"handoff_to"}
|
|
406
|
+
if mapping.get("name") == "assess":
|
|
407
|
+
unknown_branches = {
|
|
408
|
+
key
|
|
409
|
+
for key, value in mapping.items()
|
|
410
|
+
if key not in allowed and isinstance(value, dict)
|
|
411
|
+
}
|
|
412
|
+
if unknown_branches:
|
|
413
|
+
label = sorted(unknown_branches)[0]
|
|
414
|
+
raise ConfigurationError(
|
|
415
|
+
f"{path}.{label} is not a direct assessment branch; "
|
|
416
|
+
"use positive, negative, mixed, or outcomes"
|
|
417
|
+
)
|
|
418
|
+
_only(mapping, allowed, path)
|
|
419
|
+
return mapping
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _parse_assessment(
|
|
423
|
+
mapping: dict[str, Any],
|
|
424
|
+
path: str,
|
|
425
|
+
base: HandlerDefinition,
|
|
426
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
427
|
+
) -> tuple[str | None, tuple[StepDefinition, ...], HandlerDefinition]:
|
|
428
|
+
"""Parse an assess step's question and outcomes, and its prompt handler."""
|
|
429
|
+
question = (
|
|
430
|
+
_nonempty_string(mapping, "question", path) if "question" in mapping else None
|
|
431
|
+
)
|
|
432
|
+
if question is not None and mapping["name"] != "assess":
|
|
433
|
+
raise ConfigurationError(f"{path}.question is only valid for an assess step")
|
|
434
|
+
direct = {
|
|
435
|
+
label: mapping[label]
|
|
436
|
+
for label in ("positive", "negative", "mixed")
|
|
437
|
+
if label in mapping
|
|
438
|
+
}
|
|
439
|
+
if direct and question is None:
|
|
440
|
+
raise ConfigurationError(
|
|
441
|
+
f"{path}.{next(iter(direct))} requires an assess question"
|
|
442
|
+
)
|
|
443
|
+
if direct and "outcomes" in mapping:
|
|
444
|
+
raise ConfigurationError(
|
|
445
|
+
f"{path}.outcomes cannot be combined with direct assessment branches"
|
|
446
|
+
)
|
|
447
|
+
if "outcomes" in mapping and question is None:
|
|
448
|
+
raise ConfigurationError(f"{path}.outcomes requires an assess question")
|
|
449
|
+
outcomes = (
|
|
450
|
+
_parse_assessment_branches(direct, path, handlers_by_name, "")
|
|
451
|
+
if direct
|
|
452
|
+
else _parse_assessment_outcomes(mapping, path, handlers_by_name)
|
|
453
|
+
)
|
|
454
|
+
if question is None:
|
|
455
|
+
return None, outcomes, base
|
|
456
|
+
if any(
|
|
457
|
+
(
|
|
458
|
+
base.action is not None,
|
|
459
|
+
base.operation is not None,
|
|
460
|
+
"handler" in mapping,
|
|
461
|
+
"steps" in mapping,
|
|
462
|
+
"loop" in mapping,
|
|
463
|
+
)
|
|
464
|
+
):
|
|
465
|
+
raise ConfigurationError(
|
|
466
|
+
f"{path} assess cannot also declare an action or ordinary nested steps"
|
|
467
|
+
)
|
|
468
|
+
prompt = HandlerDefinition(
|
|
469
|
+
name="assess",
|
|
470
|
+
description=question,
|
|
471
|
+
action=DefinedAction("prompt", Prompt(question)),
|
|
472
|
+
workdir=base.workdir,
|
|
473
|
+
)
|
|
474
|
+
return question, outcomes, prompt
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _resolve_handler(
|
|
478
|
+
mapping: dict[str, Any],
|
|
479
|
+
path: str,
|
|
480
|
+
base: HandlerDefinition,
|
|
481
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
482
|
+
) -> tuple[HandlerDefinition, StepDefinition | None]:
|
|
483
|
+
"""Apply ``handler``, and return the step it names when it names one."""
|
|
484
|
+
if "handler" not in mapping:
|
|
485
|
+
return base, None
|
|
486
|
+
handler_name = mapping["handler"]
|
|
487
|
+
if not isinstance(handler_name, str) or not _NAME.fullmatch(handler_name):
|
|
488
|
+
raise ConfigurationError(f"{path}.handler must be a handler name")
|
|
489
|
+
try:
|
|
490
|
+
referenced = handlers_by_name[handler_name]
|
|
491
|
+
except KeyError as error:
|
|
492
|
+
raise ConfigurationError(
|
|
493
|
+
f"{path}.handler references unknown handler {handler_name!r}"
|
|
494
|
+
) from error
|
|
495
|
+
base = _step_handler_reference(mapping, base, referenced)
|
|
496
|
+
return base, referenced if isinstance(referenced, StepDefinition) else None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _parse_loop(
|
|
500
|
+
mapping: dict[str, Any],
|
|
501
|
+
path: str,
|
|
502
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
503
|
+
referenced: StepDefinition | None,
|
|
504
|
+
) -> tuple[tuple[StepDefinition, ...], int | None, LoopAssignment | None]:
|
|
505
|
+
"""Parse ``loop`` with its ``max_rounds`` and ``assignment``."""
|
|
506
|
+
loop_steps = _parse_nested_steps(
|
|
507
|
+
mapping, "loop", path, handlers_by_name, require_nonempty=True
|
|
508
|
+
)
|
|
509
|
+
max_rounds: int | None = mapping.get("max_rounds")
|
|
510
|
+
if max_rounds is not None and (not is_positive_int(max_rounds)):
|
|
511
|
+
raise ConfigurationError(f"{path}.max_rounds must be a positive integer")
|
|
512
|
+
if "max_rounds" in mapping and not loop_steps:
|
|
513
|
+
raise ConfigurationError(f"{path}.max_rounds requires a loop")
|
|
514
|
+
assignment: LoopAssignment | None = None
|
|
515
|
+
if "assignment" in mapping:
|
|
516
|
+
if not loop_steps:
|
|
517
|
+
raise ConfigurationError(
|
|
518
|
+
f"{path}.assignment on a step goes beside a loop; for items or "
|
|
519
|
+
"children, write it inside that mapping"
|
|
520
|
+
)
|
|
521
|
+
assignment = cast(
|
|
522
|
+
LoopAssignment,
|
|
523
|
+
_assignment(mapping["assignment"], f"{path}.assignment", LOOP_ASSIGNMENTS),
|
|
524
|
+
)
|
|
525
|
+
if referenced is not None:
|
|
526
|
+
if "loop" not in mapping:
|
|
527
|
+
loop_steps = referenced.loop_steps
|
|
528
|
+
if "max_rounds" not in mapping:
|
|
529
|
+
max_rounds = referenced.max_rounds
|
|
530
|
+
if "assignment" not in mapping:
|
|
531
|
+
assignment = referenced.loop_assignment
|
|
532
|
+
return loop_steps, max_rounds, assignment
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _parse_loop_controls(
|
|
536
|
+
mapping: dict[str, Any], path: str, referenced: StepDefinition | None
|
|
537
|
+
) -> tuple[str | None, str | None]:
|
|
538
|
+
"""Parse the loop's ``break`` and ``continue`` conditions."""
|
|
539
|
+
loop_break = mapping.get("break")
|
|
540
|
+
loop_continue = mapping.get("continue")
|
|
541
|
+
for control_name, control_value in (
|
|
542
|
+
("break", loop_break),
|
|
543
|
+
("continue", loop_continue),
|
|
544
|
+
):
|
|
545
|
+
if control_value is not None and (
|
|
546
|
+
not isinstance(control_value, str) or not control_value.strip()
|
|
547
|
+
):
|
|
548
|
+
raise ConfigurationError(
|
|
549
|
+
f"{path}.{control_name} must be a non-empty string"
|
|
550
|
+
)
|
|
551
|
+
if referenced is not None:
|
|
552
|
+
if "break" not in mapping:
|
|
553
|
+
loop_break = referenced.loop_break
|
|
554
|
+
if "continue" not in mapping:
|
|
555
|
+
loop_continue = referenced.loop_continue
|
|
556
|
+
return loop_break, loop_continue
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _check_loop_wrapper(
|
|
560
|
+
mapping: dict[str, Any],
|
|
561
|
+
path: str,
|
|
562
|
+
base: HandlerDefinition,
|
|
563
|
+
loop_steps: tuple[StepDefinition, ...],
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Reject a loop step that also acts or collects."""
|
|
566
|
+
if loop_steps and any(
|
|
567
|
+
(
|
|
568
|
+
base.action is not None,
|
|
569
|
+
base.operation is not None,
|
|
570
|
+
bool(base.provide),
|
|
571
|
+
bool(base.save_metadata),
|
|
572
|
+
bool(base.update_document),
|
|
573
|
+
bool(base.update_item),
|
|
574
|
+
bool(base.outputs),
|
|
575
|
+
"items" in mapping,
|
|
576
|
+
"item_phase" in mapping,
|
|
577
|
+
"children" in mapping,
|
|
578
|
+
"artifact_from" in mapping,
|
|
579
|
+
)
|
|
580
|
+
):
|
|
581
|
+
raise ConfigurationError(
|
|
582
|
+
f"{path} loop wrapper cannot also declare an action or collection"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _parse_item_phase(mapping: dict[str, Any], path: str) -> ItemOperation | None:
|
|
587
|
+
"""Parse ``item_phase`` into the item operation it marks."""
|
|
588
|
+
if "item_phase" not in mapping:
|
|
589
|
+
return None
|
|
590
|
+
phase = mapping["item_phase"]
|
|
591
|
+
if not isinstance(phase, str) or phase not in ITEM_PHASES:
|
|
592
|
+
raise ConfigurationError(
|
|
593
|
+
f"{path}.item_phase must be one of: " + ", ".join(ITEM_PHASES)
|
|
594
|
+
)
|
|
595
|
+
return ITEM_PHASES[phase]
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _parse_child_launch(mapping: dict[str, Any], path: str) -> ChildLaunch | None:
|
|
599
|
+
"""Parse ``start_child``: the launch settings ww starts the child with.
|
|
600
|
+
|
|
601
|
+
Every setting is an optional template over the child's record; an omitted
|
|
602
|
+
or empty one inherits, as an omitted ``start-child`` option does.
|
|
603
|
+
"""
|
|
604
|
+
if "start_child" not in mapping:
|
|
605
|
+
return None
|
|
606
|
+
launch_path = f"{path}.start_child"
|
|
607
|
+
value = mapping["start_child"]
|
|
608
|
+
if value is None:
|
|
609
|
+
value = {}
|
|
610
|
+
if not isinstance(value, dict):
|
|
611
|
+
raise ConfigurationError(f"{launch_path} must be a mapping of launch settings")
|
|
612
|
+
_only(value, set(CHILD_LAUNCH_SETTINGS), launch_path)
|
|
613
|
+
settings: dict[str, str] = {}
|
|
614
|
+
for name in CHILD_LAUNCH_SETTINGS:
|
|
615
|
+
if name in value:
|
|
616
|
+
setting = value[name]
|
|
617
|
+
if not isinstance(setting, str):
|
|
618
|
+
raise ConfigurationError(f"{launch_path}.{name} must be a string")
|
|
619
|
+
settings[name] = setting
|
|
620
|
+
return ChildLaunch(**settings)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _parse_child_flow(
|
|
624
|
+
mapping: dict[str, Any],
|
|
625
|
+
path: str,
|
|
626
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
627
|
+
referenced: StepDefinition | None,
|
|
628
|
+
) -> ChildFlow | None:
|
|
629
|
+
"""Parse ``children``, or take the referenced step's."""
|
|
630
|
+
if "children" in mapping:
|
|
631
|
+
return _parse_children(mapping, path, handlers_by_name)
|
|
632
|
+
return referenced.children if referenced is not None else None
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _parse_artifact(
|
|
636
|
+
mapping: dict[str, Any], path: str, referenced: StepDefinition | None
|
|
637
|
+
) -> tuple[bool, str | None]:
|
|
638
|
+
"""Parse ``artifact`` and ``artifact_from``."""
|
|
639
|
+
artifact = mapping.get("artifact", True)
|
|
640
|
+
if not isinstance(artifact, bool):
|
|
641
|
+
raise ConfigurationError(f"{path}.artifact must be true or false")
|
|
642
|
+
artifact_from = mapping.get("artifact_from")
|
|
643
|
+
if artifact_from is not None and (
|
|
644
|
+
not isinstance(artifact_from, str) or not _NAME.fullmatch(artifact_from)
|
|
645
|
+
):
|
|
646
|
+
raise ConfigurationError(f"{path}.artifact_from must be a normalized step name")
|
|
647
|
+
if referenced is not None:
|
|
648
|
+
if "artifact" not in mapping:
|
|
649
|
+
artifact = referenced.artifact
|
|
650
|
+
if "artifact_from" not in mapping:
|
|
651
|
+
artifact_from = referenced.artifact_dependency
|
|
652
|
+
return artifact, artifact_from
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _parse_performer(
|
|
656
|
+
mapping: dict[str, Any], path: str, referenced: StepDefinition | None
|
|
657
|
+
) -> tuple[StepRole | None, bool | None, dict[str, str | None]]:
|
|
658
|
+
"""Parse who performs the step: ``role``, ``subagents`` and ``profile``."""
|
|
659
|
+
role = _role(mapping, path)
|
|
660
|
+
subagents = _subagents(mapping, path)
|
|
661
|
+
profile = _profile(mapping, path)
|
|
662
|
+
if referenced is not None:
|
|
663
|
+
if "role" not in mapping:
|
|
664
|
+
role = referenced.role
|
|
665
|
+
if "subagents" not in mapping:
|
|
666
|
+
subagents = referenced.subagents
|
|
667
|
+
if "profile" not in mapping:
|
|
668
|
+
profile = {
|
|
669
|
+
"profile": referenced.profile,
|
|
670
|
+
"profile_description": referenced.profile_description,
|
|
671
|
+
}
|
|
672
|
+
return role, subagents, profile
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _parse_interactive(
|
|
676
|
+
mapping: dict[str, Any],
|
|
677
|
+
path: str,
|
|
678
|
+
referenced: StepDefinition | None,
|
|
679
|
+
item_stage: bool,
|
|
680
|
+
) -> tuple[bool, bool, tuple[ChoiceDefinition, ...]]:
|
|
681
|
+
"""Parse ``interactive`` and its ``choices``: whether, page, and choices."""
|
|
682
|
+
raw = mapping.get("interactive", False)
|
|
683
|
+
if not isinstance(raw, bool) and raw != INTERACTIVE_PAGE:
|
|
684
|
+
raise ConfigurationError(f"{path}.interactive must be true, false, or page")
|
|
685
|
+
interactive = raw is not False
|
|
686
|
+
ui = raw == INTERACTIVE_PAGE
|
|
687
|
+
choices = _parse_choices(mapping.get("choices"), path)
|
|
688
|
+
if referenced is not None:
|
|
689
|
+
if "interactive" not in mapping:
|
|
690
|
+
interactive = referenced.interactive
|
|
691
|
+
ui = referenced.ui
|
|
692
|
+
if "choices" not in mapping:
|
|
693
|
+
choices = referenced.choices
|
|
694
|
+
if choices and not interactive:
|
|
695
|
+
raise ConfigurationError(
|
|
696
|
+
f"{path}.choices are offered to the operator, so they require "
|
|
697
|
+
"interactive: true"
|
|
698
|
+
)
|
|
699
|
+
if ui and not item_stage:
|
|
700
|
+
raise ConfigurationError(
|
|
701
|
+
f"{path}.interactive: page is offered on per-item stages only; "
|
|
702
|
+
"declare it under items"
|
|
703
|
+
)
|
|
704
|
+
return interactive, ui, choices
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _parse_step_hooks(
|
|
708
|
+
mapping: dict[str, Any], path: str, referenced: StepDefinition | None
|
|
709
|
+
) -> tuple[HookDefinition, ...]:
|
|
710
|
+
"""Parse the step's ``hooks``, or take the referenced step's."""
|
|
711
|
+
if "hooks" in mapping or referenced is None:
|
|
712
|
+
return _parse_hooks(mapping.get("hooks", {}), "step", f"{path}.hooks")
|
|
713
|
+
return referenced.hooks
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _parse_rules(
|
|
717
|
+
mapping: dict[str, Any], path: str, name: str, referenced: StepDefinition | None
|
|
718
|
+
) -> tuple[StepRule, ...]:
|
|
719
|
+
"""Parse the step's ``rules``, or take the referenced step's."""
|
|
720
|
+
if "rules" in mapping:
|
|
721
|
+
return parse_step_rules(mapping["rules"], name, path)
|
|
722
|
+
return referenced.rules if referenced is not None else ()
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _parse_children(
|
|
726
|
+
mapping: dict[str, Any],
|
|
727
|
+
path: str,
|
|
728
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
729
|
+
) -> ChildFlow:
|
|
730
|
+
"""Parse ``children``: the child ``workflow``, or the parent's ``steps``."""
|
|
731
|
+
value = mapping["children"]
|
|
732
|
+
children_path = f"{path}.children"
|
|
733
|
+
if not isinstance(value, dict):
|
|
734
|
+
raise ConfigurationError(
|
|
735
|
+
f"{children_path} must be a mapping with the child workflow"
|
|
736
|
+
)
|
|
737
|
+
_only(value, CHILD_FLOW_KEYS, children_path)
|
|
738
|
+
description = _optional_string(value, "description", children_path)
|
|
739
|
+
if "assignment" in value:
|
|
740
|
+
if "steps" not in value:
|
|
741
|
+
raise ConfigurationError(
|
|
742
|
+
f"{children_path}.assignment splits children.steps into worker "
|
|
743
|
+
"assignments; without steps ww runs each child itself"
|
|
744
|
+
)
|
|
745
|
+
_assignment(
|
|
746
|
+
value["assignment"], f"{children_path}.assignment", CHILD_ASSIGNMENTS
|
|
747
|
+
)
|
|
748
|
+
if "steps" in value:
|
|
749
|
+
if "workflow" in value:
|
|
750
|
+
raise ConfigurationError(
|
|
751
|
+
f"{children_path} takes workflow or steps, not both: name the "
|
|
752
|
+
"workflow every child runs, or list the parent's stages per "
|
|
753
|
+
"child with one `workflow:` stage among them"
|
|
754
|
+
)
|
|
755
|
+
return _parse_child_stages(value, children_path, handlers_by_name, description)
|
|
756
|
+
workflow = value.get("workflow")
|
|
757
|
+
if not isinstance(workflow, str) or not _NAME.fullmatch(workflow):
|
|
758
|
+
raise ConfigurationError(f"{children_path}.workflow must be a workflow name")
|
|
759
|
+
return ChildFlow(workflow=workflow, description=description)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _parse_child_stages(
|
|
763
|
+
value: dict[str, Any],
|
|
764
|
+
children_path: str,
|
|
765
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
766
|
+
description: str | None,
|
|
767
|
+
) -> ChildFlow:
|
|
768
|
+
"""Parse ``children.steps``: the parent's stages, run once per child.
|
|
769
|
+
|
|
770
|
+
Exactly one top-level stage carries ``workflow:``; inside ``children`` it
|
|
771
|
+
runs the child task with that workflow and waits, so it becomes the
|
|
772
|
+
stage's ``ChildWorkflowRun``. A ``handoff_to`` transition cannot run
|
|
773
|
+
here.
|
|
774
|
+
"""
|
|
775
|
+
stages_path = f"{children_path}.steps"
|
|
776
|
+
if not isinstance(value["steps"], list) or not value["steps"]:
|
|
777
|
+
raise ConfigurationError(f"{stages_path} must contain at least one step")
|
|
778
|
+
entries = [_child_run_entry(entry) for entry in value["steps"]]
|
|
779
|
+
stages = _parse_nested_steps(
|
|
780
|
+
{"steps": [entry for entry, _ in entries]},
|
|
781
|
+
"steps",
|
|
782
|
+
children_path,
|
|
783
|
+
handlers_by_name,
|
|
784
|
+
)
|
|
785
|
+
runs = [
|
|
786
|
+
stage
|
|
787
|
+
for stage, (_, runs_child) in zip(stages, entries, strict=True)
|
|
788
|
+
if runs_child
|
|
789
|
+
]
|
|
790
|
+
if len(runs) != 1:
|
|
791
|
+
raise ConfigurationError(
|
|
792
|
+
f"{stages_path} needs exactly one stage with `workflow:`, the one "
|
|
793
|
+
f"that runs the child task; found {len(runs)}"
|
|
794
|
+
)
|
|
795
|
+
run = runs[0]
|
|
796
|
+
assert isinstance(run.operation, WorkflowHandoff)
|
|
797
|
+
target = run.operation.target
|
|
798
|
+
if not _NAME.fullmatch(target):
|
|
799
|
+
raise ConfigurationError(
|
|
800
|
+
f"{stages_path} stage {run.name!r}: workflow must be a workflow name"
|
|
801
|
+
)
|
|
802
|
+
if run.agent or run.model or run.reasoning:
|
|
803
|
+
raise ConfigurationError(
|
|
804
|
+
f"{stages_path} stage {run.name!r} runs the child task, which ww "
|
|
805
|
+
"does; agent, model, and reasoning do not apply to it"
|
|
806
|
+
)
|
|
807
|
+
child_run = replace(
|
|
808
|
+
run,
|
|
809
|
+
description=run.description
|
|
810
|
+
or f"Run the child task with the `{target}` workflow and wait for it.",
|
|
811
|
+
operation=ChildWorkflowRun(target, run.child_launch),
|
|
812
|
+
)
|
|
813
|
+
converted = tuple(child_run if stage is run else stage for stage in stages)
|
|
814
|
+
for stage in step_tree(converted):
|
|
815
|
+
if isinstance(stage.operation, WorkflowHandoff) or any(
|
|
816
|
+
isinstance(hook.handler.operation, WorkflowHandoff) for hook in stage.hooks
|
|
817
|
+
):
|
|
818
|
+
raise ConfigurationError(
|
|
819
|
+
f"{stages_path}: {stage.name!r} carries handoff_to, a workflow "
|
|
820
|
+
"transition, which cannot run inside children.steps; the stage "
|
|
821
|
+
"with `workflow:` runs the child task"
|
|
822
|
+
)
|
|
823
|
+
if stage.items is not None or stage.children is not None:
|
|
824
|
+
raise ConfigurationError(
|
|
825
|
+
f"{stages_path} stage {stage.name!r} cannot use items or "
|
|
826
|
+
"children: per-child stages do not nest another collection"
|
|
827
|
+
)
|
|
828
|
+
return ChildFlow(workflow=target, description=description, steps=converted)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _child_run_entry(entry: Any) -> tuple[Any, bool]:
|
|
832
|
+
"""A top-level ``children.steps`` entry, and whether it runs the child.
|
|
833
|
+
|
|
834
|
+
The stage that runs the child names its ``workflow``; it is parsed as a
|
|
835
|
+
transition to that workflow and then turned into the child run. It has
|
|
836
|
+
nothing to say but its workflow, so it may be written as its name
|
|
837
|
+
mapped to that setting: ``- implement: {workflow: task}``.
|
|
838
|
+
"""
|
|
839
|
+
if (
|
|
840
|
+
isinstance(entry, dict)
|
|
841
|
+
and len(entry) == 1
|
|
842
|
+
and "name" not in entry
|
|
843
|
+
and isinstance(next(iter(entry.values())), dict)
|
|
844
|
+
and {"workflow", "start_child"} & set(next(iter(entry.values())))
|
|
845
|
+
):
|
|
846
|
+
name, settings = next(iter(entry.items()))
|
|
847
|
+
entry = {"name": name, **settings}
|
|
848
|
+
if not isinstance(entry, dict) or "workflow" not in entry:
|
|
849
|
+
return entry, False
|
|
850
|
+
if "handoff_to" in entry:
|
|
851
|
+
raise ConfigurationError(
|
|
852
|
+
"a children.steps stage takes workflow (run the child) or "
|
|
853
|
+
"handoff_to, not both"
|
|
854
|
+
)
|
|
855
|
+
converted = {
|
|
856
|
+
("handoff_to" if key == "workflow" else key): value
|
|
857
|
+
for key, value in entry.items()
|
|
858
|
+
}
|
|
859
|
+
return converted, True
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _assignment(value: Any, path: str, allowed: tuple[str, ...]) -> str:
|
|
863
|
+
"""Check one ``assignment`` value against the ones its construct takes."""
|
|
864
|
+
if not isinstance(value, str):
|
|
865
|
+
raise ConfigurationError(f"{path} must be one of: " + ", ".join(allowed))
|
|
866
|
+
if value == "per_child":
|
|
867
|
+
raise ConfigurationError(
|
|
868
|
+
f"{path}: per_child is reserved and not built yet; use per_step"
|
|
869
|
+
)
|
|
870
|
+
if value not in allowed:
|
|
871
|
+
raise ConfigurationError(f"{path} must be one of: " + ", ".join(allowed))
|
|
872
|
+
return value
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _parse_items(
|
|
876
|
+
mapping: dict[str, Any],
|
|
877
|
+
path: str,
|
|
878
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
879
|
+
step_profile: dict[str, str | None],
|
|
880
|
+
step_role: StepRole | None,
|
|
881
|
+
step_subagents: bool | None,
|
|
882
|
+
) -> ItemFlow:
|
|
883
|
+
"""Parse ``items``: ``~``, splitting guidance text, or a full mapping."""
|
|
884
|
+
value = mapping["items"]
|
|
885
|
+
items_path = f"{path}.items"
|
|
886
|
+
if value is None:
|
|
887
|
+
value = {}
|
|
888
|
+
elif isinstance(value, str):
|
|
889
|
+
value = {"description": value}
|
|
890
|
+
elif not isinstance(value, dict):
|
|
891
|
+
raise ConfigurationError(
|
|
892
|
+
f"{items_path} must be null, splitting guidance text, or a mapping"
|
|
893
|
+
)
|
|
894
|
+
_only(value, ITEM_FLOW_KEYS, items_path)
|
|
895
|
+
description = (
|
|
896
|
+
_nonempty_string(value, "description", items_path)
|
|
897
|
+
if "description" in value
|
|
898
|
+
else None
|
|
899
|
+
)
|
|
900
|
+
persistent = value.get("persistent")
|
|
901
|
+
if "persistent" in value and not isinstance(persistent, bool):
|
|
902
|
+
raise ConfigurationError(f"{items_path}.persistent must be true or false")
|
|
903
|
+
identity = value.get("identity")
|
|
904
|
+
if identity is not None and (
|
|
905
|
+
not isinstance(identity, str) or not FIELD_NAME.fullmatch(identity)
|
|
906
|
+
):
|
|
907
|
+
raise ConfigurationError(f"{items_path}.identity must be a field name")
|
|
908
|
+
unique_raw = value.get("unique", [])
|
|
909
|
+
if not isinstance(unique_raw, list) or not all(
|
|
910
|
+
isinstance(name, str) and FIELD_NAME.fullmatch(name) for name in unique_raw
|
|
911
|
+
):
|
|
912
|
+
raise ConfigurationError(f"{items_path}.unique must be a list of field names")
|
|
913
|
+
unique = tuple(dict.fromkeys(unique_raw)) if "unique" in value else None
|
|
914
|
+
assignment = cast(
|
|
915
|
+
ItemAssignment,
|
|
916
|
+
_assignment(
|
|
917
|
+
value.get("assignment", "together"),
|
|
918
|
+
f"{items_path}.assignment",
|
|
919
|
+
get_args(ItemAssignment),
|
|
920
|
+
),
|
|
921
|
+
)
|
|
922
|
+
phase_keys = [key for key, _ in _ITEM_PHASE_GUIDANCE]
|
|
923
|
+
guidance = [
|
|
924
|
+
(label, _nonempty_string(value, key, items_path))
|
|
925
|
+
for key, label in _ITEM_PHASE_GUIDANCE
|
|
926
|
+
if key in value
|
|
927
|
+
]
|
|
928
|
+
if guidance and "steps" in value:
|
|
929
|
+
raise ConfigurationError(
|
|
930
|
+
f"{items_path} phase guidance ("
|
|
931
|
+
+ ", ".join(key for key in phase_keys if key in value)
|
|
932
|
+
+ ") describes the built-in handle-item stage; describe configured "
|
|
933
|
+
"steps directly"
|
|
934
|
+
)
|
|
935
|
+
folded = [
|
|
936
|
+
key for key in ("variables", "saves", "interactive", "choices") if key in value
|
|
937
|
+
]
|
|
938
|
+
if folded and "steps" in value:
|
|
939
|
+
raise ConfigurationError(
|
|
940
|
+
f"{items_path}.{folded[0]} belongs to the built-in handle-item stage; "
|
|
941
|
+
f"declare {folded[0]} on configured steps directly"
|
|
942
|
+
)
|
|
943
|
+
collect_only = "steps" in value and value["steps"] in ([], None)
|
|
944
|
+
if collect_only:
|
|
945
|
+
configured = [
|
|
946
|
+
key for key in ("assignment", *_ITEM_FLOW_SETTINGS) if key in value
|
|
947
|
+
]
|
|
948
|
+
if configured:
|
|
949
|
+
raise ConfigurationError(
|
|
950
|
+
f"{items_path} collects without per-item steps, so "
|
|
951
|
+
+ ", ".join(configured)
|
|
952
|
+
+ " has no effect"
|
|
953
|
+
)
|
|
954
|
+
return ItemFlow((), description, assignment, persistent, identity, unique)
|
|
955
|
+
defaults = _item_flow_defaults(value, path, step_profile, step_role, step_subagents)
|
|
956
|
+
if "steps" in value:
|
|
957
|
+
steps = _parse_nested_steps(
|
|
958
|
+
value,
|
|
959
|
+
"steps",
|
|
960
|
+
items_path,
|
|
961
|
+
handlers_by_name,
|
|
962
|
+
require_nonempty=True,
|
|
963
|
+
item_stage=True,
|
|
964
|
+
)
|
|
965
|
+
entries = value["steps"]
|
|
966
|
+
else:
|
|
967
|
+
builtin = {
|
|
968
|
+
"name": BUILTIN_ITEM_STEP_NAME,
|
|
969
|
+
"description": "\n\n".join(
|
|
970
|
+
(
|
|
971
|
+
BUILTIN_ITEM_STEP_PROMPT,
|
|
972
|
+
*(f"{label}: {text}" for label, text in guidance),
|
|
973
|
+
)
|
|
974
|
+
),
|
|
975
|
+
**{key: value[key] for key in folded},
|
|
976
|
+
}
|
|
977
|
+
steps = (
|
|
978
|
+
replace(
|
|
979
|
+
_parse_step(
|
|
980
|
+
builtin,
|
|
981
|
+
f"{items_path}.steps[0]",
|
|
982
|
+
handlers_by_name,
|
|
983
|
+
item_stage=True,
|
|
984
|
+
),
|
|
985
|
+
item_operation="handle_item",
|
|
986
|
+
),
|
|
987
|
+
)
|
|
988
|
+
entries = [builtin]
|
|
989
|
+
pages = [step.name for step in steps if step.ui]
|
|
990
|
+
if len(pages) > 1:
|
|
991
|
+
raise ConfigurationError(
|
|
992
|
+
f"{items_path} may answer one stage on the operator page; found "
|
|
993
|
+
"interactive: page on " + ", ".join(pages)
|
|
994
|
+
)
|
|
995
|
+
return ItemFlow(
|
|
996
|
+
tuple(
|
|
997
|
+
_with_item_flow_defaults(step, _declared_keys(entry), defaults)
|
|
998
|
+
for step, entry in zip(steps, entries, strict=True)
|
|
999
|
+
),
|
|
1000
|
+
description,
|
|
1001
|
+
assignment,
|
|
1002
|
+
persistent,
|
|
1003
|
+
identity,
|
|
1004
|
+
unique,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _item_flow_defaults(
|
|
1009
|
+
items_mapping: dict[str, Any],
|
|
1010
|
+
path: str,
|
|
1011
|
+
step_profile: dict[str, str | None],
|
|
1012
|
+
step_role: StepRole | None,
|
|
1013
|
+
step_subagents: bool | None,
|
|
1014
|
+
) -> dict[str, Any]:
|
|
1015
|
+
"""Worker settings every per-item stage inherits unless it sets its own.
|
|
1016
|
+
|
|
1017
|
+
``items`` settings win; profile and role otherwise come from the
|
|
1018
|
+
``items`` step itself. Agent, model, and reasoning already cascade from
|
|
1019
|
+
that step through the compiler's execution hints.
|
|
1020
|
+
"""
|
|
1021
|
+
items_path = f"{path}.items"
|
|
1022
|
+
role = _role(items_mapping, items_path)
|
|
1023
|
+
subagents = _subagents(items_mapping, items_path)
|
|
1024
|
+
return {
|
|
1025
|
+
"agent": _optional_agent(items_mapping, "agent", items_path),
|
|
1026
|
+
"model": _optional_string(items_mapping, "model", items_path),
|
|
1027
|
+
"reasoning": _optional_string(items_mapping, "reasoning", items_path),
|
|
1028
|
+
**(
|
|
1029
|
+
_profile(items_mapping, items_path)
|
|
1030
|
+
if "profile" in items_mapping
|
|
1031
|
+
else step_profile
|
|
1032
|
+
),
|
|
1033
|
+
"role": step_role if role is None else role,
|
|
1034
|
+
"subagents": step_subagents if subagents is None else subagents,
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _with_item_flow_defaults(
|
|
1039
|
+
step: StepDefinition, declared: set[str], defaults: dict[str, Any]
|
|
1040
|
+
) -> StepDefinition:
|
|
1041
|
+
"""Apply item-flow settings a stage neither declares nor copies from a handler."""
|
|
1042
|
+
changes: dict[str, Any] = {}
|
|
1043
|
+
for key in ("agent", "model", "reasoning"):
|
|
1044
|
+
if key not in declared and getattr(step, key) is None and defaults[key]:
|
|
1045
|
+
changes[key] = defaults[key]
|
|
1046
|
+
if (
|
|
1047
|
+
"profile" not in declared
|
|
1048
|
+
and step.profile is None
|
|
1049
|
+
and step.profile_description is None
|
|
1050
|
+
):
|
|
1051
|
+
changes["profile"] = defaults["profile"]
|
|
1052
|
+
changes["profile_description"] = defaults["profile_description"]
|
|
1053
|
+
if "role" not in declared and step.role is None and defaults["role"]:
|
|
1054
|
+
changes["role"] = defaults["role"]
|
|
1055
|
+
if (
|
|
1056
|
+
"subagents" not in declared
|
|
1057
|
+
and step.subagents is None
|
|
1058
|
+
and defaults["subagents"] is not None
|
|
1059
|
+
):
|
|
1060
|
+
changes["subagents"] = defaults["subagents"]
|
|
1061
|
+
return replace(step, **changes) if changes else step
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _declared_keys(entry: Any) -> set[str]:
|
|
1065
|
+
"""Return the keys a raw step entry sets, after shorthand expansion."""
|
|
1066
|
+
if not isinstance(entry, dict):
|
|
1067
|
+
return set()
|
|
1068
|
+
value = entry.get("assess")
|
|
1069
|
+
if isinstance(value, dict):
|
|
1070
|
+
return set(value) | (set(entry) - {"assess"})
|
|
1071
|
+
return set(entry)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _parse_assessment_outcomes(
|
|
1075
|
+
mapping: dict[str, Any], path: str, handlers_by_name: dict[str, HandlerDefinition]
|
|
1076
|
+
) -> tuple[StepDefinition, ...]:
|
|
1077
|
+
raw = mapping.get("outcomes", {})
|
|
1078
|
+
return _parse_assessment_branches(raw, path, handlers_by_name, ".outcomes")
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def _parse_assessment_branches(
|
|
1082
|
+
raw: Any,
|
|
1083
|
+
path: str,
|
|
1084
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
1085
|
+
field_path: str,
|
|
1086
|
+
) -> tuple[StepDefinition, ...]:
|
|
1087
|
+
"""Normalize wrapped outcomes or sibling standard branches to steps."""
|
|
1088
|
+
if raw is None:
|
|
1089
|
+
raw = {}
|
|
1090
|
+
if not isinstance(raw, dict):
|
|
1091
|
+
raise ConfigurationError(f"{path}{field_path} must be a mapping")
|
|
1092
|
+
result = []
|
|
1093
|
+
for label, value in raw.items():
|
|
1094
|
+
if not isinstance(label, str) or not _NAME.fullmatch(label):
|
|
1095
|
+
raise ConfigurationError(
|
|
1096
|
+
f"{path}{field_path} keys must be normalized names"
|
|
1097
|
+
)
|
|
1098
|
+
if not isinstance(value, dict):
|
|
1099
|
+
raise ConfigurationError(
|
|
1100
|
+
f"{path}{field_path}.{label} must be a step mapping"
|
|
1101
|
+
)
|
|
1102
|
+
if "name" in value:
|
|
1103
|
+
raise ConfigurationError(f"{path}{field_path}.{label} must not set name")
|
|
1104
|
+
if "stop_workflow" in value:
|
|
1105
|
+
if value != {"stop_workflow": True}:
|
|
1106
|
+
raise ConfigurationError(
|
|
1107
|
+
f"{path}{field_path}.{label}.stop_workflow must be true and "
|
|
1108
|
+
"stand alone: the outcome ends the workflow and runs nothing"
|
|
1109
|
+
)
|
|
1110
|
+
result.append(StepDefinition(label, stop_workflow=True))
|
|
1111
|
+
continue
|
|
1112
|
+
result.append(
|
|
1113
|
+
_parse_step(
|
|
1114
|
+
{"name": label, **value},
|
|
1115
|
+
f"{path}{field_path}.{label}",
|
|
1116
|
+
handlers_by_name,
|
|
1117
|
+
)
|
|
1118
|
+
)
|
|
1119
|
+
return tuple(result)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _parse_nested_steps(
|
|
1123
|
+
mapping: dict[str, Any],
|
|
1124
|
+
key: str,
|
|
1125
|
+
path: str,
|
|
1126
|
+
handlers_by_name: dict[str, HandlerDefinition],
|
|
1127
|
+
*,
|
|
1128
|
+
require_nonempty: bool = False,
|
|
1129
|
+
item_stage: bool = False,
|
|
1130
|
+
) -> tuple[StepDefinition, ...]:
|
|
1131
|
+
"""Parse an optional nested step list without collapsing key presence."""
|
|
1132
|
+
data = mapping.get(key, [])
|
|
1133
|
+
if data is None:
|
|
1134
|
+
data = []
|
|
1135
|
+
if not isinstance(data, list):
|
|
1136
|
+
raise ConfigurationError(f"{path}.{key} must be a list")
|
|
1137
|
+
if require_nonempty and key in mapping and not data:
|
|
1138
|
+
raise ConfigurationError(f"{path}.{key} must contain at least one step")
|
|
1139
|
+
return tuple(
|
|
1140
|
+
_parse_step(
|
|
1141
|
+
item, f"{path}.{key}[{index}]", handlers_by_name, item_stage=item_stage
|
|
1142
|
+
)
|
|
1143
|
+
for index, item in enumerate(data)
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _step_handler_reference(
|
|
1148
|
+
mapping: dict[str, Any],
|
|
1149
|
+
local: HandlerDefinition,
|
|
1150
|
+
referenced: HandlerDefinition,
|
|
1151
|
+
) -> HandlerDefinition:
|
|
1152
|
+
"""Copy a catalog handler into a step, retaining explicit step overrides.
|
|
1153
|
+
|
|
1154
|
+
``handler`` is deliberately notation-only: the result is an ordinary
|
|
1155
|
+
``StepDefinition`` and therefore follows the same compilation path as an
|
|
1156
|
+
inline step. A step name always remains its own identity, while a supplied
|
|
1157
|
+
description or handler field overrides the copied value.
|
|
1158
|
+
"""
|
|
1159
|
+
action_keys = {"kind", "mcp", "argv", "shell", "args", "env", "assert", "action"}
|
|
1160
|
+
replaces_action = bool(action_keys & set(mapping))
|
|
1161
|
+
action = local.action if replaces_action else referenced.action
|
|
1162
|
+
if (
|
|
1163
|
+
action is not None
|
|
1164
|
+
and referenced.action is not None
|
|
1165
|
+
# Aliases may intentionally share a payload contract (for example a
|
|
1166
|
+
# project command action with a different identifier). The action
|
|
1167
|
+
# capability, rather than its registry name, owns whether that payload
|
|
1168
|
+
# can inherit definition fields.
|
|
1169
|
+
and type(action.payload) is type(referenced.action.payload)
|
|
1170
|
+
):
|
|
1171
|
+
payload = actions.get(action.identifier).override_definition(
|
|
1172
|
+
action.payload,
|
|
1173
|
+
referenced.action.payload,
|
|
1174
|
+
DefinitionOverrideContext(
|
|
1175
|
+
mapping,
|
|
1176
|
+
{
|
|
1177
|
+
key: value
|
|
1178
|
+
for key, value in mapping.get("action", {}).items()
|
|
1179
|
+
if key != "type"
|
|
1180
|
+
}
|
|
1181
|
+
if isinstance(mapping.get("action"), dict)
|
|
1182
|
+
else {},
|
|
1183
|
+
"description" in mapping,
|
|
1184
|
+
local.name,
|
|
1185
|
+
local.description,
|
|
1186
|
+
),
|
|
1187
|
+
)
|
|
1188
|
+
action = DefinedAction(action.identifier, payload)
|
|
1189
|
+
return HandlerDefinition(
|
|
1190
|
+
name=local.name,
|
|
1191
|
+
description=(
|
|
1192
|
+
local.description if "description" in mapping else referenced.description
|
|
1193
|
+
),
|
|
1194
|
+
action=action,
|
|
1195
|
+
handlers=local.handlers if "handlers" in mapping else referenced.handlers,
|
|
1196
|
+
operation=local.operation if replaces_action else referenced.operation,
|
|
1197
|
+
provide=local.provide if "variables" in mapping else referenced.provide,
|
|
1198
|
+
outputs=local.outputs if "variables" in mapping else referenced.outputs,
|
|
1199
|
+
save_metadata=(
|
|
1200
|
+
local.save_metadata if "saves" in mapping else referenced.save_metadata
|
|
1201
|
+
),
|
|
1202
|
+
update_document=(
|
|
1203
|
+
local.update_document if "saves" in mapping else referenced.update_document
|
|
1204
|
+
),
|
|
1205
|
+
update_item=(
|
|
1206
|
+
local.update_item if "saves" in mapping else referenced.update_item
|
|
1207
|
+
),
|
|
1208
|
+
agent=local.agent if "agent" in mapping else referenced.agent,
|
|
1209
|
+
model=local.model if "model" in mapping else referenced.model,
|
|
1210
|
+
reasoning=(local.reasoning if "reasoning" in mapping else referenced.reasoning),
|
|
1211
|
+
workdir=local.workdir if "workdir" in mapping else referenced.workdir,
|
|
1212
|
+
on_failure=local.on_failure
|
|
1213
|
+
if "on_failure" in mapping
|
|
1214
|
+
else referenced.on_failure,
|
|
1215
|
+
on_failure_instruction=(
|
|
1216
|
+
local.on_failure_instruction
|
|
1217
|
+
if "on_failure_instruction" in mapping
|
|
1218
|
+
else referenced.on_failure_instruction
|
|
1219
|
+
),
|
|
1220
|
+
)
|