diff --git a/agent-ops/.version b/agent-ops/.version index afdf04e..56fd1c0 100644 --- a/agent-ops/.version +++ b/agent-ops/.version @@ -1 +1 @@ -1.1.199 +1.1.200 diff --git a/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md b/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md index e6978c6..0587305 100644 --- a/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md +++ b/agent-ops/skills/common/orchestrate-agent-task-loop/SKILL.md @@ -111,6 +111,9 @@ Accept self-check completion only when `## Implementation Checklist` or its supp - Record the target id, opaque agent/model identity, execution class, runtime contract, catalog evidence, process identity, workspace identity, timestamps, result, and exact failure evidence. - Treat stderr as terminal diagnostic evidence. For JSONL, recognize generic terminal event fields such as error/fatal type or severity, rejected/failed status with an error code, explicit error flags, and a non-retrying `agent_end` whose last assistant message ends with `error` or `aborted`. - Determine liveness from PID/start-token/process-marker evidence and actual stream or native-session progress. Heartbeat mtime is never agent progress. For Codex JSONL, an unmatched `item.started` `command_execution` is an active tool interval: suspend the model-response silence timer until its matching `item.completed`, then restore normal stall detection. +- The dispatcher model-silence safety net is 70 seconds. Downstream provider runtimes should emit their bounded terminal before that deadline; do not extend the dispatcher budget per target to cover nested retries. +- Treat a confirmed provider transport terminal as the end of the current dispatch. Do not resume or automatically resend the same native session; an operator may start a fresh dispatch after the provider/runtime state is corrected. +- Retry `session-stall` only with a fresh native conversation. Preserve workspace changes and logical locator evidence, but do not carry the silent conversation context into the next attempt or a restarted dispatcher. - Never start a duplicate attempt while owned live evidence remains. - Keep a 10-consecutive-failure budget per task stage. Reset only that stage's budget after success. - Preserve failed attempt logs. Delete successful attempt logs only after verified archive completion and no live evidence. diff --git a/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py b/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py index 46cb33e..8477be1 100644 --- a/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py +++ b/agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py @@ -179,7 +179,7 @@ def validated_max_parallel(value: int) -> int: STREAM_HEARTBEAT_SECONDS = 30 -MODEL_RESPONSE_STALL_SECONDS = 3 * 60 +MODEL_RESPONSE_STALL_SECONDS = 70 RECOVERY_FAILURE_LIMIT = 10 GENERIC_FAILURE_LIMIT_PER_TARGET = 3 SELF_CHECK_UNCHECKED_RETRY_LIMIT = 10 @@ -209,6 +209,7 @@ RUNTIME_FAILURE_PATTERNS = { ], "provider-connection": [ r"\bprovider[_ -]?tunnel[_ -]?error\b", + r"no provider supports the required output validation capability", ( r"(?:provider|backend|inference (?:server|endpoint))" r".{0,160}(?:connection refused|dial tcp)" @@ -3252,7 +3253,7 @@ def native_resume_locator( if ( not isinstance(record.get("runtime"), dict) or not record["runtime"].get("native_session_monitor") - or record.get("failure_class") not in {"context-limit", "session-stall"} + or record.get("failure_class") != "context-limit" or record.get("status") != "failed" ): return None @@ -3819,7 +3820,7 @@ async def invoke( record["native_pending_tool_call_ids"] = list( native_state.pending_tool_call_ids ) - record["native_stall_timeout_seconds"] = None + record["native_stall_timeout_seconds"] = MODEL_RESPONSE_STALL_SECONDS record.setdefault("native_activity_state", "starting") if ( spec.native_resume @@ -4645,6 +4646,37 @@ async def run_escalating( ], ) return False, locator + if failure in PROVIDER_TRANSPORT_FAILURES: + reason = ( + f"{role} provider transport failure requires a fresh dispatch" + ) + selected = ( + current_decision.get("selected") + if isinstance(current_decision, dict) + else None + ) + store.update_task( + task, + blocked=f"{reason} locator={locator}", + blocker_evidence={ + "role": role, + "failure_class": failure, + "locator": str(locator) if locator else None, + "selected": selected, + "work_unit_id": current_decision.get("work_unit_id") + if isinstance(current_decision, dict) + else None, + }, + ) + banner( + "작업차단", + task.name, + [ + "reason=provider-transport-terminal", + *failure_report_lines(failure, locator), + ], + ) + return False, locator if spec.native_resume: if failure in {"context-limit", "session-stall"}: native_recovery_retries += 1 @@ -4658,7 +4690,7 @@ async def run_escalating( ], ) previous_locator = locator - native_resume_locator = locator + native_resume_locator = locator if failure == "context-limit" else None await asyncio.sleep(min(30, 2 ** min(native_recovery_retries, 5))) continue native_recovery_retries += 1 @@ -5309,7 +5341,11 @@ async def run_worker( resume_locator: Path | None = None, ) -> None: retry_context = store.task_state(task).get("retry_failover_context") - if resume_locator is None and isinstance(retry_context, dict): + if ( + resume_locator is None + and isinstance(retry_context, dict) + and retry_context.get("failure_class") not in PROVIDER_TRANSPORT_FAILURES + ): locator_value = retry_context.get("locator") if isinstance(locator_value, str) and locator_value: resume_locator = Path(locator_value) diff --git a/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py b/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py index 559870e..21c86cb 100644 --- a/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py +++ b/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py @@ -393,6 +393,13 @@ class RuntimeCatalogDispatcherTests(unittest.TestCase): self.assertEqual(failure, "provider-quota") self.assertIsNotNone(evidence) + def test_output_validation_capability_rejection_is_provider_terminal(self): + failure, evidence = dispatch.classify_failure_with_evidence( + "no provider supports the required output validation capability" + ) + self.assertEqual(failure, "provider-connection") + self.assertIsNotNone(evidence) + def test_generic_json_terminal_diagnostic_has_no_agent_branch(self): diagnostic = dispatch.terminal_diagnostic( "opaque-agent",