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,310 @@
1
+ from __future__ import annotations
2
+
3
+ """Runtime 的 UI events 投影能力(mixin)。"""
4
+
5
+ import asyncio
6
+ import uuid
7
+ from typing import Any, AsyncIterator, Dict, List, Optional, Protocol
8
+
9
+ from skills_runtime.core.contracts import AgentEvent
10
+
11
+ from .logging_utils import log_suppressed_exception
12
+ from .protocol.capability import CapabilityResult, CapabilityStatus
13
+ from .protocol.context import ExecutionContext
14
+
15
+ # UI events 队列容量上限(防止内存无界增长)
16
+ _UI_EVENTS_QUEUE_MAXSIZE = 1000
17
+
18
+
19
+ class _RuntimeUIEventsConfig(Protocol):
20
+ """mixin 依赖的最小配置表面。"""
21
+
22
+ max_depth: int
23
+
24
+
25
+ class RuntimeUIEventsHost(Protocol):
26
+ """RuntimeUIEventsMixin 宿主需要提供的最小能力表面。"""
27
+
28
+ _config: _RuntimeUIEventsConfig
29
+ _agent_event_taps: List[Any]
30
+
31
+ def _register_agent_event_tap(self, tap: Any) -> None:
32
+ """注册 AgentEvent tap。"""
33
+
34
+ ...
35
+
36
+ def _unregister_agent_event_tap(self, tap: Any) -> None:
37
+ """反注册 AgentEvent tap。"""
38
+
39
+ ...
40
+
41
+ def run_stream(
42
+ self,
43
+ capability_id: str,
44
+ *,
45
+ input: Optional[Dict[str, Any]] = None,
46
+ context: Optional[ExecutionContext] = None,
47
+ ) -> AsyncIterator[Any]:
48
+ """宿主 Runtime 的流式执行入口。"""
49
+
50
+ ...
51
+
52
+
53
+ class RuntimeUIEventsMixin:
54
+ """
55
+ UI events 投影相关方法集合。
56
+
57
+ 说明:
58
+ - 以 mixin 方式抽出,减少 Runtime 主类体积(便于聚焦“注册/校验/执行”主职责)。
59
+ - 依赖 Runtime 本体持有:
60
+ - `_config`、`_agent_event_taps`、`_register_agent_event_tap/_unregister_agent_event_tap` 等状态/方法。
61
+ """
62
+
63
+ _agent_event_taps: List[Any]
64
+
65
+ def _register_agent_event_tap(self: RuntimeUIEventsHost, tap: Any) -> None:
66
+ """
67
+ 注册一个 AgentEvent 旁路 tap(内部使用)。
68
+
69
+ 参数:
70
+ - tap:可调用对象,签名为 `(ev: AgentEvent, ctx: Dict[str, Any]) -> None`
71
+ """
72
+
73
+ self._agent_event_taps.append(tap)
74
+
75
+ def _unregister_agent_event_tap(self: RuntimeUIEventsHost, tap: Any) -> None:
76
+ """反注册 AgentEvent tap(内部使用)。"""
77
+
78
+ self._agent_event_taps = [t for t in self._agent_event_taps if t is not tap]
79
+
80
+ def emit_agent_event_taps(
81
+ self: RuntimeUIEventsHost,
82
+ *,
83
+ ev: AgentEvent,
84
+ context: ExecutionContext,
85
+ capability_id: str,
86
+ ) -> None:
87
+ """
88
+ 将 SDK AgentEvent 分发给内部 taps(不影响对外事件流)。
89
+
90
+ 说明:
91
+ - 只传递最小上下文信息(避免泄露 context.bag 的业务 payload)。
92
+ """
93
+
94
+ if not getattr(self, "_agent_event_taps", None):
95
+ return
96
+
97
+ def _collect_workflow_frames(ctx: ExecutionContext) -> List[Dict[str, str]]:
98
+ """
99
+ 从当前 context 向上回溯 parent_context,收集 workflow 嵌套链的最小信息。
100
+
101
+ 设计目标:
102
+ - 不依赖“在 bag 里维护可变栈”(child() 的 bag 是浅拷贝,list 会共享引用);
103
+ - 通过 `__wf_workflow_instance_id`(若存在)进行 frame 分段,支持递归/重入;
104
+ - 以最小字段(workflow_id/workflow_instance_id/step_id/branch_id)供 UI projector 组装 path。
105
+ """
106
+
107
+ frames_inner_to_outer: List[Dict[str, str]] = []
108
+ last_frame_key: Optional[str] = None
109
+ cur: Optional[ExecutionContext] = ctx
110
+ while cur is not None:
111
+ bag = dict(getattr(cur, "bag", {}) or {})
112
+
113
+ wf_id = bag.get("__wf_workflow_id")
114
+ wf_inst = bag.get("__wf_workflow_instance_id")
115
+ step_id = bag.get("__wf_step_id")
116
+ branch_id = bag.get("__wf_branch_id")
117
+
118
+ wf_id_s = wf_id.strip() if isinstance(wf_id, str) else ""
119
+ wf_inst_s = wf_inst.strip() if isinstance(wf_inst, str) else ""
120
+ step_id_s = step_id.strip() if isinstance(step_id, str) else ""
121
+ branch_id_s = branch_id.strip() if isinstance(branch_id, str) else ""
122
+
123
+ key = wf_inst_s or wf_id_s
124
+ if key and key != last_frame_key:
125
+ frame: Dict[str, str] = {}
126
+ if wf_id_s:
127
+ frame["workflow_id"] = wf_id_s
128
+ if wf_inst_s:
129
+ frame["workflow_instance_id"] = wf_inst_s
130
+ if step_id_s:
131
+ frame["step_id"] = step_id_s
132
+ if branch_id_s:
133
+ frame["branch_id"] = branch_id_s
134
+ frames_inner_to_outer.append(frame)
135
+ last_frame_key = key
136
+
137
+ cur = getattr(cur, "parent_context", None)
138
+
139
+ return list(reversed(frames_inner_to_outer))
140
+
141
+ bag = dict(getattr(context, "bag", {}) or {})
142
+ tap_ctx: Dict[str, Any] = {"run_id": context.run_id, "capability_id": capability_id}
143
+ for k, out_key in (
144
+ ("__wf_workflow_id", "workflow_id"),
145
+ ("__wf_workflow_instance_id", "workflow_instance_id"),
146
+ ("__wf_step_id", "step_id"),
147
+ ("__wf_branch_id", "branch_id"),
148
+ ):
149
+ v = bag.get(k)
150
+ if isinstance(v, str) and v.strip():
151
+ tap_ctx[out_key] = v.strip()
152
+
153
+ frames = _collect_workflow_frames(context)
154
+ if frames:
155
+ tap_ctx["wf_frames"] = frames
156
+
157
+ for t in list(self._agent_event_taps):
158
+ try:
159
+ t(ev, tap_ctx)
160
+ except Exception as exc:
161
+ # 旁路 tap 不得影响主流程(fail-open)
162
+ log_suppressed_exception(
163
+ context="agent_event_tap",
164
+ exc=exc,
165
+ run_id=context.run_id if context else None,
166
+ capability_id=capability_id,
167
+ extra={"event_type": getattr(ev, "type", None)},
168
+ )
169
+
170
+ async def run_ui_events(
171
+ self: RuntimeUIEventsHost,
172
+ capability_id: str,
173
+ *,
174
+ input: Optional[Dict[str, Any]] = None,
175
+ context: Optional[ExecutionContext] = None,
176
+ level: Any = None,
177
+ heartbeat_interval_s: float = 15.0,
178
+ ) -> AsyncIterator[Any]:
179
+ """
180
+ UI events 流式执行:输出 RuntimeEvent v1(Envelope/path/levels/types)。
181
+
182
+ 说明:
183
+ - 不改变现有 `run_stream()` 对外行为;这是新增的投影层输出;
184
+ - UI events 不是审计真相源;证据链仍以 NodeReport/WAL 为准;
185
+ - `rid` 默认等于 `seq` 的字符串,支持 after_id exclusive 的最小实现。
186
+ - 投影输入来源(事实):
187
+ - workflow 轻量事件与终态 `CapabilityResult`:通过消费 `run_stream()` 获取;
188
+ - 上游 `AgentEvent`:通过内部 tap 旁路收集(避免重复投影,并支持 workflow 内部 Agent 的 deep stream)。
189
+ """
190
+
191
+ from .ui_events.projector import RuntimeUIEventProjector, _AgentCtx
192
+ from .ui_events.v1 import StreamLevel
193
+
194
+ ctx = context or ExecutionContext(run_id=uuid.uuid4().hex, max_depth=self._config.max_depth)
195
+ lv = level if isinstance(level, StreamLevel) else StreamLevel.UI
196
+ projector = RuntimeUIEventProjector(run_id=ctx.run_id, level=lv)
197
+
198
+ q: asyncio.Queue = asyncio.Queue(maxsize=_UI_EVENTS_QUEUE_MAXSIZE)
199
+ done = False
200
+
201
+ def _tap(agent_ev: AgentEvent, tap_ctx: Dict[str, Any]) -> None:
202
+ # run_ui_events 仅服务于单一 run:必须在入队前过滤其它 run 的旁路 AgentEvent,
203
+ # 避免无关事件进入队列导致堆积/背压(参见 docs/specs/runtime-ui-events-v1.md)。
204
+ if str(tap_ctx.get("run_id") or "") != str(ctx.run_id):
205
+ return
206
+ try:
207
+ q.put_nowait(("agent_event", agent_ev, tap_ctx))
208
+ except asyncio.QueueFull:
209
+ # 队列满时静默丢弃(fail-open);UI events 不是审计真相源
210
+ pass
211
+
212
+ self._register_agent_event_tap(_tap)
213
+
214
+ async def _runner() -> None:
215
+ try:
216
+ async for item in self.run_stream(capability_id, input=input, context=ctx):
217
+ if isinstance(item, dict):
218
+ await q.put(("workflow_event", item))
219
+ elif isinstance(item, CapabilityResult):
220
+ await q.put(("terminal", item))
221
+ return
222
+ else:
223
+ # AgentEvent:由 tap 旁路处理,避免重复投影
224
+ continue
225
+ except Exception as exc:
226
+ await q.put(("error", exc))
227
+
228
+ task = asyncio.create_task(_runner())
229
+ try:
230
+ for ev in projector.start():
231
+ yield ev
232
+
233
+ while not done:
234
+ try:
235
+ item = await asyncio.wait_for(q.get(), timeout=float(heartbeat_interval_s))
236
+ except asyncio.TimeoutError:
237
+ if lv != StreamLevel.LITE:
238
+ yield projector.heartbeat()
239
+ continue
240
+
241
+ kind = item[0]
242
+ if kind == "agent_event":
243
+ agent_ev, tap_ctx = item[1], item[2]
244
+ agent_ctx = _AgentCtx(
245
+ run_id=str(tap_ctx.get("run_id") or ctx.run_id),
246
+ capability_id=str(tap_ctx.get("capability_id") or ""),
247
+ workflow_id=str(tap_ctx.get("workflow_id")) if isinstance(tap_ctx.get("workflow_id"), str) else None,
248
+ workflow_instance_id=str(tap_ctx.get("workflow_instance_id"))
249
+ if isinstance(tap_ctx.get("workflow_instance_id"), str)
250
+ else None,
251
+ step_id=str(tap_ctx.get("step_id")) if isinstance(tap_ctx.get("step_id"), str) else None,
252
+ branch_id=str(tap_ctx.get("branch_id")) if isinstance(tap_ctx.get("branch_id"), str) else None,
253
+ wf_frames=list(tap_ctx.get("wf_frames")) if isinstance(tap_ctx.get("wf_frames"), list) else None,
254
+ )
255
+ for out_ev in projector.on_agent_event(agent_ev, ctx=agent_ctx):
256
+ yield out_ev
257
+ elif kind == "workflow_event":
258
+ wf_ev = item[1]
259
+ for out_ev in projector.on_workflow_event(wf_ev):
260
+ yield out_ev
261
+ elif kind == "terminal":
262
+ terminal = item[1]
263
+ for out_ev in projector.on_terminal(terminal):
264
+ yield out_ev
265
+ done = True
266
+ elif kind == "error":
267
+ exc = item[1]
268
+ yield projector.error(kind="runner_error", message=str(exc))
269
+ for out_ev in projector.on_terminal(CapabilityResult(status=CapabilityStatus.FAILED, error=str(exc))):
270
+ yield out_ev
271
+ done = True
272
+
273
+ await task
274
+ finally:
275
+ self._unregister_agent_event_tap(_tap)
276
+
277
+ def start_ui_events_session(
278
+ self: RuntimeUIEventsHost,
279
+ capability_id: str,
280
+ *,
281
+ input: Optional[Dict[str, Any]] = None,
282
+ context: Optional[ExecutionContext] = None,
283
+ level: Any = None,
284
+ store: Any = None,
285
+ store_max_events: int = 10_000,
286
+ heartbeat_interval_s: float = 15.0,
287
+ ) -> Any:
288
+ """
289
+ 创建一次 run 的 UI events 会话(支持多订阅者 + after_id 续传)。
290
+ """
291
+
292
+ from .ui_events.session import RuntimeUIEventsSession
293
+ from .ui_events.store import InMemoryRuntimeEventStore
294
+ from .ui_events.v1 import StreamLevel
295
+
296
+ ctx = context or ExecutionContext(run_id=uuid.uuid4().hex, max_depth=self._config.max_depth)
297
+ lv = level if isinstance(level, StreamLevel) else StreamLevel.UI
298
+ store_impl = store if store is not None else InMemoryRuntimeEventStore(max_events=int(store_max_events))
299
+ return RuntimeUIEventsSession(
300
+ runtime=self,
301
+ capability_id=capability_id,
302
+ input=input or {},
303
+ context=ctx,
304
+ level=lv,
305
+ store=store_impl,
306
+ heartbeat_interval_s=float(heartbeat_interval_s),
307
+ )
308
+
309
+
310
+ __all__ = ["RuntimeUIEventsMixin", "RuntimeUIEventsHost"]