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,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
|