jdcloud-agentgrid 0.1.3__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.
agentgrid/config.py ADDED
@@ -0,0 +1,504 @@
1
+ """AgentGrid SDK 配置模型与默认配置管理。
2
+
3
+ OpenSpec `profile-and-config-core` 要求:
4
+ - 支持 region/endpoint/data_endpoint/timeout/max_retries/user_agent_extra 核心配置。
5
+ - 支持 connect_timeout/read_timeout 细粒度超时。
6
+ - 支持 SDK 默认级与 Profile 级配置合并,Profile 级覆盖默认级。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass, field, fields, replace
14
+ from pathlib import Path
15
+ from urllib.parse import urlparse
16
+
17
+ from jdcloud_sdk.core.config import Config as JdcloudCoreConfig
18
+ import yaml
19
+
20
+
21
+ DEFAULT_RUNTIME_ENDPOINT_HOST = "agentgrid.jdcloud-api.com"
22
+ DEFAULT_RUNTIME_ENDPOINT = f"http://{DEFAULT_RUNTIME_ENDPOINT_HOST}"
23
+ DEFAULT_RUNTIME_DATA_ENDPOINT = "https://agentgrid-cn-north-1.jdcloud.com"
24
+
25
+
26
+ @dataclass(slots=True, frozen=True)
27
+ class BasicConfig:
28
+ """agentgrid.yaml basic 节点配置。"""
29
+
30
+ region: str | None = None
31
+ endpoint: str | None = None
32
+ data_endpoint: str | None = None
33
+ timeout: float | None = None
34
+ connect_timeout: float | None = None
35
+ read_timeout: float | None = None
36
+ max_retries: int | None = None
37
+ user_agent_extra: str | None = None
38
+
39
+ def __post_init__(self) -> None:
40
+ if self.region is not None and not self.region.strip():
41
+ raise ValueError("region must not be empty")
42
+ if self.endpoint is not None and not self.endpoint.strip():
43
+ raise ValueError("endpoint must not be empty")
44
+ if self.data_endpoint is not None and not self.data_endpoint.strip():
45
+ raise ValueError("data_endpoint must not be empty")
46
+ if self.timeout is not None and self.timeout < 0:
47
+ raise ValueError("timeout must be >= 0")
48
+ if self.connect_timeout is not None and self.connect_timeout < 0:
49
+ raise ValueError("connect_timeout must be >= 0")
50
+ if self.read_timeout is not None and self.read_timeout < 0:
51
+ raise ValueError("read_timeout must be >= 0")
52
+ if self.max_retries is not None and self.max_retries < 0:
53
+ raise ValueError("max_retries must be >= 0")
54
+
55
+ def to_overrides(self) -> dict[str, object]:
56
+ """转换为 AgentGridConfig 覆盖字段。"""
57
+
58
+ overrides: dict[str, object] = {}
59
+ for config_field in fields(BasicConfig):
60
+ value = getattr(self, config_field.name)
61
+ if value is not None:
62
+ overrides[config_field.name] = value
63
+ return overrides
64
+
65
+
66
+ @dataclass(slots=True, frozen=True)
67
+ class RuntimeConfig:
68
+ """agentgrid.yaml runtime 节点配置。"""
69
+
70
+ region: str | None = None
71
+ identifier: str | None = None
72
+ api_key: str | None = None
73
+
74
+ def __post_init__(self) -> None:
75
+ if self.region is not None and not self.region.strip():
76
+ raise ValueError("runtime.region must not be empty")
77
+ if self.identifier is not None and not self.identifier.strip():
78
+ raise ValueError("runtime.identifier must not be empty")
79
+ if self.api_key is not None and not self.api_key.strip():
80
+ raise ValueError("runtime.api_key must not be empty")
81
+
82
+
83
+ @dataclass(slots=True, frozen=True)
84
+ class ToolConfig:
85
+ """agentgrid.yaml 单个工具节点配置。"""
86
+
87
+ identifier: str | None = None
88
+
89
+ def __post_init__(self) -> None:
90
+ if self.identifier is not None and not self.identifier.strip():
91
+ raise ValueError("tool.identifier must not be empty")
92
+
93
+
94
+ @dataclass(slots=True, frozen=True)
95
+ class ToolsConfig:
96
+ """agentgrid.yaml tools 节点配置。"""
97
+
98
+ code_interpreter: ToolConfig | None = None
99
+ browser: ToolConfig | None = None
100
+
101
+
102
+ @dataclass(slots=True, frozen=True)
103
+ class Config:
104
+ """SDK 全局配置聚合对象。"""
105
+
106
+ basic: BasicConfig = field(default_factory=BasicConfig)
107
+ runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
108
+ tools: ToolsConfig = field(default_factory=ToolsConfig)
109
+
110
+
111
+ @dataclass(slots=True, frozen=True)
112
+ class AgentGridConfig:
113
+ """AgentGrid SDK 运行时配置。"""
114
+
115
+ region: str | None = None
116
+ endpoint: str | None = None
117
+ data_endpoint: str | None = None
118
+ timeout: float | None = None
119
+ connect_timeout: float | None = None
120
+ read_timeout: float | None = None
121
+ max_retries: int = 2
122
+ user_agent_extra: str | None = None
123
+
124
+ def resolve_endpoint_for_jdcloud(self) -> tuple[str | None, str | None]:
125
+ """解析并返回 jdcloud core Config 所需 endpoint/scheme。"""
126
+
127
+ if self.endpoint is None:
128
+ return None, None
129
+
130
+ endpoint = self.endpoint.strip()
131
+ if not endpoint:
132
+ raise ValueError("endpoint must not be empty")
133
+
134
+ if "://" not in endpoint:
135
+ return endpoint, None
136
+
137
+ parsed = urlparse(endpoint)
138
+ if not parsed.hostname:
139
+ raise ValueError("endpoint must contain hostname")
140
+ if parsed.path and parsed.path != "/":
141
+ raise ValueError("endpoint must not contain path")
142
+ if parsed.query or parsed.fragment:
143
+ raise ValueError("endpoint must not contain query or fragment")
144
+
145
+ host = parsed.hostname
146
+ if parsed.port is not None:
147
+ host = f"{host}:{parsed.port}"
148
+ return host, parsed.scheme or None
149
+
150
+ def resolve_timeout_for_jdcloud(self) -> float | tuple[float, float] | None:
151
+ """解析并返回 jdcloud core Config 可用 timeout。"""
152
+
153
+ connect_timeout = self.effective_connect_timeout
154
+ read_timeout = self.effective_read_timeout
155
+ if connect_timeout is not None and read_timeout is not None:
156
+ if connect_timeout == read_timeout:
157
+ return connect_timeout
158
+ return (connect_timeout, read_timeout)
159
+ if connect_timeout is not None:
160
+ return connect_timeout
161
+ if read_timeout is not None:
162
+ return read_timeout
163
+ return None
164
+
165
+ def to_jdcloud_core_config(self) -> JdcloudCoreConfig:
166
+ """构建 jdcloud core `Config`,用于真实 AgentgridClient。"""
167
+
168
+ endpoint, scheme = self.resolve_endpoint_for_jdcloud()
169
+ if endpoint is None:
170
+ endpoint = DEFAULT_RUNTIME_ENDPOINT_HOST
171
+ scheme = "http"
172
+ timeout = self.resolve_timeout_for_jdcloud()
173
+
174
+ config = JdcloudCoreConfig(endpoint=endpoint)
175
+ if scheme is not None:
176
+ config.scheme = scheme
177
+ if timeout is not None:
178
+ config.timeout = timeout
179
+ return config
180
+
181
+ def __post_init__(self) -> None:
182
+ if self.endpoint is not None and not self.endpoint.strip():
183
+ raise ValueError("endpoint must not be empty")
184
+ if self.data_endpoint is not None and not self.data_endpoint.strip():
185
+ raise ValueError("data_endpoint must not be empty")
186
+ if self.timeout is not None and self.timeout < 0:
187
+ raise ValueError("timeout must be >= 0")
188
+ if self.connect_timeout is not None and self.connect_timeout < 0:
189
+ raise ValueError("connect_timeout must be >= 0")
190
+ if self.read_timeout is not None and self.read_timeout < 0:
191
+ raise ValueError("read_timeout must be >= 0")
192
+ if self.max_retries < 0:
193
+ raise ValueError("max_retries must be >= 0")
194
+
195
+ @property
196
+ def effective_connect_timeout(self) -> float | None:
197
+ """返回请求链路实际生效的连接超时。
198
+
199
+ 协同规则:显式 connect_timeout 优先;否则回退到统一 timeout。
200
+ """
201
+
202
+ if self.connect_timeout is not None:
203
+ return self.connect_timeout
204
+ return self.timeout
205
+
206
+ @property
207
+ def effective_read_timeout(self) -> float | None:
208
+ """返回请求链路实际生效的读取超时。
209
+
210
+ 协同规则:显式 read_timeout 优先;否则回退到统一 timeout。
211
+ """
212
+
213
+ if self.read_timeout is not None:
214
+ return self.read_timeout
215
+ return self.timeout
216
+
217
+ @property
218
+ def effective_data_endpoint(self) -> str:
219
+ """返回数据口实际生效 endpoint。
220
+
221
+ OpenSpec 要求控制口与数据口端点分离;当用户未显式配置 data_endpoint
222
+ 时,数据口使用固定默认网关。
223
+ """
224
+
225
+ if self.data_endpoint is None:
226
+ return DEFAULT_RUNTIME_DATA_ENDPOINT
227
+ return self.data_endpoint
228
+
229
+ def merged_with(self, override: AgentGridConfig | None) -> AgentGridConfig:
230
+ """返回合并后的配置对象。
231
+
232
+ 合并规则:override 中非 None 字段覆盖 base;max_retries 永远采用 override。
233
+ """
234
+
235
+ if override is None:
236
+ return self
237
+
238
+ return AgentGridConfig(
239
+ region=override.region if override.region is not None else self.region,
240
+ endpoint=override.endpoint
241
+ if override.endpoint is not None
242
+ else self.endpoint,
243
+ data_endpoint=(
244
+ override.data_endpoint
245
+ if override.data_endpoint is not None
246
+ else self.data_endpoint
247
+ ),
248
+ timeout=override.timeout if override.timeout is not None else self.timeout,
249
+ connect_timeout=(
250
+ override.connect_timeout
251
+ if override.connect_timeout is not None
252
+ else self.connect_timeout
253
+ ),
254
+ read_timeout=(
255
+ override.read_timeout
256
+ if override.read_timeout is not None
257
+ else self.read_timeout
258
+ ),
259
+ max_retries=override.max_retries,
260
+ user_agent_extra=(
261
+ override.user_agent_extra
262
+ if override.user_agent_extra is not None
263
+ else self.user_agent_extra
264
+ ),
265
+ )
266
+
267
+ def with_overrides(self, **kwargs: object) -> AgentGridConfig:
268
+ """使用 dataclass replace 生成覆盖后的新配置。"""
269
+
270
+ return replace(self, **kwargs)
271
+
272
+
273
+ _BUILTIN_SDK_DEFAULT_CONFIG = AgentGridConfig(
274
+ endpoint=DEFAULT_RUNTIME_ENDPOINT,
275
+ data_endpoint=DEFAULT_RUNTIME_DATA_ENDPOINT,
276
+ )
277
+
278
+ _SDK_DEFAULT_CONFIG = _BUILTIN_SDK_DEFAULT_CONFIG
279
+
280
+
281
+ def _resolve_agentgrid_yaml_path() -> Path | None:
282
+ """解析入口脚本同级 agentgrid.yaml 绝对路径。"""
283
+
284
+ if not sys.argv or not sys.argv[0]:
285
+ return None
286
+
287
+ script_path = Path(sys.argv[0])
288
+ if not script_path.is_absolute():
289
+ script_path = Path.cwd() / script_path
290
+ return script_path.resolve().parent / "agentgrid.yaml"
291
+
292
+
293
+ def _validate_mapping_fields(
294
+ *,
295
+ agentgrid_path: Path,
296
+ node_name: str,
297
+ mapping_obj: Mapping[object, object],
298
+ allowed_fields: set[str],
299
+ ) -> None:
300
+ """校验 mapping 节点字段是否都在白名单内。"""
301
+
302
+ unknown_fields = sorted(
303
+ str(field_name)
304
+ for field_name in mapping_obj
305
+ if not isinstance(field_name, str) or field_name not in allowed_fields
306
+ )
307
+ if not unknown_fields:
308
+ return
309
+
310
+ allowed_values = ", ".join(sorted(allowed_fields))
311
+ unknown_values = ", ".join(unknown_fields)
312
+ raise ValueError(
313
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': unknown {node_name} fields "
314
+ f"[{unknown_values}], allowed fields: [{allowed_values}]"
315
+ )
316
+
317
+
318
+ def _parse_basic_config(*, agentgrid_path: Path, value: object) -> BasicConfig:
319
+ """解析 basic 配置。"""
320
+
321
+ if value is None:
322
+ return BasicConfig()
323
+ if not isinstance(value, Mapping):
324
+ raise ValueError(
325
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': basic must be a mapping object"
326
+ )
327
+
328
+ allowed_fields = {config_field.name for config_field in fields(BasicConfig)}
329
+ _validate_mapping_fields(
330
+ agentgrid_path=agentgrid_path,
331
+ node_name="basic",
332
+ mapping_obj=value,
333
+ allowed_fields=allowed_fields,
334
+ )
335
+ basic_dict = {str(key): field_value for key, field_value in value.items()}
336
+ try:
337
+ return BasicConfig(**basic_dict)
338
+ except (TypeError, ValueError) as exc:
339
+ raise ValueError(
340
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': invalid basic config: {exc}"
341
+ ) from exc
342
+
343
+
344
+ def _parse_runtime_config(*, agentgrid_path: Path, value: object) -> RuntimeConfig:
345
+ """解析 runtime 配置。"""
346
+
347
+ if value is None:
348
+ return RuntimeConfig()
349
+ if not isinstance(value, Mapping):
350
+ raise ValueError(
351
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': runtime must be a mapping object"
352
+ )
353
+
354
+ allowed_fields = {config_field.name for config_field in fields(RuntimeConfig)}
355
+ _validate_mapping_fields(
356
+ agentgrid_path=agentgrid_path,
357
+ node_name="runtime",
358
+ mapping_obj=value,
359
+ allowed_fields=allowed_fields,
360
+ )
361
+ runtime_dict = {str(key): field_value for key, field_value in value.items()}
362
+ try:
363
+ return RuntimeConfig(**runtime_dict)
364
+ except (TypeError, ValueError) as exc:
365
+ raise ValueError(
366
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': invalid runtime config: {exc}"
367
+ ) from exc
368
+
369
+
370
+ def _parse_tool_config(
371
+ *, agentgrid_path: Path, tool_name: str, value: object
372
+ ) -> ToolConfig:
373
+ """解析单个工具配置。"""
374
+
375
+ if not isinstance(value, Mapping):
376
+ raise ValueError(
377
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': tools.{tool_name} must be a mapping object"
378
+ )
379
+
380
+ allowed_fields = {config_field.name for config_field in fields(ToolConfig)}
381
+ _validate_mapping_fields(
382
+ agentgrid_path=agentgrid_path,
383
+ node_name=f"tools.{tool_name}",
384
+ mapping_obj=value,
385
+ allowed_fields=allowed_fields,
386
+ )
387
+ tool_dict = {str(key): field_value for key, field_value in value.items()}
388
+ try:
389
+ return ToolConfig(**tool_dict)
390
+ except (TypeError, ValueError) as exc:
391
+ raise ValueError(
392
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': invalid tools.{tool_name} config: {exc}"
393
+ ) from exc
394
+
395
+
396
+ def _parse_tools_config(*, agentgrid_path: Path, value: object) -> ToolsConfig:
397
+ """解析 tools 配置。"""
398
+
399
+ if value is None:
400
+ return ToolsConfig()
401
+ if not isinstance(value, Mapping):
402
+ raise ValueError(
403
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': tools must be a mapping object"
404
+ )
405
+
406
+ allowed_fields = {config_field.name for config_field in fields(ToolsConfig)}
407
+ _validate_mapping_fields(
408
+ agentgrid_path=agentgrid_path,
409
+ node_name="tools",
410
+ mapping_obj=value,
411
+ allowed_fields=allowed_fields,
412
+ )
413
+
414
+ tools_dict: dict[str, ToolConfig | None] = {}
415
+ for key, field_value in value.items():
416
+ key_name = str(key)
417
+ if field_value is None:
418
+ tools_dict[key_name] = None
419
+ continue
420
+ tools_dict[key_name] = _parse_tool_config(
421
+ agentgrid_path=agentgrid_path,
422
+ tool_name=key_name,
423
+ value=field_value,
424
+ )
425
+
426
+ return ToolsConfig(**tools_dict)
427
+
428
+
429
+ def load_config() -> Config:
430
+ """加载入口脚本同级 agentgrid.yaml 为 Config 对象。"""
431
+
432
+ agentgrid_path = _resolve_agentgrid_yaml_path()
433
+ if agentgrid_path is None or not agentgrid_path.exists():
434
+ return Config()
435
+
436
+ try:
437
+ with agentgrid_path.open("r", encoding="utf-8") as yaml_file:
438
+ raw_config = yaml.safe_load(yaml_file)
439
+ except OSError as exc:
440
+ raise ValueError(
441
+ f"Failed to read agentgrid.yaml at '{agentgrid_path}': {exc}"
442
+ ) from exc
443
+ except yaml.YAMLError as exc:
444
+ raise ValueError(
445
+ f"Failed to parse agentgrid.yaml at '{agentgrid_path}': {exc}"
446
+ ) from exc
447
+
448
+ if raw_config is None:
449
+ return Config()
450
+ if not isinstance(raw_config, Mapping):
451
+ raise ValueError(
452
+ f"Invalid agentgrid.yaml at '{agentgrid_path}': root must be a mapping object"
453
+ )
454
+
455
+ allowed_root_fields = {"basic", "runtime", "tools"}
456
+ _validate_mapping_fields(
457
+ agentgrid_path=agentgrid_path,
458
+ node_name="root",
459
+ mapping_obj=raw_config,
460
+ allowed_fields=allowed_root_fields,
461
+ )
462
+
463
+ return Config(
464
+ basic=_parse_basic_config(
465
+ agentgrid_path=agentgrid_path,
466
+ value=raw_config.get("basic"),
467
+ ),
468
+ runtime=_parse_runtime_config(
469
+ agentgrid_path=agentgrid_path,
470
+ value=raw_config.get("runtime"),
471
+ ),
472
+ tools=_parse_tools_config(
473
+ agentgrid_path=agentgrid_path,
474
+ value=raw_config.get("tools"),
475
+ ),
476
+ )
477
+
478
+
479
+ # 模块加载时初始化一次,全局可用。
480
+ settings = load_config()
481
+
482
+
483
+ def get_sdk_default_config() -> AgentGridConfig:
484
+ """获取 SDK 默认级配置。"""
485
+
486
+ basic_overrides = settings.basic.to_overrides()
487
+ if not basic_overrides:
488
+ return _SDK_DEFAULT_CONFIG
489
+
490
+ return _SDK_DEFAULT_CONFIG.with_overrides(**basic_overrides)
491
+
492
+
493
+ def set_sdk_default_config(config: AgentGridConfig) -> None:
494
+ """设置 SDK 默认级配置。"""
495
+
496
+ global _SDK_DEFAULT_CONFIG
497
+ _SDK_DEFAULT_CONFIG = config
498
+
499
+
500
+ def clear_sdk_default_config() -> None:
501
+ """清空 SDK 默认级配置,回退到内置默认值。"""
502
+
503
+ global _SDK_DEFAULT_CONFIG
504
+ _SDK_DEFAULT_CONFIG = _BUILTIN_SDK_DEFAULT_CONFIG
agentgrid/errors.py ADDED
@@ -0,0 +1,145 @@
1
+ """AgentGrid 统一异常定义与错误映射。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class AgentGridError(Exception):
9
+ """统一异常基类。"""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ *,
15
+ code: str | None = None,
16
+ request_id: str | None = None,
17
+ retryable: bool | None = None,
18
+ raw_error: Any | None = None,
19
+ cause: Exception | None = None,
20
+ ) -> None:
21
+ super().__init__(message)
22
+ self.code = code
23
+ self.request_id = request_id
24
+ self.retryable = retryable
25
+ self.raw_error = raw_error
26
+ self.cause = cause
27
+
28
+
29
+ class AgentGridAuthenticationError(AgentGridError):
30
+ """认证失败。"""
31
+
32
+
33
+ class AgentGridAuthorizationError(AgentGridError):
34
+ """鉴权失败。"""
35
+
36
+
37
+ class AgentGridRateLimitError(AgentGridError):
38
+ """限流错误。"""
39
+
40
+
41
+ class AgentGridTimeoutError(AgentGridError):
42
+ """超时错误。"""
43
+
44
+
45
+ class AgentGridTransportError(AgentGridError):
46
+ """传输错误。"""
47
+
48
+
49
+ class AgentGridStreamError(AgentGridError):
50
+ """流式处理错误。"""
51
+
52
+
53
+ class AgentGridServerError(AgentGridError):
54
+ """服务端错误。"""
55
+
56
+
57
+ def map_runtime_error_to_agentgrid_error(
58
+ error: Any,
59
+ *,
60
+ default_message: str,
61
+ ) -> AgentGridError:
62
+ """将底层错误 payload 统一映射到 AgentGrid 异常分层。"""
63
+
64
+ code, message, request_id, retryable, status = _extract_error_fields(error)
65
+ error_cls = _resolve_error_class(code=code, status=status)
66
+ return error_cls(
67
+ message or default_message,
68
+ code=code,
69
+ request_id=request_id,
70
+ retryable=retryable,
71
+ raw_error=error,
72
+ )
73
+
74
+
75
+ def _extract_error_fields(
76
+ error: Any,
77
+ ) -> tuple[str | None, str | None, str | None, bool | None, int | None]:
78
+ if not isinstance(error, dict):
79
+ return None, str(error), None, None, None
80
+
81
+ code = _as_optional_upper(error.get("code"))
82
+ message = _as_optional_str(error.get("message"))
83
+ request_id = _as_optional_str(error.get("request_id"))
84
+ retryable_value = error.get("retryable")
85
+ retryable = retryable_value if isinstance(retryable_value, bool) else None
86
+ status = _as_optional_int(error.get("status") or error.get("status_code"))
87
+ return code, message, request_id, retryable, status
88
+
89
+
90
+ def _resolve_error_class(*, code: str | None, status: int | None) -> type[AgentGridError]:
91
+ if code in {
92
+ "AUTHENTICATION_FAILED",
93
+ "INVALID_API_KEY",
94
+ "UNAUTHENTICATED",
95
+ "INVALID_SIGNATURE",
96
+ }:
97
+ return AgentGridAuthenticationError
98
+ if code in {"AUTHORIZATION_FAILED", "FORBIDDEN", "PERMISSION_DENIED"}:
99
+ return AgentGridAuthorizationError
100
+ if code in {"RATE_LIMITED", "TOO_MANY_REQUESTS"}:
101
+ return AgentGridRateLimitError
102
+ if code in {"TIMEOUT", "REQUEST_TIMEOUT", "GATEWAY_TIMEOUT"}:
103
+ return AgentGridTimeoutError
104
+ if code in {"TRANSPORT_ERROR", "NETWORK_ERROR", "CONNECTION_ERROR"}:
105
+ return AgentGridTransportError
106
+ if code in {"STREAM_ERROR", "STREAM_INTERRUPTED", "STREAM_METHOD_MISSING"}:
107
+ return AgentGridStreamError
108
+
109
+ if status == 401:
110
+ return AgentGridAuthenticationError
111
+ if status == 403:
112
+ return AgentGridAuthorizationError
113
+ if status == 429:
114
+ return AgentGridRateLimitError
115
+ if status in {408, 504}:
116
+ return AgentGridTimeoutError
117
+ if status is not None and 500 <= status <= 599:
118
+ return AgentGridServerError
119
+ return AgentGridServerError
120
+
121
+
122
+ def _as_optional_str(value: Any) -> str | None:
123
+ if value is None:
124
+ return None
125
+ return str(value)
126
+
127
+
128
+ def _as_optional_upper(value: Any) -> str | None:
129
+ parsed = _as_optional_str(value)
130
+ if parsed is None:
131
+ return None
132
+ return parsed.upper()
133
+
134
+
135
+ def _as_optional_int(value: Any) -> int | None:
136
+ if value is None:
137
+ return None
138
+ if isinstance(value, bool):
139
+ return None
140
+ if isinstance(value, int):
141
+ return value
142
+ try:
143
+ return int(str(value))
144
+ except ValueError:
145
+ return None
@@ -0,0 +1,4 @@
1
+ """扩展点命名空间。
2
+
3
+ v1 仅预留结构入口,不承诺完整产品能力。
4
+ """
@@ -0,0 +1,52 @@
1
+ """可观测性扩展点预留。
2
+
3
+ 对应 OpenSpec `extension-points-for-tools-and-observability`:
4
+ - 预留 `before_request`、`after_response`、`on_error` 三类 hook。
5
+ - v1 先提供可注册与触发的最小实现,便于后续观测能力平滑接入。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Callable
12
+
13
+ Hook = Callable[[dict[str, Any]], None]
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class ObservabilityHooks:
18
+ """请求生命周期 hook 容器。"""
19
+
20
+ before_request: list[Hook] = field(default_factory=list)
21
+ after_response: list[Hook] = field(default_factory=list)
22
+ on_error: list[Hook] = field(default_factory=list)
23
+
24
+ def register_before_request(self, hook: Hook) -> None:
25
+ self.before_request.append(hook)
26
+
27
+ def register_after_response(self, hook: Hook) -> None:
28
+ self.after_response.append(hook)
29
+
30
+ def register_on_error(self, hook: Hook) -> None:
31
+ self.on_error.append(hook)
32
+
33
+ def emit_before_request(self, payload: dict[str, Any]) -> None:
34
+ self._emit(self.before_request, payload)
35
+
36
+ def emit_after_response(self, payload: dict[str, Any]) -> None:
37
+ self._emit(self.after_response, payload)
38
+
39
+ def emit_on_error(self, payload: dict[str, Any]) -> None:
40
+ self._emit(self.on_error, payload)
41
+
42
+ @staticmethod
43
+ def _emit(hooks: list[Hook], payload: dict[str, Any]) -> None:
44
+ # hooks 是观测扩展点,任何单个 hook 异常都不应破坏主业务链路。
45
+ for hook in hooks:
46
+ try:
47
+ hook(payload)
48
+ except Exception:
49
+ continue
50
+
51
+
52
+ __all__ = ["ObservabilityHooks", "Hook"]