deep-worker 0.0.19__py3-none-any.whl

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 (252) hide show
  1. LICENSE +22 -0
  2. deep_worker/__init__.py +9 -0
  3. deep_worker/providers/__init__.py +13 -0
  4. deep_worker/providers/claude_code_provider/__init__.py +80 -0
  5. deep_worker/providers/claude_code_provider/api_error_detect.py +270 -0
  6. deep_worker/providers/claude_code_provider/chat_model.py +1787 -0
  7. deep_worker/providers/claude_code_provider/client_manager.py +843 -0
  8. deep_worker/providers/claude_code_provider/custom_types.py +104 -0
  9. deep_worker/providers/claude_code_provider/delivery_resolve_middleware.py +136 -0
  10. deep_worker/providers/claude_code_provider/orphaned_tool_calls.py +140 -0
  11. deep_worker/providers/claude_code_provider/proxy_mcp_server.py +838 -0
  12. deep_worker/providers/claude_code_provider/py.typed +0 -0
  13. deep_worker/providers/claude_code_provider/session_import.py +255 -0
  14. deep_worker/providers/claude_code_provider/session_restore.py +103 -0
  15. deep_worker/providers/claude_code_provider/session_state.py +114 -0
  16. deep_worker/providers/claude_code_provider/session_synthesis.py +495 -0
  17. deep_worker/providers/claude_code_provider/session_tail_match.py +112 -0
  18. deep_worker/providers/claude_code_provider/web_search/__init__.py +49 -0
  19. deep_worker/providers/claude_code_provider/web_search/web_search_delegation.py +502 -0
  20. deep_worker/providers/claude_code_provider/web_search/web_search_hooks.py +227 -0
  21. deep_worker/providers/codex_provider/__init__.py +21 -0
  22. deep_worker/providers/codex_provider/chat_model.py +1023 -0
  23. deep_worker/providers/codex_provider/client_state.py +132 -0
  24. deep_worker/providers/codex_provider/custom_types.py +119 -0
  25. deep_worker/providers/codex_provider/event_pump.py +250 -0
  26. deep_worker/providers/codex_provider/exec_runner.py +769 -0
  27. deep_worker/providers/codex_provider/native_tools.py +206 -0
  28. deep_worker/providers/codex_provider/proxy_mcp_server.py +565 -0
  29. deep_worker/providers/codex_provider/py.typed +0 -0
  30. deep_worker/providers/codex_provider/session_state.py +118 -0
  31. deep_worker/providers/deepworker_a2a/__init__.py +56 -0
  32. deep_worker/providers/deepworker_a2a/bridge.py +93 -0
  33. deep_worker/providers/deepworker_a2a/card.py +128 -0
  34. deep_worker/providers/deepworker_a2a/client.py +253 -0
  35. deep_worker/providers/deepworker_a2a/config.py +121 -0
  36. deep_worker/providers/deepworker_a2a/executor.py +142 -0
  37. deep_worker/providers/deepworker_a2a/py.typed +0 -0
  38. deep_worker/providers/deepworker_a2a/server.py +157 -0
  39. deep_worker/providers/deepworker_a2a/tools.py +120 -0
  40. deep_worker/providers/grok_provider/__init__.py +27 -0
  41. deep_worker/providers/grok_provider/acp_client.py +558 -0
  42. deep_worker/providers/grok_provider/acp_types.py +144 -0
  43. deep_worker/providers/grok_provider/arg_compat.py +153 -0
  44. deep_worker/providers/grok_provider/builtin_tool_passthrough.py +270 -0
  45. deep_worker/providers/grok_provider/chat_model.py +1523 -0
  46. deep_worker/providers/grok_provider/client_state.py +149 -0
  47. deep_worker/providers/grok_provider/custom_types.py +202 -0
  48. deep_worker/providers/grok_provider/limit_errors.py +68 -0
  49. deep_worker/providers/grok_provider/proxy_mcp_server.py +626 -0
  50. deep_worker/providers/grok_provider/py.typed +0 -0
  51. deep_worker/providers/grok_provider/session_state.py +139 -0
  52. deep_worker/providers/semantic_search/__init__.py +99 -0
  53. deep_worker/providers/semantic_search/_tokenizer.py +97 -0
  54. deep_worker/providers/semantic_search/config.py +170 -0
  55. deep_worker/providers/semantic_search/exceptions.py +11 -0
  56. deep_worker/providers/semantic_search/history_indexer.py +797 -0
  57. deep_worker/providers/semantic_search/history_retriever.py +221 -0
  58. deep_worker/providers/semantic_search/indexer.py +623 -0
  59. deep_worker/providers/semantic_search/long_text_embedding.py +446 -0
  60. deep_worker/providers/semantic_search/message_embedding_store.py +646 -0
  61. deep_worker/providers/semantic_search/message_search.py +557 -0
  62. deep_worker/providers/semantic_search/py.typed +0 -0
  63. deep_worker/providers/semantic_search/retriever.py +305 -0
  64. deep_worker/providers/semantic_search/scanner.py +180 -0
  65. deep_worker/providers/semantic_search/search_cli.py +258 -0
  66. deep_worker-0.0.19.data/data/deep_worker_cli/default_agent_prompt.md +163 -0
  67. deep_worker-0.0.19.dist-info/METADATA +326 -0
  68. deep_worker-0.0.19.dist-info/RECORD +252 -0
  69. deep_worker-0.0.19.dist-info/WHEEL +4 -0
  70. deep_worker-0.0.19.dist-info/entry_points.txt +4 -0
  71. deep_worker-0.0.19.dist-info/licenses/LICENSE +22 -0
  72. deep_worker_cli/__init__.py +7 -0
  73. deep_worker_cli/__main__.py +6 -0
  74. deep_worker_cli/_version.py +3 -0
  75. deep_worker_cli/agent.py +1370 -0
  76. deep_worker_cli/agent_config_pkg/__init__.py +5 -0
  77. deep_worker_cli/agent_config_pkg/agent_config.py +208 -0
  78. deep_worker_cli/app.py +1503 -0
  79. deep_worker_cli/app.tcss +181 -0
  80. deep_worker_cli/backends.py +1196 -0
  81. deep_worker_cli/basesession/__init__.py +25 -0
  82. deep_worker_cli/basesession/detector.py +75 -0
  83. deep_worker_cli/basesession/protocol_types.py +81 -0
  84. deep_worker_cli/basesession/session_adapter.py +85 -0
  85. deep_worker_cli/built_in_skills/__init__.py +5 -0
  86. deep_worker_cli/built_in_skills/skill-creator/SKILL.md +399 -0
  87. deep_worker_cli/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  88. deep_worker_cli/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  89. deep_worker_cli/cli/__init__.py +3 -0
  90. deep_worker_cli/cli/agent_command.py +3430 -0
  91. deep_worker_cli/cli/client_command.py +674 -0
  92. deep_worker_cli/cli/cwd.py +44 -0
  93. deep_worker_cli/cli/interactive.py +1683 -0
  94. deep_worker_cli/cli/interactive.tcss +33 -0
  95. deep_worker_cli/cli/lsp_command.py +510 -0
  96. deep_worker_cli/cli/mcp_command.py +233 -0
  97. deep_worker_cli/cli/router.py +36 -0
  98. deep_worker_cli/client/__init__.py +43 -0
  99. deep_worker_cli/client/agent_client.py +546 -0
  100. deep_worker_cli/clipboard.py +125 -0
  101. deep_worker_cli/config.py +2011 -0
  102. deep_worker_cli/constants.py +4 -0
  103. deep_worker_cli/deepseek_model.py +93 -0
  104. deep_worker_cli/default_agent_prompt.md +163 -0
  105. deep_worker_cli/delta_checkpointer.py +1018 -0
  106. deep_worker_cli/exceptions.py +297 -0
  107. deep_worker_cli/external_mcp/__init__.py +0 -0
  108. deep_worker_cli/external_mcp/adapter.py +276 -0
  109. deep_worker_cli/external_mcp/config.py +235 -0
  110. deep_worker_cli/external_mcp/interrupt.py +51 -0
  111. deep_worker_cli/external_mcp/mangler.py +132 -0
  112. deep_worker_cli/external_mcp/pool.py +226 -0
  113. deep_worker_cli/file_ops.py +516 -0
  114. deep_worker_cli/fork_agent.py +246 -0
  115. deep_worker_cli/history_access_tool.py +265 -0
  116. deep_worker_cli/history_memory_tool.py +178 -0
  117. deep_worker_cli/image_utils.py +259 -0
  118. deep_worker_cli/input.py +141 -0
  119. deep_worker_cli/integrations/__init__.py +1 -0
  120. deep_worker_cli/integrations/daytona.py +224 -0
  121. deep_worker_cli/integrations/langsmith.py +296 -0
  122. deep_worker_cli/integrations/modal.py +230 -0
  123. deep_worker_cli/integrations/runloop.py +230 -0
  124. deep_worker_cli/integrations/sandbox_factory.py +182 -0
  125. deep_worker_cli/local_context.py +693 -0
  126. deep_worker_cli/lsp/__init__.py +14 -0
  127. deep_worker_cli/lsp/assets/kotlin-android-source-sets.init.gradle +163 -0
  128. deep_worker_cli/lsp/bridge.py +100 -0
  129. deep_worker_cli/lsp/bridge_loop.py +220 -0
  130. deep_worker_cli/lsp/bridge_stdio.py +174 -0
  131. deep_worker_cli/lsp/enable.py +143 -0
  132. deep_worker_cli/lsp/gradle_init.py +178 -0
  133. deep_worker_cli/lsp/languages.py +480 -0
  134. deep_worker_cli/lsp/migrate.py +111 -0
  135. deep_worker_cli/lsp/policy_guard.py +301 -0
  136. deep_worker_cli/lsp/prompt.py +93 -0
  137. deep_worker_cli/lsp/resources/__init__.py +15 -0
  138. deep_worker_cli/lsp/resources/deep_worker_serena_mode.yml +23 -0
  139. deep_worker_cli/lsp/serena_config.py +107 -0
  140. deep_worker_cli/lsp/service/README.md +63 -0
  141. deep_worker_cli/lsp/service/__init__.py +0 -0
  142. deep_worker_cli/lsp/service/__main__.py +35 -0
  143. deep_worker_cli/lsp/service/change_detector.py +368 -0
  144. deep_worker_cli/lsp/service/grpc_server.py +1117 -0
  145. deep_worker_cli/lsp/service/proto/__init__.py +0 -0
  146. deep_worker_cli/lsp/service/proto/lsp_service.proto +215 -0
  147. deep_worker_cli/lsp/service/proto/lsp_service_pb2.py +104 -0
  148. deep_worker_cli/lsp/service/proto/lsp_service_pb2.pyi +277 -0
  149. deep_worker_cli/lsp/service/proto/lsp_service_pb2_grpc.py +707 -0
  150. deep_worker_cli/lsp/service/registry.py +253 -0
  151. deep_worker_cli/lsp/service/serena_backend.py +1084 -0
  152. deep_worker_cli/lsp/service/serena_supervisor.py +323 -0
  153. deep_worker_cli/lsp/service/service_cli.py +374 -0
  154. deep_worker_cli/lsp/service/supervisor.py +470 -0
  155. deep_worker_cli/lsp/service/watcher.py +235 -0
  156. deep_worker_cli/main.py +670 -0
  157. deep_worker_cli/middleware/__init__.py +31 -0
  158. deep_worker_cli/middleware/dangling_tool_calls.py +144 -0
  159. deep_worker_cli/middleware/history_access.py +51 -0
  160. deep_worker_cli/middleware/history_memory.py +190 -0
  161. deep_worker_cli/middleware/inter_agent_reply.py +379 -0
  162. deep_worker_cli/middleware/safe_tool_call.py +121 -0
  163. deep_worker_cli/middleware/send_message_completion.py +91 -0
  164. deep_worker_cli/middleware/tool_completion.py +261 -0
  165. deep_worker_cli/model_predicate.py +125 -0
  166. deep_worker_cli/non_interactive.py +676 -0
  167. deep_worker_cli/process/__init__.py +30 -0
  168. deep_worker_cli/process/agent_launcher.py +59 -0
  169. deep_worker_cli/process/agent_process.py +4845 -0
  170. deep_worker_cli/process/agent_stop.py +125 -0
  171. deep_worker_cli/process/env_loader.py +114 -0
  172. deep_worker_cli/process/inbox_spool.py +304 -0
  173. deep_worker_cli/process/log_rotation.py +308 -0
  174. deep_worker_cli/process/message_dict_cache.py +184 -0
  175. deep_worker_cli/process/registry.py +473 -0
  176. deep_worker_cli/process/status_callback.py +121 -0
  177. deep_worker_cli/process/stream_processor.py +355 -0
  178. deep_worker_cli/project_utils.py +57 -0
  179. deep_worker_cli/prompts/__init__.py +13 -0
  180. deep_worker_cli/prompts/proxy_tool_hints.py +135 -0
  181. deep_worker_cli/provider_paths.py +555 -0
  182. deep_worker_cli/py.typed +0 -0
  183. deep_worker_cli/rpyc_service/__init__.py +5 -0
  184. deep_worker_cli/rpyc_service/agent_service.py +1595 -0
  185. deep_worker_cli/runtime/__init__.py +1 -0
  186. deep_worker_cli/runtime/availability_tracker.py +333 -0
  187. deep_worker_cli/runtime/provider_chain.py +10 -0
  188. deep_worker_cli/session/__init__.py +5 -0
  189. deep_worker_cli/session/session_manager.py +317 -0
  190. deep_worker_cli/sessions.py +1591 -0
  191. deep_worker_cli/skills/__init__.py +18 -0
  192. deep_worker_cli/skills/commands.py +708 -0
  193. deep_worker_cli/skills/load.py +194 -0
  194. deep_worker_cli/stats/__init__.py +6 -0
  195. deep_worker_cli/stats/callback.py +215 -0
  196. deep_worker_cli/stats/token_stats.py +956 -0
  197. deep_worker_cli/storage_paths.py +347 -0
  198. deep_worker_cli/subagents.py +173 -0
  199. deep_worker_cli/summaries.py +361 -0
  200. deep_worker_cli/task_manager/__init__.py +11 -0
  201. deep_worker_cli/task_manager/middleware.py +150 -0
  202. deep_worker_cli/task_manager/prompts.py +293 -0
  203. deep_worker_cli/task_manager/task_manager.py +627 -0
  204. deep_worker_cli/textual_adapter.py +880 -0
  205. deep_worker_cli/tools.py +1025 -0
  206. deep_worker_cli/types.py +110 -0
  207. deep_worker_cli/ui.py +498 -0
  208. deep_worker_cli/utils.py +89 -0
  209. deep_worker_cli/widgets/__init__.py +27 -0
  210. deep_worker_cli/widgets/approval.py +331 -0
  211. deep_worker_cli/widgets/autocomplete.py +628 -0
  212. deep_worker_cli/widgets/chat_input.py +778 -0
  213. deep_worker_cli/widgets/diff.py +216 -0
  214. deep_worker_cli/widgets/history.py +155 -0
  215. deep_worker_cli/widgets/loading.py +173 -0
  216. deep_worker_cli/widgets/message_store.py +580 -0
  217. deep_worker_cli/widgets/messages.py +1210 -0
  218. deep_worker_cli/widgets/status.py +255 -0
  219. deep_worker_cli/widgets/tool_renderers.py +128 -0
  220. deep_worker_cli/widgets/tool_widgets.py +245 -0
  221. deep_worker_cli/widgets/welcome.py +117 -0
  222. deepagents/__init__.py +17 -0
  223. deepagents/_version.py +3 -0
  224. deepagents/backends/__init__.py +23 -0
  225. deepagents/backends/composite.py +664 -0
  226. deepagents/backends/filesystem.py +668 -0
  227. deepagents/backends/local_shell.py +305 -0
  228. deepagents/backends/protocol.py +473 -0
  229. deepagents/backends/sandbox.py +802 -0
  230. deepagents/backends/state.py +236 -0
  231. deepagents/backends/store.py +629 -0
  232. deepagents/backends/utils.py +484 -0
  233. deepagents/graph.py +421 -0
  234. deepagents/middleware/__init__.py +26 -0
  235. deepagents/middleware/_utils.py +25 -0
  236. deepagents/middleware/bounded_tool_call.py +375 -0
  237. deepagents/middleware/filesystem.py +1617 -0
  238. deepagents/middleware/memory.py +413 -0
  239. deepagents/middleware/message_timestamps.py +240 -0
  240. deepagents/middleware/parked_job_registry.py +154 -0
  241. deepagents/middleware/patch_tool_calls.py +96 -0
  242. deepagents/middleware/project_memory.py +158 -0
  243. deepagents/middleware/skills.py +829 -0
  244. deepagents/middleware/subagents.py +839 -0
  245. deepagents/middleware/summarization.py +1206 -0
  246. deepagents/middleware/summarization_history.py +120 -0
  247. deepagents/middleware/tool_dispatch_bridge.py +50 -0
  248. deepagents/py.typed +0 -0
  249. deepagents/tools/__init__.py +5 -0
  250. deepagents/tools/project_memory.py +160 -0
  251. deepagents/utils/__init__.py +5 -0
  252. deepagents/utils/project_index.py +37 -0
LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) LangChain, Inc.
4
+ Copyright (c) 2026 Roman Rodionov
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ """deep_worker — root package for the deep-worker project.
2
+
3
+ This package is the canonical import parent for the vendored providers:
4
+
5
+ deep_worker.providers.<pkg>
6
+
7
+ It intentionally imports nothing heavy at package import time. The
8
+ providers tree is resolved lazily (see ``deep_worker_cli/provider_paths.py``).
9
+ """
@@ -0,0 +1,13 @@
1
+ """deep_worker.providers — vendored provider packages.
2
+
3
+ Canonical import names for the five in-tree providers:
4
+
5
+ - ``deep_worker.providers.claude_code_provider``
6
+ - ``deep_worker.providers.codex_provider``
7
+ - ``deep_worker.providers.grok_provider``
8
+ - ``deep_worker.providers.semantic_search``
9
+ - ``deep_worker.providers.deepworker_a2a``
10
+
11
+ This package imports nothing at import time: each provider root is lazy
12
+ (PEP 562) so importing the tree never pulls optional/private deps.
13
+ """
@@ -0,0 +1,80 @@
1
+ """Claude Code Provider for deep-worker.
2
+
3
+ This package provides a LangChain-compatible AI provider that uses Claude Code
4
+ (via ClaudeSDKClient) as the backend, enabling deep-worker to leverage Claude Code's
5
+ pricing model while maintaining full transparency at the LangGraph level.
6
+
7
+ Key components:
8
+ - ClaudeCodeChatModel: LangChain BaseChatModel wrapper around ClaudeSDKClient
9
+ - ProxyMCPServer: In-process MCP server that delegates tool execution to deep-worker
10
+ - ClientManager: Manages ClaudeSDKClient instances for different thread_ids
11
+ - WebSearchDelegationTool: Universal web search tool that works with any LLM provider
12
+
13
+ Usage:
14
+ from deep_worker.providers.claude_code_provider import (
15
+ ClaudeCodeChatModel, WebSearchDelegationTool,
16
+ )
17
+
18
+ # Create the model
19
+ model = ClaudeCodeChatModel(base_options=...)
20
+
21
+ # Bind tools (including web search)
22
+ web_search = WebSearchDelegationTool()
23
+ model = model.bind_tools([web_search, ...other_tools])
24
+
25
+ # Use with LangGraph
26
+ response = await model.ainvoke(messages, config={"configurable": {"thread_id": "session-1"}})
27
+
28
+ LAZY SURFACE: importing this root package
29
+ never imports the heavy submodules (``chat_model``, ``client_manager``,
30
+ ``proxy_mcp_server``, ``web_search``) and therefore never pulls
31
+ ``claude_agent_sdk``. Submodules are loaded lazily on first attribute access
32
+ via PEP 562 ``__getattr__``. Capability classification happens at the call
33
+ sites (``deep_worker_cli.provider_paths.ensure_provider_importable``); a
34
+ missing SDK surfaces as a classified error, not an eager import crash.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import importlib as _importlib
39
+ from typing import TYPE_CHECKING
40
+
41
+ if TYPE_CHECKING:
42
+ from .chat_model import ClaudeCodeChatModel as ClaudeCodeChatModel
43
+ from .client_manager import ClientManager as ClientManager
44
+ from .proxy_mcp_server import ProxyMCPServer as ProxyMCPServer
45
+ from .web_search import WebSearchDelegationTool as WebSearchDelegationTool
46
+
47
+ # name → submodule (relative to this package). Kept as a plain mapping so the
48
+ # lazy ``__getattr__`` below stays import-free and side-effect-free. Keep in
49
+ # sync with ``__all__`` below (pyright requires ``__all__`` to be a literal).
50
+ _LAZY = {
51
+ "ClaudeCodeChatModel": "chat_model",
52
+ "ClientManager": "client_manager",
53
+ "ProxyMCPServer": "proxy_mcp_server",
54
+ "WebSearchDelegationTool": "web_search",
55
+ }
56
+
57
+ __all__ = [
58
+ "ClaudeCodeChatModel",
59
+ "ClientManager",
60
+ "ProxyMCPServer",
61
+ "WebSearchDelegationTool",
62
+ ]
63
+
64
+ __version__ = "0.1.0"
65
+
66
+
67
+ def __getattr__(name: str):
68
+ sub = _LAZY.get(name)
69
+ if sub is None:
70
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
71
+ module = _importlib.import_module(f".{sub}", __name__)
72
+ value = getattr(module, name)
73
+ # Cache on the package so repeated access does not re-import.
74
+ globals()[name] = value
75
+ return value
76
+
77
+
78
+ def __dir__() -> list[str]:
79
+ return sorted(set(globals()) | set(__all__))
80
+
@@ -0,0 +1,270 @@
1
+ """Detect Claude CLI turns that carry an upstream API error instead of an answer.
2
+
3
+ Contract source: docs/DESIGN_FIX_API_ERROR_AS_FINAL_ANSWER.md §3.2 / §5.1.
4
+
5
+ The detector is pure, side-effect free, and NEVER raises. The caller
6
+ (``chat_model.ainvoke``) decides whether to consult it based on the
7
+ authoritative ``returning_tool_calls`` state (§3.2b); once consulted, the
8
+ detector classifies the terminal turn on its own merits — prior tool blocks
9
+ in the same turn do NOT suppress a terminal API error.
10
+
11
+ Return value: ``ApiErrorDetection`` when an error is detected, ``None``
12
+ otherwise. ``ApiErrorDetection.message`` is the pre-built exception message
13
+ per the §3.2 precedence ladder — the caller feeds it directly into
14
+ ``ClaudeApiResponseError``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import enum
20
+ from collections.abc import Iterable, Mapping
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Constants
27
+ # ---------------------------------------------------------------------------
28
+
29
+ # The CLI repacks upstream Anthropic HTTP-4xx/5xx errors into a single
30
+ # ``TextBlock`` whose text starts with this prefix. Observed across recorded
31
+ # incident transcripts.
32
+ _API_ERROR_PREFIX = "API Error:"
33
+
34
+ # Recognised integer token counters — taken from the observed transcript
35
+ # schema (§1.2a of the design). Any other key in ``usage`` is ignored for
36
+ # the verdict.
37
+ _TOKEN_COUNTER_KEYS: frozenset[str] = frozenset({
38
+ "input_tokens",
39
+ "output_tokens",
40
+ "cache_creation_input_tokens",
41
+ "cache_read_input_tokens",
42
+ })
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Usage classification (BLOCKER fix)
47
+ # ---------------------------------------------------------------------------
48
+
49
+ class UsageVerdict(enum.Enum):
50
+ """Three-valued verdict on the terminal turn's ``usage`` payload.
51
+
52
+ * ``KNOWN_ZERO`` — at least one recognised integer counter is present,
53
+ every present recognised counter is an ``int`` (not ``bool``), every
54
+ such counter is ``0``.
55
+ * ``NONZERO`` — at least one recognised integer counter is present and
56
+ positive (``> 0``). A real assistant answer always bills input tokens,
57
+ so this proves the turn is a real answer.
58
+ * ``UNKNOWN`` — nothing usable to judge on: ``None``, ``{}``, only
59
+ unrecognised keys, all recognised keys have unusable types (str,
60
+ ``bool``, ...), or any recognised counter is negative (malformed).
61
+
62
+ Callers must NOT collapse ``UNKNOWN`` into ``KNOWN_ZERO`` — that would
63
+ misclassify a legitimate text-only reply whose transport did not record
64
+ ``usage`` as an error (reviewer BLOCKER, round 2).
65
+ """
66
+
67
+ KNOWN_ZERO = "known_zero"
68
+ NONZERO = "nonzero"
69
+ UNKNOWN = "unknown"
70
+
71
+
72
+ def classify_usage(usage: Mapping[str, Any] | None) -> UsageVerdict:
73
+ """Classify a ``usage`` mapping. NEVER raises."""
74
+ if usage is None:
75
+ return UsageVerdict.UNKNOWN
76
+ usable_zeros = 0
77
+ for key in _TOKEN_COUNTER_KEYS:
78
+ if key not in usage:
79
+ continue
80
+ value = usage[key]
81
+ # ``bool`` is a subclass of ``int`` — explicitly exclude it.
82
+ if isinstance(value, bool):
83
+ continue
84
+ if not isinstance(value, int):
85
+ continue
86
+ if value < 0:
87
+ # Malformed — SDK should never emit a negative token count. Treat
88
+ # as unusable (round-2 REQUIRED nit a). A single malformed
89
+ # counter poisons the whole verdict: we do NOT trust that "the
90
+ # other counters are fine" because we do not know why this one
91
+ # is negative.
92
+ return UsageVerdict.UNKNOWN
93
+ if value > 0:
94
+ return UsageVerdict.NONZERO
95
+ # value == 0
96
+ usable_zeros += 1
97
+
98
+ if usable_zeros > 0:
99
+ return UsageVerdict.KNOWN_ZERO
100
+ return UsageVerdict.UNKNOWN
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Text marker
105
+ # ---------------------------------------------------------------------------
106
+
107
+ def looks_like_api_error_text(text: str | None) -> bool:
108
+ """True iff the WHOLE turn text is a CLI-repacked upstream API error."""
109
+ if not isinstance(text, str):
110
+ return False
111
+ return text.strip().startswith(_API_ERROR_PREFIX)
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Detection result
116
+ # ---------------------------------------------------------------------------
117
+
118
+ @dataclass(frozen=True)
119
+ class ApiErrorDetection:
120
+ """Structured detection returned by :func:`detect_api_error`.
121
+
122
+ ``message`` is the pre-built exception message per §3.2 precedence:
123
+ marker-carrying ``response_text`` → ``result_text`` → joined
124
+ ``result_errors`` → synthesised description from structured fields.
125
+ Never empty; never ordinary partial assistant prose.
126
+ """
127
+
128
+ message: str
129
+ assistant_error: str | None
130
+ result_subtype: str | None
131
+ result_errors: tuple[str, ...]
132
+ stop_reason: str | None
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Detector
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def _coerce_errors(errors: Any) -> tuple[str, ...]:
140
+ """Normalise ``result_errors`` into a tuple of str; never raise."""
141
+ if errors is None:
142
+ return ()
143
+ if isinstance(errors, str):
144
+ return (errors,)
145
+ if isinstance(errors, Iterable):
146
+ try:
147
+ return tuple(str(e) for e in errors)
148
+ except Exception:
149
+ return ()
150
+ return ()
151
+
152
+
153
+ def _build_message(
154
+ *,
155
+ response_text: str | None,
156
+ result_text: str | None,
157
+ result_errors: tuple[str, ...],
158
+ assistant_error: str | None,
159
+ result_subtype: str | None,
160
+ stop_reason: str | None,
161
+ ) -> str:
162
+ """§3.2 precedence — always returns a non-empty string."""
163
+ # 1. marker-carrying response_text
164
+ if isinstance(response_text, str) and looks_like_api_error_text(response_text):
165
+ stripped = response_text.strip()
166
+ if stripped:
167
+ return stripped
168
+ # 2. result_text
169
+ if isinstance(result_text, str) and result_text.strip():
170
+ return result_text.strip()
171
+ # 3. joined result_errors
172
+ joined = "; ".join(e for e in result_errors if e)
173
+ if joined:
174
+ return joined
175
+ # 4. synthesised description from structured fields
176
+ parts: list[str] = []
177
+ if assistant_error:
178
+ parts.append(f"assistant_error={assistant_error}")
179
+ if result_subtype:
180
+ parts.append(f"subtype={result_subtype}")
181
+ if stop_reason:
182
+ parts.append(f"stop_reason={stop_reason}")
183
+ tail = f" ({', '.join(parts)})" if parts else ""
184
+ return f"Claude API error{tail}"
185
+
186
+
187
+ def detect_api_error(
188
+ *,
189
+ response_text: str | None,
190
+ assistant_error: str | None,
191
+ result_is_error: bool,
192
+ result_subtype: str | None = None,
193
+ result_errors: Any = (),
194
+ result_text: str | None = None,
195
+ stop_reason: str | None = None,
196
+ usage: Mapping[str, Any] | None = None,
197
+ ) -> ApiErrorDetection | None:
198
+ """Return a structured detection, or ``None``. NEVER raises.
199
+
200
+ Decision rule (§3.2):
201
+
202
+ 1. ``assistant_error`` set → error.
203
+ 2. Else ``result_is_error`` is True → error.
204
+ 3. Else marker text AND ``classify_usage`` is ``KNOWN_ZERO`` → error.
205
+ 4. Else → not an error.
206
+
207
+ ``result_subtype`` / ``result_errors`` / ``result_text`` / ``stop_reason``
208
+ are ENRICHMENT ONLY — used to build the message text, never as
209
+ independent proof of error (round-2 REQUIRED nit c).
210
+ """
211
+ try:
212
+ errors_tuple = _coerce_errors(result_errors)
213
+
214
+ # Rule 1 — assistant_error is a truthy structured SDK signal.
215
+ if assistant_error:
216
+ return ApiErrorDetection(
217
+ message=_build_message(
218
+ response_text=response_text,
219
+ result_text=result_text,
220
+ result_errors=errors_tuple,
221
+ assistant_error=str(assistant_error),
222
+ result_subtype=result_subtype,
223
+ stop_reason=stop_reason,
224
+ ),
225
+ assistant_error=str(assistant_error),
226
+ result_subtype=result_subtype,
227
+ result_errors=errors_tuple,
228
+ stop_reason=stop_reason,
229
+ )
230
+
231
+ # Rule 2 — the SDK's single terminal-error bool.
232
+ if bool(result_is_error):
233
+ return ApiErrorDetection(
234
+ message=_build_message(
235
+ response_text=response_text,
236
+ result_text=result_text,
237
+ result_errors=errors_tuple,
238
+ assistant_error=None,
239
+ result_subtype=result_subtype,
240
+ stop_reason=stop_reason,
241
+ ),
242
+ assistant_error=None,
243
+ result_subtype=result_subtype,
244
+ result_errors=errors_tuple,
245
+ stop_reason=stop_reason,
246
+ )
247
+
248
+ # Rule 3 — marker AND measured KNOWN_ZERO usage (conservative fallback).
249
+ if looks_like_api_error_text(response_text) and classify_usage(usage) is UsageVerdict.KNOWN_ZERO:
250
+ return ApiErrorDetection(
251
+ message=_build_message(
252
+ response_text=response_text,
253
+ result_text=result_text,
254
+ result_errors=errors_tuple,
255
+ assistant_error=None,
256
+ result_subtype=result_subtype,
257
+ stop_reason=stop_reason,
258
+ ),
259
+ assistant_error=None,
260
+ result_subtype=result_subtype,
261
+ result_errors=errors_tuple,
262
+ stop_reason=stop_reason,
263
+ )
264
+
265
+ return None
266
+ except Exception:
267
+ # Contract: never raise. On any unforeseen malformed input, treat as
268
+ # "not detected" so the caller falls through to the existing
269
+ # normal-answer path.
270
+ return None