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,64 @@
1
+ """模型选择值对象与存储端口。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Literal, Protocol
7
+
8
+ ModelSelectionScope = Literal["user", "tenant"]
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ModelSelection:
13
+ """一个按用户或租户保存的模型选择。"""
14
+
15
+ provider: str
16
+ model: str
17
+ scope: ModelSelectionScope
18
+ user_id: str | None = None
19
+ tenant_id: str | None = None
20
+ version: int = 1
21
+ updated_at: str = ""
22
+
23
+ def to_dict(self) -> dict[str, Any]:
24
+ return {
25
+ "provider": self.provider,
26
+ "model": self.model,
27
+ "scope": self.scope,
28
+ "user_id": self.user_id,
29
+ "tenant_id": self.tenant_id,
30
+ "version": self.version,
31
+ "updated_at": self.updated_at,
32
+ }
33
+
34
+
35
+ class ModelSelectionStore(Protocol):
36
+ """按用户优先、租户其次保存和解析模型选择。"""
37
+
38
+ async def resolve(
39
+ self,
40
+ *,
41
+ user_id: str | None,
42
+ tenant_id: str | None,
43
+ ) -> ModelSelection | None: ...
44
+
45
+ async def save(
46
+ self,
47
+ provider: str,
48
+ model: str,
49
+ *,
50
+ scope: ModelSelectionScope,
51
+ user_id: str | None,
52
+ tenant_id: str | None,
53
+ ) -> ModelSelection: ...
54
+
55
+ async def clear(
56
+ self,
57
+ *,
58
+ scope: ModelSelectionScope,
59
+ user_id: str | None,
60
+ tenant_id: str | None,
61
+ ) -> None: ...
62
+
63
+
64
+ __all__ = ["ModelSelection", "ModelSelectionScope", "ModelSelectionStore"]
@@ -0,0 +1,114 @@
1
+ """Agent Core 的可替换存储端口。
2
+
3
+ 协议描述运行时需要的最小能力;内置 SQLite/PostgreSQL 实现位于
4
+ ``agent_core.storage``,Redis 或业务自己的存储也可以实现这些协议。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+ from typing import Any, Protocol
11
+
12
+ from agent_core.access import AccessContext
13
+ from agent_core.protocol.messages import Message
14
+ from agent_core.protocol.runtime import ApprovalRecord, RunContext, RunEvent
15
+
16
+
17
+ class ApprovalStore(Protocol):
18
+ """审批记录持久化协议。"""
19
+
20
+ async def save_approval(self, approval: ApprovalRecord) -> None: ...
21
+
22
+ async def load_approval(self, approval_id: str) -> ApprovalRecord | None: ...
23
+
24
+ async def transition_approval(
25
+ self,
26
+ approval: ApprovalRecord,
27
+ *,
28
+ expected_status: str = "pending",
29
+ ) -> bool:
30
+ """仅当记录仍处于预期状态时保存新状态。"""
31
+ ...
32
+
33
+
34
+ class RunStore(Protocol):
35
+ """运行实例持久化协议。
36
+
37
+ 实现应按 ``RunContext`` 中的 tenant/user/thread 作用域隔离数据,并在保存时
38
+ 处理版本冲突和幂等键。Core 只要求最小的读取、保存和恢复能力。
39
+ """
40
+
41
+ async def save_run(self, context: RunContext) -> None:
42
+ """保存运行上下文。实现可以在这里递增并回写 ``context.version``。"""
43
+ ...
44
+
45
+ async def load_run(self, thread_id: str, run_id: str) -> RunContext | None:
46
+ """按会话和运行 ID读取运行上下文。"""
47
+ ...
48
+
49
+ async def find_run_by_idempotency(
50
+ self,
51
+ tenant_id: str | None,
52
+ user_id: str | None,
53
+ idempotency_key: str,
54
+ ) -> RunContext | None:
55
+ """查找同一作用域内已经存在的幂等请求。"""
56
+ ...
57
+
58
+
59
+ class SessionStore(Protocol):
60
+ """对话消息历史存储协议。"""
61
+
62
+ async def load_messages(
63
+ self, thread_id: str, *, access: AccessContext | None = None
64
+ ) -> list[Message]: ...
65
+
66
+ async def append_messages(
67
+ self,
68
+ thread_id: str,
69
+ messages: Sequence[Message],
70
+ *,
71
+ access: AccessContext | None = None,
72
+ ) -> None: ...
73
+
74
+ async def replace_messages(
75
+ self,
76
+ thread_id: str,
77
+ messages: Sequence[Message],
78
+ *,
79
+ access: AccessContext | None = None,
80
+ ) -> None: ...
81
+
82
+ async def delete_thread(self, thread_id: str, *, access: AccessContext | None = None) -> None: ...
83
+
84
+ async def list_threads(self, *, access: AccessContext | None = None) -> list[str]: ...
85
+
86
+
87
+ class ContextStore(Protocol):
88
+ """结构化长期上下文存储协议。"""
89
+
90
+ async def get_context(self, thread_id: str, *, access: AccessContext | None = None) -> dict[str, Any]: ...
91
+
92
+ async def update_context(
93
+ self,
94
+ thread_id: str,
95
+ values: dict[str, Any],
96
+ *,
97
+ access: AccessContext | None = None,
98
+ ) -> dict[str, Any]: ...
99
+
100
+ async def clear_context(self, thread_id: str, *, access: AccessContext | None = None) -> None: ...
101
+
102
+
103
+ class WorkflowStore(Protocol):
104
+ """工作流执行实例存储协议。"""
105
+
106
+ async def save_execution(self, execution: Any) -> None: ...
107
+
108
+ async def load_execution(self, execution_id: str) -> Any | None: ...
109
+
110
+
111
+ class EventSink(Protocol):
112
+ """运行事件发布协议,可接 SSE、WebSocket、日志或消息队列。"""
113
+
114
+ async def publish(self, event: RunEvent) -> None: ...
@@ -0,0 +1,19 @@
1
+ """提示词注册和版本管理公共导出。"""
2
+
3
+ from agent_core.prompts.registry import PromptEntry, PromptRegistry, PromptVersion
4
+ from agent_core.prompts.templates import (
5
+ ChatPromptTemplate,
6
+ MessagesPlaceholder,
7
+ MessageTemplate,
8
+ PromptTemplate,
9
+ )
10
+
11
+ __all__ = [
12
+ "ChatPromptTemplate",
13
+ "MessageTemplate",
14
+ "MessagesPlaceholder",
15
+ "PromptEntry",
16
+ "PromptRegistry",
17
+ "PromptTemplate",
18
+ "PromptVersion",
19
+ ]
@@ -0,0 +1,236 @@
1
+ """与业务无关的提示词版本注册表。
2
+
3
+ Core 只负责提示词的版本、渲染、回滚和 JSON 文件持久化;具体 Agent 的默认
4
+ 提示词由应用包注册,避免把某个业务领域的文案带入公共框架。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import os
12
+ import tempfile
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from agent_core.prompts.templates import PromptTemplate
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class PromptVersion:
25
+ """单个提示词版本。"""
26
+
27
+ version: int
28
+ content: str
29
+ description: str
30
+ defaults: dict[str, Any] = field(default_factory=dict)
31
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
32
+
33
+ @property
34
+ def variables(self) -> tuple[str, ...]:
35
+ """返回模板声明的变量。"""
36
+
37
+ return PromptTemplate(self.content, defaults=self.defaults).variables
38
+
39
+ def render(self, **kwargs: Any) -> str:
40
+ """使用 ``str.format`` 渲染模板变量。"""
41
+ return PromptTemplate(self.content, defaults=self.defaults).render(**kwargs)
42
+
43
+
44
+ @dataclass
45
+ class PromptEntry:
46
+ """一个提示词的全部版本。"""
47
+
48
+ name: str
49
+ current_version: int
50
+ versions: list[PromptVersion] = field(default_factory=list)
51
+
52
+ @property
53
+ def current(self) -> PromptVersion:
54
+ for version in self.versions:
55
+ if version.version == self.current_version:
56
+ return version
57
+ raise ValueError(f"找不到版本 {self.current_version}")
58
+
59
+ def get_version(self, version: int) -> PromptVersion | None:
60
+ return next((item for item in self.versions if item.version == version), None)
61
+
62
+
63
+ class PromptRegistry:
64
+ """支持版本管理和文件持久化的提示词注册表。"""
65
+
66
+ def __init__(self, storage_path: str | Path = "./prompts.json") -> None:
67
+ self._path = Path(storage_path)
68
+ self._entries: dict[str, PromptEntry] = {}
69
+ self._load()
70
+
71
+ def register(
72
+ self,
73
+ name: str,
74
+ content: str,
75
+ description: str = "",
76
+ *,
77
+ defaults: dict[str, Any] | None = None,
78
+ ) -> PromptVersion:
79
+ """注册提示词;同名提示词会创建新版本。"""
80
+ if name in self._entries:
81
+ return self.update(name, content, description=description, defaults=defaults)
82
+ version = PromptVersion(
83
+ version=1,
84
+ content=content,
85
+ description=description,
86
+ defaults=dict(defaults or {}),
87
+ )
88
+ self._entries[name] = PromptEntry(name=name, current_version=1, versions=[version])
89
+ self._save()
90
+ logger.info("[Prompt] 注册:%s v1", name)
91
+ return version
92
+
93
+ def update(
94
+ self,
95
+ name: str,
96
+ content: str,
97
+ description: str = "",
98
+ *,
99
+ defaults: dict[str, Any] | None = None,
100
+ ) -> PromptVersion:
101
+ """创建同一提示词的新版本并切换为当前版本。"""
102
+ entry = self._entries.get(name)
103
+ if entry is None:
104
+ raise KeyError(f"提示词不存在:{name},请先调用 register()")
105
+ version = PromptVersion(
106
+ version=max(item.version for item in entry.versions) + 1,
107
+ content=content,
108
+ description=description,
109
+ defaults=dict(defaults or {}),
110
+ )
111
+ entry.versions.append(version)
112
+ entry.current_version = version.version
113
+ self._save()
114
+ logger.info("[Prompt] 更新:%s v%d", name, version.version)
115
+ return version
116
+
117
+ def rollback(self, name: str, version: int) -> PromptVersion:
118
+ """切换到已有版本,不删除版本历史。"""
119
+ entry = self._get_entry(name)
120
+ target = entry.get_version(version)
121
+ if target is None:
122
+ raise KeyError(f"提示词 {name} 不存在版本 {version}")
123
+ entry.current_version = version
124
+ self._save()
125
+ logger.info("[Prompt] 回滚:%s -> v%d", name, version)
126
+ return target
127
+
128
+ def render(self, prompt_name: str, **kwargs: Any) -> str:
129
+ """渲染当前版本。"""
130
+ return self._get_entry(prompt_name).current.render(**kwargs)
131
+
132
+ def get(self, prompt_name: str) -> str:
133
+ """读取当前版本的原始模板。"""
134
+ return self._get_entry(prompt_name).current.content
135
+
136
+ def get_history(self, prompt_name: str) -> list[dict[str, Any]]:
137
+ """返回版本历史摘要。"""
138
+ entry = self._get_entry(prompt_name)
139
+ return [
140
+ {
141
+ "version": item.version,
142
+ "description": item.description,
143
+ "created_at": item.created_at,
144
+ "variables": list(item.variables),
145
+ "is_current": item.version == entry.current_version,
146
+ "content_preview": item.content[:100] + "..." if len(item.content) > 100 else item.content,
147
+ }
148
+ for item in entry.versions
149
+ ]
150
+
151
+ def list_prompts(self) -> list[str]:
152
+ """列出已注册的提示词名称。"""
153
+ return list(self._entries)
154
+
155
+ def delete(self, name: str) -> bool:
156
+ """删除提示词及其历史。"""
157
+ if name not in self._entries:
158
+ return False
159
+ del self._entries[name]
160
+ self._save()
161
+ logger.info("[Prompt] 删除:%s", name)
162
+ return True
163
+
164
+ def _save(self) -> None:
165
+ data = {
166
+ name: {
167
+ "name": entry.name,
168
+ "current_version": entry.current_version,
169
+ "versions": [
170
+ {
171
+ "version": item.version,
172
+ "content": item.content,
173
+ "description": item.description,
174
+ "defaults": item.defaults,
175
+ "created_at": item.created_at,
176
+ }
177
+ for item in entry.versions
178
+ ],
179
+ }
180
+ for name, entry in self._entries.items()
181
+ }
182
+ self._path.parent.mkdir(parents=True, exist_ok=True)
183
+ temporary_path: Path | None = None
184
+ try:
185
+ with tempfile.NamedTemporaryFile(
186
+ mode="w",
187
+ encoding="utf-8",
188
+ dir=self._path.parent,
189
+ prefix=f".{self._path.name}.",
190
+ suffix=".tmp",
191
+ delete=False,
192
+ ) as temporary:
193
+ temporary_path = Path(temporary.name)
194
+ json.dump(data, temporary, ensure_ascii=False, indent=2)
195
+ temporary.flush()
196
+ os.fsync(temporary.fileno())
197
+ os.replace(temporary_path, self._path)
198
+ finally:
199
+ if temporary_path is not None and temporary_path.exists():
200
+ temporary_path.unlink(missing_ok=True)
201
+
202
+ def _load(self) -> None:
203
+ if not self._path.exists():
204
+ return
205
+ data = json.loads(self._path.read_text(encoding="utf-8"))
206
+ if not isinstance(data, dict):
207
+ raise ValueError(f"提示词注册表根节点必须是对象:{self._path}")
208
+ loaded: dict[str, PromptEntry] = {}
209
+ for name, raw in data.items():
210
+ versions = [
211
+ PromptVersion(
212
+ version=item["version"],
213
+ content=item["content"],
214
+ description=item.get("description", ""),
215
+ defaults=dict(item.get("defaults") or {}),
216
+ created_at=item.get("created_at", ""),
217
+ )
218
+ for item in raw["versions"]
219
+ ]
220
+ loaded[name] = PromptEntry(
221
+ name=raw.get("name", name),
222
+ current_version=raw["current_version"],
223
+ versions=versions,
224
+ )
225
+ # 全量解析成功后再替换内存状态,损坏文件不会变成“空注册表”。
226
+ self._entries = loaded
227
+ logger.info("[Prompt] 从文件加载 %d 个提示词:%s", len(self._entries), self._path)
228
+
229
+ def _get_entry(self, name: str) -> PromptEntry:
230
+ entry = self._entries.get(name)
231
+ if entry is None:
232
+ raise KeyError(f"提示词不存在:{name}")
233
+ return entry
234
+
235
+
236
+ __all__ = ["PromptEntry", "PromptRegistry", "PromptVersion"]
@@ -0,0 +1,166 @@
1
+ """轻量、安全且不依赖第三方模板引擎的 Prompt 模板。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from string import Formatter
7
+ from typing import Any, Literal
8
+
9
+ from agent_core.protocol.messages import Message
10
+
11
+
12
+ class _SafeFormatter(Formatter):
13
+ """只允许简单变量名,禁止属性和下标访问。"""
14
+
15
+ def variables(self, template: str) -> tuple[str, ...]:
16
+ values: list[str] = []
17
+ try:
18
+ parts = list(self.parse(template))
19
+ except ValueError as exc:
20
+ raise ValueError(f"Prompt 模板语法错误:{exc}") from exc
21
+ for _literal, name, format_spec, _conversion in parts:
22
+ if name is None:
23
+ continue
24
+ if not name.isidentifier() or "." in name or "[" in name or "]" in name:
25
+ raise ValueError(f"Prompt 模板只允许简单变量名:{name!r}")
26
+ if format_spec and ("{" in format_spec or "}" in format_spec):
27
+ raise ValueError("Prompt 模板的格式说明不允许嵌套变量")
28
+ if name not in values:
29
+ values.append(name)
30
+ return tuple(values)
31
+
32
+
33
+ _FORMATTER = _SafeFormatter()
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class PromptTemplate:
38
+ """单段文本模板,支持默认变量和 partial。"""
39
+
40
+ content: str
41
+ defaults: dict[str, Any] = field(default_factory=dict)
42
+
43
+ def __post_init__(self) -> None:
44
+ variables = set(self.variables)
45
+ unknown_defaults = set(self.defaults) - variables
46
+ if unknown_defaults:
47
+ raise ValueError(f"默认变量未在模板中声明:{sorted(unknown_defaults)}")
48
+
49
+ @property
50
+ def variables(self) -> tuple[str, ...]:
51
+ return _FORMATTER.variables(self.content)
52
+
53
+ @property
54
+ def required_variables(self) -> tuple[str, ...]:
55
+ return tuple(name for name in self.variables if name not in self.defaults)
56
+
57
+ def render(self, **kwargs: Any) -> str:
58
+ values = {**self.defaults, **kwargs}
59
+ missing = [name for name in self.required_variables if name not in values]
60
+ if missing:
61
+ raise ValueError(f"Prompt 模板变量缺失:{', '.join(missing)}")
62
+ try:
63
+ return _FORMATTER.vformat(self.content, (), values)
64
+ except (KeyError, ValueError) as exc:
65
+ raise ValueError(f"Prompt 模板渲染失败:{exc}") from exc
66
+
67
+ def partial(self, **kwargs: Any) -> PromptTemplate:
68
+ unknown = set(kwargs) - set(self.variables)
69
+ if unknown:
70
+ raise ValueError(f"partial 包含未声明变量:{sorted(unknown)}")
71
+ return PromptTemplate(self.content, defaults={**self.defaults, **kwargs})
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class MessageTemplate:
76
+ """一条带角色的消息模板。"""
77
+
78
+ role: Literal["system", "user", "assistant", "tool"]
79
+ template: PromptTemplate | str
80
+ name: str | None = None
81
+
82
+ def render(self, **kwargs: Any) -> Message:
83
+ template = (
84
+ self.template if isinstance(self.template, PromptTemplate) else PromptTemplate(self.template)
85
+ )
86
+ return Message(role=self.role, content=template.render(**kwargs), name=self.name)
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class MessagesPlaceholder:
91
+ """在聊天模板中插入一段已经构造好的消息列表。"""
92
+
93
+ variable_name: str
94
+ optional: bool = False
95
+
96
+ def __post_init__(self) -> None:
97
+ if not self.variable_name.strip():
98
+ raise ValueError("MessagesPlaceholder 变量名不能为空")
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class ChatPromptTemplate:
103
+ """由消息模板和消息占位符组成的聊天 Prompt。"""
104
+
105
+ parts: tuple[MessageTemplate | MessagesPlaceholder, ...]
106
+ defaults: dict[str, Any] = field(default_factory=dict)
107
+
108
+ @classmethod
109
+ def from_messages(
110
+ cls,
111
+ parts: list[
112
+ MessageTemplate | MessagesPlaceholder | tuple[Literal["system", "user", "assistant", "tool"], str]
113
+ ],
114
+ ) -> ChatPromptTemplate:
115
+ normalized: list[MessageTemplate | MessagesPlaceholder] = []
116
+ for part in parts:
117
+ if isinstance(part, tuple):
118
+ normalized.append(MessageTemplate(role=part[0], template=part[1]))
119
+ else:
120
+ normalized.append(part)
121
+ return cls(tuple(normalized))
122
+
123
+ @property
124
+ def variables(self) -> tuple[str, ...]:
125
+ names: list[str] = []
126
+ for part in self.parts:
127
+ values: tuple[str, ...]
128
+ if isinstance(part, MessagesPlaceholder):
129
+ values = (part.variable_name,)
130
+ else:
131
+ template = (
132
+ part.template
133
+ if isinstance(part.template, PromptTemplate)
134
+ else PromptTemplate(part.template)
135
+ )
136
+ values = template.variables
137
+ for name in values:
138
+ if name not in names:
139
+ names.append(name)
140
+ return tuple(names)
141
+
142
+ def partial(self, **kwargs: Any) -> ChatPromptTemplate:
143
+ unknown = set(kwargs) - set(self.variables)
144
+ if unknown:
145
+ raise ValueError(f"partial 包含未声明变量:{sorted(unknown)}")
146
+ return ChatPromptTemplate(self.parts, defaults={**self.defaults, **kwargs})
147
+
148
+ def render(self, **kwargs: Any) -> list[Message]:
149
+ values = {**self.defaults, **kwargs}
150
+ messages: list[Message] = []
151
+ for part in self.parts:
152
+ if isinstance(part, MessagesPlaceholder):
153
+ if part.variable_name not in values:
154
+ if part.optional:
155
+ continue
156
+ raise ValueError(f"消息占位符变量缺失:{part.variable_name}")
157
+ inserted = values[part.variable_name]
158
+ if not isinstance(inserted, list) or not all(isinstance(item, Message) for item in inserted):
159
+ raise TypeError(f"消息占位符 {part.variable_name} 必须是 list[Message]")
160
+ messages.extend(inserted)
161
+ continue
162
+ messages.append(part.render(**values))
163
+ return messages
164
+
165
+
166
+ __all__ = ["ChatPromptTemplate", "MessageTemplate", "MessagesPlaceholder", "PromptTemplate"]
@@ -0,0 +1,31 @@
1
+ """Agent Core 公共协议。"""
2
+
3
+ from agent_core.protocol.capabilities import AgentCapabilities
4
+ from agent_core.protocol.messages import (
5
+ Message,
6
+ assistant_message,
7
+ system_message,
8
+ tool_message,
9
+ user_message,
10
+ )
11
+ from agent_core.protocol.runtime import (
12
+ ApprovalRecord,
13
+ RunContext,
14
+ RunEvent,
15
+ RunStatus,
16
+ ToolResult,
17
+ )
18
+
19
+ __all__ = [
20
+ "AgentCapabilities",
21
+ "ApprovalRecord",
22
+ "Message",
23
+ "RunContext",
24
+ "RunEvent",
25
+ "RunStatus",
26
+ "ToolResult",
27
+ "assistant_message",
28
+ "system_message",
29
+ "tool_message",
30
+ "user_message",
31
+ ]
@@ -0,0 +1,49 @@
1
+ """Agent HTTP 能力协议。
2
+
3
+ 能力声明放在 Core 的协议层,Web API 只负责把它序列化返回。
4
+ 这样前端可以先探测能力,再决定是否使用流式输出、审批、恢复和事件接口。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class AgentCapabilities:
15
+ """一个 Agent 服务对外暴露的通用运行能力。"""
16
+
17
+ protocol_version: str = "1.0"
18
+ agent_id: str = "agent"
19
+ runtime: str = "agent-core"
20
+ transports: tuple[str, ...] = ("http",)
21
+ features: frozenset[str] = field(
22
+ default_factory=lambda: frozenset(
23
+ {
24
+ "threads",
25
+ "runs",
26
+ "run_events",
27
+ "run_stream",
28
+ "run_cancel",
29
+ "run_resume",
30
+ "checkpoint_recovery",
31
+ "approval",
32
+ "client_actions",
33
+ "context_usage",
34
+ }
35
+ )
36
+ )
37
+
38
+ def to_dict(self) -> dict[str, Any]:
39
+ """返回稳定的 JSON 字段,供 HTTP 层直接使用。"""
40
+ return {
41
+ "protocol_version": self.protocol_version,
42
+ "agent_id": self.agent_id,
43
+ "runtime": self.runtime,
44
+ "transports": list(self.transports),
45
+ "features": sorted(self.features),
46
+ }
47
+
48
+
49
+ __all__ = ["AgentCapabilities"]