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.
Files changed (52) hide show
  1. capability_runtime/__init__.py +90 -0
  2. capability_runtime/adapters/__init__.py +13 -0
  3. capability_runtime/adapters/agent_adapter.py +439 -0
  4. capability_runtime/adapters/agently_backend.py +423 -0
  5. capability_runtime/adapters/triggerflow_workflow_engine.py +865 -0
  6. capability_runtime/adapters/workflow_engine.py +43 -0
  7. capability_runtime/config.py +172 -0
  8. capability_runtime/errors.py +20 -0
  9. capability_runtime/guards.py +150 -0
  10. capability_runtime/host_protocol.py +400 -0
  11. capability_runtime/host_toolkit/__init__.py +55 -0
  12. capability_runtime/host_toolkit/approvals_profiles.py +94 -0
  13. capability_runtime/host_toolkit/evidence_hooks.py +65 -0
  14. capability_runtime/host_toolkit/history.py +74 -0
  15. capability_runtime/host_toolkit/invoke_capability.py +409 -0
  16. capability_runtime/host_toolkit/resume.py +317 -0
  17. capability_runtime/host_toolkit/system_prompt.py +132 -0
  18. capability_runtime/host_toolkit/turn_delta.py +128 -0
  19. capability_runtime/logging_utils.py +94 -0
  20. capability_runtime/manifest.py +173 -0
  21. capability_runtime/output_validator.py +139 -0
  22. capability_runtime/protocol/__init__.py +43 -0
  23. capability_runtime/protocol/agent.py +62 -0
  24. capability_runtime/protocol/capability.py +98 -0
  25. capability_runtime/protocol/chat_backend.py +38 -0
  26. capability_runtime/protocol/context.py +244 -0
  27. capability_runtime/protocol/workflow.py +119 -0
  28. capability_runtime/registry.py +287 -0
  29. capability_runtime/reporting/__init__.py +2 -0
  30. capability_runtime/reporting/node_report.py +497 -0
  31. capability_runtime/runtime.py +930 -0
  32. capability_runtime/runtime_ui_events_mixin.py +310 -0
  33. capability_runtime/sdk_lifecycle.py +982 -0
  34. capability_runtime/service_facade.py +418 -0
  35. capability_runtime/services.py +181 -0
  36. capability_runtime/structured_output.py +208 -0
  37. capability_runtime/structured_stream.py +38 -0
  38. capability_runtime/types.py +103 -0
  39. capability_runtime/ui_events/__init__.py +19 -0
  40. capability_runtime/ui_events/projector.py +617 -0
  41. capability_runtime/ui_events/session.py +292 -0
  42. capability_runtime/ui_events/store.py +127 -0
  43. capability_runtime/ui_events/transport.py +33 -0
  44. capability_runtime/ui_events/v1.py +76 -0
  45. capability_runtime/upstream_compat.py +182 -0
  46. capability_runtime/utils/__init__.py +1 -0
  47. capability_runtime/utils/usage.py +65 -0
  48. capability_runtime/workflow_runtime.py +218 -0
  49. capability_runtime-0.1.0.dist-info/METADATA +232 -0
  50. capability_runtime-0.1.0.dist-info/RECORD +52 -0
  51. capability_runtime-0.1.0.dist-info/WHEEL +5 -0
  52. 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