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,548 @@
1
+ from __future__ import annotations
2
+
3
+ """面向 SDK 使用者的类式 agent 抽象。
4
+
5
+ 设计目标:
6
+
7
+ 1. YAML 配置方式和 Python 类方式都收敛到同一个规范层:`AgentDefinition`
8
+ 2. 业务方既可以直接声明完整 definition,也可以只覆写少量小钩子
9
+ 3. 基类数量不追求多,而是提供少量高价值的“半成品基类”
10
+
11
+ 建议分层:
12
+
13
+ - `BaseSDKAgent`: 只管 definition / builder / runtime / app 生命周期
14
+ - `DeclarativeSDKAgent`: 适合直接返回完整 definition 的场景
15
+ - `ConfiguredSDKAgent`: 适合用细粒度 hook 组装 definition
16
+ - `SingleMCPAgent`: 单 MCP 场景的高频半成品
17
+ - `OpenAICompatibleSingleMCPAgent`: 单 MCP + OpenAI-compatible 模型的常见半成品
18
+ """
19
+
20
+ import os
21
+ from abc import ABC, abstractmethod
22
+ from typing import Any, Callable
23
+
24
+ from .builder import AgentBuilder
25
+ from .config.definitions import (
26
+ DEFAULT_PASS_THROUGH_HEADERS,
27
+ A2ASettings,
28
+ AgentConfig,
29
+ AgentDefinition,
30
+ MCPSettings,
31
+ ModelSettings,
32
+ PluginSettings,
33
+ RuntimeConfig,
34
+ SkillDefinition,
35
+ ToolPolicy,
36
+ )
37
+ from .mcp.metadata import DiscoveredTool
38
+ from .plugins.base import BaseAgentPlugin
39
+
40
+
41
+ def _coerce_agent_config(value: AgentConfig | dict[str, Any]) -> AgentConfig:
42
+ return value if isinstance(value, AgentConfig) else AgentConfig.model_validate(value)
43
+
44
+
45
+ def _coerce_runtime_config(
46
+ value: RuntimeConfig | dict[str, Any]
47
+ ) -> RuntimeConfig:
48
+ return (
49
+ value if isinstance(value, RuntimeConfig) else RuntimeConfig.model_validate(value)
50
+ )
51
+
52
+
53
+ def _coerce_model_settings(
54
+ value: ModelSettings | dict[str, Any]
55
+ ) -> ModelSettings:
56
+ return value if isinstance(value, ModelSettings) else ModelSettings.model_validate(value)
57
+
58
+
59
+ def _coerce_plugin_settings(
60
+ value: PluginSettings | dict[str, Any] | None,
61
+ ) -> PluginSettings | None:
62
+ if value is None or isinstance(value, PluginSettings):
63
+ return value
64
+ return PluginSettings.model_validate(value)
65
+
66
+
67
+ def _coerce_skill_definition(
68
+ value: SkillDefinition | dict[str, Any]
69
+ ) -> SkillDefinition:
70
+ return (
71
+ value if isinstance(value, SkillDefinition) else SkillDefinition.model_validate(value)
72
+ )
73
+
74
+
75
+ def _coerce_tool_policies(
76
+ policies: dict[str, ToolPolicy | dict[str, Any]] | None,
77
+ ) -> dict[str, ToolPolicy]:
78
+ resolved: dict[str, ToolPolicy] = {}
79
+ for tool_name, policy in (policies or {}).items():
80
+ resolved[tool_name] = (
81
+ policy if isinstance(policy, ToolPolicy) else ToolPolicy.model_validate(policy)
82
+ )
83
+ return resolved
84
+
85
+
86
+ def _coerce_mcp_settings(
87
+ items: list[MCPSettings | dict[str, Any]]
88
+ ) -> list[MCPSettings]:
89
+ resolved: list[MCPSettings] = []
90
+ for item in items:
91
+ resolved.append(
92
+ item if isinstance(item, MCPSettings) else MCPSettings.model_validate(item)
93
+ )
94
+ return resolved
95
+
96
+
97
+ def _resolve_string_setting(
98
+ *,
99
+ explicit: str | None,
100
+ env_var: str | None = None,
101
+ default: str | None = None,
102
+ ) -> str | None:
103
+ """统一处理“显式值 -> 环境变量 -> 默认值”的解析顺序。"""
104
+
105
+ if explicit is not None:
106
+ return explicit
107
+ if env_var:
108
+ env_value = os.getenv(env_var)
109
+ if env_value:
110
+ return env_value
111
+ return default
112
+
113
+
114
+ class BaseSDKAgent(ABC):
115
+ """类方式 agent 的最小生命周期基类。
116
+
117
+ 这个类不关心业务字段长什么样,只约束一个核心事实:
118
+
119
+ - Python 类方式最终必须先收敛为 `AgentDefinition`
120
+
121
+ 这样:
122
+ - YAML 入口和 Python 入口共用同一个 definition/spec 层
123
+ - `AgentBuilder.from_definition(...)` 成为统一装配路径
124
+ """
125
+
126
+ def definition(self) -> AgentDefinition:
127
+ """返回当前 agent 的规范化 definition。
128
+
129
+ 这里把 `build_definition()` 的结果统一转成 `AgentDefinition`,
130
+ 并给子类一个最终修改 definition 的扩展点。
131
+ """
132
+
133
+ raw = self.build_definition()
134
+ definition = (
135
+ raw if isinstance(raw, AgentDefinition) else AgentDefinition.model_validate(raw)
136
+ )
137
+ # 统一用深拷贝,避免子类返回共享对象后被后续调用原地改坏。
138
+ definition = definition.model_copy(deep=True)
139
+ customized = self.customize_definition(definition)
140
+ return definition if customized is None else customized
141
+
142
+ @abstractmethod
143
+ def build_definition(self) -> AgentDefinition | dict[str, Any]:
144
+ """构造当前 agent 的 definition/spec。"""
145
+
146
+ def builder(self) -> AgentBuilder:
147
+ """把当前类式 agent 转成 `AgentBuilder`。
148
+
149
+ 这个方法是类方式和 builder 方式之间的桥接点。
150
+ """
151
+
152
+ builder = AgentBuilder.from_definition(self.definition())
153
+
154
+ explicit_model_api_key = self.model_api_key()
155
+ if explicit_model_api_key is not None:
156
+ builder.model(api_key=explicit_model_api_key)
157
+
158
+ discoverer = self.discoverer()
159
+ if discoverer is not None:
160
+ builder.discoverer(discoverer)
161
+
162
+ plugin = self.plugin_instance()
163
+ if plugin is not None:
164
+ builder.plugin(plugin)
165
+
166
+ self.customize_builder(builder)
167
+ return builder
168
+
169
+ def build_runtime(self):
170
+ """构造并返回 `ManagedAgentRuntime`。"""
171
+
172
+ return self.builder().build()
173
+
174
+ def build_app(
175
+ self,
176
+ *,
177
+ enable_management: bool = False,
178
+ public_url: str | None = None,
179
+ ):
180
+ """构造并返回可直接挂到 uvicorn 的 A2A app。"""
181
+
182
+ return self.builder().build_a2a_app(
183
+ public_url=public_url,
184
+ enable_management=enable_management,
185
+ )
186
+
187
+ def model_api_key(self) -> str | None:
188
+ """可选:返回只存在于运行时的显式 API Key。
189
+
190
+ 之所以不放进 `AgentDefinition`,是因为 definition 适合作为配置规格,
191
+ 而不是密钥载体。
192
+ """
193
+
194
+ return None
195
+
196
+ def plugin_instance(self) -> BaseAgentPlugin | None:
197
+ """可选:返回业务 plugin 实例。
198
+
199
+ 如果这里返回实例,会覆盖 definition 里的 `runtime.plugin` 加载路径。
200
+ """
201
+
202
+ return None
203
+
204
+ def discoverer(self) -> Callable[..., list[DiscoveredTool]] | None:
205
+ """可选:返回自定义 MCP discoverer。"""
206
+
207
+ return None
208
+
209
+ def customize_definition(
210
+ self, definition: AgentDefinition
211
+ ) -> AgentDefinition | None:
212
+ """可选:在 definition 完成后做最后修正。"""
213
+
214
+ return definition
215
+
216
+ def customize_builder(self, builder: AgentBuilder) -> None:
217
+ """可选:在 builder 完成后做最后修正。"""
218
+
219
+ return None
220
+
221
+
222
+ class DeclarativeSDKAgent(BaseSDKAgent):
223
+ """适合“我已经有完整 definition,只想走类入口”的场景。"""
224
+
225
+ @abstractmethod
226
+ def declared_definition(self) -> AgentDefinition | dict[str, Any]:
227
+ """返回完整 definition。"""
228
+
229
+ def build_definition(self) -> AgentDefinition | dict[str, Any]:
230
+ return self.declared_definition()
231
+
232
+
233
+ class ConfiguredSDKAgent(BaseSDKAgent):
234
+ """适合通过细粒度 hook 组装 definition 的通用基类。
235
+
236
+ 这层的重点不是“业务一定要全部重写”,而是把常见变更点拆小:
237
+
238
+ - 身份信息拆成 `agent_id/name/description/version`
239
+ - 模型拆成 provider/base/model_id/api_key_env/max_steps
240
+ - A2A 展示层拆成 skills 和输入输出模式
241
+ - MCP 拆成一个或多个 `MCPSettings`
242
+ """
243
+
244
+ def build_definition(self) -> AgentDefinition:
245
+ return AgentDefinition(
246
+ agent=_coerce_agent_config(self.agent_config()),
247
+ runtime=_coerce_runtime_config(self.runtime_config()),
248
+ mcps=_coerce_mcp_settings(self.mcp_settings()),
249
+ )
250
+
251
+ def agent_config(self) -> AgentConfig | dict[str, Any]:
252
+ return AgentConfig(
253
+ agent_id=self.agent_id(),
254
+ name=self.agent_name(),
255
+ description=self.agent_description(),
256
+ version=self.agent_version(),
257
+ a2a=self.a2a_settings(),
258
+ extra_instructions=self.extra_instructions(),
259
+ )
260
+
261
+ def runtime_config(self) -> RuntimeConfig | dict[str, Any]:
262
+ return RuntimeConfig(
263
+ base_path=self.base_path(),
264
+ public_base_url=self.public_base_url(),
265
+ model=_coerce_model_settings(self.model_settings()),
266
+ plugin=_coerce_plugin_settings(self.plugin_settings()),
267
+ )
268
+
269
+ def model_settings(self) -> ModelSettings | dict[str, Any]:
270
+ return ModelSettings(
271
+ provider=self.model_provider(),
272
+ api_base=self.model_api_base(),
273
+ model_id=self.model_id(),
274
+ api_key_env=self.model_api_key_env(),
275
+ max_steps=self.model_max_steps(),
276
+ flatten_messages_as_text=self.model_flatten_messages_as_text(),
277
+ )
278
+
279
+ def a2a_settings(self) -> A2ASettings:
280
+ return A2ASettings(
281
+ skills=[_coerce_skill_definition(skill) for skill in self.a2a_skills()],
282
+ default_input_modes=list(self.a2a_default_input_modes()),
283
+ default_output_modes=list(self.a2a_default_output_modes()),
284
+ )
285
+
286
+ @abstractmethod
287
+ def agent_id(self) -> str:
288
+ """返回稳定的内部 agent 标识。"""
289
+
290
+ def agent_name(self) -> str:
291
+ """返回展示名称。
292
+
293
+ 默认回退到 `agent_id()`,让最小子类只实现一个必填钩子也能工作。
294
+ """
295
+
296
+ return self.agent_id()
297
+
298
+ def agent_description(self) -> str:
299
+ """返回展示描述。默认回退到 `agent_name()`。"""
300
+
301
+ return self.agent_name()
302
+
303
+ def agent_version(self) -> str:
304
+ return "0.1.0"
305
+
306
+ def extra_instructions(self) -> str:
307
+ return ""
308
+
309
+ def a2a_skills(self) -> list[SkillDefinition | dict[str, Any]]:
310
+ return []
311
+
312
+ def a2a_default_input_modes(self) -> list[str]:
313
+ return ["text"]
314
+
315
+ def a2a_default_output_modes(self) -> list[str]:
316
+ return ["text"]
317
+
318
+ def base_path(self) -> str:
319
+ return "."
320
+
321
+ def public_base_url(self) -> str | None:
322
+ return None
323
+
324
+ def model_provider(self) -> str:
325
+ return "openai_compatible"
326
+
327
+ def model_api_base(self) -> str | None:
328
+ return None
329
+
330
+ def model_id(self) -> str | None:
331
+ return None
332
+
333
+ def model_api_key_env(self) -> str:
334
+ return "MODEL_SOURCE_API_KEY"
335
+
336
+ def model_max_steps(self) -> int:
337
+ return 15
338
+
339
+ def model_flatten_messages_as_text(self) -> bool:
340
+ return True
341
+
342
+ def plugin_settings(self) -> PluginSettings | dict[str, Any] | None:
343
+ return None
344
+
345
+ @abstractmethod
346
+ def mcp_settings(self) -> list[MCPSettings | dict[str, Any]]:
347
+ """返回一个或多个 MCP 绑定。"""
348
+
349
+
350
+ class SingleMCPAgent(ConfiguredSDKAgent):
351
+ """单 MCP 场景的半成品基类。"""
352
+
353
+ def mcp_settings(self) -> list[MCPSettings]:
354
+ return [
355
+ MCPSettings(
356
+ name=self.mcp_name(),
357
+ url=self.mcp_url(),
358
+ transport=self.mcp_transport(),
359
+ enabled=self.mcp_enabled(),
360
+ retry_count=self.mcp_retry_count(),
361
+ retry_delay_seconds=self.mcp_retry_delay_seconds(),
362
+ static_headers=dict(self.mcp_static_headers()),
363
+ pass_through_headers=list(self.mcp_pass_through_headers()),
364
+ tool_policies=_coerce_tool_policies(self.mcp_tool_policies()),
365
+ )
366
+ ]
367
+
368
+ @abstractmethod
369
+ def mcp_name(self) -> str:
370
+ """返回单个 MCP 的名称。"""
371
+
372
+ @abstractmethod
373
+ def mcp_url(self) -> str:
374
+ """返回单个 MCP 的地址。"""
375
+
376
+ def mcp_transport(self) -> str:
377
+ return "streamable-http"
378
+
379
+ def mcp_enabled(self) -> bool:
380
+ return True
381
+
382
+ def mcp_retry_count(self) -> int:
383
+ return 3
384
+
385
+ def mcp_retry_delay_seconds(self) -> float:
386
+ return 1.0
387
+
388
+ def mcp_static_headers(self) -> dict[str, str]:
389
+ return {}
390
+
391
+ def mcp_pass_through_headers(self) -> list[str]:
392
+ return list(DEFAULT_PASS_THROUGH_HEADERS)
393
+
394
+ def mcp_tool_policies(self) -> dict[str, ToolPolicy | dict[str, Any]]:
395
+ return {}
396
+
397
+
398
+ class OpenAICompatibleSingleMCPAgent(SingleMCPAgent):
399
+ """单 MCP + OpenAI-compatible 模型的常用半成品基类。
400
+
401
+ 这个类面向业务开发者的目标是:
402
+
403
+ - 不用关心 `AgentBuilder` 细节
404
+ - 常见配置支持“显式参数 -> 环境变量 -> 默认值”三级覆盖
405
+ - 只需要覆写少量语义化小钩子
406
+ """
407
+
408
+ def __init__(
409
+ self,
410
+ *,
411
+ public_base_url: str | None = None,
412
+ model_api_base: str | None = None,
413
+ model_id: str | None = None,
414
+ model_api_key: str | None = None,
415
+ mcp_url: str | None = None,
416
+ mcp_transport: str | None = None,
417
+ mcp_retry_count: int | None = None,
418
+ mcp_retry_delay_seconds: float | None = None,
419
+ max_steps: int | None = None,
420
+ flatten_messages_as_text: bool | None = None,
421
+ ) -> None:
422
+ self._public_base_url_override = public_base_url
423
+ self._model_api_base_override = model_api_base
424
+ self._model_id_override = model_id
425
+ self._model_api_key_override = model_api_key
426
+ self._mcp_url_override = mcp_url
427
+ self._mcp_transport_override = mcp_transport
428
+ self._mcp_retry_count_override = mcp_retry_count
429
+ self._mcp_retry_delay_seconds_override = mcp_retry_delay_seconds
430
+ self._model_max_steps_override = max_steps
431
+ self._flatten_messages_as_text_override = flatten_messages_as_text
432
+
433
+ def public_base_url(self) -> str | None:
434
+ return _resolve_string_setting(
435
+ explicit=self._public_base_url_override,
436
+ env_var=self.public_base_url_env_var(),
437
+ default=self.default_public_base_url(),
438
+ )
439
+
440
+ def model_provider(self) -> str:
441
+ return "openai_compatible"
442
+
443
+ def model_api_base(self) -> str | None:
444
+ return _resolve_string_setting(
445
+ explicit=self._model_api_base_override,
446
+ env_var=self.model_api_base_env_var(),
447
+ default=self.default_model_api_base(),
448
+ )
449
+
450
+ def model_id(self) -> str | None:
451
+ return _resolve_string_setting(
452
+ explicit=self._model_id_override,
453
+ env_var=self.model_id_env_var(),
454
+ default=self.default_model_id(),
455
+ )
456
+
457
+ def model_api_key(self) -> str | None:
458
+ # 这里有意只返回显式传入的 key。
459
+ #
460
+ # 正常情况下,SDK 仍建议通过 `model_api_key_env()` 指定环境变量读取。
461
+ # 只有业务方明确想在代码里塞一个临时 key 时,才走这个运行时 override。
462
+ return self._model_api_key_override
463
+
464
+ def model_max_steps(self) -> int:
465
+ if self._model_max_steps_override is not None:
466
+ return self._model_max_steps_override
467
+ return self.default_model_max_steps()
468
+
469
+ def model_flatten_messages_as_text(self) -> bool:
470
+ if self._flatten_messages_as_text_override is not None:
471
+ return self._flatten_messages_as_text_override
472
+ return self.default_model_flatten_messages_as_text()
473
+
474
+ def mcp_url(self) -> str:
475
+ value = _resolve_string_setting(
476
+ explicit=self._mcp_url_override,
477
+ env_var=self.mcp_url_env_var(),
478
+ default=self.default_mcp_url(),
479
+ )
480
+ if not value:
481
+ raise ValueError(
482
+ "mcp_url is not configured; pass mcp_url=..., "
483
+ f"set env {self.mcp_url_env_var()!r}, or override default_mcp_url()."
484
+ )
485
+ return value
486
+
487
+ def mcp_transport(self) -> str:
488
+ value = _resolve_string_setting(
489
+ explicit=self._mcp_transport_override,
490
+ env_var=self.mcp_transport_env_var(),
491
+ default=self.default_mcp_transport(),
492
+ )
493
+ return value or "streamable-http"
494
+
495
+ def mcp_retry_count(self) -> int:
496
+ if self._mcp_retry_count_override is not None:
497
+ return self._mcp_retry_count_override
498
+ return self.default_mcp_retry_count()
499
+
500
+ def mcp_retry_delay_seconds(self) -> float:
501
+ if self._mcp_retry_delay_seconds_override is not None:
502
+ return self._mcp_retry_delay_seconds_override
503
+ return self.default_mcp_retry_delay_seconds()
504
+
505
+ def public_base_url_env_var(self) -> str | None:
506
+ return "AGENT_PUBLIC_BASE_URL"
507
+
508
+ def default_public_base_url(self) -> str | None:
509
+ return None
510
+
511
+ def model_api_base_env_var(self) -> str | None:
512
+ return "MODEL_SOURCE_API_BASE"
513
+
514
+ def default_model_api_base(self) -> str | None:
515
+ return None
516
+
517
+ def model_id_env_var(self) -> str | None:
518
+ return "MODEL_SOURCE_MODEL_ID"
519
+
520
+ def default_model_id(self) -> str | None:
521
+ return None
522
+
523
+ def model_api_key_env(self) -> str:
524
+ return "MODEL_SOURCE_API_KEY"
525
+
526
+ def default_model_max_steps(self) -> int:
527
+ return 15
528
+
529
+ def default_model_flatten_messages_as_text(self) -> bool:
530
+ return True
531
+
532
+ def mcp_url_env_var(self) -> str | None:
533
+ return None
534
+
535
+ def default_mcp_url(self) -> str | None:
536
+ return None
537
+
538
+ def mcp_transport_env_var(self) -> str | None:
539
+ return None
540
+
541
+ def default_mcp_transport(self) -> str | None:
542
+ return "streamable-http"
543
+
544
+ def default_mcp_retry_count(self) -> int:
545
+ return 3
546
+
547
+ def default_mcp_retry_delay_seconds(self) -> float:
548
+ return 1.0
@@ -0,0 +1,15 @@
1
+ """服务装配层内部实现。"""
2
+
3
+ from .app_factory import build_app_from_runtime
4
+ from .bootstrap import DEFAULT_DEFINITION_PATH, load_single_agent_runtime
5
+ from .endpoint_utils import port_from_public_base_url, resolve_public_base_url
6
+ from .management import create_management_router
7
+
8
+ __all__ = [
9
+ "DEFAULT_DEFINITION_PATH",
10
+ "build_app_from_runtime",
11
+ "create_management_router",
12
+ "load_single_agent_runtime",
13
+ "port_from_public_base_url",
14
+ "resolve_public_base_url",
15
+ ]
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from ..runtime import ManagedAgentRuntime
6
+
7
+
8
+ def build_app_from_runtime(
9
+ runtime: ManagedAgentRuntime,
10
+ enable_management: bool = True,
11
+ ) -> FastAPI:
12
+ """把已经构造好的 runtime 包成 FastAPI 应用。"""
13
+
14
+ if runtime.load_error:
15
+ if runtime.load_exception is not None:
16
+ raise runtime.load_exception
17
+ raise RuntimeError(runtime.load_error)
18
+
19
+ app = FastAPI(title=runtime.definition.agent.name)
20
+ app.state.runtime = runtime
21
+
22
+ @app.get("/healthz")
23
+ def healthz():
24
+ return {
25
+ "status": runtime.status,
26
+ "agent_id": runtime.definition.agent.agent_id,
27
+ "tool_count": len(runtime.discovered_tools),
28
+ "load_error": runtime.load_error,
29
+ }
30
+
31
+ if enable_management:
32
+ from .management import create_management_router
33
+
34
+ app.include_router(create_management_router(runtime))
35
+
36
+ app.mount("/", runtime.get_asgi_app())
37
+ return app
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ from .endpoint_utils import resolve_public_base_url
7
+ from ..config.storage import AgentDefinitionStore
8
+ from ..runtime import ManagedAgentRuntime
9
+
10
+
11
+ DEFAULT_DEFINITION_PATH = Path.cwd() / "agent_app" / "agent.yaml"
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def load_single_agent_runtime(
16
+ definition_path: str | Path = DEFAULT_DEFINITION_PATH,
17
+ public_base_url: str | None = None,
18
+ bind_port: int | None = None,
19
+ discoverer=None,
20
+ ) -> ManagedAgentRuntime:
21
+ """加载一个 definition,并立即完成 runtime reload。"""
22
+
23
+ logger.info("Loading single-agent runtime definition_path=%s", definition_path)
24
+ definition = AgentDefinitionStore(Path(definition_path).resolve().parent).load_path(definition_path)
25
+ resolved_public_base_url = resolve_public_base_url(
26
+ definition,
27
+ public_base_url=public_base_url,
28
+ bind_port=bind_port,
29
+ )
30
+ runtime = ManagedAgentRuntime(
31
+ definition=definition,
32
+ public_base_url=resolved_public_base_url,
33
+ discoverer=discoverer,
34
+ ).reload()
35
+ if runtime.load_error:
36
+ logger.error(
37
+ "Runtime loaded with error agent_id=%s load_error=%s",
38
+ runtime.definition.agent.agent_id,
39
+ runtime.load_error,
40
+ )
41
+ else:
42
+ logger.info(
43
+ "Runtime ready agent_id=%s public_base_url=%s discovered_tools=%s",
44
+ runtime.definition.agent.agent_id,
45
+ runtime.public_url(),
46
+ len(runtime.discovered_tools),
47
+ )
48
+ return runtime
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import urlparse
4
+
5
+ from ..config.definitions import AgentDefinition
6
+
7
+
8
+ def port_from_public_base_url(public_base_url: str | None) -> int | None:
9
+ """从 public_base_url 中提取显式端口。"""
10
+
11
+ normalized = (public_base_url or "").rstrip("/")
12
+ if not normalized:
13
+ return None
14
+ parsed = urlparse(normalized)
15
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
16
+ return None
17
+ return parsed.port
18
+
19
+
20
+ def resolve_public_base_url(
21
+ definition: AgentDefinition,
22
+ *,
23
+ public_base_url: str | None = None,
24
+ bind_port: int | None = None,
25
+ public_host: str = "127.0.0.1",
26
+ ) -> str:
27
+ """解析运行时最终使用的 public_base_url。"""
28
+
29
+ resolved_public_base_url = (public_base_url or definition.runtime.public_base_url or "").rstrip("/")
30
+ if resolved_public_base_url:
31
+ return resolved_public_base_url
32
+ if bind_port is not None and bind_port > 0:
33
+ return f"http://{public_host}:{bind_port}"
34
+ raise ValueError(
35
+ "public_base_url must be configured in agent_app/agent.yaml or passed explicitly; "
36
+ "if you only want to bind a local port, pass bind_port/run_single_agent_app(..., port=...)."
37
+ )