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,90 @@
|
|
|
1
|
+
"""capability-runtime:统一 Runtime 入口(能力协议 + 执行 + 报告)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
|
|
6
|
+
# === 统一入口 ===
|
|
7
|
+
from .config import CustomTool, RuntimeConfig
|
|
8
|
+
from .runtime import Runtime
|
|
9
|
+
from .service_facade import RuntimeServiceFacade, RuntimeServiceHandle, RuntimeServiceRequest, RuntimeSession
|
|
10
|
+
from .structured_stream import StructuredStreamEvent
|
|
11
|
+
|
|
12
|
+
# === 报告类型 ===
|
|
13
|
+
from .types import NodeReport, NodeResult
|
|
14
|
+
|
|
15
|
+
# === Host toolkit(精选公共导出)===
|
|
16
|
+
from .host_toolkit import InvokeCapabilityAllowlist, make_invoke_capability_tool
|
|
17
|
+
from .host_protocol import ApprovalTicket, HostRunSnapshot, HostRunStatus, ResumeIntent
|
|
18
|
+
from .manifest import CapabilityDescriptor, CapabilityManifestEntry, CapabilityVisibility
|
|
19
|
+
|
|
20
|
+
# === Protocol 导出 ===
|
|
21
|
+
from .protocol.agent import AgentIOSchema, AgentSpec
|
|
22
|
+
from .protocol.capability import (
|
|
23
|
+
CapabilityKind,
|
|
24
|
+
CapabilityRef,
|
|
25
|
+
CapabilityResult,
|
|
26
|
+
CapabilitySpec,
|
|
27
|
+
CapabilityStatus,
|
|
28
|
+
)
|
|
29
|
+
from .protocol.context import ExecutionContext
|
|
30
|
+
from .protocol.workflow import (
|
|
31
|
+
ConditionalStep,
|
|
32
|
+
InputMapping,
|
|
33
|
+
LoopStep,
|
|
34
|
+
ParallelStep,
|
|
35
|
+
Step,
|
|
36
|
+
WorkflowSpec,
|
|
37
|
+
)
|
|
38
|
+
from .services import RuntimeServices
|
|
39
|
+
from .workflow_runtime import WorkflowReplayRequest, WorkflowRunSnapshot, WorkflowRunStatus, WorkflowStepSnapshot
|
|
40
|
+
|
|
41
|
+
# === 错误导出 ===
|
|
42
|
+
from .errors import CapabilityNotFoundError, RuntimeFrameworkError
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Runtime
|
|
46
|
+
"Runtime",
|
|
47
|
+
"RuntimeConfig",
|
|
48
|
+
"CustomTool",
|
|
49
|
+
"StructuredStreamEvent",
|
|
50
|
+
# Reports
|
|
51
|
+
"NodeReport",
|
|
52
|
+
"NodeResult",
|
|
53
|
+
# Host toolkit (selected)
|
|
54
|
+
"InvokeCapabilityAllowlist",
|
|
55
|
+
"make_invoke_capability_tool",
|
|
56
|
+
# Protocol
|
|
57
|
+
"CapabilityKind",
|
|
58
|
+
"CapabilityRef",
|
|
59
|
+
"CapabilitySpec",
|
|
60
|
+
"CapabilityStatus",
|
|
61
|
+
"CapabilityResult",
|
|
62
|
+
"CapabilityManifestEntry",
|
|
63
|
+
"CapabilityDescriptor",
|
|
64
|
+
"CapabilityVisibility",
|
|
65
|
+
"HostRunStatus",
|
|
66
|
+
"ApprovalTicket",
|
|
67
|
+
"ResumeIntent",
|
|
68
|
+
"HostRunSnapshot",
|
|
69
|
+
"WorkflowRunStatus",
|
|
70
|
+
"WorkflowStepSnapshot",
|
|
71
|
+
"WorkflowRunSnapshot",
|
|
72
|
+
"WorkflowReplayRequest",
|
|
73
|
+
"RuntimeSession",
|
|
74
|
+
"RuntimeServiceRequest",
|
|
75
|
+
"RuntimeServiceHandle",
|
|
76
|
+
"RuntimeServiceFacade",
|
|
77
|
+
"AgentSpec",
|
|
78
|
+
"AgentIOSchema",
|
|
79
|
+
"WorkflowSpec",
|
|
80
|
+
"Step",
|
|
81
|
+
"LoopStep",
|
|
82
|
+
"ParallelStep",
|
|
83
|
+
"ConditionalStep",
|
|
84
|
+
"InputMapping",
|
|
85
|
+
"ExecutionContext",
|
|
86
|
+
"RuntimeServices",
|
|
87
|
+
# Errors
|
|
88
|
+
"RuntimeFrameworkError",
|
|
89
|
+
"CapabilityNotFoundError",
|
|
90
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Adapters:桥接上游与能力组织层。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
# 已有桥接适配器(不修改)
|
|
5
|
+
# from .agently_backend import AgentlyChatBackend
|
|
6
|
+
# from .triggerflow_tool import ...
|
|
7
|
+
|
|
8
|
+
# 新增能力适配器
|
|
9
|
+
from .agent_adapter import AgentAdapter
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AgentAdapter",
|
|
13
|
+
]
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentAdapter:AgentSpec 的执行适配器(统一 mock/bridge/sdk_native)。
|
|
3
|
+
|
|
4
|
+
说明:
|
|
5
|
+
- 本仓不实现 skills 引擎;skills 的注入与执行由上游 `skills_runtime` 完成;
|
|
6
|
+
- 本适配器负责把 AgentSpec + input 翻译成 SDK Agent 的 task 文本,并驱动事件流执行;
|
|
7
|
+
- `Runtime.run_stream()` 的事件转发语义依赖本适配器的流式执行能力。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import inspect
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Union
|
|
16
|
+
|
|
17
|
+
from skills_runtime.core.contracts import AgentEvent
|
|
18
|
+
from skills_runtime.core.errors import FrameworkIssue
|
|
19
|
+
|
|
20
|
+
from ..logging_utils import log_suppressed_exception
|
|
21
|
+
from ..protocol.agent import AgentSpec
|
|
22
|
+
from ..protocol.capability import CapabilityResult, CapabilityStatus
|
|
23
|
+
from ..protocol.context import ExecutionContext
|
|
24
|
+
from ..reporting.node_report import NodeReportBuilder
|
|
25
|
+
from ..services import RuntimeServices, map_node_status
|
|
26
|
+
|
|
27
|
+
# _build_task 使用的 prompt section 常量
|
|
28
|
+
_SECTION_SYSTEM = "## 系统指令"
|
|
29
|
+
_SECTION_TASK = "## 任务"
|
|
30
|
+
_SECTION_INPUT = "## 输入"
|
|
31
|
+
_SECTION_OUTPUT_PREFIX = "## 输出要求"
|
|
32
|
+
_SECTION_SKILLS = "## 使用以下 Skills"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AgentAdapter:
|
|
36
|
+
"""
|
|
37
|
+
Agent 适配器(Runtime 内部组件)。
|
|
38
|
+
|
|
39
|
+
参数:
|
|
40
|
+
- services:RuntimeServices(供 adapter 调用的内部服务协议)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, *, services: RuntimeServices) -> None:
|
|
44
|
+
self._services = services
|
|
45
|
+
|
|
46
|
+
async def execute_stream(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
spec: AgentSpec,
|
|
50
|
+
input: Dict[str, Any],
|
|
51
|
+
context: ExecutionContext,
|
|
52
|
+
) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
|
|
53
|
+
"""
|
|
54
|
+
流式执行 AgentSpec:先 yield AgentEvent(若为真实执行),最后 yield CapabilityResult。
|
|
55
|
+
|
|
56
|
+
mock 模式:
|
|
57
|
+
- 不产出中间事件(只产出最终 CapabilityResult)
|
|
58
|
+
|
|
59
|
+
bridge/sdk_native 模式:
|
|
60
|
+
- 转发上游 `skills_runtime` 的 `AgentEvent`(事实事件流);
|
|
61
|
+
- 基于事件流聚合 `NodeReport` 并写入 `CapabilityResult.node_report`(其中 `events_path` 等字段为证据指针,真相源仍为 WAL/events)。
|
|
62
|
+
|
|
63
|
+
备注:
|
|
64
|
+
- 每条 `AgentEvent` 也会通过 Runtime 的内部 tap 旁路分发(用于 UI events 投影;不改变对外 `AgentEvent` 转发语义)。
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
mode = str(getattr(self._services.config, "mode", "mock"))
|
|
68
|
+
if mode == "mock":
|
|
69
|
+
yield await self._mock_execute(spec=spec, input=input, context=context)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
async for item in self._bridge_execute_stream(spec=spec, input=input, context=context):
|
|
73
|
+
yield item
|
|
74
|
+
|
|
75
|
+
async def _mock_execute(self, *, spec: AgentSpec, input: Dict[str, Any], context: ExecutionContext) -> CapabilityResult:
|
|
76
|
+
"""
|
|
77
|
+
mock 执行(离线回归)。
|
|
78
|
+
|
|
79
|
+
约束:
|
|
80
|
+
- handler 可返回 Any 或 CapabilityResult;
|
|
81
|
+
- handler 支持同步或 async;
|
|
82
|
+
- 异常将转为 FAILED(避免 silent success)。
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
handler = getattr(self._services.config, "mock_handler", None)
|
|
86
|
+
if handler is None:
|
|
87
|
+
return CapabilityResult(
|
|
88
|
+
status=CapabilityStatus.SUCCESS,
|
|
89
|
+
output={"mock": True, "id": spec.base.id, "input_keys": list(input.keys())},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# 用 inspect.signature 探测参数数量,避免 try/except TypeError 吞掉 handler 内部的 TypeError
|
|
94
|
+
sig = inspect.signature(handler)
|
|
95
|
+
params = sig.parameters
|
|
96
|
+
has_var_positional = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params.values())
|
|
97
|
+
positional_params = [
|
|
98
|
+
p for p in params.values()
|
|
99
|
+
if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
# 决定传入参数数量:
|
|
103
|
+
# - VAR_POSITIONAL:可接受任意数量,传入全部 3 个
|
|
104
|
+
# - 否则:按位置参数数量切片
|
|
105
|
+
all_args = (spec, input, context)
|
|
106
|
+
if has_var_positional:
|
|
107
|
+
call_args = all_args
|
|
108
|
+
elif len(positional_params) >= 3:
|
|
109
|
+
call_args = all_args
|
|
110
|
+
elif len(positional_params) == 2:
|
|
111
|
+
call_args = (spec, input)
|
|
112
|
+
elif len(positional_params) == 1:
|
|
113
|
+
call_args = (spec,)
|
|
114
|
+
else:
|
|
115
|
+
call_args = ()
|
|
116
|
+
|
|
117
|
+
is_async_handler = inspect.iscoroutinefunction(handler) or inspect.iscoroutinefunction(
|
|
118
|
+
getattr(handler, "__call__", None)
|
|
119
|
+
)
|
|
120
|
+
if is_async_handler:
|
|
121
|
+
out = handler(*call_args)
|
|
122
|
+
else:
|
|
123
|
+
# 同步 mock_handler 放到 worker thread 执行,避免阻塞当前事件循环,
|
|
124
|
+
# 使 workflow step/loop timeout 等上层护栏仍然有效。
|
|
125
|
+
out = await asyncio.to_thread(handler, *call_args)
|
|
126
|
+
|
|
127
|
+
if inspect.isawaitable(out):
|
|
128
|
+
out = await out
|
|
129
|
+
|
|
130
|
+
if isinstance(out, CapabilityResult):
|
|
131
|
+
return out
|
|
132
|
+
return CapabilityResult(status=CapabilityStatus.SUCCESS, output=out)
|
|
133
|
+
except asyncio.CancelledError:
|
|
134
|
+
report = self._services.build_fail_closed_report(
|
|
135
|
+
run_id=context.run_id,
|
|
136
|
+
status="incomplete",
|
|
137
|
+
reason="cancelled",
|
|
138
|
+
completion_reason="run_cancelled",
|
|
139
|
+
meta={"capability_id": spec.base.id, "source": "mock_handler"},
|
|
140
|
+
)
|
|
141
|
+
return CapabilityResult(
|
|
142
|
+
status=CapabilityStatus.CANCELLED,
|
|
143
|
+
error="execution cancelled",
|
|
144
|
+
error_code="RUN_CANCELLED",
|
|
145
|
+
report=report,
|
|
146
|
+
node_report=report,
|
|
147
|
+
)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
report = self._services.build_fail_closed_report(
|
|
150
|
+
run_id=context.run_id,
|
|
151
|
+
status="failed",
|
|
152
|
+
reason="mock_handler_error",
|
|
153
|
+
completion_reason="mock_handler_error",
|
|
154
|
+
meta={"capability_id": spec.base.id, "exception_type": type(exc).__name__},
|
|
155
|
+
)
|
|
156
|
+
return CapabilityResult(
|
|
157
|
+
status=CapabilityStatus.FAILED,
|
|
158
|
+
error=f"mock_handler error: {exc}",
|
|
159
|
+
error_code="MOCK_HANDLER_ERROR",
|
|
160
|
+
report=report,
|
|
161
|
+
node_report=report,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def _bridge_execute_stream(
|
|
165
|
+
self, *, spec: AgentSpec, input: Dict[str, Any], context: ExecutionContext
|
|
166
|
+
) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
|
|
167
|
+
"""
|
|
168
|
+
真实执行(bridge/sdk_native):驱动 SDK Agent.run_stream_async 并聚合 NodeReport。
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
# preflight gate(生产默认 fail-closed)
|
|
172
|
+
issues: List[FrameworkIssue] = []
|
|
173
|
+
if getattr(self._services.config, "preflight_mode", "error") != "off":
|
|
174
|
+
issues = self._services.preflight()
|
|
175
|
+
if issues and getattr(self._services.config, "preflight_mode", "error") == "error":
|
|
176
|
+
report = self._services.build_fail_closed_report(
|
|
177
|
+
run_id=context.run_id,
|
|
178
|
+
status="failed",
|
|
179
|
+
reason="skill_config_error",
|
|
180
|
+
completion_reason="preflight_failed",
|
|
181
|
+
meta={
|
|
182
|
+
"preflight_mode": "error",
|
|
183
|
+
"skill_issue": {
|
|
184
|
+
"code": "SKILL_PREFLIGHT_FAILED",
|
|
185
|
+
"details": {"issues": [self._services.redact_issue(i) for i in issues]},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
yield CapabilityResult(
|
|
190
|
+
status=CapabilityStatus.FAILED,
|
|
191
|
+
error="Skills preflight failed",
|
|
192
|
+
error_code="PREFLIGHT_FAILED",
|
|
193
|
+
report=report,
|
|
194
|
+
node_report=report,
|
|
195
|
+
metadata={"skill_issues": [self._services.redact_issue(i) for i in issues]},
|
|
196
|
+
)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
task = self._build_task(spec=spec, input=input)
|
|
200
|
+
agent = self._services.create_sdk_agent(llm_config=spec.llm_config)
|
|
201
|
+
|
|
202
|
+
events: List[AgentEvent] = []
|
|
203
|
+
host_meta = self._services.get_host_meta(context=context)
|
|
204
|
+
initial_history = host_meta.get("initial_history") if isinstance(host_meta.get("initial_history"), list) else None
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
async for ev in agent.run_stream_async(task, run_id=context.run_id, initial_history=initial_history):
|
|
208
|
+
events.append(ev)
|
|
209
|
+
# 内部旁路:UI events v1 投影(不改变对外 AgentEvent 语义)
|
|
210
|
+
try:
|
|
211
|
+
self._services.emit_agent_event_taps(ev=ev, context=context, capability_id=spec.base.id)
|
|
212
|
+
except Exception as tap_exc:
|
|
213
|
+
log_suppressed_exception(
|
|
214
|
+
context="emit_agent_event_taps",
|
|
215
|
+
exc=tap_exc,
|
|
216
|
+
run_id=context.run_id,
|
|
217
|
+
capability_id=spec.base.id,
|
|
218
|
+
extra={"event_type": getattr(ev, "type", None)},
|
|
219
|
+
)
|
|
220
|
+
if getattr(self._services.config, "on_event", None) is not None:
|
|
221
|
+
try:
|
|
222
|
+
self._services.call_callback(
|
|
223
|
+
self._services.config.on_event,
|
|
224
|
+
ev,
|
|
225
|
+
{"run_id": context.run_id, "capability_id": spec.base.id},
|
|
226
|
+
)
|
|
227
|
+
except Exception as cb_exc:
|
|
228
|
+
log_suppressed_exception(
|
|
229
|
+
context="on_event_callback",
|
|
230
|
+
exc=cb_exc,
|
|
231
|
+
run_id=context.run_id,
|
|
232
|
+
capability_id=spec.base.id,
|
|
233
|
+
extra={"event_type": getattr(ev, "type", None)},
|
|
234
|
+
)
|
|
235
|
+
yield ev
|
|
236
|
+
except asyncio.CancelledError:
|
|
237
|
+
report = self._services.build_fail_closed_report(
|
|
238
|
+
run_id=context.run_id,
|
|
239
|
+
status="incomplete",
|
|
240
|
+
reason="cancelled",
|
|
241
|
+
completion_reason="run_cancelled",
|
|
242
|
+
meta={"capability_id": spec.base.id, "source": "sdk_agent"},
|
|
243
|
+
)
|
|
244
|
+
yield CapabilityResult(
|
|
245
|
+
status=CapabilityStatus.CANCELLED,
|
|
246
|
+
error="execution cancelled",
|
|
247
|
+
error_code="RUN_CANCELLED",
|
|
248
|
+
report=report,
|
|
249
|
+
node_report=report,
|
|
250
|
+
)
|
|
251
|
+
return
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
report = self._services.build_fail_closed_report(
|
|
254
|
+
run_id=context.run_id,
|
|
255
|
+
status="failed",
|
|
256
|
+
reason="engine_error",
|
|
257
|
+
completion_reason="engine_exception",
|
|
258
|
+
meta={"engine_exception": type(exc).__name__},
|
|
259
|
+
)
|
|
260
|
+
yield CapabilityResult(
|
|
261
|
+
status=CapabilityStatus.FAILED,
|
|
262
|
+
error=str(exc),
|
|
263
|
+
error_code="ENGINE_ERROR",
|
|
264
|
+
report=report,
|
|
265
|
+
node_report=report,
|
|
266
|
+
)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
report = NodeReportBuilder().build(events=events)
|
|
270
|
+
if issues and getattr(self._services.config, "preflight_mode", "error") == "warn":
|
|
271
|
+
report.meta["preflight_mode"] = "warn"
|
|
272
|
+
report.meta["preflight_issues"] = [self._services.redact_issue(i) for i in issues]
|
|
273
|
+
|
|
274
|
+
if initial_history is not None:
|
|
275
|
+
report.meta["initial_history_injected"] = True
|
|
276
|
+
session_id = host_meta.get("session_id")
|
|
277
|
+
if isinstance(session_id, str) and session_id:
|
|
278
|
+
report.meta["session_id"] = session_id
|
|
279
|
+
host_turn_id = host_meta.get("host_turn_id")
|
|
280
|
+
if isinstance(host_turn_id, str) and host_turn_id:
|
|
281
|
+
report.meta["host_turn_id"] = host_turn_id
|
|
282
|
+
|
|
283
|
+
final_output = ""
|
|
284
|
+
for ev in events:
|
|
285
|
+
if ev.type == "run_completed":
|
|
286
|
+
final_output = str(ev.payload.get("final_output") or "")
|
|
287
|
+
if ev.type in ("run_failed", "run_cancelled"):
|
|
288
|
+
final_output = str(ev.payload.get("message") or "")
|
|
289
|
+
if ev.type == "run_waiting_human":
|
|
290
|
+
final_output = str(ev.payload.get("message") or "")
|
|
291
|
+
|
|
292
|
+
self._services.apply_output_validation(
|
|
293
|
+
final_output=final_output,
|
|
294
|
+
report=report,
|
|
295
|
+
context={"run_id": context.run_id, "capability_id": spec.base.id, "bag": dict(context.bag)},
|
|
296
|
+
output_schema=spec.output_schema,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
status = map_node_status(report)
|
|
300
|
+
yield CapabilityResult(
|
|
301
|
+
status=status,
|
|
302
|
+
output=final_output,
|
|
303
|
+
error=report.reason if status == CapabilityStatus.FAILED else None,
|
|
304
|
+
report=report,
|
|
305
|
+
node_report=report,
|
|
306
|
+
artifacts=list(report.artifacts),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def _build_task(self, *, spec: AgentSpec, input: Dict[str, Any]) -> str:
|
|
310
|
+
"""
|
|
311
|
+
将 AgentSpec + input 转换为 SDK Agent 的 task 文本(结构化拼接)。
|
|
312
|
+
|
|
313
|
+
约束:
|
|
314
|
+
- 不做 prompt engineering;
|
|
315
|
+
- 仅做结构化拼接,保证可回归与可诊断。
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
parts: List[str] = []
|
|
319
|
+
|
|
320
|
+
if spec.system_prompt and str(spec.system_prompt).strip():
|
|
321
|
+
parts.append(f"{_SECTION_SYSTEM}\n{str(spec.system_prompt).strip()}")
|
|
322
|
+
|
|
323
|
+
if spec.base.description:
|
|
324
|
+
parts.append(f"{_SECTION_TASK}\n{spec.base.description}")
|
|
325
|
+
|
|
326
|
+
if input:
|
|
327
|
+
lines: List[str] = []
|
|
328
|
+
for k, v in input.items():
|
|
329
|
+
if isinstance(v, str):
|
|
330
|
+
lines.append(f"- {k}: {v}")
|
|
331
|
+
else:
|
|
332
|
+
lines.append(f"- {k}: {json.dumps(v, ensure_ascii=False)}")
|
|
333
|
+
parts.append(f"{_SECTION_INPUT}\n" + "\n".join(lines))
|
|
334
|
+
|
|
335
|
+
if spec.output_schema and spec.output_schema.fields:
|
|
336
|
+
parts.append(self._build_output_contract(spec=spec))
|
|
337
|
+
|
|
338
|
+
# skills mention(可选)
|
|
339
|
+
mentions = self._build_skill_mentions(spec=spec)
|
|
340
|
+
if mentions:
|
|
341
|
+
parts.append(f"{_SECTION_SKILLS}\n" + "\n".join(mentions))
|
|
342
|
+
|
|
343
|
+
if spec.prompt_template:
|
|
344
|
+
parts.append(str(spec.prompt_template))
|
|
345
|
+
|
|
346
|
+
return "\n\n".join(parts)
|
|
347
|
+
|
|
348
|
+
def _build_output_contract(self, *, spec: AgentSpec) -> str:
|
|
349
|
+
"""
|
|
350
|
+
构造结构化输出约束段。
|
|
351
|
+
|
|
352
|
+
说明:
|
|
353
|
+
- 这里仍属于提示层约束,但要明确“只返回 JSON object”;
|
|
354
|
+
- 真正的收口由 Runtime 的结构化输出校验负责。
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
if spec.output_schema is None:
|
|
358
|
+
return ""
|
|
359
|
+
|
|
360
|
+
lines = [
|
|
361
|
+
_SECTION_OUTPUT_PREFIX,
|
|
362
|
+
"请只返回 JSON object,不要 Markdown code fence,不要解释性前后缀。",
|
|
363
|
+
"字段要求:",
|
|
364
|
+
]
|
|
365
|
+
lines.extend([f"- {name}: {typ}" for name, typ in spec.output_schema.fields.items()])
|
|
366
|
+
|
|
367
|
+
required = [str(name) for name in list(spec.output_schema.required or []) if str(name)]
|
|
368
|
+
if required:
|
|
369
|
+
lines.append("必填字段:")
|
|
370
|
+
lines.extend([f"- {name}" for name in required])
|
|
371
|
+
|
|
372
|
+
return "\n".join(lines)
|
|
373
|
+
|
|
374
|
+
def _build_skill_mentions(self, *, spec: AgentSpec) -> List[str]:
|
|
375
|
+
"""
|
|
376
|
+
将 spec.skills 转为 SDK 识别的 mention 文本。
|
|
377
|
+
|
|
378
|
+
优先级:
|
|
379
|
+
1) skills_mention_map 显式映射(最稳定)
|
|
380
|
+
2) 从 Runtime bridge 初始化的 skills_config 推断默认 space(尽力而为)
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
skills = list(getattr(spec, "skills", []) or [])
|
|
384
|
+
if not skills:
|
|
385
|
+
return []
|
|
386
|
+
|
|
387
|
+
mention_map: Dict[str, str] = dict(getattr(spec, "skills_mention_map", {}) or {})
|
|
388
|
+
|
|
389
|
+
inferred_prefix: Optional[str] = None
|
|
390
|
+
skills_cfg = getattr(self._services.config, "skills_config", None)
|
|
391
|
+
inferred_prefix = self._infer_space_prefix(skills_cfg)
|
|
392
|
+
|
|
393
|
+
out: List[str] = []
|
|
394
|
+
for name in skills:
|
|
395
|
+
if name in mention_map and str(mention_map[name]).strip():
|
|
396
|
+
out.append(str(mention_map[name]).strip())
|
|
397
|
+
continue
|
|
398
|
+
if inferred_prefix:
|
|
399
|
+
out.append(f"${inferred_prefix}.{name}")
|
|
400
|
+
return out
|
|
401
|
+
|
|
402
|
+
def _infer_space_prefix(self, skills_cfg: Any) -> Optional[str]:
|
|
403
|
+
"""
|
|
404
|
+
从 skills_config 推断 strict mention 的 space 前缀(版本感知)。
|
|
405
|
+
|
|
406
|
+
说明:
|
|
407
|
+
- 该逻辑是 best-effort:skills_config 的具体形态由上游决定(dict / pydantic model / dataclass)。
|
|
408
|
+
- 无法推断时返回 None(调用方将不输出 mention)。
|
|
409
|
+
|
|
410
|
+
返回值形态:
|
|
411
|
+
- legacy:`[account:domain]`
|
|
412
|
+
- v0.1.5+:`[namespace]`(namespace 允许 `a:b:c` 多段)
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
spaces = None
|
|
416
|
+
if isinstance(skills_cfg, dict):
|
|
417
|
+
spaces = skills_cfg.get("spaces")
|
|
418
|
+
else:
|
|
419
|
+
spaces = getattr(skills_cfg, "spaces", None)
|
|
420
|
+
|
|
421
|
+
if not isinstance(spaces, list):
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
def _get(obj: Any, key: str) -> Optional[str]:
|
|
425
|
+
if isinstance(obj, dict):
|
|
426
|
+
v = obj.get(key)
|
|
427
|
+
else:
|
|
428
|
+
v = getattr(obj, key, None)
|
|
429
|
+
return str(v).strip() if isinstance(v, str) and str(v).strip() else None
|
|
430
|
+
|
|
431
|
+
for sp in spaces:
|
|
432
|
+
namespace = _get(sp, "namespace")
|
|
433
|
+
if namespace:
|
|
434
|
+
return f"[{namespace}]"
|
|
435
|
+
account = _get(sp, "account")
|
|
436
|
+
domain = _get(sp, "domain")
|
|
437
|
+
if account and domain:
|
|
438
|
+
return f"[{account}:{domain}]"
|
|
439
|
+
return None
|