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,317 @@
1
+ """
2
+ Resume / 续跑辅助工具(可选)。
3
+
4
+ 定位:
5
+ - 提供“从 events.jsonl 回放得到的最小 resume 状态”的工具化入口;
6
+ - 默认用于摘要/诊断(例如:恢复 approvals cache、生成 resume summary);
7
+ - 不强制把 WAL replay 作为下一轮 prompt 的唯一 history 真相源(真相源仍由宿主持久化的 TurnDelta 决定)。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any, Iterable, List, Optional, Union
15
+
16
+ from pydantic import BaseModel, ConfigDict
17
+
18
+ from skills_runtime.core.contracts import AgentEvent
19
+ from skills_runtime.state.replay import ResumeReplayState, rebuild_resume_replay_state
20
+
21
+
22
+ def load_agent_events_from_locator(
23
+ *,
24
+ events_path: Union[Path, str],
25
+ wal_backend: Any | None = None,
26
+ ) -> List[AgentEvent]:
27
+ """
28
+ 从 locator 加载 AgentEvent 列表(用于回放/诊断)。
29
+
30
+ 参数:
31
+ - events_path:WAL 路径或 locator(本仓对外字段名为 `events_path`;上游 `skills-runtime-sdk>=1.0` 可能为 `wal_locator`)
32
+ - wal_backend:可选 WAL backend;当 locator 为 `wal://...` 时必须提供
33
+
34
+ 返回:
35
+ - AgentEvent 列表(按文件顺序)
36
+ """
37
+
38
+ loc = str(events_path)
39
+ if loc.startswith("wal://"):
40
+ if wal_backend is None:
41
+ raise ValueError("wal_backend is required for wal locator")
42
+ read_events = getattr(wal_backend, "read_events", None)
43
+ if callable(read_events):
44
+ return _coerce_agent_events(read_events(loc))
45
+ read_text = getattr(wal_backend, "read_text", None)
46
+ if callable(read_text):
47
+ raw = read_text(loc)
48
+ return _parse_agent_events_jsonl(raw)
49
+ raise TypeError("wal_backend does not support wal locator reads")
50
+
51
+ # best-effort:允许 filesystem locator 追加 `#run_id=...` 等片段;文件读取仅使用其路径部分。
52
+ if "#" in loc:
53
+ loc = loc.split("#", 1)[0]
54
+
55
+ raw = Path(loc).read_text(encoding="utf-8")
56
+ return _parse_agent_events_jsonl(raw)
57
+
58
+
59
+ def load_agent_events_from_jsonl(
60
+ *,
61
+ events_path: Union[Path, str],
62
+ wal_backend: Any | None = None,
63
+ ) -> List[AgentEvent]:
64
+ """
65
+ 兼容别名:保留旧函数名,实际委托到 locator 读取逻辑。
66
+ """
67
+
68
+ return load_agent_events_from_locator(events_path=events_path, wal_backend=wal_backend)
69
+
70
+
71
+ class ResumeReplaySummary(BaseModel):
72
+ """
73
+ resume replay 的最小摘要(默认用于诊断/观测,不包含 tool 输出明文)。
74
+ """
75
+
76
+ model_config = ConfigDict(extra="forbid")
77
+
78
+ events_count: int
79
+ last_terminal_type: Optional[str] = None
80
+ approvals: dict
81
+ tool_calls: "ReplayToolCallDigest"
82
+
83
+
84
+ class ReplayToolCallDigest(BaseModel):
85
+ """replay / resume 所需的最小 tool call 摘要。"""
86
+
87
+ model_config = ConfigDict(extra="forbid")
88
+
89
+ requested_count: int
90
+ finished_count: int
91
+ pending_count: int
92
+ latest_pending_call_ids: list[str]
93
+ latest_tool_calls: list[dict]
94
+
95
+
96
+ class HostResumeState(BaseModel):
97
+ """
98
+ 宿主续跑状态摘要。
99
+
100
+ 字段说明:
101
+ - `run_id`:本轮运行 ID
102
+ - `approvals`:最小审批统计
103
+ - `last_terminal_type`:最近终态事件类型
104
+ - `waiting_approval_key`:尚未决议的审批键
105
+ """
106
+
107
+ model_config = ConfigDict(extra="forbid")
108
+
109
+ run_id: str
110
+ approvals: dict
111
+ last_terminal_type: Optional[str] = None
112
+ waiting_approval_key: Optional[str] = None
113
+ tool_calls: ReplayToolCallDigest
114
+
115
+
116
+ def build_resume_replay_summary(
117
+ *,
118
+ events: List[AgentEvent] | None = None,
119
+ events_path: Union[Path, str, None] = None,
120
+ wal_backend: Any | None = None,
121
+ ) -> tuple[ResumeReplayState, ResumeReplaySummary]:
122
+ """
123
+ 基于 events 回放得到 resume state,并生成诊断摘要。
124
+
125
+ 参数:
126
+ - events:可选 AgentEvent 列表
127
+ - events_path:可选 locator;当未直接提供 events 时用于加载事件
128
+ - wal_backend:可选 WAL backend;当 events_path 为 `wal://...` 时透传给 locator 读取
129
+
130
+ 返回:
131
+ - (ResumeReplayState, ResumeReplaySummary)
132
+ """
133
+
134
+ events = _resolve_agent_events(events=events, events_path=events_path, wal_backend=wal_backend)
135
+ st = rebuild_resume_replay_state(events)
136
+ tool_calls = _build_replay_tool_call_digest(events)
137
+
138
+ last_terminal_type = None
139
+ for ev in reversed(events):
140
+ if ev.type in ("run_completed", "run_failed", "run_cancelled"):
141
+ last_terminal_type = ev.type
142
+ break
143
+
144
+ summary = ResumeReplaySummary(
145
+ events_count=len(events),
146
+ last_terminal_type=last_terminal_type,
147
+ approvals={
148
+ "approved_for_session_keys_count": len(st.approved_for_session_keys),
149
+ "denied_approvals_by_key_count": len(st.denied_approvals_by_key),
150
+ },
151
+ tool_calls=tool_calls,
152
+ )
153
+ return st, summary
154
+
155
+
156
+ def build_host_resume_state(
157
+ *,
158
+ events: List[AgentEvent] | None = None,
159
+ events_path: Union[Path, str, None] = None,
160
+ wal_backend: Any | None = None,
161
+ ) -> HostResumeState:
162
+ """
163
+ 基于 events 回放得到面向宿主的续跑状态。
164
+
165
+ 参数:
166
+ - events:可选 AgentEvent 列表
167
+ - events_path:可选 locator;当未直接提供 events 时用于加载事件
168
+ - wal_backend:可选 WAL backend;当 events_path 为 `wal://...` 时透传给 locator 读取
169
+
170
+ 返回:
171
+ - HostResumeState(不暴露 tool 输出明文)
172
+ """
173
+
174
+ events = _resolve_agent_events(events=events, events_path=events_path, wal_backend=wal_backend)
175
+ st, summary = build_resume_replay_summary(events=events)
176
+ requested_keys: List[str] = []
177
+ resolved_keys: set[str] = set()
178
+ run_id = ""
179
+
180
+ for ev in events:
181
+ if not run_id and isinstance(ev.run_id, str):
182
+ run_id = ev.run_id
183
+ approval_key = ev.payload.get("approval_key")
184
+ if not isinstance(approval_key, str) or not approval_key.strip():
185
+ continue
186
+ approval_key = approval_key.strip()
187
+ if ev.type == "approval_requested":
188
+ requested_keys.append(approval_key)
189
+ elif ev.type == "approval_decided":
190
+ resolved_keys.add(approval_key)
191
+
192
+ waiting_approval_key = None
193
+ for approval_key in reversed(requested_keys):
194
+ if approval_key not in resolved_keys:
195
+ waiting_approval_key = approval_key
196
+ break
197
+
198
+ return HostResumeState(
199
+ run_id=run_id,
200
+ approvals={
201
+ **summary.approvals,
202
+ "pending_approval_keys_count": len([key for key in requested_keys if key not in resolved_keys]),
203
+ },
204
+ last_terminal_type=summary.last_terminal_type,
205
+ waiting_approval_key=waiting_approval_key,
206
+ tool_calls=summary.tool_calls,
207
+ )
208
+
209
+
210
+ def _events_after_last_run_started(events: List[AgentEvent]) -> List[AgentEvent]:
211
+ """只消费最近一次 run_started 之后的事件片段,与上游 replay 口径保持一致。"""
212
+
213
+ last_idx = -1
214
+ for i, ev in enumerate(events):
215
+ if ev.type == "run_started":
216
+ last_idx = i
217
+ if last_idx < 0:
218
+ return list(events)
219
+ return list(events[last_idx + 1 :])
220
+
221
+
222
+ def _resolve_agent_events(
223
+ *,
224
+ events: List[AgentEvent] | None,
225
+ events_path: Union[Path, str, None],
226
+ wal_backend: Any | None,
227
+ ) -> List[AgentEvent]:
228
+ """统一解析 events / locator 两类输入。"""
229
+
230
+ if events is not None:
231
+ return list(events)
232
+ if events_path is None:
233
+ raise TypeError("either events or events_path is required")
234
+ return load_agent_events_from_locator(events_path=events_path, wal_backend=wal_backend)
235
+
236
+
237
+ def _parse_agent_events_jsonl(raw: str) -> List[AgentEvent]:
238
+ """把 JSONL 文本解析为 AgentEvent 列表。"""
239
+
240
+ events: List[AgentEvent] = []
241
+ for line in raw.splitlines():
242
+ s = line.strip()
243
+ if not s:
244
+ continue
245
+ obj = json.loads(s)
246
+ events.append(AgentEvent.model_validate(obj))
247
+ return events
248
+
249
+
250
+ def _coerce_agent_events(items: Iterable[Any]) -> List[AgentEvent]:
251
+ """把 backend 返回的事件序列归一为 AgentEvent 列表。"""
252
+
253
+ events: List[AgentEvent] = []
254
+ for item in items:
255
+ if isinstance(item, AgentEvent):
256
+ events.append(item)
257
+ else:
258
+ events.append(AgentEvent.model_validate(item))
259
+ return events
260
+
261
+
262
+ def _build_replay_tool_call_digest(events: List[AgentEvent]) -> ReplayToolCallDigest:
263
+ """从事件流中提取 replay / resume 所需的最小 tool call 摘要。"""
264
+
265
+ seg = _events_after_last_run_started(events)
266
+ requested_count = 0
267
+ finished_count = 0
268
+ pending_ids: list[str] = []
269
+ first_seen_order: list[str] = []
270
+ calls_by_id: dict[str, dict] = {}
271
+
272
+ for ev in seg:
273
+ if ev.type == "tool_call_requested":
274
+ call_id = str(ev.payload.get("call_id") or "").strip()
275
+ name = str(ev.payload.get("name") or ev.payload.get("tool") or "").strip()
276
+ if not call_id or not name:
277
+ continue
278
+ requested_count += 1
279
+ if call_id not in first_seen_order:
280
+ first_seen_order.append(call_id)
281
+ if call_id not in pending_ids:
282
+ pending_ids.append(call_id)
283
+ calls_by_id[call_id] = {
284
+ "call_id": call_id,
285
+ "name": name,
286
+ "step_id": str(ev.step_id or "").strip() or None,
287
+ "status": "pending",
288
+ }
289
+ continue
290
+
291
+ if ev.type == "tool_call_finished":
292
+ call_id = str(ev.payload.get("call_id") or "").strip()
293
+ name = str(ev.payload.get("tool") or ev.payload.get("name") or "").strip()
294
+ if not call_id or not name:
295
+ continue
296
+ finished_count += 1
297
+ if call_id not in first_seen_order:
298
+ first_seen_order.append(call_id)
299
+ if call_id in pending_ids:
300
+ pending_ids.remove(call_id)
301
+ prev = calls_by_id.get(call_id) or {}
302
+ calls_by_id[call_id] = {
303
+ "call_id": call_id,
304
+ "name": name,
305
+ "step_id": prev.get("step_id") or (str(ev.step_id or "").strip() or None),
306
+ "status": "finished",
307
+ }
308
+
309
+ latest_ids = first_seen_order[-20:]
310
+ latest_tool_calls = [calls_by_id[call_id] for call_id in latest_ids if call_id in calls_by_id]
311
+ return ReplayToolCallDigest(
312
+ requested_count=requested_count,
313
+ finished_count=finished_count,
314
+ pending_count=len(pending_ids),
315
+ latest_pending_call_ids=list(pending_ids),
316
+ latest_tool_calls=latest_tool_calls,
317
+ )
@@ -0,0 +1,132 @@
1
+ """
2
+ system/developer 提示词(策略层)工具。
3
+
4
+ MVR 约束:
5
+ - system/developer 提示词不通过 `initial_history` 注入;
6
+ - 由宿主生成 SDK overlays(prompt/config)并注入到 `skills_runtime.Agent`(或通过 Bridge 透传 config_paths)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ from dataclasses import dataclass
14
+ from typing import Any, Dict, Optional, Protocol, runtime_checkable
15
+
16
+ from pydantic import BaseModel, ConfigDict
17
+
18
+
19
+ class SystemPrompt(BaseModel):
20
+ """
21
+ system/developer 提示词集合。
22
+
23
+ 字段:
24
+ - system_text:system 提示词(可为空)
25
+ - developer_text:developer 提示词(可为空)
26
+ - policy_id:策略 ID/版本(可选;用于审计与回归追溯)
27
+ """
28
+
29
+ model_config = ConfigDict(extra="forbid")
30
+
31
+ system_text: Optional[str] = None
32
+ developer_text: Optional[str] = None
33
+ policy_id: Optional[str] = None
34
+
35
+
36
+ class SystemPromptDigest(BaseModel):
37
+ """
38
+ system 提示词注入摘要(最小披露)。
39
+
40
+ 注意:该对象不得包含提示词明文。
41
+ """
42
+
43
+ model_config = ConfigDict(extra="forbid")
44
+
45
+ injected: bool
46
+ sha256: Optional[str] = None
47
+ bytes: Optional[int] = None
48
+ policy_id: Optional[str] = None
49
+
50
+
51
+ @runtime_checkable
52
+ class SystemPromptProvider(Protocol):
53
+ """
54
+ SystemPromptProvider:由宿主提供/实现。
55
+
56
+ 说明:
57
+ - 框架不规定 system/developer 提示词从哪里来(文件/DB/规则引擎/租户配置)。
58
+ - 该 provider 只返回“策略文本”,实际注入由调用方通过 overlays 完成。
59
+ """
60
+
61
+ def get_system_prompt(self, *, context: Dict[str, Any]) -> SystemPrompt:
62
+ """
63
+ 读取/生成 system 提示词。
64
+
65
+ 参数:
66
+ - context:宿主上下文(例如 tenant/user/session_id 等),由宿主定义
67
+
68
+ 返回:
69
+ - SystemPrompt(可能为空)
70
+ """
71
+
72
+ ...
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class StaticSystemPromptProvider:
77
+ """最小默认实现:返回固定的 SystemPrompt(便于测试与示例)。"""
78
+
79
+ prompt: SystemPrompt
80
+
81
+ def get_system_prompt(self, *, context: Dict[str, Any]) -> SystemPrompt:
82
+ """忽略上下文,返回固定 prompt。"""
83
+
84
+ return self.prompt
85
+
86
+
87
+ def build_prompt_overlay(*, prompt: SystemPrompt) -> Dict[str, Any]:
88
+ """
89
+ 把 SystemPrompt 转成 skills_runtime 配置 overlays(prompt.*)。
90
+
91
+ 参数:
92
+ - prompt:SystemPrompt
93
+
94
+ 返回:
95
+ - overlays dict(可写入 YAML 并作为 config_paths 传入)
96
+ """
97
+
98
+ return {
99
+ "prompt": {
100
+ "system_text": prompt.system_text,
101
+ "developer_text": prompt.developer_text,
102
+ }
103
+ }
104
+
105
+
106
+ def compute_system_prompt_digest(*, prompt: SystemPrompt) -> SystemPromptDigest:
107
+ """
108
+ 计算 system prompt 摘要(sha256/bytes/policy_id),用于 NodeReport.meta。
109
+
110
+ 参数:
111
+ - prompt:SystemPrompt(可能为空)
112
+
113
+ 返回:
114
+ - SystemPromptDigest(不含明文)
115
+ """
116
+
117
+ injected = bool((prompt.system_text or "").strip() or (prompt.developer_text or "").strip())
118
+ if not injected:
119
+ return SystemPromptDigest(injected=False, sha256=None, bytes=0, policy_id=prompt.policy_id)
120
+
121
+ canonical = {
122
+ "system_text": prompt.system_text or "",
123
+ "developer_text": prompt.developer_text or "",
124
+ "policy_id": prompt.policy_id,
125
+ }
126
+ raw = json.dumps(canonical, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
127
+ return SystemPromptDigest(
128
+ injected=True,
129
+ sha256=hashlib.sha256(raw).hexdigest(),
130
+ bytes=len(raw),
131
+ policy_id=prompt.policy_id,
132
+ )
@@ -0,0 +1,128 @@
1
+ """
2
+ TurnDelta:一次 turn 的“事实记录”(数据面 + 控制面 + 审计指针)。
3
+
4
+ 说明:
5
+ - TurnDelta 的目标是让业务存储“可审计事实”,而不是被迫存储上游 messages 细节。
6
+ - TurnDelta 可用于生成下一轮的 `initial_history`(通过 HistoryAssembler),也可用于审计/回放(通过 events_path 指针)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from dataclasses import dataclass
13
+ from typing import Optional, Protocol, runtime_checkable
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+ from ..types import NodeReport
18
+
19
+
20
+ @runtime_checkable
21
+ class TurnDeltaRedactor(Protocol):
22
+ """
23
+ TurnDelta 脱敏策略接口(由宿主实现)。
24
+
25
+ 约束:
26
+ - 该接口仅处理“用户/助手文本”的脱敏;tool 证据链脱敏由 SDK/Bridge 负责。
27
+ - 默认实现不保证识别所有 secrets;宿主应按自身合规要求实现更强策略。
28
+ """
29
+
30
+ def redact_user_input(self, text: str) -> str:
31
+ """
32
+ 脱敏 user_input。
33
+
34
+ 参数:
35
+ - text:原始 user_input 文本
36
+
37
+ 返回:
38
+ - 脱敏后的文本(可截断/替换/移除)
39
+ """
40
+
41
+ ...
42
+
43
+ def redact_assistant_output(self, text: str) -> str:
44
+ """
45
+ 脱敏 assistant_output(即 final_output)。
46
+
47
+ 参数:
48
+ - text:原始 final_output 文本
49
+
50
+ 返回:
51
+ - 脱敏后的文本
52
+ """
53
+
54
+ ...
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class TruncatingTurnDeltaRedactor:
59
+ """
60
+ 默认脱敏器:仅做截断(避免过长内容进入存储/日志)。
61
+
62
+ 注意:该策略不做 secrets 识别,仅作为“最小兜底”。
63
+ """
64
+
65
+ max_chars: int = 2000
66
+
67
+ def redact_user_input(self, text: str) -> str:
68
+ """对 user_input 做截断脱敏。"""
69
+
70
+ s = str(text or "")
71
+ return s if len(s) <= self.max_chars else (s[: self.max_chars - 3] + "...")
72
+
73
+ def redact_assistant_output(self, text: str) -> str:
74
+ """对 assistant_output 做截断脱敏。"""
75
+
76
+ s = str(text or "")
77
+ return s if len(s) <= self.max_chars else (s[: self.max_chars - 3] + "...")
78
+
79
+
80
+ class TurnDelta(BaseModel):
81
+ """
82
+ TurnDelta(一次回合事实)。
83
+
84
+ 字段说明:
85
+ - session_id/host_turn_id:由宿主生成,用于生命周期管理与追溯。
86
+ - run_id:一次执行的 run 标识(推荐与 host_turn_id 一一映射)。
87
+ - user_input:本回合用户输入(可选;若缺失则无法从 TurnDelta 组装回 user message)。
88
+ - final_output:数据面输出(自由文本允许,但建议宿主按需脱敏)。
89
+ - node_report:控制面强结构(NodeReport,schema v1)。
90
+ - events_path:WAL 定位符(locator;来自 SDK/Bridge;不得伪造)。
91
+ - created_at_ms:宿主记录的本回合创建时间(用于排序与回放);不作为安全证据源。
92
+ """
93
+
94
+ model_config = ConfigDict(extra="forbid")
95
+
96
+ session_id: Optional[str] = None
97
+ host_turn_id: Optional[str] = None
98
+ run_id: Optional[str] = None
99
+
100
+ user_input: Optional[str] = None
101
+ final_output: str = ""
102
+
103
+ node_report: NodeReport
104
+ events_path: Optional[str] = None
105
+
106
+ created_at_ms: int = Field(default_factory=lambda: int(time.time() * 1000))
107
+
108
+ def redacted(self, *, redactor: TurnDeltaRedactor) -> "TurnDelta":
109
+ """
110
+ 返回脱敏后的 TurnDelta(不修改原对象)。
111
+
112
+ 参数:
113
+ - redactor:脱敏策略
114
+
115
+ 返回:
116
+ - 新的 TurnDelta(user_input/final_output 经脱敏)
117
+ """
118
+
119
+ return TurnDelta(
120
+ session_id=self.session_id,
121
+ host_turn_id=self.host_turn_id,
122
+ run_id=self.run_id,
123
+ user_input=redactor.redact_user_input(self.user_input) if self.user_input is not None else None,
124
+ final_output=redactor.redact_assistant_output(self.final_output),
125
+ node_report=self.node_report,
126
+ events_path=self.events_path,
127
+ created_at_ms=self.created_at_ms,
128
+ )
@@ -0,0 +1,94 @@
1
+ """
2
+ 日志工具模块:提供运行时错误可观测性支持。
3
+
4
+ 设计要点:
5
+ - 使用标准库 logging,不引入外部依赖
6
+ - 所有静默异常记录到 DEBUG 级别
7
+ - 日志包含 exc_info=True 以保留完整堆栈
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any, Dict, Optional
14
+
15
+ # 模块级 logger
16
+ _logger = logging.getLogger("capability_runtime")
17
+
18
+
19
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
20
+ """
21
+ 获取 capability_runtime 命名空间下的 logger。
22
+
23
+ 参数:
24
+ - name:可选子模块名,如 "runtime" → "capability_runtime.runtime"
25
+
26
+ 返回:
27
+ - logging.Logger 实例
28
+ """
29
+ if name:
30
+ return _logger.getChild(name)
31
+ return _logger
32
+
33
+
34
+ def log_suppressed_exception(
35
+ *,
36
+ context: str,
37
+ exc: BaseException,
38
+ run_id: Optional[str] = None,
39
+ capability_id: Optional[str] = None,
40
+ extra: Optional[Dict[str, Any]] = None,
41
+ ) -> None:
42
+ """
43
+ 记录被静默处理的异常(DEBUG 级别)。
44
+
45
+ 参数:
46
+ - context:异常发生上下文描述(如 "emit_agent_event_taps")
47
+ - exc:被捕获的异常实例
48
+ - run_id:可选运行 ID
49
+ - capability_id:可选能力 ID
50
+ - extra:可选额外字段
51
+
52
+ 说明:
53
+ - 使用 DEBUG 级别,避免生产日志量增加
54
+ - 自动包含 exc_info=True 保留堆栈
55
+ - 禁止在 extra 中包含敏感信息(密钥、凭证、完整 payload)
56
+ """
57
+ log_extra: Dict[str, Any] = {
58
+ "error_type": type(exc).__name__,
59
+ "error_message": str(exc)[:200], # 截断避免日志膨胀
60
+ }
61
+ if run_id:
62
+ log_extra["run_id"] = run_id
63
+ if capability_id:
64
+ log_extra["capability_id"] = capability_id
65
+ if extra:
66
+ # 过滤敏感字段
67
+ safe_extra = {k: v for k, v in extra.items() if k not in _SENSITIVE_KEYS}
68
+ log_extra.update(safe_extra)
69
+
70
+ _logger.debug(
71
+ f"suppressed exception in {context}",
72
+ exc_info=exc,
73
+ extra=log_extra,
74
+ )
75
+
76
+
77
+ # 敏感字段黑名单
78
+ _SENSITIVE_KEYS = frozenset([
79
+ "password",
80
+ "secret",
81
+ "token",
82
+ "api_key",
83
+ "apikey",
84
+ "credential",
85
+ "authorization",
86
+ "bearer",
87
+ "private_key",
88
+ ])
89
+
90
+
91
+ __all__ = [
92
+ "get_logger",
93
+ "log_suppressed_exception",
94
+ ]