capability-runtime 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.
- capability_runtime/__init__.py +90 -0
- capability_runtime/adapters/__init__.py +13 -0
- capability_runtime/adapters/agent_adapter.py +439 -0
- capability_runtime/adapters/agently_backend.py +423 -0
- capability_runtime/adapters/triggerflow_workflow_engine.py +865 -0
- capability_runtime/adapters/workflow_engine.py +43 -0
- capability_runtime/config.py +172 -0
- capability_runtime/errors.py +20 -0
- capability_runtime/guards.py +150 -0
- capability_runtime/host_protocol.py +400 -0
- capability_runtime/host_toolkit/__init__.py +55 -0
- capability_runtime/host_toolkit/approvals_profiles.py +94 -0
- capability_runtime/host_toolkit/evidence_hooks.py +65 -0
- capability_runtime/host_toolkit/history.py +74 -0
- capability_runtime/host_toolkit/invoke_capability.py +409 -0
- capability_runtime/host_toolkit/resume.py +317 -0
- capability_runtime/host_toolkit/system_prompt.py +132 -0
- capability_runtime/host_toolkit/turn_delta.py +128 -0
- capability_runtime/logging_utils.py +94 -0
- capability_runtime/manifest.py +173 -0
- capability_runtime/output_validator.py +139 -0
- capability_runtime/protocol/__init__.py +43 -0
- capability_runtime/protocol/agent.py +62 -0
- capability_runtime/protocol/capability.py +98 -0
- capability_runtime/protocol/chat_backend.py +38 -0
- capability_runtime/protocol/context.py +244 -0
- capability_runtime/protocol/workflow.py +119 -0
- capability_runtime/registry.py +287 -0
- capability_runtime/reporting/__init__.py +2 -0
- capability_runtime/reporting/node_report.py +497 -0
- capability_runtime/runtime.py +930 -0
- capability_runtime/runtime_ui_events_mixin.py +310 -0
- capability_runtime/sdk_lifecycle.py +982 -0
- capability_runtime/service_facade.py +418 -0
- capability_runtime/services.py +181 -0
- capability_runtime/structured_output.py +208 -0
- capability_runtime/structured_stream.py +38 -0
- capability_runtime/types.py +103 -0
- capability_runtime/ui_events/__init__.py +19 -0
- capability_runtime/ui_events/projector.py +617 -0
- capability_runtime/ui_events/session.py +292 -0
- capability_runtime/ui_events/store.py +127 -0
- capability_runtime/ui_events/transport.py +33 -0
- capability_runtime/ui_events/v1.py +76 -0
- capability_runtime/upstream_compat.py +182 -0
- capability_runtime/utils/__init__.py +1 -0
- capability_runtime/utils/usage.py +65 -0
- capability_runtime/workflow_runtime.py +218 -0
- capability_runtime-0.1.0.dist-info/METADATA +232 -0
- capability_runtime-0.1.0.dist-info/RECORD +52 -0
- capability_runtime-0.1.0.dist-info/WHEEL +5 -0
- capability_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Runtime service façade / session continuity bridge."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, AsyncIterator, Literal
|
|
9
|
+
|
|
10
|
+
from .host_toolkit.history import HistoryAssembler
|
|
11
|
+
from .host_toolkit.turn_delta import TurnDelta
|
|
12
|
+
from .protocol.capability import CapabilityResult, CapabilityStatus
|
|
13
|
+
from .protocol.context import ExecutionContext
|
|
14
|
+
from .runtime import Runtime
|
|
15
|
+
from .types import NodeReport
|
|
16
|
+
from .ui_events.transport import encode_json_line
|
|
17
|
+
from .ui_events.v1 import RuntimeEvent, StreamLevel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class RuntimeSession:
|
|
22
|
+
"""
|
|
23
|
+
运行时会话上下文。
|
|
24
|
+
|
|
25
|
+
参数:
|
|
26
|
+
- session_id:宿主会话 ID
|
|
27
|
+
- host_turn_id:可选宿主 turn ID
|
|
28
|
+
- history:显式 continuity history
|
|
29
|
+
- metadata:宿主会话元数据
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
session_id: str
|
|
33
|
+
host_turn_id: str | None = None
|
|
34
|
+
history: list[dict[str, str]] = field(default_factory=list)
|
|
35
|
+
turn_deltas: list[TurnDelta] = field(default_factory=list)
|
|
36
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class RuntimeServiceRequest:
|
|
41
|
+
"""
|
|
42
|
+
service façade 请求。
|
|
43
|
+
|
|
44
|
+
参数:
|
|
45
|
+
- capability_id:目标能力 ID
|
|
46
|
+
- input:输入 payload
|
|
47
|
+
- session:可选会话
|
|
48
|
+
- stream_level:事件流等级(`ui`/`lite`)
|
|
49
|
+
- transport:传输 framing(`jsonl`/`sse`)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
capability_id: str
|
|
53
|
+
input: dict[str, Any]
|
|
54
|
+
session: RuntimeSession | None = None
|
|
55
|
+
stream_level: str = "ui"
|
|
56
|
+
transport: str = "jsonl"
|
|
57
|
+
execution_target: Literal["local", "rpc"] = "local"
|
|
58
|
+
timeout_ms: int | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class RuntimeServiceHandle:
|
|
63
|
+
"""
|
|
64
|
+
service 调用句柄。
|
|
65
|
+
|
|
66
|
+
参数:
|
|
67
|
+
- run_id:运行 ID
|
|
68
|
+
- session_id:可选会话 ID
|
|
69
|
+
- capability_id:能力 ID
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
run_id: str
|
|
73
|
+
session_id: str | None = None
|
|
74
|
+
capability_id: str = ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class _HandleState:
|
|
79
|
+
request: RuntimeServiceRequest
|
|
80
|
+
context: ExecutionContext
|
|
81
|
+
session: Any | None
|
|
82
|
+
reaper_task: asyncio.Task[None] | None = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_session_context(
|
|
86
|
+
*,
|
|
87
|
+
session: RuntimeSession | None,
|
|
88
|
+
turn_deltas: list[TurnDelta] | None = None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
构造 continuity 注入 overlay。
|
|
92
|
+
|
|
93
|
+
参数:
|
|
94
|
+
- session:显式会话
|
|
95
|
+
- turn_deltas:可选 TurnDelta 列表;存在时优先组装 `initial_history`
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
history: list[dict[str, Any]] = []
|
|
99
|
+
if turn_deltas is None and session is not None and getattr(session, "turn_deltas", None):
|
|
100
|
+
raw_turn_deltas = getattr(session, "turn_deltas", None)
|
|
101
|
+
if isinstance(raw_turn_deltas, list):
|
|
102
|
+
turn_deltas = raw_turn_deltas
|
|
103
|
+
if turn_deltas:
|
|
104
|
+
history = HistoryAssembler().build_initial_history(deltas=turn_deltas)
|
|
105
|
+
elif session is not None:
|
|
106
|
+
history = [dict(item) for item in session.history]
|
|
107
|
+
|
|
108
|
+
host_meta: dict[str, Any] = {}
|
|
109
|
+
if session is not None:
|
|
110
|
+
host_meta["session_id"] = session.session_id
|
|
111
|
+
if session.host_turn_id is not None:
|
|
112
|
+
host_meta["host_turn_id"] = session.host_turn_id
|
|
113
|
+
if session.metadata:
|
|
114
|
+
host_meta["metadata"] = dict(session.metadata)
|
|
115
|
+
elif turn_deltas:
|
|
116
|
+
latest = turn_deltas[-1]
|
|
117
|
+
if latest.session_id is not None:
|
|
118
|
+
host_meta["session_id"] = latest.session_id
|
|
119
|
+
if latest.host_turn_id is not None:
|
|
120
|
+
host_meta["host_turn_id"] = latest.host_turn_id
|
|
121
|
+
|
|
122
|
+
if history:
|
|
123
|
+
host_meta["initial_history"] = history
|
|
124
|
+
|
|
125
|
+
return {"__host_meta__": host_meta} if host_meta else {}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class RuntimeServiceFacade:
|
|
129
|
+
"""
|
|
130
|
+
运行时 service façade。
|
|
131
|
+
|
|
132
|
+
说明:
|
|
133
|
+
- `start()` 负责稳定化 `run_id/session_id`
|
|
134
|
+
- `run()` 负责非流式调用
|
|
135
|
+
- `stream()` 负责 UI events 的 JSONL/SSE framing
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, runtime: Runtime) -> None:
|
|
139
|
+
self._runtime = runtime
|
|
140
|
+
self._handles: dict[str, _HandleState] = {}
|
|
141
|
+
|
|
142
|
+
async def start(self, request: RuntimeServiceRequest) -> RuntimeServiceHandle:
|
|
143
|
+
"""
|
|
144
|
+
初始化一次 service 调用并返回句柄。
|
|
145
|
+
|
|
146
|
+
参数:
|
|
147
|
+
- request:service façade 请求
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
run_id = uuid.uuid4().hex
|
|
151
|
+
context = self._build_context(run_id=run_id, request=request)
|
|
152
|
+
session = None
|
|
153
|
+
if request.execution_target == "local":
|
|
154
|
+
level = self._resolve_stream_level(request.stream_level)
|
|
155
|
+
session = self._runtime.start_ui_events_session(
|
|
156
|
+
request.capability_id,
|
|
157
|
+
input=request.input,
|
|
158
|
+
context=context,
|
|
159
|
+
level=level,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
self._require_runtime_client(request=request)
|
|
163
|
+
handle = RuntimeServiceHandle(
|
|
164
|
+
run_id=run_id,
|
|
165
|
+
session_id=request.session.session_id if request.session is not None else None,
|
|
166
|
+
capability_id=request.capability_id,
|
|
167
|
+
)
|
|
168
|
+
state = _HandleState(request=request, context=context, session=session)
|
|
169
|
+
self._handles[handle.run_id] = state
|
|
170
|
+
return handle
|
|
171
|
+
|
|
172
|
+
async def run(self, request: RuntimeServiceRequest) -> CapabilityResult:
|
|
173
|
+
"""
|
|
174
|
+
执行一次非流式 service 调用。
|
|
175
|
+
|
|
176
|
+
参数:
|
|
177
|
+
- request:service façade 请求
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
run_id = uuid.uuid4().hex
|
|
181
|
+
context = self._build_context(run_id=run_id, request=request)
|
|
182
|
+
if request.execution_target == "local":
|
|
183
|
+
return await self._runtime.run(request.capability_id, input=request.input, context=context)
|
|
184
|
+
|
|
185
|
+
client = self._require_runtime_client(request=request)
|
|
186
|
+
response = await client.invoke(self._build_rpc_request_dict(run_id=run_id, request=request))
|
|
187
|
+
return self._coerce_capability_result(response)
|
|
188
|
+
|
|
189
|
+
async def stream(self, handle: RuntimeServiceHandle) -> AsyncIterator[str]:
|
|
190
|
+
"""
|
|
191
|
+
基于句柄输出 JSONL / SSE 子集文本流。
|
|
192
|
+
|
|
193
|
+
参数:
|
|
194
|
+
- handle:`start()` 返回的 service handle
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
state = self._handles.get(handle.run_id)
|
|
198
|
+
if state is None:
|
|
199
|
+
raise KeyError(f"Unknown runtime service handle: {handle.run_id!r}")
|
|
200
|
+
|
|
201
|
+
request = state.request
|
|
202
|
+
use_sse = str(request.transport or "jsonl").strip().lower() == "sse"
|
|
203
|
+
session = state.session
|
|
204
|
+
if request.execution_target == "rpc":
|
|
205
|
+
client = self._require_runtime_client(request=request)
|
|
206
|
+
try:
|
|
207
|
+
async for item in client.stream(self._build_rpc_request_dict(run_id=handle.run_id, request=request)):
|
|
208
|
+
yield self._encode_rpc_stream_item(item, use_sse=use_sse)
|
|
209
|
+
finally:
|
|
210
|
+
self._handles.pop(handle.run_id, None)
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
if state.reaper_task is None:
|
|
214
|
+
state.reaper_task = asyncio.create_task(self._reap_handle_when_done(run_id=handle.run_id, session=session))
|
|
215
|
+
async for ev in session.subscribe(after_id=None):
|
|
216
|
+
yield encode_json_line(ev, prefix_data=use_sse)
|
|
217
|
+
self._handles.pop(handle.run_id, None)
|
|
218
|
+
|
|
219
|
+
async def cancel(self, handle: RuntimeServiceHandle) -> None:
|
|
220
|
+
"""
|
|
221
|
+
取消一个 service 调用。
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
state = self._handles.get(handle.run_id)
|
|
225
|
+
if state is None:
|
|
226
|
+
if getattr(self._runtime.config, "runtime_client", None) is not None:
|
|
227
|
+
await self._runtime.config.runtime_client.cancel(run_id=handle.run_id)
|
|
228
|
+
return
|
|
229
|
+
raise KeyError(f"Unknown runtime service handle: {handle.run_id!r}")
|
|
230
|
+
|
|
231
|
+
if state.request.execution_target == "rpc":
|
|
232
|
+
client = self._require_runtime_client(request=state.request)
|
|
233
|
+
await client.cancel(run_id=handle.run_id)
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
raise NotImplementedError("local cancel is not implemented")
|
|
237
|
+
|
|
238
|
+
async def replay(
|
|
239
|
+
self,
|
|
240
|
+
*,
|
|
241
|
+
workflow_id: str,
|
|
242
|
+
run_id: str,
|
|
243
|
+
current_input: dict[str, Any],
|
|
244
|
+
execution_target: Literal["local", "rpc"] = "local",
|
|
245
|
+
timeout_ms: int | None = None,
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
"""
|
|
248
|
+
workflow replay 的最小 service façade surface。
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
if execution_target == "rpc":
|
|
252
|
+
request = RuntimeServiceRequest(
|
|
253
|
+
capability_id=workflow_id,
|
|
254
|
+
input=current_input,
|
|
255
|
+
execution_target="rpc",
|
|
256
|
+
timeout_ms=timeout_ms,
|
|
257
|
+
)
|
|
258
|
+
client = self._require_runtime_client(request=request)
|
|
259
|
+
response = await client.replay(self._build_rpc_request_dict(run_id=run_id, request=request))
|
|
260
|
+
if not isinstance(response, dict):
|
|
261
|
+
raise TypeError("runtime_client.replay() must return dict")
|
|
262
|
+
return dict(response)
|
|
263
|
+
|
|
264
|
+
result = await self._runtime.replay(
|
|
265
|
+
workflow_id=workflow_id,
|
|
266
|
+
run_id=run_id,
|
|
267
|
+
current_input=current_input,
|
|
268
|
+
)
|
|
269
|
+
return self._capability_result_to_dict(result)
|
|
270
|
+
|
|
271
|
+
def _build_context(self, *, run_id: str, request: RuntimeServiceRequest) -> ExecutionContext:
|
|
272
|
+
"""
|
|
273
|
+
为 service request 构造 ExecutionContext。
|
|
274
|
+
|
|
275
|
+
参数:
|
|
276
|
+
- run_id:目标运行 ID
|
|
277
|
+
- request:service façade 请求
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
context = ExecutionContext(run_id=run_id, max_depth=self._runtime.config.max_depth)
|
|
281
|
+
overlay = build_session_context(
|
|
282
|
+
session=request.session,
|
|
283
|
+
turn_deltas=list(request.session.turn_deltas) if request.session is not None and request.session.turn_deltas else None,
|
|
284
|
+
)
|
|
285
|
+
if overlay:
|
|
286
|
+
context = context.with_bag_overlay(**overlay)
|
|
287
|
+
return context
|
|
288
|
+
|
|
289
|
+
def _resolve_stream_level(self, level: str) -> StreamLevel:
|
|
290
|
+
"""
|
|
291
|
+
解析字符串 stream level。
|
|
292
|
+
|
|
293
|
+
参数:
|
|
294
|
+
- level:字符串等级
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
normalized = str(level or "ui").strip().lower()
|
|
298
|
+
if normalized == "lite":
|
|
299
|
+
return StreamLevel.LITE
|
|
300
|
+
return StreamLevel.UI
|
|
301
|
+
|
|
302
|
+
def _require_runtime_client(self, *, request: RuntimeServiceRequest) -> Any:
|
|
303
|
+
"""在 RPC 目标下获取已配置的 runtime client。"""
|
|
304
|
+
|
|
305
|
+
runtime_client = getattr(self._runtime.config, "runtime_client", None)
|
|
306
|
+
if runtime_client is None:
|
|
307
|
+
raise ValueError("runtime_client is required when execution_target='rpc'")
|
|
308
|
+
return runtime_client
|
|
309
|
+
|
|
310
|
+
def _build_rpc_request_dict(self, *, run_id: str, request: RuntimeServiceRequest) -> dict[str, Any]:
|
|
311
|
+
"""按 v1 契约构造发往 runtime client 的 request dict。"""
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
"request_id": uuid.uuid4().hex,
|
|
315
|
+
"run_id": run_id,
|
|
316
|
+
"session_id": request.session.session_id if request.session is not None else None,
|
|
317
|
+
"capability_id": request.capability_id,
|
|
318
|
+
"input": dict(request.input),
|
|
319
|
+
"timeout_ms": request.timeout_ms,
|
|
320
|
+
"stream_level": str(request.stream_level or "ui").strip().lower() or "ui",
|
|
321
|
+
"transport": str(request.transport or "jsonl").strip().lower() or "jsonl",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
def _coerce_capability_result(self, payload: Any) -> CapabilityResult:
|
|
325
|
+
"""把 RPC 返回值收敛为 CapabilityResult。"""
|
|
326
|
+
|
|
327
|
+
if isinstance(payload, CapabilityResult):
|
|
328
|
+
return payload
|
|
329
|
+
if not isinstance(payload, dict):
|
|
330
|
+
raise TypeError("runtime_client.invoke() must return CapabilityResult or dict")
|
|
331
|
+
|
|
332
|
+
status_raw = payload.get("status")
|
|
333
|
+
try:
|
|
334
|
+
status = status_raw if isinstance(status_raw, CapabilityStatus) else CapabilityStatus(str(status_raw))
|
|
335
|
+
except Exception as exc: # pragma: no cover - error path by contract
|
|
336
|
+
raise TypeError("runtime_client.invoke() returned invalid CapabilityResult payload") from exc
|
|
337
|
+
|
|
338
|
+
node_report_raw = payload.get("node_report")
|
|
339
|
+
node_report = None
|
|
340
|
+
if isinstance(node_report_raw, NodeReport):
|
|
341
|
+
node_report = node_report_raw
|
|
342
|
+
elif isinstance(node_report_raw, dict):
|
|
343
|
+
node_report = NodeReport.model_validate(node_report_raw)
|
|
344
|
+
elif node_report_raw is not None:
|
|
345
|
+
raise TypeError("runtime_client.invoke() returned invalid node_report payload")
|
|
346
|
+
|
|
347
|
+
artifacts_raw = payload.get("artifacts")
|
|
348
|
+
if artifacts_raw is None:
|
|
349
|
+
artifacts = []
|
|
350
|
+
elif isinstance(artifacts_raw, list):
|
|
351
|
+
artifacts = [str(item) for item in artifacts_raw]
|
|
352
|
+
else:
|
|
353
|
+
raise TypeError("runtime_client.invoke() returned invalid artifacts payload")
|
|
354
|
+
|
|
355
|
+
metadata_raw = payload.get("metadata")
|
|
356
|
+
if metadata_raw is None:
|
|
357
|
+
metadata = {}
|
|
358
|
+
elif isinstance(metadata_raw, dict):
|
|
359
|
+
metadata = dict(metadata_raw)
|
|
360
|
+
else:
|
|
361
|
+
raise TypeError("runtime_client.invoke() returned invalid metadata payload")
|
|
362
|
+
|
|
363
|
+
duration_ms = payload.get("duration_ms")
|
|
364
|
+
if duration_ms is not None and not isinstance(duration_ms, (int, float)):
|
|
365
|
+
raise TypeError("runtime_client.invoke() returned invalid duration_ms payload")
|
|
366
|
+
|
|
367
|
+
return CapabilityResult(
|
|
368
|
+
status=status,
|
|
369
|
+
output=payload.get("output"),
|
|
370
|
+
error=payload.get("error") if isinstance(payload.get("error"), str) or payload.get("error") is None else str(payload.get("error")),
|
|
371
|
+
error_code=payload.get("error_code")
|
|
372
|
+
if isinstance(payload.get("error_code"), str) or payload.get("error_code") is None
|
|
373
|
+
else str(payload.get("error_code")),
|
|
374
|
+
report=payload.get("report"),
|
|
375
|
+
node_report=node_report,
|
|
376
|
+
artifacts=artifacts,
|
|
377
|
+
duration_ms=float(duration_ms) if isinstance(duration_ms, (int, float)) else None,
|
|
378
|
+
metadata=metadata,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def _capability_result_to_dict(self, result: CapabilityResult) -> dict[str, Any]:
|
|
382
|
+
"""把本地 CapabilityResult 收敛为 replay surface 的最小 dict。"""
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
"status": result.status.value,
|
|
386
|
+
"output": result.output,
|
|
387
|
+
"error": result.error,
|
|
388
|
+
"error_code": result.error_code,
|
|
389
|
+
"artifacts": list(result.artifacts),
|
|
390
|
+
"metadata": dict(result.metadata),
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
def _encode_rpc_stream_item(self, item: dict[str, Any] | str, *, use_sse: bool) -> str:
|
|
394
|
+
"""把 runtime client 的流式 item 统一 framing 为现有 JSONL/SSE 输出。"""
|
|
395
|
+
|
|
396
|
+
if isinstance(item, str):
|
|
397
|
+
if use_sse:
|
|
398
|
+
return item if item.startswith("data: ") else f"data: {item.rstrip()}\n\n"
|
|
399
|
+
return item if item.endswith("\n") else item + "\n"
|
|
400
|
+
|
|
401
|
+
event = RuntimeEvent.model_validate(item)
|
|
402
|
+
return encode_json_line(event, prefix_data=use_sse)
|
|
403
|
+
|
|
404
|
+
async def _reap_handle_when_done(self, *, run_id: str, session: Any) -> None:
|
|
405
|
+
wait_done = getattr(session, "wait_done", None)
|
|
406
|
+
if not callable(wait_done):
|
|
407
|
+
return
|
|
408
|
+
await wait_done()
|
|
409
|
+
self._handles.pop(run_id, None)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
__all__ = [
|
|
413
|
+
"RuntimeSession",
|
|
414
|
+
"RuntimeServiceRequest",
|
|
415
|
+
"RuntimeServiceHandle",
|
|
416
|
+
"RuntimeServiceFacade",
|
|
417
|
+
"build_session_context",
|
|
418
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Runtime 内部服务协议与可复用辅助函数。"""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from skills_runtime.core.errors import FrameworkIssue
|
|
9
|
+
|
|
10
|
+
from .config import RuntimeConfig
|
|
11
|
+
from .logging_utils import log_suppressed_exception
|
|
12
|
+
from .protocol.capability import CapabilityResult, CapabilityStatus
|
|
13
|
+
from .protocol.context import ExecutionContext
|
|
14
|
+
from .registry import CapabilityRegistry
|
|
15
|
+
from .types import NodeReport
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class RuntimeServices(Protocol):
|
|
20
|
+
"""Runtime 对内服务协议(供 adapters/engines 依赖)。"""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def config(self) -> RuntimeConfig:
|
|
24
|
+
"""运行时配置。"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def registry(self) -> CapabilityRegistry:
|
|
28
|
+
"""能力注册表。"""
|
|
29
|
+
|
|
30
|
+
async def execute_capability(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
spec: Any,
|
|
34
|
+
input: Dict[str, Any],
|
|
35
|
+
context: ExecutionContext,
|
|
36
|
+
) -> CapabilityResult:
|
|
37
|
+
"""执行能力(Runtime 内部分发)。"""
|
|
38
|
+
|
|
39
|
+
def create_sdk_agent(self, *, llm_config: Optional[Dict[str, Any]] = None) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
创建 per-run SDK Agent。
|
|
42
|
+
|
|
43
|
+
参数:
|
|
44
|
+
- llm_config:可选 LLM 覆写配置(当前仅支持 `model` 字段覆写)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def preflight(self) -> list[FrameworkIssue]:
|
|
48
|
+
"""执行 skills preflight。"""
|
|
49
|
+
|
|
50
|
+
def build_fail_closed_report(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
run_id: str,
|
|
54
|
+
status: str,
|
|
55
|
+
reason: Optional[str],
|
|
56
|
+
completion_reason: str,
|
|
57
|
+
meta: Dict[str, Any],
|
|
58
|
+
) -> NodeReport:
|
|
59
|
+
"""构造 fail-closed NodeReport。"""
|
|
60
|
+
|
|
61
|
+
def redact_issue(self, issue: Any) -> Dict[str, Any]:
|
|
62
|
+
"""把 FrameworkIssue 做最小披露归一。"""
|
|
63
|
+
|
|
64
|
+
def get_host_meta(self, *, context: ExecutionContext) -> Dict[str, Any]:
|
|
65
|
+
"""读取 host 保留元数据。"""
|
|
66
|
+
|
|
67
|
+
def call_callback(self, cb: Any, *args: Any) -> None:
|
|
68
|
+
"""兼容调用 callback。"""
|
|
69
|
+
|
|
70
|
+
def emit_agent_event_taps(self, *, ev: Any, context: ExecutionContext, capability_id: str) -> None:
|
|
71
|
+
"""分发 AgentEvent taps。"""
|
|
72
|
+
|
|
73
|
+
def apply_output_validation(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
final_output: Any,
|
|
77
|
+
report: NodeReport,
|
|
78
|
+
context: Dict[str, Any],
|
|
79
|
+
output_schema: Optional[Any] = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""执行输出校验并写入 NodeReport.meta。"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def redact_issue(issue: FrameworkIssue) -> Dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
把 FrameworkIssue 归一为"可诊断但最小披露"的 dict。
|
|
87
|
+
|
|
88
|
+
说明:
|
|
89
|
+
- 避免把 details 原样透传导致泄露或膨胀;
|
|
90
|
+
- 当前仅保留 code/message,并在 details 里保留常见定位字段(若存在)。
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
code = str(getattr(issue, "code", "") or "")
|
|
94
|
+
message = str(getattr(issue, "message", "") or "")
|
|
95
|
+
details = getattr(issue, "details", None)
|
|
96
|
+
out: Dict[str, Any] = {"code": code, "message": message}
|
|
97
|
+
if isinstance(details, dict):
|
|
98
|
+
slim: Dict[str, Any] = {}
|
|
99
|
+
for k in ("path", "source", "kind"):
|
|
100
|
+
v = details.get(k)
|
|
101
|
+
if isinstance(v, str) and v:
|
|
102
|
+
slim[k] = v
|
|
103
|
+
if slim:
|
|
104
|
+
out["details"] = slim
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_host_meta(*, context: ExecutionContext) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
从 ExecutionContext.bag 中读取 host-meta(保留字段)。
|
|
111
|
+
|
|
112
|
+
约定:
|
|
113
|
+
- 该字段为框架保留键,不应与业务输入冲突;
|
|
114
|
+
- 结构:{"session_id": str, "host_turn_id": str, "initial_history": list[dict]}
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
raw = context.bag.get("__host_meta__")
|
|
118
|
+
return raw if isinstance(raw, dict) else {}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def call_callback(cb: Any, *args: Any) -> None:
|
|
122
|
+
"""
|
|
123
|
+
以"尽量兼容"的方式调用 callback。
|
|
124
|
+
|
|
125
|
+
说明:
|
|
126
|
+
- 支持 VAR_POSITIONAL(*args)handler:全量传入;
|
|
127
|
+
- 统计 POSITIONAL_ONLY + POSITIONAL_OR_KEYWORD 类型参数(不含 KEYWORD_ONLY、VAR_KEYWORD);
|
|
128
|
+
- 若签名/调用失败,抛异常由调用方决定是否吞掉。
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
sig = inspect.signature(cb)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
# 无法获取签名时回退:尝试全量传入
|
|
135
|
+
log_suppressed_exception(
|
|
136
|
+
context="callback_signature_inspection",
|
|
137
|
+
exc=exc,
|
|
138
|
+
extra={"callback": getattr(cb, "__name__", repr(cb))},
|
|
139
|
+
)
|
|
140
|
+
cb(*args)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
# 检查是否有 VAR_POSITIONAL(*args)
|
|
144
|
+
has_var_positional = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in sig.parameters.values())
|
|
145
|
+
if has_var_positional:
|
|
146
|
+
cb(*args)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# 统计 POSITIONAL 类型参数(POSITIONAL_ONLY + POSITIONAL_OR_KEYWORD)
|
|
150
|
+
positional_params = [
|
|
151
|
+
p
|
|
152
|
+
for p in sig.parameters.values()
|
|
153
|
+
if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# 根据 positional 参数数量决定传入参数
|
|
157
|
+
if len(positional_params) >= len(args):
|
|
158
|
+
cb(*args)
|
|
159
|
+
elif len(positional_params) > 0:
|
|
160
|
+
cb(*args[: len(positional_params)])
|
|
161
|
+
else:
|
|
162
|
+
cb()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def map_node_status(report: NodeReport) -> CapabilityStatus:
|
|
166
|
+
"""
|
|
167
|
+
将 NodeReport 控制面状态映射为 CapabilityStatus。
|
|
168
|
+
|
|
169
|
+
约束:
|
|
170
|
+
- needs_approval / incomplete 不得折叠为 failed(避免编排误判)。
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
if report.status == "success":
|
|
174
|
+
return CapabilityStatus.SUCCESS
|
|
175
|
+
if report.status == "failed":
|
|
176
|
+
return CapabilityStatus.FAILED
|
|
177
|
+
if report.status == "needs_approval":
|
|
178
|
+
return CapabilityStatus.PENDING
|
|
179
|
+
if report.status == "incomplete":
|
|
180
|
+
return CapabilityStatus.CANCELLED if report.reason == "cancelled" else CapabilityStatus.PENDING
|
|
181
|
+
return CapabilityStatus.FAILED
|