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,469 @@
1
+ """运行时任务上下文与等待状态模型。
2
+
3
+ 这个文件只放 runtime 共享的状态定义,不放具体业务流程:
4
+
5
+ - `current_task_id` / `current_task_pool`: 让运行中的 agent 能拿到当前任务上下文
6
+ - `TaskContext`: 一次任务在执行期需要保存的状态
7
+ - `WaitState`: runtime 暂停等待用户输入或授权时的统一模型
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import contextvars
14
+ import threading
15
+ from concurrent.futures import Future, ThreadPoolExecutor
16
+ from dataclasses import dataclass
17
+ from enum import StrEnum
18
+ from typing import Any, Callable, Protocol
19
+
20
+
21
+ current_task_id: contextvars.ContextVar[str] = contextvars.ContextVar("current_task_id")
22
+ current_task_pool: contextvars.ContextVar["TaskPool"] = contextvars.ContextVar(
23
+ "current_task_pool"
24
+ )
25
+
26
+ WAIT_TYPE_INPUT_REQUIRED = "input_required"
27
+ WAIT_TYPE_AUTH_REQUIRED = "auth_required"
28
+
29
+
30
+ class TaskPhase(StrEnum):
31
+ """任务内部生命周期阶段。
32
+
33
+ 这里的 phase 是 runtime 内部状态,不直接等价于 A2A 的 TaskState。
34
+ 它的目标是让 task_flow / tool_flow 用一套明确状态,而不是依赖多个布尔位组合。
35
+ """
36
+
37
+ CREATED = "created"
38
+ SUBMITTED = "submitted"
39
+ RUNNING = "running"
40
+ WAITING_INPUT = "waiting_input"
41
+ WAITING_AUTH = "waiting_auth"
42
+ CANCELLING = "cancelling"
43
+ CANCELLED = "cancelled"
44
+ TIMED_OUT = "timed_out"
45
+ FAILED = "failed"
46
+ COMPLETED = "completed"
47
+ FINALIZED = "finalized"
48
+
49
+
50
+ class TaskUpdaterProtocol(Protocol):
51
+ """task updater 的最小行为约束。"""
52
+
53
+ event_queue: Any
54
+
55
+ def new_agent_message(self, *, parts: list[Any], metadata: dict[str, Any]) -> Any:
56
+ ...
57
+
58
+ async def update_status(
59
+ self, status: Any, message: Any = None, metadata: dict[str, Any] | None = None
60
+ ) -> None:
61
+ ...
62
+
63
+ async def requires_input(self, message: Any = None) -> None:
64
+ ...
65
+
66
+ async def complete(self, message: Any = None) -> None:
67
+ ...
68
+
69
+ async def failed(self, message: Any = None) -> None:
70
+ ...
71
+
72
+ async def add_artifact(
73
+ self, *, parts: list[Any], name: str, metadata: dict[str, Any]
74
+ ) -> None:
75
+ ...
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class InputRequiredWaitState:
80
+ """任务因缺少用户输入而暂停时的等待态。"""
81
+
82
+ prompt: str
83
+
84
+ @property
85
+ def type(self) -> str:
86
+ return WAIT_TYPE_INPUT_REQUIRED
87
+
88
+ def to_payload(self) -> dict[str, Any]:
89
+ return {"type": self.type, "data": {"prompt": self.prompt}}
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class AuthRequiredWaitState:
94
+ """任务因需要用户授权确认而暂停时的等待态。"""
95
+
96
+ tool_name: str | None = None
97
+ args: dict[str, Any] | None = None
98
+
99
+ @property
100
+ def type(self) -> str:
101
+ return WAIT_TYPE_AUTH_REQUIRED
102
+
103
+ def to_payload(self) -> dict[str, Any]:
104
+ data: dict[str, Any] = {}
105
+ if self.tool_name is not None:
106
+ data["tool_name"] = self.tool_name
107
+ if self.args is not None:
108
+ data["args"] = self.args
109
+ return {"type": self.type, "data": data}
110
+
111
+
112
+ WaitState = InputRequiredWaitState | AuthRequiredWaitState
113
+
114
+
115
+ def wait_state_type(wait_state: WaitState | None) -> str | None:
116
+ if wait_state is None:
117
+ return None
118
+ return wait_state.type
119
+
120
+
121
+ def wait_state_payload(wait_state: WaitState) -> dict[str, Any]:
122
+ return wait_state.to_payload()
123
+
124
+
125
+ @dataclass
126
+ class TaskRuntimeRefs:
127
+ """任务运行期持有的资源引用。"""
128
+
129
+ agent: object | None = None
130
+ agent_task: Future[Any] | None = None
131
+ task_executor: ThreadPoolExecutor | None = None
132
+ event: asyncio.Event | None = None
133
+ loop: asyncio.AbstractEventLoop | None = None
134
+ updater: TaskUpdaterProtocol | Any | None = None
135
+
136
+
137
+ @dataclass
138
+ class TaskWaitContext:
139
+ """等待用户恢复时的状态。"""
140
+
141
+ item: WaitState | None = None
142
+ emitted: bool = False
143
+ resume_payload: str | dict[str, Any] | None = None
144
+ timeout_handle: asyncio.Handle | None = None
145
+
146
+
147
+ @dataclass
148
+ class TaskOverrideContext:
149
+ """等待授权恢复时的附加状态。"""
150
+
151
+ auth_denied: bool = False
152
+ tool_args: Any = None
153
+ tool_name: str | None = None
154
+
155
+
156
+ @dataclass
157
+ class TaskControlContext:
158
+ """任务的生命周期与停止控制状态。"""
159
+
160
+ phase: TaskPhase = TaskPhase.CREATED
161
+ stop_requested: bool = False
162
+ finalized: bool = False
163
+ timed_out: bool = False
164
+ stop_reason: str | None = None
165
+ stop_error: Exception | None = None
166
+ task_timeout_handle: asyncio.Handle | None = None
167
+
168
+
169
+ @dataclass
170
+ class ActiveToolCall:
171
+ """当前正在执行的工具调用。"""
172
+
173
+ tool_name: str
174
+ mcp_name: str | None = None
175
+ cancel_hook: Callable[[], None] | None = None
176
+
177
+ def cancel(self) -> None:
178
+ if self.cancel_hook is not None:
179
+ self.cancel_hook()
180
+
181
+
182
+ @dataclass(init=False)
183
+ class TaskContext:
184
+ """一次任务在运行期的完整上下文。
185
+
186
+ 当前实现把 task 拆成几块:
187
+
188
+ - `runtime`: 线程池、future、loop、updater 这些运行时引用
189
+ - `wait`: 等待用户输入/授权时的状态
190
+ - `overrides`: 授权确认恢复时的参数覆盖状态
191
+ - `control`: phase、取消、超时、finalize 这些生命周期状态
192
+ - `active_tool`: 当前执行中的工具,供超时/取消主动打断
193
+
194
+ 为了降低迁移成本,这里保留了旧字段的 property 兼容访问。
195
+ """
196
+
197
+ runtime: TaskRuntimeRefs
198
+ wait: TaskWaitContext
199
+ overrides: TaskOverrideContext
200
+ control: TaskControlContext
201
+ active_tool: ActiveToolCall | None
202
+ auth_required: Any
203
+
204
+ def __init__(
205
+ self,
206
+ *,
207
+ agent: object | None = None,
208
+ agent_task: Future[Any] | None = None,
209
+ task_executor: ThreadPoolExecutor | None = None,
210
+ event: asyncio.Event | None = None,
211
+ wait_item: WaitState | None = None,
212
+ wait_item_emitted: bool = False,
213
+ user_input: str | dict[str, Any] | None = None,
214
+ auth_required: Any = None,
215
+ auth_denied: bool = False,
216
+ terminate: bool = False,
217
+ loop: asyncio.AbstractEventLoop | None = None,
218
+ updater: TaskUpdaterProtocol | Any | None = None,
219
+ finalized: bool = False,
220
+ tool_args_override: Any = None,
221
+ tool_name_override: str | None = None,
222
+ timed_out: bool = False,
223
+ timeout_reason: str | None = None,
224
+ timeout_handle: asyncio.Handle | None = None,
225
+ phase: TaskPhase = TaskPhase.CREATED,
226
+ runtime: TaskRuntimeRefs | None = None,
227
+ wait: TaskWaitContext | None = None,
228
+ overrides: TaskOverrideContext | None = None,
229
+ control: TaskControlContext | None = None,
230
+ active_tool: ActiveToolCall | None = None,
231
+ ) -> None:
232
+ self.runtime = runtime or TaskRuntimeRefs(
233
+ agent=agent,
234
+ agent_task=agent_task,
235
+ task_executor=task_executor,
236
+ event=event,
237
+ loop=loop,
238
+ updater=updater,
239
+ )
240
+ self.wait = wait or TaskWaitContext(
241
+ item=wait_item,
242
+ emitted=wait_item_emitted,
243
+ resume_payload=user_input,
244
+ timeout_handle=timeout_handle,
245
+ )
246
+ self.overrides = overrides or TaskOverrideContext(
247
+ auth_denied=auth_denied,
248
+ tool_args=tool_args_override,
249
+ tool_name=tool_name_override,
250
+ )
251
+ self.control = control or TaskControlContext(
252
+ phase=phase,
253
+ stop_requested=terminate,
254
+ finalized=finalized,
255
+ timed_out=timed_out,
256
+ stop_reason=timeout_reason,
257
+ )
258
+ self.active_tool = active_tool
259
+ self.auth_required = auth_required
260
+
261
+ def set_phase(self, phase: TaskPhase) -> None:
262
+ self.control.phase = phase
263
+
264
+ def request_stop(self, *, error: Exception, reason: str, timed_out: bool) -> None:
265
+ self.control.stop_requested = True
266
+ self.control.timed_out = timed_out
267
+ self.control.stop_reason = reason
268
+ self.control.stop_error = error
269
+
270
+ def clear_wait(self) -> None:
271
+ self.wait.item = None
272
+ self.wait.emitted = False
273
+ self.wait.resume_payload = None
274
+
275
+ def clear_wait_item(self) -> None:
276
+ self.wait.item = None
277
+ self.wait.emitted = False
278
+
279
+ def set_wait(self, wait_state: WaitState, phase: TaskPhase) -> None:
280
+ self.wait.item = wait_state
281
+ self.wait.emitted = False
282
+ self.wait.resume_payload = None
283
+ self.control.timed_out = False
284
+ self.control.stop_reason = None
285
+ self.set_phase(phase)
286
+
287
+ def set_active_tool(self, active_tool: ActiveToolCall | None) -> None:
288
+ self.active_tool = active_tool
289
+
290
+ def clear_active_tool(self) -> None:
291
+ self.active_tool = None
292
+
293
+ @property
294
+ def is_waiting(self) -> bool:
295
+ return self.wait.item is not None
296
+
297
+ @property
298
+ def is_terminal(self) -> bool:
299
+ return self.control.phase in {
300
+ TaskPhase.CANCELLED,
301
+ TaskPhase.COMPLETED,
302
+ TaskPhase.FAILED,
303
+ TaskPhase.TIMED_OUT,
304
+ TaskPhase.FINALIZED,
305
+ }
306
+
307
+ @property
308
+ def agent(self):
309
+ return self.runtime.agent
310
+
311
+ @agent.setter
312
+ def agent(self, value: object | None) -> None:
313
+ self.runtime.agent = value
314
+
315
+ @property
316
+ def agent_task(self):
317
+ return self.runtime.agent_task
318
+
319
+ @agent_task.setter
320
+ def agent_task(self, value: Future[Any] | None) -> None:
321
+ self.runtime.agent_task = value
322
+
323
+ @property
324
+ def task_executor(self):
325
+ return self.runtime.task_executor
326
+
327
+ @task_executor.setter
328
+ def task_executor(self, value: ThreadPoolExecutor | None) -> None:
329
+ self.runtime.task_executor = value
330
+
331
+ @property
332
+ def event(self):
333
+ return self.runtime.event
334
+
335
+ @event.setter
336
+ def event(self, value: asyncio.Event | None) -> None:
337
+ self.runtime.event = value
338
+
339
+ @property
340
+ def wait_item(self):
341
+ return self.wait.item
342
+
343
+ @wait_item.setter
344
+ def wait_item(self, value: WaitState | None) -> None:
345
+ self.wait.item = value
346
+
347
+ @property
348
+ def wait_item_emitted(self) -> bool:
349
+ return self.wait.emitted
350
+
351
+ @wait_item_emitted.setter
352
+ def wait_item_emitted(self, value: bool) -> None:
353
+ self.wait.emitted = value
354
+
355
+ @property
356
+ def user_input(self):
357
+ return self.wait.resume_payload
358
+
359
+ @user_input.setter
360
+ def user_input(self, value: str | dict[str, Any] | None) -> None:
361
+ self.wait.resume_payload = value
362
+
363
+ @property
364
+ def auth_denied(self) -> bool:
365
+ return self.overrides.auth_denied
366
+
367
+ @auth_denied.setter
368
+ def auth_denied(self, value: bool) -> None:
369
+ self.overrides.auth_denied = value
370
+
371
+ @property
372
+ def terminate(self) -> bool:
373
+ return self.control.stop_requested
374
+
375
+ @terminate.setter
376
+ def terminate(self, value: bool) -> None:
377
+ self.control.stop_requested = value
378
+
379
+ @property
380
+ def loop(self):
381
+ return self.runtime.loop
382
+
383
+ @loop.setter
384
+ def loop(self, value: asyncio.AbstractEventLoop | None) -> None:
385
+ self.runtime.loop = value
386
+
387
+ @property
388
+ def updater(self):
389
+ return self.runtime.updater
390
+
391
+ @updater.setter
392
+ def updater(self, value: TaskUpdaterProtocol | Any | None) -> None:
393
+ self.runtime.updater = value
394
+
395
+ @property
396
+ def finalized(self) -> bool:
397
+ return self.control.finalized
398
+
399
+ @finalized.setter
400
+ def finalized(self, value: bool) -> None:
401
+ self.control.finalized = value
402
+
403
+ @property
404
+ def tool_args_override(self):
405
+ return self.overrides.tool_args
406
+
407
+ @tool_args_override.setter
408
+ def tool_args_override(self, value: Any) -> None:
409
+ self.overrides.tool_args = value
410
+
411
+ @property
412
+ def tool_name_override(self) -> str | None:
413
+ return self.overrides.tool_name
414
+
415
+ @tool_name_override.setter
416
+ def tool_name_override(self, value: str | None) -> None:
417
+ self.overrides.tool_name = value
418
+
419
+ @property
420
+ def timed_out(self) -> bool:
421
+ return self.control.timed_out
422
+
423
+ @timed_out.setter
424
+ def timed_out(self, value: bool) -> None:
425
+ self.control.timed_out = value
426
+
427
+ @property
428
+ def timeout_reason(self) -> str | None:
429
+ return self.control.stop_reason
430
+
431
+ @timeout_reason.setter
432
+ def timeout_reason(self, value: str | None) -> None:
433
+ self.control.stop_reason = value
434
+
435
+ @property
436
+ def timeout_handle(self):
437
+ return self.wait.timeout_handle
438
+
439
+ @timeout_handle.setter
440
+ def timeout_handle(self, value: asyncio.Handle | None) -> None:
441
+ self.wait.timeout_handle = value
442
+
443
+
444
+ class TaskPool:
445
+ """线程安全的任务上下文池。"""
446
+
447
+ def __init__(self):
448
+ self._tasks: dict[str, TaskContext] = {}
449
+ self._lock = threading.RLock()
450
+
451
+ def __contains__(self, task_id: str) -> bool:
452
+ with self._lock:
453
+ return task_id in self._tasks
454
+
455
+ def __getitem__(self, task_id: str) -> TaskContext:
456
+ with self._lock:
457
+ return self._tasks[task_id]
458
+
459
+ def __setitem__(self, task_id: str, ctx: TaskContext) -> None:
460
+ with self._lock:
461
+ self._tasks[task_id] = ctx
462
+
463
+ def get(self, task_id: str, default: Any = None) -> Any:
464
+ with self._lock:
465
+ return self._tasks.get(task_id, default)
466
+
467
+ def pop(self, task_id: str, default: Any = None) -> Any:
468
+ with self._lock:
469
+ return self._tasks.pop(task_id, default)
@@ -0,0 +1,170 @@
1
+ """runtime reload 阶段的内部引擎。
2
+
3
+ 职责很聚焦:
4
+
5
+ - 加载或重载 plugin
6
+ - 发现所有可用 MCP 工具
7
+ - 建立 `tool_name -> source_mcp` 索引
8
+
9
+ 这一步只准备运行时元数据,不负责构造单次请求 agent。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from ..config.definitions import (
19
+ DEFAULT_PLUGIN_CLASS_NAME,
20
+ DEFAULT_PLUGIN_MODULE_PATH,
21
+ PluginSettings,
22
+ )
23
+ from ..mcp.metadata import DiscoveredTool, normalize_discovered_tools
24
+ from ..mcp.utils import index_tool_sources, merge_mcp_headers
25
+ from ..plugins import BaseAgentPlugin, load_plugin
26
+
27
+ if TYPE_CHECKING:
28
+ from ..runtime import ManagedAgentRuntime
29
+
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class RuntimeLoading:
35
+ """负责 runtime reload 阶段的 plugin 与工具元数据准备。"""
36
+
37
+ def reload(
38
+ self, discover: bool = True, skip_plugin_load: bool = False
39
+ ) -> "ManagedAgentRuntime":
40
+ """重新加载插件和工具描述。"""
41
+ self._reset_reload_state()
42
+ logger.info(
43
+ "Reloading runtime agent_id=%s discover=%s skip_plugin_load=%s",
44
+ self._agent.agent_id,
45
+ discover,
46
+ skip_plugin_load,
47
+ )
48
+ try:
49
+ plugin = self._resolve_plugin_for_reload(skip_plugin_load=skip_plugin_load)
50
+ discovered_tools = self._discover_tools_metadata() if discover else []
51
+ tool_sources = self._index_tool_sources(discovered_tools)
52
+ self._apply_reload_result(
53
+ plugin=plugin,
54
+ discovered_tools=discovered_tools,
55
+ tool_sources=tool_sources,
56
+ )
57
+ logger.info(
58
+ "Runtime reload finished agent_id=%s discovered_tools=%s",
59
+ self._agent.agent_id,
60
+ len(self.discovered_tools),
61
+ )
62
+ except Exception as exc:
63
+ self._apply_reload_failure(exc)
64
+ return self
65
+
66
+ def _reset_reload_state(self) -> None:
67
+ self._asgi_app = None
68
+ self.load_error = None
69
+ self.load_exception = None
70
+ self.discovered_tools = []
71
+ self._tool_source_by_name = {}
72
+
73
+ def _resolve_plugin_for_reload(
74
+ self, *, skip_plugin_load: bool
75
+ ) -> BaseAgentPlugin | None:
76
+ if skip_plugin_load:
77
+ return self.plugin
78
+ return self._load_plugin_instance()
79
+
80
+ def _load_plugin_instance(self) -> BaseAgentPlugin:
81
+ plugin_settings = self._effective_plugin_settings()
82
+ if plugin_settings is not None:
83
+ plugin = load_plugin(
84
+ self._resolved_plugin_module_path(plugin_settings),
85
+ plugin_settings.class_name,
86
+ plugin_settings.config,
87
+ )
88
+ else:
89
+ from ..plugins.default import DefaultPlugin
90
+
91
+ plugin = DefaultPlugin()
92
+ logger.debug(
93
+ "Loaded plugin for agent_id=%s plugin=%s",
94
+ self._agent.agent_id,
95
+ type(plugin).__name__,
96
+ )
97
+ return plugin
98
+
99
+ def _discover_tools_metadata(self) -> list[DiscoveredTool]:
100
+ """发现阶段只收集工具摘要,不保留可执行 tool。"""
101
+
102
+ discovered_tools: list[DiscoveredTool] = []
103
+ for mcp in self._mcps:
104
+ if not mcp.enabled:
105
+ logger.debug(
106
+ "Skipping disabled MCP during reload agent_id=%s mcp=%s",
107
+ self._agent.agent_id,
108
+ mcp.name,
109
+ )
110
+ continue
111
+ discovered_tools.extend(self._discover_mcp_tools_metadata(mcp))
112
+ return discovered_tools
113
+
114
+ def _discover_mcp_tools_metadata(self, mcp) -> list[DiscoveredTool]:
115
+ logger.debug(
116
+ "Discovering MCP tools agent_id=%s mcp=%s url=%s",
117
+ self._agent.agent_id,
118
+ mcp.name,
119
+ mcp.url,
120
+ )
121
+ return normalize_discovered_tools(
122
+ self.discoverer(mcp, headers=merge_mcp_headers(mcp)), mcp.name
123
+ )
124
+
125
+ def _index_tool_sources(
126
+ self, discovered_tools: list[DiscoveredTool]
127
+ ) -> dict[str, str]:
128
+ return index_tool_sources(discovered_tools)
129
+
130
+ def _apply_reload_result(
131
+ self,
132
+ *,
133
+ plugin: BaseAgentPlugin | None,
134
+ discovered_tools: list[DiscoveredTool],
135
+ tool_sources: dict[str, str],
136
+ ) -> None:
137
+ if plugin is not None:
138
+ self.plugin = plugin
139
+ self.discovered_tools = discovered_tools
140
+ self._tool_source_by_name = tool_sources
141
+
142
+ def _apply_reload_failure(self, exc: Exception) -> None:
143
+ self.discovered_tools = []
144
+ self.load_exception = exc
145
+ self.load_error = f"{type(exc).__name__}: {exc}"
146
+ logger.exception("Runtime reload failed agent_id=%s", self._agent.agent_id)
147
+
148
+ def _effective_plugin_settings(self) -> PluginSettings | None:
149
+ if self._runtime_config.plugin is not None:
150
+ return self._runtime_config.plugin
151
+
152
+ default_plugin_path = (
153
+ Path(self._runtime_config.base_path).resolve() / DEFAULT_PLUGIN_MODULE_PATH
154
+ )
155
+ if default_plugin_path.exists():
156
+ return PluginSettings(
157
+ module_path=DEFAULT_PLUGIN_MODULE_PATH,
158
+ class_name=DEFAULT_PLUGIN_CLASS_NAME,
159
+ config={},
160
+ )
161
+ return None
162
+
163
+ def _resolved_plugin_module_path(self, plugin_settings: PluginSettings) -> str:
164
+ module_path = plugin_settings.module_path
165
+ if module_path.endswith(".py") or "/" in module_path:
166
+ path = Path(module_path)
167
+ if not path.is_absolute():
168
+ path = Path(self._runtime_config.base_path).resolve() / path
169
+ return path.as_posix()
170
+ return module_path