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,865 @@
|
|
|
1
|
+
"""WorkflowEngine 的 TriggerFlow 实现(内部细节,不对外暴露)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import uuid
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, cast
|
|
10
|
+
|
|
11
|
+
from agently import TriggerFlow
|
|
12
|
+
|
|
13
|
+
from ..host_protocol import build_approval_ticket_from_report
|
|
14
|
+
from ..logging_utils import log_suppressed_exception
|
|
15
|
+
from ..protocol.capability import CapabilityResult, CapabilityStatus
|
|
16
|
+
from ..protocol.context import ExecutionContext, RecursionLimitError
|
|
17
|
+
from ..protocol.workflow import (
|
|
18
|
+
ConditionalStep,
|
|
19
|
+
InputMapping,
|
|
20
|
+
LoopStep,
|
|
21
|
+
ParallelStep,
|
|
22
|
+
Step,
|
|
23
|
+
WorkflowSpec,
|
|
24
|
+
WorkflowStep,
|
|
25
|
+
)
|
|
26
|
+
from ..services import RuntimeServices
|
|
27
|
+
from .workflow_engine import WorkflowStreamEvent, WorkflowStreamItem
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_WF_WORKFLOW_ID_KEY = "__wf_workflow_id"
|
|
31
|
+
_WF_WORKFLOW_INSTANCE_ID_KEY = "__wf_workflow_instance_id"
|
|
32
|
+
_WF_STEP_ID_KEY = "__wf_step_id"
|
|
33
|
+
_WF_BRANCH_ID_KEY = "__wf_branch_id"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class _WorkflowContextHolder:
|
|
38
|
+
"""
|
|
39
|
+
可变 context 容器(消除 nonlocal 语义歧义)。
|
|
40
|
+
|
|
41
|
+
说明:
|
|
42
|
+
- ExecutionContext 字段语义上应被视为不可变;
|
|
43
|
+
- LoopStep.collect_as 需要更新 context.bag;
|
|
44
|
+
- 通过持有可变引用来传递更新,而非使用 nonlocal 重绑定。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
context: ExecutionContext
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _to_step_result_dict(result: CapabilityResult) -> Dict[str, Any]:
|
|
51
|
+
"""把 CapabilityResult 归一为 workflow step_results 的最小可编排结构。"""
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"status": getattr(result.status, "value", str(result.status)),
|
|
55
|
+
"output": result.output,
|
|
56
|
+
"error": result.error,
|
|
57
|
+
"report": result.report or result.node_report,
|
|
58
|
+
"node_report": result.node_report,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TriggerFlowWorkflowEngine:
|
|
63
|
+
"""
|
|
64
|
+
基于 Agently TriggerFlow 的 Workflow 执行引擎。
|
|
65
|
+
|
|
66
|
+
设计说明:
|
|
67
|
+
- TriggerFlow 仅作为内部编排执行器;
|
|
68
|
+
- Runtime 对外仍只暴露 `run()`/`run_stream()`;
|
|
69
|
+
- Workflow 流式输出仅提供轻量事件字典,深审计仍依赖 WAL/events。
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def _build_fail_closed_result(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
services: RuntimeServices,
|
|
76
|
+
context: ExecutionContext,
|
|
77
|
+
workflow_id: str,
|
|
78
|
+
error: str,
|
|
79
|
+
error_code: str,
|
|
80
|
+
reason: str,
|
|
81
|
+
completion_reason: str,
|
|
82
|
+
meta: Optional[Dict[str, Any]] = None,
|
|
83
|
+
) -> CapabilityResult:
|
|
84
|
+
report = services.build_fail_closed_report(
|
|
85
|
+
run_id=context.run_id,
|
|
86
|
+
status="failed",
|
|
87
|
+
reason=reason,
|
|
88
|
+
completion_reason=completion_reason,
|
|
89
|
+
meta={"workflow_id": workflow_id, **(meta or {})},
|
|
90
|
+
)
|
|
91
|
+
return CapabilityResult(
|
|
92
|
+
status=CapabilityStatus.FAILED,
|
|
93
|
+
error=error,
|
|
94
|
+
error_code=error_code,
|
|
95
|
+
report=report,
|
|
96
|
+
node_report=report,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def execute(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
spec: WorkflowSpec,
|
|
103
|
+
input: Dict[str, Any],
|
|
104
|
+
context: ExecutionContext,
|
|
105
|
+
services: RuntimeServices,
|
|
106
|
+
) -> CapabilityResult:
|
|
107
|
+
"""执行 Workflow(非流式)。"""
|
|
108
|
+
|
|
109
|
+
terminal: CapabilityResult | None = None
|
|
110
|
+
async for item in self.execute_stream(spec=spec, input=input, context=context, services=services):
|
|
111
|
+
if isinstance(item, CapabilityResult):
|
|
112
|
+
terminal = item
|
|
113
|
+
if terminal is not None:
|
|
114
|
+
return terminal
|
|
115
|
+
report = services.build_fail_closed_report(
|
|
116
|
+
run_id=context.run_id,
|
|
117
|
+
status="failed",
|
|
118
|
+
reason="engine_error",
|
|
119
|
+
completion_reason="missing_terminal_result",
|
|
120
|
+
meta={"workflow_id": spec.base.id, "source": "workflow.execute"},
|
|
121
|
+
)
|
|
122
|
+
return CapabilityResult(
|
|
123
|
+
status=CapabilityStatus.FAILED,
|
|
124
|
+
error="Workflow execution produced no result",
|
|
125
|
+
error_code="ENGINE_ERROR",
|
|
126
|
+
report=report,
|
|
127
|
+
node_report=report,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def execute_stream(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
spec: WorkflowSpec,
|
|
134
|
+
input: Dict[str, Any],
|
|
135
|
+
context: ExecutionContext,
|
|
136
|
+
services: RuntimeServices,
|
|
137
|
+
) -> AsyncIterator[WorkflowStreamItem]:
|
|
138
|
+
"""
|
|
139
|
+
执行 Workflow(流式)。
|
|
140
|
+
|
|
141
|
+
事件策略:
|
|
142
|
+
- 仅输出轻量 workflow 事件(workflow started/finished + step started/finished);
|
|
143
|
+
- 不透传上游全量 AgentEvent,避免把深审计负担带到默认流里。
|
|
144
|
+
- 深审计与编排分支依据应读取 WAL/events + NodeReport(真相源),而非依赖轻量事件的 payload 细节。
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
workflow_instance_id = uuid.uuid4().hex
|
|
148
|
+
context = context.with_bag_overlay(**(input or {}))
|
|
149
|
+
context = context.with_bag_overlay(
|
|
150
|
+
**{
|
|
151
|
+
_WF_WORKFLOW_ID_KEY: str(spec.base.id),
|
|
152
|
+
_WF_WORKFLOW_INSTANCE_ID_KEY: str(workflow_instance_id),
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
# 使用 _WorkflowContextHolder 替代 nonlocal context,消除闭包状态语义歧义
|
|
156
|
+
context_holder = _WorkflowContextHolder(context=context)
|
|
157
|
+
event_queue: asyncio.Queue[WorkflowStreamEvent | object] = asyncio.Queue()
|
|
158
|
+
terminal_holder: Dict[str, CapabilityResult] = {}
|
|
159
|
+
queue_stop = object()
|
|
160
|
+
|
|
161
|
+
async def emit(event: WorkflowStreamEvent) -> None:
|
|
162
|
+
await event_queue.put(event)
|
|
163
|
+
|
|
164
|
+
await emit(
|
|
165
|
+
{
|
|
166
|
+
"type": "workflow.started",
|
|
167
|
+
"run_id": context.run_id,
|
|
168
|
+
"workflow_id": spec.base.id,
|
|
169
|
+
"workflow_instance_id": workflow_instance_id,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
flow = TriggerFlow(name=f"runtime-workflow-{spec.base.id}-{context.run_id[:8]}")
|
|
174
|
+
|
|
175
|
+
@flow.chunk("bootstrap")
|
|
176
|
+
async def bootstrap(data: Any) -> Dict[str, Any]:
|
|
177
|
+
payload = getattr(data, "value", None)
|
|
178
|
+
if isinstance(payload, dict):
|
|
179
|
+
return payload
|
|
180
|
+
return {"__terminal_result__": None}
|
|
181
|
+
|
|
182
|
+
chain = flow.to(bootstrap)
|
|
183
|
+
|
|
184
|
+
for index, step in enumerate(spec.steps):
|
|
185
|
+
|
|
186
|
+
async def run_step(
|
|
187
|
+
data: Any,
|
|
188
|
+
*,
|
|
189
|
+
bound_step: WorkflowStep = step,
|
|
190
|
+
bound_index: int = index,
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
payload_raw = getattr(data, "value", None)
|
|
193
|
+
payload: Dict[str, Any] = dict(payload_raw) if isinstance(payload_raw, dict) else {}
|
|
194
|
+
terminal = payload.get("__terminal_result__")
|
|
195
|
+
if isinstance(terminal, CapabilityResult):
|
|
196
|
+
# 终态已确定,后续 chunk 跳过执行(保持 stop-on-non-success 语义)。
|
|
197
|
+
return payload
|
|
198
|
+
|
|
199
|
+
step_id = getattr(bound_step, "id", f"step_{bound_index}")
|
|
200
|
+
step_context = context_holder.context.with_bag_overlay(**{_WF_STEP_ID_KEY: str(step_id)})
|
|
201
|
+
|
|
202
|
+
# 取消语义(协作式):
|
|
203
|
+
# - 当前 step 执行中取消:不强制中断,由 _execute_step 内部与下一个 step 边界决定;
|
|
204
|
+
# - 下一步开始前已取消:不得发出 step.started(避免误导为"已开始执行")。
|
|
205
|
+
if step_context.cancel_token is not None and step_context.cancel_token.is_cancelled:
|
|
206
|
+
report = services.build_fail_closed_report(
|
|
207
|
+
run_id=step_context.run_id,
|
|
208
|
+
status="incomplete",
|
|
209
|
+
reason="cancelled",
|
|
210
|
+
completion_reason="run_cancelled",
|
|
211
|
+
meta={"workflow_id": spec.base.id, "step_id": step_id},
|
|
212
|
+
)
|
|
213
|
+
payload["__terminal_result__"] = CapabilityResult(
|
|
214
|
+
status=CapabilityStatus.CANCELLED,
|
|
215
|
+
error="execution cancelled",
|
|
216
|
+
error_code="RUN_CANCELLED",
|
|
217
|
+
report=report,
|
|
218
|
+
node_report=report,
|
|
219
|
+
)
|
|
220
|
+
return payload
|
|
221
|
+
|
|
222
|
+
await emit(
|
|
223
|
+
{
|
|
224
|
+
"type": "workflow.step.started",
|
|
225
|
+
"run_id": context_holder.context.run_id,
|
|
226
|
+
"workflow_id": spec.base.id,
|
|
227
|
+
"workflow_instance_id": workflow_instance_id,
|
|
228
|
+
"step_id": step_id,
|
|
229
|
+
"capability_id": self._step_capability_id(bound_step),
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
result = await self._execute_step(cast(Any, bound_step), context=step_context, services=services)
|
|
234
|
+
# 顶层 LoopStep.collect_as 需要把结果写回 workflow 级 bag,供后续步骤使用。
|
|
235
|
+
# 使用 _WorkflowContextHolder 显式更新,而非 nonlocal 重绑定
|
|
236
|
+
if isinstance(bound_step, LoopStep) and result.status == CapabilityStatus.SUCCESS and bound_step.collect_as:
|
|
237
|
+
context_holder.context = context_holder.context.with_bag_overlay(
|
|
238
|
+
**{str(bound_step.collect_as): result.output}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
approval_ticket = build_approval_ticket_from_report(result.node_report, capability_id=spec.base.id)
|
|
242
|
+
await emit(
|
|
243
|
+
{
|
|
244
|
+
"type": "workflow.step.finished",
|
|
245
|
+
"run_id": context_holder.context.run_id,
|
|
246
|
+
"workflow_id": spec.base.id,
|
|
247
|
+
"workflow_instance_id": workflow_instance_id,
|
|
248
|
+
"step_id": step_id,
|
|
249
|
+
"capability_id": self._step_capability_id(bound_step),
|
|
250
|
+
"status": getattr(result.status, "value", str(result.status)),
|
|
251
|
+
"error": result.error,
|
|
252
|
+
"waiting_approval_key": approval_ticket.approval_key if approval_ticket is not None else None,
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if result.status != CapabilityStatus.SUCCESS:
|
|
257
|
+
payload["__terminal_result__"] = result
|
|
258
|
+
return payload
|
|
259
|
+
|
|
260
|
+
chain = chain.to((f"wf_step_{index}_{getattr(step, 'id', index)}", run_step))
|
|
261
|
+
|
|
262
|
+
@flow.chunk("finalize")
|
|
263
|
+
async def finalize(data: Any) -> CapabilityResult:
|
|
264
|
+
payload_raw = getattr(data, "value", None)
|
|
265
|
+
payload: Dict[str, Any] = dict(payload_raw) if isinstance(payload_raw, dict) else {}
|
|
266
|
+
terminal = payload.get("__terminal_result__")
|
|
267
|
+
|
|
268
|
+
if isinstance(terminal, CapabilityResult):
|
|
269
|
+
result = terminal
|
|
270
|
+
else:
|
|
271
|
+
output = self._resolve_output_mappings(spec.output_mappings, context_holder.context)
|
|
272
|
+
if output is None:
|
|
273
|
+
output = dict(context_holder.context.step_outputs)
|
|
274
|
+
result = CapabilityResult(status=CapabilityStatus.SUCCESS, output=output)
|
|
275
|
+
|
|
276
|
+
terminal_holder["result"] = result
|
|
277
|
+
await emit(
|
|
278
|
+
{
|
|
279
|
+
"type": "workflow.finished",
|
|
280
|
+
"run_id": context_holder.context.run_id,
|
|
281
|
+
"workflow_id": spec.base.id,
|
|
282
|
+
"workflow_instance_id": workflow_instance_id,
|
|
283
|
+
"status": getattr(result.status, "value", str(result.status)),
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
chain.to(finalize).end()
|
|
289
|
+
|
|
290
|
+
async def run_flow() -> None:
|
|
291
|
+
try:
|
|
292
|
+
result = await flow.async_start(
|
|
293
|
+
{"__terminal_result__": None},
|
|
294
|
+
wait_for_result=True,
|
|
295
|
+
timeout=None,
|
|
296
|
+
)
|
|
297
|
+
if isinstance(result, CapabilityResult):
|
|
298
|
+
terminal_holder.setdefault("result", result)
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
log_suppressed_exception(
|
|
301
|
+
context="workflow_triggerflow_engine",
|
|
302
|
+
exc=exc,
|
|
303
|
+
run_id=context_holder.context.run_id,
|
|
304
|
+
capability_id=spec.base.id,
|
|
305
|
+
extra={"workflow_instance_id": workflow_instance_id},
|
|
306
|
+
)
|
|
307
|
+
report = services.build_fail_closed_report(
|
|
308
|
+
run_id=context_holder.context.run_id,
|
|
309
|
+
status="failed",
|
|
310
|
+
reason="engine_error",
|
|
311
|
+
completion_reason="engine_exception",
|
|
312
|
+
meta={"engine_exception": type(exc).__name__, "workflow_instance_id": workflow_instance_id},
|
|
313
|
+
)
|
|
314
|
+
terminal_holder["result"] = CapabilityResult(
|
|
315
|
+
status=CapabilityStatus.FAILED,
|
|
316
|
+
error=f"Workflow TriggerFlow engine error: {exc}",
|
|
317
|
+
error_code="ENGINE_ERROR",
|
|
318
|
+
report=report,
|
|
319
|
+
node_report=report,
|
|
320
|
+
)
|
|
321
|
+
await emit(
|
|
322
|
+
{
|
|
323
|
+
"type": "workflow.finished",
|
|
324
|
+
"run_id": context_holder.context.run_id,
|
|
325
|
+
"workflow_id": spec.base.id,
|
|
326
|
+
"workflow_instance_id": workflow_instance_id,
|
|
327
|
+
"status": "failed",
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
finally:
|
|
331
|
+
await event_queue.put(queue_stop)
|
|
332
|
+
|
|
333
|
+
flow_task = asyncio.create_task(run_flow())
|
|
334
|
+
try:
|
|
335
|
+
while True:
|
|
336
|
+
item = await event_queue.get()
|
|
337
|
+
if item is queue_stop:
|
|
338
|
+
break
|
|
339
|
+
yield cast(WorkflowStreamEvent, item)
|
|
340
|
+
finally:
|
|
341
|
+
await flow_task
|
|
342
|
+
|
|
343
|
+
terminal = terminal_holder.get("result")
|
|
344
|
+
if terminal is None:
|
|
345
|
+
report = services.build_fail_closed_report(
|
|
346
|
+
run_id=context_holder.context.run_id,
|
|
347
|
+
status="failed",
|
|
348
|
+
reason="engine_error",
|
|
349
|
+
completion_reason="missing_terminal_result",
|
|
350
|
+
meta={"workflow_id": spec.base.id, "workflow_instance_id": workflow_instance_id},
|
|
351
|
+
)
|
|
352
|
+
terminal = CapabilityResult(
|
|
353
|
+
status=CapabilityStatus.FAILED,
|
|
354
|
+
error="Workflow execution produced no result",
|
|
355
|
+
error_code="ENGINE_ERROR",
|
|
356
|
+
report=report,
|
|
357
|
+
node_report=report,
|
|
358
|
+
)
|
|
359
|
+
yield terminal
|
|
360
|
+
|
|
361
|
+
async def _execute_step(
|
|
362
|
+
self,
|
|
363
|
+
step: Any,
|
|
364
|
+
*,
|
|
365
|
+
context: ExecutionContext,
|
|
366
|
+
services: RuntimeServices,
|
|
367
|
+
) -> CapabilityResult:
|
|
368
|
+
"""按步骤类型分发执行。"""
|
|
369
|
+
|
|
370
|
+
if context.cancel_token is not None and context.cancel_token.is_cancelled:
|
|
371
|
+
report = services.build_fail_closed_report(
|
|
372
|
+
run_id=context.run_id,
|
|
373
|
+
status="incomplete",
|
|
374
|
+
reason="cancelled",
|
|
375
|
+
completion_reason="run_cancelled",
|
|
376
|
+
meta={"workflow_step": getattr(step, "id", None)},
|
|
377
|
+
)
|
|
378
|
+
return CapabilityResult(
|
|
379
|
+
status=CapabilityStatus.CANCELLED,
|
|
380
|
+
error="execution cancelled",
|
|
381
|
+
error_code="RUN_CANCELLED",
|
|
382
|
+
report=report,
|
|
383
|
+
node_report=report,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if isinstance(step, Step):
|
|
387
|
+
return await self._execute_basic_step(step, context=context, services=services)
|
|
388
|
+
if isinstance(step, LoopStep):
|
|
389
|
+
return await self._execute_loop_step(step, context=context, services=services)
|
|
390
|
+
if isinstance(step, ParallelStep):
|
|
391
|
+
return await self._execute_parallel_step(step, context=context, services=services)
|
|
392
|
+
if isinstance(step, ConditionalStep):
|
|
393
|
+
return await self._execute_conditional_step(step, context=context, services=services)
|
|
394
|
+
return self._build_fail_closed_result(
|
|
395
|
+
services=services,
|
|
396
|
+
context=context,
|
|
397
|
+
workflow_id="unknown",
|
|
398
|
+
error=f"Unknown step type: {type(step).__name__}",
|
|
399
|
+
error_code="INVALID_WORKFLOW_STEP",
|
|
400
|
+
reason="invalid_workflow_step",
|
|
401
|
+
completion_reason="unknown_step_type",
|
|
402
|
+
meta={"actual_type": type(step).__name__},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def _step_capability_id(self, step: Any) -> str | None:
|
|
406
|
+
"""
|
|
407
|
+
提取 workflow step 绑定的 capability ID。
|
|
408
|
+
|
|
409
|
+
参数:
|
|
410
|
+
- step:WorkflowStep
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
capability = getattr(step, "capability", None)
|
|
414
|
+
capability_id = getattr(capability, "id", None)
|
|
415
|
+
return capability_id if isinstance(capability_id, str) and capability_id.strip() else None
|
|
416
|
+
|
|
417
|
+
async def _execute_basic_step(
|
|
418
|
+
self,
|
|
419
|
+
step: Step,
|
|
420
|
+
*,
|
|
421
|
+
context: ExecutionContext,
|
|
422
|
+
services: RuntimeServices,
|
|
423
|
+
) -> CapabilityResult:
|
|
424
|
+
"""执行基础步骤。"""
|
|
425
|
+
|
|
426
|
+
step_input = self._resolve_input_mappings(step.input_mappings, context)
|
|
427
|
+
target_spec = services.registry.get_or_raise(step.capability.id)
|
|
428
|
+
execute_task: asyncio.Task[CapabilityResult] | None = None
|
|
429
|
+
try:
|
|
430
|
+
if step.timeout_s is not None:
|
|
431
|
+
execute_task = asyncio.create_task(
|
|
432
|
+
services.execute_capability(spec=target_spec, input=step_input, context=context)
|
|
433
|
+
)
|
|
434
|
+
done, _pending = await asyncio.wait({execute_task}, timeout=step.timeout_s)
|
|
435
|
+
if not done:
|
|
436
|
+
execute_task.cancel()
|
|
437
|
+
with suppress(BaseException):
|
|
438
|
+
await execute_task
|
|
439
|
+
raise asyncio.TimeoutError()
|
|
440
|
+
result = execute_task.result()
|
|
441
|
+
else:
|
|
442
|
+
result = await services.execute_capability(spec=target_spec, input=step_input, context=context)
|
|
443
|
+
except asyncio.TimeoutError:
|
|
444
|
+
report = services.build_fail_closed_report(
|
|
445
|
+
run_id=context.run_id,
|
|
446
|
+
status="failed",
|
|
447
|
+
reason="timeout",
|
|
448
|
+
completion_reason="step_timeout",
|
|
449
|
+
meta={"step_id": step.id, "capability_id": step.capability.id},
|
|
450
|
+
)
|
|
451
|
+
result = CapabilityResult(
|
|
452
|
+
status=CapabilityStatus.FAILED,
|
|
453
|
+
error=f"step timeout: {step.id}",
|
|
454
|
+
error_code="STEP_TIMEOUT",
|
|
455
|
+
report=report,
|
|
456
|
+
node_report=report,
|
|
457
|
+
)
|
|
458
|
+
except asyncio.CancelledError:
|
|
459
|
+
if execute_task is not None:
|
|
460
|
+
execute_task.cancel()
|
|
461
|
+
with suppress(BaseException):
|
|
462
|
+
await execute_task
|
|
463
|
+
report = services.build_fail_closed_report(
|
|
464
|
+
run_id=context.run_id,
|
|
465
|
+
status="incomplete",
|
|
466
|
+
reason="cancelled",
|
|
467
|
+
completion_reason="run_cancelled",
|
|
468
|
+
meta={"step_id": step.id, "capability_id": step.capability.id},
|
|
469
|
+
)
|
|
470
|
+
result = CapabilityResult(
|
|
471
|
+
status=CapabilityStatus.CANCELLED,
|
|
472
|
+
error="execution cancelled",
|
|
473
|
+
error_code="RUN_CANCELLED",
|
|
474
|
+
report=report,
|
|
475
|
+
node_report=report,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
context.step_outputs[step.id] = result.output
|
|
479
|
+
context.step_results[step.id] = _to_step_result_dict(result)
|
|
480
|
+
return result
|
|
481
|
+
|
|
482
|
+
async def _execute_loop_step(
|
|
483
|
+
self,
|
|
484
|
+
step: LoopStep,
|
|
485
|
+
*,
|
|
486
|
+
context: ExecutionContext,
|
|
487
|
+
services: RuntimeServices,
|
|
488
|
+
) -> CapabilityResult:
|
|
489
|
+
"""执行循环步骤。"""
|
|
490
|
+
|
|
491
|
+
items = context.resolve_mapping(step.iterate_over)
|
|
492
|
+
if not isinstance(items, list):
|
|
493
|
+
return self._build_fail_closed_result(
|
|
494
|
+
services=services,
|
|
495
|
+
context=context,
|
|
496
|
+
workflow_id=step.id,
|
|
497
|
+
error=(
|
|
498
|
+
f"LoopStep '{step.id}': iterate_over resolved to "
|
|
499
|
+
f"{type(items).__name__}, expected list"
|
|
500
|
+
),
|
|
501
|
+
error_code="INVALID_LOOP_INPUT",
|
|
502
|
+
reason="invalid_workflow_step",
|
|
503
|
+
completion_reason="loop_iterate_over_not_list",
|
|
504
|
+
meta={"step_id": step.id, "actual_type": type(items).__name__},
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
target_spec = services.registry.get_or_raise(step.capability.id)
|
|
508
|
+
|
|
509
|
+
async def execute_item(item: Any, _idx: int) -> CapabilityResult:
|
|
510
|
+
# 深度递增并检查是否超限
|
|
511
|
+
new_depth = context.depth + 1
|
|
512
|
+
if new_depth > context.max_depth:
|
|
513
|
+
raise RecursionLimitError(
|
|
514
|
+
f"LoopStep '{step.id}' item[{_idx}]: 深度 {new_depth} 超过最大值 {context.max_depth}"
|
|
515
|
+
)
|
|
516
|
+
item_context = ExecutionContext(
|
|
517
|
+
run_id=context.run_id,
|
|
518
|
+
parent_context=context,
|
|
519
|
+
depth=new_depth,
|
|
520
|
+
max_depth=context.max_depth,
|
|
521
|
+
guards=context.guards,
|
|
522
|
+
cancel_token=context.cancel_token,
|
|
523
|
+
bag=MappingProxyType(
|
|
524
|
+
{
|
|
525
|
+
**dict(context.bag),
|
|
526
|
+
"__current_item__": item,
|
|
527
|
+
_WF_BRANCH_ID_KEY: f"{step.id}:{_idx}",
|
|
528
|
+
}
|
|
529
|
+
),
|
|
530
|
+
step_outputs=dict(context.step_outputs),
|
|
531
|
+
step_results=dict(context.step_results),
|
|
532
|
+
call_chain=list(context.call_chain),
|
|
533
|
+
)
|
|
534
|
+
step_input = self._resolve_input_mappings(step.item_input_mappings, item_context)
|
|
535
|
+
if not step_input:
|
|
536
|
+
step_input = item if isinstance(item, dict) else {"item": item}
|
|
537
|
+
return await services.execute_capability(spec=target_spec, input=step_input, context=item_context)
|
|
538
|
+
|
|
539
|
+
if context.guards is None:
|
|
540
|
+
return self._build_fail_closed_result(
|
|
541
|
+
services=services,
|
|
542
|
+
context=context,
|
|
543
|
+
workflow_id=step.id,
|
|
544
|
+
error=f"LoopStep '{step.id}': missing ExecutionGuards in ExecutionContext",
|
|
545
|
+
error_code="MISSING_EXECUTION_GUARDS",
|
|
546
|
+
reason="engine_error",
|
|
547
|
+
completion_reason="missing_execution_guards",
|
|
548
|
+
meta={"step_id": step.id},
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
if step.timeout_s is not None:
|
|
552
|
+
loop_task: asyncio.Task[CapabilityResult] | None = None
|
|
553
|
+
try:
|
|
554
|
+
loop_task = asyncio.create_task(
|
|
555
|
+
context.guards.run_loop(
|
|
556
|
+
items=items,
|
|
557
|
+
max_iterations=step.max_iterations,
|
|
558
|
+
execute_fn=execute_item,
|
|
559
|
+
fail_strategy=step.fail_strategy,
|
|
560
|
+
)
|
|
561
|
+
)
|
|
562
|
+
done, _pending = await asyncio.wait({loop_task}, timeout=step.timeout_s)
|
|
563
|
+
if not done:
|
|
564
|
+
loop_task.cancel()
|
|
565
|
+
with suppress(BaseException):
|
|
566
|
+
await loop_task
|
|
567
|
+
raise asyncio.TimeoutError()
|
|
568
|
+
result = loop_task.result()
|
|
569
|
+
except asyncio.TimeoutError:
|
|
570
|
+
report = services.build_fail_closed_report(
|
|
571
|
+
run_id=context.run_id,
|
|
572
|
+
status="failed",
|
|
573
|
+
reason="timeout",
|
|
574
|
+
completion_reason="loop_timeout",
|
|
575
|
+
meta={"step_id": step.id, "capability_id": step.capability.id},
|
|
576
|
+
)
|
|
577
|
+
result = CapabilityResult(
|
|
578
|
+
status=CapabilityStatus.FAILED,
|
|
579
|
+
error=f"loop timeout: {step.id}",
|
|
580
|
+
error_code="LOOP_TIMEOUT",
|
|
581
|
+
output=[],
|
|
582
|
+
report=report,
|
|
583
|
+
node_report=report,
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
result = await context.guards.run_loop(
|
|
587
|
+
items=items,
|
|
588
|
+
max_iterations=step.max_iterations,
|
|
589
|
+
execute_fn=execute_item,
|
|
590
|
+
fail_strategy=step.fail_strategy,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if result.status == CapabilityStatus.FAILED and result.node_report is None:
|
|
594
|
+
report = services.build_fail_closed_report(
|
|
595
|
+
run_id=context.run_id,
|
|
596
|
+
status="failed",
|
|
597
|
+
reason="workflow_step_failed",
|
|
598
|
+
completion_reason="loop_iteration_failed",
|
|
599
|
+
meta={"step_id": step.id, "capability_id": step.capability.id},
|
|
600
|
+
)
|
|
601
|
+
result.report = report
|
|
602
|
+
result.node_report = report
|
|
603
|
+
|
|
604
|
+
context.step_outputs[step.id] = result.output
|
|
605
|
+
context.step_results[step.id] = _to_step_result_dict(result)
|
|
606
|
+
return result
|
|
607
|
+
|
|
608
|
+
async def _execute_parallel_step(
|
|
609
|
+
self,
|
|
610
|
+
step: ParallelStep,
|
|
611
|
+
*,
|
|
612
|
+
context: ExecutionContext,
|
|
613
|
+
services: RuntimeServices,
|
|
614
|
+
) -> CapabilityResult:
|
|
615
|
+
"""执行并行步骤。"""
|
|
616
|
+
|
|
617
|
+
# 深度递增并检查是否超限
|
|
618
|
+
new_depth = context.depth + 1
|
|
619
|
+
if new_depth > context.max_depth:
|
|
620
|
+
return self._build_fail_closed_result(
|
|
621
|
+
services=services,
|
|
622
|
+
context=context,
|
|
623
|
+
workflow_id=step.id,
|
|
624
|
+
error=(
|
|
625
|
+
f"ParallelStep '{step.id}': 深度 {new_depth} 超过最大值 {context.max_depth}"
|
|
626
|
+
),
|
|
627
|
+
error_code="RECURSION_LIMIT",
|
|
628
|
+
reason="recursion_limit",
|
|
629
|
+
completion_reason="recursion_limit",
|
|
630
|
+
meta={"error_type": "recursion_limit", "step_id": step.id},
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
branch_contexts = [
|
|
634
|
+
ExecutionContext(
|
|
635
|
+
run_id=context.run_id,
|
|
636
|
+
parent_context=context,
|
|
637
|
+
depth=new_depth,
|
|
638
|
+
max_depth=context.max_depth,
|
|
639
|
+
guards=context.guards,
|
|
640
|
+
cancel_token=context.cancel_token,
|
|
641
|
+
bag=MappingProxyType({**dict(context.bag), _WF_BRANCH_ID_KEY: f"{step.id}:{i}"}),
|
|
642
|
+
step_outputs=dict(context.step_outputs),
|
|
643
|
+
step_results=dict(context.step_results),
|
|
644
|
+
call_chain=list(context.call_chain),
|
|
645
|
+
)
|
|
646
|
+
for i, _ in enumerate(step.branches)
|
|
647
|
+
]
|
|
648
|
+
tasks = [
|
|
649
|
+
self._execute_step(branch, context=branch_ctx, services=services)
|
|
650
|
+
for branch, branch_ctx in zip(step.branches, branch_contexts)
|
|
651
|
+
]
|
|
652
|
+
raw_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
653
|
+
|
|
654
|
+
branch_results: List[CapabilityResult] = []
|
|
655
|
+
for result in raw_results:
|
|
656
|
+
if isinstance(result, BaseException):
|
|
657
|
+
if isinstance(result, asyncio.CancelledError):
|
|
658
|
+
report = services.build_fail_closed_report(
|
|
659
|
+
run_id=context.run_id,
|
|
660
|
+
status="incomplete",
|
|
661
|
+
reason="cancelled",
|
|
662
|
+
completion_reason="parallel_branch_cancelled",
|
|
663
|
+
meta={"step_id": step.id, "exception_type": type(result).__name__},
|
|
664
|
+
)
|
|
665
|
+
branch_results.append(
|
|
666
|
+
CapabilityResult(
|
|
667
|
+
status=CapabilityStatus.CANCELLED,
|
|
668
|
+
error="execution cancelled",
|
|
669
|
+
error_code="RUN_CANCELLED",
|
|
670
|
+
report=report,
|
|
671
|
+
node_report=report,
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
continue
|
|
675
|
+
msg = str(result).strip()
|
|
676
|
+
branch_results.append(
|
|
677
|
+
self._build_fail_closed_result(
|
|
678
|
+
services=services,
|
|
679
|
+
context=context,
|
|
680
|
+
workflow_id=step.id,
|
|
681
|
+
error=msg or type(result).__name__,
|
|
682
|
+
error_code="BRANCH_EXECUTION_ERROR",
|
|
683
|
+
reason="engine_error",
|
|
684
|
+
completion_reason="parallel_branch_exception",
|
|
685
|
+
meta={"step_id": step.id, "exception_type": type(result).__name__},
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
else:
|
|
689
|
+
branch_results.append(result)
|
|
690
|
+
|
|
691
|
+
if step.join_strategy == "all_success":
|
|
692
|
+
non_success = [result for result in branch_results if result.status != CapabilityStatus.SUCCESS]
|
|
693
|
+
if non_success:
|
|
694
|
+
statuses = {result.status for result in non_success}
|
|
695
|
+
if CapabilityStatus.FAILED in statuses:
|
|
696
|
+
overall = CapabilityStatus.FAILED
|
|
697
|
+
elif CapabilityStatus.CANCELLED in statuses:
|
|
698
|
+
overall = CapabilityStatus.CANCELLED
|
|
699
|
+
elif CapabilityStatus.PENDING in statuses:
|
|
700
|
+
overall = CapabilityStatus.PENDING
|
|
701
|
+
else:
|
|
702
|
+
overall = CapabilityStatus.RUNNING
|
|
703
|
+
|
|
704
|
+
context.step_outputs[step.id] = [result.output for result in branch_results]
|
|
705
|
+
branch_statuses = [
|
|
706
|
+
getattr(result.status, "value", str(result.status)) for result in branch_results
|
|
707
|
+
]
|
|
708
|
+
aggregated = CapabilityResult(
|
|
709
|
+
status=overall,
|
|
710
|
+
output=[result.output for result in branch_results],
|
|
711
|
+
error=(
|
|
712
|
+
f"ParallelStep '{step.id}': "
|
|
713
|
+
f"{len(non_success)}/{len(branch_results)} branches not success"
|
|
714
|
+
)
|
|
715
|
+
if overall == CapabilityStatus.FAILED
|
|
716
|
+
else None,
|
|
717
|
+
error_code=(
|
|
718
|
+
"BRANCH_EXECUTION_ERROR"
|
|
719
|
+
if overall == CapabilityStatus.FAILED
|
|
720
|
+
and any(result.error_code == "BRANCH_EXECUTION_ERROR" for result in branch_results)
|
|
721
|
+
else ("RUN_CANCELLED" if overall == CapabilityStatus.CANCELLED else None)
|
|
722
|
+
),
|
|
723
|
+
metadata={"branch_statuses": branch_statuses},
|
|
724
|
+
)
|
|
725
|
+
if overall != CapabilityStatus.SUCCESS:
|
|
726
|
+
report_status = "failed"
|
|
727
|
+
report_reason = "workflow_step_failed"
|
|
728
|
+
if overall == CapabilityStatus.CANCELLED:
|
|
729
|
+
report_status = "incomplete"
|
|
730
|
+
report_reason = "cancelled"
|
|
731
|
+
elif overall in (CapabilityStatus.PENDING, CapabilityStatus.RUNNING):
|
|
732
|
+
report_status = "incomplete"
|
|
733
|
+
report_reason = "parallel_branches_incomplete"
|
|
734
|
+
report = services.build_fail_closed_report(
|
|
735
|
+
run_id=context.run_id,
|
|
736
|
+
status=report_status,
|
|
737
|
+
reason=report_reason,
|
|
738
|
+
completion_reason="parallel_all_success_not_met",
|
|
739
|
+
meta={
|
|
740
|
+
"step_id": step.id,
|
|
741
|
+
"branch_statuses": branch_statuses,
|
|
742
|
+
},
|
|
743
|
+
)
|
|
744
|
+
aggregated.report = report
|
|
745
|
+
aggregated.node_report = report
|
|
746
|
+
context.step_results[step.id] = _to_step_result_dict(aggregated)
|
|
747
|
+
return aggregated
|
|
748
|
+
elif step.join_strategy == "any_success":
|
|
749
|
+
if not any(result.status == CapabilityStatus.SUCCESS for result in branch_results):
|
|
750
|
+
statuses = {result.status for result in branch_results}
|
|
751
|
+
if CapabilityStatus.FAILED in statuses:
|
|
752
|
+
overall = CapabilityStatus.FAILED
|
|
753
|
+
elif CapabilityStatus.CANCELLED in statuses:
|
|
754
|
+
overall = CapabilityStatus.CANCELLED
|
|
755
|
+
elif CapabilityStatus.PENDING in statuses:
|
|
756
|
+
overall = CapabilityStatus.PENDING
|
|
757
|
+
elif CapabilityStatus.RUNNING in statuses:
|
|
758
|
+
overall = CapabilityStatus.RUNNING
|
|
759
|
+
else:
|
|
760
|
+
overall = CapabilityStatus.FAILED
|
|
761
|
+
|
|
762
|
+
context.step_outputs[step.id] = [result.output for result in branch_results]
|
|
763
|
+
branch_statuses = [
|
|
764
|
+
getattr(result.status, "value", str(result.status)) for result in branch_results
|
|
765
|
+
]
|
|
766
|
+
aggregated = CapabilityResult(
|
|
767
|
+
status=overall,
|
|
768
|
+
output=[result.output for result in branch_results],
|
|
769
|
+
error=f"ParallelStep '{step.id}': no branch succeeded"
|
|
770
|
+
if overall == CapabilityStatus.FAILED
|
|
771
|
+
else None,
|
|
772
|
+
error_code=(
|
|
773
|
+
"BRANCH_EXECUTION_ERROR"
|
|
774
|
+
if overall == CapabilityStatus.FAILED
|
|
775
|
+
and any(result.error_code == "BRANCH_EXECUTION_ERROR" for result in branch_results)
|
|
776
|
+
else ("RUN_CANCELLED" if overall == CapabilityStatus.CANCELLED else None)
|
|
777
|
+
),
|
|
778
|
+
metadata={"branch_statuses": branch_statuses},
|
|
779
|
+
)
|
|
780
|
+
if overall != CapabilityStatus.SUCCESS:
|
|
781
|
+
report_status = "failed"
|
|
782
|
+
report_reason = "workflow_step_failed"
|
|
783
|
+
if overall == CapabilityStatus.CANCELLED:
|
|
784
|
+
report_status = "incomplete"
|
|
785
|
+
report_reason = "cancelled"
|
|
786
|
+
elif overall in (CapabilityStatus.PENDING, CapabilityStatus.RUNNING):
|
|
787
|
+
report_status = "incomplete"
|
|
788
|
+
report_reason = "parallel_branches_incomplete"
|
|
789
|
+
report = services.build_fail_closed_report(
|
|
790
|
+
run_id=context.run_id,
|
|
791
|
+
status=report_status,
|
|
792
|
+
reason=report_reason,
|
|
793
|
+
completion_reason="parallel_any_success_not_met",
|
|
794
|
+
meta={
|
|
795
|
+
"step_id": step.id,
|
|
796
|
+
"branch_statuses": branch_statuses,
|
|
797
|
+
},
|
|
798
|
+
)
|
|
799
|
+
aggregated.report = report
|
|
800
|
+
aggregated.node_report = report
|
|
801
|
+
context.step_results[step.id] = _to_step_result_dict(aggregated)
|
|
802
|
+
return aggregated
|
|
803
|
+
|
|
804
|
+
context.step_outputs[step.id] = [result.output for result in branch_results]
|
|
805
|
+
aggregated = CapabilityResult(
|
|
806
|
+
status=CapabilityStatus.SUCCESS,
|
|
807
|
+
output=[result.output for result in branch_results],
|
|
808
|
+
)
|
|
809
|
+
context.step_results[step.id] = _to_step_result_dict(aggregated)
|
|
810
|
+
return aggregated
|
|
811
|
+
|
|
812
|
+
async def _execute_conditional_step(
|
|
813
|
+
self,
|
|
814
|
+
step: ConditionalStep,
|
|
815
|
+
*,
|
|
816
|
+
context: ExecutionContext,
|
|
817
|
+
services: RuntimeServices,
|
|
818
|
+
) -> CapabilityResult:
|
|
819
|
+
"""执行条件步骤。"""
|
|
820
|
+
|
|
821
|
+
condition_value = context.resolve_mapping(step.condition_source)
|
|
822
|
+
condition_key = str(condition_value) if condition_value is not None else ""
|
|
823
|
+
branch = step.branches.get(condition_key, step.default)
|
|
824
|
+
if branch is None:
|
|
825
|
+
return self._build_fail_closed_result(
|
|
826
|
+
services=services,
|
|
827
|
+
context=context,
|
|
828
|
+
workflow_id=step.id,
|
|
829
|
+
error=(
|
|
830
|
+
f"ConditionalStep '{step.id}': no branch for "
|
|
831
|
+
f"condition '{condition_key}' and no default"
|
|
832
|
+
),
|
|
833
|
+
error_code="CONDITIONAL_BRANCH_NOT_FOUND",
|
|
834
|
+
reason="invalid_workflow_step",
|
|
835
|
+
completion_reason="conditional_branch_not_found",
|
|
836
|
+
meta={"step_id": step.id, "condition_key": condition_key},
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
branch_context = context.with_bag_overlay(
|
|
840
|
+
**{_WF_BRANCH_ID_KEY: f"{step.id}:{condition_key or 'default'}"}
|
|
841
|
+
)
|
|
842
|
+
result = await self._execute_step(branch, context=branch_context, services=services)
|
|
843
|
+
context.step_outputs[step.id] = result.output
|
|
844
|
+
context.step_results[step.id] = _to_step_result_dict(result)
|
|
845
|
+
return result
|
|
846
|
+
|
|
847
|
+
@staticmethod
|
|
848
|
+
def _resolve_input_mappings(mappings: List[InputMapping], context: ExecutionContext) -> Dict[str, Any]:
|
|
849
|
+
"""解析输入映射列表。"""
|
|
850
|
+
|
|
851
|
+
result: Dict[str, Any] = {}
|
|
852
|
+
for mapping in mappings:
|
|
853
|
+
result[mapping.target_field] = context.resolve_mapping(mapping.source)
|
|
854
|
+
return result
|
|
855
|
+
|
|
856
|
+
@staticmethod
|
|
857
|
+
def _resolve_output_mappings(mappings: List[InputMapping], context: ExecutionContext) -> Any:
|
|
858
|
+
"""解析输出映射列表。"""
|
|
859
|
+
|
|
860
|
+
if not mappings:
|
|
861
|
+
return None
|
|
862
|
+
result: Dict[str, Any] = {}
|
|
863
|
+
for mapping in mappings:
|
|
864
|
+
result[mapping.target_field] = context.resolve_mapping(mapping.source)
|
|
865
|
+
return result
|