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,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"]
|