Skip to content

Commit 10384ad

Browse files
Wire 0050 failure-isolation conformance fixtures
Wire proposal 0050's failure-isolation conformance fixtures (pipeline-utilities/058-063) into the harness: a failure_isolation middleware directive, the expected_failure_isolation_event assertion with a FailureIsolatedEvent capture observer, callable/predicate/ on_caught variant conventions, and an extended raises directive. 058/059/060/062/063 pass; 061 is execution-deferred for a FailureIsolatedEvent.attempt_index discrepancy being reconciled with spec. The llm-provider/056-058 call-level-retry fixtures stay deferred: they assert the per-attempt span surface 0050 deferred (partial).
1 parent cc430e4 commit 10384ad

6 files changed

Lines changed: 264 additions & 32 deletions

File tree

conformance.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,13 @@ since = "0.13.0"
385385
# per-attempt spans require a dedicated within-call sub-event
386386
# (LlmRetryAttemptEvent) scoped to a future cycle. Call-level retry
387387
# ships terminal-only: exactly one LlmCompletionEvent / LlmFailedEvent
388-
# per ``complete()`` call.
388+
# per ``complete()`` call. Failure-isolation conformance fixtures
389+
# (058-063) are wired + passing this cycle EXCEPT 061 (three-piece
390+
# composition): the FailureIsolatedEvent's attempt_index there is the
391+
# node-level baseline (0) while the fixture asserts the final retry
392+
# attempt (1) per §6.3's lineage-correlation rule — RetryMiddleware
393+
# resets the attempt ContextVar before the outer isolation middleware
394+
# catches. That attempt_index reconciliation is pending.
389395
[proposals."0050"]
390396
status = "partial"
391397
since = "0.14.0"

tests/conformance/adapter.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,36 @@ async def fn(_state: Any) -> Mapping[str, Any]:
142142
return fn
143143

144144

145+
_RAISES_EXCEPTION_KINDS: dict[str, type[Exception]] = {
146+
"ValueError": ValueError,
147+
"RuntimeError": RuntimeError,
148+
"TypeError": TypeError,
149+
"KeyError": KeyError,
150+
}
151+
152+
145153
def _make_raising_fn(
146154
node_name: str,
147-
message: str,
155+
raises_spec: str | Mapping[str, Any],
148156
trace: list[str],
149157
) -> Callable[[Any], Awaitable[Mapping[str, Any]]]:
158+
# Two shapes: a bare message string (fixture 006) raises RuntimeError;
159+
# a ``{message, exception_kind}`` dict (fixture 063) raises the named
160+
# exception type with that message (an uncategorized error, so a
161+
# wrapping failure-isolation event reports a null category).
162+
if isinstance(raises_spec, Mapping):
163+
message = str(raises_spec.get("message", ""))
164+
kind = str(raises_spec.get("exception_kind", "RuntimeError"))
165+
if kind not in _RAISES_EXCEPTION_KINDS:
166+
raise ValueError(f"unsupported raises exception_kind: {kind}")
167+
exc_type = _RAISES_EXCEPTION_KINDS[kind]
168+
else:
169+
message = raises_spec
170+
exc_type = RuntimeError
171+
150172
async def fn(_state: Any) -> Mapping[str, Any]:
151173
trace.append(node_name)
152-
raise RuntimeError(message)
174+
raise exc_type(message)
153175

154176
return fn
155177

tests/conformance/harness/directives.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ class NodeSpec(_ForbidExtras):
415415
update_pure: dict[str, Any] | None = None
416416
update_pure_from_state: dict[str, Any] | None = None
417417
update_from_field: UpdateFromFieldSpec | None = None
418-
raises: str | None = None
418+
raises: str | dict[str, Any] | None = None
419419
subgraph: str | None = None
420420
fan_out: FanOutSpec | None = None
421421
parallel_branches: ParallelBranchesSpec | None = None
@@ -516,12 +516,31 @@ class TraceRecorderMiddleware(_AllowExtras):
516516
type: Literal["trace_recorder"]
517517

518518

519+
class FailureIsolationMiddleware(_AllowExtras):
520+
"""Canonical failure-isolation middleware (proposal 0050 §6.3,
521+
fixtures 058-063). Catches an exception escaping the inner chain and
522+
returns a configured degraded partial update, emitting a distinct
523+
``FailureIsolatedEvent``."""
524+
525+
type: Literal["failure_isolation"]
526+
# Static partial-update mapping, or the callable encoding
527+
# ``{callable: state_derived, template, target_field}`` (fixture 059).
528+
degraded_update: dict[str, Any]
529+
event_name: str
530+
# Optional ``{matches_category: <category>}`` predicate (fixture 060).
531+
predicate: dict[str, Any] | None = None
532+
# Optional on_caught hook
533+
# ``{kind, increment_field, capture_message_field}`` (fixture 062).
534+
on_caught: dict[str, Any] | None = None
535+
536+
519537
MiddlewareSpec = Annotated[
520538
RetryMiddleware
521539
| TimingMiddleware
522540
| ErrorRecoveryMiddleware
523541
| ShortCircuitMiddleware
524-
| TraceRecorderMiddleware,
542+
| TraceRecorderMiddleware
543+
| FailureIsolationMiddleware,
525544
Field(discriminator="type"),
526545
]
527546

@@ -642,6 +661,7 @@ class LlmCallSpec(_AllowExtras):
642661
"EdgeSpec",
643662
"EmitsLogSpec",
644663
"ErrorRecoveryMiddleware",
664+
"FailureIsolationMiddleware",
645665
"FailureSpec",
646666
"FanOutSpec",
647667
"FlakyByIndexSpec",

tests/conformance/harness/expectations.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ class PipelineUtilitiesExpected(_ForbidExtras):
135135
observer_event_invariants: dict[str, Any] | None = None
136136
# Singular form used by 015 — assert one specific event shape.
137137
expected_observer_event: dict[str, Any] | None = None
138+
# Failure-isolation event (proposal 0050 §6.3, fixtures 058-063).
139+
expected_failure_isolation_event: dict[str, Any] | None = None
140+
# 060 negative case: assert NO failure-isolation event fired.
141+
no_failure_isolation_event: bool | None = None
142+
# 061 three-piece: per-attempt NodeEvent assertions (driven by the
143+
# retry path; modeled here so the fixture parses).
144+
expected_attempt_events: list[dict[str, Any]] | None = None
138145
# Checkpointing fixtures (024–031).
139146
checkpoint_saves: list[dict[str, Any]] | None = None
140147
latest_record_assertions: dict[str, Any] | None = None
@@ -213,6 +220,8 @@ class ObservabilityExpected(_ForbidExtras):
213220
"timing_records",
214221
"trace_records",
215222
"expected_observer_event",
223+
"expected_failure_isolation_event",
224+
"no_failure_isolation_event",
216225
"recoverable_state",
217226
}
218227
)

tests/conformance/test_fixture_parsing.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -407,33 +407,22 @@ def _id(case: tuple[str, Path]) -> str:
407407
"graph-engine/038-reducer-error-non-list-update": (
408408
"Proposal 0023 canonical state reducers; impl not yet shipped"
409409
),
410-
# Proposal 0050 (failure-isolation middleware + call-level retry,
411-
# v0.42.0) — llm-provider fixtures 056-058 (call-level retry) and
412-
# pipeline-utilities fixtures 058-063 (failure-isolation
413-
# middleware) require new directive shapes. Queued for v0.14.0
414-
# retry & reliability primitives batch.
415-
"llm-provider/056-call-level-retry-transient": ("Proposal 0050 call-level retry; queued for v0.14.0"),
416-
"llm-provider/057-call-level-retry-exhaustion": ("Proposal 0050 call-level retry; queued for v0.14.0"),
417-
"llm-provider/058-call-level-retry-non-transient-no-retry": (
418-
"Proposal 0050 call-level retry; queued for v0.14.0"
419-
),
420-
"pipeline-utilities/058-failure-isolation-static-degraded": (
421-
"Proposal 0050 failure-isolation middleware; queued for v0.14.0"
422-
),
423-
"pipeline-utilities/059-failure-isolation-callable-degraded": (
424-
"Proposal 0050 failure-isolation middleware; queued for v0.14.0"
425-
),
426-
"pipeline-utilities/060-failure-isolation-predicate-filtering": (
427-
"Proposal 0050 failure-isolation middleware; queued for v0.14.0"
410+
# Proposal 0050 call-level retry — llm-provider fixtures 056-058
411+
# assert the per-attempt LLM span surface (N spans +
412+
# ``openarmature.llm.attempt_index``) that python deferred under
413+
# decision (b); 0050 is marked ``partial`` accordingly. They stay
414+
# deferred until a future LlmRetryAttemptEvent cycle implements
415+
# per-attempt spans. (pipeline-utilities failure-isolation fixtures
416+
# 058-063 now parse + run via test_pipeline_utilities.py; 061 is
417+
# execution-deferred there for the attempt_index reconciliation.)
418+
"llm-provider/056-call-level-retry-transient": (
419+
"Proposal 0050 call-level retry asserts the deferred per-attempt span surface (0050 partial)"
420+
),
421+
"llm-provider/057-call-level-retry-exhaustion": (
422+
"Proposal 0050 call-level retry asserts the deferred per-attempt span surface (0050 partial)"
428423
),
429-
"pipeline-utilities/061-failure-isolation-retry-three-piece-composition": (
430-
"Proposal 0050 failure-isolation middleware; queued for v0.14.0"
431-
),
432-
"pipeline-utilities/062-failure-isolation-on-caught-callback": (
433-
"Proposal 0050 failure-isolation middleware; queued for v0.14.0"
434-
),
435-
"pipeline-utilities/063-failure-isolation-default-predicate-bare-exception": (
436-
"Proposal 0050 failure-isolation middleware; queued for v0.14.0"
424+
"llm-provider/058-call-level-retry-non-transient-no-retry": (
425+
"Proposal 0050 call-level retry asserts the deferred per-attempt span surface (0050 partial)"
437426
),
438427
# Proposal 0052 (implementation attribution attributes, v0.44.0):
439428
# observability/059 is the Langfuse-side mapping fixture; uses the

0 commit comments

Comments
 (0)