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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""The ``./ww`` command lines that instructions ask agents to run.
|
|
3
|
+
|
|
4
|
+
Every rendered command is built here, so a renamed flag changes in one place.
|
|
5
|
+
Real values are shell-quoted; ``<placeholder>`` tokens that a person or agent
|
|
6
|
+
must replace are left as written.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
|
|
14
|
+
from ww.contracts import CallerRole
|
|
15
|
+
from ww.executable import ww_command
|
|
16
|
+
from ww.workflow_config import ProvidedVariable, SavedMetadata
|
|
17
|
+
|
|
18
|
+
from .models import InteractCommands, RecoveryCommand
|
|
19
|
+
|
|
20
|
+
TASK_PLACEHOLDER = "<task-id>"
|
|
21
|
+
SUMMARY_FLAG = "--summary"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# A bare placeholder such as ``<run-id>``, or one shown already quoted such as
|
|
25
|
+
# ``"<reason>"`` so the agent keeps the quotes when it fills the value in;
|
|
26
|
+
# "<Run-ID>" does not match.
|
|
27
|
+
_PLACEHOLDER = re.compile(r'"?<[a-z][a-z0-9-]*>"?')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _arg(value: str) -> str:
|
|
31
|
+
"""Quote a real value; keep a human placeholder such as ``<run-id>`` as is."""
|
|
32
|
+
if _PLACEHOLDER.fullmatch(value):
|
|
33
|
+
return value
|
|
34
|
+
return shlex.quote(value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _command(*parts: str) -> str:
|
|
38
|
+
return " ".join((ww_command(), *parts))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _worker(role: CallerRole, assignment: str | None) -> tuple[str, ...]:
|
|
42
|
+
"""``--role`` and, for a worker of an open assignment, its token."""
|
|
43
|
+
if role == "worker" and assignment:
|
|
44
|
+
return ("--role", role, "--assignment", _arg(assignment))
|
|
45
|
+
return ("--role", role)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def replan_command(task_id: str, *, keep: bool = False, rerun: bool = False) -> str:
|
|
49
|
+
"""The operator's choice at a ``plan_changed`` stop.
|
|
50
|
+
|
|
51
|
+
Rerunning finished items is confirmed like a retry: ``--yes`` records
|
|
52
|
+
that the operator agreed, since the agent's shell has no terminal.
|
|
53
|
+
"""
|
|
54
|
+
if keep:
|
|
55
|
+
return _command("next", _arg(task_id), "--keep-plan", "--role", "manager")
|
|
56
|
+
flags = ("--replan", "--yes") if rerun else ("--replan",)
|
|
57
|
+
return _command("next", _arg(task_id), *flags, "--role", "manager")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def next_command(
|
|
61
|
+
task_id: str,
|
|
62
|
+
*,
|
|
63
|
+
retry: bool = False,
|
|
64
|
+
force_reason: str | None = None,
|
|
65
|
+
selected_agent: str | None = None,
|
|
66
|
+
model: str | None = None,
|
|
67
|
+
reasoning: str | None = None,
|
|
68
|
+
outcome: str | None = None,
|
|
69
|
+
) -> str:
|
|
70
|
+
"""The manager command that advances a task, with optional recovery flags.
|
|
71
|
+
|
|
72
|
+
A retry or force is the operator's choice, which the agent carries out
|
|
73
|
+
only once they made it; ``--yes`` records that, since the agent's shell
|
|
74
|
+
has no terminal to confirm at.
|
|
75
|
+
"""
|
|
76
|
+
parts = ["next", _arg(task_id)]
|
|
77
|
+
if retry:
|
|
78
|
+
parts.append("--retry")
|
|
79
|
+
if force_reason is not None:
|
|
80
|
+
parts.extend(("--force", "--reason", _arg(force_reason)))
|
|
81
|
+
if retry or force_reason is not None:
|
|
82
|
+
parts.append("--yes")
|
|
83
|
+
parts.extend(("--role", "manager"))
|
|
84
|
+
for flag, value in (
|
|
85
|
+
("--selected-agent", selected_agent),
|
|
86
|
+
("--model", model),
|
|
87
|
+
("--reasoning", reasoning),
|
|
88
|
+
("--outcome", outcome),
|
|
89
|
+
):
|
|
90
|
+
if value is not None:
|
|
91
|
+
parts.extend((flag, _arg(value)))
|
|
92
|
+
return _command(*parts)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def instruction_command(
|
|
96
|
+
task_id: str,
|
|
97
|
+
run_id: str | None = None,
|
|
98
|
+
*,
|
|
99
|
+
role: CallerRole,
|
|
100
|
+
assignment: str | None = None,
|
|
101
|
+
) -> str:
|
|
102
|
+
parts = ["instruction", _arg(task_id)]
|
|
103
|
+
if run_id is not None:
|
|
104
|
+
parts.extend(("--run", _arg(run_id)))
|
|
105
|
+
parts.extend(_worker(role, assignment))
|
|
106
|
+
return _command(*parts)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def start_command(task_id: str | None, workflow: str, agent: str) -> str:
|
|
110
|
+
"""Start ``workflow``; without ``task_id`` ww assigns one."""
|
|
111
|
+
return _command(
|
|
112
|
+
"start",
|
|
113
|
+
*((_arg(task_id),) if task_id is not None else ()),
|
|
114
|
+
"--workflow",
|
|
115
|
+
_arg(workflow),
|
|
116
|
+
"--agent",
|
|
117
|
+
_arg(agent),
|
|
118
|
+
"--requirements",
|
|
119
|
+
'"<the request, normalized>"',
|
|
120
|
+
"--role",
|
|
121
|
+
"manager",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def lookup_command(reference: str = "<task>", agent: str = "<agent>") -> str:
|
|
126
|
+
return _command("lookup", _arg(reference), "--agent", _arg(agent))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def artifacts_command(task_id: str, run_id: str | None = None) -> str:
|
|
130
|
+
parts = ["artifacts", _arg(task_id)]
|
|
131
|
+
if run_id is not None:
|
|
132
|
+
parts.extend(("--run", _arg(run_id)))
|
|
133
|
+
return _command(*parts)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_command(task_id: str) -> str:
|
|
137
|
+
"""Run the active step's checks now, without completing it."""
|
|
138
|
+
return _command("check", _arg(task_id))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def rule_command(task_id: str, rule_id: str = "<id>") -> str:
|
|
142
|
+
"""Show one rule of the task in full."""
|
|
143
|
+
return _command("rule", _arg(task_id), _arg(rule_id))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def dispute_command(
|
|
147
|
+
task_id: str, check_id: str = "<id>", *, assignment: str | None = None
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Ask the operator to overrule a check that rejected the completion."""
|
|
150
|
+
return _command(
|
|
151
|
+
"dispute",
|
|
152
|
+
_arg(task_id),
|
|
153
|
+
*_worker("worker", assignment),
|
|
154
|
+
"--rule",
|
|
155
|
+
_arg(check_id),
|
|
156
|
+
"--reason",
|
|
157
|
+
'"<why the check is wrong here>"',
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def requirements_command(task_id: str) -> str:
|
|
162
|
+
"""Print the task's requirements and their amendments."""
|
|
163
|
+
return _command("requirements", _arg(task_id))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def start_child_command(task_id: str, child_id: str) -> str:
|
|
167
|
+
return _command("start-child", _arg(task_id), _arg(child_id))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def update_child_command(task_id: str, child_id: str) -> str:
|
|
171
|
+
return _command(
|
|
172
|
+
"update-child", _arg(task_id), _arg(child_id), '--text="<child task text>"'
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def add_item_command(
|
|
177
|
+
task_id: str = TASK_PLACEHOLDER, identity: str | None = None
|
|
178
|
+
) -> str:
|
|
179
|
+
parts = ["add-item", _arg(task_id), "--id", "<id>", "--text", "<text>"]
|
|
180
|
+
if identity:
|
|
181
|
+
parts.extend(("--field", f"{identity}=<value>"))
|
|
182
|
+
return _command(*parts)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def set_item_fields_command(task_id: str, item_id: str | None = None) -> str:
|
|
186
|
+
"""Set custom fields on an item; several in one call."""
|
|
187
|
+
return _command(
|
|
188
|
+
"update-item",
|
|
189
|
+
_arg(task_id),
|
|
190
|
+
"--id",
|
|
191
|
+
_arg(item_id) if item_id else "<id>",
|
|
192
|
+
"--field",
|
|
193
|
+
"<name>=<value>",
|
|
194
|
+
"[--field <name>=<value> ...]",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def remove_item_command(task_id: str = TASK_PLACEHOLDER) -> str:
|
|
199
|
+
return _command("remove-item", _arg(task_id), "--id", "<id>")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def reword_item_command(task_id: str = TASK_PLACEHOLDER) -> str:
|
|
203
|
+
return _command("update-item", _arg(task_id), "--id", "<id>", "--text", "<text>")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def add_child_command(
|
|
207
|
+
task_id: str = TASK_PLACEHOLDER, *, project: bool = False, identity: bool = False
|
|
208
|
+
) -> str:
|
|
209
|
+
"""The command that records one child; ``identity`` children take no ID."""
|
|
210
|
+
return _command(
|
|
211
|
+
"add-child",
|
|
212
|
+
_arg(task_id),
|
|
213
|
+
*(() if identity else ("--id", "<child-id>")),
|
|
214
|
+
'--text="<child task text>"',
|
|
215
|
+
*(("--project", "<project>") if project else ()),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def items_command(task_id: str = TASK_PLACEHOLDER) -> str:
|
|
220
|
+
return _command("items", _arg(task_id))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def item_command(task_id: str, item_id: str) -> str:
|
|
224
|
+
return _command("item", _arg(task_id), "--id", _arg(item_id))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
_ITEM_UPDATE_FLAGS = {
|
|
228
|
+
"process_item": ('--processed-item="<agent-friendly analysis>"',),
|
|
229
|
+
"resolve_item": ('--actual-solution="<actual solution>"', "--resolved=true"),
|
|
230
|
+
"report_item": ("--reported=true",),
|
|
231
|
+
"handle_item": (
|
|
232
|
+
'--processed-item="<agent-friendly analysis>"',
|
|
233
|
+
'--actual-solution="<actual solution>"',
|
|
234
|
+
"--resolved=true",
|
|
235
|
+
"--reported=true",
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def update_item_command(task_id: str, item_id: str, operation: str | None) -> str:
|
|
241
|
+
"""The update that saves progress for one item operation."""
|
|
242
|
+
return _command(
|
|
243
|
+
"update-item",
|
|
244
|
+
_arg(task_id),
|
|
245
|
+
"--id",
|
|
246
|
+
_arg(item_id),
|
|
247
|
+
*_ITEM_UPDATE_FLAGS.get(operation or "", ()),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def complete_command(
|
|
252
|
+
task_id: str,
|
|
253
|
+
values: tuple[ProvidedVariable, ...],
|
|
254
|
+
artifact: bool = True,
|
|
255
|
+
metadata: tuple[SavedMetadata, ...] = (),
|
|
256
|
+
*,
|
|
257
|
+
selected_agent: str | None = None,
|
|
258
|
+
selected_model: str | None = None,
|
|
259
|
+
selected_reasoning: str | None = None,
|
|
260
|
+
loop_control: str | None = None,
|
|
261
|
+
role: CallerRole = "worker",
|
|
262
|
+
summary: bool = False,
|
|
263
|
+
rule_results: tuple[str, ...] = (),
|
|
264
|
+
assignment: str | None = None,
|
|
265
|
+
) -> str:
|
|
266
|
+
"""The command that submits a step or breaks/continues its loop.
|
|
267
|
+
|
|
268
|
+
``role`` is the manager only when the pending input belongs to an
|
|
269
|
+
input-only assignment that the manager supplies itself. A verification
|
|
270
|
+
item reports one ``--rule-result`` per rule ID in ``rule_results``.
|
|
271
|
+
"""
|
|
272
|
+
parts = ["loop" if loop_control else "complete", _arg(task_id)]
|
|
273
|
+
if loop_control:
|
|
274
|
+
parts.append(f"--{loop_control}")
|
|
275
|
+
parts.extend(_worker(role, assignment))
|
|
276
|
+
parts.extend(f'--variable {value.name}="<{value.name}>"' for value in values)
|
|
277
|
+
parts.extend(f'--metadata {value.name}="<{value.name}>"' for value in metadata)
|
|
278
|
+
for flag, value in (
|
|
279
|
+
("--selected-agent", selected_agent),
|
|
280
|
+
("--selected-model", selected_model),
|
|
281
|
+
("--selected-reasoning", selected_reasoning),
|
|
282
|
+
):
|
|
283
|
+
if value:
|
|
284
|
+
parts.extend((flag, shlex.quote(value)))
|
|
285
|
+
if artifact:
|
|
286
|
+
parts.append('--artifact="<whole result in Markdown>"')
|
|
287
|
+
if summary:
|
|
288
|
+
parts.append(f'{SUMMARY_FLAG}="<one or two sentences for the next step>"')
|
|
289
|
+
parts.extend(f"--rule-result='<JSON result for {rule}>'" for rule in rule_results)
|
|
290
|
+
return _command(*parts)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def interact_commands(
|
|
294
|
+
task_id: str,
|
|
295
|
+
role: CallerRole,
|
|
296
|
+
assignment: str | None = None,
|
|
297
|
+
*,
|
|
298
|
+
choices: bool = False,
|
|
299
|
+
) -> InteractCommands:
|
|
300
|
+
"""The commands of an interactive step, for the role that performs it."""
|
|
301
|
+
|
|
302
|
+
def interact(*parts: str) -> str:
|
|
303
|
+
return _command("interact", _arg(task_id), *_worker(role, assignment), *parts)
|
|
304
|
+
|
|
305
|
+
pick = ('--choice="<label or number>"',) if choices else ()
|
|
306
|
+
transcript = "\n".join(
|
|
307
|
+
[
|
|
308
|
+
interact("--transcript", "-", *pick, "--end") + " <<'EOF'",
|
|
309
|
+
"Agent: <what you said, verbatim>",
|
|
310
|
+
"Operator: <what the operator said, verbatim>",
|
|
311
|
+
"EOF",
|
|
312
|
+
]
|
|
313
|
+
)
|
|
314
|
+
return InteractCommands(
|
|
315
|
+
transcript=transcript,
|
|
316
|
+
operator=interact('--operator-said="<what the operator said>"'),
|
|
317
|
+
agent=interact('--agent-said="<what the agent said>"'),
|
|
318
|
+
choice=interact('--choice="<label or number>"'),
|
|
319
|
+
end=interact("--end"),
|
|
320
|
+
wait=interact("--await"),
|
|
321
|
+
resume=instruction_command(task_id, role=role, assignment=assignment),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def force_command(task_id: str) -> RecoveryCommand:
|
|
326
|
+
"""The operator's force: skip a failed item, or leave a loop at its limit."""
|
|
327
|
+
return RecoveryCommand("force", next_command(task_id, force_reason='"<reason>"'))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def recovery_commands(task_id: str) -> tuple[RecoveryCommand, ...]:
|
|
331
|
+
"""The two operator choices for an interrupted automatic item."""
|
|
332
|
+
return (
|
|
333
|
+
RecoveryCommand("retry", next_command(task_id, retry=True)),
|
|
334
|
+
force_command(task_id),
|
|
335
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""The "Handoff to manager" block, built by ww when an assignment ends.
|
|
3
|
+
|
|
4
|
+
The facts of a delegated worker's assignment are all in the saved state, so ww
|
|
5
|
+
reports them to the manager itself rather than leaving it to the worker's text:
|
|
6
|
+
the items performed and how each ended, the artifacts, the files changed,
|
|
7
|
+
the checks run and waived, and the fix rounds. The worker's judgment reaches
|
|
8
|
+
the manager only through its short ``--summary``, and the
|
|
9
|
+
worker returns the block verbatim.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ww.execution_models import PlanItemExecution
|
|
17
|
+
from ww.plan import PlanItem
|
|
18
|
+
|
|
19
|
+
from .commands import next_command
|
|
20
|
+
from .models import HandoffBlock, HandoffStep
|
|
21
|
+
|
|
22
|
+
HANDOFF_TITLE = "Handoff to manager"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def handoff_block(
|
|
26
|
+
task_id: str,
|
|
27
|
+
token: str,
|
|
28
|
+
performed: tuple[tuple[PlanItem, PlanItemExecution], ...],
|
|
29
|
+
*,
|
|
30
|
+
root: Path,
|
|
31
|
+
files: tuple[str, ...] | None,
|
|
32
|
+
error: str | None,
|
|
33
|
+
loop_outcome: tuple[str, str] | None = None,
|
|
34
|
+
) -> HandoffBlock:
|
|
35
|
+
"""Report the agent items of one ended assignment, in plan order.
|
|
36
|
+
|
|
37
|
+
``performed`` pairs each agent item of the assignment with its latest
|
|
38
|
+
record; items the worker never reached are left out. ``error`` is the
|
|
39
|
+
run's failure, such as a handler that failed after the last completion.
|
|
40
|
+
``loop_outcome`` names the item that broke or continued its loop and
|
|
41
|
+
which it did.
|
|
42
|
+
"""
|
|
43
|
+
steps = tuple(
|
|
44
|
+
step
|
|
45
|
+
for item, record in performed
|
|
46
|
+
if (step := _step(item, record, root, loop_outcome)) is not None
|
|
47
|
+
)
|
|
48
|
+
summary = next(
|
|
49
|
+
(
|
|
50
|
+
record.summary_for_next
|
|
51
|
+
for _, record in reversed(performed)
|
|
52
|
+
if record.summary_for_next
|
|
53
|
+
),
|
|
54
|
+
None,
|
|
55
|
+
)
|
|
56
|
+
if error is not None and any(step.error == error for step in steps):
|
|
57
|
+
# A failed step already carries it.
|
|
58
|
+
error = None
|
|
59
|
+
return HandoffBlock(
|
|
60
|
+
task_id=task_id,
|
|
61
|
+
token=token,
|
|
62
|
+
steps=steps,
|
|
63
|
+
files=files,
|
|
64
|
+
summary=summary,
|
|
65
|
+
error=error,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _step(
|
|
70
|
+
item: PlanItem,
|
|
71
|
+
record: PlanItemExecution,
|
|
72
|
+
root: Path,
|
|
73
|
+
loop_outcome: tuple[str, str] | None,
|
|
74
|
+
) -> HandoffStep | None:
|
|
75
|
+
if record.status == "completed" and not record.attempts:
|
|
76
|
+
# Completed without being performed: skipped by a loop break.
|
|
77
|
+
return None
|
|
78
|
+
if record.status == "completed":
|
|
79
|
+
outcome = "completed"
|
|
80
|
+
if loop_outcome is not None and loop_outcome[0] == item.id:
|
|
81
|
+
outcome = f"loop {loop_outcome[1]}"
|
|
82
|
+
elif record.status == "failed":
|
|
83
|
+
outcome = "failed"
|
|
84
|
+
elif record.held_completion is not None:
|
|
85
|
+
outcome = "held for verification"
|
|
86
|
+
elif record.attempts:
|
|
87
|
+
outcome = "not completed"
|
|
88
|
+
else:
|
|
89
|
+
return None
|
|
90
|
+
last = record.check_reports[-1] if record.check_reports else None
|
|
91
|
+
return HandoffStep(
|
|
92
|
+
name=item.name,
|
|
93
|
+
outcome=outcome,
|
|
94
|
+
artifact=str((root / record.repair_artifacts[-1]).resolve())
|
|
95
|
+
if record.repair_artifacts
|
|
96
|
+
else str((root / record.artifact).resolve())
|
|
97
|
+
if record.artifact
|
|
98
|
+
else None,
|
|
99
|
+
checks=(
|
|
100
|
+
tuple((result.id, result.status) for result in last.results)
|
|
101
|
+
if last is not None
|
|
102
|
+
else ()
|
|
103
|
+
),
|
|
104
|
+
checks_waived=tuple(check_id for check_id, _ in record.checks_waived),
|
|
105
|
+
fix_rounds=record.repair_failures
|
|
106
|
+
or sum(1 for report in record.check_reports if report.failed),
|
|
107
|
+
error=record.error if record.status == "failed" else None,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def handoff_markdown(block: HandoffBlock) -> str:
|
|
112
|
+
"""The block's text, as the worker returns it and the manager reads it."""
|
|
113
|
+
lines = [f"{HANDOFF_TITLE} · assignment {block.token}", ""]
|
|
114
|
+
lines.append("Steps:")
|
|
115
|
+
if not block.steps:
|
|
116
|
+
lines.append("- none completed")
|
|
117
|
+
for step in block.steps:
|
|
118
|
+
lines.append(f"- {step.name}: {step.outcome}")
|
|
119
|
+
if step.artifact:
|
|
120
|
+
lines.append(f" artifact: {step.artifact}")
|
|
121
|
+
if step.checks:
|
|
122
|
+
lines.append(
|
|
123
|
+
" checks: "
|
|
124
|
+
+ ", ".join(f"{check} {status}" for check, status in step.checks)
|
|
125
|
+
)
|
|
126
|
+
if step.checks_waived:
|
|
127
|
+
lines.append(" waived: " + ", ".join(step.checks_waived))
|
|
128
|
+
if step.fix_rounds:
|
|
129
|
+
lines.append(f" fix rounds: {step.fix_rounds}")
|
|
130
|
+
if step.error:
|
|
131
|
+
lines.append(f" error: {_one_line(step.error)}")
|
|
132
|
+
lines.append("")
|
|
133
|
+
if block.files is None:
|
|
134
|
+
lines.append("Files changed: not tracked (no step here has rules or checks)")
|
|
135
|
+
elif block.files:
|
|
136
|
+
lines.extend(["Files changed:", *(f"- {path}" for path in block.files)])
|
|
137
|
+
else:
|
|
138
|
+
lines.append("Files changed: none")
|
|
139
|
+
if block.error:
|
|
140
|
+
lines.extend(["", f"Error: {_one_line(block.error)}"])
|
|
141
|
+
if block.summary:
|
|
142
|
+
lines.extend(["", f"Worker summary: {_one_line(block.summary)}"])
|
|
143
|
+
manager = next_command(block.task_id)
|
|
144
|
+
lines.extend(["", f"Manager: continue with `{manager}`"])
|
|
145
|
+
return "\n".join(lines)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _one_line(text: str) -> str:
|
|
149
|
+
return " ".join(text.split())
|