Skip to content
Closed
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
60 changes: 54 additions & 6 deletions openadapt_types/human_decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,20 @@ class HumanDecisionAction(str, Enum):
is wrong; that assertion changes future runs and belongs to ``teach``,
which carries the demonstration and requalification gate such authority
requires.

``reconcile`` asks the runner to re-establish the business effect after an
uncertain delivery. It is intentionally distinct from
``verify_and_resume``: reconciliation may prove that the original action
already succeeded, and therefore must never imply permission to dispatch
that action again.
"""

VERIFY_AND_RESUME = "verify_and_resume"
SKIP = "skip"
REJECT = "reject"
TEACH = "teach"
ESCALATE = "escalate"
RECONCILE = "reconcile"


class HumanDecisionDeliveryState(str, Enum):
Expand Down Expand Up @@ -298,7 +305,7 @@ class HumanDecisionTaskV1(_StrictContract):
#: can legitimately offer every member at once. Widening it is what a new
#: member costs, and it is why a consumer must re-validate against the
#: current schema rather than a cached copy.
allowed_actions: tuple[HumanDecisionAction, ...] = Field(min_length=1, max_length=5)
allowed_actions: tuple[HumanDecisionAction, ...] = Field(min_length=1, max_length=6)
required_authn: HumanDecisionRequiredAuthn
created_at: StrictStr = Field(
min_length=20, max_length=40, pattern=_TIMESTAMP_PATTERN
Expand All @@ -315,6 +322,13 @@ class HumanDecisionTaskV1(_StrictContract):
def _validate_envelope(self) -> "HumanDecisionTaskV1":
if len(set(self.allowed_actions)) != len(self.allowed_actions):
raise ValueError("allowed_actions must be unique")
if (
HumanDecisionAction.RECONCILE in self.allowed_actions
and self.delivery_state is HumanDecisionDeliveryState.NOT_DELIVERED
):
raise ValueError(
"reconcile requires a delivered or delivery-uncertain action"
)
created_at = _parse_timestamp(self.created_at, "created_at")
expires_at = _parse_timestamp(self.expires_at, "expires_at")
if expires_at <= created_at:
Expand Down Expand Up @@ -424,6 +438,7 @@ class HumanDecisionReceiptReason(str, Enum):
PENDING_RUNNER = "pending_runner"
VERIFIED_AND_RESUMED = "verified_and_resumed"
SKIPPED_AND_RESUMED = "skipped_and_resumed"
RECONCILED_AND_RESUMED = "reconciled_and_resumed"
CONTINUATION_HALTED = "continuation_halted"
REVALIDATION_REFUSED = "revalidation_refused"
EXPIRED = "expired"
Expand Down Expand Up @@ -459,6 +474,7 @@ class HumanDecisionReceiptReason(str, Enum):
{
HumanDecisionReceiptReason.VERIFIED_AND_RESUMED,
HumanDecisionReceiptReason.SKIPPED_AND_RESUMED,
HumanDecisionReceiptReason.RECONCILED_AND_RESUMED,
}
),
HumanDecisionReceiptState.REFUSED: frozenset(
Expand Down Expand Up @@ -509,6 +525,9 @@ class HumanDecisionReceiptV1(_StrictContract):
The digests are one-way commitments, not content: they let a consumer bind
this receipt to the exact capability, request, decision record, and
transition receipt it came from without ever receiving those payloads.
A completed receipt requires ``report_success=true`` and a transition
receipt digest. A producer cannot use a success-shaped state without the
corresponding transition commitment.

``signature`` is optional because a runtime that returns a receipt over an
authenticated loopback connection is already inside its own trust boundary
Expand Down Expand Up @@ -546,21 +565,50 @@ def _validate_outcome(self) -> "HumanDecisionReceiptV1":
f"reason_code {self.reason_code.value!r} is not a cause of state "
f"{self.state.value!r}"
)
if (
self.report_success
and self.state not in HUMAN_DECISION_RECEIPT_SUCCESS_STATES
):
is_success = self.state in HUMAN_DECISION_RECEIPT_SUCCESS_STATES
if self.report_success and not is_success:
raise ValueError(
f"report_success cannot be true for state {self.state.value!r}"
)
if is_success and self.report_success is not True:
raise ValueError("a completed receipt requires report_success=true")
if is_success and self.transition_receipt_digest is None:
raise ValueError("a completed receipt requires a transition receipt digest")
completed_action = {
HumanDecisionReceiptReason.VERIFIED_AND_RESUMED: (
HumanDecisionAction.VERIFY_AND_RESUME
),
HumanDecisionReceiptReason.SKIPPED_AND_RESUMED: HumanDecisionAction.SKIP,
HumanDecisionReceiptReason.RECONCILED_AND_RESUMED: (
HumanDecisionAction.RECONCILE
),
}
expected_action = completed_action.get(self.reason_code)
if expected_action is not None and self.action is not expected_action:
raise ValueError(
f"reason_code {self.reason_code.value!r} requires action "
f"{expected_action.value!r}"
)
terminal_action = {
HumanDecisionReceiptState.DEMONSTRATION_REQUESTED: HumanDecisionAction.TEACH,
HumanDecisionReceiptState.ESCALATED: HumanDecisionAction.ESCALATE,
HumanDecisionReceiptState.REJECTED: HumanDecisionAction.REJECT,
}.get(self.state)
if terminal_action is not None and self.action is not terminal_action:
raise ValueError(
f"state {self.state.value!r} requires action {terminal_action.value!r}"
)
_parse_timestamp(self.decided_at, "decided_at")
return self

@property
def succeeded(self) -> bool:
"""Whether this receipt reports a successful workflow continuation."""

return self.state in HUMAN_DECISION_RECEIPT_SUCCESS_STATES
return (
self.state in HUMAN_DECISION_RECEIPT_SUCCESS_STATES
and self.report_success is True
)

def unsigned_payload(self) -> dict[str, Any]:
"""Return the deterministic language-agnostic signed payload."""
Expand Down
8 changes: 5 additions & 3 deletions openadapt_types/schemas/human-decision-receipt-v1.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"$defs": {
"HumanDecisionAction": {
"description": "Portable action names; the runtime remains authoritative.\n\n``verify_and_resume`` maps to Flow's attended ``continue`` operation. It\nmeans that the operator prepared the live state for fresh revalidation; it\nnever authorizes blind repetition of a prior action.\n\n``reject`` and ``escalate`` are deliberately separate members rather than\ntwo labels on one action, because they do opposite things to the run.\n``escalate`` *parks* it: the durable pause stays intact and a qualified\ncolleague can still continue it. ``reject`` *terminates* it: the operator\nlooked at the live application and is asserting that this run must not\nproceed at all. Collapsing the two would leave the recorded answer\ndistribution unable to distinguish \"I don't know\" from \"stop\", which is the\nonly reason a disagreement action is worth its cost.\n\n``reject`` is scoped to one run. It is not a claim that the saved workflow\nis wrong; that assertion changes future runs and belongs to ``teach``,\nwhich carries the demonstration and requalification gate such authority\nrequires.",
"description": "Portable action names; the runtime remains authoritative.\n\n``verify_and_resume`` maps to Flow's attended ``continue`` operation. It\nmeans that the operator prepared the live state for fresh revalidation; it\nnever authorizes blind repetition of a prior action.\n\n``reject`` and ``escalate`` are deliberately separate members rather than\ntwo labels on one action, because they do opposite things to the run.\n``escalate`` *parks* it: the durable pause stays intact and a qualified\ncolleague can still continue it. ``reject`` *terminates* it: the operator\nlooked at the live application and is asserting that this run must not\nproceed at all. Collapsing the two would leave the recorded answer\ndistribution unable to distinguish \"I don't know\" from \"stop\", which is the\nonly reason a disagreement action is worth its cost.\n\n``reject`` is scoped to one run. It is not a claim that the saved workflow\nis wrong; that assertion changes future runs and belongs to ``teach``,\nwhich carries the demonstration and requalification gate such authority\nrequires.\n\n``reconcile`` asks the runner to re-establish the business effect after an\nuncertain delivery. It is intentionally distinct from\n``verify_and_resume``: reconciliation may prove that the original action\nalready succeeded, and therefore must never imply permission to dispatch\nthat action again.",
"enum": [
"verify_and_resume",
"skip",
"reject",
"teach",
"escalate"
"escalate",
"reconcile"
],
"title": "HumanDecisionAction",
"type": "string"
Expand All @@ -18,6 +19,7 @@
"pending_runner",
"verified_and_resumed",
"skipped_and_resumed",
"reconciled_and_resumed",
"continuation_halted",
"revalidation_refused",
"expired",
Expand Down Expand Up @@ -47,7 +49,7 @@
}
},
"additionalProperties": false,
"description": "The closed, PHI-free terminal outcome of one attended decision.\n\nThis is the only decision result that may cross to a phone, a tray, or an\nauthenticated remote relay. It is a rebuilt value, not a filtered copy of\na runtime's audit record: there is no free-text field, no operator\nidentity, no workflow or step label, no parameter, no path, no observed\nvalue, and no evidence. Protected content is structurally unrepresentable\nrather than stripped on send, so a later field addition on the producer\nside cannot reopen a hole here.\n\n``action`` reuses :class:`HumanDecisionAction`, the same portable\nvocabulary the task advertises in ``allowed_actions``, so a consumer\ncompares the receipt against the task it answered without translating an\nengine-internal name.\n\nThe digests are one-way commitments, not content: they let a consumer bind\nthis receipt to the exact capability, request, decision record, and\ntransition receipt it came from without ever receiving those payloads.\n\n``signature`` is optional because a runtime that returns a receipt over an\nauthenticated loopback connection is already inside its own trust boundary\nand does not sign. A receipt that arrives over a network must be signed,\nand its consumer must verify it: see :meth:`verify_hmac`.",
"description": "The closed, PHI-free terminal outcome of one attended decision.\n\nThis is the only decision result that may cross to a phone, a tray, or an\nauthenticated remote relay. It is a rebuilt value, not a filtered copy of\na runtime's audit record: there is no free-text field, no operator\nidentity, no workflow or step label, no parameter, no path, no observed\nvalue, and no evidence. Protected content is structurally unrepresentable\nrather than stripped on send, so a later field addition on the producer\nside cannot reopen a hole here.\n\n``action`` reuses :class:`HumanDecisionAction`, the same portable\nvocabulary the task advertises in ``allowed_actions``, so a consumer\ncompares the receipt against the task it answered without translating an\nengine-internal name.\n\nThe digests are one-way commitments, not content: they let a consumer bind\nthis receipt to the exact capability, request, decision record, and\ntransition receipt it came from without ever receiving those payloads.\nA completed receipt requires ``report_success=true`` and a transition\nreceipt digest. A producer cannot use a success-shaped state without the\ncorresponding transition commitment.\n\n``signature`` is optional because a runtime that returns a receipt over an\nauthenticated loopback connection is already inside its own trust boundary\nand does not sign. A receipt that arrives over a network must be signed,\nand its consumer must verify it: see :meth:`verify_hmac`.",
"properties": {
"action": {
"$ref": "#/$defs/HumanDecisionAction"
Expand Down
7 changes: 4 additions & 3 deletions openadapt_types/schemas/human-decision-task-v1.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"$defs": {
"HumanDecisionAction": {
"description": "Portable action names; the runtime remains authoritative.\n\n``verify_and_resume`` maps to Flow's attended ``continue`` operation. It\nmeans that the operator prepared the live state for fresh revalidation; it\nnever authorizes blind repetition of a prior action.\n\n``reject`` and ``escalate`` are deliberately separate members rather than\ntwo labels on one action, because they do opposite things to the run.\n``escalate`` *parks* it: the durable pause stays intact and a qualified\ncolleague can still continue it. ``reject`` *terminates* it: the operator\nlooked at the live application and is asserting that this run must not\nproceed at all. Collapsing the two would leave the recorded answer\ndistribution unable to distinguish \"I don't know\" from \"stop\", which is the\nonly reason a disagreement action is worth its cost.\n\n``reject`` is scoped to one run. It is not a claim that the saved workflow\nis wrong; that assertion changes future runs and belongs to ``teach``,\nwhich carries the demonstration and requalification gate such authority\nrequires.",
"description": "Portable action names; the runtime remains authoritative.\n\n``verify_and_resume`` maps to Flow's attended ``continue`` operation. It\nmeans that the operator prepared the live state for fresh revalidation; it\nnever authorizes blind repetition of a prior action.\n\n``reject`` and ``escalate`` are deliberately separate members rather than\ntwo labels on one action, because they do opposite things to the run.\n``escalate`` *parks* it: the durable pause stays intact and a qualified\ncolleague can still continue it. ``reject`` *terminates* it: the operator\nlooked at the live application and is asserting that this run must not\nproceed at all. Collapsing the two would leave the recorded answer\ndistribution unable to distinguish \"I don't know\" from \"stop\", which is the\nonly reason a disagreement action is worth its cost.\n\n``reject`` is scoped to one run. It is not a claim that the saved workflow\nis wrong; that assertion changes future runs and belongs to ``teach``,\nwhich carries the demonstration and requalification gate such authority\nrequires.\n\n``reconcile`` asks the runner to re-establish the business effect after an\nuncertain delivery. It is intentionally distinct from\n``verify_and_resume``: reconciliation may prove that the original action\nalready succeeded, and therefore must never imply permission to dispatch\nthat action again.",
"enum": [
"verify_and_resume",
"skip",
"reject",
"teach",
"escalate"
"escalate",
"reconcile"
],
"title": "HumanDecisionAction",
"type": "string"
Expand Down Expand Up @@ -256,7 +257,7 @@
"items": {
"$ref": "#/$defs/HumanDecisionAction"
},
"maxItems": 5,
"maxItems": 6,
"minItems": 1,
"title": "Allowed Actions",
"type": "array"
Expand Down
Loading
Loading