agent-runtime-sdk 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_runtime/__init__.py +84 -0
- agent_runtime/builder.py +317 -0
- agent_runtime/config/__init__.py +29 -0
- agent_runtime/config/definitions.py +144 -0
- agent_runtime/config/policies.py +63 -0
- agent_runtime/config/storage.py +117 -0
- agent_runtime/context.py +10 -0
- agent_runtime/definitions.py +33 -0
- agent_runtime/discovery.py +16 -0
- agent_runtime/exceptions.py +74 -0
- agent_runtime/mcp/__init__.py +28 -0
- agent_runtime/mcp/discovery.py +146 -0
- agent_runtime/mcp/metadata.py +68 -0
- agent_runtime/mcp/utils.py +52 -0
- agent_runtime/model_registry.py +40 -0
- agent_runtime/plugins/__init__.py +4 -0
- agent_runtime/plugins/base.py +90 -0
- agent_runtime/plugins/default.py +19 -0
- agent_runtime/plugins/instructions.py +38 -0
- agent_runtime/plugins/loader.py +59 -0
- agent_runtime/policies.py +15 -0
- agent_runtime/runtime.py +110 -0
- agent_runtime/runtime_engine/__init__.py +22 -0
- agent_runtime/runtime_engine/a2a_bridge.py +190 -0
- agent_runtime/runtime_engine/a2a_task_io.py +165 -0
- agent_runtime/runtime_engine/agent_build.py +315 -0
- agent_runtime/runtime_engine/context.py +469 -0
- agent_runtime/runtime_engine/loading.py +170 -0
- agent_runtime/runtime_engine/observability.py +154 -0
- agent_runtime/runtime_engine/policy_registry.py +98 -0
- agent_runtime/runtime_engine/protocol_tools.py +94 -0
- agent_runtime/runtime_engine/task_flow.py +897 -0
- agent_runtime/runtime_engine/tool_flow.py +332 -0
- agent_runtime/sdk_agent.py +548 -0
- agent_runtime/server/__init__.py +15 -0
- agent_runtime/server/app_factory.py +37 -0
- agent_runtime/server/bootstrap.py +48 -0
- agent_runtime/server/endpoint_utils.py +37 -0
- agent_runtime/server/management.py +107 -0
- agent_runtime/smol/__init__.py +4 -0
- agent_runtime/smol/agents.py +431 -0
- agent_runtime/smol/llm_models.py +212 -0
- agent_runtime/smol/memory.py +111 -0
- agent_runtime/smol/models.py +69 -0
- agent_runtime/standalone.py +57 -0
- agent_runtime/storage.py +5 -0
- agent_runtime/tools.py +5 -0
- agent_runtime_sdk-0.1.0.dist-info/METADATA +125 -0
- agent_runtime_sdk-0.1.0.dist-info/RECORD +51 -0
- agent_runtime_sdk-0.1.0.dist-info/WHEEL +5 -0
- agent_runtime_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""单次请求的 agent 构建引擎。
|
|
2
|
+
|
|
3
|
+
当一条新请求到来时,这里负责把下面几样东西组装成一个可执行 agent:
|
|
4
|
+
|
|
5
|
+
- model
|
|
6
|
+
- 当前请求可用的 MCP tools
|
|
7
|
+
- runtime 内置协议工具
|
|
8
|
+
- plugin 提供的额外工具
|
|
9
|
+
- framework/plugin/instructions
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any
|
|
18
|
+
import weakref
|
|
19
|
+
|
|
20
|
+
from ..mcp.discovery import load_mcp_client_and_tools
|
|
21
|
+
from ..mcp.metadata import DiscoveredTool
|
|
22
|
+
from ..mcp.utils import merge_mcp_headers, register_tool_source
|
|
23
|
+
from ..exceptions import AgentBuildError, AgentRuntimeError, MCPToolLoadError
|
|
24
|
+
from ..plugins import ToolExecutionContext
|
|
25
|
+
from ..plugins.instructions import (
|
|
26
|
+
build_framework_instructions,
|
|
27
|
+
join_instruction_sections,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RuntimeAgentBuild:
|
|
35
|
+
"""负责把一次请求需要的资源组装成可执行 agent。"""
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _make_disconnect_cancel_hook(client: Any):
|
|
39
|
+
"""构造一个不会强持有 client 的取消钩子。
|
|
40
|
+
|
|
41
|
+
这里不能直接把 `client.disconnect` 挂到 tool 上。
|
|
42
|
+
否则会形成:
|
|
43
|
+
|
|
44
|
+
- client -> tools
|
|
45
|
+
- tool -> bound method
|
|
46
|
+
- bound method -> client
|
|
47
|
+
|
|
48
|
+
的运行时对象环。
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
disconnect = getattr(type(client), "disconnect", None)
|
|
52
|
+
if disconnect is None:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
client_ref = weakref.ref(client)
|
|
56
|
+
except TypeError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def _cancel() -> None:
|
|
60
|
+
resolved_client = client_ref()
|
|
61
|
+
if resolved_client is None:
|
|
62
|
+
return
|
|
63
|
+
disconnect(resolved_client)
|
|
64
|
+
|
|
65
|
+
return _cancel
|
|
66
|
+
|
|
67
|
+
def build_agent(self, mcp_headers: dict[str, str] | None = None):
|
|
68
|
+
"""按单次请求构建 agent。"""
|
|
69
|
+
if self.plugin is None:
|
|
70
|
+
self.reload()
|
|
71
|
+
if self.plugin is None:
|
|
72
|
+
logger.error(
|
|
73
|
+
"Plugin unavailable while building agent agent_id=%s load_error=%s",
|
|
74
|
+
self._agent.agent_id,
|
|
75
|
+
self.load_error,
|
|
76
|
+
)
|
|
77
|
+
raise AgentBuildError(
|
|
78
|
+
self.load_error
|
|
79
|
+
or f"agent {self._agent.agent_id} plugin is not available",
|
|
80
|
+
user_message="Agent 构建失败,请检查启动日志",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
model = self._build_model()
|
|
84
|
+
mcp_clients, tools = self._load_request_tools(mcp_headers or {})
|
|
85
|
+
try:
|
|
86
|
+
extra_tools = self._collect_extra_tools()
|
|
87
|
+
instructions = self._build_agent_instructions()
|
|
88
|
+
all_tools = self._compose_agent_tools(tools, extra_tools)
|
|
89
|
+
agent = self._create_agent_instance(
|
|
90
|
+
model=model,
|
|
91
|
+
all_tools=all_tools,
|
|
92
|
+
tools=tools,
|
|
93
|
+
extra_tools=extra_tools,
|
|
94
|
+
instructions=instructions,
|
|
95
|
+
)
|
|
96
|
+
self._attach_request_resources(agent, mcp_clients)
|
|
97
|
+
logger.debug(
|
|
98
|
+
"Built agent instance agent_id=%s mcp_tools=%s extra_tools=%s",
|
|
99
|
+
self._agent.agent_id,
|
|
100
|
+
len(tools),
|
|
101
|
+
len(extra_tools),
|
|
102
|
+
)
|
|
103
|
+
return agent
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.exception("Failed to build agent agent_id=%s", self._agent.agent_id)
|
|
106
|
+
for client in mcp_clients:
|
|
107
|
+
try:
|
|
108
|
+
client.disconnect()
|
|
109
|
+
except Exception:
|
|
110
|
+
logger.warning(
|
|
111
|
+
"Failed to disconnect MCP client while rolling back agent build agent_id=%s",
|
|
112
|
+
self._agent.agent_id,
|
|
113
|
+
exc_info=True,
|
|
114
|
+
)
|
|
115
|
+
if isinstance(exc, AgentRuntimeError):
|
|
116
|
+
raise
|
|
117
|
+
raise AgentBuildError(
|
|
118
|
+
f"failed to build agent '{self._agent.agent_id}': {exc}",
|
|
119
|
+
) from exc
|
|
120
|
+
|
|
121
|
+
def _build_model(self, model_cls=None):
|
|
122
|
+
model_settings = self._runtime_config.model
|
|
123
|
+
if model_cls is not None:
|
|
124
|
+
return model_cls(
|
|
125
|
+
api_base=model_settings.api_base or "",
|
|
126
|
+
model_id=model_settings.model_id or "",
|
|
127
|
+
api_key=os.getenv(model_settings.api_key_env, ""),
|
|
128
|
+
flatten_messages_as_text=model_settings.flatten_messages_as_text,
|
|
129
|
+
)
|
|
130
|
+
from ..model_registry import ModelRegistry
|
|
131
|
+
|
|
132
|
+
return ModelRegistry.create(
|
|
133
|
+
model_settings.provider,
|
|
134
|
+
api_base=model_settings.api_base or "",
|
|
135
|
+
model_id=model_settings.model_id or "",
|
|
136
|
+
api_key=os.getenv(model_settings.api_key_env, ""),
|
|
137
|
+
flatten_messages_as_text=model_settings.flatten_messages_as_text,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _load_request_tools(
|
|
141
|
+
self, request_headers: dict[str, str]
|
|
142
|
+
) -> tuple[list[Any], list[Any]]:
|
|
143
|
+
clients: list[Any] = []
|
|
144
|
+
enabled_tools: list[Any] = []
|
|
145
|
+
loaded_tool_sources: dict[str, str] = {}
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
for mcp in self._mcps:
|
|
149
|
+
if not mcp.enabled:
|
|
150
|
+
logger.debug(
|
|
151
|
+
"Skipping disabled MCP during runtime load agent_id=%s mcp=%s",
|
|
152
|
+
self._agent.agent_id,
|
|
153
|
+
mcp.name,
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
156
|
+
logger.debug(
|
|
157
|
+
"Loading MCP tools agent_id=%s mcp=%s url=%s",
|
|
158
|
+
self._agent.agent_id,
|
|
159
|
+
mcp.name,
|
|
160
|
+
mcp.url,
|
|
161
|
+
)
|
|
162
|
+
client, tools = load_mcp_client_and_tools(
|
|
163
|
+
mcp,
|
|
164
|
+
headers=self._request_mcp_headers(mcp, request_headers),
|
|
165
|
+
)
|
|
166
|
+
clients.append(client)
|
|
167
|
+
|
|
168
|
+
for tool in tools:
|
|
169
|
+
tool_name = getattr(tool, "name", "")
|
|
170
|
+
if not tool_name or not self._is_tool_enabled(tool_name):
|
|
171
|
+
continue
|
|
172
|
+
register_tool_source(loaded_tool_sources, tool_name, mcp.name)
|
|
173
|
+
try:
|
|
174
|
+
tool._runtime_mcp_name = mcp.name
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
try:
|
|
178
|
+
tool._runtime_cancel_hook = self._make_disconnect_cancel_hook(
|
|
179
|
+
client
|
|
180
|
+
)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
enabled_tools.append(tool)
|
|
184
|
+
logger.debug(
|
|
185
|
+
"Loaded enabled MCP tools agent_id=%s tool_count=%s",
|
|
186
|
+
self._agent.agent_id,
|
|
187
|
+
len(enabled_tools),
|
|
188
|
+
)
|
|
189
|
+
return clients, enabled_tools
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
logger.exception(
|
|
192
|
+
"Failed to load MCP tools agent_id=%s", self._agent.agent_id
|
|
193
|
+
)
|
|
194
|
+
for client in clients:
|
|
195
|
+
try:
|
|
196
|
+
client.disconnect()
|
|
197
|
+
except Exception:
|
|
198
|
+
logger.warning(
|
|
199
|
+
"Failed to disconnect MCP client during tool load rollback agent_id=%s",
|
|
200
|
+
self._agent.agent_id,
|
|
201
|
+
exc_info=True,
|
|
202
|
+
)
|
|
203
|
+
if isinstance(exc, MCPToolLoadError):
|
|
204
|
+
raise
|
|
205
|
+
raise MCPToolLoadError(
|
|
206
|
+
f"failed to load MCP tools for agent '{self._agent.agent_id}': {exc}",
|
|
207
|
+
) from exc
|
|
208
|
+
|
|
209
|
+
def _collect_extra_tools(self) -> list[Any]:
|
|
210
|
+
if self.plugin is None:
|
|
211
|
+
return []
|
|
212
|
+
return self.plugin.extra_tools(self.definition)
|
|
213
|
+
|
|
214
|
+
def _build_agent_instructions(self) -> str:
|
|
215
|
+
plugin_instructions = ""
|
|
216
|
+
if self.plugin is not None:
|
|
217
|
+
plugin_instructions = self.plugin.build_instructions(
|
|
218
|
+
self.definition, self.discovered_tools
|
|
219
|
+
)
|
|
220
|
+
return join_instruction_sections(
|
|
221
|
+
build_framework_instructions(self.definition, self.discovered_tools),
|
|
222
|
+
self._agent.extra_instructions,
|
|
223
|
+
plugin_instructions,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _compose_agent_tools(tools: list[Any], extra_tools: list[Any]) -> list[Any]:
|
|
228
|
+
from .protocol_tools import AskAuthTool, AskUserTool, FinalAnswerTool
|
|
229
|
+
|
|
230
|
+
return [*tools, AskUserTool(), AskAuthTool(), FinalAnswerTool(), *extra_tools]
|
|
231
|
+
|
|
232
|
+
def _create_agent_instance(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
model: Any,
|
|
236
|
+
all_tools: list[Any],
|
|
237
|
+
tools: list[Any],
|
|
238
|
+
extra_tools: list[Any],
|
|
239
|
+
instructions: str,
|
|
240
|
+
):
|
|
241
|
+
from ..smol import ToolCallingCheckAgent
|
|
242
|
+
|
|
243
|
+
return ToolCallingCheckAgent(
|
|
244
|
+
model=model,
|
|
245
|
+
tools=all_tools,
|
|
246
|
+
prompt_templates=None,
|
|
247
|
+
instructions=instructions,
|
|
248
|
+
tool_callbacks=self._build_tool_callbacks(tools, extra_tools),
|
|
249
|
+
max_steps=self._runtime_config.model.max_steps,
|
|
250
|
+
stream_outputs=False,
|
|
251
|
+
name=_safe_agent_name(self._agent.agent_id),
|
|
252
|
+
description=self._agent.description,
|
|
253
|
+
after_tool_hook=self._after_tool_hook,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _attach_request_resources(agent: Any, mcp_clients: list[Any]) -> None:
|
|
258
|
+
agent._runtime_mcp_clients = mcp_clients
|
|
259
|
+
|
|
260
|
+
def format_result(self, result: Any) -> Any:
|
|
261
|
+
if self.plugin is None:
|
|
262
|
+
return result
|
|
263
|
+
formatted = self.plugin.format_result(
|
|
264
|
+
ToolExecutionContext(
|
|
265
|
+
agent_definition=self.definition,
|
|
266
|
+
tool_name="final_answer",
|
|
267
|
+
args={},
|
|
268
|
+
result=result,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return result if formatted is None else formatted
|
|
272
|
+
|
|
273
|
+
def list_tools(self, mcp_name: str | None = None) -> list[DiscoveredTool]:
|
|
274
|
+
"""返回 discovery 阶段的工具摘要,不返回可执行 tool 实例。"""
|
|
275
|
+
|
|
276
|
+
if mcp_name is None:
|
|
277
|
+
return list(self.discovered_tools)
|
|
278
|
+
return [tool for tool in self.discovered_tools if tool.source_mcp == mcp_name]
|
|
279
|
+
|
|
280
|
+
def get_tool(
|
|
281
|
+
self, tool_name: str, mcp_name: str | None = None
|
|
282
|
+
) -> DiscoveredTool | None:
|
|
283
|
+
"""按名字获取 discovery 阶段工具摘要。"""
|
|
284
|
+
|
|
285
|
+
if mcp_name is None:
|
|
286
|
+
return next((t for t in self.discovered_tools if t.name == tool_name), None)
|
|
287
|
+
return next(
|
|
288
|
+
(
|
|
289
|
+
t
|
|
290
|
+
for t in self.discovered_tools
|
|
291
|
+
if t.name == tool_name and t.source_mcp == mcp_name
|
|
292
|
+
),
|
|
293
|
+
None,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def _request_mcp_headers(
|
|
297
|
+
self, mcp, request_headers: dict[str, str]
|
|
298
|
+
) -> dict[str, str]:
|
|
299
|
+
passed_headers = {
|
|
300
|
+
header_name: request_headers.get(header_name, "")
|
|
301
|
+
for header_name in mcp.pass_through_headers
|
|
302
|
+
}
|
|
303
|
+
return merge_mcp_headers(mcp, passed_headers)
|
|
304
|
+
|
|
305
|
+
def _is_tool_enabled(self, tool_name: str) -> bool:
|
|
306
|
+
return self._policy_registry.is_tool_enabled(tool_name)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _safe_agent_name(value: str) -> str:
|
|
310
|
+
normalized = re.sub(r"[^0-9a-zA-Z_]", "_", value)
|
|
311
|
+
if not normalized:
|
|
312
|
+
return "agent"
|
|
313
|
+
if normalized[0].isdigit():
|
|
314
|
+
normalized = f"agent_{normalized}"
|
|
315
|
+
return normalized
|