@@ -40,6 +40,14 @@ class StepStatus(Enum):
4040 ROLLED_BACK = "rolled_back"
4141
4242
43+ class TaskExecutionDeferred (RuntimeError ):
44+ """Execution admission was postponed without consuming a task attempt."""
45+
46+ def __init__ (self , reason : str ):
47+ self .reason = str (reason or "execution_admission_deferred" )[:240 ]
48+ super ().__init__ (self .reason )
49+
50+
4351def _persistable_context (context : Any ) -> dict [str , Any ]:
4452 """Plan context reduced to what can survive a restart.
4553
@@ -468,6 +476,19 @@ def _repair_plan_argument_contracts(self, plan: TaskPlan) -> list[str]:
468476 errors .append (f"{ step .step_id or step .tool } :{ step .tool } :{ ',' .join (missing )} " )
469477 return errors
470478
479+ @staticmethod
480+ def _tool_result_deferral_reason (result : Any ) -> str :
481+ if not isinstance (result , dict ):
482+ return ""
483+ if str (result .get ("status" , "" ) or "" ).strip ().lower () != "deferred" :
484+ return ""
485+ return str (
486+ result .get ("reason" )
487+ or result .get ("message" )
488+ or result .get ("error" )
489+ or "execution_admission_deferred"
490+ )[:240 ]
491+
471492 def _persist_active_plans (self ) -> None :
472493 try :
473494 payload = {
@@ -1036,6 +1057,34 @@ async def execute_goal(
10361057 # 2. Execute steps
10371058 await self ._execute_plan (plan , on_progress )
10381059
1060+ if plan .status == "deferred" :
1061+ deferred_reason = str (
1062+ plan .context .get ("execution_deferred_reason" , "" )
1063+ or "execution_admission_deferred"
1064+ )[:240 ]
1065+ await self ._report_progress_event (
1066+ on_progress ,
1067+ {
1068+ "event" : "execution_deferred" ,
1069+ "plan_id" : plan .plan_id ,
1070+ "status" : "deferred" ,
1071+ "reason" : deferred_reason ,
1072+ "steps_completed" : len (plan .succeeded_steps ),
1073+ "steps_total" : len (plan .steps ),
1074+ },
1075+ )
1076+ self ._persist_plan_state (plan )
1077+ return TaskResult (
1078+ plan_id = plan .plan_id ,
1079+ goal = goal ,
1080+ succeeded = False ,
1081+ summary = "Execution remains queued until runtime admission clears." ,
1082+ steps_completed = len (plan .succeeded_steps ),
1083+ steps_total = len (plan .steps ),
1084+ trace_id = trace_id ,
1085+ deferred_reason = deferred_reason ,
1086+ )
1087+
10391088 # 3. Synthesize result
10401089 result = await self ._synthesize_result (plan , time .time () - plan .created_at )
10411090 self ._record_coding_execution (
@@ -2587,6 +2636,8 @@ async def _execute_plan(
25872636 completed_ids .add (step .step_id )
25882637 await self ._emit_step_progress (step , on_progress , plan )
25892638 self ._persist_plan_state (plan )
2639+ if plan .status == "deferred" :
2640+ return
25902641 failed_step = next (
25912642 (step for step in parallel_wave if step .status == StepStatus .FAILED ), None
25922643 )
@@ -2612,6 +2663,9 @@ async def _execute_plan(
26122663 await self ._emit_step_progress (step , on_progress , plan )
26132664 self ._persist_plan_state (plan )
26142665
2666+ if plan .status == "deferred" :
2667+ return
2668+
26152669 if step .status == StepStatus .FAILED :
26162670 await self ._fail_plan (
26172671 plan ,
@@ -2735,6 +2789,10 @@ async def _execute_step_with_retry(self, step: TaskStep, plan: TaskPlan) -> None
27352789 timeout = step_timeout ,
27362790 )
27372791
2792+ deferred_reason = self ._tool_result_deferral_reason (raw_result )
2793+ if deferred_reason :
2794+ raise TaskExecutionDeferred (deferred_reason )
2795+
27382796 step .raw_result = self ._compact_tool_result (raw_result )
27392797 step .result_summary = step .raw_result
27402798
@@ -2846,6 +2904,24 @@ async def _execute_step_with_retry(self, step: TaskStep, plan: TaskPlan) -> None
28462904 )
28472905 self ._persist_plan_state (plan )
28482906
2907+ except TaskExecutionDeferred as exc :
2908+ # Admission pressure and boot grace are scheduling facts, not
2909+ # failed actions. Preserve the exact step and its attempt
2910+ # budget so the plan can resume when the runtime is ready.
2911+ step .attempts = max (0 , step .attempts - 1 )
2912+ step .status = StepStatus .PENDING
2913+ step .error = f"execution deferred: { exc .reason } "
2914+ step .completed_at = None
2915+ plan .status = "deferred"
2916+ plan .context ["execution_deferred_reason" ] = exc .reason
2917+ plan .context ["execution_deferred_at" ] = time .time ()
2918+ self ._persist_plan_state (plan )
2919+ logger .info (
2920+ "TaskEngine: step '%s' deferred without consuming an attempt: %s" ,
2921+ step .description [:40 ],
2922+ exc .reason ,
2923+ )
2924+ return
28492925 except TimeoutError :
28502926 step .error = f"timeout after { step_timeout } s"
28512927 self ._record_coding_execution (
@@ -3006,6 +3082,7 @@ async def _verify_step(self, step: TaskStep, result: Any) -> bool:
30063082 f"Result: { result_str } \n \n "
30073083 "Answer with ONLY 'YES' or 'NO' followed by one sentence of evidence."
30083084 )
3085+ verification_started_at = time .time ()
30093086 raw = await asyncio .wait_for (
30103087 llm .think (
30113088 prompt ,
@@ -3017,6 +3094,12 @@ async def _verify_step(self, step: TaskStep, result: Any) -> bool:
30173094 timeout = 15.0 ,
30183095 )
30193096 if not str (raw or "" ).strip ():
3097+ deferral = take_deferral (
3098+ origin = "autonomous_task_engine" ,
3099+ not_before = verification_started_at ,
3100+ )
3101+ if deferral is not None :
3102+ raise TaskExecutionDeferred (str (deferral .reason or "verifier_deferred" ))
30203103 # Blank verifier output is NOT a pass: an empty verdict is a
30213104 # verifier outage, and treating "nothing came back" as success
30223105 # let unrelated or failure output satisfy arbitrary criteria.
@@ -3033,6 +3116,8 @@ async def _verify_step(self, step: TaskStep, result: Any) -> bool:
30333116 # Cancellation is shutdown/caller intent, never a verification
30343117 # outcome — propagate it rather than converting it into a pass.
30353118 raise
3119+ except TaskExecutionDeferred :
3120+ raise
30363121 except (RuntimeError , TimeoutError , AttributeError ) as e :
30373122 # Verifier exception or timeout: FAIL CLOSED. Assuming pass here
30383123 # let every verifier outage certify arbitrary output as correct.
@@ -3106,7 +3191,13 @@ async def _get_alternative_approach(self, step: TaskStep) -> dict | None:
31063191 "Alternative approach returned identical args for '%s'" , step .description [:40 ]
31073192 )
31083193 except (json .JSONDecodeError , TypeError , ValueError ) as e :
3109- record_degradation ("autonomous_task_engine" , e )
3194+ record_degradation (
3195+ "autonomous_task_engine_retry" ,
3196+ e ,
3197+ severity = "warning" ,
3198+ action = "failed the step after retry synthesis produced no executable alternative" ,
3199+ enforce_failure_policy = False ,
3200+ )
31103201 logger .debug ("Alternative approach generation failed: %s" , e )
31113202
31123203 # Signal exhaustion rather than silently looping identically
0 commit comments