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,409 @@
|
|
|
1
|
+
"""
|
|
2
|
+
invoke_capability:宿主侧“能力委托工具”(CustomTool)公共 API。
|
|
3
|
+
|
|
4
|
+
定位:
|
|
5
|
+
- 保持协议层二元:对外可寻址的能力仍只有 Agent/Workflow(capability_id)。
|
|
6
|
+
- 把“委托子能力”的发生过程纳入 tool evidence(WAL + NodeReport.tool_calls)。
|
|
7
|
+
- 遵守上游现实约束:tool handler 为同步函数;当子能力为 async API 时,使用“后台线程 + 常驻 event loop runner”执行。
|
|
8
|
+
|
|
9
|
+
对齐规格(delta):
|
|
10
|
+
- `openspec/specs/host-lifecycle-toolkit/spec.md`
|
|
11
|
+
- `openspec/specs/examples-coding-agent-pack/spec.md`
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import atexit
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
import threading
|
|
22
|
+
import uuid
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
28
|
+
from skills_runtime.tools.protocol import ToolCall, ToolResult, ToolSpec
|
|
29
|
+
from skills_runtime.tools.registry import ToolExecutionContext
|
|
30
|
+
|
|
31
|
+
from ..config import CustomTool, RuntimeConfig
|
|
32
|
+
from ..protocol.context import ExecutionContext
|
|
33
|
+
from ..registry import AnySpec
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
36
|
+
from ..runtime import Runtime
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_ARTIFACT_SCHEMA_ID = "capability-runtime.invoke_capability.v1"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InvokeCapabilityArgs(BaseModel):
|
|
43
|
+
"""
|
|
44
|
+
invoke_capability 的 tool args(最小集合)。
|
|
45
|
+
|
|
46
|
+
字段:
|
|
47
|
+
- capability_id:目标能力 ID(Agent/Workflow)
|
|
48
|
+
- input:输入参数(JSON object;敏感/大 payload 建议以 artifact 指针方式传递)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(extra="forbid")
|
|
52
|
+
|
|
53
|
+
capability_id: str = Field(min_length=1)
|
|
54
|
+
input: Dict[str, Any] = Field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class InvokeCapabilityAllowlist:
|
|
59
|
+
"""
|
|
60
|
+
invoke_capability 的能力白名单(fail-closed 由调用方选择是否启用)。
|
|
61
|
+
|
|
62
|
+
约束:
|
|
63
|
+
- 若同时提供 allowed_ids 与 allowed_prefixes,则任一命中即可允许。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
allowed_ids: Sequence[str] = ()
|
|
67
|
+
allowed_prefixes: Sequence[str] = ()
|
|
68
|
+
|
|
69
|
+
def is_allowed(self, capability_id: str) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
判断 capability_id 是否在允许范围内。
|
|
72
|
+
|
|
73
|
+
参数:
|
|
74
|
+
- capability_id:能力 ID
|
|
75
|
+
|
|
76
|
+
返回:
|
|
77
|
+
- True 表示允许;False 表示拒绝
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
cid = str(capability_id or "").strip()
|
|
81
|
+
if not cid:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
for x in self.allowed_ids:
|
|
85
|
+
if str(x).strip() == cid:
|
|
86
|
+
return True
|
|
87
|
+
for p in self.allowed_prefixes:
|
|
88
|
+
pp = str(p).strip()
|
|
89
|
+
if pp and cid.startswith(pp):
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _sha256_bytes(data: bytes) -> str:
|
|
95
|
+
"""计算 bytes 的 sha256 hex。"""
|
|
96
|
+
|
|
97
|
+
return hashlib.sha256(data).hexdigest()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _write_json_artifact(*, path: Path, obj: Dict[str, Any]) -> tuple[str, int]:
|
|
101
|
+
"""
|
|
102
|
+
将 JSON artifact 写入 path,并返回(sha256, bytes)。
|
|
103
|
+
|
|
104
|
+
参数:
|
|
105
|
+
- path:artifact 绝对路径(必须位于 workspace_root 下)
|
|
106
|
+
- obj:可 JSON 序列化的 dict
|
|
107
|
+
|
|
108
|
+
返回:
|
|
109
|
+
- (sha256_hex, bytes_count)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
raw = (json.dumps(obj, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
114
|
+
path.write_bytes(raw)
|
|
115
|
+
return _sha256_bytes(raw), len(raw)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class _AsyncRunner:
|
|
119
|
+
"""
|
|
120
|
+
后台 event loop runner(daemon thread)。
|
|
121
|
+
|
|
122
|
+
背景:
|
|
123
|
+
- 上游 tool handler 为同步函数;
|
|
124
|
+
- 但 Runtime.run(...) 为 async API;
|
|
125
|
+
- 因此需要一个后台 event loop 来承载 async 子调用,并用 `asyncio.run_coroutine_threadsafe`
|
|
126
|
+
在同步 handler 内“阻塞等待结果”(不会死锁主 Agent Loop)。
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
self._thread: threading.Thread | None = None
|
|
131
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
132
|
+
self._ready = threading.Event()
|
|
133
|
+
self._state_lock = threading.Lock()
|
|
134
|
+
|
|
135
|
+
def _thread_main(self) -> None:
|
|
136
|
+
loop = asyncio.new_event_loop()
|
|
137
|
+
asyncio.set_event_loop(loop)
|
|
138
|
+
with self._state_lock:
|
|
139
|
+
self._loop = loop
|
|
140
|
+
self._ready.set()
|
|
141
|
+
try:
|
|
142
|
+
loop.run_forever()
|
|
143
|
+
finally:
|
|
144
|
+
pending = [task for task in asyncio.all_tasks(loop) if not task.done()]
|
|
145
|
+
for task in pending:
|
|
146
|
+
task.cancel()
|
|
147
|
+
if pending:
|
|
148
|
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
149
|
+
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
150
|
+
shutdown_default_executor = getattr(loop, "shutdown_default_executor", None)
|
|
151
|
+
if callable(shutdown_default_executor):
|
|
152
|
+
loop.run_until_complete(shutdown_default_executor())
|
|
153
|
+
loop.close()
|
|
154
|
+
with self._state_lock:
|
|
155
|
+
if self._loop is loop:
|
|
156
|
+
self._loop = None
|
|
157
|
+
if self._thread is threading.current_thread():
|
|
158
|
+
self._thread = None
|
|
159
|
+
self._ready.clear()
|
|
160
|
+
|
|
161
|
+
def ensure_started(self) -> None:
|
|
162
|
+
with self._state_lock:
|
|
163
|
+
loop = self._loop
|
|
164
|
+
thread = self._thread
|
|
165
|
+
if loop is not None and thread is not None and thread.is_alive() and not loop.is_closed():
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
if thread is not None and thread.is_alive():
|
|
169
|
+
ready_event = self._ready
|
|
170
|
+
else:
|
|
171
|
+
self._ready.clear()
|
|
172
|
+
self._loop = None
|
|
173
|
+
thread = threading.Thread(target=self._thread_main, name="caprt-invoke-capability-runner", daemon=True)
|
|
174
|
+
self._thread = thread
|
|
175
|
+
thread.start()
|
|
176
|
+
ready_event = self._ready
|
|
177
|
+
|
|
178
|
+
ready = ready_event.wait(timeout=5.0)
|
|
179
|
+
with self._state_lock:
|
|
180
|
+
loop = self._loop
|
|
181
|
+
if (not ready) or loop is None or loop.is_closed():
|
|
182
|
+
if self._thread is thread:
|
|
183
|
+
self._thread = None
|
|
184
|
+
self._loop = None
|
|
185
|
+
self._ready.clear()
|
|
186
|
+
raise RuntimeError("invoke_capability runner loop failed to start")
|
|
187
|
+
|
|
188
|
+
def shutdown(self, *, timeout_s: float = 5.0) -> None:
|
|
189
|
+
"""停止后台 loop 并重置状态;供测试与进程退出清理复用。"""
|
|
190
|
+
|
|
191
|
+
with self._state_lock:
|
|
192
|
+
loop = self._loop
|
|
193
|
+
thread = self._thread
|
|
194
|
+
|
|
195
|
+
if loop is not None and not loop.is_closed():
|
|
196
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
197
|
+
|
|
198
|
+
if thread is not None and thread.is_alive():
|
|
199
|
+
thread.join(timeout=timeout_s)
|
|
200
|
+
|
|
201
|
+
with self._state_lock:
|
|
202
|
+
live_thread = self._thread
|
|
203
|
+
if live_thread is None or not live_thread.is_alive():
|
|
204
|
+
self._thread = None
|
|
205
|
+
self._loop = None
|
|
206
|
+
self._ready.clear()
|
|
207
|
+
|
|
208
|
+
def run(self, coro: Any, *, timeout_s: float | None) -> Any:
|
|
209
|
+
self.ensure_started()
|
|
210
|
+
with self._state_lock:
|
|
211
|
+
loop = self._loop
|
|
212
|
+
if loop is None or loop.is_closed():
|
|
213
|
+
raise RuntimeError("invoke_capability runner loop is unavailable")
|
|
214
|
+
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
215
|
+
try:
|
|
216
|
+
return fut.result(timeout=timeout_s)
|
|
217
|
+
except TimeoutError:
|
|
218
|
+
# timeout 必须尝试取消后台 child run,避免父调用已超时但子调用继续执行。
|
|
219
|
+
fut.cancel()
|
|
220
|
+
raise
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
_INVOKE_CAPABILITY_RUNNER = _AsyncRunner()
|
|
224
|
+
atexit.register(_INVOKE_CAPABILITY_RUNNER.shutdown)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def make_invoke_capability_tool(
|
|
228
|
+
*,
|
|
229
|
+
child_runtime_config: RuntimeConfig,
|
|
230
|
+
child_specs: List[AnySpec],
|
|
231
|
+
shared_runtime: Optional["Runtime"] = None,
|
|
232
|
+
allowlist: Optional[InvokeCapabilityAllowlist] = None,
|
|
233
|
+
requires_approval: bool = True,
|
|
234
|
+
artifacts_subdir: str = "artifacts/invoke_capability",
|
|
235
|
+
timeout_ms: int = 60_000,
|
|
236
|
+
override: bool = False,
|
|
237
|
+
) -> CustomTool:
|
|
238
|
+
"""
|
|
239
|
+
构造一个可注入到 RuntimeConfig.custom_tools 的 invoke_capability 工具。
|
|
240
|
+
|
|
241
|
+
参数:
|
|
242
|
+
- child_runtime_config:子调用 Runtime 的配置模板(在后台 runner 的 event loop 中创建 Runtime)
|
|
243
|
+
- child_specs:子调用可执行的能力声明快照(AgentSpec/WorkflowSpec 列表)
|
|
244
|
+
- shared_runtime:可选共享 Runtime;提供时将复用该实例执行子能力(不再为每次子调用创建新 Runtime)
|
|
245
|
+
- allowlist:可选能力白名单;提供后将启用校验(未命中则拒绝)
|
|
246
|
+
- requires_approval:是否提示该 tool 需要审批(最终由 safety/policy 决定)
|
|
247
|
+
- artifacts_subdir:产物子目录(相对 workspace_root)
|
|
248
|
+
- timeout_ms:子调用超时(毫秒);超时将返回 tool error_kind=timeout
|
|
249
|
+
- override:是否允许覆盖同名工具
|
|
250
|
+
|
|
251
|
+
返回:
|
|
252
|
+
- CustomTool(ToolSpec + handler)
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
spec = ToolSpec(
|
|
256
|
+
name="invoke_capability",
|
|
257
|
+
description="\n".join(
|
|
258
|
+
[
|
|
259
|
+
"宿主能力委托工具:执行子 Agent/子 Workflow,并返回最小披露摘要。",
|
|
260
|
+
"约束:tool handler 同步;子调用通过后台线程 + 常驻 event loop runner 执行。",
|
|
261
|
+
"入参:{capability_id, input}。",
|
|
262
|
+
]
|
|
263
|
+
),
|
|
264
|
+
parameters={
|
|
265
|
+
"type": "object",
|
|
266
|
+
"properties": {
|
|
267
|
+
"capability_id": {"type": "string", "minLength": 1, "description": "目标能力 ID(Agent/Workflow)"},
|
|
268
|
+
"input": {"type": "object", "description": "输入参数(敏感/大 payload 建议以 artifact 指针传递)"},
|
|
269
|
+
},
|
|
270
|
+
"required": ["capability_id", "input"],
|
|
271
|
+
"additionalProperties": False,
|
|
272
|
+
},
|
|
273
|
+
requires_approval=bool(requires_approval),
|
|
274
|
+
idempotency="unknown",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def handler(call: ToolCall, ctx: ToolExecutionContext) -> ToolResult:
|
|
278
|
+
"""
|
|
279
|
+
工具 handler(同步):委托执行子能力并返回摘要。
|
|
280
|
+
|
|
281
|
+
参数:
|
|
282
|
+
- call:工具调用(包含 args)
|
|
283
|
+
- ctx:工具执行上下文(提供 workspace_root/run_id/WAL emitter 等)
|
|
284
|
+
|
|
285
|
+
返回:
|
|
286
|
+
- ToolResult(结构化结果写入 ToolResultPayload.data)
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
started = time.monotonic()
|
|
290
|
+
duration_ms = 0
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
args = InvokeCapabilityArgs.model_validate(call.args)
|
|
294
|
+
except ValidationError as exc:
|
|
295
|
+
duration_ms = int((time.monotonic() - started) * 1000)
|
|
296
|
+
return ToolResult.error_payload(
|
|
297
|
+
error_kind="validation",
|
|
298
|
+
stderr="invoke_capability args validation failed",
|
|
299
|
+
data={"error": str(exc)},
|
|
300
|
+
duration_ms=duration_ms,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
capability_id = str(args.capability_id).strip()
|
|
304
|
+
input_dict = dict(args.input or {})
|
|
305
|
+
|
|
306
|
+
if allowlist is not None and not allowlist.is_allowed(capability_id):
|
|
307
|
+
duration_ms = int((time.monotonic() - started) * 1000)
|
|
308
|
+
return ToolResult.error_payload(
|
|
309
|
+
error_kind="permission",
|
|
310
|
+
stderr=f"capability_id is not allowed: {capability_id!r}",
|
|
311
|
+
data={"capability_id": capability_id},
|
|
312
|
+
duration_ms=duration_ms,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# 子调用 run_id:用于审计追溯与 WAL 定位(不得与父 run 混淆)。
|
|
316
|
+
child_run_id = uuid.uuid4().hex
|
|
317
|
+
|
|
318
|
+
# 产物路径:位于 workspace_root 下,便于复制/审计。
|
|
319
|
+
artifact_rel = f"{str(artifacts_subdir).strip().strip('/')}/{ctx.run_id}/{call.call_id}.json"
|
|
320
|
+
artifact_path = ctx.resolve_path(artifact_rel)
|
|
321
|
+
|
|
322
|
+
async def _run_child() -> Dict[str, Any]:
|
|
323
|
+
if shared_runtime is None:
|
|
324
|
+
# 在 runner loop 内创建 Runtime(避免 asyncio 原语绑定到错误的 loop)。
|
|
325
|
+
from ..runtime import Runtime # lazy import(避免循环导入)
|
|
326
|
+
|
|
327
|
+
rt = Runtime(child_runtime_config)
|
|
328
|
+
rt.register_many(list(child_specs or []))
|
|
329
|
+
max_depth = child_runtime_config.max_depth
|
|
330
|
+
else:
|
|
331
|
+
# 复用宿主提供的 Runtime 实例。
|
|
332
|
+
rt = shared_runtime
|
|
333
|
+
# 兼容:允许调用方仍提供 child_specs;此时将其注册到 shared runtime(last-write-wins)。
|
|
334
|
+
rt.register_many(list(child_specs or []))
|
|
335
|
+
max_depth = int(getattr(rt.config, "max_depth", child_runtime_config.max_depth))
|
|
336
|
+
|
|
337
|
+
# 子调用上下文(独立 run_id,避免与父 run 的证据链混淆)。
|
|
338
|
+
child_ctx = ExecutionContext(run_id=child_run_id, max_depth=max_depth, guards=None)
|
|
339
|
+
result = await rt.run(capability_id, input=input_dict, context=child_ctx)
|
|
340
|
+
|
|
341
|
+
capability_status = getattr(result.status, "value", str(result.status))
|
|
342
|
+
node_status = None
|
|
343
|
+
events_path = None
|
|
344
|
+
if result.node_report is not None:
|
|
345
|
+
node_status = result.node_report.status
|
|
346
|
+
events_path = result.node_report.events_path
|
|
347
|
+
|
|
348
|
+
out = str(result.output or "")
|
|
349
|
+
out_bytes = out.encode("utf-8")
|
|
350
|
+
out_sha256 = _sha256_bytes(out_bytes)
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
"child_capability_status": capability_status,
|
|
354
|
+
"child_node_status": node_status,
|
|
355
|
+
"child_events_path": events_path,
|
|
356
|
+
"child_output_sha256": out_sha256,
|
|
357
|
+
"child_output_bytes": len(out_bytes),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
# 执行子调用(阻塞等待完成)。
|
|
362
|
+
digest = _INVOKE_CAPABILITY_RUNNER.run(
|
|
363
|
+
_run_child(),
|
|
364
|
+
timeout_s=float(timeout_ms) / 1000.0 if timeout_ms else None,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
artifact_obj: Dict[str, Any] = {
|
|
368
|
+
"schema": _ARTIFACT_SCHEMA_ID,
|
|
369
|
+
"created_at_ms": int(time.time() * 1000),
|
|
370
|
+
"parent_run_id": str(ctx.run_id),
|
|
371
|
+
"call_id": str(call.call_id),
|
|
372
|
+
"capability_id": capability_id,
|
|
373
|
+
"child_run_id": child_run_id,
|
|
374
|
+
**digest,
|
|
375
|
+
}
|
|
376
|
+
artifact_sha256, artifact_bytes = _write_json_artifact(path=artifact_path, obj=artifact_obj)
|
|
377
|
+
|
|
378
|
+
duration_ms = int((time.monotonic() - started) * 1000)
|
|
379
|
+
data = {
|
|
380
|
+
"capability_id": capability_id,
|
|
381
|
+
"child_run_id": child_run_id,
|
|
382
|
+
"child_capability_status": digest.get("child_capability_status"),
|
|
383
|
+
"child_node_status": digest.get("child_node_status"),
|
|
384
|
+
"child_events_path": digest.get("child_events_path"),
|
|
385
|
+
"artifact_path": str(artifact_path),
|
|
386
|
+
"artifact_sha256": artifact_sha256,
|
|
387
|
+
"artifact_bytes": artifact_bytes,
|
|
388
|
+
}
|
|
389
|
+
return ToolResult.ok_payload(stdout="invoke_capability ok", data=data, duration_ms=duration_ms)
|
|
390
|
+
except TimeoutError:
|
|
391
|
+
duration_ms = int((time.monotonic() - started) * 1000)
|
|
392
|
+
return ToolResult.error_payload(
|
|
393
|
+
error_kind="timeout",
|
|
394
|
+
stderr="invoke_capability child run timed out",
|
|
395
|
+
data={"capability_id": capability_id, "child_run_id": child_run_id},
|
|
396
|
+
duration_ms=duration_ms,
|
|
397
|
+
retryable=False,
|
|
398
|
+
)
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
duration_ms = int((time.monotonic() - started) * 1000)
|
|
401
|
+
return ToolResult.error_payload(
|
|
402
|
+
error_kind="unknown",
|
|
403
|
+
stderr=f"invoke_capability failed: {type(exc).__name__}",
|
|
404
|
+
data={"capability_id": capability_id, "child_run_id": child_run_id},
|
|
405
|
+
duration_ms=duration_ms,
|
|
406
|
+
retryable=False,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return CustomTool(spec=spec, handler=handler, override=bool(override))
|