Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ storage boundary may supply at-rest encryption, or `--profile demo` for an
explicitly non-production run.
Demo completions are `COMPLETED_UNVERIFIED`; Standard and Regulated return
`VERIFIED` only when every consequential effect is confirmed at the workflow's
configured minimum evidence tier.
configured minimum evidence tier. Every run also carries a first-class
`transaction_outcome` that states what is known about the business effect
(`VERIFIED`, `HALTED_BEFORE_EFFECT`, `RECONCILIATION_REQUIRED`, `FAILED_PLATFORM`,
`CANCELED`, `REJECTED_POLICY`, `COMPLETED_UNVERIFIED`) plus a per-step effect
journal.
See [execution profiles](docs/EXECUTION_PROFILES.md).

**Secrets never get recorded.** An `input[type=password]` field (or any field
Expand Down
44 changes: 44 additions & 0 deletions docs/EXECUTION_PROFILES.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,47 @@ Reports retain the legacy `success` field for compatibility and add
`execution_profile`, `execution_outcome`, and `production_eligible`. Production
callers must use `execution_outcome`; Standard and Regulated treat
`COMPLETED_UNVERIFIED` as a non-success exit.

## Transaction outcomes (Section 3)

The coarse `execution_outcome` (`VERIFIED` / `COMPLETED_UNVERIFIED` / `HALTED` /
`FAILED` / `ROLLED_BACK`) is refined into a first-class **terminal transaction
outcome** that states what is known about the BUSINESS EFFECT. It is additive:
`execution_outcome`, `success`, `production_eligible`, and `outcome_envelope`
are unchanged, and the new `transaction_outcome` is derived from the same typed
evidence (see `openadapt_flow.transaction`).

| `transaction_outcome` | Meaning | Billable | Production success |
| --- | --- | --- | --- |
| `VERIFIED` | Every declared effect (and collateral-effect check) passed at/above the required tier. | yes | yes |
| `HALTED_BEFORE_EFFECT` | The run stopped AND the verifier established that no business effect occurred. | no | no |
| `RECONCILIATION_REQUIRED` | Delivery/persistence is uncertain, conflicting, or temporarily unverifiable. The runtime does NOT blind-retry; resuming must reconcile current state first. | no | no |
| `FAILED_PLATFORM` | An OpenAdapt/platform failure before any possible effect. | no (`transaction_platform_fault=true`) | no |
| `CANCELED` | Canceled before any business effect. | no | no |
| `REJECTED_POLICY` | Authorization / identity / qualification / environment refused execution before any effect. | no | no |
| `COMPLETED_UNVERIFIED` | Demo-only completion with no production-grade effect evidence. | never | never |
| `ROLLED_BACK` | A detected duplicate / collateral write was compensated and re-verified (legacy compensation path). | no | no |

Mapping from the coarse outcome: `VERIFIED` and `ROLLED_BACK` map through
1:1; a coarse `HALTED` splits into `REJECTED_POLICY` (a governed pre-execution
refusal or identity refusal), `HALTED_BEFORE_EFFECT` (verifier-established
absence), or `RECONCILIATION_REQUIRED` (any uncertain/conflicting delivery or
persistence, which always dominates); a coarse `FAILED` maps to `CANCELED` (when
the run was canceled) or `FAILED_PLATFORM`.

Each run also persists an **effect journal** (`effect_journal`): one PHI-free
entry per consequential step recording the intended effect (by contract hash),
the actuation attempt state, the verifier's observed effect, verifier freshness,
and any collateral reconciliation. Callers that meter usage should read
`transaction_billable` / `transaction_platform_fault`: a `FAILED_PLATFORM` is
never a billable success and a `COMPLETED_UNVERIFIED` is never a production
success.

**Idempotency.** A caller may pass a run-level `idempotency_key`; when the
`Replayer` is built with an `idempotency_ledger`, a repeat under the same key is
suppressed before any actuation (`idempotent_replay=true`, no consequential
action re-performed) rather than blind-retried.

Scoped out of the runtime and tracked as follow-ups: full saga compensation
steps, the human-reconciliation-task UI, and Cloud/runner propagation of the
transaction taxonomy.
6 changes: 6 additions & 0 deletions openadapt_flow/execution_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ def stamp_execution_outcome(
elif outcome is ExecutionOutcome.ROLLED_BACK:
report.success = False
report.outcome_envelope = build_outcome_envelope(report, workflow)
# Section 3: refine the coarse outcome into a first-class terminal
# transaction outcome + effect journal. Additive -- reads the fields set
# above and never mutates them (leaf import; see openadapt_flow.transaction).
from openadapt_flow.transaction import stamp_transaction_outcome

stamp_transaction_outcome(report, workflow)
return outcome


Expand Down
130 changes: 130 additions & 0 deletions openadapt_flow/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,17 @@ class EffectVerificationEvidence(BaseModel):
final_verdict: Literal["confirmed", "refuted", "indeterminate"]
reconciliation_completed: bool = False
reconciliation_actions: int = Field(default=0, ge=0)
#: What the verifier OBSERVED about the business effect in the system of
#: record, independent of the pass/fail verdict. This is what lets a halted
#: run state honestly what is known about the write: "absent" (the verifier
#: established NO record was written -> HALTED_BEFORE_EFFECT), "conflicting"
#: (a record WAS written but is a duplicate / wrong value -> a business
#: effect that must be RECONCILED), "unknown" (the record could not be read,
#: or a refutation carried no count, so absence cannot be claimed ->
#: RECONCILIATION_REQUIRED, fail-safe), or "present" (accompanies a CONFIRMED
#: or reconciled effect). Additive; defaults to the fail-safe "unknown" so an
#: evidence record that predates this field never asserts "no effect".
observed_effect: Literal["present", "absent", "conflicting", "unknown"] = "unknown"


class StepResult(BaseModel):
Expand Down Expand Up @@ -2132,6 +2143,53 @@ def _validate_evidence_contract(self) -> "ExecutionOutcomeEnvelope":
return self


class EffectJournalEntry(BaseModel):
"""One consequential step's transaction-effect record (PHI-free).

The effect journal is the per-step ledger the reconciliation model reads.
For each consequential step it states the INTENDED effect (by stable
contract hash), the ATTEMPT state (delivered / uncertain / actuated / not
actuated), the OBSERVED effect the independent verifier read from the
system of record, how FRESH that verification was, and any COLLATERAL delta
(reconciling compensations applied). It carries no record values,
parameters, or free text -- only typed evidence already present on the
:class:`StepResult`, so it can be persisted in the run's evidence output
without leaking PHI.
"""

step_id: str
intent: str
consequential: bool = True
#: Stable, non-secret digests of the effect contracts THIS step intended to
#: apply (mirrors ``StepResult.effect_contract_hashes``).
intended_effect_contract_hashes: list[str] = Field(default_factory=list)
#: How far actuation got before verification: the write was never actuated,
#: was delivered through the GUI/native ladder, was delivered via the API
#: tier, or the action API raised AFTER delivery may have begun (the #250
#: uncertain-delivery path -- never blind-retried).
attempt_state: Literal[
"not_actuated", "delivered", "actuated_api", "delivery_uncertain"
] = "not_actuated"
#: The verifier's independent read of the business effect, taken as the worst
#: case across this step's declared effects (unknown < conflicting < absent
#: < present is NOT the ordering; see the classifier -- "conflicting" and
#: "unknown" both force reconciliation, "absent" proves no effect).
observed_effect: Literal["present", "absent", "conflicting", "unknown"] = "unknown"
#: True when every declared effect was CONFIRMED at/above the required tier.
effect_verified: Optional[bool] = None
#: Whether this was an approved-but-unverified GUI write (risk accepted).
approved_unverified: bool = False
#: Verifier freshness: whether independent verification actually ran this
#: run, and the ISO-8601 instant a delivery-uncertainty was observed (the
#: tightest freshness signal we retain, present only on the #250 path).
verification_performed: bool = False
observed_at: Optional[str] = None
#: Collateral delta: count of reconciling compensation actions completed for
#: this step (0 when none). Nonzero means the system of record was mutated to
#: undo a detected duplicate / collateral write.
collateral_reconciliation_actions: int = Field(default=0, ge=0)


class RunReport(BaseModel):
workflow_name: str
started_at: str
Expand Down Expand Up @@ -2178,6 +2236,78 @@ class RunReport(BaseModel):
"model calls, and external-network-call observability."
),
)
# -- Section 3: explicit transaction / reconciliation semantics ------------
# The coarse ``execution_outcome`` (VERIFIED/HALTED/FAILED/...) is refined
# into a first-class TERMINAL TRANSACTION outcome that states what is known
# about the BUSINESS EFFECT. Additive and derived from the same typed
# evidence: ``execution_outcome`` and ``outcome_envelope`` are unchanged, so
# every existing consumer keeps working (see openadapt_flow.transaction).
transaction_outcome: Optional[
Literal[
"VERIFIED",
"HALTED_BEFORE_EFFECT",
"RECONCILIATION_REQUIRED",
"FAILED_PLATFORM",
"CANCELED",
"REJECTED_POLICY",
"COMPLETED_UNVERIFIED",
"ROLLED_BACK",
]
] = Field(
default=None,
description=(
"Terminal transaction outcome describing what is known about the "
"business effect. Refines the coarse execution_outcome without "
"replacing it."
),
)
transaction_billable: Optional[bool] = Field(
default=None,
description=(
"Whether this run represents a chargeable business outcome. A "
"FAILED_PLATFORM (OpenAdapt/platform fault) and a Demo-only "
"COMPLETED_UNVERIFIED are never billable."
),
)
transaction_platform_fault: Optional[bool] = Field(
default=None,
description=(
"True only for FAILED_PLATFORM: an OpenAdapt/platform failure that "
"occurred before any possible business effect, distinct from a "
"customer/governed outcome."
),
)
effect_journal: list[EffectJournalEntry] = Field(
default_factory=list,
description=(
"Per consequential-step transaction ledger (intended effect, "
"attempt state, observed effect, verifier freshness, collateral "
"delta). PHI-free; the substrate the reconciliation model reads."
),
)
idempotency_key: Optional[str] = Field(
default=None,
description=(
"Caller-supplied at-most-once key for this run. When an "
"idempotency ledger is configured, a repeat with the same key is "
"suppressed rather than re-actuated."
),
)
idempotent_replay: bool = Field(
default=False,
description=(
"True when this run was SUPPRESSED as a duplicate of a prior run "
"that already actuated under the same idempotency key. No "
"consequential action was re-performed."
),
)
canceled: bool = Field(
default=False,
description=(
"True when the run was canceled before any business effect could "
"occur. Drives the CANCELED terminal transaction outcome."
),
)
execution_target_kind: Optional[ExecutionTargetKind] = Field(
default=None,
description=(
Expand Down
29 changes: 28 additions & 1 deletion openadapt_flow/runtime/effects/effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from collections.abc import Mapping
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Optional, Protocol, runtime_checkable
from typing import Any, Literal, Optional, Protocol, runtime_checkable

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

Expand Down Expand Up @@ -475,6 +475,33 @@ def should_halt(self) -> bool:
def confirmed(self) -> bool:
return self.verdict is Verdict.CONFIRMED

@property
def observed_effect(self) -> Literal["present", "absent", "conflicting", "unknown"]:
"""Honest read of the BUSINESS EFFECT behind this verdict.

Independent of pass/fail, this states what the system of record showed
so a halted run can distinguish "no write happened" from "a write may
have happened":

- ``present``: CONFIRMED -- the effect landed and is correct.
- ``absent``: REFUTED with an observed count of zero -- the verifier
established that NO record was written (a screen may claim success,
but nothing landed). This is what makes HALTED_BEFORE_EFFECT honest.
- ``conflicting``: REFUTED with a nonzero observed count -- a record WAS
written but is a duplicate / partial / wrong value. A business effect
occurred and must be reconciled.
- ``unknown``: INDETERMINATE (system of record unreachable/unreadable),
or a REFUTED verdict that carried no count. Absence cannot be claimed;
fail safe to reconciliation.
"""
if self.verdict is Verdict.CONFIRMED:
return "present"
if self.verdict is Verdict.INDETERMINATE:
return "unknown"
if self.observed_count is None:
return "unknown"
return "absent" if self.observed_count == 0 else "conflicting"


@runtime_checkable
class EffectVerifier(Protocol):
Expand Down
Loading
Loading