prettyplay 0.0.2__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.
Files changed (75) hide show
  1. prettyplay/.usages/lifecycle.md +117 -0
  2. prettyplay/.usages/steps.md +169 -0
  3. prettyplay/__init__.py +18 -0
  4. prettyplay/cache/.usages/addressing.md +44 -0
  5. prettyplay/cache/.usages/budgets.md +51 -0
  6. prettyplay/cache/.usages/storage.md +32 -0
  7. prettyplay/cache/__init__.py +8 -0
  8. prettyplay/cache/budgets.py +93 -0
  9. prettyplay/cache/models.py +55 -0
  10. prettyplay/cache/store.py +276 -0
  11. prettyplay/cache/text.py +36 -0
  12. prettyplay/config/.usages/configuration.md +246 -0
  13. prettyplay/config/__init__.py +6 -0
  14. prettyplay/config/loader.py +336 -0
  15. prettyplay/config/models.py +288 -0
  16. prettyplay/driver/.usages/error_kinds.md +26 -0
  17. prettyplay/driver/.usages/plumbing.md +52 -0
  18. prettyplay/driver/__init__.py +11 -0
  19. prettyplay/driver/errors.py +76 -0
  20. prettyplay/driver/page.py +175 -0
  21. prettyplay/driver/session.py +330 -0
  22. prettyplay/engine/.usages/generation.md +157 -0
  23. prettyplay/engine/.usages/healing.md +61 -0
  24. prettyplay/engine/__init__.py +19 -0
  25. prettyplay/engine/attempts.py +80 -0
  26. prettyplay/engine/classification.py +70 -0
  27. prettyplay/engine/compliance.py +125 -0
  28. prettyplay/engine/execution.py +48 -0
  29. prettyplay/engine/generator.py +1110 -0
  30. prettyplay/engine/groups/.usages/recovery.md +71 -0
  31. prettyplay/engine/groups/__init__.py +7 -0
  32. prettyplay/engine/groups/diagnosis.py +128 -0
  33. prettyplay/engine/groups/outcome.py +55 -0
  34. prettyplay/engine/groups/recovery.py +491 -0
  35. prettyplay/engine/healer.py +149 -0
  36. prettyplay/engine/polling/.usages/settle.md +55 -0
  37. prettyplay/engine/polling/__init__.py +6 -0
  38. prettyplay/engine/polling/settle.py +98 -0
  39. prettyplay/engine/polling/window.py +72 -0
  40. prettyplay/engine/renderer/.usages/memory.md +24 -0
  41. prettyplay/engine/renderer/.usages/rendering.md +44 -0
  42. prettyplay/engine/renderer/.usages/validation.md +20 -0
  43. prettyplay/engine/renderer/__init__.py +7 -0
  44. prettyplay/engine/renderer/memory.py +37 -0
  45. prettyplay/engine/renderer/render.py +184 -0
  46. prettyplay/engine/renderer/validation.py +76 -0
  47. prettyplay/engine/steering/.usages/steering.md +95 -0
  48. prettyplay/engine/steering/__init__.py +5 -0
  49. prettyplay/engine/steering/steering.py +765 -0
  50. prettyplay/engine/text.py +41 -0
  51. prettyplay/executor.py +654 -0
  52. prettyplay/failures/.usages/taxonomy.md +131 -0
  53. prettyplay/failures/__init__.py +25 -0
  54. prettyplay/failures/errors.py +359 -0
  55. prettyplay/groups.py +276 -0
  56. prettyplay/llm/.usages/classification.md +31 -0
  57. prettyplay/llm/.usages/providers.md +127 -0
  58. prettyplay/llm/.usages/transport.md +81 -0
  59. prettyplay/llm/__init__.py +38 -0
  60. prettyplay/llm/_request.py +823 -0
  61. prettyplay/llm/anthropic_provider.py +468 -0
  62. prettyplay/llm/models.py +273 -0
  63. prettyplay/llm/openai_provider.py +444 -0
  64. prettyplay/llm/provider.py +279 -0
  65. prettyplay/reporting/.usages/hooks.md +61 -0
  66. prettyplay/reporting/__init__.py +6 -0
  67. prettyplay/reporting/hooks.py +47 -0
  68. prettyplay/reporting/reporter.py +70 -0
  69. prettyplay/runtime.py +81 -0
  70. prettyplay/scenario.py +469 -0
  71. prettyplay-0.0.2.dist-info/METADATA +515 -0
  72. prettyplay-0.0.2.dist-info/RECORD +75 -0
  73. prettyplay-0.0.2.dist-info/WHEEL +5 -0
  74. prettyplay-0.0.2.dist-info/licenses/LICENSE +28 -0
  75. prettyplay-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,131 @@
1
+ # Failure taxonomy
2
+
3
+ Domain: failure kinds of prettyplay. Audience: integrators wiring library failures into runner and CI reporting.
4
+
5
+ Every step failure is one of four distinct kinds; all derive from `PrettyplayError`, so one except clause catches any
6
+ prettyplay failure. A fifth kind — the configuration error — joins the base from the config cell.
7
+
8
+ | Exception | Meaning | When it happens | Recommended reaction |
9
+ |---|---|---|---|
10
+ | ProductDefectError | real product regression | an assertion expectation legitimately failed | treat as a bug: file it, fix the product — this failure is the value of the suite |
11
+ | IncurableStepError | the step cannot be (re)generated | attempt budget exhausted, step text no longer matches reality, ambiguity, strict mode forbids generation | follow the verdict `recommendation`: reword the step or refresh the cache |
12
+ | LLMUnavailableError | LLM infrastructure down | an LLM operation (generation, failure classification, group diagnosis, compliance verdict) exhausted its bounded transport retries, met a permanent provider rejection, or received Retry-After above 10 seconds | restore provider access or keys, or wait the indicated interval before rerunning; cached steps are unaffected |
13
+ | ComplianceVerdictError | the compliance gate could not obtain a usable verdict | a successfully executed candidate was checked, but the verdict model answer did not parse | rerun the step to retry generation; a repeatedly malformed verdict points at the verdict model — the candidate was never cached |
14
+ | ConfigurationError | settings are invalid | the first library use loaded an invalid [tool.prettyplay] section | fix the named setting — the message lists the allowed values |
15
+
16
+ ## Verdict categories
17
+
18
+ ProductDefectError and IncurableStepError may carry a verdict with a category — the classification label the LLM
19
+ assigned to the failure. The label set is four:
20
+
21
+ | Category | Meaning | Consequence inside the library |
22
+ |---|---|---|
23
+ | rot | the UI changed: selectors, texts, structure | the step is regenerated from the current page |
24
+ | product_defect | the expectation legitimately failed | the test fails loudly — never healed green |
25
+ | fixable | the step code is at fault (ambiguous or wrong locator/strategy); the intent is satisfiable | the step is regenerated for the same intent, the request carrying the classification recommendation |
26
+ | incurable | regeneration cannot help: budget exhausted, text no longer matches reality, ambiguity | the incurable failure carries step, reason, recommendation |
27
+
28
+ The category travels in the structured fields of the `on_step_verdict` hook event — never in the rendered message.
29
+
30
+ ## The structured failure message
31
+
32
+ ProductDefectError and IncurableStepError render one structured message — the same text
33
+ reaches the exception message, the log record and the `error` field of the `on_step_failed`
34
+ hook event:
35
+
36
+ ```text
37
+ ProductDefectError: the "Sign in" button stayed invisible after submitting the form
38
+ ---
39
+ step: Check that the "Sign in" button appears
40
+ error: Locator expected to be visible
41
+ ---
42
+ received: <actual value — only when the underlying error carries one>
43
+ cause: <error cause>
44
+ Call log:
45
+ - waiting for locator("button[name='Sign in']")
46
+ ---
47
+ explanation: the page has no element with role button and name "Sign in"
48
+ recommendation: check the selector or the button text in the application
49
+ ```
50
+
51
+ - The first line carries the class name of the terminal failure and the authored
52
+ reason — the render is self-sufficient in log records and hook payloads where no runner
53
+ prefix exists
54
+ - The `step:`/`error:` section carries the step sentence and the decomposed headline of the
55
+ underlying error (its expectation or kind); for failed checks the `error:` text never
56
+ carries an AssertionError prefix; the section is omitted entirely when both are empty
57
+ - The details section — `received:`/`cause:`/`Call log:` — appears only when the underlying
58
+ error carries those parts (Playwright expect failures do); otherwise the whole section is
59
+ omitted
60
+ - No label padding anywhere — labels render at column zero; multi-line verdict values
61
+ indent by two spaces
62
+ - A page snapshot never appears in any terminal error output
63
+ - The `category:` line is gone — the category travels in the structured fields of
64
+ `on_step_verdict`, never in the render; IncurableStepError without a verdict keeps the
65
+ fallback `recommendation:` line
66
+ - The failed step's code is never included — it lives in the cache and the `code` field of
67
+ IncurableStepError
68
+
69
+ ## Verdicts on terminal failures
70
+
71
+ ProductDefectError and IncurableStepError carry an optional verdict: category, explanation, recommendation. When the
72
+ LLM is unavailable the verdict is skipped quietly (WARNING in the log) — the failure itself is never delayed or
73
+ distorted.
74
+
75
+ ```python
76
+ import pytest
77
+
78
+ from prettyplay.failures import IncurableStepError
79
+
80
+
81
+ def test_reports_only_library_failures():
82
+ with pytest.raises(IncurableStepError) as info:
83
+ ...
84
+ assert info.value.recommendation
85
+ # info.value.verdict may be None when the LLM was unavailable
86
+ # info.value.error carries the full underlying error text ("" when none)
87
+ # info.value.code carries the failed step code ("" when unknown) — programmatic
88
+ # consumers only: never rendered, never in hook or log payloads
89
+ ```
90
+
91
+ ## Compliance verdict failure — ComplianceVerdictError
92
+
93
+ Raised when the instruction compliance gate cannot parse the verdict model's answer into
94
+ findings: the JSON shape is invalid, a priority label is outside {high, medium, low}, or a
95
+ finding misses its fields. The gate is strict by design — a flaky verdict model surfaces
96
+ loudly instead of waving candidates through.
97
+
98
+ Catch it together with every other library failure:
99
+
100
+ ```python
101
+ from prettyplay.failures import ComplianceVerdictError, PrettyplayError
102
+
103
+ try:
104
+ scenario.step("open the dashboard")
105
+ except ComplianceVerdictError as error:
106
+ ... # the candidate was NOT cached; rerun the step to retry generation
107
+ except PrettyplayError:
108
+ ...
109
+ ```
110
+
111
+ What it means for the engineer:
112
+ - the step candidate executed successfully but was never verified against the project
113
+ instructions, so it was not cached
114
+ - remediation is on the verdict side, not the page: rerun the test, check the provider
115
+ state and the model behind the effective classification model; a repeatedly malformed
116
+ verdict points at a model unable to follow the verdict format
117
+ - switching `generation_approve` off removes the gate entirely (the old behavior)
118
+
119
+ ## Assertion semantics
120
+
121
+ ProductDefectError is also an AssertionError: unittest reports the failed check as a failure (not an error), pytest
122
+ shows it as an ordinary assertion failure, the traceback is folded to the library boundary. Catch it with
123
+ `except PrettyplayError` or `except AssertionError` — both work.
124
+
125
+ ## Rules
126
+
127
+ - Every failure message is actionable: what happened, on which step, what to do next
128
+ - A healed run never turns a ProductDefectError into a green test
129
+ - LLMUnavailableError never occurs on the cached path — a cached suite runs without any LLM
130
+ - One render per terminal failure: integrators parsing messages parse the structured template above; the category
131
+ comes from the `on_step_verdict` event fields, never from the message
@@ -0,0 +1,25 @@
1
+ """Facade of the prettyplay.failures cell: the failure taxonomy of the library."""
2
+
3
+ from .errors import (
4
+ ComplianceVerdictError,
5
+ ErrorParts,
6
+ FailureVerdict,
7
+ IncurableStepError,
8
+ LLMUnavailableError,
9
+ PrettyplayError,
10
+ ProductDefectError,
11
+ decompose_error_text,
12
+ render_terminal_message,
13
+ )
14
+
15
+ __all__ = [
16
+ "ComplianceVerdictError",
17
+ "ErrorParts",
18
+ "FailureVerdict",
19
+ "IncurableStepError",
20
+ "LLMUnavailableError",
21
+ "PrettyplayError",
22
+ "ProductDefectError",
23
+ "decompose_error_text",
24
+ "render_terminal_message",
25
+ ]
@@ -0,0 +1,359 @@
1
+ """The failure taxonomy of prettyplay: four distinct kinds, one library base."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+ #: The built-in path guidance rendered when a terminal failure carries no verdict.
9
+ _FALLBACK_GUIDANCE = "reword the step or refresh the cache"
10
+
11
+ #: The dotted-identifier class prefix of an error head — ``TimeoutError:``, ``Page.reload:``.
12
+ #: The whitespace-or-end requirement after the colon keeps ``net::ERR_…`` heads unrecognized.
13
+ _CLASS_HEAD = re.compile(r"^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*):(?:\s+(.*))?$")
14
+
15
+ #: The fixed shape prefixes recognized in the error tail — by prefix only, never by position.
16
+ _SHAPE_PREFIXES = ("Actual value:", "Caused by:", "Call log:")
17
+
18
+
19
+ class PrettyplayError(Exception):
20
+ """The common base of every library failure — one except clause catches them all.
21
+
22
+ Args:
23
+ message: the failure description.
24
+ """
25
+
26
+ def __init__(self, message: str) -> None:
27
+ self.message = message
28
+
29
+ super().__init__(message)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class FailureVerdict:
34
+ """The engine-built verdict of a terminal step failure, carried by the errors that stop the run.
35
+
36
+ Args:
37
+ category: the classification label: rot, product_defect, fixable or incurable.
38
+ explanation: what happened on the page — one short sentence.
39
+ recommendation: the recommended engineer action — one short sentence.
40
+ """
41
+
42
+ category: str
43
+ explanation: str
44
+ recommendation: str
45
+
46
+ def render(self) -> str:
47
+ """Render the verdict block of the structured terminal message.
48
+
49
+ Returns:
50
+ The rendered verdict block — one line per non-empty field at
51
+ column zero; empty when both fields are empty. The category
52
+ travels in the ``on_step_verdict`` event fields, never here.
53
+ """
54
+ lines = []
55
+
56
+ for label, value in (("explanation", self.explanation), ("recommendation", self.recommendation)):
57
+ if value:
58
+ indented = value.replace("\n", "\n ")
59
+ lines.append(f"{label}: {indented}")
60
+
61
+ return "\n".join(lines)
62
+
63
+
64
+ class ErrorParts(BaseModel):
65
+ """The decomposed parts of a terminal failure's underlying error — the data shape of the single render.
66
+
67
+ Args:
68
+ class_name: the exception class prefix of the underlying error; empty —
69
+ the text carries none.
70
+ reason: the underlying error headline — the expectation of a failed
71
+ check or the message head of a typed error.
72
+ received: the actual-value detail; empty — absent.
73
+ cause: the error-cause detail; empty — absent.
74
+ call_log: the Call log block; empty — absent.
75
+ """
76
+
77
+ model_config = ConfigDict(kw_only=True)
78
+
79
+ class_name: str = ""
80
+ reason: str = ""
81
+ received: str = ""
82
+ cause: str = ""
83
+ call_log: str = ""
84
+
85
+
86
+ def _consume_actual_value(lines: list[str], start: int) -> tuple[str, int]:
87
+ """Collect the received detail starting at an ``Actual value:`` line.
88
+
89
+ Args:
90
+ lines: the full text, split into lines.
91
+ start: the index of the ``Actual value:`` line.
92
+
93
+ Returns:
94
+ The received detail — the text after the prefix plus the following
95
+ non-blank lines, verbatim — and the first unconsumed index.
96
+ """
97
+ collected = [lines[start][len("Actual value:") :].strip()]
98
+ index = start + 1
99
+
100
+ while index < len(lines):
101
+ follower = lines[index]
102
+ if follower == "" or follower.startswith(_SHAPE_PREFIXES):
103
+ break
104
+ collected.append(follower)
105
+ index += 1
106
+
107
+ return "\n".join(collected), index
108
+
109
+
110
+ def _consume_call_log(lines: list[str], start: int) -> tuple[str, int]:
111
+ """Collect the Call log block starting at a ``Call log:`` line.
112
+
113
+ Args:
114
+ lines: the full text, split into lines.
115
+ start: the index of the ``Call log:`` line.
116
+
117
+ Returns:
118
+ The Call log block — the following blank or indented lines, verbatim
119
+ — and the first unconsumed index.
120
+ """
121
+ block = []
122
+ index = start + 1
123
+
124
+ while index < len(lines):
125
+ follower = lines[index]
126
+ if follower != "" and not follower[0].isspace():
127
+ break
128
+ block.append(follower)
129
+ index += 1
130
+
131
+ while block and block[-1] == "":
132
+ block.pop()
133
+
134
+ return "\n".join(block), index
135
+
136
+
137
+ def decompose_error_text(error: str) -> ErrorParts:
138
+ """Decompose the full underlying error text of a failed step into the render parts.
139
+
140
+ Args:
141
+ error: the full underlying error text as formatted by the engine
142
+ error-text policy; empty — all parts empty.
143
+
144
+ Returns:
145
+ The decomposed parts — the class prefix of the first line plus the
146
+ detail shapes by fixed prefix, never by position; never raises.
147
+ """
148
+ if error == "":
149
+ return ErrorParts()
150
+
151
+ lines = error.splitlines()
152
+ class_name = ""
153
+ reason = ""
154
+ received = ""
155
+ cause = ""
156
+ call_log = ""
157
+
158
+ head = _CLASS_HEAD.match(lines[0])
159
+ if head:
160
+ class_name = head[1]
161
+ reason = head[2] or ""
162
+ else:
163
+ reason = lines[0]
164
+
165
+ index = 1
166
+
167
+ while index < len(lines):
168
+ line = lines[index]
169
+
170
+ if line.startswith("Actual value:"):
171
+ received, index = _consume_actual_value(lines, index)
172
+ continue
173
+
174
+ if line.startswith("Caused by:"):
175
+ cause = line[len("Caused by:") :].strip()
176
+ index += 1
177
+ continue
178
+
179
+ if line.startswith("Call log:"):
180
+ call_log, index = _consume_call_log(lines, index)
181
+ continue
182
+
183
+ index += 1
184
+
185
+ return ErrorParts(class_name=class_name, reason=reason, received=received, cause=cause, call_log=call_log)
186
+
187
+
188
+ def render_terminal_message(
189
+ error_class: str,
190
+ reason: str,
191
+ step_text: str,
192
+ error: str,
193
+ verdict: FailureVerdict | None,
194
+ ) -> str:
195
+ """Compose the single structured render of a terminal failure.
196
+
197
+ Args:
198
+ error_class: the short class name of the terminal failure — the first
199
+ line's prefix; ``type(self).__qualname__`` at the call site.
200
+ reason: the primary reason — authored without colons; the first line
201
+ after the class name.
202
+ step_text: the sentence of the failed step; empty — no step line.
203
+ error: the full underlying error text of the failed step code; empty —
204
+ no error line.
205
+ verdict: the optional :class:`FailureVerdict`; ``None`` or an empty
206
+ render — no verdict block.
207
+
208
+ Returns:
209
+ The one structured render of the terminal failure — the fixed block
210
+ order, shared by the exception text, the log record and the
211
+ ``on_step_failed`` payload; never re-composed.
212
+ """
213
+ parts = decompose_error_text(error)
214
+ lines = [f"{error_class}: {reason}" if reason else error_class]
215
+ headline = f"{parts.class_name}: {parts.reason}" if parts.class_name else parts.reason
216
+
217
+ if step_text or headline:
218
+ lines.append("---")
219
+ if step_text:
220
+ lines.append(f"step: {step_text}")
221
+ if headline:
222
+ lines.append(f"error: {headline}")
223
+
224
+ if parts.received or parts.cause or parts.call_log:
225
+ lines.append("---")
226
+ if parts.received:
227
+ lines.append(f"received: {parts.received}")
228
+ if parts.cause:
229
+ lines.append(f"cause: {parts.cause}")
230
+ if parts.call_log:
231
+ lines += ["Call log:", *parts.call_log.splitlines()]
232
+
233
+ block = verdict.render() if verdict is not None else ""
234
+
235
+ if block:
236
+ lines += ["---", block]
237
+
238
+ return "\n".join(lines)
239
+
240
+
241
+ class ProductDefectError(PrettyplayError, AssertionError):
242
+ """A real functional product defect — the assertion expectation did not hold: no retry, no healing.
243
+
244
+ Args:
245
+ step_text: the sentence of the failed step.
246
+ message: what exactly was expected and what was observed — the primary reason.
247
+ error: the full underlying error text of the failed step code; empty —
248
+ the render carries no error line.
249
+ verdict: the optional :class:`FailureVerdict`; ``None`` — the explicit
250
+ absence when the LLM was unavailable, the failure never waits for it.
251
+ """
252
+
253
+ def __init__(
254
+ self,
255
+ step_text: str,
256
+ message: str,
257
+ error: str = "",
258
+ verdict: FailureVerdict | None = None,
259
+ ) -> None:
260
+ self.step_text = step_text
261
+ self.message = message
262
+ self.error = error
263
+ self.verdict = verdict
264
+
265
+ # Exception.__init__ directly: routing through PrettyplayError.__init__
266
+ # would overwrite the public message attribute with the full render.
267
+ # The first line carries the SHORT class name of the raised type —
268
+ # type(self).__qualname__ stays correct under subclassing.
269
+ Exception.__init__(self, render_terminal_message(type(self).__qualname__, message, step_text, error, verdict))
270
+
271
+
272
+ class IncurableStepError(PrettyplayError):
273
+ """An incurable step: regeneration cannot produce working code — an execution failure, never a failed check.
274
+
275
+ Args:
276
+ step_text: the sentence of the failed step.
277
+ reason: the specific incurability cause — the primary reason.
278
+ error: the full underlying error text of the failed step code; empty —
279
+ the render carries no error line.
280
+ code: the step code that terminally failed; empty — unknown. Filled by
281
+ the raiser — the cached step code on the healing and strict failure
282
+ paths, the last candidate code on the generation path. A field for
283
+ programmatic consumers only: never rendered, never carried by hook
284
+ or log payloads.
285
+ verdict: the optional :class:`FailureVerdict` — reused from a
286
+ classification that already happened or requested at budget
287
+ exhaustion; ``None`` when the LLM was unavailable.
288
+ """
289
+
290
+ def __init__(
291
+ self,
292
+ step_text: str,
293
+ reason: str,
294
+ error: str = "",
295
+ code: str = "",
296
+ verdict: FailureVerdict | None = None,
297
+ ) -> None:
298
+ self.step_text = step_text
299
+ self.reason = reason
300
+ # Exception.__init__ directly (as in ProductDefectError); message keeps
301
+ # the base-class attribute contract — the primary reason, not the render.
302
+ self.message = reason
303
+ self.error = error
304
+ self.code = code
305
+ self.verdict = verdict
306
+
307
+ # Render-only fallback verdict: keeps the message actionable without a
308
+ # verdict; the verdict attribute stays None — on_step_verdict never
309
+ # fires for the fallback. The first line carries the SHORT class name
310
+ # of the raised type — type(self).__qualname__ stays correct under
311
+ # subclassing.
312
+ render_verdict = verdict if verdict is not None else FailureVerdict("incurable", "", _FALLBACK_GUIDANCE)
313
+
314
+ Exception.__init__(
315
+ self, render_terminal_message(type(self).__qualname__, reason, step_text, error, render_verdict)
316
+ )
317
+
318
+ @property
319
+ def recommendation(self) -> str:
320
+ """The recommended engineer action.
321
+
322
+ Returns:
323
+ The verdict recommendation when present, the built-in path guidance otherwise.
324
+ """
325
+ if self.verdict is not None:
326
+ return self.verdict.recommendation
327
+
328
+ return _FALLBACK_GUIDANCE
329
+
330
+
331
+ class LLMUnavailableError(PrettyplayError):
332
+ """LLM infrastructure failure: the provider service is unreachable, times out, rate-limits or
333
+ rejects authentication — raised when the bounded transport retries of one logical LLM attempt
334
+ are exhausted, skipped by a permanent provider rejection or cut short by an over-cap
335
+ Retry-After. Blocks every LLM operation — code generation, step failure classification, group
336
+ diagnosis and the compliance verdict; cached steps keep running.
337
+
338
+ Args:
339
+ message: the failure description naming the provider.
340
+ """
341
+
342
+ def __init__(self, message: str) -> None:
343
+ self.message = message
344
+
345
+ super().__init__(message)
346
+
347
+
348
+ class ComplianceVerdictError(PrettyplayError):
349
+ """The compliance gate verdict did not parse into findings — the executed candidate stays unchecked, never cached.
350
+
351
+ Args:
352
+ message: the rendered actionable text — names the compliance gate, the
353
+ parse failure and a fragment of the raw verdict answer.
354
+ """
355
+
356
+ def __init__(self, message: str) -> None:
357
+ self.message = message
358
+
359
+ super().__init__(message)