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,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.
|