Skip to content

Commit 898e8fe

Browse files
Trecekclaude
andauthored
Implementation Plan: T5-P5-A5-WP1 Define the Typed Failure Envelope and Factory Helpers (#4106)
## Summary Add `ToolFailureEnvelope` TypedDict with a `retriable` discriminator and two factory helpers (`server_failure_envelope`, `input_failure_envelope`) to `src/autoskillit/server/tools/_types.py`. This provides a typed contract for structured failure responses that downstream P5-A4 WPs will consume, enabling orchestrator routing based on retriability. The new type is intentionally distinct from the existing `_kitchen_failure_envelope` function in `tools_kitchen.py` — that function returns a raw JSON string with a `kitchen` field; this TypedDict provides a typed dict contract with a `retriable` discriminator. ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260613-032734-695911/.autoskillit/temp/make-plan/t5-p5-a5-wp1-define-typed-failure-envelope_plan_2026-06-13_033600.md` Closes #4038 🤖 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 | 49 | 7.9k | 748.5k | 74.9k | 26 | 75.6k | 9m 7s | | verify* | sonnet | 1 | 52 | 8.2k | 217.3k | 49.0k | 17 | 28.1k | 3m 1s | | implement* | MiniMax-M3 | 1 | 1.3M | 5.1k | 0 | 0 | 43 | 0 | 2m 31s | | audit_impl* | sonnet | 1 | 44 | 6.2k | 166.0k | 41.6k | 11 | 26.7k | 3m 26s | | prepare_pr* | MiniMax-M3 | 1 | 150.3k | 1.7k | 0 | 0 | 10 | 0 | 37s | | compose_pr* | MiniMax-M3 | 1 | 211.8k | 1.2k | 0 | 0 | 11 | 0 | 41s | | **Total** | | | 1.6M | 30.4k | 1.1M | 74.9k | | 130.5k | 19m 26s | \* *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 | 132 | 0.0 | 0.0 | 38.9 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **132** | 8574.3 | 988.7 | 230.0 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 49 | 7.9k | 748.5k | 75.6k | 9m 7s | | sonnet | 2 | 96 | 14.5k | 383.3k | 54.9k | 6m 28s | | MiniMax-M3 | 3 | 1.6M | 8.0k | 0 | 0 | 3m 50s | --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 4d95c66 commit 898e8fe

4 files changed

Lines changed: 129 additions & 3 deletions

File tree

src/autoskillit/server/tools/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ MCP `@mcp.tool()` handlers registered on import (20 tool modules).
99
| `__init__.py` | Docstring-only — tools register via `@mcp.tool()` on import |
1010
| `_auto_overrides.py` | Shared `_build_auto_overrides()` factory for server-authoritative ingredient injection |
1111
| `_cancellation_shield.py` | `_cancellation_shield` decorator — catches `asyncio.CancelledError` at MCP tool boundary, returns structured JSON |
12-
| `_types.py` | TypedDict definitions for server tool JSON responses (RunSkillResult, RunCmdResult, etc.) |
12+
| `_types.py` | TypedDict definitions for server tool JSON responses (RunSkillResult, RunCmdResult, ToolFailureEnvelope, etc.) and failure envelope factory helpers |
1313
| `tools_kitchen.py` | `open_kitchen`, `close_kitchen` (gate lifecycle), `recipe://` MCP resource |
1414
| `tools_config.py` | `configure_fleet`, `configure_order` (session config overlay) |
1515
| `tools_agents.py` | `unlock_agent_pack` tool + `agent://` resource templates |

src/autoskillit/server/tools/_types.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from __future__ import annotations
99

10-
from typing import Any, TypedDict
10+
from typing import Any, Literal, TypedDict
1111

1212
from autoskillit.core import ModelTotalEntry, RetryReason
1313

@@ -20,6 +20,9 @@
2020
"TimingSummaryResult",
2121
"KitchenStatusResult",
2222
"DispatchEnvelopeResult",
23+
"ToolFailureEnvelope",
24+
"server_failure_envelope",
25+
"input_failure_envelope",
2326
]
2427

2528

@@ -190,3 +193,53 @@ class DispatchEnvelopeResult(TypedDict, total=False):
190193
error: str
191194
user_visible_message: str
192195
details: dict[str, Any] | None
196+
197+
198+
class _ToolFailureEnvelopeRequired(TypedDict):
199+
"""Required fields for the structured failure envelope."""
200+
201+
success: Literal[False]
202+
error: str
203+
stage: str
204+
retriable: bool
205+
206+
207+
class ToolFailureEnvelope(_ToolFailureEnvelopeRequired, total=False):
208+
"""Typed failure envelope with retriable discriminator for orchestrator routing.
209+
210+
Distinct from ``_kitchen_failure_envelope`` in tools_kitchen.py which returns
211+
a raw JSON string with a ``kitchen`` field. This TypedDict provides a typed
212+
dict contract with a ``retriable`` discriminator for P5-A4 orchestrator routing.
213+
"""
214+
215+
user_visible_message: str
216+
217+
218+
def server_failure_envelope(
219+
exc: BaseException,
220+
stage: str,
221+
) -> ToolFailureEnvelope:
222+
"""Build a failure envelope for server/infrastructure errors (retriable=True)."""
223+
return ToolFailureEnvelope(
224+
success=False,
225+
error=f"{type(exc).__name__}: {exc}",
226+
stage=stage,
227+
retriable=True,
228+
user_visible_message=(
229+
f"Server error during {stage}: {type(exc).__name__}. "
230+
"This may be transient — the orchestrator may retry."
231+
),
232+
)
233+
234+
235+
def input_failure_envelope(
236+
message: str,
237+
stage: str,
238+
) -> ToolFailureEnvelope:
239+
"""Build a failure envelope for input/validation errors (retriable=False)."""
240+
return ToolFailureEnvelope(
241+
success=False,
242+
error=message,
243+
stage=stage,
244+
retriable=False,
245+
)

tests/server/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Server tool handler unit tests — kitchen, execution, CI, clone, workspace tool
131131
| `test_tools_status_mcp_response.py` | Tests for MCP response tracking integration in tools_status handlers |
132132
| `test_tools_status_quota_and_db.py` | Tests for server status tools: quota events, telemetry writing, and DB access |
133133
| `test_tools_status_summaries.py` | Tests for server status tools: token and timing summaries |
134-
| `test_tools_types_module.py` | Tests for server/tools/_types.py TypedDict imports |
134+
| `test_tools_types_module.py` | Tests for server/tools/_types.py TypedDict imports and failure envelope factory functions |
135135
| `test_tools_workspace.py` | Tests for autoskillit server workspace tools |
136136
| `test_tools_agents.py` | Tests for agent pack registry, MCP resources, and `unlock_agent_pack` |
137137
| `test_track_response_size.py` | Tests for the track_response_size decorator in autoskillit.server._notify |

tests/server/test_tools_types_module.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,76 @@ def test_model_total_entry_still_in_core(self):
6565
from autoskillit.core.types._type_results import ModelTotalEntry
6666

6767
assert "model" in ModelTotalEntry.__required_keys__
68+
69+
70+
class TestToolFailureEnvelope:
71+
"""Verify ToolFailureEnvelope TypedDict and factory helpers."""
72+
73+
def test_tool_failure_envelope_importable(self):
74+
from autoskillit.server.tools._types import ToolFailureEnvelope
75+
76+
assert "success" in ToolFailureEnvelope.__required_keys__
77+
78+
def test_tool_failure_envelope_required_keys(self):
79+
from autoskillit.server.tools._types import ToolFailureEnvelope
80+
81+
expected = {"success", "error", "stage", "retriable"}
82+
assert expected <= ToolFailureEnvelope.__required_keys__
83+
84+
def test_tool_failure_envelope_optional_keys(self):
85+
from autoskillit.server.tools._types import ToolFailureEnvelope
86+
87+
assert "user_visible_message" in ToolFailureEnvelope.__optional_keys__
88+
89+
def test_tool_failure_envelope_success_type(self):
90+
import typing
91+
92+
from autoskillit.server.tools._types import ToolFailureEnvelope
93+
94+
hints = typing.get_type_hints(ToolFailureEnvelope)
95+
assert typing.get_args(hints["success"]) == (False,)
96+
97+
def test_server_failure_envelope_factory(self):
98+
from autoskillit.server.tools._types import server_failure_envelope
99+
100+
result = server_failure_envelope(ValueError("boom"), "init")
101+
assert result["success"] is False
102+
assert result["retriable"] is True
103+
assert result["stage"] == "init"
104+
assert "ValueError" in result["error"]
105+
assert "boom" in result["error"]
106+
107+
def test_input_failure_envelope_factory(self):
108+
from autoskillit.server.tools._types import input_failure_envelope
109+
110+
result = input_failure_envelope("bad input", "validate")
111+
assert result["success"] is False
112+
assert result["retriable"] is False
113+
assert result["stage"] == "validate"
114+
assert result["error"] == "bad input"
115+
116+
def test_server_failure_envelope_user_visible_message(self):
117+
from autoskillit.server.tools._types import server_failure_envelope
118+
119+
result = server_failure_envelope(RuntimeError("oops"), "startup")
120+
assert "user_visible_message" in result
121+
assert len(result["user_visible_message"]) > 0
122+
123+
def test_tool_failure_envelope_in_all(self):
124+
from autoskillit.server.tools._types import __all__ as types_all
125+
126+
assert "ToolFailureEnvelope" in types_all
127+
assert "server_failure_envelope" in types_all
128+
assert "input_failure_envelope" in types_all
129+
130+
def test_tool_failure_envelope_distinct_from_kitchen_envelope(self):
131+
from autoskillit.server.tools._types import ToolFailureEnvelope
132+
from autoskillit.server.tools.tools_kitchen import (
133+
_kitchen_failure_envelope,
134+
)
135+
136+
assert "retriable" in ToolFailureEnvelope.__required_keys__
137+
all_keys = ToolFailureEnvelope.__required_keys__ | ToolFailureEnvelope.__optional_keys__
138+
assert "kitchen" not in all_keys
139+
result = _kitchen_failure_envelope(ValueError("x"), "test")
140+
assert isinstance(result, str)

0 commit comments

Comments
 (0)