zero-agent-core 0.2.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 (104) hide show
  1. agent_core/__init__.py +525 -0
  2. agent_core/access/__init__.py +6 -0
  3. agent_core/access/context.py +45 -0
  4. agent_core/access/guard.py +40 -0
  5. agent_core/acp/README.md +236 -0
  6. agent_core/acp/__init__.py +20 -0
  7. agent_core/acp/server.py +351 -0
  8. agent_core/acp/types.py +86 -0
  9. agent_core/callbacks/__init__.py +17 -0
  10. agent_core/callbacks/manager.py +105 -0
  11. agent_core/checkpoint/__init__.py +19 -0
  12. agent_core/checkpoint/naming.py +93 -0
  13. agent_core/checkpoint/store.py +186 -0
  14. agent_core/checkpoint/travel.py +448 -0
  15. agent_core/compaction/__init__.py +57 -0
  16. agent_core/compaction/middleware.py +428 -0
  17. agent_core/compaction/prompts.py +32 -0
  18. agent_core/compaction/pruner.py +121 -0
  19. agent_core/compaction/region.py +145 -0
  20. agent_core/compaction/spill.py +81 -0
  21. agent_core/compaction/spill_tool.py +42 -0
  22. agent_core/compaction/summarizer.py +95 -0
  23. agent_core/compaction/token_limit.py +74 -0
  24. agent_core/compaction/types.py +93 -0
  25. agent_core/docs.py +15 -0
  26. agent_core/documents/__init__.py +23 -0
  27. agent_core/documents/loaders.py +79 -0
  28. agent_core/documents/splitters.py +94 -0
  29. agent_core/documents/types.py +197 -0
  30. agent_core/errors.py +81 -0
  31. agent_core/guardrails/__init__.py +19 -0
  32. agent_core/guardrails/core.py +186 -0
  33. agent_core/hitl/__init__.py +15 -0
  34. agent_core/hitl/core.py +517 -0
  35. agent_core/mcp/__init__.py +14 -0
  36. agent_core/mcp/client.py +212 -0
  37. agent_core/mcp/errors.py +26 -0
  38. agent_core/middleware/__init__.py +26 -0
  39. agent_core/middleware/base.py +283 -0
  40. agent_core/model/__init__.py +73 -0
  41. agent_core/model/catalog.py +73 -0
  42. agent_core/model/core.py +179 -0
  43. agent_core/model/governance.py +317 -0
  44. agent_core/model/providers/__init__.py +43 -0
  45. agent_core/model/providers/anthropic.py +294 -0
  46. agent_core/model/providers/common.py +28 -0
  47. agent_core/model/providers/factory.py +107 -0
  48. agent_core/model/providers/gemini.py +273 -0
  49. agent_core/model/providers/ollama.py +80 -0
  50. agent_core/model/providers/openai_compatible.py +526 -0
  51. agent_core/model/structured.py +200 -0
  52. agent_core/observability/__init__.py +21 -0
  53. agent_core/observability/core.py +311 -0
  54. agent_core/ports/__init__.py +31 -0
  55. agent_core/ports/memory.py +130 -0
  56. agent_core/ports/model_selection.py +64 -0
  57. agent_core/ports/storage.py +114 -0
  58. agent_core/prompts/__init__.py +19 -0
  59. agent_core/prompts/registry.py +236 -0
  60. agent_core/prompts/templates.py +166 -0
  61. agent_core/protocol/__init__.py +31 -0
  62. agent_core/protocol/capabilities.py +49 -0
  63. agent_core/protocol/messages.py +100 -0
  64. agent_core/protocol/runtime.py +293 -0
  65. agent_core/py.typed +1 -0
  66. agent_core/retrieval/__init__.py +40 -0
  67. agent_core/retrieval/_common.py +121 -0
  68. agent_core/retrieval/chroma.py +269 -0
  69. agent_core/retrieval/core.py +63 -0
  70. agent_core/retrieval/factory.py +72 -0
  71. agent_core/retrieval/memory.py +232 -0
  72. agent_core/retrieval/pgvector.py +239 -0
  73. agent_core/retrieval/types.py +95 -0
  74. agent_core/runtime/__init__.py +28 -0
  75. agent_core/runtime/compat.py +98 -0
  76. agent_core/runtime/react.py +888 -0
  77. agent_core/runtime/service.py +557 -0
  78. agent_core/runtime/store.py +55 -0
  79. agent_core/serialization/__init__.py +21 -0
  80. agent_core/serialization/registry.py +318 -0
  81. agent_core/skills/__init__.py +24 -0
  82. agent_core/skills/errors.py +27 -0
  83. agent_core/skills/loader.py +258 -0
  84. agent_core/skills/registry.py +205 -0
  85. agent_core/skills/types.py +56 -0
  86. agent_core/storage/__init__.py +107 -0
  87. agent_core/storage/_sqlite_utils.py +92 -0
  88. agent_core/storage/context.py +382 -0
  89. agent_core/storage/lease.py +435 -0
  90. agent_core/storage/memory.py +604 -0
  91. agent_core/storage/model_selection.py +360 -0
  92. agent_core/storage/runtime.py +899 -0
  93. agent_core/storage/session.py +904 -0
  94. agent_core/storage/workflow.py +411 -0
  95. agent_core/tools/__init__.py +5 -0
  96. agent_core/tools/executor.py +306 -0
  97. agent_core/workflow/__init__.py +25 -0
  98. agent_core/workflow/durable.py +175 -0
  99. agent_core/workflow/state_machine.py +427 -0
  100. zero_agent_core-0.2.0.dist-info/METADATA +237 -0
  101. zero_agent_core-0.2.0.dist-info/RECORD +104 -0
  102. zero_agent_core-0.2.0.dist-info/WHEEL +4 -0
  103. zero_agent_core-0.2.0.dist-info/entry_points.txt +3 -0
  104. zero_agent_core-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,517 @@
1
+ """通用人机协同审批队列。
2
+
3
+ Core 只负责审批状态机和异步等待,不负责 HTTP 路由、认证或具体业务动作。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import logging
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from datetime import UTC, datetime, timedelta
13
+ from enum import Enum
14
+ from typing import Any, Literal
15
+
16
+ from agent_core.access import AccessContext
17
+ from agent_core.middleware.base import (
18
+ Middleware,
19
+ MiddlewareAction,
20
+ MiddlewareContext,
21
+ MiddlewareResult,
22
+ )
23
+ from agent_core.ports import ApprovalStore
24
+ from agent_core.protocol.runtime import ApprovalRecord
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class ApprovalStatus(Enum):
30
+ PENDING = "pending"
31
+ APPROVED = "approved"
32
+ REJECTED = "rejected"
33
+ TIMEOUT = "timeout"
34
+
35
+
36
+ @dataclass
37
+ class ApprovalRequest:
38
+ """一个等待外部决定的动作请求。"""
39
+
40
+ request_id: str
41
+ thread_id: str
42
+ tool_name: str
43
+ tool_args: dict[str, Any]
44
+ run_id: str = ""
45
+ user_id: str | None = None
46
+ tenant_id: str | None = None
47
+ status: ApprovalStatus = ApprovalStatus.PENDING
48
+ reason: str | None = None
49
+ expires_at: str | None = None
50
+ _event: asyncio.Event = field(default_factory=asyncio.Event, repr=False)
51
+
52
+ @property
53
+ def approval_id(self) -> str:
54
+ return self.request_id
55
+
56
+ def to_record(self) -> ApprovalRecord:
57
+ status_by_request: dict[
58
+ ApprovalStatus,
59
+ Literal["pending", "approved", "rejected", "expired"],
60
+ ] = {
61
+ ApprovalStatus.PENDING: "pending",
62
+ ApprovalStatus.APPROVED: "approved",
63
+ ApprovalStatus.REJECTED: "rejected",
64
+ ApprovalStatus.TIMEOUT: "expired",
65
+ }
66
+ return ApprovalRecord(
67
+ approval_id=self.request_id,
68
+ thread_id=self.thread_id,
69
+ run_id=self.run_id,
70
+ action=self.tool_name,
71
+ arguments=dict(self.tool_args),
72
+ user_id=self.user_id,
73
+ tenant_id=self.tenant_id,
74
+ status=status_by_request[self.status],
75
+ reason=self.reason,
76
+ expires_at=self.expires_at,
77
+ )
78
+
79
+ def to_dict(self) -> dict[str, Any]:
80
+ return {
81
+ "request_id": self.request_id,
82
+ "thread_id": self.thread_id,
83
+ "run_id": self.run_id,
84
+ "user_id": self.user_id,
85
+ "tenant_id": self.tenant_id,
86
+ "tool_name": self.tool_name,
87
+ "tool_args": self.tool_args,
88
+ "status": self.status.value,
89
+ "reason": self.reason,
90
+ "expires_at": self.expires_at,
91
+ }
92
+
93
+ @classmethod
94
+ def from_record(cls, record: ApprovalRecord) -> ApprovalRequest:
95
+ """从持久化记录恢复审批请求。"""
96
+ request = cls(
97
+ request_id=record.approval_id,
98
+ thread_id=record.thread_id,
99
+ run_id=record.run_id,
100
+ tool_name=record.action,
101
+ tool_args=dict(record.arguments),
102
+ user_id=record.user_id,
103
+ tenant_id=record.tenant_id,
104
+ status={
105
+ "pending": ApprovalStatus.PENDING,
106
+ "approved": ApprovalStatus.APPROVED,
107
+ "rejected": ApprovalStatus.REJECTED,
108
+ "expired": ApprovalStatus.TIMEOUT,
109
+ }.get(record.status, ApprovalStatus.PENDING),
110
+ reason=record.reason,
111
+ expires_at=record.expires_at,
112
+ )
113
+ if request.status != ApprovalStatus.PENDING:
114
+ request._event.set()
115
+ return request
116
+
117
+
118
+ class ApprovalQueue:
119
+ """进程内等待器,可选地把状态写入 ``ApprovalStore``。"""
120
+
121
+ def __init__(
122
+ self,
123
+ approval_store: ApprovalStore | None = None,
124
+ *,
125
+ runtime_store: ApprovalStore | None = None,
126
+ poll_interval_seconds: float = 0.5,
127
+ require_access: bool = False,
128
+ ) -> None:
129
+ # runtime_store 是旧 Vanilla 命名,保留它避免升级时破坏调用方。
130
+ if approval_store is not None and runtime_store is not None:
131
+ raise ValueError("approval_store 和 runtime_store 只能配置一个")
132
+ self._requests: dict[str, ApprovalRequest] = {}
133
+ self._approval_store = approval_store or runtime_store
134
+ self._require_access = require_access
135
+ if poll_interval_seconds <= 0:
136
+ raise ValueError("poll_interval_seconds 必须大于 0")
137
+ self._poll_interval = poll_interval_seconds
138
+
139
+ def create_request(
140
+ self,
141
+ thread_id: str,
142
+ tool_name: str,
143
+ tool_args: dict[str, Any],
144
+ *,
145
+ run_id: str = "",
146
+ user_id: str | None = None,
147
+ tenant_id: str | None = None,
148
+ timeout_seconds: float | None = None,
149
+ ) -> ApprovalRequest:
150
+ """创建仅驻留当前进程的审批请求。
151
+
152
+ 配置持久化存储后必须使用 ``create_request_async``,确保方法返回时初始审批
153
+ 已写入存储,避免进程退出或后台任务失败造成记录丢失。
154
+ """
155
+ self._require_owner(user_id, tenant_id)
156
+ if self._approval_store is not None:
157
+ raise RuntimeError("配置审批存储后必须使用 await create_request_async()")
158
+ request = ApprovalRequest(
159
+ request_id=str(uuid.uuid4()),
160
+ thread_id=thread_id,
161
+ tool_name=tool_name,
162
+ tool_args=dict(tool_args),
163
+ run_id=run_id,
164
+ user_id=user_id,
165
+ tenant_id=tenant_id,
166
+ expires_at=self._expires_at(timeout_seconds),
167
+ )
168
+ self._requests[request.request_id] = request
169
+ return request
170
+
171
+ async def create_request_async(
172
+ self,
173
+ thread_id: str,
174
+ tool_name: str,
175
+ tool_args: dict[str, Any],
176
+ *,
177
+ run_id: str = "",
178
+ user_id: str | None = None,
179
+ tenant_id: str | None = None,
180
+ timeout_seconds: float | None = None,
181
+ ) -> ApprovalRequest:
182
+ """创建请求并等待持久化完成,适合生产执行路径。"""
183
+ self._require_owner(user_id, tenant_id)
184
+ request = ApprovalRequest(
185
+ request_id=str(uuid.uuid4()),
186
+ thread_id=thread_id,
187
+ tool_name=tool_name,
188
+ tool_args=dict(tool_args),
189
+ run_id=run_id,
190
+ user_id=user_id,
191
+ tenant_id=tenant_id,
192
+ expires_at=self._expires_at(timeout_seconds),
193
+ )
194
+ self._requests[request.request_id] = request
195
+ if self._approval_store:
196
+ await self._approval_store.save_approval(request.to_record())
197
+ return request
198
+
199
+ async def wait_for_decision(
200
+ self,
201
+ request: ApprovalRequest,
202
+ timeout_seconds: float = 300.0,
203
+ ) -> ApprovalStatus:
204
+ if timeout_seconds <= 0:
205
+ raise ValueError("timeout_seconds 必须大于 0")
206
+ loop = asyncio.get_running_loop()
207
+ if request.expires_at is None:
208
+ request.expires_at = self._expires_at(timeout_seconds)
209
+ remaining_lifetime = self._remaining_seconds(request.expires_at)
210
+ deadline = loop.time() + min(timeout_seconds, max(0.0, remaining_lifetime))
211
+ if self._approval_store is not None:
212
+ # pending -> pending 也是条件更新,用于持久化 deadline,不能覆盖已经完成的决定。
213
+ updated = await self._transition(request.to_record())
214
+ if not updated:
215
+ stored = await self._approval_store.load_approval(request.request_id)
216
+ if stored is not None:
217
+ self._apply_record(request, stored)
218
+ try:
219
+ while request.status == ApprovalStatus.PENDING:
220
+ if self._approval_store is not None:
221
+ stored = await self._approval_store.load_approval(request.request_id)
222
+ if stored is not None and stored.status != "pending":
223
+ restored = ApprovalRequest.from_record(stored)
224
+ request.status = restored.status
225
+ request.reason = restored.reason
226
+ request._event.set()
227
+ break
228
+ remaining = deadline - loop.time()
229
+ if remaining <= 0:
230
+ break
231
+ try:
232
+ await asyncio.wait_for(
233
+ request._event.wait(),
234
+ timeout=min(self._poll_interval, remaining),
235
+ )
236
+ except TimeoutError:
237
+ continue
238
+ if request.status != ApprovalStatus.PENDING:
239
+ return request.status
240
+ if self._approval_store is None:
241
+ request.status = ApprovalStatus.TIMEOUT
242
+ return request.status
243
+
244
+ expired = request.to_record()
245
+ expired.status = "expired"
246
+ if await self._transition(expired):
247
+ request.status = ApprovalStatus.TIMEOUT
248
+ request._event.set()
249
+ return request.status
250
+
251
+ # 决定可能刚好在 deadline 到达时提交;以持久化终态为准。
252
+ stored = await self._approval_store.load_approval(request.request_id)
253
+ if stored is not None:
254
+ self._apply_record(request, stored)
255
+ return request.status
256
+ # 记录不存在(幻影请求):不可能再有决定提交,按超时收口而不是返回 PENDING。
257
+ request.status = ApprovalStatus.TIMEOUT
258
+ request._event.set()
259
+ return request.status
260
+ finally:
261
+ self._requests.pop(request.request_id, None)
262
+
263
+ async def approve(
264
+ self,
265
+ request_id: str,
266
+ *,
267
+ access: AccessContext | None = None,
268
+ ) -> bool:
269
+ request = await self.load_request(request_id, access=access)
270
+ if request is None or request.status != ApprovalStatus.PENDING:
271
+ return False
272
+ if self._approval_store:
273
+ approved = request.to_record()
274
+ approved.status = "approved"
275
+ if not await self._transition(approved):
276
+ return False
277
+ request.status = ApprovalStatus.APPROVED
278
+ request._event.set()
279
+ return True
280
+
281
+ async def reject(
282
+ self,
283
+ request_id: str,
284
+ reason: str = "用户拒绝",
285
+ *,
286
+ access: AccessContext | None = None,
287
+ ) -> bool:
288
+ request = await self.load_request(request_id, access=access)
289
+ if request is None or request.status != ApprovalStatus.PENDING:
290
+ return False
291
+ if self._approval_store:
292
+ rejected = request.to_record()
293
+ rejected.status = "rejected"
294
+ rejected.reason = reason
295
+ if not await self._transition(rejected):
296
+ return False
297
+ request.status = ApprovalStatus.REJECTED
298
+ request.reason = reason
299
+ request._event.set()
300
+ return True
301
+
302
+ def list_pending(
303
+ self,
304
+ *,
305
+ access: AccessContext | None = None,
306
+ ) -> list[dict[str, Any]]:
307
+ if self._require_access and access is None:
308
+ raise PermissionError("该 ApprovalQueue 要求提供 AccessContext")
309
+ return [
310
+ request.to_dict()
311
+ for request in self._requests.values()
312
+ if request.status == ApprovalStatus.PENDING
313
+ and not (request.expires_at and self._is_past(request.expires_at))
314
+ and (access is None or access.can_access(request.user_id, request.tenant_id))
315
+ ]
316
+
317
+ def get_request(
318
+ self,
319
+ request_id: str,
320
+ *,
321
+ access: AccessContext | None = None,
322
+ ) -> ApprovalRequest | None:
323
+ request = self._requests.get(request_id)
324
+ if request is not None:
325
+ self._check_access(request, access)
326
+ elif self._require_access and access is None:
327
+ raise PermissionError("该 ApprovalQueue 要求提供 AccessContext")
328
+ return request
329
+
330
+ async def load_request(
331
+ self,
332
+ request_id: str,
333
+ *,
334
+ access: AccessContext | None = None,
335
+ ) -> ApprovalRequest | None:
336
+ """优先读取进程内请求,否则从持久化记录恢复。"""
337
+ request = self._requests.get(request_id)
338
+ if request is not None:
339
+ await self._expire_if_due(request)
340
+ self._check_access(request, access)
341
+ return request
342
+ if self._require_access and access is None:
343
+ raise PermissionError("该 ApprovalQueue 要求提供 AccessContext")
344
+ if self._approval_store is None:
345
+ return None
346
+ record = await self._approval_store.load_approval(request_id)
347
+ if record is None:
348
+ return None
349
+ request = ApprovalRequest.from_record(record)
350
+ await self._expire_if_due(request)
351
+ self._check_access(request, access)
352
+ self._requests[request_id] = request
353
+ return request
354
+
355
+ async def _expire_if_due(self, request: ApprovalRequest) -> None:
356
+ """对已过期的 pending 请求执行过期收口;配置存储时以 CAS 结果为准。"""
357
+ if request.status is not ApprovalStatus.PENDING or not request.expires_at:
358
+ return
359
+ if not self._is_past(request.expires_at):
360
+ return
361
+ if self._approval_store is None:
362
+ request.status = ApprovalStatus.TIMEOUT
363
+ request._event.set()
364
+ return
365
+ expired = request.to_record()
366
+ expired.status = "expired"
367
+ if await self._transition(expired):
368
+ request.status = ApprovalStatus.TIMEOUT
369
+ request._event.set()
370
+ else:
371
+ latest = await self._approval_store.load_approval(request.request_id)
372
+ if latest is not None:
373
+ self._apply_record(request, latest)
374
+
375
+ async def _transition(self, record: ApprovalRecord) -> bool:
376
+ """执行审批 CAS;兼容尚未升级条件更新接口的外部 Store。
377
+
378
+ 内置 Memory/SQLite/PostgreSQL Store 的 ``transition_approval`` 是原子条件更新。
379
+ 未实现该接口的外部 Store 会退化为 load→check→save,跨进程并发下存在
380
+ last-writer-wins 窗口;生产环境应让外部 Store 实现 ``transition_approval``。
381
+ """
382
+ if self._approval_store is None:
383
+ return False
384
+ transition = getattr(self._approval_store, "transition_approval", None)
385
+ if transition is not None:
386
+ return bool(await transition(record, expected_status="pending"))
387
+ current = await self._approval_store.load_approval(record.approval_id)
388
+ if current is None or current.status != "pending":
389
+ return False
390
+ await self._approval_store.save_approval(record)
391
+ return True
392
+
393
+ @staticmethod
394
+ def _apply_record(request: ApprovalRequest, record: ApprovalRecord) -> None:
395
+ restored = ApprovalRequest.from_record(record)
396
+ request.status = restored.status
397
+ request.reason = restored.reason
398
+ request.expires_at = restored.expires_at
399
+ if request.status != ApprovalStatus.PENDING:
400
+ request._event.set()
401
+
402
+ @staticmethod
403
+ def _record_expired(record: ApprovalRecord) -> bool:
404
+ return ApprovalQueue._is_past(record.expires_at)
405
+
406
+ @staticmethod
407
+ def _is_past(expires_at: str | None) -> bool:
408
+ if not expires_at:
409
+ return False
410
+ expires = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
411
+ if expires.tzinfo is None:
412
+ expires = expires.replace(tzinfo=UTC)
413
+ return expires <= datetime.now(UTC)
414
+
415
+ @staticmethod
416
+ def _expires_at(timeout_seconds: float | None) -> str | None:
417
+ if timeout_seconds is None:
418
+ return None
419
+ if timeout_seconds <= 0:
420
+ raise ValueError("timeout_seconds 必须大于 0")
421
+ return (datetime.now(UTC) + timedelta(seconds=timeout_seconds)).isoformat()
422
+
423
+ @staticmethod
424
+ def _remaining_seconds(expires_at: str | None) -> float:
425
+ if expires_at is None:
426
+ return float("inf")
427
+ parsed = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
428
+ if parsed.tzinfo is None:
429
+ parsed = parsed.replace(tzinfo=UTC)
430
+ return (parsed - datetime.now(UTC)).total_seconds()
431
+
432
+ def _check_access(
433
+ self,
434
+ request: ApprovalRequest,
435
+ access: AccessContext | None,
436
+ ) -> None:
437
+ if self._require_access and access is None:
438
+ raise PermissionError("该 ApprovalQueue 要求提供 AccessContext")
439
+ if self._require_access and (request.user_id is None or request.tenant_id is None):
440
+ raise PermissionError("该审批请求尚未绑定用户和租户")
441
+ if access is not None and not access.can_access(request.user_id, request.tenant_id):
442
+ raise PermissionError("无权处理该审批请求")
443
+
444
+ def _require_owner(
445
+ self,
446
+ user_id: str | None,
447
+ tenant_id: str | None,
448
+ ) -> None:
449
+ if self._require_access and (user_id is None or tenant_id is None):
450
+ raise PermissionError("严格访问模式要求审批绑定 user_id 和 tenant_id")
451
+
452
+
453
+ class HumanInTheLoopMiddleware(Middleware):
454
+ """在工具执行前暂停,等待审批队列返回决定。"""
455
+
456
+ def __init__(
457
+ self,
458
+ queue: ApprovalQueue | None = None,
459
+ approval_needed: list[str] | None = None,
460
+ thread_id: str = "default",
461
+ auto_approve: bool = False,
462
+ approval_callback: Any | None = None,
463
+ timeout_seconds: float = 300.0,
464
+ ) -> None:
465
+ self._queue = queue or ApprovalQueue()
466
+ self._needed = set(approval_needed or ())
467
+ self._thread = thread_id
468
+ self._auto = auto_approve
469
+ self._callback = approval_callback
470
+ self._timeout = timeout_seconds
471
+
472
+ async def before_tool(self, ctx: MiddlewareContext) -> MiddlewareResult:
473
+ if ctx.tool_name not in self._needed or self._auto:
474
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
475
+ if self._callback is not None:
476
+ try:
477
+ approved = await asyncio.wait_for(
478
+ self._callback(ctx.tool_name, ctx.tool_args),
479
+ timeout=self._timeout,
480
+ )
481
+ except TimeoutError:
482
+ return MiddlewareResult(
483
+ action=MiddlewareAction.STOP,
484
+ data={"reason": "approval_timeout", "tool": ctx.tool_name},
485
+ )
486
+ if approved:
487
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
488
+ return MiddlewareResult(
489
+ action=MiddlewareAction.STOP,
490
+ data={"reason": "approval_denied", "tool": ctx.tool_name},
491
+ )
492
+ request = await self._queue.create_request_async(
493
+ thread_id=str(ctx.metadata.get("thread_id") or self._thread),
494
+ tool_name=ctx.tool_name or "unknown",
495
+ tool_args=dict(ctx.tool_args),
496
+ run_id=str(ctx.metadata.get("run_id") or ""),
497
+ user_id=ctx.metadata.get("user_id"),
498
+ tenant_id=ctx.metadata.get("tenant_id"),
499
+ timeout_seconds=self._timeout,
500
+ )
501
+ ctx.metadata["approval_request_id"] = request.request_id
502
+ status = await self._queue.wait_for_decision(request, timeout_seconds=self._timeout)
503
+ if status == ApprovalStatus.APPROVED:
504
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
505
+ reason = request.reason or status.value
506
+ return MiddlewareResult(
507
+ action=MiddlewareAction.STOP,
508
+ data={"reason": reason, "tool": ctx.tool_name, "status": status.value},
509
+ )
510
+
511
+
512
+ __all__ = [
513
+ "ApprovalQueue",
514
+ "ApprovalRequest",
515
+ "ApprovalStatus",
516
+ "HumanInTheLoopMiddleware",
517
+ ]
@@ -0,0 +1,14 @@
1
+ """MCP 客户端协议、stdio 适配器和通用错误。"""
2
+
3
+ from agent_core.mcp.client import JsonObject, McpToolCaller, McpToolClient
4
+ from agent_core.mcp.errors import McpConnectionError, McpError, McpTimeoutError, McpToolError
5
+
6
+ __all__ = [
7
+ "JsonObject",
8
+ "McpConnectionError",
9
+ "McpError",
10
+ "McpTimeoutError",
11
+ "McpToolCaller",
12
+ "McpToolClient",
13
+ "McpToolError",
14
+ ]