zero-agent-core-multi-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.
- agent_core_multi_agent/__init__.py +68 -0
- agent_core_multi_agent/adapters.py +139 -0
- agent_core_multi_agent/errors.py +39 -0
- agent_core_multi_agent/protocols.py +49 -0
- agent_core_multi_agent/py.typed +1 -0
- agent_core_multi_agent/registry.py +133 -0
- agent_core_multi_agent/routing.py +124 -0
- agent_core_multi_agent/storage.py +110 -0
- agent_core_multi_agent/supervisor.py +554 -0
- agent_core_multi_agent/types.py +450 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/METADATA +72 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/RECORD +14 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/WHEEL +4 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""多 Agent 编排的可持久化值对象。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from typing import Any, Literal, cast
|
|
9
|
+
|
|
10
|
+
from agent_core import AccessContext, RunEvent
|
|
11
|
+
|
|
12
|
+
AgentResultStatus = Literal["completed", "failed", "cancelled", "interrupted"]
|
|
13
|
+
CoordinationStatus = Literal["queued", "running", "completed", "failed", "cancelled", "interrupted"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _now() -> str:
|
|
17
|
+
return datetime.now(UTC).isoformat()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _non_empty(value: str, field_name: str) -> str:
|
|
21
|
+
normalized = value.strip()
|
|
22
|
+
if not normalized:
|
|
23
|
+
raise ValueError(f"{field_name} 不能为空")
|
|
24
|
+
return normalized
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AgentDescriptor:
|
|
29
|
+
"""一个可被 Supervisor 发现和调用的 Agent。"""
|
|
30
|
+
|
|
31
|
+
agent_id: str
|
|
32
|
+
description: str
|
|
33
|
+
capabilities: frozenset[str] = field(default_factory=frozenset)
|
|
34
|
+
aliases: frozenset[str] = field(default_factory=frozenset)
|
|
35
|
+
required_roles: frozenset[str] = field(default_factory=frozenset)
|
|
36
|
+
priority: int = 0
|
|
37
|
+
max_concurrency: int | None = None
|
|
38
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
object.__setattr__(self, "agent_id", _non_empty(self.agent_id, "agent_id"))
|
|
42
|
+
object.__setattr__(self, "description", _non_empty(self.description, "description"))
|
|
43
|
+
object.__setattr__(
|
|
44
|
+
self,
|
|
45
|
+
"capabilities",
|
|
46
|
+
frozenset(_non_empty(value, "capability") for value in self.capabilities),
|
|
47
|
+
)
|
|
48
|
+
object.__setattr__(
|
|
49
|
+
self,
|
|
50
|
+
"aliases",
|
|
51
|
+
frozenset(_non_empty(value, "alias") for value in self.aliases),
|
|
52
|
+
)
|
|
53
|
+
object.__setattr__(
|
|
54
|
+
self,
|
|
55
|
+
"required_roles",
|
|
56
|
+
frozenset(_non_empty(value, "required_role") for value in self.required_roles),
|
|
57
|
+
)
|
|
58
|
+
object.__setattr__(self, "metadata", dict(self.metadata))
|
|
59
|
+
if self.agent_id in self.aliases:
|
|
60
|
+
raise ValueError("aliases 不需要重复 agent_id")
|
|
61
|
+
if self.max_concurrency is not None and self.max_concurrency <= 0:
|
|
62
|
+
raise ValueError("max_concurrency 必须大于 0")
|
|
63
|
+
|
|
64
|
+
def can_invoke(self, access: AccessContext | None) -> bool:
|
|
65
|
+
if not self.required_roles:
|
|
66
|
+
return True
|
|
67
|
+
return access is not None and self.required_roles.issubset(access.roles)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"agent_id": self.agent_id,
|
|
72
|
+
"description": self.description,
|
|
73
|
+
"capabilities": sorted(self.capabilities),
|
|
74
|
+
"aliases": sorted(self.aliases),
|
|
75
|
+
"required_roles": sorted(self.required_roles),
|
|
76
|
+
"priority": self.priority,
|
|
77
|
+
"max_concurrency": self.max_concurrency,
|
|
78
|
+
"metadata": dict(self.metadata),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class HandoffRequest:
|
|
84
|
+
"""当前 Agent 请求把任务交给另一个 Agent。"""
|
|
85
|
+
|
|
86
|
+
target_agent_id: str | None = None
|
|
87
|
+
required_capability: str | None = None
|
|
88
|
+
input: str | None = None
|
|
89
|
+
reason: str = ""
|
|
90
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
91
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
92
|
+
|
|
93
|
+
def __post_init__(self) -> None:
|
|
94
|
+
target = self.target_agent_id.strip() if self.target_agent_id else None
|
|
95
|
+
capability = self.required_capability.strip() if self.required_capability else None
|
|
96
|
+
if not target and not capability:
|
|
97
|
+
raise ValueError("handoff 必须指定 target_agent_id 或 required_capability")
|
|
98
|
+
object.__setattr__(self, "target_agent_id", target)
|
|
99
|
+
object.__setattr__(self, "required_capability", capability)
|
|
100
|
+
object.__setattr__(self, "context", dict(self.context))
|
|
101
|
+
object.__setattr__(self, "metadata", dict(self.metadata))
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict[str, Any]:
|
|
104
|
+
return {
|
|
105
|
+
"target_agent_id": self.target_agent_id,
|
|
106
|
+
"required_capability": self.required_capability,
|
|
107
|
+
"input": self.input,
|
|
108
|
+
"reason": self.reason,
|
|
109
|
+
"context": dict(self.context),
|
|
110
|
+
"metadata": dict(self.metadata),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_dict(cls, value: dict[str, Any]) -> HandoffRequest:
|
|
115
|
+
return cls(
|
|
116
|
+
target_agent_id=value.get("target_agent_id"),
|
|
117
|
+
required_capability=value.get("required_capability"),
|
|
118
|
+
input=value.get("input"),
|
|
119
|
+
reason=str(value.get("reason", "")),
|
|
120
|
+
context=dict(value.get("context") or {}),
|
|
121
|
+
metadata=dict(value.get("metadata") or {}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class AgentTask:
|
|
127
|
+
"""Supervisor 交给某个 Agent 的不可变任务。"""
|
|
128
|
+
|
|
129
|
+
input: str
|
|
130
|
+
thread_id: str
|
|
131
|
+
target_agent_id: str | None = None
|
|
132
|
+
required_capability: str | None = None
|
|
133
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
134
|
+
access: AccessContext | None = None
|
|
135
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
136
|
+
task_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
137
|
+
root_task_id: str | None = None
|
|
138
|
+
parent_task_id: str | None = None
|
|
139
|
+
parent_run_id: str | None = None
|
|
140
|
+
coordination_id: str | None = None
|
|
141
|
+
idempotency_key: str | None = None
|
|
142
|
+
resume_requested: bool = False
|
|
143
|
+
depth: int = 0
|
|
144
|
+
lineage: tuple[str, ...] = ()
|
|
145
|
+
|
|
146
|
+
def __post_init__(self) -> None:
|
|
147
|
+
object.__setattr__(self, "input", _non_empty(self.input, "input"))
|
|
148
|
+
object.__setattr__(self, "thread_id", _non_empty(self.thread_id, "thread_id"))
|
|
149
|
+
object.__setattr__(self, "task_id", _non_empty(self.task_id, "task_id"))
|
|
150
|
+
object.__setattr__(self, "root_task_id", self.root_task_id or self.task_id)
|
|
151
|
+
object.__setattr__(self, "context", dict(self.context))
|
|
152
|
+
object.__setattr__(self, "metadata", dict(self.metadata))
|
|
153
|
+
object.__setattr__(self, "lineage", tuple(self.lineage))
|
|
154
|
+
if self.depth < 0:
|
|
155
|
+
raise ValueError("depth 不能小于 0")
|
|
156
|
+
if self.target_agent_id is not None and not self.target_agent_id.strip():
|
|
157
|
+
raise ValueError("target_agent_id 不能为空字符串")
|
|
158
|
+
if self.required_capability is not None and not self.required_capability.strip():
|
|
159
|
+
raise ValueError("required_capability 不能为空字符串")
|
|
160
|
+
if self.target_agent_id is not None:
|
|
161
|
+
object.__setattr__(self, "target_agent_id", self.target_agent_id.strip())
|
|
162
|
+
if self.required_capability is not None:
|
|
163
|
+
object.__setattr__(self, "required_capability", self.required_capability.strip())
|
|
164
|
+
|
|
165
|
+
def bind_execution(self, execution_id: str) -> AgentTask:
|
|
166
|
+
"""把根任务绑定到协调实例,并生成稳定的子调用幂等键。"""
|
|
167
|
+
return replace(
|
|
168
|
+
self,
|
|
169
|
+
coordination_id=execution_id,
|
|
170
|
+
idempotency_key=self.idempotency_key or f"multi-agent:{execution_id}:{self.task_id}",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def handoff(
|
|
174
|
+
self,
|
|
175
|
+
request: HandoffRequest,
|
|
176
|
+
*,
|
|
177
|
+
current_agent_id: str,
|
|
178
|
+
parent_run_id: str | None,
|
|
179
|
+
fallback_input: str,
|
|
180
|
+
) -> AgentTask:
|
|
181
|
+
"""从 handoff 请求构造下一个子任务。"""
|
|
182
|
+
task_id = str(uuid.uuid4())
|
|
183
|
+
context = {**self.context, **request.context}
|
|
184
|
+
metadata = {**self.metadata, **request.metadata, "handoff_reason": request.reason}
|
|
185
|
+
return AgentTask(
|
|
186
|
+
task_id=task_id,
|
|
187
|
+
root_task_id=self.root_task_id,
|
|
188
|
+
parent_task_id=self.task_id,
|
|
189
|
+
parent_run_id=parent_run_id or self.parent_run_id,
|
|
190
|
+
coordination_id=self.coordination_id,
|
|
191
|
+
thread_id=self.thread_id,
|
|
192
|
+
input=request.input or fallback_input,
|
|
193
|
+
target_agent_id=request.target_agent_id,
|
|
194
|
+
required_capability=request.required_capability,
|
|
195
|
+
context=context,
|
|
196
|
+
access=self.access,
|
|
197
|
+
metadata=metadata,
|
|
198
|
+
idempotency_key=f"multi-agent:{self.coordination_id}:{task_id}",
|
|
199
|
+
depth=self.depth + 1,
|
|
200
|
+
lineage=(*self.lineage, current_agent_id),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def to_dict(self) -> dict[str, Any]:
|
|
204
|
+
return {
|
|
205
|
+
"task_id": self.task_id,
|
|
206
|
+
"root_task_id": self.root_task_id,
|
|
207
|
+
"parent_task_id": self.parent_task_id,
|
|
208
|
+
"parent_run_id": self.parent_run_id,
|
|
209
|
+
"coordination_id": self.coordination_id,
|
|
210
|
+
"thread_id": self.thread_id,
|
|
211
|
+
"input": self.input,
|
|
212
|
+
"target_agent_id": self.target_agent_id,
|
|
213
|
+
"required_capability": self.required_capability,
|
|
214
|
+
"context": dict(self.context),
|
|
215
|
+
"access": self.access.to_dict() if self.access else None,
|
|
216
|
+
"metadata": dict(self.metadata),
|
|
217
|
+
"idempotency_key": self.idempotency_key,
|
|
218
|
+
"resume_requested": self.resume_requested,
|
|
219
|
+
"depth": self.depth,
|
|
220
|
+
"lineage": list(self.lineage),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def from_dict(cls, value: dict[str, Any]) -> AgentTask:
|
|
225
|
+
access_value = value.get("access")
|
|
226
|
+
access = None
|
|
227
|
+
if isinstance(access_value, dict):
|
|
228
|
+
access = AccessContext(
|
|
229
|
+
user_id=str(access_value.get("user_id", "anonymous")),
|
|
230
|
+
tenant_id=str(access_value.get("tenant_id", "default")),
|
|
231
|
+
roles=frozenset(str(role) for role in access_value.get("roles", ["user"])),
|
|
232
|
+
)
|
|
233
|
+
return cls(
|
|
234
|
+
task_id=str(value["task_id"]),
|
|
235
|
+
root_task_id=value.get("root_task_id"),
|
|
236
|
+
parent_task_id=value.get("parent_task_id"),
|
|
237
|
+
parent_run_id=value.get("parent_run_id"),
|
|
238
|
+
coordination_id=value.get("coordination_id"),
|
|
239
|
+
thread_id=str(value["thread_id"]),
|
|
240
|
+
input=str(value["input"]),
|
|
241
|
+
target_agent_id=value.get("target_agent_id"),
|
|
242
|
+
required_capability=value.get("required_capability"),
|
|
243
|
+
context=dict(value.get("context") or {}),
|
|
244
|
+
access=access,
|
|
245
|
+
metadata=dict(value.get("metadata") or {}),
|
|
246
|
+
idempotency_key=value.get("idempotency_key"),
|
|
247
|
+
resume_requested=bool(value.get("resume_requested", False)),
|
|
248
|
+
depth=int(value.get("depth", 0)),
|
|
249
|
+
lineage=tuple(str(item) for item in value.get("lineage", [])),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass(frozen=True)
|
|
254
|
+
class AgentResult:
|
|
255
|
+
"""子 Agent 的统一执行结果。"""
|
|
256
|
+
|
|
257
|
+
task_id: str
|
|
258
|
+
agent_id: str
|
|
259
|
+
status: AgentResultStatus
|
|
260
|
+
output: str = ""
|
|
261
|
+
run_id: str | None = None
|
|
262
|
+
handoff: HandoffRequest | None = None
|
|
263
|
+
error: str | None = None
|
|
264
|
+
retryable: bool = False
|
|
265
|
+
usage: dict[str, float] = field(default_factory=dict)
|
|
266
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
267
|
+
|
|
268
|
+
def __post_init__(self) -> None:
|
|
269
|
+
object.__setattr__(self, "task_id", _non_empty(self.task_id, "task_id"))
|
|
270
|
+
object.__setattr__(self, "agent_id", _non_empty(self.agent_id, "agent_id"))
|
|
271
|
+
object.__setattr__(self, "usage", {str(key): float(value) for key, value in self.usage.items()})
|
|
272
|
+
object.__setattr__(self, "metadata", dict(self.metadata))
|
|
273
|
+
if self.status not in {"completed", "failed", "cancelled", "interrupted"}:
|
|
274
|
+
raise ValueError(f"无效 AgentResult status:{self.status}")
|
|
275
|
+
if self.handoff is not None and self.status != "completed":
|
|
276
|
+
raise ValueError("只有 completed 结果可以携带 handoff")
|
|
277
|
+
if self.status == "failed" and not self.error:
|
|
278
|
+
raise ValueError("failed 结果必须提供 error")
|
|
279
|
+
|
|
280
|
+
def to_dict(self) -> dict[str, Any]:
|
|
281
|
+
return {
|
|
282
|
+
"task_id": self.task_id,
|
|
283
|
+
"agent_id": self.agent_id,
|
|
284
|
+
"status": self.status,
|
|
285
|
+
"output": self.output,
|
|
286
|
+
"run_id": self.run_id,
|
|
287
|
+
"handoff": self.handoff.to_dict() if self.handoff else None,
|
|
288
|
+
"error": self.error,
|
|
289
|
+
"retryable": self.retryable,
|
|
290
|
+
"usage": dict(self.usage),
|
|
291
|
+
"metadata": dict(self.metadata),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def from_dict(cls, value: dict[str, Any]) -> AgentResult:
|
|
296
|
+
handoff = value.get("handoff")
|
|
297
|
+
return cls(
|
|
298
|
+
task_id=str(value["task_id"]),
|
|
299
|
+
agent_id=str(value["agent_id"]),
|
|
300
|
+
status=cast(AgentResultStatus, value["status"]),
|
|
301
|
+
output=str(value.get("output", "")),
|
|
302
|
+
run_id=value.get("run_id"),
|
|
303
|
+
handoff=HandoffRequest.from_dict(handoff) if isinstance(handoff, dict) else None,
|
|
304
|
+
error=value.get("error"),
|
|
305
|
+
retryable=bool(value.get("retryable", False)),
|
|
306
|
+
usage={str(key): float(item) for key, item in dict(value.get("usage") or {}).items()},
|
|
307
|
+
metadata=dict(value.get("metadata") or {}),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@dataclass
|
|
312
|
+
class CoordinationExecution:
|
|
313
|
+
"""可在每个 Agent 边界保存和恢复的协调实例。"""
|
|
314
|
+
|
|
315
|
+
root_task: AgentTask
|
|
316
|
+
current_task: AgentTask | None
|
|
317
|
+
execution_id: str
|
|
318
|
+
status: CoordinationStatus = "queued"
|
|
319
|
+
current_agent_id: str | None = None
|
|
320
|
+
results: list[AgentResult] = field(default_factory=list)
|
|
321
|
+
route_history: list[str] = field(default_factory=list)
|
|
322
|
+
events: list[RunEvent] = field(default_factory=list)
|
|
323
|
+
output: str = ""
|
|
324
|
+
error: str | None = None
|
|
325
|
+
usage: dict[str, float] = field(default_factory=dict)
|
|
326
|
+
handoffs_completed: int = 0
|
|
327
|
+
agent_calls: int = 0
|
|
328
|
+
version: int = 0
|
|
329
|
+
created_at: str = field(default_factory=_now)
|
|
330
|
+
updated_at: str = field(default_factory=_now)
|
|
331
|
+
|
|
332
|
+
def __post_init__(self) -> None:
|
|
333
|
+
self.execution_id = _non_empty(self.execution_id, "execution_id")
|
|
334
|
+
if self.status not in {"queued", "running", "completed", "failed", "cancelled", "interrupted"}:
|
|
335
|
+
raise ValueError(f"无效 CoordinationExecution status:{self.status}")
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def access(self) -> AccessContext | None:
|
|
339
|
+
return self.root_task.access
|
|
340
|
+
|
|
341
|
+
def emit(self, event_type: str, **payload: Any) -> RunEvent:
|
|
342
|
+
error_value = payload.get("error")
|
|
343
|
+
usage_value = payload.get("usage")
|
|
344
|
+
event = RunEvent(
|
|
345
|
+
event_type=event_type,
|
|
346
|
+
run_id=self.execution_id,
|
|
347
|
+
thread_id=self.root_task.thread_id,
|
|
348
|
+
sequence=len(self.events),
|
|
349
|
+
payload=payload,
|
|
350
|
+
parent_run_id=self.root_task.parent_run_id,
|
|
351
|
+
component="multi_agent",
|
|
352
|
+
tags=["multi-agent"],
|
|
353
|
+
usage=dict(usage_value) if isinstance(usage_value, dict) else {},
|
|
354
|
+
error=str(error_value) if error_value is not None else None,
|
|
355
|
+
)
|
|
356
|
+
self.events.append(event)
|
|
357
|
+
return event
|
|
358
|
+
|
|
359
|
+
def to_dict(self) -> dict[str, Any]:
|
|
360
|
+
return {
|
|
361
|
+
"execution_id": self.execution_id,
|
|
362
|
+
"root_task": self.root_task.to_dict(),
|
|
363
|
+
"current_task": self.current_task.to_dict() if self.current_task else None,
|
|
364
|
+
"status": self.status,
|
|
365
|
+
"current_agent_id": self.current_agent_id,
|
|
366
|
+
"results": [result.to_dict() for result in self.results],
|
|
367
|
+
"route_history": list(self.route_history),
|
|
368
|
+
"events": [event.to_dict() for event in self.events],
|
|
369
|
+
"output": self.output,
|
|
370
|
+
"error": self.error,
|
|
371
|
+
"usage": dict(self.usage),
|
|
372
|
+
"handoffs_completed": self.handoffs_completed,
|
|
373
|
+
"agent_calls": self.agent_calls,
|
|
374
|
+
"version": self.version,
|
|
375
|
+
"created_at": self.created_at,
|
|
376
|
+
"updated_at": self.updated_at,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def from_dict(cls, value: dict[str, Any]) -> CoordinationExecution:
|
|
381
|
+
current = value.get("current_task")
|
|
382
|
+
return cls(
|
|
383
|
+
execution_id=str(value["execution_id"]),
|
|
384
|
+
root_task=AgentTask.from_dict(dict(value["root_task"])),
|
|
385
|
+
current_task=AgentTask.from_dict(current) if isinstance(current, dict) else None,
|
|
386
|
+
status=cast(CoordinationStatus, value.get("status", "queued")),
|
|
387
|
+
current_agent_id=value.get("current_agent_id"),
|
|
388
|
+
results=[AgentResult.from_dict(item) for item in value.get("results", [])],
|
|
389
|
+
route_history=[str(item) for item in value.get("route_history", [])],
|
|
390
|
+
events=[RunEvent.from_dict(item) for item in value.get("events", [])],
|
|
391
|
+
output=str(value.get("output", "")),
|
|
392
|
+
error=value.get("error"),
|
|
393
|
+
usage={str(key): float(item) for key, item in dict(value.get("usage") or {}).items()},
|
|
394
|
+
handoffs_completed=int(value.get("handoffs_completed", 0)),
|
|
395
|
+
agent_calls=int(value.get("agent_calls", 0)),
|
|
396
|
+
version=int(value.get("version", 0)),
|
|
397
|
+
created_at=str(value.get("created_at") or _now()),
|
|
398
|
+
updated_at=str(value.get("updated_at") or _now()),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@dataclass(frozen=True)
|
|
403
|
+
class RoutingDecision:
|
|
404
|
+
"""一次可审计的路由结果。"""
|
|
405
|
+
|
|
406
|
+
agent_id: str
|
|
407
|
+
reason: str
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@dataclass(frozen=True)
|
|
411
|
+
class CoordinationPolicy:
|
|
412
|
+
"""Supervisor 的调用、恢复和资源预算。"""
|
|
413
|
+
|
|
414
|
+
max_handoffs: int = 8
|
|
415
|
+
max_agent_calls: int = 16
|
|
416
|
+
max_visits_per_agent: int = 2
|
|
417
|
+
max_agent_attempts: int = 1
|
|
418
|
+
agent_timeout_seconds: float = 120.0
|
|
419
|
+
retry_base_seconds: float = 0.5
|
|
420
|
+
max_parallelism: int = 4
|
|
421
|
+
max_total_tokens: float | None = None
|
|
422
|
+
max_total_cost: float | None = None
|
|
423
|
+
require_access: bool = False
|
|
424
|
+
lease_seconds: float = 30.0
|
|
425
|
+
heartbeat_seconds: float = 10.0
|
|
426
|
+
|
|
427
|
+
def __post_init__(self) -> None:
|
|
428
|
+
positive_ints = {
|
|
429
|
+
"max_agent_calls": self.max_agent_calls,
|
|
430
|
+
"max_visits_per_agent": self.max_visits_per_agent,
|
|
431
|
+
"max_agent_attempts": self.max_agent_attempts,
|
|
432
|
+
"max_parallelism": self.max_parallelism,
|
|
433
|
+
}
|
|
434
|
+
if self.max_handoffs < 0:
|
|
435
|
+
raise ValueError("max_handoffs 不能小于 0")
|
|
436
|
+
for name, value in positive_ints.items():
|
|
437
|
+
if value <= 0:
|
|
438
|
+
raise ValueError(f"{name} 必须大于 0")
|
|
439
|
+
if self.agent_timeout_seconds <= 0:
|
|
440
|
+
raise ValueError("agent_timeout_seconds 必须大于 0")
|
|
441
|
+
if self.retry_base_seconds < 0:
|
|
442
|
+
raise ValueError("retry_base_seconds 不能小于 0")
|
|
443
|
+
if self.max_total_tokens is not None and self.max_total_tokens <= 0:
|
|
444
|
+
raise ValueError("max_total_tokens 必须大于 0")
|
|
445
|
+
if self.max_total_cost is not None and self.max_total_cost <= 0:
|
|
446
|
+
raise ValueError("max_total_cost 必须大于 0")
|
|
447
|
+
if self.lease_seconds <= 0:
|
|
448
|
+
raise ValueError("lease_seconds 必须大于 0")
|
|
449
|
+
if self.heartbeat_seconds <= 0 or self.heartbeat_seconds >= self.lease_seconds:
|
|
450
|
+
raise ValueError("heartbeat_seconds 必须大于 0 且小于 lease_seconds")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zero-agent-core-multi-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Handwritten Agent Core 的多 Agent 编排扩展
|
|
5
|
+
Project-URL: Homepage, https://github.com/qiuzixu/agent-core
|
|
6
|
+
Project-URL: Repository, https://github.com/qiuzixu/agent-core
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: <3.14,>=3.13
|
|
15
|
+
Requires-Dist: zero-agent-core<0.3,>=0.2.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mypy<2,>=1.18; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio<2,>=1.3; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest<10,>=9; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff<1,>=0.14; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Handwritten Agent Core Multi-Agent
|
|
24
|
+
|
|
25
|
+
`zero-agent-core-multi-agent` 是 Agent Core 的可选多 Agent 编排扩展。它不依赖
|
|
26
|
+
LangChain、LangGraph、AutoGen 或 CrewAI,直接复用 Core 的运行时、事件、访问上下文、Workflow Store
|
|
27
|
+
和 Run Lease。
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install zero-agent-core-multi-agent
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from agent_core_multi_agent import (
|
|
35
|
+
AgentDescriptor,
|
|
36
|
+
AgentRegistry,
|
|
37
|
+
AgentTask,
|
|
38
|
+
CallableAgentInvoker,
|
|
39
|
+
HandoffRequest,
|
|
40
|
+
SupervisorAgent,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
registry = AgentRegistry()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def researcher(task):
|
|
47
|
+
return "检索完成"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
registry.register(
|
|
51
|
+
AgentDescriptor(
|
|
52
|
+
agent_id="researcher",
|
|
53
|
+
description="检索资料",
|
|
54
|
+
capabilities=frozenset({"research"}),
|
|
55
|
+
),
|
|
56
|
+
CallableAgentInvoker("researcher", researcher),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
supervisor = SupervisorAgent(registry)
|
|
60
|
+
execution = await supervisor.run(
|
|
61
|
+
AgentTask(
|
|
62
|
+
thread_id="thread-1",
|
|
63
|
+
input="查询飞行规则",
|
|
64
|
+
required_capability="research",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
print(execution.output)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
具体应用可以用 `RuntimeAgentInvoker` 包装现有 `AgentRuntime`。`WorkflowCoordinationStore` 支持复用
|
|
71
|
+
Core 的内存、SQLite、PostgreSQL Workflow Store;多 Worker 部署时再注入对应的 `RunLeaseStore`。
|
|
72
|
+
每次路由、调用和 handoff 都会落 checkpoint,恢复时使用相同任务 ID 和幂等键。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
agent_core_multi_agent/__init__.py,sha256=OVZYpu5RVZZAH0rXqI5dNK9pwEKI4H02P4o_rkMokrs,1869
|
|
2
|
+
agent_core_multi_agent/adapters.py,sha256=fbmtBwVeU_gHFuiYK0dYgesa3d5hhNg1woa8pXh_59o,4582
|
|
3
|
+
agent_core_multi_agent/errors.py,sha256=0Ikj9rKYGXYNanLvysN411hOknt1h39pAACjvAm-Yag,1025
|
|
4
|
+
agent_core_multi_agent/protocols.py,sha256=3cUYrCJ1y1DMHJyY61HLUCSFe8rqj6s46p680Ktkgkg,1289
|
|
5
|
+
agent_core_multi_agent/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
6
|
+
agent_core_multi_agent/registry.py,sha256=SZhu1p3g4fxN35Su_KAhU84YpjAdsiRfcO1wQRyHJzE,4834
|
|
7
|
+
agent_core_multi_agent/routing.py,sha256=8q2IqDc5F2wNejW5d-bQWcw88Neh6fk2SBV_YJnKR9U,4893
|
|
8
|
+
agent_core_multi_agent/storage.py,sha256=y6-4zLgpdF9cSWXb25PgQZqiUo8Kbbuceqa_jv9stlM,4267
|
|
9
|
+
agent_core_multi_agent/supervisor.py,sha256=j60TomQ_I16pmmrO2gRPN41WItaTFWpjwxC-2dq9Fz8,22448
|
|
10
|
+
agent_core_multi_agent/types.py,sha256=rQ7o9fsxI8LAffqy3bssKRiUNoYvcaBdyWEi3PJAUfI,18610
|
|
11
|
+
zero_agent_core_multi_agent-0.1.0.dist-info/METADATA,sha256=KzJnSQe5aiRh_cJq3XanrgOy5qQSlDdoAfdSQfhR-YE,2201
|
|
12
|
+
zero_agent_core_multi_agent-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
13
|
+
zero_agent_core_multi_agent-0.1.0.dist-info/licenses/LICENSE,sha256=qAQIBbXbJKNe_4lmjwAGFnthk5xS-ROfzttb-OMh-Tk,11554
|
|
14
|
+
zero_agent_core_multi_agent-0.1.0.dist-info/RECORD,,
|