mycode-coding-agent 0.1.0__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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/error_handling.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from email.utils import parsedate_to_datetime
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import socket
|
|
8
|
+
import ssl
|
|
9
|
+
from time import time
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from openai import (
|
|
14
|
+
APIConnectionError,
|
|
15
|
+
APIStatusError,
|
|
16
|
+
APITimeoutError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
BadRequestError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
PermissionDeniedError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MAX_MODEL_RETRY_DELAY_SECONDS = 30.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ModelErrorCode = Literal[
|
|
29
|
+
"timeout",
|
|
30
|
+
"authentication",
|
|
31
|
+
"permission_denied",
|
|
32
|
+
"rate_limit",
|
|
33
|
+
"bad_request",
|
|
34
|
+
"not_found",
|
|
35
|
+
"server_error",
|
|
36
|
+
"tls_error",
|
|
37
|
+
"dns_error",
|
|
38
|
+
"connection_error",
|
|
39
|
+
"unknown",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class UserFacingModelError:
|
|
45
|
+
code: ModelErrorCode
|
|
46
|
+
message: str
|
|
47
|
+
retryable: bool | None
|
|
48
|
+
retry_after_seconds: float | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ProviderDiagnostic:
|
|
53
|
+
http_status: int | None = None
|
|
54
|
+
code: str | None = None
|
|
55
|
+
error_type: str | None = None
|
|
56
|
+
message: str | None = None
|
|
57
|
+
request_id: str | None = None
|
|
58
|
+
retry_after: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def classify_model_error(error: BaseException) -> UserFacingModelError:
|
|
62
|
+
chain = _exception_chain(error)
|
|
63
|
+
status_error = next(
|
|
64
|
+
(item for item in chain if isinstance(item, APIStatusError)), None
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if any(isinstance(item, AuthenticationError) for item in chain):
|
|
68
|
+
return UserFacingModelError(
|
|
69
|
+
code="authentication",
|
|
70
|
+
message="模型服务鉴权失败,请检查 API Key 和 API 地址。",
|
|
71
|
+
retryable=False,
|
|
72
|
+
)
|
|
73
|
+
if any(isinstance(item, PermissionDeniedError) for item in chain):
|
|
74
|
+
return UserFacingModelError(
|
|
75
|
+
code="permission_denied",
|
|
76
|
+
message="模型服务拒绝访问,请检查订阅状态和模型使用权限。",
|
|
77
|
+
retryable=False,
|
|
78
|
+
)
|
|
79
|
+
if status_error is not None and status_error.status_code in {401, 403}:
|
|
80
|
+
if status_error.status_code == 401:
|
|
81
|
+
return UserFacingModelError(
|
|
82
|
+
code="authentication",
|
|
83
|
+
message="模型服务鉴权失败,请检查 API Key 和 API 地址。",
|
|
84
|
+
retryable=False,
|
|
85
|
+
)
|
|
86
|
+
return UserFacingModelError(
|
|
87
|
+
code="permission_denied",
|
|
88
|
+
message="模型服务拒绝访问,请检查订阅状态和模型使用权限。",
|
|
89
|
+
retryable=False,
|
|
90
|
+
)
|
|
91
|
+
rate_limit = next(
|
|
92
|
+
(
|
|
93
|
+
item
|
|
94
|
+
for item in chain
|
|
95
|
+
if isinstance(item, RateLimitError)
|
|
96
|
+
or (
|
|
97
|
+
isinstance(item, APIStatusError)
|
|
98
|
+
and item.status_code == 429
|
|
99
|
+
)
|
|
100
|
+
),
|
|
101
|
+
None,
|
|
102
|
+
)
|
|
103
|
+
if rate_limit is not None:
|
|
104
|
+
quota_exhausted = _quota_exhausted(chain)
|
|
105
|
+
return UserFacingModelError(
|
|
106
|
+
code="rate_limit",
|
|
107
|
+
message=(
|
|
108
|
+
"模型服务额度已耗尽,请检查账户额度或计费状态。"
|
|
109
|
+
if quota_exhausted
|
|
110
|
+
else "模型服务当前限流或额度不足,请稍后重试并检查额度。"
|
|
111
|
+
),
|
|
112
|
+
retryable=not quota_exhausted,
|
|
113
|
+
retry_after_seconds=(
|
|
114
|
+
None if quota_exhausted else _retry_after_seconds(rate_limit)
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
if any(isinstance(item, BadRequestError) for item in chain):
|
|
118
|
+
return UserFacingModelError(
|
|
119
|
+
code="bad_request",
|
|
120
|
+
message="模型服务拒绝了当前请求,请检查模型名称和请求参数。",
|
|
121
|
+
retryable=False,
|
|
122
|
+
)
|
|
123
|
+
if any(isinstance(item, NotFoundError) for item in chain):
|
|
124
|
+
return UserFacingModelError(
|
|
125
|
+
code="not_found",
|
|
126
|
+
message="未找到模型服务端点或指定模型,请检查 API 地址和模型名称。",
|
|
127
|
+
retryable=False,
|
|
128
|
+
)
|
|
129
|
+
if status_error is not None and status_error.status_code == 408:
|
|
130
|
+
return UserFacingModelError(
|
|
131
|
+
code="timeout",
|
|
132
|
+
message="模型服务请求超时(HTTP 408),请稍后重试。",
|
|
133
|
+
retryable=True,
|
|
134
|
+
)
|
|
135
|
+
if status_error is not None and status_error.status_code in {400, 404}:
|
|
136
|
+
if status_error.status_code == 400:
|
|
137
|
+
return UserFacingModelError(
|
|
138
|
+
code="bad_request",
|
|
139
|
+
message="模型服务拒绝了当前请求,请检查模型名称和请求参数。",
|
|
140
|
+
retryable=False,
|
|
141
|
+
)
|
|
142
|
+
return UserFacingModelError(
|
|
143
|
+
code="not_found",
|
|
144
|
+
message="未找到模型服务端点或指定模型,请检查 API 地址和模型名称。",
|
|
145
|
+
retryable=False,
|
|
146
|
+
)
|
|
147
|
+
if status_error is not None and status_error.status_code >= 500:
|
|
148
|
+
return UserFacingModelError(
|
|
149
|
+
code="server_error",
|
|
150
|
+
message=(
|
|
151
|
+
f"模型服务暂时不可用(HTTP {status_error.status_code}),请稍后重试。"
|
|
152
|
+
),
|
|
153
|
+
retryable=True,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if any(isinstance(item, ssl.SSLError) for item in chain):
|
|
157
|
+
return UserFacingModelError(
|
|
158
|
+
code="tls_error",
|
|
159
|
+
message=(
|
|
160
|
+
"HTTPS/TLS 连接被提前关闭;如使用代理,请尝试切换节点或改为直连。"
|
|
161
|
+
),
|
|
162
|
+
retryable=True,
|
|
163
|
+
)
|
|
164
|
+
if any(isinstance(item, socket.gaierror) for item in chain):
|
|
165
|
+
return UserFacingModelError(
|
|
166
|
+
code="dns_error",
|
|
167
|
+
message="无法解析模型服务域名,请检查 DNS、网络和 API 地址。",
|
|
168
|
+
retryable=True,
|
|
169
|
+
)
|
|
170
|
+
if any(isinstance(item, httpx.ConnectTimeout) for item in chain):
|
|
171
|
+
return UserFacingModelError(
|
|
172
|
+
code="timeout",
|
|
173
|
+
message="连接模型服务超时,请检查网络、代理节点和 API 地址。",
|
|
174
|
+
retryable=True,
|
|
175
|
+
)
|
|
176
|
+
if any(isinstance(item, httpx.ReadTimeout) for item in chain):
|
|
177
|
+
return UserFacingModelError(
|
|
178
|
+
code="timeout",
|
|
179
|
+
message="等待模型响应超时,请稍后重试并检查代理节点或服务状态。",
|
|
180
|
+
retryable=True,
|
|
181
|
+
)
|
|
182
|
+
if isinstance(error, APITimeoutError) or any(
|
|
183
|
+
isinstance(item, httpx.TimeoutException) for item in chain
|
|
184
|
+
):
|
|
185
|
+
return UserFacingModelError(
|
|
186
|
+
code="timeout",
|
|
187
|
+
message="模型服务请求超时,请稍后重试并检查网络或代理节点。",
|
|
188
|
+
retryable=True,
|
|
189
|
+
)
|
|
190
|
+
if any(
|
|
191
|
+
isinstance(
|
|
192
|
+
item,
|
|
193
|
+
(
|
|
194
|
+
ConnectionResetError,
|
|
195
|
+
BrokenPipeError,
|
|
196
|
+
httpx.RemoteProtocolError,
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
for item in chain
|
|
200
|
+
):
|
|
201
|
+
return UserFacingModelError(
|
|
202
|
+
code="connection_error",
|
|
203
|
+
message="模型服务连接在响应完成前中断,请稍后重试或切换网络节点。",
|
|
204
|
+
retryable=True,
|
|
205
|
+
)
|
|
206
|
+
if isinstance(error, APIConnectionError) or any(
|
|
207
|
+
isinstance(item, httpx.ConnectError) for item in chain
|
|
208
|
+
):
|
|
209
|
+
return UserFacingModelError(
|
|
210
|
+
code="connection_error",
|
|
211
|
+
message="无法连接模型服务,请检查网络、代理节点和 API 地址。",
|
|
212
|
+
retryable=True,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return UserFacingModelError(
|
|
216
|
+
code="unknown",
|
|
217
|
+
message=error_summary(error),
|
|
218
|
+
retryable=None,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def format_model_error(error: BaseException, *, operation: str) -> str:
|
|
223
|
+
classified = classify_model_error(error)
|
|
224
|
+
if classified.code == "unknown":
|
|
225
|
+
return f"{operation}:{classified.message}"
|
|
226
|
+
diagnostic = extract_provider_diagnostic(error)
|
|
227
|
+
message = classified.message
|
|
228
|
+
if (
|
|
229
|
+
diagnostic.http_status is not None
|
|
230
|
+
and f"HTTP {diagnostic.http_status}" not in message
|
|
231
|
+
):
|
|
232
|
+
message = message.rstrip("。") + f"(HTTP {diagnostic.http_status})。"
|
|
233
|
+
|
|
234
|
+
details: list[str] = []
|
|
235
|
+
if diagnostic.retry_after is not None:
|
|
236
|
+
details.append(f"retry-after={diagnostic.retry_after}")
|
|
237
|
+
provider_label = diagnostic.code or diagnostic.error_type
|
|
238
|
+
if diagnostic.message is not None:
|
|
239
|
+
details.append(
|
|
240
|
+
diagnostic.message
|
|
241
|
+
if provider_label is None
|
|
242
|
+
else f"{provider_label}: {diagnostic.message}"
|
|
243
|
+
)
|
|
244
|
+
elif provider_label is not None:
|
|
245
|
+
details.append(provider_label)
|
|
246
|
+
if (
|
|
247
|
+
diagnostic.error_type is not None
|
|
248
|
+
and diagnostic.error_type != diagnostic.code
|
|
249
|
+
):
|
|
250
|
+
details.append(f"provider-type={diagnostic.error_type}")
|
|
251
|
+
if diagnostic.request_id is not None:
|
|
252
|
+
details.append(f"request-id={diagnostic.request_id}")
|
|
253
|
+
return "\n".join([message, *details])
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def extract_provider_diagnostic(error: BaseException) -> ProviderDiagnostic:
|
|
257
|
+
http_status: int | None = None
|
|
258
|
+
code: str | None = None
|
|
259
|
+
error_type: str | None = None
|
|
260
|
+
message: str | None = None
|
|
261
|
+
request_id: str | None = None
|
|
262
|
+
retry_after: str | None = None
|
|
263
|
+
|
|
264
|
+
for current in _exception_chain(error):
|
|
265
|
+
if http_status is None:
|
|
266
|
+
http_status = _safe_http_status(current)
|
|
267
|
+
body = getattr(current, "body", None)
|
|
268
|
+
if isinstance(body, Mapping):
|
|
269
|
+
body_code, body_type, body_message = _mapping_provider_fields(body)
|
|
270
|
+
code = code or body_code
|
|
271
|
+
error_type = error_type or body_type
|
|
272
|
+
message = message or body_message
|
|
273
|
+
code = code or _safe_provider_text(getattr(current, "code", None), 200)
|
|
274
|
+
error_type = error_type or _safe_provider_text(
|
|
275
|
+
getattr(current, "type", None), 200
|
|
276
|
+
)
|
|
277
|
+
request_id = request_id or _safe_provider_text(
|
|
278
|
+
getattr(current, "request_id", None), 200
|
|
279
|
+
)
|
|
280
|
+
request_id = request_id or _safe_provider_text(
|
|
281
|
+
getattr(current, "_request_id", None), 200
|
|
282
|
+
)
|
|
283
|
+
response = getattr(current, "response", None)
|
|
284
|
+
request_id = request_id or _safe_provider_text(
|
|
285
|
+
_header_value(response, "x-request-id"), 200
|
|
286
|
+
)
|
|
287
|
+
retry_after = retry_after or _safe_provider_text(
|
|
288
|
+
_header_value(response, "retry-after"), 100
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return ProviderDiagnostic(
|
|
292
|
+
http_status=http_status,
|
|
293
|
+
code=code,
|
|
294
|
+
error_type=error_type,
|
|
295
|
+
message=message,
|
|
296
|
+
request_id=request_id,
|
|
297
|
+
retry_after=retry_after,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def error_summary(error: BaseException) -> str:
|
|
302
|
+
message = str(error).strip()
|
|
303
|
+
if message == "":
|
|
304
|
+
return type(error).__name__
|
|
305
|
+
first_line = message.splitlines()[0].strip()
|
|
306
|
+
if len(first_line) <= 500:
|
|
307
|
+
return first_line
|
|
308
|
+
return first_line[:497] + "..."
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
_PROVIDER_SECRET_PATTERNS = (
|
|
312
|
+
re.compile(r"(?i)\bbearer\s+[^\s,;]+"),
|
|
313
|
+
re.compile(
|
|
314
|
+
r"(?i)\b(?:authorization|api[\s_-]*key|cookie)\b"
|
|
315
|
+
r"\s*(?::|=|\bis\b)?\s*[^\s,;]+"
|
|
316
|
+
),
|
|
317
|
+
re.compile(r"(?i)\bsk-[a-z0-9_-]{6,}"),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _safe_provider_text(value: object, max_chars: int) -> str | None:
|
|
322
|
+
if not isinstance(value, (str, int, float)) or isinstance(value, bool):
|
|
323
|
+
return None
|
|
324
|
+
text = " ".join(str(value).split())
|
|
325
|
+
if text == "":
|
|
326
|
+
return None
|
|
327
|
+
for pattern in _PROVIDER_SECRET_PATTERNS:
|
|
328
|
+
text = pattern.sub("[redacted]", text)
|
|
329
|
+
if len(text) <= max_chars:
|
|
330
|
+
return text
|
|
331
|
+
return text[: max_chars - 3] + "..."
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _safe_http_status(error: BaseException) -> int | None:
|
|
335
|
+
status = getattr(error, "status_code", None)
|
|
336
|
+
if not isinstance(status, int) or isinstance(status, bool):
|
|
337
|
+
response = getattr(error, "response", None)
|
|
338
|
+
status = getattr(response, "status_code", None)
|
|
339
|
+
if (
|
|
340
|
+
isinstance(status, int)
|
|
341
|
+
and not isinstance(status, bool)
|
|
342
|
+
and 100 <= status <= 599
|
|
343
|
+
):
|
|
344
|
+
return status
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _mapping_provider_fields(
|
|
349
|
+
body: Mapping[object, object],
|
|
350
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
351
|
+
pending: list[tuple[Mapping[object, object], int]] = []
|
|
352
|
+
nested_error = _mapping_value(body, "error")
|
|
353
|
+
if isinstance(nested_error, Mapping):
|
|
354
|
+
pending.append((nested_error, 0))
|
|
355
|
+
pending.append((body, 0))
|
|
356
|
+
seen: set[int] = set()
|
|
357
|
+
code: str | None = None
|
|
358
|
+
error_type: str | None = None
|
|
359
|
+
message: str | None = None
|
|
360
|
+
while pending and (code is None or error_type is None or message is None):
|
|
361
|
+
current, depth = pending.pop(0)
|
|
362
|
+
if id(current) in seen:
|
|
363
|
+
continue
|
|
364
|
+
seen.add(id(current))
|
|
365
|
+
code = code or _safe_provider_text(_mapping_value(current, "code"), 200)
|
|
366
|
+
error_type = error_type or _safe_provider_text(
|
|
367
|
+
_mapping_value(current, "type"), 200
|
|
368
|
+
)
|
|
369
|
+
message = message or _safe_provider_text(
|
|
370
|
+
_mapping_value(current, "message"), 500
|
|
371
|
+
)
|
|
372
|
+
if message is None:
|
|
373
|
+
message = _safe_provider_text(_mapping_value(current, "detail"), 500)
|
|
374
|
+
if depth < 2:
|
|
375
|
+
pending.extend(
|
|
376
|
+
(nested, depth + 1)
|
|
377
|
+
for nested in current.values()
|
|
378
|
+
if isinstance(nested, Mapping)
|
|
379
|
+
)
|
|
380
|
+
return code, error_type, message
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _mapping_value(mapping: Mapping[object, object], name: str) -> object | None:
|
|
384
|
+
for key, value in mapping.items():
|
|
385
|
+
if isinstance(key, str) and key.casefold() == name:
|
|
386
|
+
return value
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _header_value(response: object, name: str) -> object | None:
|
|
391
|
+
headers = getattr(response, "headers", None)
|
|
392
|
+
getter = getattr(headers, "get", None)
|
|
393
|
+
if not callable(getter):
|
|
394
|
+
return None
|
|
395
|
+
value = getter(name)
|
|
396
|
+
if value is not None:
|
|
397
|
+
return value
|
|
398
|
+
return getter(name.title())
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
_QUOTA_EXHAUSTED_MARKERS = {
|
|
402
|
+
"insufficient_quota",
|
|
403
|
+
"quota_exceeded",
|
|
404
|
+
"usage_limit",
|
|
405
|
+
"billing_hard_limit_reached",
|
|
406
|
+
"billing_limit",
|
|
407
|
+
"billing_error",
|
|
408
|
+
"credits_exhausted",
|
|
409
|
+
"billing",
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _quota_exhausted(chain: tuple[BaseException, ...]) -> bool:
|
|
414
|
+
for error in chain:
|
|
415
|
+
for marker in _error_markers(error):
|
|
416
|
+
if marker in _QUOTA_EXHAUSTED_MARKERS:
|
|
417
|
+
return True
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _error_markers(error: BaseException):
|
|
422
|
+
for attribute in ("code", "type"):
|
|
423
|
+
value = getattr(error, attribute, None)
|
|
424
|
+
if isinstance(value, str):
|
|
425
|
+
yield value.strip().casefold()
|
|
426
|
+
body = getattr(error, "body", None)
|
|
427
|
+
if isinstance(body, Mapping):
|
|
428
|
+
yield from _mapping_error_markers(body)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _mapping_error_markers(value: Mapping[object, object]):
|
|
432
|
+
for key, nested in value.items():
|
|
433
|
+
if key in {"code", "type"} and isinstance(nested, str):
|
|
434
|
+
yield nested.strip().casefold()
|
|
435
|
+
if isinstance(nested, Mapping):
|
|
436
|
+
yield from _mapping_error_markers(nested)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _retry_after_seconds(error: BaseException) -> float | None:
|
|
440
|
+
response = getattr(error, "response", None)
|
|
441
|
+
headers = getattr(response, "headers", None)
|
|
442
|
+
if headers is None:
|
|
443
|
+
return None
|
|
444
|
+
value = headers.get("Retry-After")
|
|
445
|
+
if value is None:
|
|
446
|
+
return None
|
|
447
|
+
try:
|
|
448
|
+
delay = float(str(value).strip())
|
|
449
|
+
except (TypeError, ValueError):
|
|
450
|
+
delay = None
|
|
451
|
+
if delay is not None and math.isfinite(delay) and delay >= 0:
|
|
452
|
+
return min(delay, MAX_MODEL_RETRY_DELAY_SECONDS)
|
|
453
|
+
try:
|
|
454
|
+
retry_at = parsedate_to_datetime(str(value))
|
|
455
|
+
if retry_at.tzinfo is None:
|
|
456
|
+
retry_at = retry_at.replace(tzinfo=timezone.utc)
|
|
457
|
+
delay = retry_at.timestamp() - time()
|
|
458
|
+
except (TypeError, ValueError, OverflowError):
|
|
459
|
+
return None
|
|
460
|
+
if not math.isfinite(delay):
|
|
461
|
+
return None
|
|
462
|
+
return min(max(0.0, delay), MAX_MODEL_RETRY_DELAY_SECONDS)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _exception_chain(error: BaseException) -> tuple[BaseException, ...]:
|
|
466
|
+
chain: list[BaseException] = []
|
|
467
|
+
seen: set[int] = set()
|
|
468
|
+
pending: list[BaseException] = [error]
|
|
469
|
+
while pending:
|
|
470
|
+
current = pending.pop()
|
|
471
|
+
if id(current) in seen:
|
|
472
|
+
continue
|
|
473
|
+
seen.add(id(current))
|
|
474
|
+
chain.append(current)
|
|
475
|
+
if isinstance(current, BaseExceptionGroup):
|
|
476
|
+
pending.extend(reversed(current.exceptions))
|
|
477
|
+
if current.__cause__ is not None:
|
|
478
|
+
pending.append(current.__cause__)
|
|
479
|
+
elif not current.__suppress_context__ and current.__context__ is not None:
|
|
480
|
+
pending.append(current.__context__)
|
|
481
|
+
return tuple(chain)
|
mycode/event_format.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Shared, presentation-neutral formatting for Agent event diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def summarize_event_content(content: str | None, max_chars: int = 160) -> str:
|
|
9
|
+
"""Collapse event content to one bounded diagnostic line."""
|
|
10
|
+
|
|
11
|
+
if content is None or content == "":
|
|
12
|
+
return ""
|
|
13
|
+
summary = " ".join(content.split())
|
|
14
|
+
if len(summary) <= max_chars:
|
|
15
|
+
return summary
|
|
16
|
+
return f"{summary[: max_chars - 3]}..."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def summarize_tool_arguments(name: str, arguments: dict[str, object]) -> str:
|
|
20
|
+
"""Format useful tool diagnostics while replacing large text bodies with sizes."""
|
|
21
|
+
|
|
22
|
+
if name == "read_file":
|
|
23
|
+
return _format_arguments(
|
|
24
|
+
{
|
|
25
|
+
"path": arguments.get("path"),
|
|
26
|
+
"start_line": arguments.get("start_line"),
|
|
27
|
+
"max_lines": arguments.get("max_lines"),
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
if name == "read_artifact":
|
|
31
|
+
return _format_arguments(
|
|
32
|
+
{
|
|
33
|
+
"artifact_path": arguments.get("artifact_path"),
|
|
34
|
+
"offset_chars": arguments.get("offset_chars"),
|
|
35
|
+
"max_chars": arguments.get("max_chars"),
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
if name == "glob":
|
|
39
|
+
return _format_arguments(
|
|
40
|
+
{
|
|
41
|
+
"pattern": arguments.get("pattern"),
|
|
42
|
+
"max_results": arguments.get("max_results"),
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
if name == "grep":
|
|
46
|
+
query = arguments.get("query")
|
|
47
|
+
return _format_arguments(
|
|
48
|
+
{
|
|
49
|
+
"query_chars": len(query) if isinstance(query, str) else None,
|
|
50
|
+
"path_pattern": arguments.get("path_pattern"),
|
|
51
|
+
"case_sensitive": arguments.get("case_sensitive"),
|
|
52
|
+
"max_results": arguments.get("max_results"),
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
if name == "delegate_task":
|
|
56
|
+
objective = arguments.get("objective")
|
|
57
|
+
context = arguments.get("context")
|
|
58
|
+
scope_paths = arguments.get("scope_paths")
|
|
59
|
+
return _format_arguments(
|
|
60
|
+
{
|
|
61
|
+
"role": arguments.get("role"),
|
|
62
|
+
"objective_chars": (
|
|
63
|
+
len(objective) if isinstance(objective, str) else None
|
|
64
|
+
),
|
|
65
|
+
"context_chars": len(context) if isinstance(context, str) else None,
|
|
66
|
+
"scope_path_count": (
|
|
67
|
+
len(scope_paths) if isinstance(scope_paths, list) else None
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
if name == "write_file":
|
|
72
|
+
content = arguments.get("content")
|
|
73
|
+
return _format_arguments(
|
|
74
|
+
{
|
|
75
|
+
"path": arguments.get("path"),
|
|
76
|
+
"content_chars": len(content) if isinstance(content, str) else None,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
if name == "edit_file":
|
|
80
|
+
old_text = arguments.get("old_text")
|
|
81
|
+
new_text = arguments.get("new_text")
|
|
82
|
+
return _format_arguments(
|
|
83
|
+
{
|
|
84
|
+
"path": arguments.get("path"),
|
|
85
|
+
"old_text_chars": (
|
|
86
|
+
len(old_text) if isinstance(old_text, str) else None
|
|
87
|
+
),
|
|
88
|
+
"new_text_chars": (
|
|
89
|
+
len(new_text) if isinstance(new_text, str) else None
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
if name in {"run_command", "run_validation"}:
|
|
94
|
+
command = arguments.get("command")
|
|
95
|
+
command_display = (
|
|
96
|
+
subprocess.list2cmdline(command)
|
|
97
|
+
if isinstance(command, list)
|
|
98
|
+
and all(isinstance(part, str) for part in command)
|
|
99
|
+
else command
|
|
100
|
+
)
|
|
101
|
+
return _format_arguments(
|
|
102
|
+
{
|
|
103
|
+
"command": command_display,
|
|
104
|
+
"cwd": arguments.get("cwd"),
|
|
105
|
+
"timeout_seconds": arguments.get("timeout_seconds"),
|
|
106
|
+
"max_output_chars": arguments.get("max_output_chars"),
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
if name == "inspect_changes":
|
|
110
|
+
paths = arguments.get("paths")
|
|
111
|
+
return _format_arguments(
|
|
112
|
+
{
|
|
113
|
+
"action": arguments.get("action"),
|
|
114
|
+
"path_count": len(paths) if isinstance(paths, list) else None,
|
|
115
|
+
"staged": arguments.get("staged"),
|
|
116
|
+
"base_ref": arguments.get("base_ref"),
|
|
117
|
+
"max_output_chars": arguments.get("max_output_chars"),
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
if name == "list_memories":
|
|
121
|
+
return _format_arguments({"scope": arguments.get("scope")})
|
|
122
|
+
if name == "save_memory":
|
|
123
|
+
content = arguments.get("content")
|
|
124
|
+
return _format_arguments(
|
|
125
|
+
{
|
|
126
|
+
"scope": arguments.get("scope"),
|
|
127
|
+
"kind": arguments.get("kind"),
|
|
128
|
+
"key": arguments.get("key"),
|
|
129
|
+
"content_chars": len(content) if isinstance(content, str) else None,
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
if name == "delete_memory":
|
|
133
|
+
return _format_arguments(
|
|
134
|
+
{"scope": arguments.get("scope"), "key": arguments.get("key")}
|
|
135
|
+
)
|
|
136
|
+
return _format_arguments(
|
|
137
|
+
{
|
|
138
|
+
"argument_keys": sorted(arguments),
|
|
139
|
+
"argument_count": len(arguments),
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _format_arguments(arguments: dict[str, object]) -> str:
|
|
145
|
+
return ", ".join(
|
|
146
|
+
f"{key}={value!r}" for key, value in arguments.items() if value is not None
|
|
147
|
+
)
|