Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -4077,6 +4077,15 @@ msgstr ""
"Eine erfolgreiche Bereitstellung muss warten, bis ros_deploy "
"CREATE_COMPLETE zurückgibt."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"A failed deployment must report the ROS status_reason to the user and "
"then retry, roll back, or return to candidate selection."
msgstr ""
"Eine fehlgeschlagene Bereitstellung muss den ROS-status_reason an den "
"Benutzer melden und danach erneut versuchen, zurückrollen oder zur "
"Kandidatenauswahl zurückkehren."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"Every explicit user hard constraint must be covered by a satisfied check "
Expand Down Expand Up @@ -4603,6 +4612,15 @@ msgstr "Der Template-Dateipfad muss relativ zum Arbeitsverzeichnis sein"
msgid "Template file path cannot escape the working directory"
msgstr "Der Template-Dateipfad darf das Arbeitsverzeichnis nicht verlassen"

#: src/iac_code/pipeline/engine/step_executor.py
msgid "no failure reason reported"
msgstr "kein Fehlergrund gemeldet"

#: src/iac_code/pipeline/engine/step_executor.py
#, python-brace-format
msgid "Step {step_id} reported a failed conclusion: {reason}"
msgstr "Schritt {step_id} hat ein fehlgeschlagenes Ergebnis gemeldet: {reason}"

#: src/iac_code/pipeline/engine/user_input.py
msgid "[Image input]"
msgstr "[Bildeingabe]"
Expand Down
17 changes: 17 additions & 0 deletions src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -4055,6 +4055,14 @@ msgstr ""
"Un despliegue correcto debe esperar hasta que ros_deploy devuelva "
"CREATE_COMPLETE."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"A failed deployment must report the ROS status_reason to the user and "
"then retry, roll back, or return to candidate selection."
msgstr ""
"Un despliegue fallido debe informar al usuario del status_reason de ROS y"
" después reintentar, revertir o volver a la selección de candidatos."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"Every explicit user hard constraint must be covered by a satisfied check "
Expand Down Expand Up @@ -4570,6 +4578,15 @@ msgstr ""
msgid "Template file path cannot escape the working directory"
msgstr "La ruta del archivo de plantilla no puede salir del directorio de trabajo"

#: src/iac_code/pipeline/engine/step_executor.py
msgid "no failure reason reported"
msgstr "no se informó ningún motivo de fallo"

#: src/iac_code/pipeline/engine/step_executor.py
#, python-brace-format
msgid "Step {step_id} reported a failed conclusion: {reason}"
msgstr "El paso {step_id} informó una conclusión fallida: {reason}"

#: src/iac_code/pipeline/engine/user_input.py
msgid "[Image input]"
msgstr "[Entrada de imagen]"
Expand Down
18 changes: 18 additions & 0 deletions src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -4063,6 +4063,15 @@ msgstr ""
"Un déploiement réussi doit attendre que ros_deploy retourne "
"CREATE_COMPLETE."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"A failed deployment must report the ROS status_reason to the user and "
"then retry, roll back, or return to candidate selection."
msgstr ""
"Un déploiement en échec doit signaler le status_reason ROS à "
"l'utilisateur, puis réessayer, revenir en arrière ou retourner à la "
"sélection des candidats."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"Every explicit user hard constraint must be covered by a satisfied check "
Expand Down Expand Up @@ -4580,6 +4589,15 @@ msgstr "Le chemin du fichier de modèle doit être relatif au répertoire de tra
msgid "Template file path cannot escape the working directory"
msgstr "Le chemin du fichier de modèle ne peut pas sortir du répertoire de travail"

#: src/iac_code/pipeline/engine/step_executor.py
msgid "no failure reason reported"
msgstr "aucune raison d'échec signalée"

#: src/iac_code/pipeline/engine/step_executor.py
#, python-brace-format
msgid "Step {step_id} reported a failed conclusion: {reason}"
msgstr "L'étape {step_id} a signalé une conclusion en échec : {reason}"

#: src/iac_code/pipeline/engine/user_input.py
msgid "[Image input]"
msgstr "[Entrée d'image]"
Expand Down
15 changes: 15 additions & 0 deletions src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -3868,6 +3868,12 @@ msgid ""
"CREATE_COMPLETE."
msgstr "デプロイ成功には、ros_deploy が CREATE_COMPLETE を返すまで待つ必要があります。"

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"A failed deployment must report the ROS status_reason to the user and "
"then retry, roll back, or return to candidate selection."
msgstr "デプロイ失敗時は ROS の status_reason をユーザーに報告し、その後リトライ、ロールバック、または候補選択に戻る必要があります。"

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"Every explicit user hard constraint must be covered by a satisfied check "
Expand Down Expand Up @@ -4335,6 +4341,15 @@ msgstr "テンプレートファイルのパスは作業ディレクトリから
msgid "Template file path cannot escape the working directory"
msgstr "テンプレートファイルのパスは作業ディレクトリの外に出られません"

#: src/iac_code/pipeline/engine/step_executor.py
msgid "no failure reason reported"
msgstr "失敗理由が報告されていません"

#: src/iac_code/pipeline/engine/step_executor.py
#, python-brace-format
msgid "Step {step_id} reported a failed conclusion: {reason}"
msgstr "ステップ {step_id} が失敗の結論を報告しました: {reason}"

#: src/iac_code/pipeline/engine/user_input.py
msgid "[Image input]"
msgstr "[画像入力]"
Expand Down
17 changes: 17 additions & 0 deletions src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -4036,6 +4036,14 @@ msgstr ""
"Uma implantação bem-sucedida deve aguardar até que ros_deploy retorne "
"CREATE_COMPLETE."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"A failed deployment must report the ROS status_reason to the user and "
"then retry, roll back, or return to candidate selection."
msgstr ""
"Uma implantação com falha deve informar o status_reason do ROS ao usuário"
" e depois tentar novamente, reverter ou voltar à seleção de candidatos."

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"Every explicit user hard constraint must be covered by a satisfied check "
Expand Down Expand Up @@ -4544,6 +4552,15 @@ msgstr ""
msgid "Template file path cannot escape the working directory"
msgstr "O caminho do arquivo de template não pode sair do diretório de trabalho"

#: src/iac_code/pipeline/engine/step_executor.py
msgid "no failure reason reported"
msgstr "nenhum motivo de falha informado"

#: src/iac_code/pipeline/engine/step_executor.py
#, python-brace-format
msgid "Step {step_id} reported a failed conclusion: {reason}"
msgstr "A etapa {step_id} informou uma conclusão com falha: {reason}"

#: src/iac_code/pipeline/engine/user_input.py
msgid "[Image input]"
msgstr "[Entrada de imagem]"
Expand Down
15 changes: 15 additions & 0 deletions src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -3832,6 +3832,12 @@ msgid ""
"CREATE_COMPLETE."
msgstr "部署成功必须等待 ros_deploy 返回 CREATE_COMPLETE。"

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"A failed deployment must report the ROS status_reason to the user and "
"then retry, roll back, or return to candidate selection."
msgstr "部署失败必须向用户报告 ROS 返回的 status_reason,然后重试、回滚或返回方案选择。"

#: src/iac_code/pipeline/engine/complete_step_tool.py
msgid ""
"Every explicit user hard constraint must be covered by a satisfied check "
Expand Down Expand Up @@ -4280,6 +4286,15 @@ msgstr "模板文件路径必须是相对于工作目录的路径"
msgid "Template file path cannot escape the working directory"
msgstr "模板文件路径不能跳出工作目录"

#: src/iac_code/pipeline/engine/step_executor.py
msgid "no failure reason reported"
msgstr "未报告失败原因"

#: src/iac_code/pipeline/engine/step_executor.py
#, python-brace-format
msgid "Step {step_id} reported a failed conclusion: {reason}"
msgstr "步骤 {step_id} 提交了失败结论:{reason}"

#: src/iac_code/pipeline/engine/user_input.py
msgid "[Image input]"
msgstr "[图片输入]"
Expand Down
8 changes: 8 additions & 0 deletions src/iac_code/pipeline/engine/complete_step_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"or confirm that it should not be handled for now."
),
"deploy_wait_create_complete": ("A successful deployment must wait until ros_deploy returns CREATE_COMPLETE."),
"deploy_report_failure_reason": (
"A failed deployment must report the ROS status_reason to the user and then retry, "
"roll back, or return to candidate selection."
),
"hard_constraint_verification_required": (
"Every explicit user hard constraint must be covered by a satisfied check with matching parameters "
"and evidence."
Expand Down Expand Up @@ -96,6 +100,10 @@ def _completion_guard_message_i18n_markers() -> tuple[str, ...]:
"or confirm that it should not be handled for now."
),
_("A successful deployment must wait until ros_deploy returns CREATE_COMPLETE."),
_(
"A failed deployment must report the ROS status_reason to the user and then retry, "
"roll back, or return to candidate selection."
),
_(
"Every explicit user hard constraint must be covered by a satisfied check with matching parameters "
"and evidence."
Expand Down
26 changes: 26 additions & 0 deletions src/iac_code/pipeline/engine/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def _parse_steps(raw_steps: list[dict]) -> list[StepSpec]:
completion_guards=_parse_completion_guards(raw.get("completion_guards"), step_id),
description=raw.get("description", ""),
exit_condition=_parse_exit_condition(raw.get("exit_condition"), step_id),
failure_condition=_parse_failure_condition(raw.get("failure_condition"), step_id),
a2a_artifacts=_parse_a2a_artifacts(raw.get("a2a_artifacts"), step_id),
surface_overrides=_parse_surface_overrides(raw.get("surface_overrides"), step_id),
config=_parse_mapping(raw.get("config"), "config", step_id),
Expand Down Expand Up @@ -410,6 +411,31 @@ def _parse_exit_condition(raw: dict | None, step_id: str) -> dict | None:
return raw


def _parse_failure_condition(raw: dict | None, step_id: str) -> dict | None:
if raw is None:
return None
if not isinstance(raw, dict) or "field" not in raw or "value" not in raw:
raise ValueError(
f"Step '{step_id}': failure_condition must be a dict with 'field' and 'value' keys, got {raw!r}"
)
unsupported = set(raw) - {"field", "value", "reason_fields"}
if unsupported:
unknown = ", ".join(sorted(str(key) for key in unsupported))
raise ValueError(
f"Step '{step_id}': failure_condition has unsupported keys: {unknown}; "
"supported: field, reason_fields, value"
)
reason_fields = raw.get("reason_fields")
if reason_fields is not None and (
not isinstance(reason_fields, list) or not all(isinstance(name, str) and name for name in reason_fields)
):
raise ValueError(
f"Step '{step_id}': failure_condition.reason_fields must be a list of non-empty strings, "
f"got {reason_fields!r}"
)
return raw


def _filter_and_relink(steps: list[StepSpec], feature_flags: dict[str, bool]) -> list[StepSpec]:
"""Remove disabled steps and fix forward links."""
enabled = [s for s in steps if _is_enabled(s, feature_flags)]
Expand Down
40 changes: 34 additions & 6 deletions src/iac_code/pipeline/engine/step_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from iac_code.agent.message import ContentBlock, Message
from iac_code.agent.system_prompt import SECTION_BUILDERS, build_base_sections
from iac_code.i18n import _
from iac_code.mcp.prompt_dispatch import mcp_prompt_command_stream
from iac_code.pipeline.engine.complete_step_tool import CompleteStepTool
from iac_code.pipeline.engine.completion_guard_state import (
Expand Down Expand Up @@ -79,6 +80,28 @@ def _completion_guard_tool_result_content(event: ToolResultEvent) -> str:
return event.result


def _failure_error_from_conclusion(step: StepSpec, conclusion: Any) -> str | None:
"""Return a user-facing error when the conclusion matches the step failure condition."""
condition = step.failure_condition
if not condition or not isinstance(conclusion, dict):
return None

expected = condition.get("value")
actual = conclusion.get(str(condition.get("field", "")))
matched = actual is expected if isinstance(expected, bool) else actual == expected
if not matched:
return None

reason_fields = condition.get("reason_fields") or []
reasons = []
for name in reason_fields:
value = conclusion.get(name)
if isinstance(value, str) and value.strip():
reasons.append(f"{name}={value.strip()}")
reason = "; ".join(reasons) if reasons else _("no failure reason reported")
return _("Step {step_id} reported a failed conclusion: {reason}").format(step_id=step.step_id, reason=reason)


@dataclass
class StepAgentLoopContext:
"""AgentLoop context built by the same path used for step execution."""
Expand Down Expand Up @@ -430,15 +453,17 @@ async def consume_complete_step_events(
conclusion = self._merge_preserved_candidate_selection(preserved_selection, conclusion)
rollback = complete_step_input.get("rollback_request")
rollback_tuple = (rollback["target_step"], rollback["reason"]) if rollback else None
context.set_conclusion(step.conclusion_field, conclusion)
if step.on_exit:
step.on_exit(context, conclusion)
failure_error = None if rollback_tuple else _failure_error_from_conclusion(step, conclusion)
step_result = StepResult(
step_id=step.step_id,
status=StepStatus.COMPLETED,
status=StepStatus.FAILED if failure_error else StepStatus.COMPLETED,
conclusion=conclusion,
rollback_request=rollback_tuple,
error=failure_error,
)
context.set_conclusion(step.conclusion_field, conclusion)
if step.on_exit:
step.on_exit(context, conclusion)
else:
step_result = StepResult(
step_id=step.step_id,
Expand Down Expand Up @@ -610,11 +635,14 @@ def _restore_completed_step_result(
if isinstance(rollback, dict) and rollback.get("target_step") and rollback.get("reason"):
rollback_tuple = (str(rollback["target_step"]), str(rollback["reason"]))

conclusion = conclusion if isinstance(conclusion, dict) else {}
failure_error = None if rollback_tuple else _failure_error_from_conclusion(step, conclusion)
return StepResult(
step_id=step.step_id,
status=StepStatus.COMPLETED,
conclusion=conclusion if isinstance(conclusion, dict) else {},
status=StepStatus.FAILED if failure_error else StepStatus.COMPLETED,
conclusion=conclusion,
rollback_request=rollback_tuple,
error=failure_error,
)

def _build_full_system_prompt(self, step: StepSpec, context: PipelineContext) -> str:
Expand Down
1 change: 1 addition & 0 deletions src/iac_code/pipeline/engine/step_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class StepSpec:
completion_guards: list[dict] = field(default_factory=list)
description: str = ""
exit_condition: dict | None = None
failure_condition: dict | None = None
a2a_artifacts: list[A2AArtifactSpec] = field(default_factory=list)
surface_overrides: dict[str, StepSurfaceOverride] = field(default_factory=dict)
config: dict[str, Any] = field(default_factory=dict)
Expand Down
13 changes: 13 additions & 0 deletions src/iac_code/pipeline/selling/hooks/deploying.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,19 @@ def on_enter(ctx: PipelineContext) -> None:
ctx.set_conclusion("selected_plan", normalized)


def on_exit(ctx: PipelineContext, conclusion: dict[str, Any]) -> None:
"""Make sure a failed deployment conclusion always carries a ROS failure reason."""
_ = ctx
if not isinstance(conclusion, dict) or conclusion.get("status") != "failed":
return
status_reason = conclusion.get("status_reason")
if isinstance(status_reason, str) and status_reason.strip():
return
error = conclusion.get("error")
if isinstance(error, str) and error.strip():
conclusion["status_reason"] = error.strip()


def on_resource_observed(
ctx: PipelineContext,
event: ResourceObservedEvent,
Expand Down
8 changes: 8 additions & 0 deletions src/iac_code/pipeline/selling/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,10 @@ steps:
interrupt_judge_failure: pause
context_fields: [intent, selected_plan, evaluated_candidates]
hooks_file: hooks/deploying.py
failure_condition:
field: status
value: failed
reason_fields: [status_reason, error]
completion_guards:
- when_conclusion_field_equals:
status: success
Expand All @@ -600,6 +604,10 @@ steps:
status_in: [CREATE_COMPLETE]
match_conclusion_field: stack_id
message_key: deploy_wait_create_complete
- when_conclusion_field_equals:
status: failed
required_conclusion_field: status_reason
message_key: deploy_report_failure_reason
tools:
include: []
exclude:
Expand Down
5 changes: 3 additions & 2 deletions src/iac_code/pipeline/selling/prompts/deploying.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@

## 错误处理
- 模板校验失败 → 就地修复模板后重试(最多 5 轮)
- 部署失败或等待超时 → 按技能的参数补全与 `ros_deploy` 恢复策略处理
- 架构层面必须变更(如产品组合不可行)→ rollback_request 到 `architecture_planning`
- 部署失败或等待超时 → 先把 `ros_deploy` 结果中的 `status` 与 `status_reason` 原文报告给用户,再按技能的参数补全与 `ros_deploy` 恢复策略重试
- 恢复动作用尽仍不能创建成功 → 用 rollback_request 回到 `confirm_and_select` 重新选择方案/参数,或在架构层面必须变更(如产品组合不可行)时回到 `architecture_planning`
- 确实无法恢复时才提交 `status: failed`,并同时填写 `status_reason`(ROS 原文)与 `error`(结论摘要);不得用 `status: success` 或省略 `status_reason` 掩盖失败

## 注意事项
- 不要读取项目文件或记忆,所需的上下文已在上方提供。
Loading
Loading