agentshim 0.11.0__tar.gz → 0.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {agentshim-0.11.0 → agentshim-0.12.0}/CHANGELOG.md +23 -0
  2. {agentshim-0.11.0 → agentshim-0.12.0}/PKG-INFO +6 -3
  3. {agentshim-0.11.0 → agentshim-0.12.0}/README.md +5 -2
  4. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/__init__.py +1 -1
  5. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/errors.py +4 -0
  6. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/failures.py +17 -3
  7. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/parser.py +30 -11
  8. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/scripted.py +35 -10
  9. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/scripted.py +13 -3
  10. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/testing/__init__.py +5 -2
  11. {agentshim-0.11.0 → agentshim-0.12.0}/pyproject.toml +1 -1
  12. {agentshim-0.11.0 → agentshim-0.12.0}/.gitignore +0 -0
  13. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/agent.py +0 -0
  14. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/__init__.py +0 -0
  15. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/_files.py +0 -0
  16. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/env.py +0 -0
  17. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/events.py +0 -0
  18. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/mcp.py +0 -0
  19. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/pricing.py +0 -0
  20. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/profile.py +0 -0
  21. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/provider.py +0 -0
  22. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/schema.py +0 -0
  23. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/skills.py +0 -0
  24. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/stream.py +0 -0
  25. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/turn.py +0 -0
  26. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/core/usage.py +0 -0
  27. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/execution/__init__.py +0 -0
  28. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/execution/executor.py +0 -0
  29. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/execution/host.py +0 -0
  30. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/execution/transform.py +0 -0
  31. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/__init__.py +0 -0
  32. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/__init__.py +0 -0
  33. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/events.py +0 -0
  34. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
  35. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  36. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/provider.py +0 -0
  37. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/sandbox.py +0 -0
  38. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/claude/user_hooks.py +0 -0
  39. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/__init__.py +0 -0
  40. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/_toml.py +0 -0
  41. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/events.py +0 -0
  42. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/failures.py +0 -0
  43. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/mcp_scope.py +0 -0
  44. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/parser.py +0 -0
  45. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/provider.py +0 -0
  46. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/rules.py +0 -0
  47. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/sandbox.py +0 -0
  48. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/codex/skills.py +0 -0
  49. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/copilot/__init__.py +0 -0
  50. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/copilot/events.py +0 -0
  51. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/copilot/parser.py +0 -0
  52. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/copilot/provider.py +0 -0
  53. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/copilot/scripted.py +0 -0
  54. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/gemini/__init__.py +0 -0
  55. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/gemini/events.py +0 -0
  56. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/gemini/parser.py +0 -0
  57. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/gemini/provider.py +0 -0
  58. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/gemini/scripted.py +0 -0
  59. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/opencode/__init__.py +0 -0
  60. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/opencode/events.py +0 -0
  61. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/opencode/parser.py +0 -0
  62. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/opencode/provider.py +0 -0
  63. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/providers/opencode/scripted.py +0 -0
  64. {agentshim-0.11.0 → agentshim-0.12.0}/agentshim/py.typed +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 (2026-10-03)
4
+
5
+ A turn whose output never matched its schema says so. Additive except that
6
+ Claude's `error_max_structured_output_retries` is no longer `OTHER`.
7
+
8
+ ### Added
9
+
10
+ - `FailureKind.SCHEMA`: the provider gave up producing output that matches the
11
+ requested schema. Claude Code reports it for the
12
+ `error_max_structured_output_retries` result, with the validation errors of
13
+ the last rejected `StructuredOutput` call on `CliExitError.detail`. A resumed
14
+ turn that fails this way keeps its conversation. Codex never reports it:
15
+ `--output-schema` constrains decoding, so its output always parses against
16
+ the schema.
17
+ - `scripted_failure("claude", FailureKind.SCHEMA)`.
18
+
19
+ ### Changed
20
+
21
+ - `scripted_failure(provider, FailureKind.OTHER)` scripts a failure that is
22
+ not about the schema (Claude `error_during_execution`, Codex running out of
23
+ context). `scripted_failure("codex", FailureKind.SCHEMA)` raises
24
+ `ValueError`.
25
+
3
26
  ## 0.11.0 (2026-10-03)
4
27
 
5
28
  A failed turn says why it failed. Additive except that a classified failure of
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: tomli>=2; python_full_version < '3.11'
@@ -200,8 +200,11 @@ AgentShimError
200
200
  `CliExitError.kind` says why a turn failed, so a caller can pick a policy
201
201
  without knowing how any CLI words its errors: `TRANSIENT` (an overload, rate
202
202
  limit or server error that waiting may outlast), `USAGE_LIMIT` (a quota or
203
- billing limit), `AUTH` (credentials), or `OTHER`. Claude Code and Codex
204
- classify their failures; the other providers report `OTHER`.
203
+ billing limit), `AUTH` (credentials), `SCHEMA` (the provider gave up producing
204
+ output that matches the requested schema; `.detail` holds the last validation
205
+ errors and the conversation survives, so a correction can follow), or `OTHER`.
206
+ Claude Code and Codex classify their failures; the other providers report
207
+ `OTHER`. Codex never reports `SCHEMA`: it constrains decoding to the schema.
205
208
  `agentshim.testing.scripted_failure(provider, kind)` scripts each one.
206
209
 
207
210
  ## Adding a provider
@@ -190,8 +190,11 @@ AgentShimError
190
190
  `CliExitError.kind` says why a turn failed, so a caller can pick a policy
191
191
  without knowing how any CLI words its errors: `TRANSIENT` (an overload, rate
192
192
  limit or server error that waiting may outlast), `USAGE_LIMIT` (a quota or
193
- billing limit), `AUTH` (credentials), or `OTHER`. Claude Code and Codex
194
- classify their failures; the other providers report `OTHER`.
193
+ billing limit), `AUTH` (credentials), `SCHEMA` (the provider gave up producing
194
+ output that matches the requested schema; `.detail` holds the last validation
195
+ errors and the conversation survives, so a correction can follow), or `OTHER`.
196
+ Claude Code and Codex classify their failures; the other providers report
197
+ `OTHER`. Codex never reports `SCHEMA`: it constrains decoding to the schema.
195
198
  `agentshim.testing.scripted_failure(provider, kind)` scripts each one.
196
199
 
197
200
  ## Adding a provider
@@ -99,7 +99,7 @@ from .providers.copilot import CopilotProvider
99
99
  from .providers.gemini import GeminiProvider
100
100
  from .providers.opencode import OpencodeProvider
101
101
 
102
- __version__ = "0.11.0"
102
+ __version__ = "0.12.0"
103
103
 
104
104
  __all__ = [
105
105
  "AgentEvent",
@@ -29,6 +29,10 @@ class FailureKind(str, Enum):
29
29
  USAGE_LIMIT = "usage_limit"
30
30
  #: Missing, expired, or rejected credentials.
31
31
  AUTH = "auth"
32
+ #: The provider gave up producing output that matches the requested output
33
+ #: schema. ``detail`` carries the last validation errors it reported, and
34
+ #: the conversation survives: a correction can be sent as the next turn.
35
+ SCHEMA = "schema"
32
36
  #: Anything else, including every failure a provider cannot classify.
33
37
  OTHER = "other"
34
38
 
@@ -4,7 +4,9 @@ Claude reports a failed API request three ways, newest first: an ``error``
4
4
  kind on the synthetic assistant message it writes for the failure, an
5
5
  ``api_error_status`` on the final ``result`` frame, and the text
6
6
  ``API Error: <status> <body>`` in that frame's ``result``. Older builds only
7
- have the text, so all three are read.
7
+ have the text, so all three are read. A turn whose ``StructuredOutput`` calls
8
+ never validated ends with the ``result`` subtype
9
+ ``error_max_structured_output_retries`` instead.
8
10
  """
9
11
 
10
12
  from __future__ import annotations
@@ -39,17 +41,29 @@ _AUTH_STATUSES = frozenset({401, 403})
39
41
  #: A request timeout or a rate limit; every 5xx (529 is an overload) is transient too.
40
42
  _TRANSIENT_STATUSES = frozenset({408, 429})
41
43
  _SERVER_ERRORS = range(500, 600)
44
+ #: The ``result`` subtype of a turn that used up its output-schema retries.
45
+ SCHEMA_RETRIES_SUBTYPE = "error_max_structured_output_retries"
42
46
 
43
47
 
44
- def classify_failure(*, api_error: str | None, status: int | None, text: str) -> FailureKind:
48
+ def classify_failure(
49
+ *, subtype: str | None, api_error: str | None, status: int | None, text: str
50
+ ) -> FailureKind:
45
51
  """Classify one failed turn from what Claude reported about it.
46
52
 
47
- ``api_error`` is the assistant frame's ``error`` kind, ``status`` the
53
+ ``subtype`` is the error ``result`` frame's subtype, ``api_error`` the
54
+ assistant frame's ``error`` kind, ``status`` the
48
55
  result frame's ``api_error_status``, and ``text`` the error text the
49
56
  stream (or, when the stream had none, stderr) carried. Tool output is
50
57
  never part of ``text``, so a tool that printed an API error cannot make
51
58
  the turn look like one.
52
59
  """
60
+ if subtype == SCHEMA_RETRIES_SUBTYPE:
61
+ return FailureKind.SCHEMA
62
+ return _classify_api_failure(api_error=api_error, status=status, text=text)
63
+
64
+
65
+ def _classify_api_failure(*, api_error: str | None, status: int | None, text: str) -> FailureKind:
66
+ """Classify a failure from the API error Claude reported, if it reported one."""
53
67
  if api_error in _AUTH_KINDS:
54
68
  return FailureKind.AUTH
55
69
  if api_error in _USAGE_LIMIT_KINDS:
@@ -32,7 +32,7 @@ from .events import (
32
32
  ToolResultBlock,
33
33
  parse_frame,
34
34
  )
35
- from .failures import classify_failure
35
+ from .failures import SCHEMA_RETRIES_SUBTYPE, classify_failure
36
36
 
37
37
  if TYPE_CHECKING:
38
38
  from collections.abc import Callable, Mapping
@@ -45,6 +45,10 @@ PROVIDER_NAME = "claude"
45
45
  #: The built-in tool through which Claude Code loads a skill.
46
46
  SKILL_TOOL = "Skill"
47
47
 
48
+ #: The tool through which Claude Code submits a schema-constrained answer;
49
+ #: its error result is the validation failure of one submission.
50
+ STRUCTURED_OUTPUT_TOOL = "StructuredOutput"
51
+
48
52
  #: The built-in tool through which Claude Code reads a file.
49
53
  READ_TOOL = "Read"
50
54
 
@@ -142,6 +146,9 @@ class ClaudeStreamParser:
142
146
  self._api_error: str | None = None
143
147
  self._api_error_text: str | None = None
144
148
  self._api_error_status: int | None = None
149
+ self._subtype: str | None = None
150
+ # The validation errors of the last rejected StructuredOutput call.
151
+ self._schema_errors: str | None = None
145
152
  self._stderr: list[str] = []
146
153
 
147
154
  def feed_stdout(self, line: str) -> None:
@@ -189,7 +196,12 @@ class ClaudeStreamParser:
189
196
  """
190
197
  reported = [text for text in (self._error, self._api_error_text) if text]
191
198
  text = "\n".join(reported) if reported else "\n".join(self._stderr)
192
- return classify_failure(api_error=self._api_error, status=self._api_error_status, text=text)
199
+ return classify_failure(
200
+ subtype=self._subtype,
201
+ api_error=self._api_error,
202
+ status=self._api_error_status,
203
+ text=text,
204
+ )
193
205
 
194
206
  def _handle(self, frame: object) -> None:
195
207
  if isinstance(frame, SystemInit):
@@ -238,6 +250,8 @@ class ClaudeStreamParser:
238
250
 
239
251
  def _tool_result(self, frame: ToolResultBlock) -> None:
240
252
  name = self._tools.name(frame.tool_id)
253
+ if name == STRUCTURED_OUTPUT_TOOL and frame.is_error:
254
+ self._schema_errors = frame.output
241
255
  self._emit(
242
256
  ToolResult(
243
257
  tool_id=frame.tool_id,
@@ -262,7 +276,8 @@ class ClaudeStreamParser:
262
276
  self._emit(UsageReport(self._usage, frame.total_cost_usd))
263
277
  if frame.is_error:
264
278
  self._api_error_status = frame.api_error_status
265
- self._error = _error_text(frame)
279
+ self._subtype = frame.subtype
280
+ self._error = self._error_text(frame)
266
281
  self._emit(ProviderError(self._error))
267
282
 
268
283
  def _structured_payload(self, frame: ResultFrame) -> object | None:
@@ -281,12 +296,16 @@ class ClaudeStreamParser:
281
296
  except (json.JSONDecodeError, ValueError):
282
297
  return None
283
298
 
299
+ def _error_text(self, frame: ResultFrame) -> str:
300
+ """Say what an error ``result`` frame reported.
284
301
 
285
- def _error_text(frame: ResultFrame) -> str:
286
- """Say what an error ``result`` frame reported.
287
-
288
- An ``error_*`` subtype carries its messages in ``errors`` and often no
289
- ``result`` text at all, so both are read before falling back to the
290
- subtype, which is the only part that names some failures.
291
- """
292
- return frame.text or "; ".join(frame.errors) or frame.subtype or "claude reported an error"
302
+ A turn that used up its schema retries is described by the validation
303
+ errors of its last rejected submission, which only the
304
+ ``StructuredOutput`` tool result carries. Otherwise an ``error_*``
305
+ subtype carries its messages in ``errors`` and often no ``result``
306
+ text at all, so both are read before falling back to the subtype,
307
+ which is the only part that names some failures.
308
+ """
309
+ if frame.subtype == SCHEMA_RETRIES_SUBTYPE and self._schema_errors:
310
+ return self._schema_errors
311
+ return frame.text or "; ".join(frame.errors) or frame.subtype or "claude reported an error"
@@ -120,9 +120,7 @@ def resume_failure_lines(*, session_id: str | None = None) -> tuple[list[str], l
120
120
 
121
121
 
122
122
  #: What Claude writes for a failed request of each kind: the assistant
123
- #: ``error`` kind, the HTTP status, and the message. ``OTHER`` is a turn that
124
- #: ran but could not satisfy its output schema, the subtype Claude reports
125
- #: when no StructuredOutput call validated.
123
+ #: ``error`` kind, the HTTP status, and the message.
126
124
  _FAILURES: dict[FailureKind, tuple[str, int, str]] = {
127
125
  FailureKind.TRANSIENT: (
128
126
  "overloaded",
@@ -133,6 +131,10 @@ _FAILURES: dict[FailureKind, tuple[str, int, str]] = {
133
131
  FailureKind.AUTH: ("authentication_failed", 401, "Invalid API key \u00b7 Please run /login"),
134
132
  }
135
133
 
134
+ #: The validation errors Claude's ``StructuredOutput`` tool returns for a
135
+ #: submission that does not match the schema.
136
+ SCHEMA_ERRORS = "Output does not match required schema: root: must have required property 'answer'"
137
+
136
138
 
137
139
  def failure_lines(
138
140
  kind: FailureKind, *, session_id: str | None = None
@@ -140,8 +142,10 @@ def failure_lines(
140
142
  """Build the stdout, stderr and exit code of a turn that fails with *kind*.
141
143
 
142
144
  An API failure is the synthetic assistant message Claude writes for it,
143
- then an error ``result`` frame with the HTTP status. ``OTHER`` is an
144
- ``error_max_structured_output_retries`` result, which names no API error.
145
+ then an error ``result`` frame with the HTTP status. ``SCHEMA`` is a
146
+ ``StructuredOutput`` call the tool rejects, then an
147
+ ``error_max_structured_output_retries`` result. ``OTHER`` is an
148
+ ``error_during_execution`` result, which names no API error.
145
149
 
146
150
  Args:
147
151
  kind: The classification the scripted failure must produce.
@@ -156,12 +160,13 @@ def failure_lines(
156
160
  result: dict[str, Any] = {"type": "result", "is_error": True, "num_turns": 1}
157
161
  if session_id is not None:
158
162
  result["session_id"] = session_id
159
- failure = _FAILURES.get(kind)
160
- if failure is None:
161
- result["subtype"] = "error_max_structured_output_retries"
162
- result["errors"] = ["no StructuredOutput call produced a valid output"]
163
+ if kind is FailureKind.SCHEMA:
164
+ lines.extend(_rejected_structured_output())
165
+ result.update(subtype="error_max_structured_output_retries", errors=[])
166
+ elif kind is FailureKind.OTHER:
167
+ result.update(subtype="error_during_execution", errors=["the turn stopped unexpectedly"])
163
168
  else:
164
- error, status, text = failure
169
+ error, status, text = _FAILURES[kind]
165
170
  message = {"role": "assistant", "content": [{"type": "text", "text": text}]}
166
171
  lines.append(_line({"type": "assistant", "message": message, "error": error}))
167
172
  result.update(subtype="success", result=text, api_error_status=status)
@@ -169,6 +174,26 @@ def failure_lines(
169
174
  return lines, [], 1
170
175
 
171
176
 
177
+ def _rejected_structured_output() -> list[str]:
178
+ """One ``StructuredOutput`` call and the validation error it got back."""
179
+ call: dict[str, Any] = {
180
+ "type": "tool_use",
181
+ "id": "toolu_schema",
182
+ "name": "StructuredOutput",
183
+ "input": {},
184
+ }
185
+ rejection = {
186
+ "type": "tool_result",
187
+ "tool_use_id": "toolu_schema",
188
+ "content": SCHEMA_ERRORS,
189
+ "is_error": True,
190
+ }
191
+ return [
192
+ _line({"type": "assistant", "message": {"role": "assistant", "content": [call]}}),
193
+ _line({"type": "user", "message": {"role": "user", "content": [rejection]}}),
194
+ ]
195
+
196
+
172
197
  def _usage_payload(usage: TokenUsage | None) -> dict[str, Any]:
173
198
  if usage is None:
174
199
  return {
@@ -130,17 +130,25 @@ _FAILURES: dict[FailureKind, str] = {
130
130
  "(https://chatgpt.com/explore/plus), or try again in 2 hours."
131
131
  ),
132
132
  FailureKind.AUTH: "unexpected status 401 Unauthorized: Missing bearer authentication",
133
- FailureKind.OTHER: "model response did not match the output schema",
133
+ FailureKind.OTHER: "Codex ran out of room in the model's context window.",
134
134
  }
135
135
 
136
136
 
137
+ #: Codex sends ``--output-schema`` to the model as a strict response format,
138
+ #: so decoding is constrained to the schema and a turn never fails over it.
139
+ _NO_SCHEMA_FAILURE = (
140
+ "codex never fails a turn over its output schema: decoding is constrained to the schema"
141
+ )
142
+
143
+
137
144
  def failure_lines(
138
145
  kind: FailureKind, *, session_id: str | None = None
139
146
  ) -> tuple[list[str], list[str], int]:
140
147
  """Build the stdout, stderr and exit code of a turn that fails with *kind*.
141
148
 
142
149
  Codex reports the failure as an ``error`` event followed by
143
- ``turn.failed``, both carrying the same message.
150
+ ``turn.failed``, both carrying the same message. ``SCHEMA`` raises
151
+ ``ValueError``: Codex has no such failure.
144
152
 
145
153
  Args:
146
154
  kind: The classification the scripted failure must produce.
@@ -149,7 +157,9 @@ def failure_lines(
149
157
  Returns:
150
158
  ``(stdout, stderr, returncode)`` for a ``FakeRun``.
151
159
  """
152
- message = _FAILURES[kind]
160
+ message = _FAILURES.get(kind)
161
+ if message is None:
162
+ raise ValueError(_NO_SCHEMA_FAILURE)
153
163
  lines: list[str] = []
154
164
  if session_id is not None:
155
165
  lines.append(_line({"type": "thread.started", "thread_id": session_id}))
@@ -222,14 +222,17 @@ def scripted_failure(provider: str, kind: FailureKind, *, session_id: str | None
222
222
  """Build a ``FakeRun`` for a turn that fails and classifies as *kind*.
223
223
 
224
224
  The run is *provider*'s real report of such a failure (an overload, a
225
- usage limit, a rejected login, or an unclassifiable failure), so the
225
+ usage limit, a rejected login, output that never matched the requested
226
+ schema, or an unclassifiable failure), so the
226
227
  raised ``CliExitError`` carries ``kind`` through the real parser. A
227
228
  consumer can therefore test its retry policy without knowing how any
228
229
  CLI phrases a failure.
229
230
 
230
231
  Args:
231
232
  provider: A provider whose stream reports failure kinds (``claude``
232
- or ``codex``); any other raises ``ValueError``.
233
+ or ``codex``); any other raises ``ValueError``, as does
234
+ ``FailureKind.SCHEMA`` on ``codex``, whose decoding is
235
+ constrained to the schema.
233
236
  kind: The ``FailureKind`` the raised error must carry.
234
237
  session_id: The conversation the failed run names, if any.
235
238
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentshim"
3
- version = "0.11.0"
3
+ version = "0.12.0"
4
4
  description = "Provider-agnostic coding-agent CLI shims"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes