ADR-0021 — Belief-to-Action Contract Layer v1¶
Status¶
Accepted (2026-06-07).
Context¶
Hasta ADR-0020 todo el sistema vive a un lado de la creencia. El agente produce belief, lo introspecta y el operador lo analiza offline. Nada cierra el ciclo: la creencia no se consume para producir consecuencias.
La pregunta central de misión —"¿qué consecuencias tiene la diferencia entre lo que el agente cree saber y lo que sabe?"— es indemostrable porque el agente carece de un contrato runtime que conecte creencia + introspección con acción.
Cinco gaps estructurales están bloqueados por la misma falta de contrato:
- ADR-0009 (Autonomy Under Uncertainty) define tiers T0/T1/T2/T3
con autoridad sobre actuadores; ninguno es instanciable sin un tipo
Decision. Cada futura implementación inventaría el suyo. - ADR-0011 (T0 Safety Vetoes) define vetos sobre comandos; no
existe el "comando-de-intención" que vetar. Los
ActuatorCommandson un nivel demasiado bajo. - ADR-0020 (BeliefSelfAssessment) publica una afirmación
estructurada sobre lo que el agente cree saber. Nadie la consume.
Sin un
Policyque la lea, el canal/self_assessmentes señal observacional sin efecto. - Cualquier estimador real futuro (Kalman, factor graph) duplicaría wiring belief → acción si el contrato no existe primero.
- Cualquier backend de simulación concreto (PyBullet, Gazebo) acabaría wireando belief → policy → actuator ad-hoc.
ADR-0021 cierra este gap con contratos, no implementaciones. Define los shapes mínimos auditables para que toda futura ADR de control / safety / pilot override / mission planning / hardware migration se componga sobre la misma capa, sin re-invención y sin acoplamiento prematuro a una política específica.
Esta es la primera ADR que define el lado decide / act de la misión.
Decision¶
Añadir el paquete project_ghost.core.decisions con cinco contratos
puros y un wiring mínimo de telemetría. No incluye controladores,
planners, vetos concretos ni translation a ActuatorCommand. Solo
shapes.
1. DecisionKind (StrEnum cerrada)¶
Catálogo de decisiones legales del agente. Modificar requiere ADR
explícito (misma disciplina que PerceptionMode, Validity,
SelfAssessmentLevel).
proceed— "Continúo la misión con la creencia actual."hold— "Mantengo posición; mi creencia no es suficiente para navegar pero sí para sostenerme."yield_to_pilot— "Cedo autoridad al piloto humano."engage_rtl— "Inicio Return-To-Launch por degradación de creencia."engage_land— "Inicio aterrizaje controlado."engage_kill— "Corto thrust por imposibilidad de operar de forma segura."abstain_uncertain— "Me abstengo de tomar una decisión; mi creencia no soporta ninguna afirmación."
2. DecisionContext (frozen dataclass)¶
Lo que el Policy ve. Auto-contenido para que decide sea pure
function.
belief_stamp_sim_ns: intself_assessment: BeliefSelfAssessment | None(None cuando no hay introspección disponible)flight_status: FlightStatusmission_status: MissionStatusperception_mode: PerceptionMode | None(opcional; presente cuando unPerceptionModeDetectoresté wireado)
3. Decision (frozen dataclass)¶
La afirmación clasificatoria del agente sobre qué decide hacer.
kind: DecisionKinddecision_stamp_sim_ns: int(igual acontext.belief_stamp_sim_nstípicamente; reactivo síncrono)reason: str— taxonomizado por formato (^[a-z][a-z0-9_]*$, longitud 1-64). No es free text; es un identificador estable que el caller usa consistentemente.
4. DecisionRationale (frozen dataclass)¶
Artefacto auditable que une la decisión con sus inputs. Content-addressed
vía SHA-256 del BeliefSelfAssessment input.
decision: Decisionbelief_stamp_sim_ns: int(debe matcheardecision.decision_stamp_sim_ns)self_assessment_sha256: str | None(64-char lowercase hex; None cuando no hubo self-assessment en el context)policy_id: str(identificador estable del policy productor)
Invariante: dado un BeliefSelfAssessment S, el DecisionRationale
que lo cite con self_assessment_sha256 = sha256(canonical(S)) deja
trazable bit-a-bit qué assessment justificó la decisión.
5. Protocols¶
@runtime_checkable
class Policy(Protocol):
@property
def policy_id(self) -> str: ...
def decide(self, context: DecisionContext) -> Decision: ...
@runtime_checkable
class DecisionSink(Protocol):
def publish(
self,
decision: Decision,
rationale: DecisionRationale,
) -> None: ...
DecisionSink.publish toma siempre los dos — Decision Y Rationale.
Esta es la enforcement contractual de "ninguna decisión sin
justificación". No se puede publicar una decisión sola.
6. Implementaciones de referencia¶
NullDecisionSink— descarta. Para tests y para usardecide_and_publishcuando no se quiere persistir.RecordingDecisionSink— guarda en memoria. Para tests y verificación.UncertaintyAwareReferencePolicy— policy mínima documentada que mapeaSelfAssessmentLevel.{KNOWN, UNCERTAIN, UNKNOWN}→DecisionKind.{proceed, hold, abstain_uncertain}. No usayield_to_pilot,engage_rtl,engage_land,engage_kill— esos del catálogo quedan disponibles para policies futuras (tier 0/1 safety, pilot override). Es observacional: PROCEED dice "afirmo poder navegar", HOLD dice "afirmo que debo esperar", ABSTAIN dice "afirmo no poder decidir".
7. Orquestación¶
def decide_with_rationale(
policy: Policy, context: DecisionContext,
) -> tuple[Decision, DecisionRationale]: ...
def decide_and_publish(
policy: Policy, context: DecisionContext, sink: DecisionSink,
) -> Decision: ...
decide_with_rationale ejecuta el policy y construye el DecisionRationale
calculando el SHA-256 canónico del self-assessment del context (stdlib
puro; sin dep de telemetry).
decide_and_publish es el one-shot canónico para la mayoría de
callers.
8. Telemetría¶
CHANNEL_DECISIONS = "/decisions"entelemetry.channels.DecisionToTelemetryAdapter(mismo patrón queSelfAssessmentToTelemetryAdapter). Acepta(decision, rationale), valida querationale.decision == decision, publica elrationalecomo record (contiene el decision dentro) usandodecision.decision_stamp_sim_nscomolog_time(ADR-0002).- Decoder registrado en
telemetry.replay._build_decoder_table()para el qualified name deDecisionRationale.
Inputs¶
- En runtime: un
Policy, unDecisionContext, opcionalmente unDecisionSink. - Para replay: un
.mcapcon records en/decisions.
Outputs¶
Decision+DecisionRationalepor cada invocación de policy.- Records
DecisionRationalepublicados en/decisions(cuando un sink esté wireado).
Limits¶
- No traduce
DecisionaActuatorCommand. Esa translation layer es una ADR distinta. Decision es la intención; ActuatorCommand es la ejecución; el mapeo entre ambas tiene grados de libertad (qué ActuatorLevel, qué trayectoria) que merecen su propio diseño. - No define cómo se construye un
DecisionContextdesde el runtime. Caller responsibility. El context es input, no output, de esta capa. - No define cómo se compone una policy con otra. La envoltura
estilo
SafetyVetoPolicy(inner_policy)es construcción del caller; el contratoPolicylo permite trivialmente porque es un Protocol. - No persiste el
BeliefSelfAssessmentreferenciado por el rationale. El operador debe garantizar que el assessment está accesible (típicamente en/self_assessmentdel mismo MCAP); el rationale sólo carga su hash. - El catálogo
DecisionKindes cerrado para v1. Añadir un kind requiere ADR amendment (mismo posture quePerceptionMode). reasones taxonómico por formato, no por catálogo cerrado. Cualquier policy puede usar nuevos reasons; el contrato sólo asegura formato estable. Permite extensibilidad sin re-versionar el ADR.- No reacciona a perception mode automáticamente. El reference
policy ignora
context.perception_mode. Una future policy puede consumirlo; el contrato lo expone. decision_stamp_sim_ns == belief_stamp_sim_nsenforced. Decisión diferida (decisión que reaccione a un belief de hace N ns) queda fuera de scope; una ADR futura la habilitaría con un campo separado.
Determinism¶
- Mismo
(policy, context)→ mismoDecisiony mismoDecisionRationalebyte-a-byte tras serialización canónica. decide_with_rationalees pure function (siempre que la policy lo sea).UncertaintyAwareReferencePolicy.decidees pure function.- SHA-256 del self-assessment es estable cross-CPython porque se
computa con
hashlib.sha256+json.dumps(sort_keys=True, ensure_ascii=False, separators=(",", ":"))sobredataclasses.asdictdel assessment — mismo posture quethresholds_sha256(ADR-0020). - Round-trip MCAP:
DecisionRationalecapturado → leído → decoded → igual al original. DecisionToTelemetryAdapterusadecision.decision_stamp_sim_nscomolog_time. Sin reloj de pared (ADR-0002).
Exclusiones explícitas¶
NO implementadas y NO extension points sancionados por esta ADR:
- No controlador concreto. Ningún PID, MPC, LQR.
- No safety supervisor concreto. ADR-0011 define qué debe hacer un
T0; esta ADR provee
Policy/Decisionpara que un T0 futuro se modele sin reinventar el tipo. - No mission planner concreto. ADR-0009 §3 lo describe; esta ADR no lo implementa.
- No pilot override concreto. ADR-0011 §5; futuro ADR lo modela
como un
Policy. - No translation Decision → ActuatorCommand. Capa distinta.
- No backend de simulación. PyBullet/Gazebo siguen fuera.
- No real estimator. Sigue toy (ADR-0015).
- No "decision score" / "decision confidence". Las decisiones son categóricas; no se le pone score a una decisión sin pasar por inferencia probabilística (rechazado de raíz).
- No detección de "decisiones malas" post-hoc. Cualquier análisis comparativo de decisiones es ADR distinta.
- No corrección automática de decisiones. Las decisiones son publicadas; consumidores aguas abajo deciden cómo reaccionar.
Cláusula reforzada:
Decidir es declarar. No es inferir, no es calcular, no es scoring. El sistema expone el shape del declaramiento; el operador escoge la policy.
Cadena de provenance completa¶
Tras este ADR, la cadena auditable de runtime es:
VehicleState (belief)
→ assess_belief()
→ BeliefSelfAssessment (sha = S)
→ DecisionContext (incluye assessment)
→ Policy.decide(context)
→ Decision
→ DecisionRationale (carga self_assessment_sha256 = S)
→ DecisionSink.publish(decision, rationale)
→ MCAP /decisions
Un auditor con el MCAP puede:
- Leer
/decisions→ obtener(decision, rationale). - Leer
rationale.self_assessment_sha256yrationale.policy_id. - Leer
/self_assessmenty verificar que existe un assessment con el mismo SHA ybelief_stamp_sim_ns. - Confirmar que el
decision_stamp_sim_nsmatchea. - Re-aplicar el policy (si es público) y verificar bit-a-bit que
produce el mismo
Decision.
Trazabilidad bit-a-bit del belief al acto.
Consequences¶
Positivo.
- Cierre del ciclo creencia → introspección → acción. Por primera vez el agente puede afirmar qué decide hacer, no sólo qué cree.
- Cinco ADRs futuras (todos los tiers de ADR-0009, el safety de
ADR-0011, futuro pilot override, futura mission planner) se
modelan trivialmente como
Policydistintas sobre el mismo contract. - El self-assessment de ADR-0020 gana un consumer natural: deja de ser señal observacional sin efecto.
- El channel
/decisionsse suma a los canales auditables estándar. - La cadena de provenance content-addressed vía SHA-256 cierra end-to-end: belief → assessment → rationale → decision.
Negativo.
- El operador ahora debe declarar una
Policy. Es responsabilidad consciente; las consecuencias son trazables. - El catálogo cerrado
DecisionKindlimita la expresividad — sólo siete kinds. Si la investigación requiere finer-grained, ADR amendment necesaria. Trade-off intencional: auditabilidad sobre flexibilidad.
Alternativas consideradas¶
- Hacer
DecisionunActuatorCommanddirecto. Rechazado: acopla intención con ejecución. Imposibilita la composición de policies sobre la misma intención. - No incluir
DecisionRationale; sóloDecision. Rechazado: perdería content-address al assessment. La cadena de provenance se rompería. reasoncomo StrEnum cerrada. Rechazado: cada policy nueva requeriría amendment. Trade-off: extensibilidad sin re-versionar.- Incluir
PerceptionModecomo required, no optional. Rechazado: forzaría el wiring delPerceptionModeDetectorantes de que sea necesario. Optional permite construir DecisionContext sin él. - Permitir
decision_stamp_sim_ns != belief_stamp_sim_ns. Rechazado en v1: introduciría ambigüedad sobre qué creencia justifica la decisión. Una future ADR puede liberar este enforcement.
Backward compatibility¶
- ADR-0001..0020 sin tocar.
- Nuevo paquete
core.decisions. - Nuevo canal
/decisions(canales previos intactos). - Nuevo adapter (adapters previos intactos).
- Decoder añadido al catálogo cerrado (entradas previas intactas).
- Cero rotura.
Mission posture¶
Es el primer ADR que da al agente el shape para actuar. Hasta hoy el agente sólo afirmaba creer. Desde hoy puede afirmar qué hacer.
La misión del proyecto sigue siendo construir agentes que sepan cuándo no saben y actúen en consecuencia. ADR-0020 implementó la primera parte. ADR-0021 da el shape para la segunda, sin asumir ninguna política específica de actuación.
Las consecuencias de creer mal — la tercera cláusula de la pregunta de misión — son ahora representables y auditables. Una future ADR les pondrá implementación; ésta les pone contrato.