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,1206 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Persisted, plan-driven workflow execution models.
|
|
3
|
+
|
|
4
|
+
These models deliberately contain no YAML or handler-resolution concepts. A
|
|
5
|
+
task starts with a compiled :class:`WorkflowPlan` snapshot and progresses only
|
|
6
|
+
through the normalized plan items in that snapshot.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ww.contracts import (
|
|
15
|
+
CheckSource,
|
|
16
|
+
CheckStatus,
|
|
17
|
+
CommandStatus,
|
|
18
|
+
ExecutionStatus,
|
|
19
|
+
FailureKind,
|
|
20
|
+
ItemStatus,
|
|
21
|
+
RuleResolutionStatus,
|
|
22
|
+
StepStatus,
|
|
23
|
+
Verdict,
|
|
24
|
+
)
|
|
25
|
+
from ww.plan import PlannedCheck
|
|
26
|
+
from ww.validation import (
|
|
27
|
+
expect_bool,
|
|
28
|
+
expect_literal,
|
|
29
|
+
expect_nonnegative_int,
|
|
30
|
+
expect_optional_int,
|
|
31
|
+
expect_optional_string,
|
|
32
|
+
expect_positive_int,
|
|
33
|
+
expect_string,
|
|
34
|
+
is_strict_int,
|
|
35
|
+
require_keys,
|
|
36
|
+
)
|
|
37
|
+
from ww.workflow_config import ProvidedVariable
|
|
38
|
+
|
|
39
|
+
from .decoding import _positive_int_mapping, _variables
|
|
40
|
+
from .plan_codec import _planned_checks_from_list
|
|
41
|
+
|
|
42
|
+
EXECUTION_SCHEMA_VERSION = 1
|
|
43
|
+
|
|
44
|
+
PAIR_SIZE = 2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class CheckResult:
|
|
49
|
+
"""The outcome of one check ww ran when a step's worker completed it.
|
|
50
|
+
|
|
51
|
+
``output`` is the tail of what the command printed; the full streams are
|
|
52
|
+
command-output artifacts at ``stdout_ref`` and ``stderr_ref``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
id: str
|
|
56
|
+
source: CheckSource
|
|
57
|
+
status: CheckStatus
|
|
58
|
+
command: str = ""
|
|
59
|
+
output: str = ""
|
|
60
|
+
exit_code: int | None = None
|
|
61
|
+
stdout_ref: str | None = None
|
|
62
|
+
stderr_ref: str | None = None
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
"id": self.id,
|
|
67
|
+
"source": self.source,
|
|
68
|
+
"status": self.status,
|
|
69
|
+
"command": self.command,
|
|
70
|
+
"output": self.output,
|
|
71
|
+
"exit_code": self.exit_code,
|
|
72
|
+
"stdout_ref": self.stdout_ref,
|
|
73
|
+
"stderr_ref": self.stderr_ref,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, data: Any) -> CheckResult:
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
raise ValueError("check result must be a mapping")
|
|
80
|
+
require_keys(data, {"id", "source", "status"}, "check result")
|
|
81
|
+
return cls(
|
|
82
|
+
id=expect_string(data["id"], "check result.id"),
|
|
83
|
+
source=expect_literal(data["source"], CheckSource, "check result.source"),
|
|
84
|
+
status=expect_literal(data["status"], CheckStatus, "check result.status"),
|
|
85
|
+
command=expect_string(data.get("command", ""), "check result.command"),
|
|
86
|
+
output=expect_string(data.get("output", ""), "check result.output"),
|
|
87
|
+
exit_code=expect_optional_int(
|
|
88
|
+
data.get("exit_code"), "check result.exit_code"
|
|
89
|
+
),
|
|
90
|
+
stdout_ref=expect_optional_string(
|
|
91
|
+
data.get("stdout_ref"), "check result.stdout_ref"
|
|
92
|
+
),
|
|
93
|
+
stderr_ref=expect_optional_string(
|
|
94
|
+
data.get("stderr_ref"), "check result.stderr_ref"
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class CheckReport:
|
|
101
|
+
"""Every check of one completion attempt, in plan order.
|
|
102
|
+
|
|
103
|
+
``mark`` is the tree the change set was measured to; ``all_files`` says
|
|
104
|
+
there was no change set (no git) and globs selected every project file.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
attempt: int
|
|
108
|
+
checked_at: str
|
|
109
|
+
results: tuple[CheckResult, ...] = ()
|
|
110
|
+
mark: str | None = None
|
|
111
|
+
all_files: bool = False
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def failed(self) -> tuple[CheckResult, ...]:
|
|
115
|
+
return tuple(result for result in self.results if result.status == "failed")
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict[str, Any]:
|
|
118
|
+
return {
|
|
119
|
+
"attempt": self.attempt,
|
|
120
|
+
"checked_at": self.checked_at,
|
|
121
|
+
"results": [result.to_dict() for result in self.results],
|
|
122
|
+
"mark": self.mark,
|
|
123
|
+
"all_files": self.all_files,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def from_dict(cls, data: Any) -> CheckReport:
|
|
128
|
+
if not isinstance(data, dict):
|
|
129
|
+
raise ValueError("check report must be a mapping")
|
|
130
|
+
require_keys(data, {"attempt", "checked_at", "results"}, "check report")
|
|
131
|
+
results = data["results"]
|
|
132
|
+
if not isinstance(results, list):
|
|
133
|
+
raise ValueError("check report results must be a list")
|
|
134
|
+
return cls(
|
|
135
|
+
attempt=expect_positive_int(data["attempt"], "check report.attempt"),
|
|
136
|
+
checked_at=expect_string(data["checked_at"], "check report.checked_at"),
|
|
137
|
+
results=tuple(CheckResult.from_dict(item) for item in results),
|
|
138
|
+
mark=expect_optional_string(data.get("mark"), "check report.mark"),
|
|
139
|
+
all_files=expect_bool(
|
|
140
|
+
data.get("all_files", False), "check report.all_files"
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class RuleResolution:
|
|
147
|
+
"""How one rule without a command of its own is enforced in this step.
|
|
148
|
+
|
|
149
|
+
Decided from the rule-automation store when the step begins, so a run
|
|
150
|
+
never re-reads the store to decide it again: ``converted`` rules are
|
|
151
|
+
checked by the derived check ``check``; ``judged`` ones get a verifier's
|
|
152
|
+
verdict. ``interpretation`` is the store's one-sentence reading, shown
|
|
153
|
+
under the rule on the page. ``missing`` is set on a judged rule whose
|
|
154
|
+
converted ``check`` does not apply here: the first of its configuration
|
|
155
|
+
files the step's directory lacks.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
id: str
|
|
159
|
+
status: RuleResolutionStatus
|
|
160
|
+
check: str | None = None
|
|
161
|
+
interpretation: str | None = None
|
|
162
|
+
missing: str | None = None
|
|
163
|
+
|
|
164
|
+
def to_dict(self) -> dict[str, Any]:
|
|
165
|
+
data: dict[str, Any] = {
|
|
166
|
+
"id": self.id,
|
|
167
|
+
"status": self.status,
|
|
168
|
+
"check": self.check,
|
|
169
|
+
"interpretation": self.interpretation,
|
|
170
|
+
}
|
|
171
|
+
if self.missing is not None:
|
|
172
|
+
data["missing"] = self.missing
|
|
173
|
+
return data
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def from_dict(cls, data: Any) -> RuleResolution:
|
|
177
|
+
if not isinstance(data, dict):
|
|
178
|
+
raise ValueError("rule resolution must be a mapping")
|
|
179
|
+
require_keys(
|
|
180
|
+
data, {"id", "status", "check", "interpretation"}, "rule resolution"
|
|
181
|
+
)
|
|
182
|
+
status = data["status"]
|
|
183
|
+
# The previous format also recorded rules a verifier would propose a
|
|
184
|
+
# check for (``unresolved``) or waiting on the operator
|
|
185
|
+
# (``pending_operator``); both are judged now.
|
|
186
|
+
if status in {"unresolved", "pending_operator"}:
|
|
187
|
+
status = "judged"
|
|
188
|
+
return cls(
|
|
189
|
+
id=expect_string(data["id"], "rule resolution.id"),
|
|
190
|
+
status=expect_literal(
|
|
191
|
+
status, RuleResolutionStatus, "rule resolution.status"
|
|
192
|
+
),
|
|
193
|
+
check=expect_optional_string(data["check"], "rule resolution.check"),
|
|
194
|
+
interpretation=expect_optional_string(
|
|
195
|
+
data["interpretation"], "rule resolution.interpretation"
|
|
196
|
+
),
|
|
197
|
+
missing=expect_optional_string(
|
|
198
|
+
data.get("missing"), "rule resolution.missing"
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True)
|
|
204
|
+
class RuleVerdict:
|
|
205
|
+
"""A verifier's verdict on one rule of the held completion.
|
|
206
|
+
|
|
207
|
+
``by`` is the verification item that gave it; ``failures`` are its
|
|
208
|
+
evidence, one ``file:line — what`` each.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
id: str
|
|
212
|
+
verdict: Verdict
|
|
213
|
+
by: str
|
|
214
|
+
failures: tuple[str, ...] = ()
|
|
215
|
+
|
|
216
|
+
def to_dict(self) -> dict[str, Any]:
|
|
217
|
+
return {
|
|
218
|
+
"id": self.id,
|
|
219
|
+
"verdict": self.verdict,
|
|
220
|
+
"by": self.by,
|
|
221
|
+
"failures": list(self.failures),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def from_dict(cls, data: Any) -> RuleVerdict:
|
|
226
|
+
if not isinstance(data, dict):
|
|
227
|
+
raise ValueError("rule verdict must be a mapping")
|
|
228
|
+
require_keys(data, {"id", "verdict", "by", "failures"}, "rule verdict")
|
|
229
|
+
return cls(
|
|
230
|
+
id=expect_string(data["id"], "rule verdict.id"),
|
|
231
|
+
verdict=expect_literal(data["verdict"], Verdict, "rule verdict.verdict"),
|
|
232
|
+
by=expect_string(data["by"], "rule verdict.by"),
|
|
233
|
+
failures=_strings(data["failures"], "rule verdict.failures"),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass(frozen=True)
|
|
238
|
+
class HeldCompletion:
|
|
239
|
+
"""A completion whose checks passed, held while verifiers judge its rules.
|
|
240
|
+
|
|
241
|
+
Everything the worker supplied except the artifact, which stays in
|
|
242
|
+
``draft_artifact`` beside it, so ww can record the completion exactly as
|
|
243
|
+
submitted once nothing is pending. ``mark``, ``files`` and ``all_files``
|
|
244
|
+
are the change set the verifiers look at; ``draft_ref`` is where the
|
|
245
|
+
draft artifact was written for them. ``verdicts`` collect across the
|
|
246
|
+
verification rounds of this hold; ``report`` is the passing check report,
|
|
247
|
+
reused while the working tree has not changed.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
variables: tuple[tuple[str, str], ...] = ()
|
|
251
|
+
metadata_values: tuple[tuple[str, str], ...] = ()
|
|
252
|
+
selected_agent: str | None = None
|
|
253
|
+
selected_model: str | None = None
|
|
254
|
+
selected_reasoning: str | None = None
|
|
255
|
+
summary_for_next: str | None = None
|
|
256
|
+
loop_control: str | None = None
|
|
257
|
+
mark: str | None = None
|
|
258
|
+
files: tuple[str, ...] = ()
|
|
259
|
+
all_files: bool = False
|
|
260
|
+
draft_ref: str | None = None
|
|
261
|
+
verdicts: tuple[RuleVerdict, ...] = ()
|
|
262
|
+
report: CheckReport | None = None
|
|
263
|
+
|
|
264
|
+
def __post_init__(self) -> None:
|
|
265
|
+
if self.loop_control not in {None, "break", "continue"}:
|
|
266
|
+
raise ValueError(f"invalid held loop control: {self.loop_control!r}")
|
|
267
|
+
|
|
268
|
+
def verdict(self, rule_id: str) -> RuleVerdict | None:
|
|
269
|
+
return next((entry for entry in self.verdicts if entry.id == rule_id), None)
|
|
270
|
+
|
|
271
|
+
def to_dict(self) -> dict[str, Any]:
|
|
272
|
+
return {
|
|
273
|
+
"variables": [list(pair) for pair in self.variables],
|
|
274
|
+
"metadata_values": [list(pair) for pair in self.metadata_values],
|
|
275
|
+
"selected_agent": self.selected_agent,
|
|
276
|
+
"selected_model": self.selected_model,
|
|
277
|
+
"selected_reasoning": self.selected_reasoning,
|
|
278
|
+
"summary_for_next": self.summary_for_next,
|
|
279
|
+
"loop_control": self.loop_control,
|
|
280
|
+
"mark": self.mark,
|
|
281
|
+
"files": list(self.files),
|
|
282
|
+
"all_files": self.all_files,
|
|
283
|
+
"draft_ref": self.draft_ref,
|
|
284
|
+
"verdicts": [verdict.to_dict() for verdict in self.verdicts],
|
|
285
|
+
"report": self.report.to_dict() if self.report else None,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def from_dict(cls, data: Any) -> HeldCompletion:
|
|
290
|
+
if not isinstance(data, dict):
|
|
291
|
+
raise ValueError("held completion must be a mapping")
|
|
292
|
+
require_keys(
|
|
293
|
+
data,
|
|
294
|
+
{
|
|
295
|
+
"variables",
|
|
296
|
+
"metadata_values",
|
|
297
|
+
"selected_agent",
|
|
298
|
+
"selected_model",
|
|
299
|
+
"selected_reasoning",
|
|
300
|
+
"summary_for_next",
|
|
301
|
+
"loop_control",
|
|
302
|
+
"mark",
|
|
303
|
+
"files",
|
|
304
|
+
"all_files",
|
|
305
|
+
"draft_ref",
|
|
306
|
+
"verdicts",
|
|
307
|
+
"report",
|
|
308
|
+
},
|
|
309
|
+
"held completion",
|
|
310
|
+
)
|
|
311
|
+
verdicts = data["verdicts"]
|
|
312
|
+
if not isinstance(verdicts, list):
|
|
313
|
+
raise ValueError("held completion verdicts must be a list")
|
|
314
|
+
return cls(
|
|
315
|
+
variables=_pairs(data["variables"], "held completion.variables"),
|
|
316
|
+
metadata_values=_pairs(
|
|
317
|
+
data["metadata_values"], "held completion.metadata_values"
|
|
318
|
+
),
|
|
319
|
+
selected_agent=expect_optional_string(
|
|
320
|
+
data["selected_agent"], "held completion.selected_agent"
|
|
321
|
+
),
|
|
322
|
+
selected_model=expect_optional_string(
|
|
323
|
+
data["selected_model"], "held completion.selected_model"
|
|
324
|
+
),
|
|
325
|
+
selected_reasoning=expect_optional_string(
|
|
326
|
+
data["selected_reasoning"], "held completion.selected_reasoning"
|
|
327
|
+
),
|
|
328
|
+
summary_for_next=expect_optional_string(
|
|
329
|
+
data["summary_for_next"], "held completion.summary_for_next"
|
|
330
|
+
),
|
|
331
|
+
loop_control=expect_optional_string(
|
|
332
|
+
data["loop_control"], "held completion.loop_control"
|
|
333
|
+
),
|
|
334
|
+
mark=expect_optional_string(data["mark"], "held completion.mark"),
|
|
335
|
+
files=_strings(data["files"], "held completion.files"),
|
|
336
|
+
all_files=expect_bool(data["all_files"], "held completion.all_files"),
|
|
337
|
+
draft_ref=expect_optional_string(
|
|
338
|
+
data["draft_ref"], "held completion.draft_ref"
|
|
339
|
+
),
|
|
340
|
+
verdicts=tuple(RuleVerdict.from_dict(item) for item in verdicts),
|
|
341
|
+
report=(
|
|
342
|
+
CheckReport.from_dict(data["report"])
|
|
343
|
+
if data["report"] is not None
|
|
344
|
+
else None
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@dataclass(frozen=True)
|
|
350
|
+
class Dispute:
|
|
351
|
+
"""A worker's objection to a check that rejected its completion.
|
|
352
|
+
|
|
353
|
+
``check`` is the ID the fix page named: a rule, a ``fix`` hook, a derived
|
|
354
|
+
check, or a rule a verifier judged. ``output`` and ``command`` are what
|
|
355
|
+
that check reported when it last failed, on rejected completion
|
|
356
|
+
``attempt``; ``reason`` is the worker's argument for the operator.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
check: str
|
|
360
|
+
reason: str
|
|
361
|
+
attempt: int
|
|
362
|
+
disputed_at: str
|
|
363
|
+
command: str = ""
|
|
364
|
+
output: str = ""
|
|
365
|
+
|
|
366
|
+
def to_dict(self) -> dict[str, Any]:
|
|
367
|
+
return {
|
|
368
|
+
"check": self.check,
|
|
369
|
+
"reason": self.reason,
|
|
370
|
+
"attempt": self.attempt,
|
|
371
|
+
"disputed_at": self.disputed_at,
|
|
372
|
+
"command": self.command,
|
|
373
|
+
"output": self.output,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
@classmethod
|
|
377
|
+
def from_dict(cls, data: Any) -> Dispute:
|
|
378
|
+
if not isinstance(data, dict):
|
|
379
|
+
raise ValueError("dispute must be a mapping")
|
|
380
|
+
keys = {"check", "reason", "attempt", "disputed_at", "command", "output"}
|
|
381
|
+
require_keys(data, keys, "dispute")
|
|
382
|
+
unknown = set(data) - keys
|
|
383
|
+
if unknown:
|
|
384
|
+
raise ValueError("dispute has unknown keys: " + ", ".join(sorted(unknown)))
|
|
385
|
+
return cls(
|
|
386
|
+
check=expect_string(data["check"], "dispute.check"),
|
|
387
|
+
reason=expect_string(data["reason"], "dispute.reason"),
|
|
388
|
+
attempt=expect_positive_int(data["attempt"], "dispute.attempt"),
|
|
389
|
+
disputed_at=expect_string(data["disputed_at"], "dispute.disputed_at"),
|
|
390
|
+
command=expect_string(data["command"], "dispute.command"),
|
|
391
|
+
output=expect_string(data["output"], "dispute.output"),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@dataclass(frozen=True)
|
|
396
|
+
class VerificationRule:
|
|
397
|
+
"""One rule a verification item judges, as the step began with it.
|
|
398
|
+
|
|
399
|
+
``interpretation`` is the store's reading of the rule, when it has one.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
id: str
|
|
403
|
+
text: str
|
|
404
|
+
text_hash: str
|
|
405
|
+
interpretation: str | None = None
|
|
406
|
+
# A judged rule whose converted ``check`` does not apply in this step:
|
|
407
|
+
# the configuration file the step's directory lacks.
|
|
408
|
+
check: str | None = None
|
|
409
|
+
missing: str | None = None
|
|
410
|
+
|
|
411
|
+
def to_dict(self) -> dict[str, Any]:
|
|
412
|
+
data: dict[str, Any] = {
|
|
413
|
+
"id": self.id,
|
|
414
|
+
"text": self.text,
|
|
415
|
+
"text_hash": self.text_hash,
|
|
416
|
+
"interpretation": self.interpretation,
|
|
417
|
+
"check": self.check,
|
|
418
|
+
}
|
|
419
|
+
if self.missing is not None:
|
|
420
|
+
data["missing"] = self.missing
|
|
421
|
+
return data
|
|
422
|
+
|
|
423
|
+
@classmethod
|
|
424
|
+
def from_dict(cls, data: Any) -> VerificationRule:
|
|
425
|
+
if not isinstance(data, dict):
|
|
426
|
+
raise ValueError("verification rule must be a mapping")
|
|
427
|
+
# The previous format also stored what was asked (``state``: an
|
|
428
|
+
# approach, a prepared check or a verdict) with ``approach`` and
|
|
429
|
+
# ``pending_operator``; a verifier now only judges, so they are ignored.
|
|
430
|
+
require_keys(
|
|
431
|
+
data,
|
|
432
|
+
{"id", "text", "text_hash", "interpretation", "check"},
|
|
433
|
+
"verification rule",
|
|
434
|
+
)
|
|
435
|
+
return cls(
|
|
436
|
+
id=expect_string(data["id"], "verification rule.id"),
|
|
437
|
+
text=expect_string(data["text"], "verification rule.text"),
|
|
438
|
+
text_hash=expect_string(data["text_hash"], "verification rule.text_hash"),
|
|
439
|
+
interpretation=expect_optional_string(
|
|
440
|
+
data["interpretation"], "verification rule.interpretation"
|
|
441
|
+
),
|
|
442
|
+
check=expect_optional_string(data["check"], "verification rule.check"),
|
|
443
|
+
missing=expect_optional_string(
|
|
444
|
+
data.get("missing"), "verification rule.missing"
|
|
445
|
+
),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _strings(value: Any, context: str) -> tuple[str, ...]:
|
|
450
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
451
|
+
raise ValueError(f"{context} must be a list of strings")
|
|
452
|
+
return tuple(value)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _pairs(value: Any, context: str) -> tuple[tuple[str, str], ...]:
|
|
456
|
+
if not isinstance(value, list) or not all(
|
|
457
|
+
isinstance(pair, list)
|
|
458
|
+
and len(pair) == PAIR_SIZE
|
|
459
|
+
and all(isinstance(part, str) for part in pair)
|
|
460
|
+
for pair in value
|
|
461
|
+
):
|
|
462
|
+
raise ValueError(f"{context} must be a list of name and value pairs")
|
|
463
|
+
return tuple((pair[0], pair[1]) for pair in value)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@dataclass(frozen=True)
|
|
467
|
+
class CommandExecution:
|
|
468
|
+
index: int
|
|
469
|
+
status: CommandStatus = "pending"
|
|
470
|
+
started_at: str | None = None
|
|
471
|
+
completed_at: str | None = None
|
|
472
|
+
exit_code: int | None = None
|
|
473
|
+
stdout: str = ""
|
|
474
|
+
stderr: str = ""
|
|
475
|
+
stdout_ref: str | None = None
|
|
476
|
+
stderr_ref: str | None = None
|
|
477
|
+
attempts: int = 0
|
|
478
|
+
# Commands without an external operation do not need an operation ID.
|
|
479
|
+
operation_id: str | None = None
|
|
480
|
+
|
|
481
|
+
def to_dict(self) -> dict[str, Any]:
|
|
482
|
+
return self.__dict__.copy()
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
def from_dict(cls, data: Any) -> CommandExecution:
|
|
486
|
+
if not isinstance(data, dict):
|
|
487
|
+
raise ValueError("command execution must be a mapping")
|
|
488
|
+
require_keys(
|
|
489
|
+
data,
|
|
490
|
+
{
|
|
491
|
+
"index",
|
|
492
|
+
"status",
|
|
493
|
+
"started_at",
|
|
494
|
+
"completed_at",
|
|
495
|
+
"exit_code",
|
|
496
|
+
"stdout",
|
|
497
|
+
"stderr",
|
|
498
|
+
},
|
|
499
|
+
"command execution",
|
|
500
|
+
)
|
|
501
|
+
return cls(
|
|
502
|
+
index=expect_positive_int(data["index"], "command execution.index"),
|
|
503
|
+
status=expect_literal(
|
|
504
|
+
data["status"], CommandStatus, "command execution.status"
|
|
505
|
+
),
|
|
506
|
+
started_at=expect_optional_string(
|
|
507
|
+
data["started_at"], "command execution.started_at"
|
|
508
|
+
),
|
|
509
|
+
completed_at=expect_optional_string(
|
|
510
|
+
data["completed_at"], "command execution.completed_at"
|
|
511
|
+
),
|
|
512
|
+
exit_code=expect_optional_int(
|
|
513
|
+
data["exit_code"], "command execution.exit_code"
|
|
514
|
+
),
|
|
515
|
+
stdout=expect_string(data["stdout"], "command execution.stdout"),
|
|
516
|
+
stderr=expect_string(data["stderr"], "command execution.stderr"),
|
|
517
|
+
stdout_ref=expect_optional_string(
|
|
518
|
+
data.get("stdout_ref"), "command execution.stdout_ref"
|
|
519
|
+
),
|
|
520
|
+
stderr_ref=expect_optional_string(
|
|
521
|
+
data.get("stderr_ref"), "command execution.stderr_ref"
|
|
522
|
+
),
|
|
523
|
+
operation_id=expect_optional_string(
|
|
524
|
+
data.get("operation_id"), "command execution.operation_id"
|
|
525
|
+
),
|
|
526
|
+
attempts=expect_nonnegative_int(
|
|
527
|
+
data.get("attempts", 0), "command execution.attempts"
|
|
528
|
+
),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@dataclass(frozen=True)
|
|
533
|
+
class PlanItemExecution:
|
|
534
|
+
plan_item_id: str
|
|
535
|
+
position: int
|
|
536
|
+
status: ItemStatus = "pending"
|
|
537
|
+
started_at: str | None = None
|
|
538
|
+
completed_at: str | None = None
|
|
539
|
+
attempts: int = 0
|
|
540
|
+
supplied_values: tuple[tuple[str, str], ...] = ()
|
|
541
|
+
output_values: tuple[tuple[str, str], ...] = ()
|
|
542
|
+
result: str | None = None
|
|
543
|
+
error: str | None = None
|
|
544
|
+
commands: tuple[CommandExecution, ...] = ()
|
|
545
|
+
artifact: str | None = None
|
|
546
|
+
# Repairs belong to this execution, without inserting items into the plan.
|
|
547
|
+
repair_pending: bool = False
|
|
548
|
+
repair_failures: int = 0
|
|
549
|
+
repair_artifacts: tuple[str, ...] = ()
|
|
550
|
+
# The failures ww retried by itself (``limits.auto_retries``), oldest
|
|
551
|
+
# first; an operator retry starts the list over.
|
|
552
|
+
retry_errors: tuple[str, ...] = ()
|
|
553
|
+
# The short handover a step's worker wrote for the next step.
|
|
554
|
+
summary_for_next: str | None = None
|
|
555
|
+
# An interactive step's conversation with the operator, as recorded by
|
|
556
|
+
# ``interact``: how many entries, and whether the operator ended it.
|
|
557
|
+
interaction_entries: int = 0
|
|
558
|
+
interaction_ended: bool = False
|
|
559
|
+
# The option the operator chose, when the step offered choices.
|
|
560
|
+
chosen: str | None = None
|
|
561
|
+
# On an assessment's record: the outcome ``next --outcome`` chose, once
|
|
562
|
+
# chosen. Until then the assessment is waiting for its answer.
|
|
563
|
+
assessment_outcome: str | None = None
|
|
564
|
+
# Stable across retries of this plan item. It identifies the external
|
|
565
|
+
# operation whose outcome may be checked after an interrupted process.
|
|
566
|
+
operation_id: str | None = None
|
|
567
|
+
model: str | None = None
|
|
568
|
+
reasoning: str | None = None
|
|
569
|
+
selected_agent: str | None = None
|
|
570
|
+
selected_model: str | None = None
|
|
571
|
+
selected_reasoning: str | None = None
|
|
572
|
+
# The worktree's tree when the step began, the base of its change set;
|
|
573
|
+
# ``None`` without git, or for a step without rules or checks.
|
|
574
|
+
change_mark: str | None = None
|
|
575
|
+
# Every completion attempt ww checked, rejected ones first; the number of
|
|
576
|
+
# failed reports is the number of fixes the worker was sent back for.
|
|
577
|
+
check_reports: tuple[CheckReport, ...] = ()
|
|
578
|
+
# The artifact of the last rejected completion, kept for its revision.
|
|
579
|
+
draft_artifact: str | None = None
|
|
580
|
+
# The checks the operator let the step complete without, by check or
|
|
581
|
+
# rule ID, each with the operator's reason: every one of the step's at
|
|
582
|
+
# its fix limit, or the one a dispute named.
|
|
583
|
+
checks_waived: tuple[tuple[str, str], ...] = ()
|
|
584
|
+
# The worker's open objection to a check, while the operator decides.
|
|
585
|
+
dispute: Dispute | None = None
|
|
586
|
+
# How each rule without a command is enforced, and the converted derived
|
|
587
|
+
# checks that enforce the converted ones; both settled when the step
|
|
588
|
+
# first begins.
|
|
589
|
+
rule_resolutions: tuple[RuleResolution, ...] = ()
|
|
590
|
+
resolved_checks: tuple[PlannedCheck, ...] = ()
|
|
591
|
+
# A completion accepted by the checks and held while verifiers judge the
|
|
592
|
+
# step's rules; its artifact is ``draft_artifact``.
|
|
593
|
+
held_completion: HeldCompletion | None = None
|
|
594
|
+
# On a verification item's record: the rules it is asked about.
|
|
595
|
+
verification: tuple[VerificationRule, ...] = ()
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def fix_attempts(self) -> int:
|
|
599
|
+
"""How many completions of this step ww rejected for failed checks."""
|
|
600
|
+
return sum(1 for report in self.check_reports if report.failed)
|
|
601
|
+
|
|
602
|
+
def check_failures(self, check_id: str) -> int:
|
|
603
|
+
"""How many rejected completions this check failed."""
|
|
604
|
+
return sum(
|
|
605
|
+
1
|
|
606
|
+
for report in self.check_reports
|
|
607
|
+
for result in report.failed
|
|
608
|
+
if result.id == check_id
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def to_dict(self) -> dict[str, Any]:
|
|
612
|
+
return {
|
|
613
|
+
"plan_item_id": self.plan_item_id,
|
|
614
|
+
"position": self.position,
|
|
615
|
+
"status": self.status,
|
|
616
|
+
"started_at": self.started_at,
|
|
617
|
+
"completed_at": self.completed_at,
|
|
618
|
+
"attempts": self.attempts,
|
|
619
|
+
"supplied_values": dict(self.supplied_values),
|
|
620
|
+
"output_values": dict(self.output_values),
|
|
621
|
+
"result": self.result,
|
|
622
|
+
"error": self.error,
|
|
623
|
+
"commands": [item.to_dict() for item in self.commands],
|
|
624
|
+
"artifact": self.artifact,
|
|
625
|
+
"repair_pending": self.repair_pending,
|
|
626
|
+
"repair_failures": self.repair_failures,
|
|
627
|
+
"repair_artifacts": list(self.repair_artifacts),
|
|
628
|
+
"summary_for_next": self.summary_for_next,
|
|
629
|
+
"interaction_entries": self.interaction_entries,
|
|
630
|
+
"interaction_ended": self.interaction_ended,
|
|
631
|
+
"chosen": self.chosen,
|
|
632
|
+
"operation_id": self.operation_id,
|
|
633
|
+
"model": self.model,
|
|
634
|
+
"reasoning": self.reasoning,
|
|
635
|
+
"selected_agent": self.selected_agent,
|
|
636
|
+
"selected_model": self.selected_model,
|
|
637
|
+
"selected_reasoning": self.selected_reasoning,
|
|
638
|
+
"change_mark": self.change_mark,
|
|
639
|
+
"check_reports": [report.to_dict() for report in self.check_reports],
|
|
640
|
+
"draft_artifact": self.draft_artifact,
|
|
641
|
+
"checks_waived": dict(self.checks_waived),
|
|
642
|
+
"dispute": self.dispute.to_dict() if self.dispute else None,
|
|
643
|
+
"rule_resolutions": [entry.to_dict() for entry in self.rule_resolutions],
|
|
644
|
+
"resolved_checks": [check.to_dict() for check in self.resolved_checks],
|
|
645
|
+
"held_completion": (
|
|
646
|
+
self.held_completion.to_dict() if self.held_completion else None
|
|
647
|
+
),
|
|
648
|
+
"verification": [rule.to_dict() for rule in self.verification],
|
|
649
|
+
**(
|
|
650
|
+
{"assessment_outcome": self.assessment_outcome}
|
|
651
|
+
if self.assessment_outcome is not None
|
|
652
|
+
else {}
|
|
653
|
+
),
|
|
654
|
+
**({"retry_errors": list(self.retry_errors)} if self.retry_errors else {}),
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
@classmethod
|
|
658
|
+
def from_dict(cls, data: Any) -> PlanItemExecution:
|
|
659
|
+
if not isinstance(data, dict):
|
|
660
|
+
raise ValueError("plan item execution must be a mapping")
|
|
661
|
+
required = {
|
|
662
|
+
"plan_item_id",
|
|
663
|
+
"position",
|
|
664
|
+
"status",
|
|
665
|
+
"started_at",
|
|
666
|
+
"completed_at",
|
|
667
|
+
"attempts",
|
|
668
|
+
"supplied_values",
|
|
669
|
+
"result",
|
|
670
|
+
"error",
|
|
671
|
+
"commands",
|
|
672
|
+
"artifact",
|
|
673
|
+
}
|
|
674
|
+
require_keys(data, required, "plan item execution")
|
|
675
|
+
values = _variables(data["supplied_values"], "supplied values")
|
|
676
|
+
commands = data["commands"]
|
|
677
|
+
if not isinstance(commands, list):
|
|
678
|
+
raise ValueError("command executions must be a list")
|
|
679
|
+
return cls(
|
|
680
|
+
plan_item_id=expect_string(data["plan_item_id"], "plan item ID"),
|
|
681
|
+
position=expect_positive_int(data["position"], "plan item position"),
|
|
682
|
+
status=expect_literal(
|
|
683
|
+
data["status"], ItemStatus, "plan item execution.status"
|
|
684
|
+
),
|
|
685
|
+
started_at=expect_optional_string(data["started_at"], "started_at"),
|
|
686
|
+
completed_at=expect_optional_string(data["completed_at"], "completed_at"),
|
|
687
|
+
attempts=expect_nonnegative_int(data["attempts"], "attempts"),
|
|
688
|
+
supplied_values=values,
|
|
689
|
+
output_values=_variables(data.get("output_values", {}), "output values"),
|
|
690
|
+
result=expect_optional_string(data["result"], "result"),
|
|
691
|
+
error=expect_optional_string(data["error"], "error"),
|
|
692
|
+
commands=tuple(CommandExecution.from_dict(item) for item in commands),
|
|
693
|
+
artifact=expect_optional_string(data["artifact"], "artifact"),
|
|
694
|
+
repair_pending=expect_bool(
|
|
695
|
+
data.get("repair_pending", False), "repair pending"
|
|
696
|
+
),
|
|
697
|
+
repair_failures=expect_nonnegative_int(
|
|
698
|
+
data.get("repair_failures", 0), "repair failures"
|
|
699
|
+
),
|
|
700
|
+
repair_artifacts=tuple(
|
|
701
|
+
expect_string(value, "repair artifact")
|
|
702
|
+
for value in _list(data.get("repair_artifacts", []), "repair artifacts")
|
|
703
|
+
),
|
|
704
|
+
retry_errors=tuple(
|
|
705
|
+
expect_string(value, "retry error")
|
|
706
|
+
for value in _list(data.get("retry_errors", []), "retry errors")
|
|
707
|
+
),
|
|
708
|
+
summary_for_next=expect_optional_string(
|
|
709
|
+
data.get("summary_for_next"), "summary for next step"
|
|
710
|
+
),
|
|
711
|
+
interaction_entries=expect_nonnegative_int(
|
|
712
|
+
data.get("interaction_entries", 0), "interaction entries"
|
|
713
|
+
),
|
|
714
|
+
interaction_ended=expect_bool(
|
|
715
|
+
data.get("interaction_ended", False), "interaction ended"
|
|
716
|
+
),
|
|
717
|
+
chosen=expect_optional_string(data.get("chosen"), "chosen option"),
|
|
718
|
+
assessment_outcome=expect_optional_string(
|
|
719
|
+
data.get("assessment_outcome"), "assessment outcome"
|
|
720
|
+
),
|
|
721
|
+
operation_id=expect_optional_string(
|
|
722
|
+
data.get("operation_id"), "operation_id"
|
|
723
|
+
),
|
|
724
|
+
model=expect_optional_string(data.get("model"), "execution model"),
|
|
725
|
+
reasoning=expect_optional_string(
|
|
726
|
+
data.get("reasoning"), "execution reasoning"
|
|
727
|
+
),
|
|
728
|
+
selected_agent=expect_optional_string(
|
|
729
|
+
data.get("selected_agent"), "selected agent"
|
|
730
|
+
),
|
|
731
|
+
selected_model=expect_optional_string(
|
|
732
|
+
data.get("selected_model"), "selected model"
|
|
733
|
+
),
|
|
734
|
+
selected_reasoning=expect_optional_string(
|
|
735
|
+
data.get("selected_reasoning"), "selected reasoning"
|
|
736
|
+
),
|
|
737
|
+
change_mark=expect_optional_string(data.get("change_mark"), "change mark"),
|
|
738
|
+
check_reports=_check_reports(data.get("check_reports", [])),
|
|
739
|
+
draft_artifact=expect_optional_string(
|
|
740
|
+
data.get("draft_artifact"), "draft artifact"
|
|
741
|
+
),
|
|
742
|
+
checks_waived=_waivers(data.get("checks_waived", {})),
|
|
743
|
+
dispute=(
|
|
744
|
+
Dispute.from_dict(data["dispute"])
|
|
745
|
+
if data.get("dispute") is not None
|
|
746
|
+
else None
|
|
747
|
+
),
|
|
748
|
+
rule_resolutions=tuple(
|
|
749
|
+
RuleResolution.from_dict(entry)
|
|
750
|
+
for entry in _list(data.get("rule_resolutions", []), "rule resolutions")
|
|
751
|
+
),
|
|
752
|
+
resolved_checks=_planned_checks_from_list(
|
|
753
|
+
data.get("resolved_checks", []), "plan item execution"
|
|
754
|
+
),
|
|
755
|
+
held_completion=(
|
|
756
|
+
HeldCompletion.from_dict(data["held_completion"])
|
|
757
|
+
if data.get("held_completion") is not None
|
|
758
|
+
else None
|
|
759
|
+
),
|
|
760
|
+
verification=tuple(
|
|
761
|
+
VerificationRule.from_dict(entry)
|
|
762
|
+
for entry in _list(data.get("verification", []), "verification")
|
|
763
|
+
),
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _list(value: Any, context: str) -> list[Any]:
|
|
768
|
+
if not isinstance(value, list):
|
|
769
|
+
raise ValueError(f"{context} must be a list")
|
|
770
|
+
return value
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _waivers(value: Any) -> tuple[tuple[str, str], ...]:
|
|
774
|
+
"""Waived check IDs and the operator's reasons, as an ordered mapping."""
|
|
775
|
+
if not isinstance(value, dict) or not all(
|
|
776
|
+
isinstance(key, str) and key and isinstance(reason, str) and reason
|
|
777
|
+
for key, reason in value.items()
|
|
778
|
+
):
|
|
779
|
+
raise ValueError("checks waived must map check IDs to non-empty reasons")
|
|
780
|
+
return tuple(value.items())
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _check_reports(value: Any) -> tuple[CheckReport, ...]:
|
|
784
|
+
if not isinstance(value, list):
|
|
785
|
+
raise ValueError("check reports must be a list")
|
|
786
|
+
return tuple(CheckReport.from_dict(item) for item in value)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
@dataclass(frozen=True)
|
|
790
|
+
class StepProgress:
|
|
791
|
+
"""Recursive human-facing state projection; item records stay authoritative."""
|
|
792
|
+
|
|
793
|
+
name: str
|
|
794
|
+
path: str
|
|
795
|
+
status: StepStatus = "pending"
|
|
796
|
+
started_at: str | None = None
|
|
797
|
+
completed_at: str | None = None
|
|
798
|
+
children: tuple[StepProgress, ...] = ()
|
|
799
|
+
|
|
800
|
+
def to_dict(self) -> dict[str, Any]:
|
|
801
|
+
return {
|
|
802
|
+
"name": self.name,
|
|
803
|
+
"path": self.path,
|
|
804
|
+
"status": self.status,
|
|
805
|
+
"started_at": self.started_at,
|
|
806
|
+
"completed_at": self.completed_at,
|
|
807
|
+
"children": [child.to_dict() for child in self.children],
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
@classmethod
|
|
811
|
+
def from_dict(cls, data: Any) -> StepProgress:
|
|
812
|
+
if not isinstance(data, dict):
|
|
813
|
+
raise ValueError("step progress must be a mapping")
|
|
814
|
+
require_keys(
|
|
815
|
+
data,
|
|
816
|
+
{"name", "path", "status", "started_at", "completed_at", "children"},
|
|
817
|
+
"step progress",
|
|
818
|
+
)
|
|
819
|
+
if not isinstance(data["children"], list):
|
|
820
|
+
raise ValueError("step progress children must be a list")
|
|
821
|
+
return cls(
|
|
822
|
+
name=expect_string(data["name"], "step name"),
|
|
823
|
+
path=expect_string(data["path"], "step path"),
|
|
824
|
+
status=expect_literal(data["status"], StepStatus, "step progress.status"),
|
|
825
|
+
started_at=expect_optional_string(data["started_at"], "started_at"),
|
|
826
|
+
completed_at=expect_optional_string(data["completed_at"], "completed_at"),
|
|
827
|
+
children=tuple(cls.from_dict(item) for item in data["children"]),
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@dataclass(frozen=True)
|
|
832
|
+
class InputRequest:
|
|
833
|
+
item_id: str
|
|
834
|
+
values: tuple[ProvidedVariable, ...]
|
|
835
|
+
continuation: str = "complete"
|
|
836
|
+
|
|
837
|
+
def to_dict(self) -> dict[str, Any]:
|
|
838
|
+
return {
|
|
839
|
+
"item_id": self.item_id,
|
|
840
|
+
"values": [value.to_dict() for value in self.values],
|
|
841
|
+
"continuation": self.continuation,
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
@classmethod
|
|
845
|
+
def from_dict(cls, data: Any) -> InputRequest:
|
|
846
|
+
if not isinstance(data, dict):
|
|
847
|
+
raise ValueError("input request must be a mapping")
|
|
848
|
+
require_keys(data, {"item_id", "values", "continuation"}, "input request")
|
|
849
|
+
values = data["values"]
|
|
850
|
+
if not isinstance(values, list):
|
|
851
|
+
raise ValueError("input request values must be a list")
|
|
852
|
+
if not all(isinstance(item, dict) for item in values):
|
|
853
|
+
raise ValueError("input request values must contain only objects")
|
|
854
|
+
return cls(
|
|
855
|
+
item_id=expect_string(data["item_id"], "input request item ID"),
|
|
856
|
+
values=tuple(
|
|
857
|
+
ProvidedVariable(
|
|
858
|
+
expect_string(item.get("name"), "provided name"),
|
|
859
|
+
expect_optional_string(
|
|
860
|
+
item.get("description", ""), "provided description"
|
|
861
|
+
)
|
|
862
|
+
or "",
|
|
863
|
+
)
|
|
864
|
+
for item in values
|
|
865
|
+
),
|
|
866
|
+
continuation=expect_string(data["continuation"], "input continuation"),
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _is_metadata_leaf(value: object) -> bool:
|
|
871
|
+
return isinstance(value, str) or (
|
|
872
|
+
isinstance(value, tuple) and all(isinstance(item, str) for item in value)
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _metadata_leaves_to_dict(
|
|
877
|
+
values: tuple[tuple[str, str | tuple[str, ...]], ...],
|
|
878
|
+
) -> dict[str, str | list[str]]:
|
|
879
|
+
return {
|
|
880
|
+
key: list(value) if isinstance(value, tuple) else value for key, value in values
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _metadata_leaves(
|
|
885
|
+
data: Any, label: str
|
|
886
|
+
) -> tuple[tuple[str, str | tuple[str, ...]], ...]:
|
|
887
|
+
"""Decode metadata leaves: strings, or lists of strings for append keys."""
|
|
888
|
+
if not isinstance(data, dict):
|
|
889
|
+
raise ValueError(f"{label} must be a mapping")
|
|
890
|
+
result: list[tuple[str, str | tuple[str, ...]]] = []
|
|
891
|
+
for key, value in data.items():
|
|
892
|
+
if not isinstance(key, str):
|
|
893
|
+
raise ValueError(f"{label} keys must be strings")
|
|
894
|
+
if isinstance(value, str):
|
|
895
|
+
result.append((key, value))
|
|
896
|
+
elif isinstance(value, list) and all(isinstance(item, str) for item in value):
|
|
897
|
+
result.append((key, tuple(value)))
|
|
898
|
+
else:
|
|
899
|
+
raise ValueError(f"{label} values must be strings or lists of strings")
|
|
900
|
+
return tuple(result)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
@dataclass(frozen=True)
|
|
904
|
+
class ProjectMetadataPublication:
|
|
905
|
+
"""A project-metadata update durably tied to one completed operation.
|
|
906
|
+
|
|
907
|
+
``expected_values`` records the values observed before the source operation
|
|
908
|
+
committed. Reconciliation may apply the update only when that snapshot is
|
|
909
|
+
still current, or when the desired values are already present from an
|
|
910
|
+
earlier attempt.
|
|
911
|
+
"""
|
|
912
|
+
|
|
913
|
+
operation_id: str
|
|
914
|
+
values: tuple[tuple[str, str | tuple[str, ...]], ...]
|
|
915
|
+
expected_values: tuple[tuple[str, str | None], ...]
|
|
916
|
+
|
|
917
|
+
def __post_init__(self) -> None:
|
|
918
|
+
if not self.operation_id:
|
|
919
|
+
raise ValueError("project metadata publication requires an operation ID")
|
|
920
|
+
if not self.values or any(
|
|
921
|
+
not isinstance(key, str) or not key or not _is_metadata_leaf(value)
|
|
922
|
+
for key, value in self.values
|
|
923
|
+
):
|
|
924
|
+
raise ValueError("project metadata publication has invalid values")
|
|
925
|
+
if tuple(key for key, _ in self.values) != tuple(
|
|
926
|
+
key for key, _ in self.expected_values
|
|
927
|
+
):
|
|
928
|
+
raise ValueError("project metadata publication has mismatched keys")
|
|
929
|
+
|
|
930
|
+
def to_dict(self) -> dict[str, Any]:
|
|
931
|
+
return {
|
|
932
|
+
"operation_id": self.operation_id,
|
|
933
|
+
"values": _metadata_leaves_to_dict(self.values),
|
|
934
|
+
"expected_values": dict(self.expected_values),
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
@classmethod
|
|
938
|
+
def from_dict(cls, data: Any) -> ProjectMetadataPublication:
|
|
939
|
+
if not isinstance(data, dict):
|
|
940
|
+
raise ValueError("project metadata publication must be a mapping")
|
|
941
|
+
values = _metadata_leaves(data.get("values", {}), "project metadata values")
|
|
942
|
+
expected = data.get("expected_values", {})
|
|
943
|
+
if not isinstance(expected, dict) or not all(
|
|
944
|
+
isinstance(key, str) and (value is None or isinstance(value, str))
|
|
945
|
+
for key, value in expected.items()
|
|
946
|
+
):
|
|
947
|
+
raise ValueError("project metadata expected values must be string values")
|
|
948
|
+
return cls(
|
|
949
|
+
expect_string(data.get("operation_id"), "project metadata operation ID"),
|
|
950
|
+
values,
|
|
951
|
+
tuple(expected.items()),
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
@dataclass(frozen=True)
|
|
956
|
+
class ExecutionState:
|
|
957
|
+
task_id: str
|
|
958
|
+
run_id: str
|
|
959
|
+
workflow: str
|
|
960
|
+
agent: str
|
|
961
|
+
modes: tuple[str, ...]
|
|
962
|
+
status: ExecutionStatus
|
|
963
|
+
created_at: str
|
|
964
|
+
updated_at: str
|
|
965
|
+
snapshot_digest: str
|
|
966
|
+
cursor: int
|
|
967
|
+
active_item_id: str | None
|
|
968
|
+
item_executions: tuple[PlanItemExecution, ...]
|
|
969
|
+
steps: tuple[StepProgress, ...]
|
|
970
|
+
# Completed records displaced by a loop reset. They retain the operation
|
|
971
|
+
# IDs and stream references needed to inspect earlier iterations.
|
|
972
|
+
execution_history: tuple[PlanItemExecution, ...] = ()
|
|
973
|
+
workflow_values: tuple[tuple[str, str], ...] = ()
|
|
974
|
+
pending_input_request: InputRequest | None = None
|
|
975
|
+
last_error: str | None = None
|
|
976
|
+
execution_instance_id: str | None = None
|
|
977
|
+
working_directory: str | None = None
|
|
978
|
+
plan_revision: int = 1
|
|
979
|
+
plan_digest: str | None = None
|
|
980
|
+
parent_task_id: str | None = None
|
|
981
|
+
start_operation_id: str | None = None
|
|
982
|
+
workflow_runtime: str = "single"
|
|
983
|
+
model: str = "auto"
|
|
984
|
+
reasoning: str = "auto"
|
|
985
|
+
assignment_item_id: str | None = None
|
|
986
|
+
# The open assignment's token: every worker command of the ``auto``
|
|
987
|
+
# runtime must carry it, so a worker whose assignment ended cannot act.
|
|
988
|
+
assignment_token: str | None = None
|
|
989
|
+
assignment_model: str | None = None
|
|
990
|
+
assignment_reasoning: str | None = None
|
|
991
|
+
assignment_selected_agent: str | None = None
|
|
992
|
+
assignment_selected_model: str | None = None
|
|
993
|
+
assignment_selected_reasoning: str | None = None
|
|
994
|
+
loop_iterations: tuple[tuple[str, int], ...] = ()
|
|
995
|
+
loop_exit_item_id: str | None = None
|
|
996
|
+
loop_continue_item_id: str | None = None
|
|
997
|
+
# The requirements submitted with start survive preparation hooks and are
|
|
998
|
+
# consumed only by the compiler-owned init item.
|
|
999
|
+
pending_init_artifact: str | None = None
|
|
1000
|
+
# Metadata is projected after the source completion has committed. These
|
|
1001
|
+
# intents make a failed projection retryable without early visibility.
|
|
1002
|
+
pending_task_metadata: tuple[tuple[str, str | tuple[str, ...]], ...] = ()
|
|
1003
|
+
pending_project_metadata: ProjectMetadataPublication | None = None
|
|
1004
|
+
# The operator said they are done for now. The agent stops until they
|
|
1005
|
+
# return; any further word or answer of theirs lifts it.
|
|
1006
|
+
operator_paused: bool = False
|
|
1007
|
+
# Why a ``failed`` run failed when that is not the item's own error.
|
|
1008
|
+
failure_kind: FailureKind | None = None
|
|
1009
|
+
|
|
1010
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1011
|
+
return {
|
|
1012
|
+
"schema_version": EXECUTION_SCHEMA_VERSION,
|
|
1013
|
+
"task_id": self.task_id,
|
|
1014
|
+
"workflow": self.workflow,
|
|
1015
|
+
"agent": self.agent,
|
|
1016
|
+
"modes": list(self.modes),
|
|
1017
|
+
"status": self.status,
|
|
1018
|
+
"created_at": self.created_at,
|
|
1019
|
+
"updated_at": self.updated_at,
|
|
1020
|
+
"snapshot_digest": self.snapshot_digest,
|
|
1021
|
+
"cursor": self.cursor,
|
|
1022
|
+
"active_item_id": self.active_item_id,
|
|
1023
|
+
"item_executions": [item.to_dict() for item in self.item_executions],
|
|
1024
|
+
"execution_history": [item.to_dict() for item in self.execution_history],
|
|
1025
|
+
"steps": [item.to_dict() for item in self.steps],
|
|
1026
|
+
"workflow_values": dict(self.workflow_values),
|
|
1027
|
+
"pending_input_request": self.pending_input_request.to_dict()
|
|
1028
|
+
if self.pending_input_request
|
|
1029
|
+
else None,
|
|
1030
|
+
"last_error": self.last_error,
|
|
1031
|
+
"run_id": self.run_id,
|
|
1032
|
+
"execution_instance_id": self.execution_instance_id,
|
|
1033
|
+
"working_directory": self.working_directory,
|
|
1034
|
+
"plan_revision": self.plan_revision,
|
|
1035
|
+
"plan_digest": self.plan_digest,
|
|
1036
|
+
"parent_task_id": self.parent_task_id,
|
|
1037
|
+
"start_operation_id": self.start_operation_id,
|
|
1038
|
+
"workflow_runtime": self.workflow_runtime,
|
|
1039
|
+
"model": self.model,
|
|
1040
|
+
"reasoning": self.reasoning,
|
|
1041
|
+
"assignment_item_id": self.assignment_item_id,
|
|
1042
|
+
"assignment_token": self.assignment_token,
|
|
1043
|
+
"assignment_model": self.assignment_model,
|
|
1044
|
+
"assignment_reasoning": self.assignment_reasoning,
|
|
1045
|
+
"assignment_selected_agent": self.assignment_selected_agent,
|
|
1046
|
+
"assignment_selected_model": self.assignment_selected_model,
|
|
1047
|
+
"assignment_selected_reasoning": self.assignment_selected_reasoning,
|
|
1048
|
+
"loop_iterations": dict(self.loop_iterations),
|
|
1049
|
+
"loop_exit_item_id": self.loop_exit_item_id,
|
|
1050
|
+
"loop_continue_item_id": self.loop_continue_item_id,
|
|
1051
|
+
"pending_init_artifact": self.pending_init_artifact,
|
|
1052
|
+
"pending_task_metadata": _metadata_leaves_to_dict(
|
|
1053
|
+
self.pending_task_metadata
|
|
1054
|
+
),
|
|
1055
|
+
"pending_project_metadata": self.pending_project_metadata.to_dict()
|
|
1056
|
+
if self.pending_project_metadata
|
|
1057
|
+
else None,
|
|
1058
|
+
"operator_paused": self.operator_paused,
|
|
1059
|
+
"failure_kind": self.failure_kind,
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
@classmethod
|
|
1063
|
+
def from_dict(cls, data: Any) -> ExecutionState:
|
|
1064
|
+
if not isinstance(data, dict):
|
|
1065
|
+
raise ValueError("execution state must be a mapping")
|
|
1066
|
+
version = data.get("schema_version")
|
|
1067
|
+
if not is_strict_int(version) or version != EXECUTION_SCHEMA_VERSION:
|
|
1068
|
+
raise ValueError(f"unsupported execution state schema: {version!r}")
|
|
1069
|
+
required = {
|
|
1070
|
+
"schema_version",
|
|
1071
|
+
"task_id",
|
|
1072
|
+
"run_id",
|
|
1073
|
+
"workflow",
|
|
1074
|
+
"agent",
|
|
1075
|
+
"modes",
|
|
1076
|
+
"status",
|
|
1077
|
+
"created_at",
|
|
1078
|
+
"updated_at",
|
|
1079
|
+
"snapshot_digest",
|
|
1080
|
+
"cursor",
|
|
1081
|
+
"active_item_id",
|
|
1082
|
+
"item_executions",
|
|
1083
|
+
"steps",
|
|
1084
|
+
"workflow_values",
|
|
1085
|
+
"pending_input_request",
|
|
1086
|
+
"last_error",
|
|
1087
|
+
}
|
|
1088
|
+
require_keys(data, required, "execution state")
|
|
1089
|
+
if not isinstance(data["modes"], list) or not all(
|
|
1090
|
+
isinstance(item, str) for item in data["modes"]
|
|
1091
|
+
):
|
|
1092
|
+
raise ValueError("modes must be a list of strings")
|
|
1093
|
+
if not isinstance(data["item_executions"], list) or not isinstance(
|
|
1094
|
+
data["steps"], list
|
|
1095
|
+
):
|
|
1096
|
+
raise ValueError("execution records and steps must be lists")
|
|
1097
|
+
task_id = expect_string(data["task_id"], "task ID")
|
|
1098
|
+
run_id = expect_string(data["run_id"], "run ID")
|
|
1099
|
+
raw_items = data["item_executions"]
|
|
1100
|
+
item_executions = tuple(PlanItemExecution.from_dict(item) for item in raw_items)
|
|
1101
|
+
raw_history = data.get("execution_history", [])
|
|
1102
|
+
if not isinstance(raw_history, list):
|
|
1103
|
+
raise ValueError("execution history must be a list")
|
|
1104
|
+
execution_history = tuple(
|
|
1105
|
+
PlanItemExecution.from_dict(item) for item in raw_history
|
|
1106
|
+
)
|
|
1107
|
+
return cls(
|
|
1108
|
+
task_id=task_id,
|
|
1109
|
+
workflow=expect_string(data["workflow"], "workflow"),
|
|
1110
|
+
agent=expect_string(data["agent"], "agent"),
|
|
1111
|
+
modes=tuple(data["modes"]),
|
|
1112
|
+
status=expect_literal(
|
|
1113
|
+
data["status"], ExecutionStatus, "execution state.status"
|
|
1114
|
+
),
|
|
1115
|
+
created_at=expect_string(data["created_at"], "created_at"),
|
|
1116
|
+
updated_at=expect_string(data["updated_at"], "updated_at"),
|
|
1117
|
+
snapshot_digest=expect_string(data["snapshot_digest"], "snapshot digest"),
|
|
1118
|
+
cursor=expect_nonnegative_int(data["cursor"], "cursor"),
|
|
1119
|
+
active_item_id=expect_optional_string(
|
|
1120
|
+
data["active_item_id"], "active item ID"
|
|
1121
|
+
),
|
|
1122
|
+
item_executions=item_executions,
|
|
1123
|
+
execution_history=execution_history,
|
|
1124
|
+
steps=tuple(StepProgress.from_dict(item) for item in data["steps"]),
|
|
1125
|
+
workflow_values=_variables(data["workflow_values"], "workflow values"),
|
|
1126
|
+
pending_input_request=InputRequest.from_dict(data["pending_input_request"])
|
|
1127
|
+
if data["pending_input_request"] is not None
|
|
1128
|
+
else None,
|
|
1129
|
+
last_error=expect_optional_string(data["last_error"], "last error"),
|
|
1130
|
+
run_id=run_id,
|
|
1131
|
+
execution_instance_id=expect_optional_string(
|
|
1132
|
+
data.get("execution_instance_id"), "execution instance ID"
|
|
1133
|
+
),
|
|
1134
|
+
working_directory=expect_optional_string(
|
|
1135
|
+
data.get("working_directory"), "working directory"
|
|
1136
|
+
),
|
|
1137
|
+
plan_revision=expect_positive_int(
|
|
1138
|
+
data.get("plan_revision", 1), "plan revision"
|
|
1139
|
+
),
|
|
1140
|
+
plan_digest=expect_optional_string(data.get("plan_digest"), "plan digest"),
|
|
1141
|
+
parent_task_id=expect_optional_string(
|
|
1142
|
+
data.get("parent_task_id"), "parent task ID"
|
|
1143
|
+
),
|
|
1144
|
+
start_operation_id=expect_optional_string(
|
|
1145
|
+
data.get("start_operation_id"), "start operation ID"
|
|
1146
|
+
),
|
|
1147
|
+
workflow_runtime=expect_string(
|
|
1148
|
+
data.get("workflow_runtime", "single"), "workflow runtime"
|
|
1149
|
+
),
|
|
1150
|
+
model=expect_string(data.get("model", "auto"), "model"),
|
|
1151
|
+
reasoning=expect_string(data.get("reasoning", "auto"), "reasoning"),
|
|
1152
|
+
assignment_item_id=expect_optional_string(
|
|
1153
|
+
data.get("assignment_item_id"), "assignment item ID"
|
|
1154
|
+
),
|
|
1155
|
+
assignment_token=expect_optional_string(
|
|
1156
|
+
data.get("assignment_token"), "assignment token"
|
|
1157
|
+
),
|
|
1158
|
+
assignment_model=expect_optional_string(
|
|
1159
|
+
data.get("assignment_model"), "assignment model"
|
|
1160
|
+
),
|
|
1161
|
+
assignment_reasoning=expect_optional_string(
|
|
1162
|
+
data.get("assignment_reasoning"), "assignment reasoning"
|
|
1163
|
+
),
|
|
1164
|
+
assignment_selected_agent=expect_optional_string(
|
|
1165
|
+
data.get("assignment_selected_agent"), "assignment selected agent"
|
|
1166
|
+
),
|
|
1167
|
+
assignment_selected_model=expect_optional_string(
|
|
1168
|
+
data.get("assignment_selected_model"), "assignment selected model"
|
|
1169
|
+
),
|
|
1170
|
+
assignment_selected_reasoning=expect_optional_string(
|
|
1171
|
+
data.get("assignment_selected_reasoning"),
|
|
1172
|
+
"assignment selected reasoning",
|
|
1173
|
+
),
|
|
1174
|
+
loop_iterations=_positive_int_mapping(
|
|
1175
|
+
data.get("loop_iterations", {}), "execution state.loop_iterations"
|
|
1176
|
+
),
|
|
1177
|
+
loop_exit_item_id=expect_optional_string(
|
|
1178
|
+
data.get("loop_exit_item_id"), "execution state.loop_exit_item_id"
|
|
1179
|
+
),
|
|
1180
|
+
loop_continue_item_id=expect_optional_string(
|
|
1181
|
+
data.get("loop_continue_item_id"),
|
|
1182
|
+
"execution state.loop_continue_item_id",
|
|
1183
|
+
),
|
|
1184
|
+
pending_init_artifact=expect_optional_string(
|
|
1185
|
+
data.get("pending_init_artifact"),
|
|
1186
|
+
"execution state.pending_init_artifact",
|
|
1187
|
+
),
|
|
1188
|
+
pending_task_metadata=_metadata_leaves(
|
|
1189
|
+
data.get("pending_task_metadata", {}), "pending task metadata"
|
|
1190
|
+
),
|
|
1191
|
+
pending_project_metadata=ProjectMetadataPublication.from_dict(
|
|
1192
|
+
data["pending_project_metadata"]
|
|
1193
|
+
)
|
|
1194
|
+
if data.get("pending_project_metadata") is not None
|
|
1195
|
+
else None,
|
|
1196
|
+
operator_paused=expect_bool(
|
|
1197
|
+
data.get("operator_paused", False), "operator paused"
|
|
1198
|
+
),
|
|
1199
|
+
failure_kind=(
|
|
1200
|
+
None
|
|
1201
|
+
if data.get("failure_kind") is None
|
|
1202
|
+
else expect_literal(
|
|
1203
|
+
data["failure_kind"], FailureKind, "execution state.failure_kind"
|
|
1204
|
+
)
|
|
1205
|
+
),
|
|
1206
|
+
)
|