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,930 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ 统一运行时:声明 → 注册 → 校验 → 执行 → 报告。
5
+
6
+ 定位:
7
+ - 对外只提供一个执行入口(Runtime),避免"双入口/双路径"导致的语义分叉;
8
+ - mock/bridge/sdk_native 通过 `RuntimeConfig.mode` 切换;
9
+ - 控制面证据链以 `NodeReport` 为主(事件聚合),数据面输出保持生态兼容。
10
+ """
11
+
12
+ import asyncio
13
+ import time
14
+ import uuid
15
+ from dataclasses import replace
16
+ from typing import Any, AsyncIterator, Dict, List, Optional, Union
17
+
18
+ from skills_runtime.core.contracts import AgentEvent
19
+
20
+ from .config import RuntimeConfig
21
+ from .guards import ExecutionGuards
22
+ from .host_protocol import (
23
+ ApprovalTicket,
24
+ HostRunSnapshot,
25
+ ResumeIntent,
26
+ build_approval_ticket_from_report,
27
+ build_resume_intent as build_host_resume_intent,
28
+ summarize_host_run_result,
29
+ )
30
+ from .manifest import CapabilityDescriptor, CapabilityManifestEntry, CapabilityVisibility, build_manifest_entry_from_spec
31
+ from .output_validator import OutputValidator
32
+ from .protocol.agent import AgentSpec
33
+ from .protocol.capability import CapabilityKind, CapabilityResult, CapabilityStatus
34
+ from .protocol.context import ExecutionContext, RecursionLimitError
35
+ from .protocol.workflow import WorkflowSpec
36
+ from .registry import AnySpec, CapabilityRegistry, _get_base
37
+ from .reporting.node_report import build_fail_closed_report
38
+ from .sdk_lifecycle import (
39
+ SdkLifecycle,
40
+ _normalize_skills_config_for_skills_runtime,
41
+ )
42
+ from .services import call_callback, get_host_meta, redact_issue
43
+ from .structured_output import (
44
+ finalize_structured_result,
45
+ parse_json_object_snapshot,
46
+ validate_structured_output,
47
+ )
48
+ from .structured_stream import StructuredStreamEvent, diff_top_level_fields
49
+ from .types import NodeReport
50
+ from .adapters.agent_adapter import AgentAdapter
51
+ from .adapters.workflow_engine import WorkflowStreamEvent
52
+ from .adapters.triggerflow_workflow_engine import TriggerFlowWorkflowEngine
53
+ from .runtime_ui_events_mixin import RuntimeUIEventsMixin
54
+ from .workflow_runtime import (
55
+ WorkflowReplayRequest,
56
+ WorkflowRunSnapshot,
57
+ summarize_workflow_items,
58
+ )
59
+
60
+
61
+ class Runtime(RuntimeUIEventsMixin):
62
+ """
63
+ 统一运行时(唯一入口)。
64
+
65
+ 关键语义:
66
+ - 注册与校验由 Registry 驱动;
67
+ - 执行入口只有 `run()` / `run_stream()`;
68
+ - `run()` 基于 `run_stream()` 实现;
69
+ - 并发安全:per-run guards、per-run SDK Agent(由实现保证不共享可变状态)。
70
+ """
71
+
72
+ def __init__(self, config: RuntimeConfig) -> None:
73
+ """
74
+ 构造 Runtime。
75
+
76
+ 参数:
77
+ - config:运行时配置(含 mode 与桥接注入点)
78
+
79
+ 注意:
80
+ - RuntimeConfig 为 frozen dataclass;内部可能通过 `replace()` 回填有效配置;
81
+ - 通过 `Runtime.config` 读取的配置可能与初始化时传入的实例不同。
82
+ """
83
+
84
+ self._config = config
85
+ self._registry = CapabilityRegistry()
86
+ self._sdk: Optional[SdkLifecycle] = None
87
+ # 兼容保留:部分内部逻辑仍通过 `_sdk_state` 读取 skills_config(仅只读)。
88
+ self._sdk_state: Any = None
89
+ self._output_validator = OutputValidator(
90
+ mode=self._config.output_validation_mode,
91
+ validator=self._config.output_validator,
92
+ )
93
+ # UI events taps:用于把 SDK AgentEvent(含 workflow 内 nested agent 事件)
94
+ # 旁路投影为 RuntimeEvent v1,不影响 NodeReport/WAL 真相源。
95
+ self._agent_event_taps: List[Any] = []
96
+ self._agent_adapter = AgentAdapter(services=self)
97
+ injected_engine = getattr(config, "workflow_engine", None)
98
+ self._workflow_engine = injected_engine if injected_engine is not None else TriggerFlowWorkflowEngine()
99
+
100
+ if config.mode in ("bridge", "sdk_native"):
101
+ self._sdk = SdkLifecycle(config)
102
+ self._sdk_state = self._sdk.state
103
+ # 兼容:大部分调用方仅提供 sdk_config_paths(overlay),并不显式传 skills_config。
104
+ # 为了让 Adapter/Engine 只依赖 RuntimeServices.config(而不读取内部 _sdk_state),
105
+ # 这里把"有效 skills_config"回填到对外暴露的 config 视图中(只读替换)。
106
+ if self._config.skills_config is None:
107
+ self._config = replace(self._config, skills_config=self._sdk_state.skills_config)
108
+
109
+ @property
110
+ def config(self) -> RuntimeConfig:
111
+ """
112
+ 运行时配置(只读)。
113
+
114
+ 注意:
115
+ - 返回的是运行时有效配置,可能与初始化时传入的 RuntimeConfig 实例不同;
116
+ - 内部可能通过 `replace()` 回填 skills_config 等字段。
117
+ """
118
+
119
+ return self._config
120
+
121
+ def register(self, spec: AnySpec) -> None:
122
+ """
123
+ 注册一个能力。
124
+
125
+ 参数:
126
+ - spec:AgentSpec 或 WorkflowSpec
127
+ """
128
+
129
+ self._registry.register_with_manifest(
130
+ spec,
131
+ entry=build_manifest_entry_from_spec(spec, source="runtime.register"),
132
+ )
133
+
134
+ def register_many(self, specs: List[AnySpec]) -> None:
135
+ """批量注册能力。"""
136
+
137
+ for s in specs:
138
+ self._registry.register_with_manifest(
139
+ s,
140
+ entry=build_manifest_entry_from_spec(s, source="runtime.register_many"),
141
+ )
142
+
143
+ def register_manifest_entry(self, entry: CapabilityManifestEntry) -> CapabilityManifestEntry:
144
+ """
145
+ 仅注册 manifest entry(允许先于 spec)。
146
+
147
+ 参数:
148
+ - entry:宿主定义的 manifest 元数据
149
+
150
+ 返回:
151
+ - 已注册的 manifest entry
152
+ """
153
+
154
+ return self._registry.register_manifest_entry(entry)
155
+
156
+ def register_with_manifest(
157
+ self,
158
+ spec: AnySpec,
159
+ *,
160
+ entry: CapabilityManifestEntry | None = None,
161
+ ) -> CapabilityManifestEntry:
162
+ """
163
+ 注册能力并同步 manifest entry。
164
+
165
+ 参数:
166
+ - spec:AgentSpec 或 WorkflowSpec
167
+ - entry:可选显式 manifest entry
168
+
169
+ 返回:
170
+ - 实际注册的 manifest entry
171
+ """
172
+
173
+ manifest_entry = entry or build_manifest_entry_from_spec(spec, source="runtime.register_with_manifest")
174
+ return self._registry.register_with_manifest(spec, entry=manifest_entry)
175
+
176
+ @property
177
+ def registry(self) -> CapabilityRegistry:
178
+ """
179
+ 能力注册表(只读视角)。
180
+
181
+ 说明:
182
+ - 主要用于 workflow 引擎递归分发执行时查询 target spec;
183
+ - 调用方不应直接修改内部状态(注册应通过 Runtime.register* 完成)。
184
+ """
185
+
186
+ return self._registry
187
+
188
+ def validate(self) -> List[str]:
189
+ """
190
+ 校验所有依赖,返回缺失能力 ID 列表。
191
+
192
+ 返回:
193
+ - 缺失 ID 列表;空列表表示全部满足
194
+ """
195
+
196
+ return self._registry.validate_dependencies()
197
+
198
+ def describe_capability(self, capability_id: str) -> CapabilityDescriptor | None:
199
+ """
200
+ 返回宿主可消费的 capability descriptor。
201
+
202
+ 参数:
203
+ - capability_id:能力 ID
204
+ """
205
+
206
+ return self._registry.get_descriptor(capability_id)
207
+
208
+ def list_capabilities(
209
+ self,
210
+ *,
211
+ visibility: CapabilityVisibility | None = None,
212
+ exposed_only: bool = False,
213
+ ) -> list[CapabilityDescriptor]:
214
+ """
215
+ 列出 capability descriptors。
216
+
217
+ 参数:
218
+ - visibility:可选可见性过滤
219
+ - exposed_only:仅返回 `entry.expose=True` 的能力
220
+ """
221
+
222
+ return self._registry.list_descriptors(visibility=visibility, exposed_only=exposed_only)
223
+
224
+ def build_approval_ticket(
225
+ self,
226
+ result: CapabilityResult,
227
+ *,
228
+ capability_id: str,
229
+ ) -> ApprovalTicket | None:
230
+ """
231
+ 从 terminal result 构造宿主 ApprovalTicket。
232
+
233
+ 参数:
234
+ - result:终态 CapabilityResult
235
+ - capability_id:能力 ID
236
+ """
237
+
238
+ return build_approval_ticket_from_report(result.node_report, capability_id=capability_id)
239
+
240
+ def summarize_host_run(
241
+ self,
242
+ result: CapabilityResult,
243
+ *,
244
+ capability_id: str,
245
+ ) -> HostRunSnapshot:
246
+ """
247
+ 把 terminal result 收敛为宿主运行摘要。
248
+
249
+ 参数:
250
+ - result:终态 CapabilityResult
251
+ - capability_id:能力 ID
252
+ """
253
+
254
+ return summarize_host_run_result(result, capability_id=capability_id)
255
+
256
+ def build_resume_intent(
257
+ self,
258
+ *,
259
+ run_id: str,
260
+ approval_key: str | None = None,
261
+ decision: str | None = None,
262
+ session_id: str | None = None,
263
+ host_turn_id: str | None = None,
264
+ ) -> ResumeIntent:
265
+ """
266
+ 构造宿主续跑意图。
267
+
268
+ 参数:
269
+ - run_id:运行 ID
270
+ - approval_key:可选审批键
271
+ - decision:可选审批决定
272
+ - session_id:可选会话 ID
273
+ - host_turn_id:可选宿主 turn ID
274
+ """
275
+
276
+ return build_host_resume_intent(
277
+ run_id=run_id,
278
+ approval_key=approval_key,
279
+ decision=decision,
280
+ session_id=session_id,
281
+ host_turn_id=host_turn_id,
282
+ )
283
+
284
+ def _build_missing_terminal_result(
285
+ self,
286
+ *,
287
+ run_id: str,
288
+ capability_id: str,
289
+ source: str,
290
+ error: str,
291
+ error_code: str = "ENGINE_ERROR",
292
+ ) -> CapabilityResult:
293
+ """
294
+ 为“内部未产出 terminal/result”这类不变量破坏合成 fail-closed 终态。
295
+ """
296
+
297
+ report = self.build_fail_closed_report(
298
+ run_id=run_id,
299
+ status="failed",
300
+ reason="engine_error",
301
+ completion_reason="missing_terminal_result",
302
+ meta={"source": source, "capability_id": capability_id},
303
+ )
304
+ return CapabilityResult(
305
+ status=CapabilityStatus.FAILED,
306
+ error=error,
307
+ error_code=error_code,
308
+ report=report,
309
+ node_report=report,
310
+ )
311
+
312
+ def _build_fail_closed_result(
313
+ self,
314
+ *,
315
+ run_id: str,
316
+ capability_id: str,
317
+ source: str,
318
+ error: str,
319
+ error_code: str,
320
+ reason: str,
321
+ completion_reason: str,
322
+ meta: Optional[Dict[str, Any]] = None,
323
+ ) -> CapabilityResult:
324
+ """为 public failure / contract failure 构造带 node_report 的 FAILED terminal。"""
325
+
326
+ report = self.build_fail_closed_report(
327
+ run_id=run_id,
328
+ status="failed",
329
+ reason=reason,
330
+ completion_reason=completion_reason,
331
+ meta={"source": source, "capability_id": capability_id, **(meta or {})},
332
+ )
333
+ return CapabilityResult(
334
+ status=CapabilityStatus.FAILED,
335
+ error=error,
336
+ error_code=error_code,
337
+ report=report,
338
+ node_report=report,
339
+ )
340
+
341
+ async def run(
342
+ self,
343
+ capability_id: str,
344
+ *,
345
+ input: Optional[Dict[str, Any]] = None,
346
+ context: Optional[ExecutionContext] = None,
347
+ ) -> CapabilityResult:
348
+ """
349
+ 非流式执行(等待完成后返回)。
350
+
351
+ 参数:
352
+ - capability_id:能力 ID
353
+ - input:输入参数 dict
354
+ - context:可选执行上下文(宿主控制;若不传则由 Runtime 创建)
355
+ """
356
+
357
+ result: Optional[CapabilityResult] = None
358
+ async for item in self.run_stream(capability_id, input=input, context=context):
359
+ if isinstance(item, CapabilityResult):
360
+ result = item
361
+ if result is None:
362
+ return self._build_missing_terminal_result(
363
+ run_id=context.run_id if context is not None else uuid.uuid4().hex,
364
+ capability_id=capability_id,
365
+ source="runtime.run",
366
+ error="Runtime.run_stream produced no terminal CapabilityResult",
367
+ )
368
+ return result
369
+
370
+ async def run_structured(
371
+ self,
372
+ capability_id: str,
373
+ *,
374
+ input: Optional[Dict[str, Any]] = None,
375
+ context: Optional[ExecutionContext] = None,
376
+ ) -> CapabilityResult:
377
+ """
378
+ 强结构输出入口:成功时返回 `dict` 输出。
379
+
380
+ 约束:
381
+ - 仅支持带 `output_schema` 的 Agent capability;
382
+ - 不改变 `run()` 的既有语义,而是在其之上做强结构收口。
383
+ """
384
+
385
+ spec = self._registry.get(capability_id)
386
+ if spec is None:
387
+ return await self.run(capability_id, input=input, context=context)
388
+ if not isinstance(spec, AgentSpec):
389
+ run_id = context.run_id if context is not None else uuid.uuid4().hex
390
+ return self._build_fail_closed_result(
391
+ run_id=run_id,
392
+ capability_id=capability_id,
393
+ source="runtime.run_structured",
394
+ error=f"Structured output is only supported for Agent capability: {capability_id!r}",
395
+ error_code="STRUCTURED_OUTPUT_UNSUPPORTED_KIND",
396
+ reason="structured_output_error",
397
+ completion_reason="structured_output_unsupported_kind",
398
+ )
399
+ if spec.output_schema is None or not spec.output_schema.fields:
400
+ run_id = context.run_id if context is not None else uuid.uuid4().hex
401
+ return self._build_fail_closed_result(
402
+ run_id=run_id,
403
+ capability_id=capability_id,
404
+ source="runtime.run_structured",
405
+ error=f"Structured output schema is missing for capability: {capability_id!r}",
406
+ error_code="STRUCTURED_OUTPUT_SCHEMA_MISSING",
407
+ reason="structured_output_error",
408
+ completion_reason="structured_output_schema_missing",
409
+ )
410
+
411
+ result = await self.run(capability_id, input=input, context=context)
412
+ if result.status != CapabilityStatus.SUCCESS:
413
+ return result
414
+
415
+ validation = validate_structured_output(
416
+ final_output=result.output,
417
+ output_schema=spec.output_schema,
418
+ capability_id=capability_id,
419
+ mode="error",
420
+ )
421
+ return finalize_structured_result(result=result, validation=validation, fail_on_error=True)
422
+
423
+ async def run_structured_stream(
424
+ self,
425
+ capability_id: str,
426
+ *,
427
+ input: Optional[Dict[str, Any]] = None,
428
+ context: Optional[ExecutionContext] = None,
429
+ ) -> AsyncIterator[StructuredStreamEvent]:
430
+ """
431
+ 结构化输出流式消费入口。
432
+
433
+ 说明:
434
+ - 面向业务代码,不透传 mixed stream / UI events 的全部细节;
435
+ - 仅在观察到 `llm_response_delta(text)` 时产出中途快照与字段更新。
436
+ """
437
+
438
+ spec = self._registry.get(capability_id)
439
+ ctx = context or ExecutionContext(run_id=uuid.uuid4().hex, max_depth=self._config.max_depth)
440
+ yield StructuredStreamEvent(type="started", run_id=ctx.run_id, capability_id=capability_id)
441
+
442
+ if spec is None:
443
+ yield StructuredStreamEvent(
444
+ type="terminal",
445
+ run_id=ctx.run_id,
446
+ capability_id=capability_id,
447
+ status=CapabilityStatus.FAILED.value,
448
+ error=f"Capability not found: {capability_id!r}",
449
+ error_code="CAPABILITY_NOT_FOUND",
450
+ )
451
+ return
452
+ if not isinstance(spec, AgentSpec):
453
+ yield StructuredStreamEvent(
454
+ type="terminal",
455
+ run_id=ctx.run_id,
456
+ capability_id=capability_id,
457
+ status=CapabilityStatus.FAILED.value,
458
+ error=f"Structured output is only supported for Agent capability: {capability_id!r}",
459
+ error_code="STRUCTURED_OUTPUT_UNSUPPORTED_KIND",
460
+ )
461
+ return
462
+ if spec.output_schema is None or not spec.output_schema.fields:
463
+ yield StructuredStreamEvent(
464
+ type="terminal",
465
+ run_id=ctx.run_id,
466
+ capability_id=capability_id,
467
+ status=CapabilityStatus.FAILED.value,
468
+ error=f"Structured output schema is missing for capability: {capability_id!r}",
469
+ error_code="STRUCTURED_OUTPUT_SCHEMA_MISSING",
470
+ )
471
+ return
472
+
473
+ accumulated_text = ""
474
+ previous_snapshot: Optional[Dict[str, Any]] = None
475
+ terminal: Optional[CapabilityResult] = None
476
+
477
+ async for item in self.run_stream(capability_id, input=input, context=ctx):
478
+ if isinstance(item, CapabilityResult):
479
+ terminal = item
480
+ continue
481
+ if not isinstance(item, AgentEvent):
482
+ continue
483
+ if item.type != "llm_response_delta":
484
+ continue
485
+ if str(item.payload.get("delta_type") or "") != "text":
486
+ continue
487
+
488
+ text = str(item.payload.get("text") or "")
489
+ if not text:
490
+ continue
491
+ accumulated_text += text
492
+ yield StructuredStreamEvent(
493
+ type="text_delta",
494
+ run_id=ctx.run_id,
495
+ capability_id=capability_id,
496
+ text=text,
497
+ )
498
+
499
+ snapshot = parse_json_object_snapshot(accumulated_text)
500
+ if snapshot is None or snapshot == previous_snapshot:
501
+ continue
502
+
503
+ yield StructuredStreamEvent(
504
+ type="object_snapshot",
505
+ run_id=ctx.run_id,
506
+ capability_id=capability_id,
507
+ snapshot=dict(snapshot),
508
+ )
509
+ for field, value in diff_top_level_fields(previous_snapshot, snapshot):
510
+ yield StructuredStreamEvent(
511
+ type="field_updated",
512
+ run_id=ctx.run_id,
513
+ capability_id=capability_id,
514
+ field=field,
515
+ value=value,
516
+ )
517
+ previous_snapshot = dict(snapshot)
518
+
519
+ if terminal is None:
520
+ fail_closed = self._build_missing_terminal_result(
521
+ run_id=ctx.run_id,
522
+ capability_id=capability_id,
523
+ source="runtime.run_structured_stream",
524
+ error="Runtime.run_stream produced no terminal CapabilityResult",
525
+ error_code="STRUCTURED_OUTPUT_MISSING_TERMINAL",
526
+ )
527
+ terminal = fail_closed
528
+
529
+ structured_terminal = terminal
530
+ if terminal.status == CapabilityStatus.SUCCESS:
531
+ validation = validate_structured_output(
532
+ final_output=terminal.output,
533
+ output_schema=spec.output_schema,
534
+ capability_id=capability_id,
535
+ mode="error",
536
+ )
537
+ structured_terminal = finalize_structured_result(
538
+ result=terminal,
539
+ validation=validation,
540
+ fail_on_error=True,
541
+ )
542
+
543
+ raw_output = structured_terminal.metadata.get("raw_output")
544
+ if not isinstance(raw_output, str):
545
+ if isinstance(terminal.output, str):
546
+ raw_output = terminal.output
547
+ else:
548
+ raw_output = None
549
+
550
+ yield StructuredStreamEvent(
551
+ type="terminal",
552
+ run_id=ctx.run_id,
553
+ capability_id=capability_id,
554
+ status=structured_terminal.status.value,
555
+ output=structured_terminal.output if isinstance(structured_terminal.output, dict) else None,
556
+ raw_output=raw_output,
557
+ error=structured_terminal.error,
558
+ error_code=structured_terminal.error_code,
559
+ )
560
+
561
+ async def run_stream(
562
+ self,
563
+ capability_id: str,
564
+ *,
565
+ input: Optional[Dict[str, Any]] = None,
566
+ context: Optional[ExecutionContext] = None,
567
+ ) -> AsyncIterator[Union[AgentEvent, WorkflowStreamEvent, CapabilityResult]]:
568
+ """
569
+ 流式执行(执行层混合流):过程中可能产出事件,最后产出终态 CapabilityResult。
570
+
571
+ 事实定义(本方法可能产出三类 item;消费端需自行分流处理):
572
+ - `AgentEvent`:仅在执行 Agent 且为 bridge/sdk_native 时出现;来自上游 `skills_runtime` 的事实事件流。
573
+ - `dict`(workflow.* 轻量事件):仅在执行 Workflow 时出现;只表达编排进度(started/step.* /finished),不承诺深审计细节。
574
+ - `CapabilityResult`:终态结果(最后一条)。其中 `node_report/events_path` 为证据指针(真相源仍为 WAL/events + NodeReport)。
575
+
576
+ 约束:
577
+ - mock 模式可能只产出终态 `CapabilityResult`(无中间事件);
578
+ - bridge/sdk_native 模式 MUST 转发上游 `AgentEvent`(如执行路径确实进入上游引擎);
579
+ - 如果你需要"单一稳定事件协议 + 续传游标 + 最小披露",请使用 `run_ui_events()` / `start_ui_events_session()`。
580
+
581
+ 注意:
582
+ - 本方法为内部/进阶接口;消费方需自行 isinstance 分流三种类型;
583
+ - 推荐使用 `run_ui_events()` 或 `start_ui_events_session()` 获得统一事件协议。
584
+ """
585
+
586
+ spec = self._registry.get(capability_id)
587
+ if spec is None:
588
+ run_id = context.run_id if context is not None else uuid.uuid4().hex
589
+ yield self._build_fail_closed_result(
590
+ run_id=run_id,
591
+ capability_id=capability_id,
592
+ source="runtime.run_stream",
593
+ error=f"Capability not found: {capability_id!r}",
594
+ error_code="CAPABILITY_NOT_FOUND",
595
+ reason="capability_not_found",
596
+ completion_reason="capability_not_found",
597
+ )
598
+ return
599
+
600
+ guards = ExecutionGuards(max_total_loop_iterations=self._config.max_total_loop_iterations)
601
+ if context is not None:
602
+ # 使用 with_guards() 确保 per-run 隔离,复制可变容器避免共享引用
603
+ ctx = context.with_guards(guards)
604
+ else:
605
+ ctx = ExecutionContext(
606
+ run_id=uuid.uuid4().hex,
607
+ max_depth=self._config.max_depth,
608
+ guards=guards,
609
+ )
610
+
611
+ started = time.monotonic()
612
+ if _get_base(spec).kind == CapabilityKind.AGENT:
613
+ async for x in self._execute_agent_stream(spec=spec, input=input or {}, context=ctx):
614
+ if isinstance(x, CapabilityResult):
615
+ x.duration_ms = (time.monotonic() - started) * 1000
616
+ yield x
617
+ return
618
+
619
+ async for x in self._execute_workflow_stream(spec=spec, input=input or {}, context=ctx):
620
+ if isinstance(x, CapabilityResult):
621
+ x.duration_ms = (time.monotonic() - started) * 1000
622
+ yield x
623
+ return
624
+
625
+ async def run_workflow_observable(
626
+ self,
627
+ workflow_id: str,
628
+ *,
629
+ input: Optional[Dict[str, Any]] = None,
630
+ context: Optional[ExecutionContext] = None,
631
+ ) -> AsyncIterator[Union[WorkflowStreamEvent, CapabilityResult]]:
632
+ """
633
+ workflow host-facing observable surface。
634
+
635
+ 参数:
636
+ - workflow_id:workflow ID
637
+ - input:可选输入
638
+ - context:可选执行上下文
639
+ """
640
+
641
+ async for item in self.run_stream(workflow_id, input=input, context=context):
642
+ if isinstance(item, AgentEvent):
643
+ continue
644
+ yield item
645
+
646
+ def summarize_workflow_run(
647
+ self,
648
+ *,
649
+ workflow_id: str,
650
+ items: list[Any],
651
+ terminal: CapabilityResult | None = None,
652
+ ) -> WorkflowRunSnapshot:
653
+ """
654
+ 收敛 workflow 运行摘要。
655
+
656
+ 参数:
657
+ - workflow_id:workflow ID
658
+ - items:workflow 轻量事件列表
659
+ - terminal:可选终态结果
660
+ """
661
+
662
+ return summarize_workflow_items(workflow_id=workflow_id, items=items, terminal=terminal)
663
+
664
+ async def replay_workflow(
665
+ self,
666
+ request: WorkflowReplayRequest,
667
+ *,
668
+ context: Optional[ExecutionContext] = None,
669
+ ) -> CapabilityResult:
670
+ """
671
+ 基于 host request 重放 workflow。
672
+
673
+ 参数:
674
+ - request:workflow replay 请求
675
+ - context:可选执行上下文;为空时使用 request.run_id 新建
676
+ """
677
+
678
+ ctx = context or ExecutionContext(run_id=request.run_id, max_depth=self._config.max_depth)
679
+ return await self.run(request.workflow_id, input=request.current_input or {}, context=ctx)
680
+
681
+ async def replay(
682
+ self,
683
+ *,
684
+ workflow_id: str,
685
+ run_id: str,
686
+ current_input: dict[str, Any],
687
+ ) -> CapabilityResult:
688
+ """
689
+ workflow replay 的最小公共桥接入口。
690
+
691
+ 参数:
692
+ - workflow_id:目标 workflow ID
693
+ - run_id:沿用的运行 ID
694
+ - current_input:当前输入
695
+ """
696
+
697
+ return await self.replay_workflow(
698
+ WorkflowReplayRequest(
699
+ workflow_id=workflow_id,
700
+ run_id=run_id,
701
+ current_input=current_input,
702
+ )
703
+ )
704
+
705
+ def bind_runtime_server(self) -> None:
706
+ """
707
+ 若配置了 runtime_server,则显式把本地 Runtime 绑定给它。
708
+ """
709
+
710
+ runtime_server = getattr(self._config, "runtime_server", None)
711
+ if runtime_server is None:
712
+ return
713
+ bind_runtime = getattr(runtime_server, "bind_runtime", None)
714
+ if callable(bind_runtime):
715
+ bind_runtime(self)
716
+ return
717
+ set_runtime = getattr(runtime_server, "set_runtime", None)
718
+ if callable(set_runtime):
719
+ set_runtime(self)
720
+ return
721
+ setattr(runtime_server, "runtime", self)
722
+
723
+ async def _execute(self, *, spec: AnySpec, input: Dict[str, Any], context: ExecutionContext) -> CapabilityResult:
724
+ """
725
+ 内部执行: 创建子 context 并分发到 Agent/Workflow 执行器。
726
+
727
+ 参数:
728
+ - spec: 能力声明
729
+ - input: 输入参数
730
+ - context: 执行上下文
731
+ """
732
+
733
+ base = _get_base(spec)
734
+ try:
735
+ child_ctx = context.child(base.id)
736
+ except RecursionLimitError as exc:
737
+ return self._build_fail_closed_result(
738
+ run_id=context.run_id,
739
+ capability_id=base.id,
740
+ source="runtime.execute",
741
+ error=str(exc),
742
+ error_code="RECURSION_LIMIT",
743
+ reason="recursion_limit",
744
+ completion_reason="recursion_limit",
745
+ meta={"error_type": "recursion_limit"},
746
+ )
747
+
748
+ if base.kind == CapabilityKind.AGENT:
749
+ # 非流式入口内部执行时,仍走流式实现并收敛为最终结果。
750
+ last: Optional[CapabilityResult] = None
751
+ async for item in self._execute_agent_stream(spec=spec, input=input, context=child_ctx):
752
+ if isinstance(item, CapabilityResult):
753
+ last = item
754
+ return last or self._build_missing_terminal_result(
755
+ run_id=child_ctx.run_id,
756
+ capability_id=base.id,
757
+ source="runtime.execute_agent",
758
+ error="Agent execution produced no result",
759
+ )
760
+
761
+ if not isinstance(spec, WorkflowSpec):
762
+ return self._build_fail_closed_result(
763
+ run_id=child_ctx.run_id,
764
+ capability_id=base.id,
765
+ source="runtime.execute",
766
+ error=f"Invalid workflow spec type: {type(spec).__name__}",
767
+ error_code="INVALID_WORKFLOW_SPEC",
768
+ reason="invalid_spec",
769
+ completion_reason="invalid_workflow_spec",
770
+ meta={"actual_type": type(spec).__name__},
771
+ )
772
+ return await self._workflow_engine.execute(spec=spec, input=input, context=child_ctx, services=self)
773
+
774
+ async def execute_capability(
775
+ self,
776
+ *,
777
+ spec: AnySpec,
778
+ input: Dict[str, Any],
779
+ context: ExecutionContext,
780
+ ) -> CapabilityResult:
781
+ """RuntimeServices 协议方法:执行能力并返回终态结果。"""
782
+
783
+ return await self._execute(spec=spec, input=input, context=context)
784
+
785
+ async def _execute_workflow_stream(
786
+ self,
787
+ *,
788
+ spec: AnySpec,
789
+ input: Dict[str, Any],
790
+ context: ExecutionContext,
791
+ ) -> AsyncIterator[Union[WorkflowStreamEvent, CapabilityResult]]:
792
+ """执行 WorkflowSpec(流式):轻量 workflow 事件 + 终态 CapabilityResult。"""
793
+
794
+ base = _get_base(spec)
795
+ try:
796
+ child_ctx = context.child(base.id)
797
+ except RecursionLimitError as exc:
798
+ yield self._build_fail_closed_result(
799
+ run_id=context.run_id,
800
+ capability_id=base.id,
801
+ source="runtime.execute_workflow_stream",
802
+ error=str(exc),
803
+ error_code="RECURSION_LIMIT",
804
+ reason="recursion_limit",
805
+ completion_reason="recursion_limit",
806
+ meta={"error_type": "recursion_limit"},
807
+ )
808
+ return
809
+
810
+ if not isinstance(spec, WorkflowSpec):
811
+ yield self._build_fail_closed_result(
812
+ run_id=child_ctx.run_id,
813
+ capability_id=base.id,
814
+ source="runtime.execute_workflow_stream",
815
+ error=f"Invalid workflow spec type: {type(spec).__name__}",
816
+ error_code="INVALID_WORKFLOW_SPEC",
817
+ reason="invalid_spec",
818
+ completion_reason="invalid_workflow_spec",
819
+ meta={"actual_type": type(spec).__name__},
820
+ )
821
+ return
822
+
823
+ async for item in self._workflow_engine.execute_stream(
824
+ spec=spec, input=input, context=child_ctx, services=self
825
+ ):
826
+ yield item
827
+
828
+ async def _execute_agent_stream(
829
+ self, *, spec: AnySpec, input: Dict[str, Any], context: ExecutionContext
830
+ ) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
831
+ """
832
+ 执行 AgentSpec(流式)。
833
+
834
+ 说明:
835
+ - mock 模式:直接调用 mock_handler,产出 CapabilityResult;
836
+ - bridge/sdk_native:使用上游 SDK Agent 执行并转发 AgentEvent,最终聚合 NodeReport。
837
+ """
838
+
839
+ if not isinstance(spec, AgentSpec):
840
+ yield self._build_fail_closed_result(
841
+ run_id=context.run_id,
842
+ capability_id=_get_base(spec).id,
843
+ source="runtime.execute_agent_stream",
844
+ error=f"Invalid agent spec type: {type(spec).__name__}",
845
+ error_code="INVALID_AGENT_SPEC",
846
+ reason="invalid_spec",
847
+ completion_reason="invalid_agent_spec",
848
+ meta={"actual_type": type(spec).__name__},
849
+ )
850
+ return
851
+
852
+ async for item in self._agent_adapter.execute_stream(spec=spec, input=input, context=context):
853
+ yield item
854
+
855
+ def build_fail_closed_report(
856
+ self,
857
+ *,
858
+ run_id: str,
859
+ status: str,
860
+ reason: Optional[str],
861
+ completion_reason: str,
862
+ meta: Dict[str, Any],
863
+ ) -> NodeReport:
864
+ """
865
+ RuntimeServices 协议方法:构造 fail-closed NodeReport。
866
+ """
867
+
868
+ return build_fail_closed_report(
869
+ run_id=run_id,
870
+ status=status,
871
+ reason=reason,
872
+ completion_reason=completion_reason,
873
+ meta=meta,
874
+ )
875
+
876
+ def apply_output_validation(
877
+ self,
878
+ *,
879
+ final_output: Any,
880
+ report: NodeReport,
881
+ context: Dict[str, Any],
882
+ output_schema: Optional[Any] = None,
883
+ ) -> None:
884
+ """
885
+ RuntimeServices 协议方法:执行输出校验并写入 NodeReport.meta。
886
+ """
887
+
888
+ self._output_validator.validate(
889
+ final_output=final_output,
890
+ report=report,
891
+ context=context,
892
+ output_schema=output_schema,
893
+ )
894
+
895
+ def redact_issue(self, issue: Any) -> Dict[str, Any]:
896
+ """RuntimeServices 协议方法:issue 最小披露归一。"""
897
+
898
+ return redact_issue(issue)
899
+
900
+ def get_host_meta(self, *, context: ExecutionContext) -> Dict[str, Any]:
901
+ """RuntimeServices 协议方法:读取 host 保留元数据。"""
902
+
903
+ return get_host_meta(context=context)
904
+
905
+ def call_callback(self, cb: Any, *args: Any) -> None:
906
+ """RuntimeServices 协议方法:兼容 callback 调用。"""
907
+
908
+ call_callback(cb, *args)
909
+
910
+ def preflight(self) -> list[Any]:
911
+ """RuntimeServices 协议方法:执行 skills preflight。"""
912
+
913
+ if self._sdk is None:
914
+ return []
915
+ return self._sdk.preflight()
916
+
917
+ def create_sdk_agent(self, *, llm_config: Optional[Dict[str, Any]] = None) -> Any:
918
+ """
919
+ RuntimeServices 协议方法:创建 per-run SDK Agent。
920
+
921
+ 参数:
922
+ - llm_config:可选 LLM 覆写配置(当前仅支持 `model` 字段覆写)
923
+ """
924
+
925
+ if self._sdk is None:
926
+ raise RuntimeError("SDK lifecycle is not initialized")
927
+ return self._sdk.create_agent(custom_tools=list(self._config.custom_tools), llm_config=llm_config)
928
+
929
+
930
+ __all__ = ["Runtime"]