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,557 @@
1
+ """通用 Agent Runtime 生命周期服务。
2
+
3
+ Runtime 负责运行实例和生命周期,业务编排仍由调用方注入到工具或上层服务中。
4
+ 这样 API 层不需要重复实现 start、resume、cancel 和事件发布逻辑。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import uuid
12
+ from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Sequence
13
+ from dataclasses import dataclass
14
+ from typing import Any, Protocol
15
+
16
+ from agent_core.access import AccessContext, enforce_access
17
+ from agent_core.callbacks import CallbackHandler, CallbackManager
18
+ from agent_core.ports import EventSink, RunStore
19
+ from agent_core.protocol.runtime import RunContext, RunEvent
20
+ from agent_core.runtime.react import ReActAgent
21
+ from agent_core.runtime.store import MemoryRunStore
22
+ from agent_core.storage.lease import RunLeaseStore
23
+ from agent_core.storage.runtime import RuntimeConcurrencyError
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class RunExecutor(Protocol):
29
+ """自定义 Run 执行器:在 Run 生命周期内替代默认的 ``agent.run`` 调用。
30
+
31
+ Runtime 会在挂接执行器前完成状态置位、``run_started`` 事件、checkpoint
32
+ 回调绑定和持久化;执行器只需驱动具体的 Agent 实现并返回最终回答。
33
+ 应用可用它接入按模型快照选择 Agent、业务编排分支等自定义执行策略。
34
+ """
35
+
36
+ async def __call__(
37
+ self,
38
+ context: RunContext,
39
+ user_input: str,
40
+ *,
41
+ memory_context: str | None = None,
42
+ checkpoint_state: dict[str, Any] | None = None,
43
+ ) -> str: ...
44
+
45
+
46
+ class MemoryEventSink(EventSink):
47
+ """测试和开发环境使用的事件接收器。"""
48
+
49
+ def __init__(self) -> None:
50
+ self.events: list[RunEvent] = []
51
+
52
+ async def publish(self, event: RunEvent) -> None:
53
+ self.events.append(event)
54
+
55
+
56
+ @dataclass
57
+ class RuntimeRun:
58
+ """运行句柄,供需要后台任务的 API 层使用。"""
59
+
60
+ context: RunContext
61
+ task: asyncio.Task[str] | None = None
62
+
63
+
64
+ class AgentRuntime:
65
+ """围绕 ``ReActAgent`` 的通用生命周期门面。"""
66
+
67
+ def __init__(
68
+ self,
69
+ agent: ReActAgent,
70
+ *,
71
+ run_store: RunStore | None = None,
72
+ event_sink: EventSink | None = None,
73
+ callbacks: Sequence[CallbackHandler] | None = None,
74
+ callback_manager: CallbackManager | None = None,
75
+ lease_store: RunLeaseStore | None = None,
76
+ worker_id: str | None = None,
77
+ lease_seconds: float = 30.0,
78
+ heartbeat_seconds: float = 10.0,
79
+ require_access: bool = False,
80
+ run_executor: RunExecutor | None = None,
81
+ ) -> None:
82
+ if lease_seconds <= 0:
83
+ raise ValueError("lease_seconds 必须大于 0")
84
+ if heartbeat_seconds <= 0 or heartbeat_seconds >= lease_seconds:
85
+ raise ValueError("heartbeat_seconds 必须大于 0 且小于 lease_seconds")
86
+ if callbacks is not None and callback_manager is not None:
87
+ raise ValueError("callbacks 和 callback_manager 不能同时传入")
88
+ self._agent = agent
89
+ self._run_store = run_store or MemoryRunStore()
90
+ self._event_sink = event_sink
91
+ self._callback_manager = callback_manager or CallbackManager(callbacks)
92
+ self._lease_store = lease_store
93
+ self._worker_id = worker_id or f"worker-{uuid.uuid4()}"
94
+ self._lease_seconds = lease_seconds
95
+ self._heartbeat_seconds = heartbeat_seconds
96
+ self._require_access = require_access
97
+ self._run_executor = run_executor
98
+ self._tasks: dict[str, asyncio.Task[str]] = {}
99
+ self._event_cursors: dict[str, int] = {}
100
+ self._lock = asyncio.Lock()
101
+
102
+ async def start(
103
+ self,
104
+ thread_id: str,
105
+ user_input: str,
106
+ *,
107
+ user_id: str | None = None,
108
+ tenant_id: str | None = None,
109
+ parent_run_id: str | None = None,
110
+ tags: Sequence[str] | None = None,
111
+ idempotency_key: str | None = None,
112
+ memory_context: str | None = None,
113
+ metadata: dict[str, Any] | None = None,
114
+ checkpoint_state: dict[str, Any] | None = None,
115
+ ) -> RuntimeRun:
116
+ """创建后台运行实例并立即返回句柄。"""
117
+ self._require_owner(user_id, tenant_id)
118
+ async with self._lock:
119
+ if idempotency_key:
120
+ existing = await self._run_store.find_run_by_idempotency(tenant_id, user_id, idempotency_key)
121
+ if existing is not None:
122
+ task = self._tasks.get(existing.run_id)
123
+ return RuntimeRun(context=existing, task=task)
124
+
125
+ context = RunContext(
126
+ thread_id=thread_id,
127
+ run_id=str(uuid.uuid4()),
128
+ user_id=user_id,
129
+ tenant_id=tenant_id,
130
+ parent_run_id=parent_run_id,
131
+ tags=list(tags or ()),
132
+ idempotency_key=idempotency_key,
133
+ metadata={
134
+ **(metadata or {}),
135
+ "_input": user_input,
136
+ },
137
+ checkpoint=checkpoint_state or {},
138
+ )
139
+ await self._run_store.save_run(context)
140
+ await self._claim(context)
141
+ task = asyncio.create_task(
142
+ self._run_with_lease(
143
+ context,
144
+ self._execute(
145
+ context,
146
+ user_input,
147
+ memory_context=memory_context,
148
+ checkpoint_state=checkpoint_state,
149
+ ),
150
+ )
151
+ )
152
+ self._tasks[context.run_id] = task
153
+ task.add_done_callback(lambda _: self._cleanup_run(context.run_id))
154
+ return RuntimeRun(context=context, task=task)
155
+
156
+ async def run(
157
+ self,
158
+ thread_id: str,
159
+ user_input: str,
160
+ **kwargs: Any,
161
+ ) -> RunContext:
162
+ """同步等待一次完整运行并返回最终上下文。"""
163
+ handle = await self.start(thread_id, user_input, **kwargs)
164
+ access = self._context_access(handle.context)
165
+ await self.wait(
166
+ handle.context.thread_id,
167
+ handle.context.run_id,
168
+ access=access,
169
+ )
170
+ return await self.get(
171
+ handle.context.thread_id,
172
+ handle.context.run_id,
173
+ access=access,
174
+ )
175
+
176
+ async def wait(
177
+ self,
178
+ thread_id: str,
179
+ run_id: str,
180
+ *,
181
+ access: AccessContext | None = None,
182
+ ) -> RunContext:
183
+ """等待进程内任务;进程重启后可读取持久化状态。"""
184
+ context = await self._run_store.load_run(thread_id, run_id)
185
+ if context is None:
186
+ raise KeyError(f"run 不存在:{run_id}")
187
+ self._check_access(context, access)
188
+ task = self._tasks.get(run_id)
189
+ if task is not None:
190
+ await asyncio.shield(task)
191
+ context = await self._run_store.load_run(thread_id, run_id)
192
+ if context is None:
193
+ raise KeyError(f"run 不存在:{run_id}")
194
+ self._check_access(context, access)
195
+ return context
196
+
197
+ async def get(
198
+ self,
199
+ thread_id: str,
200
+ run_id: str,
201
+ *,
202
+ access: AccessContext | None = None,
203
+ ) -> RunContext:
204
+ """读取运行上下文。"""
205
+ context = await self._run_store.load_run(thread_id, run_id)
206
+ if context is None:
207
+ raise KeyError(f"run 不存在:{run_id}")
208
+ self._check_access(context, access)
209
+ return context
210
+
211
+ async def resume(
212
+ self,
213
+ thread_id: str,
214
+ run_id: str,
215
+ *,
216
+ access: AccessContext | None = None,
217
+ ) -> RuntimeRun:
218
+ """从已有 checkpoint 继续运行同一个 run。"""
219
+ async with self._lock:
220
+ if run_id in self._tasks:
221
+ return RuntimeRun(
222
+ context=await self.get(thread_id, run_id, access=access),
223
+ task=self._tasks[run_id],
224
+ )
225
+ context = await self.get(thread_id, run_id, access=access)
226
+ if not context.checkpoint:
227
+ raise ValueError("该 run 没有可恢复的 checkpoint")
228
+ if context.status in {"completed", "cancelled"}:
229
+ raise ValueError(f"状态为 {context.status} 的 run 不能恢复")
230
+ self._event_cursors.setdefault(context.run_id, len(context.events))
231
+ await self._claim(context)
232
+ try:
233
+ context.status = "queued"
234
+ context.emit("run_resumed")
235
+ await self._persist(context)
236
+ except Exception:
237
+ await self._release(context)
238
+ raise
239
+ task = asyncio.create_task(
240
+ self._run_with_lease(
241
+ context,
242
+ self._execute(
243
+ context,
244
+ "",
245
+ checkpoint_state=context.checkpoint,
246
+ ),
247
+ )
248
+ )
249
+ self._tasks[run_id] = task
250
+ task.add_done_callback(lambda _: self._cleanup_run(run_id))
251
+ return RuntimeRun(context=context, task=task)
252
+
253
+ async def cancel(
254
+ self,
255
+ thread_id: str,
256
+ run_id: str,
257
+ *,
258
+ access: AccessContext | None = None,
259
+ ) -> bool:
260
+ """取消进程内运行;已完成或只存在于数据库中的 run 不会被伪造修改。"""
261
+ context = await self.get(thread_id, run_id, access=access)
262
+ task = self._tasks.get(run_id)
263
+ if task is None or task.done():
264
+ return False
265
+ task.cancel()
266
+ gather_result = await asyncio.gather(task, return_exceptions=True)
267
+ for outcome in gather_result:
268
+ # 取消方不再吞掉业务异常;这里记录日志,避免取消失败与取消成功不可区分。
269
+ if isinstance(outcome, BaseException) and not isinstance(outcome, asyncio.CancelledError):
270
+ logger.warning("被取消的 run %s 在结束前抛出异常", run_id, exc_info=outcome)
271
+ context = await self.get(thread_id, run_id, access=access)
272
+ return context.status == "cancelled"
273
+
274
+ async def stream(
275
+ self,
276
+ thread_id: str,
277
+ user_input: str,
278
+ **kwargs: Any,
279
+ ) -> AsyncIterator[str]:
280
+ """以流式方式执行一次运行,并复用同一生命周期持久化逻辑。"""
281
+ context = RunContext(
282
+ thread_id=thread_id,
283
+ run_id=str(uuid.uuid4()),
284
+ user_id=kwargs.pop("user_id", None),
285
+ tenant_id=kwargs.pop("tenant_id", None),
286
+ parent_run_id=kwargs.pop("parent_run_id", None),
287
+ tags=list(kwargs.pop("tags", ())),
288
+ idempotency_key=kwargs.pop("idempotency_key", None),
289
+ metadata={**kwargs.pop("metadata", {}), "_input": user_input},
290
+ )
291
+ self._require_owner(context.user_id, context.tenant_id)
292
+ await self._run_store.save_run(context)
293
+ await self._claim(context)
294
+ context.status = "running"
295
+ context.emit("run_started", input=user_input)
296
+ context.checkpoint_callback = self._checkpoint_callback(context)
297
+ await self._persist(context)
298
+ lease_errors: list[BaseException] = []
299
+ heartbeat = self._start_stream_heartbeat(context, lease_errors)
300
+ try:
301
+ async for chunk in self._agent.stream(
302
+ user_input,
303
+ memory_context=kwargs.pop("memory_context", None),
304
+ thread_id=thread_id,
305
+ run_context=context,
306
+ checkpoint_state=kwargs.pop("checkpoint_state", None),
307
+ ):
308
+ context.emit("model_stream_chunk", component="model", delta=chunk)
309
+ await self._publish_pending_events(context)
310
+ yield chunk
311
+ except asyncio.CancelledError:
312
+ if not lease_errors:
313
+ raise
314
+ context.status = "interrupted"
315
+ context.emit(
316
+ "run_interrupted",
317
+ reason="worker_lease_lost",
318
+ error=str(lease_errors[0]),
319
+ )
320
+ raise RuntimeConcurrencyError(f"run {context.run_id} 的 Worker 租约已丢失") from lease_errors[0]
321
+ finally:
322
+ if heartbeat is not None:
323
+ heartbeat.cancel()
324
+ await asyncio.gather(heartbeat, return_exceptions=True)
325
+ await self._release(context)
326
+ await self._persist(context)
327
+ self._event_cursors.pop(context.run_id, None)
328
+
329
+ async def recover_expired_runs(self, *, auto_resume: bool = False) -> list[RunContext]:
330
+ """标记租约过期的 Run,并可从 checkpoint 自动恢复。"""
331
+ if self._lease_store is None:
332
+ return []
333
+ recovered: list[RunContext] = []
334
+ for lease in await self._lease_store.list_expired():
335
+ context = await self._run_store.load_run(lease.thread_id, lease.run_id)
336
+ await self._lease_store.release(lease.thread_id, lease.run_id, lease.worker_id)
337
+ if context is None or context.status not in {"queued", "running"}:
338
+ continue
339
+ self._event_cursors.setdefault(context.run_id, len(context.events))
340
+ context.status = "interrupted"
341
+ context.emit("run_interrupted", reason="worker_lease_expired")
342
+ await self._persist(context)
343
+ recovered.append(context)
344
+ if auto_resume and context.checkpoint:
345
+ await self.resume(
346
+ context.thread_id,
347
+ context.run_id,
348
+ access=self._context_access(context),
349
+ )
350
+ return recovered
351
+
352
+ async def _claim(self, context: RunContext) -> None:
353
+ if self._lease_store is None:
354
+ return
355
+ claimed = await self._lease_store.claim(
356
+ context.thread_id,
357
+ context.run_id,
358
+ self._worker_id,
359
+ ttl_seconds=self._lease_seconds,
360
+ )
361
+ if not claimed:
362
+ raise RuntimeConcurrencyError(f"run {context.run_id} 已被其他 Worker 认领")
363
+
364
+ async def _release(self, context: RunContext) -> None:
365
+ if self._lease_store is not None:
366
+ await self._lease_store.release(
367
+ context.thread_id,
368
+ context.run_id,
369
+ self._worker_id,
370
+ )
371
+
372
+ async def _heartbeat(self, context: RunContext) -> None:
373
+ assert self._lease_store is not None
374
+ while True:
375
+ await asyncio.sleep(self._heartbeat_seconds)
376
+ renewed = await self._lease_store.renew(
377
+ context.thread_id,
378
+ context.run_id,
379
+ self._worker_id,
380
+ ttl_seconds=self._lease_seconds,
381
+ )
382
+ if not renewed:
383
+ raise RuntimeConcurrencyError(f"run {context.run_id} 的 Worker 租约已丢失")
384
+
385
+ async def _run_with_lease(
386
+ self,
387
+ context: RunContext,
388
+ execution: Coroutine[Any, Any, str],
389
+ ) -> str:
390
+ if self._lease_store is None:
391
+ return await execution
392
+ execution_task = asyncio.create_task(execution)
393
+ heartbeat_task = asyncio.create_task(self._heartbeat(context))
394
+ try:
395
+ done, _ = await asyncio.wait(
396
+ {execution_task, heartbeat_task},
397
+ return_when=asyncio.FIRST_COMPLETED,
398
+ )
399
+ # 两个任务可能在同一轮事件循环同时完成;执行结果优先,避免丢失成功终态。
400
+ if execution_task in done:
401
+ return await execution_task
402
+ if heartbeat_task in done:
403
+ error = heartbeat_task.exception()
404
+ execution_task.cancel()
405
+ await asyncio.gather(execution_task, return_exceptions=True)
406
+ context.status = "interrupted"
407
+ context.emit(
408
+ "run_interrupted",
409
+ reason="worker_lease_lost",
410
+ error=str(error) if error is not None else None,
411
+ )
412
+ await self._persist(context)
413
+ if error is not None:
414
+ raise error
415
+ raise RuntimeConcurrencyError(f"run {context.run_id} 的 Worker 心跳意外结束")
416
+ return await execution_task
417
+ finally:
418
+ if not execution_task.done():
419
+ execution_task.cancel()
420
+ await asyncio.gather(execution_task, return_exceptions=True)
421
+ heartbeat_task.cancel()
422
+ await asyncio.gather(heartbeat_task, return_exceptions=True)
423
+ await self._release(context)
424
+
425
+ def _start_stream_heartbeat(
426
+ self,
427
+ context: RunContext,
428
+ errors: list[BaseException],
429
+ ) -> asyncio.Task[None] | None:
430
+ if self._lease_store is None:
431
+ return None
432
+ owner = asyncio.current_task()
433
+
434
+ async def monitor() -> None:
435
+ try:
436
+ await self._heartbeat(context)
437
+ except Exception as exc:
438
+ errors.append(exc)
439
+ context.emit("run_lease_lost", worker_id=self._worker_id)
440
+ if owner is not None:
441
+ owner.cancel()
442
+ raise
443
+
444
+ return asyncio.create_task(monitor())
445
+
446
+ def _check_access(
447
+ self,
448
+ context: RunContext,
449
+ access: AccessContext | None,
450
+ ) -> None:
451
+ enforce_access(
452
+ strict=self._require_access,
453
+ accessor="AgentRuntime",
454
+ subject="run",
455
+ owner_user_id=context.user_id,
456
+ owner_tenant_id=context.tenant_id,
457
+ access=access,
458
+ )
459
+
460
+ def _require_owner(
461
+ self,
462
+ user_id: str | None,
463
+ tenant_id: str | None,
464
+ ) -> None:
465
+ if self._require_access and (user_id is None or tenant_id is None):
466
+ raise PermissionError("严格访问模式要求同时提供 user_id 和 tenant_id")
467
+
468
+ @staticmethod
469
+ def _context_access(context: RunContext) -> AccessContext | None:
470
+ if context.user_id is None or context.tenant_id is None:
471
+ return None
472
+ return AccessContext(user_id=context.user_id, tenant_id=context.tenant_id)
473
+
474
+ async def _execute(
475
+ self,
476
+ context: RunContext,
477
+ user_input: str,
478
+ *,
479
+ memory_context: str | None = None,
480
+ checkpoint_state: dict[str, Any] | None = None,
481
+ ) -> str:
482
+ context.status = "running"
483
+ context.emit("run_started", input=user_input)
484
+ context.checkpoint_callback = self._checkpoint_callback(context)
485
+ await self._persist(context)
486
+ try:
487
+ if self._run_executor is not None:
488
+ # 自定义执行器:生命周期(状态/事件/持久化/租约)仍由 Runtime 收口。
489
+ answer = await self._run_executor(
490
+ context,
491
+ user_input,
492
+ memory_context=memory_context,
493
+ checkpoint_state=checkpoint_state,
494
+ )
495
+ else:
496
+ answer = await self._agent.run(
497
+ user_input,
498
+ memory_context=memory_context,
499
+ thread_id=context.thread_id,
500
+ run_context=context,
501
+ checkpoint_state=checkpoint_state,
502
+ )
503
+ except asyncio.CancelledError:
504
+ if context.status == "running":
505
+ await self._finish_terminal(context, "cancelled", "run_cancelled")
506
+ raise
507
+ except Exception as exc:
508
+ if context.status == "running":
509
+ await self._finish_terminal(context, "failed", "run_failed", error=str(exc))
510
+ raise
511
+ # 内置路径由 Agent Loop 自己收口;执行器路径在此统一落到 completed。
512
+ if context.status == "running":
513
+ await self._finish_terminal(context, "completed", "run_completed", answer=answer)
514
+ return answer
515
+
516
+ async def _finish_terminal(
517
+ self,
518
+ context: RunContext,
519
+ status: str,
520
+ event_type: str,
521
+ **payload: Any,
522
+ ) -> None:
523
+ context.status = status # type: ignore[assignment]
524
+ context.emit(event_type, **payload)
525
+ await self._persist(context)
526
+
527
+ def _checkpoint_callback(
528
+ self,
529
+ context: RunContext,
530
+ ) -> Callable[[dict[str, Any]], Awaitable[None]]:
531
+ async def persist(_state: dict[str, Any]) -> None:
532
+ await self._persist(context)
533
+
534
+ return persist
535
+
536
+ async def _persist(self, context: RunContext) -> None:
537
+ await self._run_store.save_run(context)
538
+ await self._publish_pending_events(context)
539
+
540
+ async def _publish_pending_events(self, context: RunContext) -> None:
541
+ """把尚未发布的事件依次交给进程内 Callback 和进程外 EventSink。"""
542
+
543
+ cursor = self._event_cursors.get(context.run_id, 0)
544
+ for event in context.events[cursor:]:
545
+ await self._callback_manager.dispatch(event)
546
+ if self._event_sink is not None:
547
+ try:
548
+ await self._event_sink.publish(event)
549
+ except Exception:
550
+ # 事件订阅者故障不能覆盖 Agent 的最终结果。
551
+ logger.exception("发布 Agent 运行事件失败:%s", event.event_type)
552
+ self._event_cursors[context.run_id] = len(context.events)
553
+
554
+ def _cleanup_run(self, run_id: str) -> None:
555
+ """后台任务完全结束后释放进程内句柄和事件游标。"""
556
+ self._tasks.pop(run_id, None)
557
+ self._event_cursors.pop(run_id, None)
@@ -0,0 +1,55 @@
1
+ """Runtime 的默认内存实现。
2
+
3
+ 它只适合测试和单进程开发环境。生产应用应通过 ``RunStore`` 注入 SQLite、
4
+ PostgreSQL 或其他持久化实现。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ from typing import Any
11
+
12
+ from agent_core.ports import RunStore
13
+ from agent_core.protocol.runtime import RunContext
14
+
15
+
16
+ class MemoryRunStore(RunStore):
17
+ """带基本版本控制和幂等查询的内存运行存储。"""
18
+
19
+ def __init__(self) -> None:
20
+ self._runs: dict[tuple[str, str], dict[str, Any]] = {}
21
+
22
+ async def save_run(self, context: RunContext) -> None:
23
+ key = (context.thread_id, context.run_id)
24
+ previous = self._runs.get(key)
25
+ previous_version = int(previous.get("version", 0)) if previous else 0
26
+ if previous is not None and context.version != previous_version:
27
+ raise RuntimeError(f"run {context.run_id} 版本冲突")
28
+ context.version = previous_version + 1
29
+ self._runs[key] = copy.deepcopy(context.to_dict())
30
+
31
+ async def load_run(self, thread_id: str, run_id: str) -> RunContext | None:
32
+ value = self._runs.get((thread_id, run_id))
33
+ if value is None:
34
+ return None
35
+ return _context_from_dict(value)
36
+
37
+ async def find_run_by_idempotency(
38
+ self,
39
+ tenant_id: str | None,
40
+ user_id: str | None,
41
+ idempotency_key: str,
42
+ ) -> RunContext | None:
43
+ for value in self._runs.values():
44
+ if (
45
+ value.get("tenant_id") == tenant_id
46
+ and value.get("user_id") == user_id
47
+ and value.get("idempotency_key") == idempotency_key
48
+ ):
49
+ return _context_from_dict(value)
50
+ return None
51
+
52
+
53
+ def _context_from_dict(value: dict[str, Any]) -> RunContext:
54
+ """从持久化字典恢复 Core 上下文,忽略不可序列化的回调。"""
55
+ return RunContext.from_dict(copy.deepcopy(value))
@@ -0,0 +1,21 @@
1
+ """安全、版本化的 Core 对象序列化。"""
2
+
3
+ from agent_core.serialization.registry import (
4
+ SerializedEnvelope,
5
+ SerializerRegistry,
6
+ default_serializer_registry,
7
+ dumpd,
8
+ dumps,
9
+ load,
10
+ loads,
11
+ )
12
+
13
+ __all__ = [
14
+ "SerializedEnvelope",
15
+ "SerializerRegistry",
16
+ "default_serializer_registry",
17
+ "dumpd",
18
+ "dumps",
19
+ "load",
20
+ "loads",
21
+ ]