From cdef6be96a6864eefcdf0485db33409cfb95f0f9 Mon Sep 17 00:00:00 2001 From: toki Date: Mon, 10 Aug 2026 02:57:29 +0900 Subject: [PATCH] =?UTF-8?q?fix(agent-ops):=20Dispatcher=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EC=99=80=20verdict=20=ED=8C=90=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/dispatch.py | 14 +++++++++++--- .../tests/test_dispatch.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) 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 b8babc81..6b761598 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 @@ -101,11 +101,13 @@ VERDICT_SCHEMA_MATCHERS = tuple( ( re.compile(rf"^##\s*{re.escape(heading)}[ \t]*$", re.MULTILINE), re.compile( - rf"^(?:-\s*)?(?:\*\*)?{re.escape(label)}(?:\*\*)?\s*:\s*(PASS|WARN|FAIL)[ \t]*$", + rf"^(?:-\s*)?(?:\*\*)?{re.escape(label)}(?:\*\*)?\s*:\s*" + rf"(?:(PASS|WARN|FAIL)|`(PASS|WARN|FAIL)`)[ \t]*$", re.MULTILINE, ), re.compile( - rf"^###\s+{re.escape(label)}[ \t]*$\s*^(?:\*\*)?(PASS|WARN|FAIL)(?:\*\*)?[ \t]*$", + rf"^###\s+{re.escape(label)}[ \t]*$\s*^(?:\*\*)?" + rf"(?:(PASS|WARN|FAIL)|`(PASS|WARN|FAIL)`)(?:\*\*)?[ \t]*$", re.MULTILINE, ), ) @@ -203,6 +205,7 @@ RUNTIME_FAILURE_PATTERNS = { "model-unavailable": [ r"model.{0,40}(?:not found|unavailable)", r"overloaded", r"temporarily unavailable", + r"reasoning[_ ]effort must be one of", ], "provider-connection": [ r"\bprovider[_ -]?tunnel[_ -]?error\b", @@ -4349,7 +4352,12 @@ def verdict_from_text(text: str) -> str | None: inline_matches = list(line_re.finditer(section)) block_matches = list(block_re.finditer(section)) matches = inline_matches + block_matches - return matches[0].group(1) if len(matches) == 1 else None + if len(matches) != 1: + return None + return next( + (value for value in matches[0].groups() if value in {"PASS", "WARN", "FAIL"}), + None, + ) def matching_archive_directories_by_name( 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 fe5f141b..3116f091 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 @@ -456,6 +456,22 @@ class RuntimeCatalogDispatcherTests(unittest.TestCase): class GenericDispatcherContractTests(unittest.TestCase): + def test_verdict_parser_accepts_inline_code_value(self): + text = "## Code Review Result\n\n- Overall Verdict: `FAIL`\n" + + self.assertEqual(dispatch.verdict_from_text(text), "FAIL") + + def test_invalid_reasoning_effort_is_target_unavailable(self): + output = ( + '{"type":"error","error":{"data":{"message":' + '"reasoning_effort must be one of none, low, medium, or high"}}}' + ) + + failure, evidence = dispatch.classify_failure_with_evidence(output) + + self.assertEqual(failure, "model-unavailable") + self.assertIn("reasoning_effort", evidence or "") + def test_selfcheck_work_log_uses_worker_plan_artifact(self): with TemporaryDirectory() as tmp: directory = Path(tmp) / "agent-task" / "group" / "01_task"