| """Tier 3 consumer-side workflows for Pulse-ER. |
| |
| These workflows sit above the low-level tool layer and transform the current |
| observation or episode trace into judge-friendly and agent-friendly outputs. |
| They are intentionally backend-agnostic so the same logic can be used against |
| mock and real Pulse runtimes. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from enum import Enum |
|
|
| from pydantic import BaseModel, ConfigDict, Field |
|
|
| from .episode_runner import EpisodeTrace |
| from .models import PulsePhysiologyObservation |
|
|
|
|
| class DeteriorationStatus(str, Enum): |
| """Clinical status buckets used for judge-facing Tier 3 reasoning.""" |
|
|
| STABLE = "stable" |
| MONITORING = "monitoring" |
| DETERIORATING = "deteriorating" |
| CRITICAL = "critical" |
|
|
|
|
| MINOR_ALERTS = {"tachycardia", "tachypnea"} |
| MAJOR_ALERTS = {"hypotension", "hypoxemia", "blood_loss", "cardiovascular_collapse"} |
| MAJOR_VITALS = ( |
| "mean_arterial_pressure_mmhg", |
| "spo2", |
| "heart_rate_bpm", |
| "respiration_rate_bpm", |
| ) |
| CRITICAL_ACCELERATION_VITALS = ( |
| "mean_arterial_pressure_mmhg", |
| "spo2", |
| "heart_rate_bpm", |
| ) |
|
|
|
|
| def _mental_status_value(mental_status) -> str: |
| return getattr(mental_status, "value", str(mental_status)) |
|
|
|
|
| def _risk_level(observation: PulsePhysiologyObservation) -> str: |
| alerts = set(observation.active_alerts) |
| mental_status = _mental_status_value(observation.mental_status) |
| if observation.done or "cardiovascular_collapse" in alerts or mental_status == "unresponsive": |
| return "critical" |
| if {"hypotension", "blood_loss", "hypoxemia"} & alerts or mental_status in {"pain", "verbal"}: |
| return "high" |
| if alerts: |
| return "moderate" |
| return "low" |
|
|
|
|
| def _priority_reasons(observation: PulsePhysiologyObservation) -> list[str]: |
| alerts = set(observation.active_alerts) |
| reasons: list[str] = [] |
| if "blood_loss" in alerts: |
| reasons.append("Ongoing blood loss threatens perfusion and should be controlled early.") |
| if "hypotension" in alerts: |
| reasons.append("Low blood pressure suggests reduced perfusion and possible shock.") |
| if "hypoxemia" in alerts: |
| reasons.append("Low oxygen saturation requires respiratory support or oxygen therapy.") |
| if "tachypnea" in alerts: |
| reasons.append("High respiratory rate suggests respiratory stress or compensation.") |
| if "tachycardia" in alerts: |
| reasons.append("Persistent tachycardia may reflect compensation, stress, or ongoing instability.") |
| if not reasons: |
| reasons.append("No active high-priority alerts are present, so reassessment and safe monitoring are appropriate.") |
| return reasons |
|
|
|
|
| def _mean_arterial_pressure(observation: PulsePhysiologyObservation) -> float | None: |
| """Return MAP directly or derive it from systolic and diastolic pressure.""" |
|
|
| if observation.mean_arterial_pressure_mmhg is not None: |
| return observation.mean_arterial_pressure_mmhg |
| if observation.systolic_bp_mmhg is None or observation.diastolic_bp_mmhg is None: |
| return None |
| return (observation.systolic_bp_mmhg + (2 * observation.diastolic_bp_mmhg)) / 3 |
|
|
|
|
| def _spo2_percent(observation: PulsePhysiologyObservation) -> float | None: |
| """Return oxygen saturation as a human-readable percentage when available.""" |
|
|
| if observation.spo2 is None: |
| return None |
| return round(observation.spo2 * 100, 1) |
|
|
|
|
| def _vital_value(observation: PulsePhysiologyObservation, vital_name: str) -> float | None: |
| """Resolve one vital from an observation, including derived MAP support.""" |
|
|
| if vital_name == "mean_arterial_pressure_mmhg": |
| return _mean_arterial_pressure(observation) |
| return getattr(observation, vital_name, None) |
|
|
|
|
| def _vital_deviation(vital_name: str, value: float | None) -> float: |
| """Measure how far a vital has drifted from its safe range for trend scoring.""" |
|
|
| if value is None: |
| return 0.0 |
|
|
| if vital_name == "mean_arterial_pressure_mmhg": |
| return max(0.0, 65.0 - value) |
| if vital_name == "spo2": |
| return max(0.0, 0.94 - value) |
| if vital_name == "heart_rate_bpm": |
| if 60.0 <= value <= 100.0: |
| return 0.0 |
| return min(abs(value - 60.0), abs(value - 100.0)) |
| if vital_name == "respiration_rate_bpm": |
| if 12.0 <= value <= 20.0: |
| return 0.0 |
| return min(abs(value - 12.0), abs(value - 20.0)) |
| return 0.0 |
|
|
|
|
| def _trend_tolerance(vital_name: str) -> float: |
| """Return the minimum meaningful drift for one vital trend calculation.""" |
|
|
| if vital_name == "mean_arterial_pressure_mmhg": |
| return 3.0 |
| if vital_name == "spo2": |
| return 0.02 |
| if vital_name == "heart_rate_bpm": |
| return 5.0 |
| if vital_name == "respiration_rate_bpm": |
| return 2.0 |
| return 0.0 |
|
|
|
|
| def _recent_observations( |
| observation: PulsePhysiologyObservation, |
| previous_observation: PulsePhysiologyObservation | None = None, |
| observations: list[PulsePhysiologyObservation] | None = None, |
| *, |
| window: int = 3, |
| ) -> list[PulsePhysiologyObservation]: |
| """Assemble the most recent observation window for trend-aware Tier 3 logic.""" |
|
|
| recent: list[PulsePhysiologyObservation] = [] |
| if observations: |
| recent.extend(observations) |
| elif previous_observation is not None: |
| recent.extend([previous_observation, observation]) |
| else: |
| recent.append(observation) |
|
|
| if not recent or recent[-1] != observation: |
| recent.append(observation) |
| return recent[-window:] |
|
|
|
|
| def get_trend( |
| vital_name: str, |
| observations: list[PulsePhysiologyObservation], |
| window: int = 3, |
| ) -> str: |
| """Classify a vital as improving, stable, or worsening over recent observations.""" |
|
|
| recent = observations[-window:] |
| deviations = [ |
| _vital_deviation(vital_name, _vital_value(observation, vital_name)) |
| for observation in recent |
| ] |
| if len(deviations) < 2: |
| return "stable" |
|
|
| delta = deviations[-1] - deviations[0] |
| tolerance = _trend_tolerance(vital_name) |
| if delta > tolerance: |
| return "worsening" |
| if delta < -tolerance: |
| return "improving" |
| return "stable" |
|
|
|
|
| def _is_accelerating(vital_name: str, observations: list[PulsePhysiologyObservation]) -> bool: |
| """Detect acceleration when a vital drifts farther out of range step over step.""" |
|
|
| recent = observations[-3:] |
| if len(recent) < 3: |
| return False |
|
|
| deviations = [ |
| _vital_deviation(vital_name, _vital_value(observation, vital_name)) |
| for observation in recent |
| ] |
| first_delta = deviations[1] - deviations[0] |
| second_delta = deviations[2] - deviations[1] |
| tolerance = _trend_tolerance(vital_name) |
| return ( |
| first_delta > 0 |
| and second_delta > first_delta |
| and second_delta > (tolerance / 2) |
| and deviations[-1] > (2 * tolerance) |
| ) |
|
|
|
|
| def _hard_threshold_reason(observation: PulsePhysiologyObservation) -> str | None: |
| """Return the first critical threshold breach, if one is present.""" |
|
|
| map_value = _mean_arterial_pressure(observation) |
| if map_value is not None and map_value < 50.0: |
| return "MAP is below 50 mmHg, indicating critical perfusion failure." |
| if observation.spo2 is not None and observation.spo2 < 0.85: |
| return "SpO2 is below 85%, indicating critical hypoxemia." |
| if observation.heart_rate_bpm is not None and observation.heart_rate_bpm > 150.0: |
| return "Heart rate is above 150 bpm, indicating critical cardiovascular stress." |
| if observation.heart_rate_bpm is not None and observation.heart_rate_bpm < 40.0: |
| return "Heart rate is below 40 bpm, indicating critical bradycardia." |
| return None |
|
|
|
|
| def _classify_deterioration_status( |
| observation: PulsePhysiologyObservation, |
| recent_observations: list[PulsePhysiologyObservation], |
| ) -> tuple[DeteriorationStatus, str, dict[str, str]]: |
| """Classify status from alert severity and recent vital trends.""" |
|
|
| alerts = set(observation.active_alerts) |
| trend_map = { |
| vital_name: get_trend(vital_name, recent_observations) |
| for vital_name in MAJOR_VITALS |
| } |
|
|
| hard_threshold_reason = _hard_threshold_reason(observation) |
| if hard_threshold_reason is not None: |
| return DeteriorationStatus.CRITICAL, hard_threshold_reason, trend_map |
|
|
| if any(_is_accelerating(vital_name, recent_observations) for vital_name in CRITICAL_ACCELERATION_VITALS): |
| return ( |
| DeteriorationStatus.CRITICAL, |
| "At least one major vital is accelerating away from the safe range.", |
| trend_map, |
| ) |
|
|
| worsening_major_vitals = [ |
| vital_name |
| for vital_name, trend in trend_map.items() |
| if vital_name in {"mean_arterial_pressure_mmhg", "spo2", "heart_rate_bpm"} and trend == "worsening" |
| ] |
| minor_alert_count = len(alerts & MINOR_ALERTS) |
|
|
| if not alerts and all(trend != "worsening" for trend in trend_map.values()): |
| return DeteriorationStatus.STABLE, "No active alerts and no worsening vital trends are present.", trend_map |
|
|
| if worsening_major_vitals or minor_alert_count >= 2: |
| return ( |
| DeteriorationStatus.DETERIORATING, |
| "A major vital is trending the wrong way or multiple minor alerts are accumulating.", |
| trend_map, |
| ) |
|
|
| return ( |
| DeteriorationStatus.MONITORING, |
| "Only minor or non-progressive abnormalities are present, so close monitoring is appropriate.", |
| trend_map, |
| ) |
|
|
|
|
| class NextStepRecommendation(BaseModel): |
| """Tier 3 recommendation for the next best action.""" |
|
|
| model_config = ConfigDict(extra="forbid") |
|
|
| scenario_id: str |
| risk_level: str |
| recommended_tool: str |
| arguments: dict = Field(default_factory=dict) |
| rationale: str |
| alternatives: list[str] = Field(default_factory=list) |
|
|
|
|
| class TriageSummary(BaseModel): |
| """Tier 3 triage framing for the current patient state.""" |
|
|
| model_config = ConfigDict(extra="forbid") |
|
|
| scenario_id: str |
| acuity: str |
| headline: str |
| active_alerts: list[str] |
| vitals_snapshot: dict |
| immediate_focus: list[str] |
|
|
|
|
| class DeteriorationExplanation(BaseModel): |
| """Tier 3 explanation of why the patient is worsening or stable.""" |
|
|
| model_config = ConfigDict(extra="forbid") |
|
|
| scenario_id: str |
| status: str |
| cascade_risk: str |
| primary_driver: str |
| supporting_findings: list[str] |
| recommended_response: str |
|
|
|
|
| class InterventionPlan(BaseModel): |
| """Tier 3 short intervention plan based on current state.""" |
|
|
| model_config = ConfigDict(extra="forbid") |
|
|
| scenario_id: str |
| risk_level: str |
| ordered_steps: list[dict] |
| monitoring_targets: list[str] |
| escalation_trigger: str |
|
|
|
|
| class EpisodeReport(BaseModel): |
| """Tier 3 episode-level report for demos and judge summaries.""" |
|
|
| model_config = ConfigDict(extra="forbid") |
|
|
| scenario_id: str |
| policy_name: str |
| total_reward: float |
| outcome: str |
| key_actions: list[str] |
| final_alerts: list[str] |
| summary: str |
|
|
|
|
| def recommend_next_step(observation: PulsePhysiologyObservation) -> NextStepRecommendation: |
| """Recommend the next best tool call from the current observation.""" |
|
|
| alerts = set(observation.active_alerts) |
| available_tools = set(observation.available_tools) |
| risk_level = _risk_level(observation) |
| mean_arterial_pressure = _mean_arterial_pressure(observation) |
|
|
| if ( |
| {"possible_tension_pneumothorax", "unilateral_absent_breath_sounds"} & alerts |
| and "needle_decompression" in available_tools |
| ): |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="needle_decompression", |
| rationale="The current alert pattern suggests tension physiology, so decompression is the highest-yield immediate action.", |
| alternatives=[tool for tool in ("get_respiratory_status", "airway_support", "give_oxygen") if tool in available_tools], |
| ) |
| if "possible_cardiac_tamponade" in alerts and "pericardiocentesis" in available_tools: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="pericardiocentesis", |
| rationale="Possible tamponade physiology is present, so pericardial drainage is the most direct next step.", |
| alternatives=[tool for tool in ("give_fluids", "give_pressor", "check_deterioration") if tool in available_tools], |
| ) |
|
|
| if "blood_loss" in alerts and "control_bleeding" in available_tools: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="control_bleeding", |
| rationale="Active blood loss is present and should be controlled before deterioration progresses.", |
| alternatives=[tool for tool in ("give_fluids", "give_oxygen", "check_deterioration") if tool in available_tools], |
| ) |
| if ( |
| observation.scenario_id == "hemorrhagic_shock" |
| and "tachycardia" in alerts |
| and "give_fluids" in available_tools |
| ): |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="give_fluids", |
| arguments={"volume_ml": 250}, |
| rationale="Persistent tachycardia after initial hemorrhage control suggests the patient may still benefit from additional perfusion support.", |
| alternatives=[tool for tool in ("check_deterioration", "summarize_state", "advance_time") if tool in available_tools], |
| ) |
| if ( |
| mean_arterial_pressure is not None |
| and mean_arterial_pressure < 65 |
| and observation.active_infusions |
| and "give_pressor" in available_tools |
| ): |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="give_pressor", |
| rationale="Perfusion remains low despite active infusions, so vasopressor support is a reasonable escalation.", |
| alternatives=[tool for tool in ("give_fluids", "check_deterioration", "get_blood_gas") if tool in available_tools], |
| ) |
| if "hypotension" in alerts and "give_fluids" in available_tools: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="give_fluids", |
| arguments={"volume_ml": 500}, |
| rationale="Hypotension suggests poor perfusion and fluid resuscitation is the next most direct support.", |
| alternatives=[tool for tool in ("control_bleeding", "position_patient", "check_deterioration") if tool in available_tools], |
| ) |
| if "hypoxemia" in alerts and "give_oxygen" in available_tools: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="give_oxygen", |
| arguments={"flow_lpm": 15}, |
| rationale="Hypoxemia is active and oxygen support is the fastest way to improve oxygenation.", |
| alternatives=[tool for tool in ("airway_support", "position_patient", "check_deterioration") if tool in available_tools], |
| ) |
| if "hypoxemia" in alerts and "get_respiratory_status" in available_tools: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="get_respiratory_status", |
| rationale="A focused respiratory reassessment can clarify whether the next move should be oxygen, airway support, or decompression.", |
| alternatives=[tool for tool in ("give_oxygen", "airway_support", "position_patient") if tool in available_tools], |
| ) |
| if "tachypnea" in alerts and "airway_support" in available_tools: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="airway_support", |
| arguments={"mode": "auto"}, |
| rationale="Respiratory effort remains elevated and airway support may prevent further deterioration.", |
| alternatives=[tool for tool in ("give_oxygen", "position_patient", "check_deterioration") if tool in available_tools], |
| ) |
| for diagnostic_tool in ("get_blood_gas", "get_cbc", "get_bmp"): |
| if diagnostic_tool in available_tools and diagnostic_tool in observation.ready_diagnostics: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool=diagnostic_tool, |
| rationale="A diagnostic result is ready and should be reviewed before the next intervention sequence.", |
| alternatives=[tool for tool in ("summarize_state", "check_deterioration", "get_vitals") if tool in available_tools], |
| ) |
| if "check_deterioration" in available_tools: |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="check_deterioration", |
| rationale="The patient is not in obvious immediate crisis, so reassessment is the safest next step.", |
| alternatives=[tool for tool in ("summarize_state", "advance_time") if tool in available_tools], |
| ) |
| return NextStepRecommendation( |
| scenario_id=observation.scenario_id, |
| risk_level=risk_level, |
| recommended_tool="advance_time", |
| arguments={"seconds": 30}, |
| rationale="No higher-priority intervention is exposed, so advance time to generate the next signal.", |
| alternatives=[], |
| ) |
|
|
|
|
| def build_triage_summary(observation: PulsePhysiologyObservation) -> TriageSummary: |
| """Generate a compact triage summary from the current state.""" |
|
|
| acuity = _risk_level(observation) |
| alerts = list(observation.active_alerts) |
| mental_status = _mental_status_value(observation.mental_status) |
| headline = ( |
| f"{observation.scenario_id}: {acuity.upper()} acuity with " |
| f"HR {observation.heart_rate_bpm:.0f}, " |
| f"BP {observation.systolic_bp_mmhg:.0f}/{observation.diastolic_bp_mmhg:.0f}, " |
| f"SpO2 {_spo2_percent(observation):.1f}%, " |
| f"mental status {mental_status}." |
| ) |
| return TriageSummary( |
| scenario_id=observation.scenario_id, |
| acuity=acuity, |
| headline=headline, |
| active_alerts=alerts, |
| vitals_snapshot={ |
| "heart_rate_bpm": observation.heart_rate_bpm, |
| "systolic_bp_mmhg": observation.systolic_bp_mmhg, |
| "diastolic_bp_mmhg": observation.diastolic_bp_mmhg, |
| "spo2": observation.spo2, |
| "spo2_percent": _spo2_percent(observation), |
| "respiration_rate_bpm": observation.respiration_rate_bpm, |
| "blood_volume_ml": observation.blood_volume_ml, |
| }, |
| immediate_focus=_priority_reasons(observation), |
| ) |
|
|
|
|
| def explain_deterioration( |
| observation: PulsePhysiologyObservation, |
| previous_observation: PulsePhysiologyObservation | None = None, |
| observations: list[PulsePhysiologyObservation] | None = None, |
| ) -> DeteriorationExplanation: |
| """Explain the likely deterioration driver or current stability.""" |
|
|
| recent_observations = _recent_observations( |
| observation, |
| previous_observation, |
| observations, |
| window=3, |
| ) |
| alerts = set(observation.active_alerts) |
| status, status_reason, trend_map = _classify_deterioration_status(observation, recent_observations) |
|
|
| if "blood_loss" in alerts: |
| primary_driver = "hemorrhagic shock physiology" |
| response = "Control bleeding and support perfusion with fluids before reassessing." |
| elif observation.scenario_id == "hemorrhagic_shock" and "tachycardia" in alerts: |
| primary_driver = "residual shock burden after initial resuscitation" |
| response = "Reassess perfusion closely and consider additional volume support if the trend does not settle." |
| elif "hypoxemia" in alerts: |
| primary_driver = "respiratory decompensation" |
| response = "Provide oxygen and airway or positioning support, then reassess oxygenation." |
| elif "tachycardia" in alerts and "hypotension" in alerts: |
| primary_driver = "compensated shock" |
| response = "Support perfusion and reassess for ongoing blood loss or inadequate resuscitation." |
| elif alerts: |
| primary_driver = "ongoing physiological stress" |
| response = "Use focused reassessment and the highest-yield intervention exposed by the current tool set." |
| else: |
| primary_driver = "no active deterioration signal" |
| response = "Continue reassessment and controlled monitoring over time." |
|
|
| supporting_findings = _priority_reasons(observation) |
| supporting_findings.append(status_reason) |
| if trend_map["spo2"] == "worsening": |
| supporting_findings.append("Oxygenation is worsening over the recent observation window.") |
| elif trend_map["spo2"] == "improving": |
| supporting_findings.append("Oxygenation is improving over the recent observation window.") |
|
|
| if trend_map["mean_arterial_pressure_mmhg"] == "worsening": |
| supporting_findings.append("Perfusion is worsening based on the recent MAP trend.") |
| elif trend_map["mean_arterial_pressure_mmhg"] == "improving": |
| supporting_findings.append("Perfusion is improving based on the recent MAP trend.") |
|
|
| if trend_map["heart_rate_bpm"] == "worsening": |
| supporting_findings.append("Heart rate is drifting farther from the safe range.") |
| if trend_map["respiration_rate_bpm"] == "worsening": |
| supporting_findings.append("Respiratory rate is moving in the wrong direction.") |
|
|
| cascade_risk = "low" |
| if status == DeteriorationStatus.DETERIORATING: |
| cascade_risk = "medium" |
| elif status == DeteriorationStatus.CRITICAL: |
| cascade_risk = "imminent" |
|
|
| return DeteriorationExplanation( |
| scenario_id=observation.scenario_id, |
| status=status.value, |
| cascade_risk=cascade_risk, |
| primary_driver=primary_driver, |
| supporting_findings=supporting_findings, |
| recommended_response=response, |
| ) |
|
|
|
|
| def generate_intervention_plan(observation: PulsePhysiologyObservation) -> InterventionPlan: |
| """Create a short ordered plan from the current observation.""" |
|
|
| recommendation = recommend_next_step(observation) |
| steps: list[dict] = [ |
| { |
| "priority": 1, |
| "tool_name": recommendation.recommended_tool, |
| "arguments": recommendation.arguments, |
| "why": recommendation.rationale, |
| } |
| ] |
| priority = 2 |
| for alternative in recommendation.alternatives[:3]: |
| steps.append( |
| { |
| "priority": priority, |
| "tool_name": alternative, |
| "arguments": {}, |
| "why": f"Keep {alternative} ready if the primary step does not adequately stabilize the patient.", |
| } |
| ) |
| priority += 1 |
|
|
| return InterventionPlan( |
| scenario_id=observation.scenario_id, |
| risk_level=recommendation.risk_level, |
| ordered_steps=steps, |
| monitoring_targets=[ |
| "heart_rate_bpm", |
| "systolic_bp_mmhg", |
| "spo2", |
| "respiration_rate_bpm", |
| "active_alerts", |
| ], |
| escalation_trigger="Escalate if alerts increase, mental status worsens, or perfusion/oxygenation declines after intervention.", |
| ) |
|
|
|
|
| def build_episode_report(trace: EpisodeTrace) -> EpisodeReport: |
| """Summarize one episode into a compact Tier 3 report.""" |
|
|
| final_alerts = list(trace.final_observation.active_alerts) |
| if trace.final_observation.done: |
| outcome = "critical deterioration" |
| elif final_alerts: |
| outcome = "partially stabilized" |
| else: |
| outcome = "stabilized" |
|
|
| key_actions: list[str] = [] |
| for step in trace.steps: |
| if step.action.tool_name not in key_actions: |
| key_actions.append(step.action.tool_name) |
|
|
| summary = ( |
| f"{trace.policy_name} completed {trace.num_steps} steps in {trace.scenario_id} " |
| f"with total reward {trace.total_reward:.3f}; outcome: {outcome}." |
| ) |
| return EpisodeReport( |
| scenario_id=trace.scenario_id, |
| policy_name=trace.policy_name, |
| total_reward=trace.total_reward, |
| outcome=outcome, |
| key_actions=key_actions, |
| final_alerts=final_alerts, |
| summary=summary, |
| ) |
|
|