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,182 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ 上游兼容层(skills-runtime-sdk)。
5
+
6
+ 定位:
7
+ - 本仓作为 runtime/adapter/bridge 的“契约收敛层”,需要在不 fork 上游的前提下吸收破坏性变更;
8
+ - 本模块只做“版本感知的最小适配”,避免把上游变更扩散到业务与协议层(protocol/ 不应 import 上游)。
9
+
10
+ 当前覆盖:
11
+ - skills spaces schema:`account/domain` ↔ `namespace`
12
+ - strict mention:`$[account:domain].skill` ↔ `$[namespace].skill`
13
+ """
14
+
15
+ from typing import Any, Dict, List, Literal, Optional, Tuple
16
+
17
+ from .logging_utils import log_suppressed_exception
18
+
19
+
20
+ SkillsSpaceSchema = Literal["account_domain", "namespace"]
21
+
22
+
23
+ def detect_skills_space_schema() -> SkillsSpaceSchema:
24
+ """
25
+ 探测当前安装的 skills-runtime-sdk 期望的 skills.spaces schema。
26
+
27
+ 返回:
28
+ - "namespace":上游要求 `skills.spaces[].namespace`(并可能拒绝 legacy 字段)
29
+ - "account_domain":上游要求 `skills.spaces[].account` + `domain`
30
+ """
31
+
32
+ try:
33
+ import skills_runtime.config.loader as loader
34
+
35
+ space = getattr(getattr(loader, "AgentSdkSkillsConfig", None), "Space", None)
36
+ if space is not None:
37
+ fields = getattr(space, "model_fields", None)
38
+ if isinstance(fields, dict) and "namespace" in fields:
39
+ return "namespace"
40
+ except Exception as exc:
41
+ # 探测失败时保持保守:沿用历史 schema,避免误判导致初始化期直接崩。
42
+ log_suppressed_exception(
43
+ context="detect_skills_space_schema_loader",
44
+ exc=exc,
45
+ extra={"method": "AgentSdkSkillsConfig.Space"},
46
+ )
47
+ return "account_domain"
48
+
49
+ # fallback:通过 mentions API 特性推断(v0.1.5 引入 is_valid_namespace)
50
+ try:
51
+ import skills_runtime.skills.mentions as mentions
52
+
53
+ if hasattr(mentions, "is_valid_namespace"):
54
+ return "namespace"
55
+ except Exception as exc:
56
+ log_suppressed_exception(
57
+ context="detect_skills_space_schema_mentions",
58
+ exc=exc,
59
+ extra={"method": "mentions.is_valid_namespace"},
60
+ )
61
+ return "account_domain"
62
+
63
+ return "account_domain"
64
+
65
+
66
+ def build_namespace_from_account_domain(*, account: str, domain: str) -> str:
67
+ """
68
+ 将 legacy account/domain 映射为 namespace(最小无损映射:两段拼接)。
69
+
70
+ 参数:
71
+ - account:旧版 account slug
72
+ - domain:旧版 domain slug
73
+
74
+ 返回:
75
+ - namespace 字符串(形如 `account:domain`)
76
+ """
77
+
78
+ return f"{str(account).strip()}:{str(domain).strip()}"
79
+
80
+
81
+ def split_namespace_to_account_domain(namespace: str) -> Tuple[str, str]:
82
+ """
83
+ 将 namespace 映射回 legacy account/domain(仅当 namespace 恰好 2 段时允许)。
84
+
85
+ 参数:
86
+ - namespace:namespace 字符串(形如 `a:b` / `a:b:c`)
87
+
88
+ 返回:
89
+ - (account, domain)
90
+
91
+ 异常:
92
+ - ValueError:当 namespace 不是 2 段时(无法无损映射到旧版 schema)
93
+ """
94
+
95
+ raw = str(namespace).strip()
96
+ parts = [p for p in raw.split(":") if p]
97
+ if len(parts) != 2:
98
+ raise ValueError("namespace must have exactly 2 segments to map into legacy account/domain")
99
+ return parts[0], parts[1]
100
+
101
+
102
+ def normalize_spaces_for_upstream(
103
+ *,
104
+ spaces: Any,
105
+ target_schema: SkillsSpaceSchema,
106
+ ) -> Tuple[Optional[List[Dict[str, Any]]], List[str]]:
107
+ """
108
+ 归一化 `skills.spaces` 为上游可接受的字段集合(最小转换 + 可追溯 warnings)。
109
+
110
+ 参数:
111
+ - spaces:可能为 None / list[dict] / 其它(非 list 时返回 None)
112
+ - target_schema:目标 schema(account_domain 或 namespace)
113
+
114
+ 返回:
115
+ - normalized_spaces:归一后的 spaces(无法处理时为 None,表示“不改动/交给上游报错”)
116
+ - warnings:转换/丢弃/拒绝的摘要文本(用于 worklog/测试取证;不绑定上游 Issue 结构)
117
+ """
118
+
119
+ if spaces is None:
120
+ return None, []
121
+ if not isinstance(spaces, list):
122
+ return None, []
123
+
124
+ warnings: List[str] = []
125
+ out: List[Dict[str, Any]] = []
126
+
127
+ for idx, sp in enumerate(spaces):
128
+ if not isinstance(sp, dict):
129
+ warnings.append(f"skills.spaces[{idx}] is not a dict; keep as-is (skip normalize)")
130
+ return None, warnings
131
+
132
+ sp_obj: Dict[str, Any] = dict(sp)
133
+
134
+ if target_schema == "namespace":
135
+ if isinstance(sp_obj.get("namespace"), str) and sp_obj.get("namespace", "").strip():
136
+ sp_obj.pop("account", None)
137
+ sp_obj.pop("domain", None)
138
+ out.append(sp_obj)
139
+ continue
140
+
141
+ account = sp_obj.get("account")
142
+ domain = sp_obj.get("domain")
143
+ if isinstance(account, str) and account.strip() and isinstance(domain, str) and domain.strip():
144
+ sp_obj["namespace"] = build_namespace_from_account_domain(account=account, domain=domain)
145
+ sp_obj.pop("account", None)
146
+ sp_obj.pop("domain", None)
147
+ warnings.append(f"converted skills.spaces[{idx}] account/domain -> namespace")
148
+ out.append(sp_obj)
149
+ continue
150
+
151
+ warnings.append(f"skills.spaces[{idx}] missing namespace and account/domain; keep as-is (skip normalize)")
152
+ return None, warnings
153
+
154
+ # target_schema == "account_domain"
155
+ if isinstance(sp_obj.get("account"), str) and sp_obj.get("account", "").strip() and isinstance(
156
+ sp_obj.get("domain"), str
157
+ ) and sp_obj.get("domain", "").strip():
158
+ sp_obj.pop("namespace", None)
159
+ out.append(sp_obj)
160
+ continue
161
+
162
+ namespace = sp_obj.get("namespace")
163
+ if isinstance(namespace, str) and namespace.strip():
164
+ try:
165
+ account, domain = split_namespace_to_account_domain(namespace)
166
+ except ValueError:
167
+ warnings.append(
168
+ f"skills.spaces[{idx}] namespace cannot map to legacy account/domain (need 2 segments)"
169
+ )
170
+ return None, warnings
171
+ sp_obj["account"] = account
172
+ sp_obj["domain"] = domain
173
+ sp_obj.pop("namespace", None)
174
+ warnings.append(f"converted skills.spaces[{idx}] namespace -> account/domain")
175
+ out.append(sp_obj)
176
+ continue
177
+
178
+ warnings.append(f"skills.spaces[{idx}] missing account/domain and namespace; keep as-is (skip normalize)")
179
+ return None, warnings
180
+
181
+ return out, warnings
182
+
@@ -0,0 +1 @@
1
+ """公共工具函数模块。"""
@@ -0,0 +1,65 @@
1
+ """
2
+ Usage 提取工具:从 LLM usage payload 中提取标准化指标。
3
+
4
+ 兼容:
5
+ - 本仓规范字段:`input_tokens/output_tokens/total_tokens`
6
+ - OpenAI 风格字段:`prompt_tokens/completion_tokens/total_tokens`
7
+ - 可选嵌套:`payload["usage"]`
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, Optional
13
+
14
+
15
+ def _usage_int(value: Any) -> Optional[int]:
16
+ """把 usage 数值归一为非负 int;无法识别时返回 None。"""
17
+
18
+ if isinstance(value, bool):
19
+ return None
20
+ if isinstance(value, int):
21
+ return value if value >= 0 else None
22
+ try:
23
+ parsed = int(value)
24
+ except (TypeError, ValueError):
25
+ return None
26
+ return parsed if parsed >= 0 else None
27
+
28
+
29
+ def extract_usage_metrics(payload: Any) -> Dict[str, Optional[Any]]:
30
+ """
31
+ 从 `llm_usage` payload 中提取 usage 摘要。
32
+
33
+ 参数:
34
+ - payload:AgentEvent.payload 或类似结构
35
+
36
+ 返回:
37
+ - dict 包含 model/input_tokens/output_tokens/total_tokens
38
+ """
39
+
40
+ if not isinstance(payload, dict):
41
+ return {"model": None, "input_tokens": None, "output_tokens": None, "total_tokens": None}
42
+
43
+ usage_raw = payload.get("usage")
44
+ usage_dict: Dict[str, Any] = usage_raw if isinstance(usage_raw, dict) else payload
45
+ model = payload.get("model")
46
+ if not isinstance(model, str) or not model.strip():
47
+ model = usage_dict.get("model")
48
+ model_text = model.strip() if isinstance(model, str) and model.strip() else None
49
+
50
+ input_tokens = _usage_int(usage_dict.get("input_tokens"))
51
+ if input_tokens is None:
52
+ input_tokens = _usage_int(usage_dict.get("prompt_tokens"))
53
+
54
+ output_tokens = _usage_int(usage_dict.get("output_tokens"))
55
+ if output_tokens is None:
56
+ output_tokens = _usage_int(usage_dict.get("completion_tokens"))
57
+
58
+ total_tokens = _usage_int(usage_dict.get("total_tokens"))
59
+
60
+ return {
61
+ "model": model_text,
62
+ "input_tokens": input_tokens,
63
+ "output_tokens": output_tokens,
64
+ "total_tokens": total_tokens,
65
+ }
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ """Workflow host-facing runtime state / replay surface."""
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from .host_protocol import project_host_runtime_data
10
+ from .protocol.capability import CapabilityResult, CapabilityStatus
11
+
12
+
13
+ class WorkflowRunStatus(str, Enum):
14
+ """workflow 宿主状态。"""
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 WorkflowStepSnapshot:
25
+ """
26
+ workflow 步骤摘要。
27
+
28
+ 参数:
29
+ - step_id:步骤 ID
30
+ - status:步骤状态
31
+ - capability_id:可选能力 ID
32
+ - error:可选错误信息
33
+ """
34
+
35
+ step_id: str
36
+ status: str
37
+ capability_id: str | None = None
38
+ error: str | None = None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class WorkflowRunSnapshot:
43
+ """
44
+ workflow 运行摘要。
45
+
46
+ 参数:
47
+ - run_id:运行 ID
48
+ - workflow_id:workflow ID
49
+ - workflow_instance_id:workflow 实例 ID
50
+ - status:宿主状态
51
+ - steps:步骤摘要列表
52
+ - current_step_id:当前步骤 ID
53
+ - waiting_approval_key:等待审批键
54
+ - events_path:证据链 events 定位符
55
+ """
56
+
57
+ run_id: str
58
+ workflow_id: str
59
+ workflow_instance_id: str
60
+ status: WorkflowRunStatus
61
+ steps: list[WorkflowStepSnapshot] = field(default_factory=list)
62
+ current_step_id: str | None = None
63
+ waiting_approval_key: str | None = None
64
+ events_path: str | None = None
65
+ host_runtime: dict[str, Any] | None = None
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class WorkflowReplayRequest:
70
+ """
71
+ workflow replay 请求。
72
+
73
+ 参数:
74
+ - workflow_id:workflow ID
75
+ - run_id:宿主指定的 replay run ID
76
+ - from_snapshot:可选上次运行快照
77
+ - current_input:可选当前输入
78
+ """
79
+
80
+ workflow_id: str
81
+ run_id: str
82
+ from_snapshot: WorkflowRunSnapshot | None = None
83
+ current_input: dict[str, Any] | None = None
84
+
85
+
86
+ def summarize_workflow_items(
87
+ *,
88
+ workflow_id: str,
89
+ items: list[Any],
90
+ terminal: CapabilityResult | None = None,
91
+ ) -> WorkflowRunSnapshot:
92
+ """
93
+ 从 workflow 轻量事件和 terminal result 收敛 WorkflowRunSnapshot。
94
+
95
+ 参数:
96
+ - workflow_id:workflow ID
97
+ - items:workflow 事件列表
98
+ - terminal:可选终态结果
99
+ """
100
+
101
+ run_id = ""
102
+ workflow_instance_id = ""
103
+ current_step_id: str | None = None
104
+ ordered_steps: list[str] = []
105
+ steps: dict[str, WorkflowStepSnapshot] = {}
106
+ final_status: WorkflowRunStatus = WorkflowRunStatus.RUNNING
107
+
108
+ for item in items:
109
+ if not isinstance(item, dict):
110
+ continue
111
+ typ = str(item.get("type") or "")
112
+ if not run_id:
113
+ run_id = str(item.get("run_id") or "")
114
+ if typ == "workflow.started":
115
+ workflow_instance_id = str(item.get("workflow_instance_id") or workflow_id)
116
+ continue
117
+ if typ == "workflow.step.started":
118
+ step_id = str(item.get("step_id") or "").strip()
119
+ if not step_id:
120
+ continue
121
+ current_step_id = step_id
122
+ if step_id not in ordered_steps:
123
+ ordered_steps.append(step_id)
124
+ steps[step_id] = WorkflowStepSnapshot(
125
+ step_id=step_id,
126
+ status="running",
127
+ capability_id=_optional_text(item.get("capability_id")),
128
+ )
129
+ continue
130
+ if typ == "workflow.step.finished":
131
+ step_id = str(item.get("step_id") or "").strip()
132
+ if not step_id:
133
+ continue
134
+ if step_id not in ordered_steps:
135
+ ordered_steps.append(step_id)
136
+ status = str(item.get("status") or "pending").strip() or "pending"
137
+ steps[step_id] = WorkflowStepSnapshot(
138
+ step_id=step_id,
139
+ status=status,
140
+ capability_id=_optional_text(item.get("capability_id")),
141
+ error=_optional_text(item.get("error")),
142
+ )
143
+ if status in {"running", "pending"}:
144
+ current_step_id = step_id
145
+ elif current_step_id == step_id:
146
+ current_step_id = None
147
+ continue
148
+ if typ == "workflow.finished":
149
+ final_status = _map_workflow_status_from_event(str(item.get("status") or "pending"))
150
+
151
+ waiting_approval_key = None
152
+ events_path = None
153
+ host_runtime: dict[str, Any] | None = None
154
+ if terminal is not None and terminal.node_report is not None:
155
+ host_runtime = project_host_runtime_data(terminal, capability_id=workflow_id)
156
+ if isinstance(host_runtime, dict):
157
+ final_status = WorkflowRunStatus.WAITING_HUMAN
158
+ waiting_approval_key = _optional_text(host_runtime.get("approval_key"))
159
+ host_step_id = _optional_text(host_runtime.get("step_id"))
160
+ if host_step_id:
161
+ current_step_id = host_step_id
162
+ if isinstance(terminal.node_report.events_path, str) and terminal.node_report.events_path:
163
+ events_path = terminal.node_report.events_path
164
+ if not run_id:
165
+ run_id = terminal.node_report.run_id
166
+
167
+ if terminal is not None and final_status == WorkflowRunStatus.RUNNING:
168
+ final_status = _map_workflow_status_from_terminal(terminal.status)
169
+
170
+ if not run_id and terminal is not None:
171
+ run_id = str(terminal.metadata.get("run_id") or "")
172
+ if not workflow_instance_id:
173
+ workflow_instance_id = workflow_id
174
+
175
+ return WorkflowRunSnapshot(
176
+ run_id=run_id,
177
+ workflow_id=workflow_id,
178
+ workflow_instance_id=workflow_instance_id,
179
+ status=final_status,
180
+ steps=[steps[step_id] for step_id in ordered_steps],
181
+ current_step_id=current_step_id,
182
+ waiting_approval_key=waiting_approval_key,
183
+ events_path=events_path,
184
+ host_runtime=host_runtime,
185
+ )
186
+
187
+
188
+ def _map_workflow_status_from_event(status: str) -> WorkflowRunStatus:
189
+ """把 workflow 事件状态归一为 WorkflowRunStatus。"""
190
+
191
+ normalized = str(status or "").strip()
192
+ if normalized == "success":
193
+ return WorkflowRunStatus.COMPLETED
194
+ if normalized == "failed":
195
+ return WorkflowRunStatus.FAILED
196
+ if normalized == "cancelled":
197
+ return WorkflowRunStatus.CANCELLED
198
+ return WorkflowRunStatus.RUNNING
199
+
200
+
201
+ def _map_workflow_status_from_terminal(status: CapabilityStatus) -> WorkflowRunStatus:
202
+ """把 terminal CapabilityStatus 映射为 WorkflowRunStatus。"""
203
+
204
+ if status == CapabilityStatus.SUCCESS:
205
+ return WorkflowRunStatus.COMPLETED
206
+ if status == CapabilityStatus.FAILED:
207
+ return WorkflowRunStatus.FAILED
208
+ if status == CapabilityStatus.CANCELLED:
209
+ return WorkflowRunStatus.CANCELLED
210
+ return WorkflowRunStatus.RUNNING
211
+
212
+
213
+ def _optional_text(value: Any) -> str | None:
214
+ """把可选值归一为非空字符串。"""
215
+
216
+ if isinstance(value, str) and value.strip():
217
+ return value.strip()
218
+ return None
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: capability-runtime
3
+ Version: 0.1.0
4
+ Summary: Bridge/glue layer that composes Agently (LLM/TriggerFlow) with skills-runtime-sdk (skills/tools/WAL/events).
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: PyYAML>=6
9
+ Requires-Dist: pydantic<3,>=2
10
+ Requires-Dist: agently==4.0.8
11
+ Requires-Dist: skills-runtime-sdk==0.1.11
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
15
+ Requires-Dist: mypy>=1.8; extra == "dev"
16
+
17
+ <div align="center">
18
+
19
+ [English](README.md) | [中文](README.zh-CN.md)
20
+
21
+ </div>
22
+
23
+ # capability-runtime
24
+
25
+ `capability-runtime` is a production-oriented runtime/adapter layer that exposes a
26
+ stable `Runtime` API while composing two upstream systems:
27
+
28
+ - `skills-runtime-sdk` for skills, tools, approvals, WAL, and event evidence
29
+ - `Agently` for OpenAI-compatible transport and TriggerFlow-based orchestration internals
30
+
31
+ The public contract of this repository is intentionally narrow:
32
+
33
+ - capability primitives: `AgentSpec` and `WorkflowSpec`
34
+ - execution entrypoint: `Runtime`
35
+ - evidence surface: `NodeReport`, host snapshots, and service-facade helpers
36
+
37
+ ## What You Get
38
+
39
+ - A single execution surface: `Runtime.run()` and `Runtime.run_stream()`
40
+ - Public capability registration and manifest descriptors
41
+ - Workflow orchestration on top of the runtime without exposing TriggerFlow as a public API
42
+ - Evidence-first results through `NodeReport`, tool-call reports, approval summaries, and WAL locators
43
+ - Host-facing helpers for wait/resume, approval tickets, continuity, and service streaming
44
+
45
+ ## Architecture At A Glance
46
+
47
+ ```text
48
+ +-----------------------------+
49
+ | Host Application |
50
+ | - register capabilities |
51
+ | - run / stream / continue |
52
+ +--------------+--------------+
53
+ |
54
+ v
55
+ +------------------------------------------------------------------------+
56
+ | capability-runtime |
57
+ | |
58
+ | Public contract |
59
+ | - AgentSpec / WorkflowSpec |
60
+ | - Runtime |
61
+ | - NodeReport / HostRunSnapshot / RuntimeServiceFacade |
62
+ | |
63
+ | Internal adapters |
64
+ | - AgentAdapter |
65
+ | - TriggerFlowWorkflowEngine |
66
+ | - service/session continuity bridge |
67
+ +------------------------------+-----------------------------------------+
68
+ |
69
+ v
70
+ +-------------------------------+
71
+ | skills-runtime-sdk |
72
+ | - skills + tools |
73
+ | - approvals + exec sessions |
74
+ | - WAL / AgentEvent evidence |
75
+ +---------------+---------------+
76
+ |
77
+ v
78
+ +-------------------------------+
79
+ | Agently / TriggerFlow |
80
+ | - OpenAI-compatible transport |
81
+ | - workflow execution internals|
82
+ +-------------------------------+
83
+ ```
84
+
85
+ ## Install
86
+
87
+ From source:
88
+
89
+ ```bash
90
+ python -m pip install -e .
91
+ ```
92
+
93
+ With development dependencies:
94
+
95
+ ```bash
96
+ python -m pip install -e ".[dev]"
97
+ ```
98
+
99
+ When the package is published, the install form is:
100
+
101
+ ```bash
102
+ python -m pip install capability-runtime
103
+ ```
104
+
105
+ Import name:
106
+
107
+ ```python
108
+ import capability_runtime
109
+ ```
110
+
111
+ ## Quickstart
112
+
113
+ ### 1. Offline runtime loop
114
+
115
+ ```bash
116
+ python examples/01_quickstart/run_mock.py
117
+ ```
118
+
119
+ This path is the smallest reproducible loop:
120
+
121
+ - register an `AgentSpec`
122
+ - validate the registry
123
+ - run in `mode="mock"`
124
+ - inspect the terminal `CapabilityResult`
125
+
126
+ ### 2. Bridge mode with a real model backend
127
+
128
+ ```bash
129
+ cp examples/01_quickstart/.env.example examples/01_quickstart/.env
130
+ python examples/01_quickstart/run_bridge.py
131
+ ```
132
+
133
+ Bridge mode reuses Agently's OpenAI-compatible transport but still delegates the
134
+ actual skills/tools/WAL semantics to `skills-runtime-sdk`.
135
+
136
+ ### 3. Workflow orchestration
137
+
138
+ ```bash
139
+ python examples/02_workflow/run.py
140
+ ```
141
+
142
+ For a higher-level index, start with [examples/README.md](examples/README.md).
143
+
144
+ ## Public API At A Glance
145
+
146
+ The package root exposes the supported contract:
147
+
148
+ ```python
149
+ from capability_runtime import (
150
+ Runtime,
151
+ RuntimeConfig,
152
+ CustomTool,
153
+ AgentSpec,
154
+ AgentIOSchema,
155
+ WorkflowSpec,
156
+ Step,
157
+ LoopStep,
158
+ ParallelStep,
159
+ ConditionalStep,
160
+ InputMapping,
161
+ CapabilitySpec,
162
+ CapabilityKind,
163
+ CapabilityResult,
164
+ CapabilityStatus,
165
+ NodeReport,
166
+ HostRunSnapshot,
167
+ ApprovalTicket,
168
+ ResumeIntent,
169
+ RuntimeServiceFacade,
170
+ RuntimeServiceRequest,
171
+ RuntimeServiceHandle,
172
+ RuntimeSession,
173
+ )
174
+ ```
175
+
176
+ The runtime currently supports three execution modes through `RuntimeConfig.mode`:
177
+
178
+ - `mock`: deterministic local testing without a real LLM backend
179
+ - `bridge`: Agently transport + `skills-runtime-sdk` execution semantics
180
+ - `sdk_native`: native `skills-runtime-sdk` backend without Agently transport
181
+
182
+ ## Repository Layout
183
+
184
+ ```text
185
+ .
186
+ ├── src/capability_runtime/ # package source
187
+ ├── examples/ # human-facing runnable examples
188
+ ├── docs_for_coding_agent/ # compact pack for coding agents
189
+ ├── help/ # public help and operational guides
190
+ ├── config/ # example config shapes
191
+ ├── scripts/ # release / validation helpers
192
+ └── tests/ # offline regression guardrails
193
+ ```
194
+
195
+ ## Documentation Map
196
+
197
+ - [help/README.md](help/README.md): public help index
198
+ - [examples/README.md](examples/README.md): runnable examples by scenario
199
+ - [docs_for_coding_agent/README.md](docs_for_coding_agent/README.md): compact coding-agent pack
200
+ - [config/README.md](config/README.md): config shape reference
201
+
202
+ Recommended reading order for new users:
203
+
204
+ 1. [help/00-overview.md](help/00-overview.md)
205
+ 2. [help/01-quickstart.md](help/01-quickstart.md)
206
+ 3. [help/03-python-api.md](help/03-python-api.md)
207
+ 4. [examples/README.md](examples/README.md)
208
+
209
+ ## Release And PyPI Publishing
210
+
211
+ This repository ships GitHub Actions workflows for:
212
+
213
+ - Tier-0 CI on push and pull request
214
+ - tag-driven and manual PyPI publishing
215
+
216
+ Release guardrails:
217
+
218
+ - the Git tag must match `pyproject.toml`'s `[project].version`
219
+ - the Git tag must match `capability_runtime.__version__`
220
+ - the publish job builds both sdist and wheel before uploading
221
+
222
+ The publish workflow is designed for PyPI Trusted Publishing. You still need to
223
+ configure the corresponding Trusted Publisher entry on `pypi.org`.
224
+
225
+ ## Relationship To The Upstreams
226
+
227
+ - `skills-runtime-sdk` remains the source of truth for skills, approvals, tools,
228
+ WAL, and event evidence.
229
+ - `Agently` remains the transport/orchestration substrate where this repository
230
+ chooses to bridge instead of forking or reimplementing.
231
+ - `capability-runtime` is the contract-convergence layer: it narrows those
232
+ upstream capabilities into a smaller host-facing runtime surface.