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,497 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NodeReportBuilder:把 SDK `AgentEvent` 聚合为 NodeReport(schema v1)。
|
|
3
|
+
|
|
4
|
+
对齐规格:
|
|
5
|
+
- `openspec/specs/evidence-chain/spec.md`
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
13
|
+
|
|
14
|
+
from skills_runtime.core.contracts import AgentEvent
|
|
15
|
+
|
|
16
|
+
from ..logging_utils import log_suppressed_exception
|
|
17
|
+
from ..types import NodeReport, NodeToolCallReport, NodeUsageReport
|
|
18
|
+
from ..utils.usage import extract_usage_metrics
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_skills_runtime_version() -> Optional[str]:
|
|
22
|
+
"""读取 skills_runtime.__version__(若可用)。"""
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import skills_runtime
|
|
26
|
+
|
|
27
|
+
v = getattr(skills_runtime, "__version__", None)
|
|
28
|
+
if isinstance(v, str) and v.strip():
|
|
29
|
+
return v.strip()
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
log_suppressed_exception(
|
|
32
|
+
context="get_skills_runtime_version",
|
|
33
|
+
exc=exc,
|
|
34
|
+
)
|
|
35
|
+
return None
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_dist_version(dist_name: str) -> Optional[str]:
|
|
40
|
+
"""读取已安装 distribution 的版本号;不存在则返回 None。"""
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return importlib.metadata.version(dist_name)
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
log_suppressed_exception(
|
|
46
|
+
context="get_dist_version",
|
|
47
|
+
exc=exc,
|
|
48
|
+
extra={"dist_name": dist_name},
|
|
49
|
+
)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_first_dist_version(dist_names: List[str]) -> Optional[str]:
|
|
54
|
+
"""
|
|
55
|
+
读取一组候选 distribution 名称中的第一个可用版本号。
|
|
56
|
+
|
|
57
|
+
参数:
|
|
58
|
+
- dist_names:候选 dist 名称列表(按优先级排序)
|
|
59
|
+
|
|
60
|
+
返回:
|
|
61
|
+
- 第一个可读取的版本号;都不可用则返回 None
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
for name in dist_names:
|
|
65
|
+
v = _get_dist_version(name)
|
|
66
|
+
if v:
|
|
67
|
+
return v
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class NodeReportBuilder:
|
|
73
|
+
"""
|
|
74
|
+
NodeReport 聚合器。
|
|
75
|
+
|
|
76
|
+
约束:
|
|
77
|
+
- 以证据链为主(tool/approval/run_* 事件)
|
|
78
|
+
- 不推断 domain payload
|
|
79
|
+
- 不把 stdout/stderr 大段塞进 NodeReport(避免泄露与膨胀)
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def build(self, *, events: List[AgentEvent]) -> NodeReport:
|
|
83
|
+
"""
|
|
84
|
+
从一次 run 的事件序列构造 NodeReport(schema v1)。
|
|
85
|
+
|
|
86
|
+
参数:
|
|
87
|
+
- `events`:按发生顺序排列的 SDK AgentEvent 列表
|
|
88
|
+
|
|
89
|
+
返回:
|
|
90
|
+
- NodeReport
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
if not events:
|
|
94
|
+
return NodeReport(
|
|
95
|
+
status="failed",
|
|
96
|
+
reason="no_events",
|
|
97
|
+
completion_reason="no_events",
|
|
98
|
+
engine={
|
|
99
|
+
"name": "skills-runtime-sdk-python",
|
|
100
|
+
"module": "skills_runtime",
|
|
101
|
+
"version": _get_skills_runtime_version()
|
|
102
|
+
or _get_first_dist_version(["skills-runtime-sdk", "skills-runtime-sdk-python"]),
|
|
103
|
+
},
|
|
104
|
+
bridge={"name": "capability-runtime", "version": _get_first_dist_version(["capability-runtime"])},
|
|
105
|
+
run_id="",
|
|
106
|
+
turn_id=None,
|
|
107
|
+
events_path=None,
|
|
108
|
+
activated_skills=[],
|
|
109
|
+
tool_calls=[],
|
|
110
|
+
artifacts=[],
|
|
111
|
+
meta={"missing_events_path": True, "final_message": None},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
run_id = events[0].run_id
|
|
115
|
+
turn_id = None
|
|
116
|
+
for ev in events:
|
|
117
|
+
if ev.turn_id:
|
|
118
|
+
turn_id = ev.turn_id
|
|
119
|
+
|
|
120
|
+
activated_skills: List[str] = []
|
|
121
|
+
seen_skills: set[str] = set()
|
|
122
|
+
|
|
123
|
+
artifacts: List[str] = []
|
|
124
|
+
seen_artifacts: set[str] = set()
|
|
125
|
+
|
|
126
|
+
def _add_artifact(path: str) -> None:
|
|
127
|
+
"""
|
|
128
|
+
记录 artifact 路径(去重但保持首次出现顺序)。
|
|
129
|
+
|
|
130
|
+
参数:
|
|
131
|
+
- path:artifact 路径字符串(必须为非空)
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
p = str(path or "").strip()
|
|
135
|
+
if not p or p in seen_artifacts:
|
|
136
|
+
return
|
|
137
|
+
artifacts.append(p)
|
|
138
|
+
seen_artifacts.add(p)
|
|
139
|
+
|
|
140
|
+
# call_id -> aggregated fields
|
|
141
|
+
tool_calls: Dict[str, Dict[str, Any]] = {}
|
|
142
|
+
approval_pending: set[str] = set()
|
|
143
|
+
requires_approval_inferred: set[str] = set()
|
|
144
|
+
|
|
145
|
+
# 兼容 SDK 默认 approvals 事件形态:
|
|
146
|
+
# - tool_call_requested payload 带 call_id/name
|
|
147
|
+
# - approval_requested/approval_decided payload 可能不带 call_id,仅能通过 step_id 关联到同一步的 tool_call_requested
|
|
148
|
+
step_to_call: Dict[str, Dict[str, str]] = {}
|
|
149
|
+
tool_safety: Dict[str, Dict[str, str]] = {}
|
|
150
|
+
|
|
151
|
+
completion_status: Optional[str] = None
|
|
152
|
+
completion_reason = ""
|
|
153
|
+
events_path: Optional[str] = None
|
|
154
|
+
final_error_kind: Optional[str] = None
|
|
155
|
+
final_message: Optional[str] = None
|
|
156
|
+
|
|
157
|
+
usage_model: Optional[str] = None
|
|
158
|
+
usage_input_total = 0
|
|
159
|
+
usage_output_total = 0
|
|
160
|
+
usage_total_total = 0
|
|
161
|
+
usage_input_seen = False
|
|
162
|
+
usage_output_seen = False
|
|
163
|
+
usage_total_seen = False
|
|
164
|
+
|
|
165
|
+
def _ensure_tool(call_id: str, *, name: str) -> Dict[str, Any]:
|
|
166
|
+
"""获取/初始化工具调用聚合槽位(以 call_id 为主键)。"""
|
|
167
|
+
|
|
168
|
+
if call_id not in tool_calls:
|
|
169
|
+
tool_calls[call_id] = {
|
|
170
|
+
"call_id": call_id,
|
|
171
|
+
"name": name,
|
|
172
|
+
"requires_approval": False,
|
|
173
|
+
"approval_key": None,
|
|
174
|
+
"approval_decision": None,
|
|
175
|
+
"approval_reason": None,
|
|
176
|
+
"ok": False,
|
|
177
|
+
"error_kind": None,
|
|
178
|
+
"data": None,
|
|
179
|
+
}
|
|
180
|
+
return tool_calls[call_id]
|
|
181
|
+
|
|
182
|
+
def _record_tool_safety(*, call_id: str, payload: Dict[str, Any], source: str) -> None:
|
|
183
|
+
"""记录最小 tool safety 摘要(仅保留枚举字符串与来源)。"""
|
|
184
|
+
|
|
185
|
+
args = payload.get("arguments")
|
|
186
|
+
if not isinstance(args, dict):
|
|
187
|
+
args = payload.get("args")
|
|
188
|
+
if not isinstance(args, dict):
|
|
189
|
+
args = payload.get("request")
|
|
190
|
+
if not isinstance(args, dict):
|
|
191
|
+
return
|
|
192
|
+
sandbox_permissions = args.get("sandbox_permissions")
|
|
193
|
+
if not isinstance(sandbox_permissions, str) or not sandbox_permissions.strip():
|
|
194
|
+
return
|
|
195
|
+
tool_safety.setdefault(
|
|
196
|
+
call_id,
|
|
197
|
+
{
|
|
198
|
+
"sandbox_permissions": sandbox_permissions.strip(),
|
|
199
|
+
"source": source,
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
for ev in events:
|
|
204
|
+
artifact_path = ev.payload.get("artifact_path")
|
|
205
|
+
if isinstance(artifact_path, str) and artifact_path.strip():
|
|
206
|
+
_add_artifact(artifact_path)
|
|
207
|
+
|
|
208
|
+
if ev.type == "skill_injected":
|
|
209
|
+
skill_name = str(ev.payload.get("skill_name") or "").strip()
|
|
210
|
+
if skill_name and skill_name not in seen_skills:
|
|
211
|
+
activated_skills.append(skill_name)
|
|
212
|
+
seen_skills.add(skill_name)
|
|
213
|
+
|
|
214
|
+
if ev.type == "llm_usage":
|
|
215
|
+
usage_summary = extract_usage_metrics(ev.payload)
|
|
216
|
+
model_name = usage_summary.get("model")
|
|
217
|
+
if isinstance(model_name, str) and model_name.strip():
|
|
218
|
+
usage_model = model_name.strip()
|
|
219
|
+
|
|
220
|
+
input_tokens = usage_summary.get("input_tokens")
|
|
221
|
+
if isinstance(input_tokens, int):
|
|
222
|
+
usage_input_total += input_tokens
|
|
223
|
+
usage_input_seen = True
|
|
224
|
+
|
|
225
|
+
output_tokens = usage_summary.get("output_tokens")
|
|
226
|
+
if isinstance(output_tokens, int):
|
|
227
|
+
usage_output_total += output_tokens
|
|
228
|
+
usage_output_seen = True
|
|
229
|
+
|
|
230
|
+
total_tokens = usage_summary.get("total_tokens")
|
|
231
|
+
if isinstance(total_tokens, int):
|
|
232
|
+
usage_total_total += total_tokens
|
|
233
|
+
usage_total_seen = True
|
|
234
|
+
|
|
235
|
+
if ev.type == "tool_call_requested":
|
|
236
|
+
call_id = str(ev.payload.get("call_id") or "").strip()
|
|
237
|
+
name = str(ev.payload.get("name") or "").strip()
|
|
238
|
+
if call_id and name:
|
|
239
|
+
_ensure_tool(call_id, name=name)
|
|
240
|
+
_record_tool_safety(call_id=call_id, payload=ev.payload, source="tool_call_requested")
|
|
241
|
+
if ev.step_id:
|
|
242
|
+
step_to_call[str(ev.step_id)] = {"call_id": call_id, "tool": name}
|
|
243
|
+
|
|
244
|
+
if ev.type == "approval_requested":
|
|
245
|
+
tool = str(ev.payload.get("tool") or "").strip()
|
|
246
|
+
approval_key = ev.payload.get("approval_key")
|
|
247
|
+
call_id = str(ev.payload.get("call_id") or "").strip()
|
|
248
|
+
if not call_id and ev.step_id:
|
|
249
|
+
mapped = step_to_call.get(str(ev.step_id))
|
|
250
|
+
if mapped and (not tool or tool == mapped.get("tool")):
|
|
251
|
+
call_id = mapped.get("call_id", "")
|
|
252
|
+
tool = tool or mapped.get("tool", "")
|
|
253
|
+
if call_id and tool:
|
|
254
|
+
t = _ensure_tool(call_id, name=tool)
|
|
255
|
+
t["requires_approval"] = True
|
|
256
|
+
# Bridge 无法稳定读取 tool spec 时,只能通过 approval_* 事件推断 requires_approval。
|
|
257
|
+
requires_approval_inferred.add(call_id)
|
|
258
|
+
_record_tool_safety(call_id=call_id, payload=ev.payload, source="approval_requested")
|
|
259
|
+
if isinstance(approval_key, str) and approval_key:
|
|
260
|
+
t["approval_key"] = approval_key
|
|
261
|
+
approval_pending.add(call_id)
|
|
262
|
+
|
|
263
|
+
if ev.type == "approval_decided":
|
|
264
|
+
tool = str(ev.payload.get("tool") or "").strip()
|
|
265
|
+
decision = ev.payload.get("decision")
|
|
266
|
+
reason = ev.payload.get("reason")
|
|
267
|
+
call_id = str(ev.payload.get("call_id") or "").strip()
|
|
268
|
+
if not call_id and ev.step_id:
|
|
269
|
+
mapped = step_to_call.get(str(ev.step_id))
|
|
270
|
+
if mapped and (not tool or tool == mapped.get("tool")):
|
|
271
|
+
call_id = mapped.get("call_id", "")
|
|
272
|
+
tool = tool or mapped.get("tool", "")
|
|
273
|
+
if call_id and tool:
|
|
274
|
+
t = _ensure_tool(call_id, name=tool)
|
|
275
|
+
t["requires_approval"] = True
|
|
276
|
+
requires_approval_inferred.add(call_id)
|
|
277
|
+
if isinstance(decision, str) and decision:
|
|
278
|
+
t["approval_decision"] = decision
|
|
279
|
+
if isinstance(reason, str) and reason:
|
|
280
|
+
t["approval_reason"] = reason
|
|
281
|
+
approval_pending.discard(call_id)
|
|
282
|
+
|
|
283
|
+
if ev.type == "tool_call_finished":
|
|
284
|
+
call_id = str(ev.payload.get("call_id") or "").strip()
|
|
285
|
+
tool = str(ev.payload.get("tool") or "").strip()
|
|
286
|
+
result = ev.payload.get("result") or {}
|
|
287
|
+
if call_id and tool:
|
|
288
|
+
t = _ensure_tool(call_id, name=tool)
|
|
289
|
+
if isinstance(result, dict):
|
|
290
|
+
ok = result.get("ok")
|
|
291
|
+
if isinstance(ok, bool):
|
|
292
|
+
t["ok"] = ok
|
|
293
|
+
error_kind = result.get("error_kind")
|
|
294
|
+
if isinstance(error_kind, str) and error_kind:
|
|
295
|
+
t["error_kind"] = error_kind
|
|
296
|
+
data = result.get("data")
|
|
297
|
+
if isinstance(data, dict):
|
|
298
|
+
t["data"] = data
|
|
299
|
+
|
|
300
|
+
if ev.type in ("run_completed", "run_failed", "run_cancelled", "run_waiting_human"):
|
|
301
|
+
# skills-runtime-sdk>=1.0 使用 `wal_locator` 作为 WAL/事件证据链定位符;
|
|
302
|
+
# 本仓对外仍沿用 `events_path` 字段名,语义上存放 locator 字符串(可能是文件路径,也可能是 wal://...)。
|
|
303
|
+
locator_raw = ev.payload.get("events_path")
|
|
304
|
+
if not (isinstance(locator_raw, str) and locator_raw.strip()):
|
|
305
|
+
locator_raw = ev.payload.get("wal_locator")
|
|
306
|
+
if isinstance(locator_raw, str) and locator_raw.strip():
|
|
307
|
+
events_path = locator_raw.strip()
|
|
308
|
+
|
|
309
|
+
# artifacts(run 级别产物列表)
|
|
310
|
+
artifacts_raw = ev.payload.get("artifacts")
|
|
311
|
+
if isinstance(artifacts_raw, list):
|
|
312
|
+
for item in artifacts_raw:
|
|
313
|
+
if isinstance(item, str) and item.strip():
|
|
314
|
+
_add_artifact(item)
|
|
315
|
+
|
|
316
|
+
completion_reason = ev.type
|
|
317
|
+
if ev.type == "run_completed":
|
|
318
|
+
completion_status = "success"
|
|
319
|
+
elif ev.type == "run_failed":
|
|
320
|
+
final_error_kind = ev.payload.get("error_kind") if isinstance(ev.payload.get("error_kind"), str) else None
|
|
321
|
+
final_message = ev.payload.get("message") if isinstance(ev.payload.get("message"), str) else None
|
|
322
|
+
# 对齐契约:预算耗尽属于“未完成”而非“失败”(Host 可能需要走补偿/降级)。
|
|
323
|
+
if final_error_kind in ("budget_exceeded", "terminated"):
|
|
324
|
+
completion_status = "incomplete"
|
|
325
|
+
else:
|
|
326
|
+
completion_status = "failed"
|
|
327
|
+
elif ev.type == "run_waiting_human":
|
|
328
|
+
completion_status = "needs_approval"
|
|
329
|
+
final_error_kind = ev.payload.get("error_kind") if isinstance(ev.payload.get("error_kind"), str) else None
|
|
330
|
+
final_message = ev.payload.get("message") if isinstance(ev.payload.get("message"), str) else None
|
|
331
|
+
else:
|
|
332
|
+
completion_status = "incomplete"
|
|
333
|
+
final_message = ev.payload.get("message") if isinstance(ev.payload.get("message"), str) else None
|
|
334
|
+
|
|
335
|
+
# 优先级:needs_approval(approval_requested / run_waiting_human) > run_failed > run_cancelled > run_completed
|
|
336
|
+
status = completion_status or "incomplete"
|
|
337
|
+
reason = None
|
|
338
|
+
if approval_pending:
|
|
339
|
+
status = "needs_approval"
|
|
340
|
+
reason = "approval_pending"
|
|
341
|
+
elif status == "needs_approval":
|
|
342
|
+
reason = "approval_pending"
|
|
343
|
+
elif status == "failed":
|
|
344
|
+
# 失败原因粗分类:优先 error_kind,其次 message
|
|
345
|
+
if final_error_kind:
|
|
346
|
+
if final_error_kind in ("permission", "policy", "approval_denied"):
|
|
347
|
+
reason = "tool_error"
|
|
348
|
+
elif final_error_kind in (
|
|
349
|
+
"validation",
|
|
350
|
+
"not_found",
|
|
351
|
+
"config_error",
|
|
352
|
+
"skill_config_error",
|
|
353
|
+
"missing_env_var",
|
|
354
|
+
"SKILL_PREFLIGHT_FAILED",
|
|
355
|
+
):
|
|
356
|
+
reason = "skill_config_error"
|
|
357
|
+
elif final_error_kind.startswith("network_") or final_error_kind in (
|
|
358
|
+
"auth_error",
|
|
359
|
+
"rate_limited",
|
|
360
|
+
"server_error",
|
|
361
|
+
"http_error",
|
|
362
|
+
"context_length_exceeded",
|
|
363
|
+
):
|
|
364
|
+
reason = "llm_error"
|
|
365
|
+
else:
|
|
366
|
+
reason = "unknown"
|
|
367
|
+
else:
|
|
368
|
+
reason = "unknown"
|
|
369
|
+
elif status == "incomplete":
|
|
370
|
+
if final_error_kind == "budget_exceeded":
|
|
371
|
+
reason = "budget_exceeded"
|
|
372
|
+
elif final_error_kind == "terminated":
|
|
373
|
+
reason = "cancelled"
|
|
374
|
+
else:
|
|
375
|
+
reason = "cancelled" if completion_reason == "run_cancelled" else "unknown"
|
|
376
|
+
|
|
377
|
+
tool_reports = [
|
|
378
|
+
NodeToolCallReport.model_validate(item) for item in tool_calls.values() if isinstance(item, dict)
|
|
379
|
+
]
|
|
380
|
+
usage_report: Optional[NodeUsageReport] = None
|
|
381
|
+
if usage_model is not None or usage_input_seen or usage_output_seen or usage_total_seen:
|
|
382
|
+
usage_report = NodeUsageReport(
|
|
383
|
+
model=usage_model,
|
|
384
|
+
input_tokens=usage_input_total if usage_input_seen else None,
|
|
385
|
+
output_tokens=usage_output_total if usage_output_seen else None,
|
|
386
|
+
total_tokens=usage_total_total if usage_total_seen else None,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
report = NodeReport(
|
|
390
|
+
status=status, # type: ignore[arg-type]
|
|
391
|
+
reason=reason,
|
|
392
|
+
completion_reason=completion_reason or "",
|
|
393
|
+
engine={
|
|
394
|
+
"name": "skills-runtime-sdk-python",
|
|
395
|
+
"module": "skills_runtime",
|
|
396
|
+
"version": _get_skills_runtime_version()
|
|
397
|
+
or _get_first_dist_version(["skills-runtime-sdk", "skills-runtime-sdk-python"]),
|
|
398
|
+
},
|
|
399
|
+
bridge={"name": "capability-runtime", "version": _get_first_dist_version(["capability-runtime"])},
|
|
400
|
+
run_id=run_id,
|
|
401
|
+
turn_id=turn_id,
|
|
402
|
+
events_path=events_path,
|
|
403
|
+
usage=usage_report,
|
|
404
|
+
activated_skills=activated_skills,
|
|
405
|
+
tool_calls=tool_reports,
|
|
406
|
+
artifacts=artifacts,
|
|
407
|
+
meta={
|
|
408
|
+
"missing_events_path": events_path is None,
|
|
409
|
+
"final_message": final_message,
|
|
410
|
+
**(
|
|
411
|
+
{
|
|
412
|
+
"approval_inference": {
|
|
413
|
+
"requires_approval_call_ids": sorted(requires_approval_inferred),
|
|
414
|
+
"source": "events_only",
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if requires_approval_inferred
|
|
418
|
+
else {}
|
|
419
|
+
),
|
|
420
|
+
**({"tool_safety": tool_safety} if tool_safety else {}),
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
return report
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def build_node_report_from_events(events: Iterable[AgentEvent]) -> NodeReport:
|
|
427
|
+
"""便捷函数:从事件迭代器构造 NodeReport(schema v1)。"""
|
|
428
|
+
|
|
429
|
+
builder = NodeReportBuilder()
|
|
430
|
+
return builder.build(events=list(events))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def build_fail_closed_report(
|
|
434
|
+
*,
|
|
435
|
+
run_id: str,
|
|
436
|
+
status: str,
|
|
437
|
+
reason: Optional[str],
|
|
438
|
+
completion_reason: str,
|
|
439
|
+
meta: Dict[str, Any],
|
|
440
|
+
) -> NodeReport:
|
|
441
|
+
"""
|
|
442
|
+
构造不依赖事件流的最小 NodeReport(用于 fail-closed 分支)。
|
|
443
|
+
|
|
444
|
+
说明:
|
|
445
|
+
- preflight/output validator 等 gate 可能在启动引擎前返回;
|
|
446
|
+
- 仍需产出稳定的 engine/bridge 身份信息,供编排与审计使用;
|
|
447
|
+
- events_path 在此分支为 None(不得伪造)。
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
def _get_version(names: List[str]) -> Optional[str]:
|
|
451
|
+
# 证据链上优先使用 skills_runtime.__version__(比 dist-info 更可靠,尤其在 editable 安装场景)。
|
|
452
|
+
if any(n in ("skills-runtime-sdk", "skills-runtime-sdk-python") for n in names):
|
|
453
|
+
try:
|
|
454
|
+
import skills_runtime
|
|
455
|
+
|
|
456
|
+
v = getattr(skills_runtime, "__version__", None)
|
|
457
|
+
if isinstance(v, str) and v.strip():
|
|
458
|
+
return v.strip()
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
log_suppressed_exception(
|
|
461
|
+
context="node_report_get_version_skills_runtime",
|
|
462
|
+
exc=exc,
|
|
463
|
+
)
|
|
464
|
+
for n in names:
|
|
465
|
+
try:
|
|
466
|
+
return importlib.metadata.version(n)
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
log_suppressed_exception(
|
|
469
|
+
context="node_report_get_version_dist",
|
|
470
|
+
exc=exc,
|
|
471
|
+
extra={"dist_name": n},
|
|
472
|
+
)
|
|
473
|
+
continue
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
return NodeReport(
|
|
477
|
+
status=status, # type: ignore[arg-type]
|
|
478
|
+
reason=reason,
|
|
479
|
+
completion_reason=completion_reason,
|
|
480
|
+
engine={
|
|
481
|
+
"name": "skills-runtime-sdk-python",
|
|
482
|
+
"module": "skills_runtime",
|
|
483
|
+
"version": _get_version(["skills-runtime-sdk", "skills-runtime-sdk-python"]),
|
|
484
|
+
},
|
|
485
|
+
bridge={
|
|
486
|
+
"name": "capability-runtime",
|
|
487
|
+
"version": _get_version(["capability-runtime"]),
|
|
488
|
+
},
|
|
489
|
+
run_id=run_id,
|
|
490
|
+
turn_id=None,
|
|
491
|
+
events_path=None,
|
|
492
|
+
usage=None,
|
|
493
|
+
activated_skills=[],
|
|
494
|
+
tool_calls=[],
|
|
495
|
+
artifacts=[],
|
|
496
|
+
meta=dict(meta or {}),
|
|
497
|
+
)
|