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,400 @@
1
+ from __future__ import annotations
2
+
3
+ """HITL / wait-resume / approval 的宿主协议收敛层。"""
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any, Optional
8
+
9
+ from .protocol.capability import CapabilityResult, CapabilityStatus
10
+ from .types import NodeReport, NodeToolCallReport
11
+
12
+
13
+ class HostRunStatus(str, Enum):
14
+ """宿主视角的运行状态。"""
15
+
16
+ RUNNING = "running"
17
+ WAITING_HUMAN = "waiting_human"
18
+ COMPLETED = "completed"
19
+ FAILED = "failed"
20
+ CANCELLED = "cancelled"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ApprovalTicket:
25
+ """
26
+ 待审批票据。
27
+
28
+ 参数:
29
+ - run_id:运行 ID
30
+ - capability_id:能力 ID
31
+ - approval_key:审批键
32
+ - tool_name/call_id:审批对应的工具调用标识
33
+ - workflow_id/workflow_instance_id/step_id:可选的 workflow 归属
34
+ - created_at_ms:best-effort 的创建时间戳
35
+ """
36
+
37
+ run_id: str
38
+ capability_id: str
39
+ approval_key: str
40
+ tool_name: str | None = None
41
+ call_id: str | None = None
42
+ workflow_id: str | None = None
43
+ workflow_instance_id: str | None = None
44
+ step_id: str | None = None
45
+ created_at_ms: int = 0
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class ResumeIntent:
50
+ """宿主续跑意图。"""
51
+
52
+ run_id: str
53
+ approval_key: str | None = None
54
+ decision: str | None = None
55
+ session_id: str | None = None
56
+ host_turn_id: str | None = None
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class HostRunSnapshot:
61
+ """
62
+ 宿主运行摘要。
63
+
64
+ 参数:
65
+ - run_id:运行 ID
66
+ - capability_id:能力 ID
67
+ - status:宿主状态
68
+ - node_status:NodeReport.status 原值(若有)
69
+ - approval_ticket:等待审批时的票据
70
+ - resume_state:宿主恢复所需的最小状态
71
+ - events_path:证据链 events 定位符
72
+ """
73
+
74
+ run_id: str
75
+ capability_id: str
76
+ status: HostRunStatus
77
+ node_status: str | None = None
78
+ approval_ticket: ApprovalTicket | None = None
79
+ resume_state: dict[str, Any] = field(default_factory=dict)
80
+ events_path: str | None = None
81
+ wait_kind: str | None = None
82
+ tool_name: str | None = None
83
+ call_id: str | None = None
84
+ workflow_id: str | None = None
85
+ workflow_instance_id: str | None = None
86
+ step_id: str | None = None
87
+ message_kind: str | None = None
88
+ message_preview: str | None = None
89
+
90
+
91
+ def build_resume_intent(
92
+ *,
93
+ run_id: str,
94
+ approval_key: str | None = None,
95
+ decision: str | None = None,
96
+ session_id: str | None = None,
97
+ host_turn_id: str | None = None,
98
+ ) -> ResumeIntent:
99
+ """
100
+ 构造宿主续跑意图。
101
+
102
+ 参数:
103
+ - run_id:运行 ID
104
+ - approval_key:可选审批键
105
+ - decision:可选审批决定
106
+ - session_id:可选会话 ID
107
+ - host_turn_id:可选宿主 turn ID
108
+ """
109
+
110
+ return ResumeIntent(
111
+ run_id=run_id,
112
+ approval_key=approval_key,
113
+ decision=decision,
114
+ session_id=session_id,
115
+ host_turn_id=host_turn_id,
116
+ )
117
+
118
+
119
+ def build_approval_ticket_from_report(
120
+ report: NodeReport | None,
121
+ *,
122
+ capability_id: str,
123
+ ) -> ApprovalTicket | None:
124
+ """
125
+ 从 NodeReport 恢复 ApprovalTicket。
126
+
127
+ 参数:
128
+ - report:NodeReport
129
+ - capability_id:能力 ID
130
+
131
+ 返回:
132
+ - 待审批票据;无法恢复时返回 None
133
+ """
134
+
135
+ if report is None:
136
+ return None
137
+
138
+ call = _select_waiting_tool_call(report)
139
+ if call is None:
140
+ return None
141
+
142
+ approval_key = (call.approval_key or "").strip()
143
+ if not approval_key:
144
+ return None
145
+
146
+ meta = report.meta if isinstance(report.meta, dict) else {}
147
+ created_at_ms = meta.get("approval_requested_at_ms")
148
+ if not isinstance(created_at_ms, int):
149
+ created_at_ms = 0
150
+
151
+ return ApprovalTicket(
152
+ run_id=_resolve_run_id(result=None, report=report, metadata=None),
153
+ capability_id=capability_id,
154
+ approval_key=approval_key,
155
+ tool_name=str(call.name or "").strip() or None,
156
+ call_id=str(call.call_id or "").strip() or None,
157
+ workflow_id=_optional_text(meta.get("workflow_id")),
158
+ workflow_instance_id=_optional_text(meta.get("workflow_instance_id")),
159
+ step_id=_optional_text(meta.get("step_id")),
160
+ created_at_ms=created_at_ms,
161
+ )
162
+
163
+
164
+ def summarize_host_run_result(result: CapabilityResult, *, capability_id: str) -> HostRunSnapshot:
165
+ """
166
+ 把 terminal result 收敛成宿主运行摘要。
167
+
168
+ 参数:
169
+ - result:终态 CapabilityResult
170
+ - capability_id:能力 ID
171
+ """
172
+
173
+ report = result.node_report
174
+ node_status = getattr(report, "status", None)
175
+ approval_ticket = build_approval_ticket_from_report(report, capability_id=capability_id)
176
+ status = _map_host_run_status(result=result, report=report, approval_ticket=approval_ticket)
177
+ wait_summary = _summarize_waiting_human_context(report=report, approval_ticket=approval_ticket)
178
+
179
+ resume_state: dict[str, Any] = {}
180
+ if approval_ticket is not None:
181
+ resume_state["waiting_approval_key"] = approval_ticket.approval_key
182
+
183
+ return HostRunSnapshot(
184
+ run_id=_resolve_run_id(result=result, report=report, metadata=result.metadata),
185
+ capability_id=capability_id,
186
+ status=status,
187
+ node_status=node_status,
188
+ approval_ticket=approval_ticket,
189
+ resume_state=resume_state,
190
+ events_path=_optional_text(getattr(report, "events_path", None)),
191
+ wait_kind=wait_summary["wait_kind"],
192
+ tool_name=wait_summary["tool_name"],
193
+ call_id=wait_summary["call_id"],
194
+ workflow_id=wait_summary["workflow_id"],
195
+ workflow_instance_id=wait_summary["workflow_instance_id"],
196
+ step_id=wait_summary["step_id"],
197
+ message_kind=wait_summary["message_kind"],
198
+ message_preview=wait_summary["message_preview"],
199
+ )
200
+
201
+
202
+ def project_host_runtime_data(result: CapabilityResult, *, capability_id: str = "") -> dict[str, Any] | None:
203
+ """
204
+ 为 UI terminal 事件投影最小宿主状态。
205
+
206
+ 参数:
207
+ - result:终态 CapabilityResult
208
+ - capability_id:可选能力 ID;投影等待审批最小摘要时可为空
209
+ """
210
+
211
+ snapshot = summarize_host_run_result(result, capability_id=capability_id)
212
+ if snapshot.status != HostRunStatus.WAITING_HUMAN:
213
+ return None
214
+ return {
215
+ "status": snapshot.status.value,
216
+ "wait_kind": snapshot.wait_kind,
217
+ "run_id": snapshot.run_id,
218
+ "node_status": snapshot.node_status,
219
+ "events_path": snapshot.events_path,
220
+ "tool_name": snapshot.tool_name,
221
+ "call_id": snapshot.call_id,
222
+ "workflow_id": snapshot.workflow_id,
223
+ "workflow_instance_id": snapshot.workflow_instance_id,
224
+ "step_id": snapshot.step_id,
225
+ "approval_key": snapshot.approval_ticket.approval_key if snapshot.approval_ticket is not None else None,
226
+ "message_kind": snapshot.message_kind,
227
+ "message_preview": snapshot.message_preview,
228
+ "resume_state": dict(snapshot.resume_state),
229
+ }
230
+
231
+
232
+ def _select_waiting_tool_call(report: NodeReport) -> NodeToolCallReport | None:
233
+ """选择最能代表 waiting approval 的工具调用。"""
234
+
235
+ waiting_candidates: list[NodeToolCallReport] = []
236
+ fallback_candidates: list[NodeToolCallReport] = []
237
+ for call in report.tool_calls:
238
+ if call.requires_approval:
239
+ fallback_candidates.append(call)
240
+ if call.approval_key and call.approval_decision is None:
241
+ waiting_candidates.append(call)
242
+ if waiting_candidates:
243
+ return waiting_candidates[0]
244
+ if report.status == "needs_approval" and fallback_candidates:
245
+ return fallback_candidates[0]
246
+ return None
247
+
248
+
249
+ def _map_host_run_status(
250
+ *,
251
+ result: CapabilityResult,
252
+ report: NodeReport | None,
253
+ approval_ticket: ApprovalTicket | None,
254
+ ) -> HostRunStatus:
255
+ """将 terminal result + NodeReport 映射为宿主状态。"""
256
+
257
+ if getattr(report, "status", None) == "needs_approval" or approval_ticket is not None:
258
+ return HostRunStatus.WAITING_HUMAN
259
+ if result.status == CapabilityStatus.SUCCESS:
260
+ return HostRunStatus.COMPLETED
261
+ if result.status == CapabilityStatus.FAILED:
262
+ return HostRunStatus.FAILED
263
+ if result.status == CapabilityStatus.CANCELLED:
264
+ return HostRunStatus.CANCELLED
265
+ return HostRunStatus.RUNNING
266
+
267
+
268
+ def _resolve_run_id(
269
+ *,
270
+ result: CapabilityResult | None,
271
+ report: NodeReport | None,
272
+ metadata: dict[str, Any] | None,
273
+ ) -> str:
274
+ """按 report → metadata → result.metadata 的顺序恢复 run_id。"""
275
+
276
+ if report is not None and isinstance(report.run_id, str) and report.run_id.strip():
277
+ return report.run_id.strip()
278
+ if isinstance(metadata, dict):
279
+ run_id = metadata.get("run_id")
280
+ if isinstance(run_id, str) and run_id.strip():
281
+ return run_id.strip()
282
+ if result is not None and isinstance(result.metadata, dict):
283
+ run_id = result.metadata.get("run_id")
284
+ if isinstance(run_id, str) and run_id.strip():
285
+ return run_id.strip()
286
+ return ""
287
+
288
+
289
+ def _optional_text(value: Any) -> Optional[str]:
290
+ """把可选字段归一为非空字符串。"""
291
+
292
+ if isinstance(value, str) and value.strip():
293
+ return value.strip()
294
+ return None
295
+
296
+
297
+ def _summarize_waiting_human_context(
298
+ *,
299
+ report: NodeReport | None,
300
+ approval_ticket: ApprovalTicket | None,
301
+ ) -> dict[str, str | None]:
302
+ """提取 waiting_human 场景的最小宿主摘要字段。"""
303
+
304
+ empty = {
305
+ "wait_kind": None,
306
+ "tool_name": None,
307
+ "call_id": None,
308
+ "workflow_id": None,
309
+ "workflow_instance_id": None,
310
+ "step_id": None,
311
+ "message_kind": None,
312
+ "message_preview": None,
313
+ }
314
+ if report is None:
315
+ return empty
316
+
317
+ meta = report.meta if isinstance(report.meta, dict) else {}
318
+ message_preview = _build_message_preview(meta.get("final_message"))
319
+ waiting_call = _select_host_waiting_tool_call(report)
320
+
321
+ tool_name = approval_ticket.tool_name if approval_ticket is not None else _optional_text(getattr(waiting_call, "name", None))
322
+ call_id = approval_ticket.call_id if approval_ticket is not None else _optional_text(getattr(waiting_call, "call_id", None))
323
+ workflow_id = approval_ticket.workflow_id if approval_ticket is not None else _optional_text(meta.get("workflow_id"))
324
+ workflow_instance_id = (
325
+ approval_ticket.workflow_instance_id if approval_ticket is not None else _optional_text(meta.get("workflow_instance_id"))
326
+ )
327
+ step_id = approval_ticket.step_id if approval_ticket is not None else _optional_text(meta.get("step_id"))
328
+
329
+ if approval_ticket is not None:
330
+ wait_kind = "approval"
331
+ message_kind = "approval_message"
332
+ else:
333
+ wait_kind, message_kind = _classify_non_approval_waiting(
334
+ raw_kind=meta.get("waiting_human_kind"),
335
+ tool_name=tool_name,
336
+ message_preview=message_preview,
337
+ )
338
+
339
+ return {
340
+ "wait_kind": wait_kind,
341
+ "tool_name": tool_name,
342
+ "call_id": call_id,
343
+ "workflow_id": workflow_id,
344
+ "workflow_instance_id": workflow_instance_id,
345
+ "step_id": step_id,
346
+ "message_kind": message_kind,
347
+ "message_preview": message_preview,
348
+ }
349
+
350
+
351
+ def _select_host_waiting_tool_call(report: NodeReport) -> NodeToolCallReport | None:
352
+ """选择最能代表 waiting_human 摘要的工具调用。"""
353
+
354
+ approval_call = _select_waiting_tool_call(report)
355
+ if approval_call is not None:
356
+ return approval_call
357
+
358
+ for call in report.tool_calls:
359
+ if _optional_text(call.name) in {"ask_human", "request_user_input"}:
360
+ return call
361
+ if report.tool_calls:
362
+ return report.tool_calls[0]
363
+ return None
364
+
365
+
366
+ def _classify_non_approval_waiting(
367
+ *,
368
+ raw_kind: Any,
369
+ tool_name: str | None,
370
+ message_preview: str | None,
371
+ ) -> tuple[str, str]:
372
+ """best-effort 分类非 approval 型 waiting_human。"""
373
+
374
+ normalized_kind = _optional_text(raw_kind)
375
+ if normalized_kind == "workflow_continue":
376
+ return "workflow_continue", "generic_waiting_human"
377
+ if normalized_kind in {"host_input", "ask_human", "request_user_input"}:
378
+ message_kind = "request_user_input" if normalized_kind == "request_user_input" else "ask_human"
379
+ return "host_input", message_kind
380
+
381
+ if tool_name == "request_user_input":
382
+ return "host_input", "request_user_input"
383
+ if tool_name == "ask_human":
384
+ return "host_input", "ask_human"
385
+
386
+ preview = str(message_preview or "").lower()
387
+ if "ask" in preview or "input" in preview:
388
+ return "host_input", "generic_waiting_human"
389
+ return "unknown", "generic_waiting_human"
390
+
391
+
392
+ def _build_message_preview(value: Any) -> str | None:
393
+ """把 waiting message 归一为单行、最多 120 个字符的摘要。"""
394
+
395
+ if not isinstance(value, str):
396
+ return None
397
+ normalized = " ".join(value.split())
398
+ if not normalized:
399
+ return None
400
+ return normalized[:120]
@@ -0,0 +1,55 @@
1
+ """
2
+ Host toolkit(宿主工具箱)。
3
+
4
+ 定位:
5
+ - 本包用于帮助宿主(Host)以“业务主控(真相源)+ 框架提供工具”的方式落地单智能体 session/turn 生命周期。
6
+ - 本包不实现业务存储/权限/审批 UI;仅提供可复用的模型、组装器与证据链辅助。
7
+
8
+ 注意:
9
+ - system/developer 提示词在 MVR 中不通过 `initial_history` 注入,而通过 SDK prompt/config overlays 注入(见 `system_prompt`)。
10
+ """
11
+
12
+ from .approvals_profiles import ApprovalsProfile, ApprovalsProfiles, validate_approvals_profile
13
+ from .evidence_hooks import SystemPromptEvidence, SystemPromptEvidenceHook
14
+ from .history import HistoryAssembler, HistoryAssemblerConfig
15
+ from .invoke_capability import InvokeCapabilityAllowlist, make_invoke_capability_tool
16
+ from .resume import (
17
+ ResumeReplaySummary,
18
+ build_resume_replay_summary,
19
+ load_agent_events_from_jsonl,
20
+ load_agent_events_from_locator,
21
+ )
22
+ from .system_prompt import (
23
+ StaticSystemPromptProvider,
24
+ SystemPrompt,
25
+ SystemPromptDigest,
26
+ SystemPromptProvider,
27
+ build_prompt_overlay,
28
+ compute_system_prompt_digest,
29
+ )
30
+ from .turn_delta import TurnDelta, TurnDeltaRedactor, TruncatingTurnDeltaRedactor
31
+
32
+ __all__ = [
33
+ "ApprovalsProfile",
34
+ "ApprovalsProfiles",
35
+ "validate_approvals_profile",
36
+ "SystemPromptEvidence",
37
+ "SystemPromptEvidenceHook",
38
+ "HistoryAssembler",
39
+ "HistoryAssemblerConfig",
40
+ "InvokeCapabilityAllowlist",
41
+ "make_invoke_capability_tool",
42
+ "ResumeReplaySummary",
43
+ "build_resume_replay_summary",
44
+ "load_agent_events_from_jsonl",
45
+ "load_agent_events_from_locator",
46
+ "StaticSystemPromptProvider",
47
+ "SystemPrompt",
48
+ "SystemPromptDigest",
49
+ "SystemPromptProvider",
50
+ "build_prompt_overlay",
51
+ "compute_system_prompt_digest",
52
+ "TurnDelta",
53
+ "TurnDeltaRedactor",
54
+ "TruncatingTurnDeltaRedactor",
55
+ ]
@@ -0,0 +1,94 @@
1
+ """
2
+ Approvals profiles:阻塞等待审批的最佳实践配置与校验。
3
+
4
+ 目标:
5
+ - 提供可移植的 dev/prod profile 模板(不绑定业务 UI);
6
+ - 强制校验 `approval_timeout_ms` 与 `max_wall_time_sec` 的关系,避免“审批永远等不到结果”。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, Optional
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+
17
+ class ApprovalsProfile(BaseModel):
18
+ """
19
+ ApprovalsProfile(可移植配置模板)。
20
+
21
+ 参数:
22
+ - name:profile 名称(dev/prod/custom)
23
+ - approval_timeout_ms:等待人类审批的最长时间(超时按 denied)
24
+ - max_wall_time_sec:一次 run 的最大墙钟时间(预算)
25
+ - buffer_ms:安全 buffer(避免 wall-time 先于 approval timeout 触发)
26
+ """
27
+
28
+ model_config = ConfigDict(extra="forbid")
29
+
30
+ name: str
31
+ approval_timeout_ms: int = Field(ge=1)
32
+ max_wall_time_sec: int = Field(ge=1)
33
+ buffer_ms: int = Field(default=60_000, ge=0)
34
+
35
+ def to_sdk_overlay(self) -> Dict[str, Any]:
36
+ """
37
+ 转为 skills_runtime 配置 overlays。
38
+
39
+ 返回:
40
+ - overlays dict(可写入 YAML 并作为 config_paths 传入)
41
+ """
42
+
43
+ return {
44
+ "run": {"max_wall_time_sec": int(self.max_wall_time_sec)},
45
+ "safety": {"approval_timeout_ms": int(self.approval_timeout_ms)},
46
+ }
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ApprovalsProfiles:
51
+ """
52
+ 推荐 profiles 集合(可作为默认值参考)。
53
+
54
+ 注意:这些值仅是框架建议;调用方必须根据实际审批时延与节点预算调整。
55
+ """
56
+
57
+ dev: ApprovalsProfile = field(
58
+ default_factory=lambda: ApprovalsProfile(
59
+ name="dev",
60
+ approval_timeout_ms=60_000,
61
+ max_wall_time_sec=600,
62
+ buffer_ms=60_000,
63
+ )
64
+ )
65
+ prod: ApprovalsProfile = field(
66
+ default_factory=lambda: ApprovalsProfile(
67
+ name="prod",
68
+ approval_timeout_ms=600_000,
69
+ max_wall_time_sec=1800,
70
+ buffer_ms=120_000,
71
+ )
72
+ )
73
+
74
+
75
+ def validate_approvals_profile(*, profile: ApprovalsProfile) -> None:
76
+ """
77
+ 校验 approvals profile 的 timeout 关系。
78
+
79
+ 规则:
80
+ - approval_timeout_ms <= max_wall_time_sec*1000 - buffer_ms
81
+
82
+ 参数:
83
+ - profile:待校验 profile
84
+
85
+ 异常:
86
+ - ValueError:关系不满足时抛出(用于 fail-closed)
87
+ """
88
+
89
+ max_ms = int(profile.max_wall_time_sec) * 1000
90
+ if int(profile.approval_timeout_ms) > max_ms - int(profile.buffer_ms):
91
+ raise ValueError(
92
+ "invalid approvals profile: approval_timeout_ms must be <= max_wall_time_sec*1000 - buffer_ms "
93
+ f"(got approval_timeout_ms={profile.approval_timeout_ms}, max_wall_time_sec={profile.max_wall_time_sec}, buffer_ms={profile.buffer_ms})"
94
+ )
@@ -0,0 +1,65 @@
1
+ """
2
+ 证据链辅助:把“system 策略注入摘要”写入 NodeReport.meta。
3
+
4
+ 说明:
5
+ - Bridge core 不应理解 system 策略文本;这里仅写入最小披露摘要(sha256/bytes/policy_id)。
6
+ - 该 hook 不要求必须实现 BridgeHook 全量方法;Bridge 调用侧通过 getattr 探测并安全调用。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any, Dict, Optional
13
+
14
+ from pydantic import BaseModel, ConfigDict
15
+
16
+ from ..types import NodeResult
17
+
18
+
19
+ class SystemPromptEvidence(BaseModel):
20
+ """
21
+ system prompt 注入证据(最小披露)。
22
+
23
+ 字段对齐 `openspec/specs/evidence-chain/spec.md`:
24
+ - system_prompt_injected
25
+ - system_prompt_sha256
26
+ - system_prompt_bytes
27
+ - system_policy_id
28
+ """
29
+
30
+ model_config = ConfigDict(extra="forbid")
31
+
32
+ system_prompt_injected: bool
33
+ system_prompt_sha256: Optional[str] = None
34
+ system_prompt_bytes: Optional[int] = None
35
+ system_policy_id: Optional[str] = None
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class SystemPromptEvidenceHook:
40
+ """
41
+ BridgeHook(片段):在返回结果前把 system prompt 摘要写入 NodeReport.meta。
42
+
43
+ 参数:
44
+ - evidence:system prompt 注入证据摘要(不得包含明文)
45
+ """
46
+
47
+ evidence: SystemPromptEvidence
48
+
49
+ def before_return_result(self, context: Dict[str, Any], node_result: NodeResult) -> None:
50
+ """
51
+ 在 Bridge 返回 NodeResult 前写入证据链摘要。
52
+
53
+ 参数:
54
+ - context:Bridge hook_context(脱敏摘要)
55
+ - node_result:桥接层返回值
56
+ """
57
+
58
+ meta = node_result.node_report.meta
59
+ meta.setdefault("system_prompt_injected", self.evidence.system_prompt_injected)
60
+ if self.evidence.system_prompt_sha256 is not None:
61
+ meta.setdefault("system_prompt_sha256", self.evidence.system_prompt_sha256)
62
+ if self.evidence.system_prompt_bytes is not None:
63
+ meta.setdefault("system_prompt_bytes", self.evidence.system_prompt_bytes)
64
+ if self.evidence.system_policy_id is not None:
65
+ meta.setdefault("system_policy_id", self.evidence.system_policy_id)
@@ -0,0 +1,74 @@
1
+ """
2
+ HistoryAssembler:从 TurnDelta 序列组装 `initial_history`(最小披露)。
3
+
4
+ 约束(MVR):
5
+ - 仅输出 `role in {"user","assistant"}` 的最小消息形态 `{role, content}`;
6
+ - tool 结果与敏感信息默认不进入 prompt(如需进入必须由宿主生成摘要后写入 user/assistant 文本)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+ from .turn_delta import TurnDelta
17
+
18
+
19
+ class HistoryAssemblerConfig(BaseModel):
20
+ """
21
+ HistoryAssembler 配置。
22
+
23
+ 参数:
24
+ - max_turns:最多回传多少个 turn(按时间顺序取末尾 N 个)
25
+ - max_message_chars:单条 message 的最大字符数(截断兜底,避免 prompt 膨胀)
26
+ """
27
+
28
+ model_config = ConfigDict(extra="forbid")
29
+
30
+ max_turns: int = Field(default=20, ge=0)
31
+ max_message_chars: int = Field(default=8000, ge=1)
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class HistoryAssembler:
36
+ """
37
+ HistoryAssembler:将 TurnDelta[] 组装为 `initial_history`。
38
+
39
+ 说明:
40
+ - 该类不读取 WAL,不推断工具行为;只消费宿主存储的 TurnDelta(真相源)。
41
+ - TurnDelta.user_input 缺失时会跳过 user message(保守策略)。
42
+ """
43
+
44
+ config: HistoryAssemblerConfig = field(default_factory=HistoryAssemblerConfig)
45
+
46
+ def build_initial_history(self, *, deltas: List[TurnDelta]) -> List[Dict[str, Any]]:
47
+ """
48
+ 组装 `initial_history`(OpenAI wire messages 子集)。
49
+
50
+ 参数:
51
+ - deltas:按时间顺序的 TurnDelta 列表(建议已按 created_at_ms 排序)
52
+
53
+ 返回:
54
+ - `[{role, content}, ...]`,其中 role 仅包含 user/assistant
55
+ """
56
+
57
+ if self.config.max_turns <= 0 or not deltas:
58
+ return []
59
+
60
+ tail = deltas[-self.config.max_turns :]
61
+ out: List[Dict[str, Any]] = []
62
+ for d in tail:
63
+ if d.user_input is not None:
64
+ out.append({"role": "user", "content": self._truncate(d.user_input)})
65
+ if d.final_output:
66
+ out.append({"role": "assistant", "content": self._truncate(d.final_output)})
67
+ return out
68
+
69
+ def _truncate(self, text: str) -> str:
70
+ """截断 message 内容(兜底)。"""
71
+
72
+ s = str(text or "")
73
+ n = self.config.max_message_chars
74
+ return s if len(s) <= n else (s[: n - 3] + "...")