Skip to content

Commit 46facbc

Browse files
authored
T5-P1-A3-WP1 — Prevent Orchestrator From Improvising Recovery on Degraded Tool Responses (#4067)
## Summary Add an explicit catch-all halt rule to both the cook-session orchestrator prompt (`_prompts_orchestrator.py`) and the fleet food-truck prompt (`_prompts.py`) that declares any tool response with `success:false` AND absent/null/empty `content` as a degraded state. The orchestrator must print `user_visible_message`, refuse to improvise recovery, and halt. The fleet prompt must emit the sentinel with `reason="degraded_tool_response"` and stop. The canonical sentinel contract in `_type_dispatch_identity.py` must also include the new reason value. Both prompt builders get corresponding contract tests. ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260611-081716-639257/.autoskillit/temp/make-plan/t5_p1_a3_wp1_degraded_tool_response_halt_plan_2026-06-11_081900.md` Closes #4000 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit <!-- autoskillit:pipeline-signature steps=prepare_pr,run_arch_lenses,compose_pr,annotate_pr_diff,review_pr --> ## Token Usage Summary | Step | Model | count | uncached | output | cache_read | peak_ctx | turns | cache_write | time | |------|-------|-------|----------|--------|------------|----------|-------|-------------|------| | plan* | opus[1m] | 1 | 1.8k | 13.5k | 1.1M | 87.2k | 41 | 70.3k | 9m 9s | | verify* | sonnet | 1 | 108 | 8.8k | 849.3k | 84.4k | 42 | 63.6k | 2m 59s | | implement* | MiniMax-M3 | 1 | 2.1M | 7.6k | 0 | 0 | 59 | 0 | 4m 29s | | audit_impl* | sonnet | 1 | 882 | 7.5k | 207.1k | 43.4k | 16 | 31.2k | 3m 36s | | prepare_pr* | MiniMax-M3 | 1 | 200.4k | 1.8k | 0 | 0 | 13 | 0 | 49s | | compose_pr* | MiniMax-M3 | 1 | 211.8k | 1.3k | 0 | 0 | 13 | 0 | 40s | | **Total** | | | 2.5M | 40.5k | 2.1M | 87.2k | | 165.1k | 21m 43s | \* *Step used a non-Anthropic provider; caching behavior may differ.* ## Token Efficiency | Step | LoC Changed | cache_read/LoC | cache_write/LoC | output/LoC | |------|-------------|----------------|-----------------|------------| | plan | 0 | — | — | — | | verify | 0 | — | — | — | | implement | 116 | 0.0 | 0.0 | 65.8 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **116** | 18525.6 | 1423.5 | 349.3 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 1.8k | 13.5k | 1.1M | 70.3k | 9m 9s | | sonnet | 2 | 990 | 16.4k | 1.1M | 94.8k | 6m 35s | | MiniMax-M3 | 3 | 2.5M | 10.7k | 0 | 0 | 5m 58s |
1 parent 954dbad commit 46facbc

5 files changed

Lines changed: 114 additions & 2 deletions

File tree

src/autoskillit/cli/_prompts_orchestrator.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ def _build_orchestrator_prompt(
163163
2. DO NOT call AskUserQuestion.
164164
3. End the session with a final text response.
165165
166+
FAILURE PREDICATE — DEGRADED TOOL RESPONSE:
167+
If ANY tool response contains `"success": false` AND the `content` field is
168+
absent, null, or an empty string — this is a degraded state.
169+
1. Extract and print the value of "user_visible_message" from the
170+
response (fall back to the raw response text if the field is missing).
171+
2. DO NOT call AskUserQuestion.
172+
3. Do not improvise a recovery path. Do not route to on_failure.
173+
Do not retry the tool without explicit instructions from the recipe step.
174+
4. End the session with a final text response.
175+
166176
CONTEXT LIMIT ROUTING — run_skill only (check BEFORE on_failure):
167177
- When run_skill returns "success: False" AND "needs_retry: true" AND "retry_reason: resume":
168178
- Check "subtype" to discriminate the termination cause:

src/autoskillit/core/types/_type_dispatch_identity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def _build_sentinel_contract(dispatch_id: str, short: str) -> str:
3232
Fields:
3333
- success: true if all mandatory steps completed without unresolved failures
3434
- reason: "completed", "failed", "fleet_quota_exhausted", "timeout",
35-
"open_kitchen_failed", "missing_on_failure"
35+
"open_kitchen_failed", "missing_on_failure", "degraded_tool_response"
3636
- summary: One-line description of what happened
3737
3838
The sentinel markers ---l3-result::{dispatch_id}--- and ---end-l3-result::{dispatch_id}---

src/autoskillit/fleet/_prompts.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ def _build_food_truck_prompt(
206206
1. Emit the sentinel block with success=false and reason="open_kitchen_failed".
207207
2. End the session.
208208
209+
FAILURE PREDICATE — DEGRADED RESPONSE:
210+
If ANY tool response contains success=false AND the content field is
211+
absent, null, or an empty string — this is a degraded state.
212+
1. Emit the sentinel block with success=false and reason="degraded_tool_response".
213+
2. Stop. Do not improvise a recovery path, do not route to on_failure,
214+
and do not retry the tool.
215+
209216
TWO FAILURE TIERS FOR PREDICATE-FORMAT STEPS:
210217
- Tool-level failure (run_skill returns "success: False"): Follow on_failure. This fires
211218
BEFORE any result object exists. on_result conditions are NOT evaluated.
@@ -340,7 +347,7 @@ def _build_food_truck_prompt(
340347
Fields:
341348
- success: true if all mandatory steps completed without unresolved failures
342349
- reason: "completed", "failed", "quota_exhausted", "timeout",
343-
"open_kitchen_failed", "missing_on_failure"
350+
"open_kitchen_failed", "missing_on_failure", "degraded_tool_response"
344351
- summary: One-line description of what happened{extra_fields_docs}
345352
346353
The sentinel markers ---l3-result::{dispatch_id}--- and ---end-l3-result::{dispatch_id}---

tests/cli/test_orchestrator_prompt_contract.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,47 @@ def test_open_kitchen_predicate_uses_json_field_not_substring(self):
5151
assert "ingredients_table" in section
5252

5353

54+
class TestDegradedToolResponsePredicate:
55+
"""Guards for the FAILURE PREDICATE — DEGRADED TOOL RESPONSE block."""
56+
57+
def _get_section(self) -> str:
58+
prompt = _get_prompt()
59+
start = prompt.index("FAILURE PREDICATE — DEGRADED TOOL RESPONSE")
60+
end = prompt.index("CONTEXT LIMIT ROUTING", start)
61+
return prompt[start:end]
62+
63+
def test_prompt_contains_degraded_tool_response_section(self):
64+
prompt = _get_prompt()
65+
ok_idx = prompt.index("FAILURE PREDICATE — open_kitchen")
66+
deg_idx = prompt.index("FAILURE PREDICATE — DEGRADED TOOL RESPONSE")
67+
ctx_idx = prompt.index("CONTEXT LIMIT ROUTING")
68+
assert ok_idx < deg_idx < ctx_idx, (
69+
"Degraded predicate must appear after open_kitchen and before CONTEXT LIMIT ROUTING"
70+
)
71+
72+
def test_degraded_predicate_covers_success_false_empty_content(self):
73+
section = self._get_section()
74+
assert "success" in section
75+
assert "false" in section
76+
assert any(word in section for word in ("absent", "null", "empty")), (
77+
"Section must reference absent, null, or empty content"
78+
)
79+
80+
def test_degraded_predicate_prohibits_improvisation(self):
81+
section = self._get_section()
82+
lower = section.lower()
83+
assert any(
84+
phrase in lower
85+
for phrase in (
86+
"do not improvise",
87+
"do not retry",
88+
"do not attempt",
89+
"never",
90+
"halt",
91+
)
92+
), "Section must prohibit improvisation or retry"
93+
94+
5495
class TestStep0ToolPredicateCoverage:
5596
"""Every tool referenced in STEP 0 must have a failure predicate or shared rule."""
5697

tests/fleet/test_food_truck_prompt.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,57 @@ def test_food_truck_open_kitchen_predicate_uses_json_field_not_substring():
174174
section = prompt[idx : idx + 500]
175175
assert "--- INGREDIENTS TABLE ---" not in section
176176
assert "ingredients_table" in section
177+
178+
179+
def test_food_truck_prompt_has_degraded_response_halt_rule():
180+
prompt = _build_food_truck_prompt(
181+
recipe="test-recipe",
182+
task="Test task",
183+
ingredients={},
184+
mcp_prefix=DIRECT_PREFIX,
185+
dispatch_id="test-dispatch",
186+
campaign_id="test-campaign",
187+
l3_timeout_sec=300,
188+
)
189+
assert "FAILURE PREDICATE — DEGRADED RESPONSE" in prompt
190+
assert 'reason="degraded_tool_response"' in prompt
191+
192+
193+
def test_food_truck_prompt_degraded_rule_triggers_sentinel_on_empty_content():
194+
prompt = _build_food_truck_prompt(
195+
recipe="test-recipe",
196+
task="Test task",
197+
ingredients={},
198+
mcp_prefix=DIRECT_PREFIX,
199+
dispatch_id="test-dispatch",
200+
campaign_id="test-campaign",
201+
l3_timeout_sec=300,
202+
)
203+
start = prompt.index("FAILURE PREDICATE — DEGRADED RESPONSE")
204+
end = prompt.index("TWO FAILURE TIERS", start)
205+
section = prompt[start:end]
206+
assert "success" in section
207+
assert "false" in section
208+
assert any(word in section for word in ("absent", "null", "empty")), (
209+
"Rule must reference absent, null, or empty content"
210+
)
211+
212+
213+
def test_food_truck_prompt_degraded_reason_in_reason_enum():
214+
prompt = _build_food_truck_prompt(
215+
recipe="test-recipe",
216+
task="Test task",
217+
ingredients={},
218+
mcp_prefix=DIRECT_PREFIX,
219+
dispatch_id="test-dispatch",
220+
campaign_id="test-campaign",
221+
l3_timeout_sec=300,
222+
)
223+
fields_idx = prompt.index("Fields:", prompt.index("--- SECTION 8:"))
224+
reason_region = prompt[fields_idx : fields_idx + 300]
225+
assert "degraded_tool_response" in reason_region, (
226+
"degraded_tool_response must appear in Section 8 reason enum"
227+
)
228+
ok_idx = prompt.index("FAILURE PREDICATE — open_kitchen")
229+
deg_idx = prompt.index("FAILURE PREDICATE — DEGRADED RESPONSE")
230+
assert ok_idx != deg_idx, "Degraded and open_kitchen predicates must be distinct blocks"

0 commit comments

Comments
 (0)