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.
- prettyplay/.usages/lifecycle.md +117 -0
- prettyplay/.usages/steps.md +169 -0
- prettyplay/__init__.py +18 -0
- prettyplay/cache/.usages/addressing.md +44 -0
- prettyplay/cache/.usages/budgets.md +51 -0
- prettyplay/cache/.usages/storage.md +32 -0
- prettyplay/cache/__init__.py +8 -0
- prettyplay/cache/budgets.py +93 -0
- prettyplay/cache/models.py +55 -0
- prettyplay/cache/store.py +276 -0
- prettyplay/cache/text.py +36 -0
- prettyplay/config/.usages/configuration.md +246 -0
- prettyplay/config/__init__.py +6 -0
- prettyplay/config/loader.py +336 -0
- prettyplay/config/models.py +288 -0
- prettyplay/driver/.usages/error_kinds.md +26 -0
- prettyplay/driver/.usages/plumbing.md +52 -0
- prettyplay/driver/__init__.py +11 -0
- prettyplay/driver/errors.py +76 -0
- prettyplay/driver/page.py +175 -0
- prettyplay/driver/session.py +330 -0
- prettyplay/engine/.usages/generation.md +157 -0
- prettyplay/engine/.usages/healing.md +61 -0
- prettyplay/engine/__init__.py +19 -0
- prettyplay/engine/attempts.py +80 -0
- prettyplay/engine/classification.py +70 -0
- prettyplay/engine/compliance.py +125 -0
- prettyplay/engine/execution.py +48 -0
- prettyplay/engine/generator.py +1110 -0
- prettyplay/engine/groups/.usages/recovery.md +71 -0
- prettyplay/engine/groups/__init__.py +7 -0
- prettyplay/engine/groups/diagnosis.py +128 -0
- prettyplay/engine/groups/outcome.py +55 -0
- prettyplay/engine/groups/recovery.py +491 -0
- prettyplay/engine/healer.py +149 -0
- prettyplay/engine/polling/.usages/settle.md +55 -0
- prettyplay/engine/polling/__init__.py +6 -0
- prettyplay/engine/polling/settle.py +98 -0
- prettyplay/engine/polling/window.py +72 -0
- prettyplay/engine/renderer/.usages/memory.md +24 -0
- prettyplay/engine/renderer/.usages/rendering.md +44 -0
- prettyplay/engine/renderer/.usages/validation.md +20 -0
- prettyplay/engine/renderer/__init__.py +7 -0
- prettyplay/engine/renderer/memory.py +37 -0
- prettyplay/engine/renderer/render.py +184 -0
- prettyplay/engine/renderer/validation.py +76 -0
- prettyplay/engine/steering/.usages/steering.md +95 -0
- prettyplay/engine/steering/__init__.py +5 -0
- prettyplay/engine/steering/steering.py +765 -0
- prettyplay/engine/text.py +41 -0
- prettyplay/executor.py +654 -0
- prettyplay/failures/.usages/taxonomy.md +131 -0
- prettyplay/failures/__init__.py +25 -0
- prettyplay/failures/errors.py +359 -0
- prettyplay/groups.py +276 -0
- prettyplay/llm/.usages/classification.md +31 -0
- prettyplay/llm/.usages/providers.md +127 -0
- prettyplay/llm/.usages/transport.md +81 -0
- prettyplay/llm/__init__.py +38 -0
- prettyplay/llm/_request.py +823 -0
- prettyplay/llm/anthropic_provider.py +468 -0
- prettyplay/llm/models.py +273 -0
- prettyplay/llm/openai_provider.py +444 -0
- prettyplay/llm/provider.py +279 -0
- prettyplay/reporting/.usages/hooks.md +61 -0
- prettyplay/reporting/__init__.py +6 -0
- prettyplay/reporting/hooks.py +47 -0
- prettyplay/reporting/reporter.py +70 -0
- prettyplay/runtime.py +81 -0
- prettyplay/scenario.py +469 -0
- prettyplay-0.0.2.dist-info/METADATA +515 -0
- prettyplay-0.0.2.dist-info/RECORD +75 -0
- prettyplay-0.0.2.dist-info/WHEEL +5 -0
- prettyplay-0.0.2.dist-info/licenses/LICENSE +28 -0
- prettyplay-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""The diagnosis-driven recovery engine of step groups: bounded cycles over a group-scoped row."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from ...cache import CachedStep, RunBudgets, StepCache, StepIdentity
|
|
7
|
+
from ...config import Config
|
|
8
|
+
from ...driver import PageFacade
|
|
9
|
+
from ...failures import FailureVerdict, IncurableStepError, ProductDefectError
|
|
10
|
+
from ...llm import GroupFailureClassification, LLMProvider, ScenarioStep
|
|
11
|
+
from ...reporting import StepReporter
|
|
12
|
+
from ..attempts import OUTCOME_ORIGINAL, StepAttempt
|
|
13
|
+
from ..generator import StepGenerator
|
|
14
|
+
from ..polling import SettleWindow
|
|
15
|
+
from ..renderer import PreparedStep, StepMemory, render_step
|
|
16
|
+
from .diagnosis import classify_group_failure
|
|
17
|
+
from .outcome import GroupStepOutcome
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("prettyplay")
|
|
20
|
+
|
|
21
|
+
#: The authored reason of a refused recovery cycle — colon-free; the verdict explanation is the same text.
|
|
22
|
+
_CYCLE_CAP = "the per-group recovery cycle cap is exhausted"
|
|
23
|
+
|
|
24
|
+
#: The authored recommendation of the refused-cycle verdict.
|
|
25
|
+
_CYCLE_CAP_RECOMMENDATION = "raise healing_attempts or rework the group"
|
|
26
|
+
|
|
27
|
+
#: The authored explanation head of an out-of-mandate root — the verbatim step sentence follows the dash.
|
|
28
|
+
_OUTSIDE_ROOT = "the root lives outside the group"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _scenario_occurrences(
|
|
32
|
+
scenario: list[ScenarioStep], traces: list[GroupStepOutcome], group_prompt: str
|
|
33
|
+
) -> list[int | None]:
|
|
34
|
+
"""Match this group's accepted traces to their latest scenario records by occurrence."""
|
|
35
|
+
occurrences: list[int | None] = [None] * len(traces)
|
|
36
|
+
search_end = len(scenario)
|
|
37
|
+
for occurrence in range(len(traces) - 2, -1, -1):
|
|
38
|
+
trace = traces[occurrence]
|
|
39
|
+
for index in range(search_end - 1, -1, -1):
|
|
40
|
+
record = scenario[index]
|
|
41
|
+
if record.group_prompt == group_prompt and record.sentence == trace.sentence:
|
|
42
|
+
occurrences[occurrence] = index
|
|
43
|
+
search_end = index
|
|
44
|
+
break
|
|
45
|
+
return occurrences
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _ActiveFailure:
|
|
49
|
+
"""The failure facts the next diagnosis describes — the failed step on entry, fresh per row failure.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
prepared: the render product of the failing step — the executor's
|
|
53
|
+
product on entry, the failing row occurrence's fresh render
|
|
54
|
+
after a row failure.
|
|
55
|
+
step_type: the step kind of the failing step.
|
|
56
|
+
identity: the address of the failing step.
|
|
57
|
+
history: the per-step attempt history grown to the failure.
|
|
58
|
+
occurrence: the group-order index of the failing step's trace in
|
|
59
|
+
the recovery-local view.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
prepared: PreparedStep,
|
|
65
|
+
step_type: str,
|
|
66
|
+
identity: StepIdentity,
|
|
67
|
+
history: list[StepAttempt],
|
|
68
|
+
occurrence: int,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Keep the active failure facts of the next diagnosis.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
prepared: the render product of the failing step.
|
|
74
|
+
step_type: the step kind of the failing step.
|
|
75
|
+
identity: the address of the failing step.
|
|
76
|
+
history: the per-step attempt history grown to the failure.
|
|
77
|
+
occurrence: the group-order index of the failing step's trace.
|
|
78
|
+
"""
|
|
79
|
+
self.prepared = prepared
|
|
80
|
+
self.step_type = step_type
|
|
81
|
+
self.identity = identity
|
|
82
|
+
self.history = history
|
|
83
|
+
self.occurrence = occurrence
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class GroupRecovery:
|
|
87
|
+
"""The diagnosis-driven recovery engine of step groups.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
_config: project settings; the polling settings build every row
|
|
91
|
+
step's window.
|
|
92
|
+
_provider: the LLM port implementation of the diagnosis request.
|
|
93
|
+
_generator: the regeneration engine — every row step regenerates as
|
|
94
|
+
its own per-step unit.
|
|
95
|
+
_cache: the step cache of the test — record 0 of an earlier row step
|
|
96
|
+
anchors its cached code, the write-back happens per step.
|
|
97
|
+
_budgets: the per-test attempt registry — the cycle cap and the
|
|
98
|
+
per-cycle refresh.
|
|
99
|
+
_reporter: the visibility point for the healing events of the row.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__( # noqa: PLR0913, PLR0917 — the signature is fixed by the groups cell contract
|
|
103
|
+
self,
|
|
104
|
+
config: Config,
|
|
105
|
+
provider: LLMProvider,
|
|
106
|
+
generator: StepGenerator,
|
|
107
|
+
cache: StepCache,
|
|
108
|
+
budgets: RunBudgets,
|
|
109
|
+
reporter: StepReporter,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Keep the collaborators of the recovery cycles.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: project settings; the polling settings build every row
|
|
115
|
+
step's settle window.
|
|
116
|
+
provider: the LLM port implementation diagnosing the failure.
|
|
117
|
+
generator: the regeneration engine of the row steps.
|
|
118
|
+
cache: the step cache of the test — record 0 anchors and the
|
|
119
|
+
per-step write-backs.
|
|
120
|
+
budgets: the per-test attempt registry — the cycle cap and the
|
|
121
|
+
per-cycle healing refresh.
|
|
122
|
+
reporter: the visibility point for the healing events of the row.
|
|
123
|
+
"""
|
|
124
|
+
self._config = config
|
|
125
|
+
self._provider = provider
|
|
126
|
+
self._generator = generator
|
|
127
|
+
self._cache = cache
|
|
128
|
+
self._budgets = budgets
|
|
129
|
+
self._reporter = reporter
|
|
130
|
+
|
|
131
|
+
def recover( # noqa: PLR0913, PLR0917 — the signature is fixed by the groups cell contract
|
|
132
|
+
self,
|
|
133
|
+
group_prompt: str,
|
|
134
|
+
traces: list[GroupStepOutcome],
|
|
135
|
+
prepared: PreparedStep,
|
|
136
|
+
step_type: str,
|
|
137
|
+
previous_steps: list[ScenarioStep],
|
|
138
|
+
identity: StepIdentity,
|
|
139
|
+
attempt_history: list[StepAttempt],
|
|
140
|
+
page: PageFacade,
|
|
141
|
+
window: SettleWindow, # noqa: ARG002 — the contract parameter; every row step builds its own window from its trace
|
|
142
|
+
memory: StepMemory,
|
|
143
|
+
) -> CachedStep:
|
|
144
|
+
"""Diagnose a failed group step and recover the affected row of the group.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
group_prompt: the group prompt of the failed step's group,
|
|
148
|
+
verbatim — reaches the diagnosis and every row request.
|
|
149
|
+
traces: the group's step traces in execution order — the failed
|
|
150
|
+
step's record is the last one; copied into the recovery-local
|
|
151
|
+
view on entry, the caller's list is never mutated.
|
|
152
|
+
prepared: the render product of the failed step, threaded from
|
|
153
|
+
the executor — the active failure's instruction rides the
|
|
154
|
+
diagnosis as STEP; its INPUTS and RESULTS blocks reach the
|
|
155
|
+
requests of the failed step's own row entry through the
|
|
156
|
+
fresh re-render of its trace.
|
|
157
|
+
step_type: action or assertion — the failed step's kind, carried
|
|
158
|
+
into the diagnosis.
|
|
159
|
+
previous_steps: the typed scenario records of the test, in
|
|
160
|
+
execution order — copied into the recovery-local view, every
|
|
161
|
+
row regeneration request carries the view, and the
|
|
162
|
+
outside-group resolution matches the quote against its
|
|
163
|
+
instructions.
|
|
164
|
+
identity: the address of the failed step — the active failure's
|
|
165
|
+
identity on entry; the row resolves every regeneration
|
|
166
|
+
identity, the failed step's included, from the trace records.
|
|
167
|
+
attempt_history: the per-step attempt history of the failed
|
|
168
|
+
step, grown to the failure — the active failure's history;
|
|
169
|
+
the failed step's row entry reuses it, the terminal-failure
|
|
170
|
+
facts derive from its last record.
|
|
171
|
+
page: the live page facade of the test — the row re-executes
|
|
172
|
+
forward on the current page.
|
|
173
|
+
window: the settle window of the failed step's execution — the
|
|
174
|
+
contract parameter; every row step builds its own window
|
|
175
|
+
from its trace.
|
|
176
|
+
memory: the test memory — the render source of every row
|
|
177
|
+
re-render and the publication point of every accepted row
|
|
178
|
+
step's captures.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The healed cached step of the failed step — written back to the
|
|
182
|
+
cache by the regeneration of its row entry; the row replays the
|
|
183
|
+
group's own steps from the earliest affected one, each
|
|
184
|
+
re-rendered against the current memory and regenerating under a
|
|
185
|
+
fresh full healing counter; a repeat failure re-enters the
|
|
186
|
+
cap-checked cycle with the failing row step as the active
|
|
187
|
+
failure.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ProductDefectError: the diagnosis says the application is
|
|
191
|
+
genuinely broken — the loud test failure, no cycle is spent
|
|
192
|
+
on it.
|
|
193
|
+
IncurableStepError: the per-group cycle cap is exhausted, the
|
|
194
|
+
diagnosis is incurable, or the quoted root lives outside the
|
|
195
|
+
group — every form carries its verdict.
|
|
196
|
+
PrettyplayError: a row step's re-render hit an authoring error —
|
|
197
|
+
that step's failure, loud, never silently skipped.
|
|
198
|
+
LLMUnavailableError: the provider service failed after the
|
|
199
|
+
bounded transport retries; no engine retry, the steering
|
|
200
|
+
dialog never intercepts it.
|
|
201
|
+
"""
|
|
202
|
+
local_scenario = [record.model_copy() for record in previous_steps]
|
|
203
|
+
local_traces = [trace.model_copy() for trace in traces]
|
|
204
|
+
scenario_occurrences = _scenario_occurrences(local_scenario, local_traces, group_prompt)
|
|
205
|
+
active = _ActiveFailure(prepared, step_type, identity, attempt_history, len(local_traces) - 1)
|
|
206
|
+
|
|
207
|
+
while True:
|
|
208
|
+
if not self._budgets.open_group_cycle(group_prompt):
|
|
209
|
+
# colon-free authored reason; the verdict authors the same explanation
|
|
210
|
+
raise IncurableStepError(
|
|
211
|
+
active.prepared.instruction,
|
|
212
|
+
_CYCLE_CAP,
|
|
213
|
+
"",
|
|
214
|
+
verdict=FailureVerdict("incurable", _CYCLE_CAP, _CYCLE_CAP_RECOMMENDATION),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
code, error = _last_facts(active.history) # the active failure's terminal-failure facts
|
|
218
|
+
verdict = classify_group_failure(
|
|
219
|
+
self._config,
|
|
220
|
+
self._provider,
|
|
221
|
+
group_prompt,
|
|
222
|
+
local_traces,
|
|
223
|
+
local_scenario,
|
|
224
|
+
active.prepared.instruction,
|
|
225
|
+
active.step_type,
|
|
226
|
+
active.history,
|
|
227
|
+
page,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
quoted_index = _quoted_group_index(verdict, local_traces)
|
|
231
|
+
outside = _outside_root(verdict, local_scenario, group_prompt)
|
|
232
|
+
|
|
233
|
+
if verdict.category == "product_defect":
|
|
234
|
+
# anti-masking — the loud test failure, even when the quote points anywhere
|
|
235
|
+
explanation = _mapped_explanation(verdict, quoted_index is not None)
|
|
236
|
+
raise ProductDefectError(
|
|
237
|
+
active.prepared.instruction,
|
|
238
|
+
explanation,
|
|
239
|
+
error,
|
|
240
|
+
FailureVerdict(verdict.category, explanation, verdict.recommendation),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if outside is not None and quoted_index is None:
|
|
244
|
+
# the quoted root lives outside the group — recovery is out of mandate; a quote
|
|
245
|
+
# that matched a group step names the row start first, never the outside raise
|
|
246
|
+
explanation = f"{_OUTSIDE_ROOT} — {outside}"
|
|
247
|
+
raise IncurableStepError(
|
|
248
|
+
active.prepared.instruction,
|
|
249
|
+
explanation,
|
|
250
|
+
error,
|
|
251
|
+
code=code,
|
|
252
|
+
verdict=FailureVerdict("incurable", explanation, verdict.recommendation),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if verdict.category == "incurable":
|
|
256
|
+
explanation = _mapped_explanation(verdict, quoted_index is not None)
|
|
257
|
+
raise IncurableStepError(
|
|
258
|
+
active.prepared.instruction,
|
|
259
|
+
explanation,
|
|
260
|
+
error,
|
|
261
|
+
code=code,
|
|
262
|
+
verdict=FailureVerdict(verdict.category, explanation, verdict.recommendation),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# recoverable — the row runs from the quoted group step, else from the active failure's occurrence
|
|
266
|
+
row_start = quoted_index if quoted_index is not None else active.occurrence
|
|
267
|
+
row = list(enumerate(local_traces))[row_start:]
|
|
268
|
+
|
|
269
|
+
for _occurrence, trace in row: # a fresh full healing counter per row step, every cycle
|
|
270
|
+
self._budgets.refresh_healing(trace.identity)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
return self._run_row(
|
|
274
|
+
row, group_prompt, local_scenario, local_traces, scenario_occurrences, active, verdict, memory, page
|
|
275
|
+
)
|
|
276
|
+
except IncurableStepError:
|
|
277
|
+
# the failure state is fresh — the failing row step became the active failure; a
|
|
278
|
+
# new cycle is cap-checked and re-diagnosed with the updated local views
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
def _run_row( # noqa: PLR0913, PLR0917 — the row threading is fixed by the recovery contract
|
|
282
|
+
self,
|
|
283
|
+
row: list[tuple[int, GroupStepOutcome]],
|
|
284
|
+
group_prompt: str,
|
|
285
|
+
local_scenario: list[ScenarioStep],
|
|
286
|
+
local_traces: list[GroupStepOutcome],
|
|
287
|
+
scenario_occurrences: list[int | None],
|
|
288
|
+
active: _ActiveFailure,
|
|
289
|
+
verdict: GroupFailureClassification,
|
|
290
|
+
memory: StepMemory,
|
|
291
|
+
page: PageFacade,
|
|
292
|
+
) -> CachedStep:
|
|
293
|
+
"""Regenerate the row sequentially and return the failed step's healed step.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
row: the row's occurrence-index and trace-record pairs, the
|
|
297
|
+
failed step's last.
|
|
298
|
+
group_prompt: the group prompt of the recovered group, verbatim
|
|
299
|
+
— the framing of every row request.
|
|
300
|
+
local_scenario: the recovery-local scenario view — carried into
|
|
301
|
+
every row request, its occurrence instructions updated per
|
|
302
|
+
acceptance.
|
|
303
|
+
local_traces: the recovery-local trace view — the occurrence
|
|
304
|
+
records updated per acceptance, marked failed per row
|
|
305
|
+
failure.
|
|
306
|
+
scenario_occurrences: the local_scenario indices of the group's
|
|
307
|
+
own records, in group order.
|
|
308
|
+
active: the active failure — its occurrence's entry reuses its
|
|
309
|
+
grown history; a failing row step replaces its facts before
|
|
310
|
+
the re-raise.
|
|
311
|
+
verdict: the recoverable diagnosis verdict of the cycle — its
|
|
312
|
+
recommendation rides every row request.
|
|
313
|
+
memory: the test memory — the render source of every row
|
|
314
|
+
re-render and the publication point of every acceptance.
|
|
315
|
+
page: the live page facade — the row re-executes forward on the
|
|
316
|
+
current page.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The healed cached step of the failed step — every row step
|
|
320
|
+
regenerates as its own per-step unit; the active failure's entry
|
|
321
|
+
reuses its grown history, an earlier one anchors a fresh list by
|
|
322
|
+
record 0.
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
IncurableStepError: a row step's regeneration exhausted its
|
|
326
|
+
fresh healing pool — the active failure already updated to
|
|
327
|
+
that occurrence; caught by the caller for the cycle
|
|
328
|
+
re-entry.
|
|
329
|
+
PrettyplayError: a row step's re-render hit an authoring error —
|
|
330
|
+
that step's failure, loud, never silently skipped.
|
|
331
|
+
LLMUnavailableError: the provider service failed after the
|
|
332
|
+
bounded transport retries; no engine retry.
|
|
333
|
+
ComplianceVerdictError: the compliance verdict of a green row
|
|
334
|
+
candidate did not parse; nothing is cached.
|
|
335
|
+
"""
|
|
336
|
+
healed: CachedStep | None = None
|
|
337
|
+
|
|
338
|
+
for occurrence, trace in row:
|
|
339
|
+
if trace.delay is not None:
|
|
340
|
+
time.sleep(trace.delay) # the quiet library-level pause — never slow_mo
|
|
341
|
+
|
|
342
|
+
# the fresh render against the current context: sentence + recorded vars + the current memory
|
|
343
|
+
fresh = render_step(trace.sentence, trace.step_type, memory, trace.vars)
|
|
344
|
+
|
|
345
|
+
# the active failure's own entry reuses its grown history; an earlier step anchors a fresh one
|
|
346
|
+
row_history = active.history if occurrence == active.occurrence else self._anchored_history(trace)
|
|
347
|
+
row_window = SettleWindow(self._config.polling_timeout, self._config.polling_delay, trace.tries)
|
|
348
|
+
|
|
349
|
+
self._reporter.emit("on_healing_started", {"step_text": fresh.instruction, "category": "recoverable"})
|
|
350
|
+
try:
|
|
351
|
+
healed = self._generator.regenerate(
|
|
352
|
+
identity=trace.identity,
|
|
353
|
+
prepared=fresh,
|
|
354
|
+
step_type=trace.step_type,
|
|
355
|
+
previous_steps=local_scenario,
|
|
356
|
+
group_prompt=group_prompt,
|
|
357
|
+
page=page,
|
|
358
|
+
attempt_history=row_history,
|
|
359
|
+
recommendation=verdict.recommendation,
|
|
360
|
+
window=row_window,
|
|
361
|
+
memory=memory,
|
|
362
|
+
)
|
|
363
|
+
except IncurableStepError:
|
|
364
|
+
# the failing occurrence becomes the active failure of the next diagnosis — its
|
|
365
|
+
# fresh prepared step, identity, kind and grown history; the local trace view
|
|
366
|
+
# marks the occurrence failed, preserving the freshly prepared instruction
|
|
367
|
+
active.prepared = fresh
|
|
368
|
+
active.step_type = trace.step_type
|
|
369
|
+
active.identity = trace.identity
|
|
370
|
+
active.history = row_history
|
|
371
|
+
active.occurrence = occurrence
|
|
372
|
+
local_traces[occurrence] = trace.model_copy(
|
|
373
|
+
update={"instruction": fresh.instruction, "outcome": "failed"}
|
|
374
|
+
)
|
|
375
|
+
raise
|
|
376
|
+
|
|
377
|
+
# accepted: the fresh instruction replaces the occurrence in both local views, by index —
|
|
378
|
+
# the failed step's own record was never appended to the scenario, its occurrence finds none
|
|
379
|
+
local_traces[occurrence] = trace.model_copy(update={"instruction": fresh.instruction})
|
|
380
|
+
scenario_index = scenario_occurrences[occurrence]
|
|
381
|
+
if scenario_index is not None:
|
|
382
|
+
local_scenario[scenario_index] = local_scenario[scenario_index].model_copy(
|
|
383
|
+
update={"instruction": fresh.instruction}
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
self._reporter.emit("on_healed", {"step_text": fresh.instruction, "explanation": verdict.root_cause})
|
|
387
|
+
logger.info(
|
|
388
|
+
"group_row_recovered",
|
|
389
|
+
extra={"group": group_prompt, "step": fresh.instruction, "earliest": verdict.earliest_step},
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return healed # type: ignore[return-value] — the row always ends with the active failure's entry
|
|
393
|
+
|
|
394
|
+
def _anchored_history(self, trace: GroupStepOutcome) -> list[StepAttempt]:
|
|
395
|
+
"""Compose the fresh per-step history of an earlier row step, anchored by record 0.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
trace: the trace record of the earlier row step.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
The one-record history list the regeneration appends into —
|
|
402
|
+
record 0 carries the cached code of the trace identity, empty
|
|
403
|
+
when none is cached.
|
|
404
|
+
"""
|
|
405
|
+
cached = self._cache.load(trace.identity)
|
|
406
|
+
|
|
407
|
+
return [
|
|
408
|
+
StepAttempt(
|
|
409
|
+
code=cached.code if cached is not None else "",
|
|
410
|
+
error="",
|
|
411
|
+
outcome=OUTCOME_ORIGINAL,
|
|
412
|
+
url_before=trace.url_before,
|
|
413
|
+
url_after=trace.url_after,
|
|
414
|
+
)
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _quoted_group_index(verdict: GroupFailureClassification, traces: list[GroupStepOutcome]) -> int | None:
|
|
419
|
+
"""Resolve the earliest-step quote against the group's own recorded instructions.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
verdict: the diagnosis verdict of the cycle.
|
|
423
|
+
traces: the recovery-local trace view of the group's steps, in
|
|
424
|
+
execution order.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
The index of the trace whose instruction equals the quote verbatim,
|
|
428
|
+
or ``None`` when the quote names no group step.
|
|
429
|
+
"""
|
|
430
|
+
return next((index for index, trace in enumerate(traces) if trace.instruction == verdict.earliest_step), None)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _outside_root(
|
|
434
|
+
verdict: GroupFailureClassification,
|
|
435
|
+
previous_steps: list[ScenarioStep],
|
|
436
|
+
group_prompt: str,
|
|
437
|
+
) -> str | None:
|
|
438
|
+
"""Resolve the earliest-step quote against the instructions of the steps outside the group.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
verdict: the diagnosis verdict of the cycle.
|
|
442
|
+
previous_steps: the recovery-local scenario view of the test, in
|
|
443
|
+
execution order.
|
|
444
|
+
group_prompt: the group prompt of the recovered group.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
The verbatim instruction of the outside step the quote names, or
|
|
448
|
+
``None`` when the quote names none.
|
|
449
|
+
"""
|
|
450
|
+
return next(
|
|
451
|
+
(
|
|
452
|
+
record.instruction
|
|
453
|
+
for record in previous_steps
|
|
454
|
+
if record.instruction == verdict.earliest_step and record.group_prompt != group_prompt
|
|
455
|
+
),
|
|
456
|
+
None,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _mapped_explanation(verdict: GroupFailureClassification, quoted_in_group: bool) -> str:
|
|
461
|
+
"""Map the diagnosis explanation onto the failure taxonomy.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
verdict: the diagnosis verdict of the cycle.
|
|
465
|
+
quoted_in_group: whether the quote matched a group step sentence.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
The mapped explanation of the terminal failure — the root cause
|
|
469
|
+
plus the earliest-step quote in parentheses when it named a group
|
|
470
|
+
step.
|
|
471
|
+
"""
|
|
472
|
+
if quoted_in_group:
|
|
473
|
+
return f"{verdict.root_cause} (earliest affected step: {verdict.earliest_step})"
|
|
474
|
+
|
|
475
|
+
return verdict.root_cause
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _last_facts(history: list[StepAttempt]) -> tuple[str, str]:
|
|
479
|
+
"""Derive the terminal-failure facts from the last record of the failed step's history.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
history: the per-step attempt history of the failed step.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
The code and the error of the last record; the empty pair when the
|
|
486
|
+
history is empty.
|
|
487
|
+
"""
|
|
488
|
+
if not history:
|
|
489
|
+
return "", ""
|
|
490
|
+
|
|
491
|
+
return history[-1].code, history[-1].error
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Healing of failed cached steps: classification verdict decides the branch."""
|
|
2
|
+
|
|
3
|
+
from ..cache import CachedStep, RunBudgets, StepCache
|
|
4
|
+
from ..config import Config
|
|
5
|
+
from ..driver import PageFacade
|
|
6
|
+
from ..failures import FailureVerdict, IncurableStepError, ProductDefectError
|
|
7
|
+
from ..llm import LLMProvider, ScenarioStep
|
|
8
|
+
from ..reporting import StepReporter
|
|
9
|
+
from .attempts import StepAttempt
|
|
10
|
+
from .classification import classify_step_failure
|
|
11
|
+
from .generator import StepGenerator
|
|
12
|
+
from .polling import SettleWindow
|
|
13
|
+
from .renderer import PreparedStep, StepMemory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StepHealer:
|
|
17
|
+
"""Heals a failed cached step according to the classification verdict.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
_config: project settings; the screenshot flag feeds the requests.
|
|
21
|
+
_provider: the LLM port implementation classifying the failure.
|
|
22
|
+
_generator: the regeneration loop of the rot branch.
|
|
23
|
+
_reporter: the visibility point for healing events.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__( # noqa: PLR0913, PLR0917 — the signature is fixed by the engine contract
|
|
27
|
+
self,
|
|
28
|
+
config: Config,
|
|
29
|
+
provider: LLMProvider,
|
|
30
|
+
generator: StepGenerator,
|
|
31
|
+
cache: StepCache, # noqa: ARG002 — written by the generator only; kept for contract symmetry
|
|
32
|
+
budgets: RunBudgets, # noqa: ARG002 — spent by the generator only; kept for contract symmetry
|
|
33
|
+
reporter: StepReporter,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Keep the collaborators of the healing branch.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: project settings; ``send_screenshots`` attaches page images.
|
|
39
|
+
provider: the LLM port implementation classifying the failure.
|
|
40
|
+
generator: the regeneration loop handling the rot verdict.
|
|
41
|
+
cache: the store of the failed step; written by the generator
|
|
42
|
+
only — accepted for contract symmetry, never read here.
|
|
43
|
+
budgets: the per-test attempt registry; spent by the generator —
|
|
44
|
+
accepted for contract symmetry, never read here.
|
|
45
|
+
reporter: the visibility point for engine events.
|
|
46
|
+
"""
|
|
47
|
+
self._config = config
|
|
48
|
+
self._provider = provider
|
|
49
|
+
self._generator = generator
|
|
50
|
+
self._reporter = reporter
|
|
51
|
+
|
|
52
|
+
def heal( # noqa: PLR0913, PLR0917 — the signature is fixed by the engine contract
|
|
53
|
+
self,
|
|
54
|
+
step: CachedStep,
|
|
55
|
+
error: str,
|
|
56
|
+
prepared: PreparedStep,
|
|
57
|
+
step_type: str,
|
|
58
|
+
previous_steps: list[ScenarioStep],
|
|
59
|
+
page: PageFacade,
|
|
60
|
+
attempt_history: list[StepAttempt],
|
|
61
|
+
window: SettleWindow,
|
|
62
|
+
memory: StepMemory,
|
|
63
|
+
) -> CachedStep:
|
|
64
|
+
"""Heal a failed cached step according to the classification verdict.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
step: the cached step whose code failed.
|
|
68
|
+
error: the failure description of the cached code.
|
|
69
|
+
prepared: the render product of the failed step — the
|
|
70
|
+
classification and every regeneration request carry the
|
|
71
|
+
prepared instruction with its INPUTS and RESULTS blocks; the
|
|
72
|
+
raw template sentence never reaches a request.
|
|
73
|
+
step_type: action or assertion — forwarded into every regeneration
|
|
74
|
+
request.
|
|
75
|
+
previous_steps: the typed scenario records of the previous steps
|
|
76
|
+
of the test, in execution order — each the raw sentence plus
|
|
77
|
+
its permanent group membership; scenario context for
|
|
78
|
+
regeneration, never the normalized addressing forms.
|
|
79
|
+
page: the live page facade the healed code runs against.
|
|
80
|
+
attempt_history: the anchored per-step attempt history — record 0
|
|
81
|
+
carries the original cached code seeded by the executor;
|
|
82
|
+
threaded by reference into the regeneration loop, which
|
|
83
|
+
appends every further attempt of the healing.
|
|
84
|
+
window: the settle window of the current step execution — the
|
|
85
|
+
regenerated candidates absorb transient failures inside it.
|
|
86
|
+
memory: the test memory of the executor — the accepted
|
|
87
|
+
regeneration's captures publish inside the regeneration loop
|
|
88
|
+
at the acceptance point, after validation and the gate.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The healed step with proven code, already cached by the
|
|
92
|
+
generator — the healer serves ordinary steps only: a group step
|
|
93
|
+
never reaches it, the regeneration request always carries
|
|
94
|
+
``group_prompt=None``.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ProductDefectError: the expectation of the step is genuinely
|
|
98
|
+
broken in the product; the cache stays untouched.
|
|
99
|
+
IncurableStepError: the verdict says regeneration cannot help,
|
|
100
|
+
or the regeneration attempt budget is exhausted — the
|
|
101
|
+
exhaustion reuses the verdict of this classification, no
|
|
102
|
+
second LLM request is made; the code field carries the
|
|
103
|
+
cached step code.
|
|
104
|
+
LLMUnavailableError: the provider service failed after the
|
|
105
|
+
bounded transport retries; no engine retry.
|
|
106
|
+
"""
|
|
107
|
+
classification = classify_step_failure(
|
|
108
|
+
self._config, self._provider, prepared.instruction, step.code, error, page
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
verdict = FailureVerdict(
|
|
112
|
+
category=classification.category,
|
|
113
|
+
explanation=classification.explanation,
|
|
114
|
+
recommendation=classification.recommendation,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self._reporter.emit(
|
|
118
|
+
"on_healing_started", {"step_text": prepared.instruction, "category": classification.category}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if classification.category == "product_defect":
|
|
122
|
+
raise ProductDefectError(prepared.instruction, classification.explanation, error, verdict)
|
|
123
|
+
if classification.category == "incurable":
|
|
124
|
+
raise IncurableStepError(
|
|
125
|
+
prepared.instruction, classification.explanation, error, code=step.code, verdict=verdict
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# rot | fixable — regeneration carrying the recommendation, the render product and the memory
|
|
129
|
+
try:
|
|
130
|
+
healed = self._generator.regenerate(
|
|
131
|
+
identity=step.identity,
|
|
132
|
+
prepared=prepared,
|
|
133
|
+
step_type=step_type,
|
|
134
|
+
previous_steps=previous_steps,
|
|
135
|
+
group_prompt=None, # a group step never reaches the healer — no framing on this path
|
|
136
|
+
page=page,
|
|
137
|
+
attempt_history=attempt_history,
|
|
138
|
+
recommendation=classification.recommendation,
|
|
139
|
+
window=window,
|
|
140
|
+
memory=memory,
|
|
141
|
+
)
|
|
142
|
+
except IncurableStepError as inner: # regeneration exhausted — the verdict stays None inside
|
|
143
|
+
# the entry verdict of this classification, never a second LLM request; raise … from inner
|
|
144
|
+
raise IncurableStepError(
|
|
145
|
+
prepared.instruction, inner.reason, inner.error, code=step.code, verdict=verdict
|
|
146
|
+
) from inner
|
|
147
|
+
|
|
148
|
+
self._reporter.emit("on_healed", {"step_text": prepared.instruction, "explanation": classification.explanation})
|
|
149
|
+
return healed
|