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,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] + "...")
|