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.
Files changed (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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
+ )