"""管道基础设施。 定义应用层管道基类 ``Pipeline`` 与阶段协议 ``AppStage``,提供阶段顺序执行、 条件跳过、失败策略处理与补偿执行能力。``FailureStrategy`` 从契约层复用, 不重新定义,保证失败语义在全链路一致。 可观测性(§11.2 / INV-10):``Pipeline.run()`` 在每个阶段执行后输出结构化 日志,必填字段 ``trace_id`` / ``stage_id`` / ``duration_ms`` / ``status`` / ``error_type``(失败时),支持阶段延迟分布与成功率度量。 """ from __future__ import annotations import sys import time from collections.abc import Callable from typing import Any, Protocol, runtime_checkable from yuxi.channels.contract.errors import Error, InternalError, PipelineConfigError from yuxi.channels.contract.plugin.extension_point import FailureStrategy from yuxi.channels.contract.ports.driven.logger_port import LoggerPort @runtime_checkable class AppStage(Protocol): """应用层管道阶段协议。 对齐契约层 ``Stage`` Protocol 并扩展: - ``compensate`` 由 ``bool`` 调整为 ``str | None``,携带补偿阶段名称 (如 "outbox-rollback"),None 表示无补偿; - 新增 ``condition`` 字段,支持基于上下文的阶段执行条件,None 表示 无条件执行。 阶段处理方法 ``process`` 返回 ``bool``:True 表示成功,False 或抛出 异常表示失败。 """ id: str reads: tuple[str, ...] writes: tuple[str, ...] idempotent: bool thread_safe: bool failure: FailureStrategy compensate: str | None condition: Callable[[Any], bool] | None async def process(self, context: Any) -> bool: """处理管道阶段。 参数: context: 管道上下文。 返回: True 表示成功,False 或抛出异常表示失败。 """ ... class Pipeline: """管道基类。 顺序执行阶段,按 ``AppStage.failure`` 策略处理失败。支持条件跳过、 异常翻译与补偿执行。 失败策略处理: - TERMINATE:终止管道,返回失败; - SKIP:跳过当前阶段,继续下一阶段; - COMPENSATE:执行补偿阶段后返回失败; - DEGRADE:标记降级并继续下一阶段。 命名等价说明(6-P2-01):上下文 ``trace_id`` 与 Agent 层 ``turn_id`` 为同一概念的不同命名,均标识一次请求/会话回合的链路追踪 ID。 ``_logStageExecution`` 输出的 ``trace_id`` 字段即等价于 ``turn_id``。 """ name: str stages: list[AppStage] logger: LoggerPort | None def __init__( self, name: str, stages: list[AppStage], logger: LoggerPort | None = None, ) -> None: """初始化管道。 参数: name: 管道名称(如 "inbound" / "outbound" / "control-plane")。 stages: 阶段列表,按列表顺序执行。 logger: 可选日志端口,用于记录补偿阶段异常,None 时不记录。 Raises: PipelineConfigError: 任一阶段的 ``compensate`` 字段指向不存在 的阶段 ID。构造期 fail-fast,避免运行期 ``_runCompensate`` 按名称查找静默失败(§3.1 补偿执行约束)。 """ self.name = name self.stages = stages self.logger = logger self._validateCompensateReferences() def _validateCompensateReferences(self) -> None: """校验所有阶段的 ``compensate`` 引用指向已注册的阶段 ID。 ``compensate`` 字段为字符串约定,``_runCompensate`` 按名称查找 阶段,未找到时**静默返回原错误**(不报错)。若因 typo 导致引用 不存在,补偿逻辑永远不会执行且无任何告警。 本方法在构造期校验所有 ``compensate`` 引用,发现不存在的引用 立即抛出 ``PipelineConfigError``(fail-fast),将运行期静默失败 转换为构造期显式失败。 """ stage_ids = {stage.id for stage in self.stages} for stage in self.stages: if stage.compensate is not None and stage.compensate not in stage_ids: raise PipelineConfigError( pipeline=self.name, stage_id=stage.id, compensate=stage.compensate, ) async def run(self, ctx: Any) -> tuple[bool, Error | None]: """执行管道。 按 ``stages`` 列表顺序执行每个阶段。阶段返回 True 表示成功,继续 下一阶段;返回 False 或抛出异常表示失败,按 ``failure`` 策略处理。 若 ``condition`` 返回 False 则跳过该阶段。 异常翻译:``Error`` 子类直接处理,原生 ``Exception`` 翻译为 ``InternalError``(携带 ``trace_id`` 与异常信息),不得让原生异常 穿透管道边界(INV-7 / FF-ERR-01)。 可观测性(§11.2 / INV-10):每个阶段执行后输出结构化日志,必填 字段 ``trace_id`` / ``stage_id`` / ``duration_ms`` / ``status`` / ``error_type``(失败时),日志失败不影响管道执行。 参数: ctx: 管道上下文。 返回: 元组 (success, error)。全部阶段成功时 success 为 True、 error 为 None;否则 success 为 False、error 携带失败原因。 """ for stage in self.stages: if stage.condition is not None and not stage.condition(ctx): continue ok = False err: Error | None = None start = time.monotonic() try: ok = await stage.process(ctx) except Error as e: err = e except Exception as e: trace_id = getattr(ctx, "trace_id", None) err = InternalError( trace_id=trace_id, message=f"stage '{stage.id}' raised native exception: {type(e).__name__}: {e}", cause=e, ) duration_ms = (time.monotonic() - start) * 1000 # 结构化可观测性日志(§11.2 / INV-10) await self._logStageExecution(ctx, stage.id, duration_ms, ok, err) if ok: continue match stage.failure: case FailureStrategy.TERMINATE: return False, err case FailureStrategy.SKIP: continue case FailureStrategy.COMPENSATE: return False, await self._runCompensate(ctx, stage.compensate, err) case FailureStrategy.DEGRADE: ctx.degraded = True ctx.degraded_reason = err continue return True, None async def _logStageExecution( self, ctx: Any, stage_id: str, duration_ms: float, ok: bool, err: Error | None, ) -> None: """输出阶段执行结构化日志(§11.2 / INV-10)。 必填字段:``trace_id`` / ``stage_id`` / ``duration_ms`` / ``status`` / ``error_type``(失败时)。日志端口为 None 或日志写入失败时不影响 管道执行(降级至 stderr)。 渠道语义字段(6-P0-04 / 6-P1-07 / 6-P2-02):从 ctx 读取并输出 ``channel_type`` / ``account_id`` / ``agent_run_id`` / ``plugin_id`` / ``pairing_id`` / ``dm_decision`` / ``channel_format_spec`` / ``channel_context_note``(仅当值非 None 时输出)。当 ``channel_type`` 与 ``account_id`` 均非 None 时派生 ``channel_id`` 并输出, ``channel_id`` 等价于 ``${channel_type}:${account_id}``。 参数: ctx: 管道上下文(用于提取 trace_id 与渠道语义字段)。 stage_id: 阶段唯一标识。 duration_ms: 阶段执行耗时(毫秒)。 ok: 阶段是否成功。 err: 失败时的错误对象,成功时为 None。 """ if self.logger is None: return trace_id = getattr(ctx, "trace_id", None) # 收集必填字段 fields: dict[str, Any] = { "trace_id": trace_id, "pipeline": self.name, "stage_id": stage_id, "duration_ms": duration_ms, "status": "ok" if ok else "failed", } # 收集渠道语义字段(仅当值非 None 时输出,6-P0-04 / 6-P1-07 / 6-P2-02) channel_type = getattr(ctx, "channel_type", None) account_id = getattr(ctx, "account_id", None) agent_run_id = getattr(ctx, "agent_run_id", None) plugin_id = getattr(ctx, "plugin_id", None) pairing_id = getattr(ctx, "pairing_id", None) dm_decision = getattr(ctx, "dm_decision", None) channel_format_spec = getattr(ctx, "channel_format_spec", None) channel_context_note = getattr(ctx, "channel_context_note", None) if channel_type is not None: fields["channel_type"] = channel_type if account_id is not None: fields["account_id"] = account_id if agent_run_id is not None: fields["agent_run_id"] = agent_run_id if plugin_id is not None: fields["plugin_id"] = plugin_id if pairing_id is not None: fields["pairing_id"] = pairing_id if dm_decision is not None: fields["dm_decision"] = dm_decision.value if channel_format_spec is not None: fields["channel_format_spec"] = channel_format_spec if channel_context_note is not None: fields["channel_context_note"] = channel_context_note # 派生 channel_id,等价于 ${channel_type}:${account_id} if channel_type is not None and account_id is not None: fields["channel_id"] = f"{channel_type}:{account_id}" if not ok: fields["error_type"] = type(err).__name__ if err else None try: if ok: await self.logger.info( f"stage executed: {stage_id}", **fields, ) else: await self.logger.warn( f"stage failed: {stage_id}", **fields, ) except Exception as e: sys.stderr.write(f"[trace_id={trace_id}] stage logging failed: {stage_id}: {e}\n") async def _runCompensate( self, ctx: Any, compensate_name: str | None, original_error: Error | None, ) -> Error | None: """执行补偿阶段。 查找补偿阶段并执行。补偿阶段自身的异常 **不** 覆盖原错误,仅通过 日志端口记录(若注入),保留原始失败原因(§3.1 补偿执行约束)。 参数: ctx: 管道上下文。 compensate_name: 补偿阶段名称,None 表示无补偿。 original_error: 原始失败错误。 返回: 原始失败错误(补偿不改变管道失败状态)。 """ if compensate_name is None: return original_error compensate_stage = self._findStage(compensate_name) if compensate_stage is None: return original_error try: await compensate_stage.process(ctx) except Exception as e: trace_id = getattr(ctx, "trace_id", None) if self.logger is not None: try: await self.logger.error( f"compensate stage {compensate_name} failed: {e}", trace_id=trace_id, compensate_stage=compensate_name, ) except Exception: sys.stderr.write(f"[trace_id={trace_id}] compensate stage {compensate_name} failed: {e}\n") else: sys.stderr.write(f"[trace_id={trace_id}] compensate stage {compensate_name} failed: {e}\n") return original_error def _findStage(self, stage_id: str) -> AppStage | None: """按 id 查找阶段。 参数: stage_id: 阶段唯一标识。 返回: 匹配的阶段实例,未找到时返回 None。 """ for stage in self.stages: if stage.id == stage_id: return stage return None