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.
Files changed (51) hide show
  1. agent_runtime/__init__.py +84 -0
  2. agent_runtime/builder.py +317 -0
  3. agent_runtime/config/__init__.py +29 -0
  4. agent_runtime/config/definitions.py +144 -0
  5. agent_runtime/config/policies.py +63 -0
  6. agent_runtime/config/storage.py +117 -0
  7. agent_runtime/context.py +10 -0
  8. agent_runtime/definitions.py +33 -0
  9. agent_runtime/discovery.py +16 -0
  10. agent_runtime/exceptions.py +74 -0
  11. agent_runtime/mcp/__init__.py +28 -0
  12. agent_runtime/mcp/discovery.py +146 -0
  13. agent_runtime/mcp/metadata.py +68 -0
  14. agent_runtime/mcp/utils.py +52 -0
  15. agent_runtime/model_registry.py +40 -0
  16. agent_runtime/plugins/__init__.py +4 -0
  17. agent_runtime/plugins/base.py +90 -0
  18. agent_runtime/plugins/default.py +19 -0
  19. agent_runtime/plugins/instructions.py +38 -0
  20. agent_runtime/plugins/loader.py +59 -0
  21. agent_runtime/policies.py +15 -0
  22. agent_runtime/runtime.py +110 -0
  23. agent_runtime/runtime_engine/__init__.py +22 -0
  24. agent_runtime/runtime_engine/a2a_bridge.py +190 -0
  25. agent_runtime/runtime_engine/a2a_task_io.py +165 -0
  26. agent_runtime/runtime_engine/agent_build.py +315 -0
  27. agent_runtime/runtime_engine/context.py +469 -0
  28. agent_runtime/runtime_engine/loading.py +170 -0
  29. agent_runtime/runtime_engine/observability.py +154 -0
  30. agent_runtime/runtime_engine/policy_registry.py +98 -0
  31. agent_runtime/runtime_engine/protocol_tools.py +94 -0
  32. agent_runtime/runtime_engine/task_flow.py +897 -0
  33. agent_runtime/runtime_engine/tool_flow.py +332 -0
  34. agent_runtime/sdk_agent.py +548 -0
  35. agent_runtime/server/__init__.py +15 -0
  36. agent_runtime/server/app_factory.py +37 -0
  37. agent_runtime/server/bootstrap.py +48 -0
  38. agent_runtime/server/endpoint_utils.py +37 -0
  39. agent_runtime/server/management.py +107 -0
  40. agent_runtime/smol/__init__.py +4 -0
  41. agent_runtime/smol/agents.py +431 -0
  42. agent_runtime/smol/llm_models.py +212 -0
  43. agent_runtime/smol/memory.py +111 -0
  44. agent_runtime/smol/models.py +69 -0
  45. agent_runtime/standalone.py +57 -0
  46. agent_runtime/storage.py +5 -0
  47. agent_runtime/tools.py +5 -0
  48. agent_runtime_sdk-0.1.0.dist-info/METADATA +125 -0
  49. agent_runtime_sdk-0.1.0.dist-info/RECORD +51 -0
  50. agent_runtime_sdk-0.1.0.dist-info/WHEEL +5 -0
  51. 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