重试bug修复
This commit is contained in:
parent
30c6532f23
commit
1cb1b42395
@ -64,7 +64,7 @@ packaging/
|
|||||||
- 实现 `config.txt.example` 风格和 JSON 风格参数读取。
|
- 实现 `config.txt.example` 风格和 JSON 风格参数读取。
|
||||||
- 实现 fake 全局流程和完整部署流程,便于不触碰真实环境地验证 Agent 路由。
|
- 实现 fake 全局流程和完整部署流程,便于不触碰真实环境地验证 Agent 路由。
|
||||||
- 实现逐 IP 处理骨架:升级、轮询、启动、校验、日志下载。
|
- 实现逐 IP 处理骨架:升级、轮询、启动、校验、日志下载。
|
||||||
- 实现单 IP 失败后暂停并保留失败 action,修复后 `resume` 会从失败 action 重试。
|
- 实现 action 失败或审核阻断后暂停并保留当前 action,修复后 `resume` 会从当前 action 重试。
|
||||||
- 回滚已从主 workflow 中拆出,改为 chat/CLI 的显式 `rollback` 命令;旧 `confirm` 入口仅作为兼容保留。
|
- 回滚已从主 workflow 中拆出,改为 chat/CLI 的显式 `rollback` 命令;旧 `confirm` 入口仅作为兼容保留。
|
||||||
- 实现 checkpoint 自动保存和 `resume` 续跑:全局步骤、成功 IP、单 IP 已完成 action 会跳过。
|
- 实现 checkpoint 自动保存和 `resume` 续跑:全局步骤、成功 IP、单 IP 已完成 action 会跳过。
|
||||||
- 实现 LLM structured output 骨架:意图识别、参数抽取、部署计划生成。
|
- 实现 LLM structured output 骨架:意图识别、参数抽取、部署计划生成。
|
||||||
@ -82,7 +82,7 @@ packaging/
|
|||||||
- chat 在开发环境和默认发布包中都会优先启用 `rich` / `prompt_toolkit`;如果增强输入初始化失败,会自动降级到普通 `input()`。
|
- chat 在开发环境和默认发布包中都会优先启用 `rich` / `prompt_toolkit`;如果增强输入初始化失败,会自动降级到普通 `input()`。
|
||||||
- chat 执行前会归一化参数并展示实际写入脚本配置的值;`script_only` / `hybrid_node_mcp` 会提前检查 `ZIP_FILE_PATH` 是否存在。
|
- chat 执行前会归一化参数并展示实际写入脚本配置的值;`script_only` / `hybrid_node_mcp` 会提前检查 `ZIP_FILE_PATH` 是否存在。
|
||||||
- chat 执行中会播报每个 action 的开始、完成或失败;action 执行失败会停在当前 checkpoint,不再误报 LangGraph 不可用。
|
- chat 执行中会播报每个 action 的开始、完成或失败;action 执行失败会停在当前 checkpoint,不再误报 LangGraph 不可用。
|
||||||
- 每个 action 完成后都会进入一次 LLM/规则审核;如果审核建议停止,流程会暂停并给出建议,等待用户 `resume`。
|
- 每个 action 完成后都会进入一次 LLM/规则审核;只有审核通过才会把 action 记为 completed,如果审核建议停止,流程会暂停并等待用户 `resume` 重试当前 action。
|
||||||
- `--analyze-actions` 和 `llm action-analysis on` 改为只控制是否把详细审核结果写入 `events`,不再控制审核是否执行。
|
- `--analyze-actions` 和 `llm action-analysis on` 改为只控制是否把详细审核结果写入 `events`,不再控制审核是否执行。
|
||||||
- chat 会播报 action 审核开始、审核完成和审核失败,避免黑盒执行。
|
- chat 会播报 action 审核开始、审核完成和审核失败,避免黑盒执行。
|
||||||
- chat 支持执行中按 `Ctrl+C` 中断,保存 checkpoint 后再 `resume`。
|
- chat 支持执行中按 `Ctrl+C` 中断,保存 checkpoint 后再 `resume`。
|
||||||
@ -299,7 +299,7 @@ PAM> resume
|
|||||||
PAM> exit
|
PAM> exit
|
||||||
```
|
```
|
||||||
|
|
||||||
`chat` 默认仍要求在会话内显式输入 `run`,并确认参数、目标 IP 范围和最终执行后才会执行 action。输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时可直接描述部署任务,或显式使用 `analyze <需求>`。每个 action 完成后都会自动进入一次 LLM/规则审核,并播报审核开始/结束;如果审核建议停止或审核本身失败,流程会暂停并输出建议,等待用户决定是否 `resume`。逐 IP action 失败时也会暂停,修复外部环境后输入 `resume` 会从失败 action 重试;如果确实需要回滚,使用 `rollback [IP]` 显式执行。`llm test [文本]` 可测试当前 LLM client 是否可用。`--analyze-actions` 仅控制详细审核结果是否写入 `events`。执行中可按 `Ctrl+C` 中断,chat 会保存当前 checkpoint 并把流程标记为 `user_interrupted`。`set KEY=VALUE` 和 `load params <路径>` 会把更新同步到当前运行 state、`config.txt` 和 checkpoint。`chat` 也支持 `--llm-base-url` / `--llm-api-key` / `--llm-model` / `--llm-action-analysis-prompt-file`、`--mcp-config` 和 `--analyze-actions`。
|
`chat` 默认仍要求在会话内显式输入 `run`,并确认参数、目标 IP 范围和最终执行后才会执行 action。输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时可直接描述部署任务,或显式使用 `analyze <需求>`。每个 action 完成后都会自动进入一次 LLM/规则审核,并播报审核开始/结束;只有审核通过才会把 action 记为 completed;如果审核建议停止或审核本身失败,流程会暂停并输出建议,等待用户决定是否 `resume` 重试当前 action。逐 IP action 失败时也会暂停,修复外部环境后输入 `resume` 会从当前 action 重试;如果确实需要回滚,使用 `rollback [IP]` 显式执行。`llm test [文本]` 可测试当前 LLM client 是否可用。`--analyze-actions` 仅控制详细审核结果是否写入 `events`。执行中可按 `Ctrl+C` 中断,chat 会保存当前 checkpoint 并把流程标记为 `user_interrupted`。`set KEY=VALUE` 和 `load params <路径>` 会把更新同步到当前运行 state、`config.txt` 和 checkpoint。`chat` 也支持 `--llm-base-url` / `--llm-api-key` / `--llm-model` / `--llm-action-analysis-prompt-file`、`--mcp-config` 和 `--analyze-actions`。
|
||||||
|
|
||||||
## 日志
|
## 日志
|
||||||
|
|
||||||
@ -332,7 +332,7 @@ fake 完整部署流程验证:
|
|||||||
python -m pam_deploy_graph.cli run-deploy --config doc_scripts/config.txt.example --strategy fake --checkpoint runtime/checkpoints/demo.json --confirm
|
python -m pam_deploy_graph.cli run-deploy --config doc_scripts/config.txt.example --strategy fake --checkpoint runtime/checkpoints/demo.json --confirm
|
||||||
```
|
```
|
||||||
|
|
||||||
如果某个 IP 失败,流程会保存 checkpoint 并暂停;修复外部环境后可直接续跑,Agent 会从失败 action 重试:
|
如果 action 失败或审核阻断,流程会保存 checkpoint 并暂停;修复外部环境后可直接续跑,Agent 会从当前 action 重试:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pam_deploy_graph.cli resume --checkpoint runtime/checkpoints/demo.json --confirm
|
python -m pam_deploy_graph.cli resume --checkpoint runtime/checkpoints/demo.json --confirm
|
||||||
|
|||||||
@ -116,8 +116,8 @@ flowchart TD
|
|||||||
E -- 否 --> G[RuleBasedLlmClient 本地规则审核]
|
E -- 否 --> G[RuleBasedLlmClient 本地规则审核]
|
||||||
F --> H{should_continue}
|
F --> H{should_continue}
|
||||||
G --> H
|
G --> H
|
||||||
H -- true --> I[继续后续 action]
|
H -- true --> I[标记 action completed 并继续后续 action]
|
||||||
H -- false --> J[暂停流程并写入 review_context]
|
H -- false --> J[不写 completed,暂停流程并写入 review_context]
|
||||||
J --> K[chat/CLI 播报审核建议并等待 resume]
|
J --> K[chat/CLI 播报审核建议并等待 resume]
|
||||||
F --> L{是否开启 analyze-actions}
|
F --> L{是否开启 analyze-actions}
|
||||||
G --> L
|
G --> L
|
||||||
@ -129,6 +129,8 @@ flowchart TD
|
|||||||
|
|
||||||
- 每个 action 完成后都会进入一次审核,不再依赖 `--analyze-actions` 开关。
|
- 每个 action 完成后都会进入一次审核,不再依赖 `--analyze-actions` 开关。
|
||||||
- `--analyze-actions` 或 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
- `--analyze-actions` 或 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
||||||
|
- 只有 action 执行成功且审核允许继续时,才会写入 `completed_global_steps` 或 `ip_states[ip].completed_steps`。
|
||||||
|
- 如果审核建议停止或审核本身失败,当前 action 不会计入 completed,`resume` 会重试当前 action。
|
||||||
- 如果审核本身失败,也会生成“停止继续”的审核结果并暂停流程,避免黑盒继续执行。
|
- 如果审核本身失败,也会生成“停止继续”的审核结果并暂停流程,避免黑盒继续执行。
|
||||||
|
|
||||||
## 失败、显式回滚和续跑
|
## 失败、显式回滚和续跑
|
||||||
@ -136,19 +138,18 @@ flowchart TD
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A[逐 IP action 执行] --> B{action 失败或业务校验失败}
|
A[逐 IP action 执行] --> B{action 失败或业务校验失败}
|
||||||
B -- 否 --> C[记录 completed_steps 并保存 checkpoint]
|
B -- 否 --> C{LLM 审核是否允许继续}
|
||||||
C --> C1{LLM 审核是否允许继续}
|
C -- 是 --> C1[记录 completed_steps 并保存 checkpoint]
|
||||||
C1 -- 是 --> C2[继续后续 action]
|
C1 --> C2[继续后续 action]
|
||||||
C1 -- 否 --> G[保存 checkpoint 并暂停]
|
C -- 否 --> G[不记录 completed_steps,保存 checkpoint 并暂停]
|
||||||
B -- 是 --> D[记录 ip_state 为 FAILED]
|
B -- 是 --> D[记录 ip_state 为 FAILED]
|
||||||
D --> E[download-log 尽力下载日志]
|
D --> F[保存 failed_stage 和 failure_reason]
|
||||||
E --> F[保存 failed_stage 和 failure_reason]
|
|
||||||
F --> G[保存 checkpoint 并暂停]
|
F --> G[保存 checkpoint 并暂停]
|
||||||
|
|
||||||
G --> H{用户决定}
|
G --> H{用户决定}
|
||||||
H -- 修复后继续 --> I[resume 清理 paused]
|
H -- 修复后继续 --> I[resume 清理 paused]
|
||||||
I --> J[next_ip_action 返回 failed_stage]
|
I --> J[next_ip_action 返回 failed_stage]
|
||||||
J --> K[重试失败 action]
|
J --> K[重试当前 action]
|
||||||
H -- 需要回滚 --> L[rollback IP 显式执行 rollback-ip]
|
H -- 需要回滚 --> L[rollback IP 显式执行 rollback-ip]
|
||||||
L --> M{rollback 是否成功}
|
L --> M{rollback 是否成功}
|
||||||
M -- 是 --> N[标记 ROLLBACK_DONE]
|
M -- 是 --> N[标记 ROLLBACK_DONE]
|
||||||
@ -179,10 +180,11 @@ flowchart TD
|
|||||||
## checkpoint 续跑语义
|
## checkpoint 续跑语义
|
||||||
|
|
||||||
- `completed_global_steps`:全局阶段已经完成的 action 会跳过。
|
- `completed_global_steps`:全局阶段已经完成的 action 会跳过。
|
||||||
|
- `completed_global_steps` 只记录“执行成功且审核通过”的全局 action;审核阻断时不会提前写入,`resume` 会重试该 action。
|
||||||
- `ip_states[ip].status == SUCCESS`:成功 IP 会跳过。
|
- `ip_states[ip].status == SUCCESS`:成功 IP 会跳过。
|
||||||
- `ip_states[ip].rollback_status == ROLLBACK_DONE`:已显式回滚的失败 IP 会跳过,继续后续目标。
|
- `ip_states[ip].rollback_status == ROLLBACK_DONE`:已显式回滚的失败 IP 会跳过,继续后续目标。
|
||||||
- `ip_states[ip].failed_stage`:失败 IP 未回滚时,`resume` 会从该 action 重试。
|
- `ip_states[ip].failed_stage`:失败 IP 未回滚时,`resume` 会从该 action 重试。
|
||||||
- `ip_states[ip].completed_steps`:同一个 IP 已完成的 action 会跳过。
|
- `ip_states[ip].completed_steps`:同一个 IP 已完成且审核通过的 action 会跳过;审核阻断时不会提前写入,`resume` 会重试当前 action。
|
||||||
- `pending_confirmation`:仅保留为旧 checkpoint/旧 confirm 入口的兼容字段,新失败流程不再自动设置。
|
- `pending_confirmation`:仅保留为旧 checkpoint/旧 confirm 入口的兼容字段,新失败流程不再自动设置。
|
||||||
- `paused` / `pause_reason`:流程可能因 action 失败、LLM 审核阻断、用户中断、回滚失败等原因暂停;`resume` 会先清理暂停标记,再继续执行。
|
- `paused` / `pause_reason`:流程可能因 action 失败、LLM 审核阻断、用户中断、回滚失败等原因暂停;`resume` 会先清理暂停标记,再继续执行。
|
||||||
- `review_context`:保存最近一次暂停时的审核建议、失败原因、IP 和阶段,供 chat/CLI 输出给用户。
|
- `review_context`:保存最近一次暂停时的审核建议、失败原因、IP 和阶段,供 chat/CLI 输出给用户。
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
- [x] 增加参数确认和目标 IP 范围确认,不只在回滚阶段确认。
|
- [x] 增加参数确认和目标 IP 范围确认,不只在回滚阶段确认。
|
||||||
- [x] 增加 LLM/MCP 配置热加载,例如 `llm config`、`mcp config`。
|
- [x] 增加 LLM/MCP 配置热加载,例如 `llm config`、`mcp config`。
|
||||||
- [x] 增加执行中 `Ctrl+C` 中断处理:保存 checkpoint、标记 `user_interrupted`,再由 `resume` 继续。
|
- [x] 增加执行中 `Ctrl+C` 中断处理:保存 checkpoint、标记 `user_interrupted`,再由 `resume` 继续。
|
||||||
- [x] 将 chat 执行接入 action 级 LangGraph runtime;逐 IP action 失败后保存 checkpoint 并暂停,`resume` 从失败 action 重试,`rollback [IP]` 作为显式命令单独执行。
|
- [x] 将 chat 执行接入 action 级 LangGraph runtime;action 失败或审核阻断后保存 checkpoint 并暂停,`resume` 从当前 action 重试,`rollback [IP]` 作为显式命令单独执行。
|
||||||
|
|
||||||
## LLM action 后分析
|
## LLM action 后分析
|
||||||
|
|
||||||
|
|||||||
@ -70,11 +70,11 @@ cd pam-deploy-agent-linux-x86_64
|
|||||||
|
|
||||||
本次发布包对应的运行时行为也已同步到包内 `README.md`:
|
本次发布包对应的运行时行为也已同步到包内 `README.md`:
|
||||||
|
|
||||||
- 每个 action 完成后都会自动执行一次 LLM/规则审核。
|
- 每个 action 完成后都会自动执行一次 LLM/规则审核,只有审核通过才会把 action 记为 completed。
|
||||||
- `--analyze-actions` 只控制是否把详细审核结果写入 `events`。
|
- `--analyze-actions` 只控制是否把详细审核结果写入 `events`。
|
||||||
- 逐 IP action 失败后会保存 checkpoint 并暂停;修复外部环境后通过 `resume` 从失败 action 重试。
|
- action 失败或审核阻断后会保存 checkpoint 并暂停;修复外部环境后通过 `resume` 从当前 action 重试。
|
||||||
- 回滚不再属于主 workflow 自动分支;需要时使用 chat 内 `rollback [IP]` 或 CLI `rollback --checkpoint ...` 显式执行。
|
- 回滚不再属于主 workflow 自动分支;需要时使用 chat 内 `rollback [IP]` 或 CLI `rollback --checkpoint ...` 显式执行。
|
||||||
- chat 支持执行中 `Ctrl+C` 中断后保存 checkpoint,再通过 `resume` 继续。
|
- chat 支持执行中 `Ctrl+C` 中断后保存 checkpoint,再通过 `resume` 重试当前 action。
|
||||||
- chat 支持 `set KEY=VALUE` 和 `load params <路径>` 热更新当前运行任务参数。
|
- chat 支持 `set KEY=VALUE` 和 `load params <路径>` 热更新当前运行任务参数。
|
||||||
- 支持通过 `--llm-action-analysis-prompt-file` 或 chat 内 `llm config action_analysis_prompt_file=...` 自定义 action 审核提示词。
|
- 支持通过 `--llm-action-analysis-prompt-file` 或 chat 内 `llm config action_analysis_prompt_file=...` 自定义 action 审核提示词。
|
||||||
- chat 支持 `llm test [文本]` 测试当前 LLM client 是否正常加载。
|
- chat 支持 `llm test [文本]` 测试当前 LLM client 是否正常加载。
|
||||||
|
|||||||
@ -35,8 +35,8 @@ pam-deploy-agent-linux-x86_64/
|
|||||||
```
|
```
|
||||||
|
|
||||||
发布包默认会优先使用 `prompt_toolkit` 增强输入,支持更稳定的退格、历史记录和补全;如果增强输入初始化失败,会自动降级到普通 `input()`。输出仍会在可用时使用 `rich` 做更清晰的文本展示。
|
发布包默认会优先使用 `prompt_toolkit` 增强输入,支持更稳定的退格、历史记录和补全;如果增强输入初始化失败,会自动降级到普通 `input()`。输出仍会在可用时使用 `rich` 做更清晰的文本展示。
|
||||||
逐 IP action 失败后会保存 checkpoint 并暂停;修复外部环境后输入 `resume` 会从失败 action 重试。回滚不再属于主 workflow 自动分支,需要时在 chat 内输入 `rollback [IP]` 显式执行。
|
action 失败或审核阻断后会保存 checkpoint 并暂停;修复外部环境后输入 `resume` 会从当前 action 重试。回滚不再属于主 workflow 自动分支,需要时在 chat 内输入 `rollback [IP]` 显式执行。
|
||||||
chat 会在执行前归一化并展示实际写入脚本配置的参数;`script_only` / `hybrid_node_mcp` 会先检查 `ZIP_FILE_PATH` 是否存在,避免脚本运行后才用默认路径失败。执行过程中每个 action 都会输出开始、完成或失败状态;每个 action 完成后还会自动进入一次 LLM/规则审核,并播报审核开始和审核结果。
|
chat 会在执行前归一化并展示实际写入脚本配置的参数;`script_only` / `hybrid_node_mcp` 会先检查 `ZIP_FILE_PATH` 是否存在,避免脚本运行后才用默认路径失败。执行过程中每个 action 都会输出开始、完成或失败状态;每个 action 完成后还会自动进入一次 LLM/规则审核,并播报审核开始和审核结果;只有审核通过才会把 action 记为 completed。
|
||||||
|
|
||||||
## 交互式使用
|
## 交互式使用
|
||||||
|
|
||||||
@ -244,7 +244,7 @@ MCP token 获取方式与 HOME 一致,默认按 `client_credentials` POST 到
|
|||||||
- `chat` 中输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时请直接描述部署任务,或显式使用 `analyze <需求>`。
|
- `chat` 中输入 `你好`、`hello` 这类问候不会触发 LLM/结构化分析;需要分析部署需求时请直接描述部署任务,或显式使用 `analyze <需求>`。
|
||||||
- 每个 action 完成后都会自动执行一次 LLM/规则审核;`--analyze-actions` 和 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
- 每个 action 完成后都会自动执行一次 LLM/规则审核;`--analyze-actions` 和 `llm action-analysis on` 只控制是否把详细审核结果写入 `events`。
|
||||||
- `llm test [文本]` 可测试当前 LLM client 是否可用。
|
- `llm test [文本]` 可测试当前 LLM client 是否可用。
|
||||||
- 如果审核建议停止、审核本身失败,或用户在执行中按下 `Ctrl+C`,流程都会保存 checkpoint 并进入暂停状态;后续可使用 `resume` 继续。
|
- 如果审核建议停止、审核本身失败,或用户在执行中按下 `Ctrl+C`,流程都会保存 checkpoint 并进入暂停状态;后续可使用 `resume` 重试当前 action。
|
||||||
- `set KEY=VALUE` 和 `load params <路径>` 会热更新当前运行任务的参数,并回写运行中的 `config.txt` 和 checkpoint。
|
- `set KEY=VALUE` 和 `load params <路径>` 会热更新当前运行任务的参数,并回写运行中的 `config.txt` 和 checkpoint。
|
||||||
- `checkpoint` 会保存完整运行参数,请放在受控目录。
|
- `checkpoint` 会保存完整运行参数,请放在受控目录。
|
||||||
- `hybrid_node_mcp`、`resume`、`rollback` 如果需要执行 MCP action,请同时传入 `--mcp-config`。
|
- `hybrid_node_mcp`、`resume`、`rollback` 如果需要执行 MCP action,请同时传入 `--mcp-config`。
|
||||||
|
|||||||
@ -138,7 +138,8 @@ PAM 部署 Agent 解压即用包
|
|||||||
|
|
||||||
--analyze-actions
|
--analyze-actions
|
||||||
每个 action 完成后的 LLM/规则审核默认都会执行;该参数只控制
|
每个 action 完成后的 LLM/规则审核默认都会执行;该参数只控制
|
||||||
是否把详细审核结果写入 events。审核建议停止时流程会暂停。
|
是否把详细审核结果写入 events。审核建议停止时流程会暂停,
|
||||||
|
resume 会重试当前 action。
|
||||||
|
|
||||||
LLM 参数:
|
LLM 参数:
|
||||||
--llm-base-url <URL>
|
--llm-base-url <URL>
|
||||||
@ -176,7 +177,7 @@ LLM 环境变量:
|
|||||||
|
|
||||||
./run.sh run-deploy --config doc_scripts/config.txt.example --strategy fake --checkpoint runtime/checkpoints/demo.json --confirm
|
./run.sh run-deploy --config doc_scripts/config.txt.example --strategy fake --checkpoint runtime/checkpoints/demo.json --confirm
|
||||||
|
|
||||||
# 失败暂停后,修复外部环境并从失败 action 重试:
|
# 失败或审核阻断暂停后,修复外部环境并从当前 action 重试:
|
||||||
./run.sh resume --checkpoint runtime/checkpoints/demo.json --confirm
|
./run.sh resume --checkpoint runtime/checkpoints/demo.json --confirm
|
||||||
# 需要回滚失败 IP 时显式执行:
|
# 需要回滚失败 IP 时显式执行:
|
||||||
./run.sh rollback --checkpoint runtime/checkpoints/demo.json --confirm
|
./run.sh rollback --checkpoint runtime/checkpoints/demo.json --confirm
|
||||||
@ -191,7 +192,7 @@ LLM 环境变量:
|
|||||||
2. doc_scripts 只包含运行必需文件:deploy.sh、config.txt.example、PAM_AUTO_DEPLY_SKILL.md。
|
2. doc_scripts 只包含运行必需文件:deploy.sh、config.txt.example、PAM_AUTO_DEPLY_SKILL.md。
|
||||||
3. prompts/action_review.txt 是当前默认 action 审核提示词基线,可复制后自行修改。
|
3. prompts/action_review.txt 是当前默认 action 审核提示词基线,可复制后自行修改。
|
||||||
4. mcp_client.example.json 是 MCP server URL + 独立鉴权配置示例,需要按真实 MCP server 修改。
|
4. mcp_client.example.json 是 MCP server URL + 独立鉴权配置示例,需要按真实 MCP server 修改。
|
||||||
5. 逐 IP action 失败后会暂停;修复后用 resume 从失败 action 重试,需要回滚时用 rollback 显式执行。
|
5. action 失败或审核阻断后会暂停;修复后用 resume 从当前 action 重试,需要回滚时用 rollback 显式执行。
|
||||||
6. chat 会在执行前归一化并展示实际写入脚本配置的参数;script_only / hybrid_node_mcp 会先检查 ZIP_FILE_PATH 是否存在。
|
6. chat 会在执行前归一化并展示实际写入脚本配置的参数;script_only / hybrid_node_mcp 会先检查 ZIP_FILE_PATH 是否存在。
|
||||||
7. chat 执行过程中会播报每个 action 的开始、完成或失败;普通问候不会触发 LLM/结构化分析。
|
7. chat 执行过程中会播报每个 action 的开始、完成或失败;普通问候不会触发 LLM/结构化分析。
|
||||||
8. chat 内可使用 params、events、rollback、list checkpoints、load checkpoint、load params、llm config、llm test、mcp config 等命令。
|
8. chat 内可使用 params、events、rollback、list checkpoints、load checkpoint、load params、llm config、llm test、mcp config 等命令。
|
||||||
|
|||||||
@ -395,17 +395,6 @@ class PamDeployAgent:
|
|||||||
)
|
)
|
||||||
self._save_checkpoint(state)
|
self._save_checkpoint(state)
|
||||||
raise RuntimeError(message)
|
raise RuntimeError(message)
|
||||||
self._apply_result(state, action, result.values)
|
|
||||||
state.completed_global_steps.append(action)
|
|
||||||
state.last_success_step = action
|
|
||||||
self._emit_progress(
|
|
||||||
{
|
|
||||||
"type": "ACTION_DONE",
|
|
||||||
"stage": action,
|
|
||||||
"backend": result.backend,
|
|
||||||
"message": result.values.get("MESSAGE", "ok"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if analysis is not None and not analysis.should_continue:
|
if analysis is not None and not analysis.should_continue:
|
||||||
state.last_failed_step = action
|
state.last_failed_step = action
|
||||||
self.pause_state(
|
self.pause_state(
|
||||||
@ -415,6 +404,19 @@ class PamDeployAgent:
|
|||||||
)
|
)
|
||||||
logger.info("全局 action 被 LLM 审核拦截 run_id=%s action=%s analysis=%s", state.run_id, action, json_for_log(asdict(analysis)))
|
logger.info("全局 action 被 LLM 审核拦截 run_id=%s action=%s analysis=%s", state.run_id, action, json_for_log(asdict(analysis)))
|
||||||
return state
|
return state
|
||||||
|
self._apply_result(state, action, result.values)
|
||||||
|
state.completed_global_steps.append(action)
|
||||||
|
state.last_success_step = action
|
||||||
|
if state.last_failed_step == action:
|
||||||
|
state.last_failed_step = ""
|
||||||
|
self._emit_progress(
|
||||||
|
{
|
||||||
|
"type": "ACTION_DONE",
|
||||||
|
"stage": action,
|
||||||
|
"backend": result.backend,
|
||||||
|
"message": result.values.get("MESSAGE", "ok"),
|
||||||
|
}
|
||||||
|
)
|
||||||
self._save_checkpoint(state)
|
self._save_checkpoint(state)
|
||||||
logger.info("全局 action 完成 run_id=%s action=%s completed=%s", state.run_id, action, state.completed_global_steps)
|
logger.info("全局 action 完成 run_id=%s action=%s completed=%s", state.run_id, action, state.completed_global_steps)
|
||||||
return state
|
return state
|
||||||
@ -442,7 +444,7 @@ class PamDeployAgent:
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
def run_ip_flow(self, state: AgentState) -> AgentState:
|
def run_ip_flow(self, state: AgentState) -> AgentState:
|
||||||
"""执行逐 IP 部署流程,失败时暂停在失败 action,等待修复后重试。"""
|
"""执行逐 IP 部署流程,失败时暂停在当前 action,等待修复后重试。"""
|
||||||
logger.info(
|
logger.info(
|
||||||
"逐 IP 流程开始 run_id=%s paused=%s target_ips=%s online_ips=%s",
|
"逐 IP 流程开始 run_id=%s paused=%s target_ips=%s online_ips=%s",
|
||||||
state.run_id,
|
state.run_id,
|
||||||
@ -575,12 +577,21 @@ class PamDeployAgent:
|
|||||||
reason="action_failed",
|
reason="action_failed",
|
||||||
review_context=self._review_context(action=action, analysis=analysis, result=result, ip=ip),
|
review_context=self._review_context(action=action, analysis=analysis, result=result, ip=ip),
|
||||||
)
|
)
|
||||||
if action != "download-log":
|
|
||||||
self._download_log_best_effort(state, ip)
|
|
||||||
self._save_checkpoint(state)
|
self._save_checkpoint(state)
|
||||||
logger.info("IP action 失败并暂停等待重试 run_id=%s ip=%s action=%s", state.run_id, ip, action)
|
logger.info("IP action 失败并暂停等待重试 run_id=%s ip=%s action=%s", state.run_id, ip, action)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
if analysis is not None and not analysis.should_continue:
|
||||||
|
ip_state["failed_stage"] = action
|
||||||
|
ip_state["failure_reason"] = analysis.possible_reason or analysis.suggested_action or "LLM 审核要求暂停"
|
||||||
|
state.last_failed_step = action
|
||||||
|
self.pause_state(
|
||||||
|
state,
|
||||||
|
reason="llm_review_blocked",
|
||||||
|
review_context=self._review_context(action=action, analysis=analysis, result=result, ip=ip),
|
||||||
|
)
|
||||||
|
logger.info("IP action 被 LLM 审核拦截 run_id=%s ip=%s action=%s analysis=%s", state.run_id, ip, action, json_for_log(asdict(analysis)))
|
||||||
|
return state
|
||||||
self._apply_ip_result(ip_state, action, result.values)
|
self._apply_ip_result(ip_state, action, result.values)
|
||||||
ip_state["status"] = "RUNNING"
|
ip_state["status"] = "RUNNING"
|
||||||
ip_state["failed_stage"] = ""
|
ip_state["failed_stage"] = ""
|
||||||
@ -597,14 +608,6 @@ class PamDeployAgent:
|
|||||||
"message": result.values.get("MESSAGE", "ok"),
|
"message": result.values.get("MESSAGE", "ok"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if analysis is not None and not analysis.should_continue:
|
|
||||||
self.pause_state(
|
|
||||||
state,
|
|
||||||
reason="llm_review_blocked",
|
|
||||||
review_context=self._review_context(action=action, analysis=analysis, result=result, ip=ip),
|
|
||||||
)
|
|
||||||
logger.info("IP action 被 LLM 审核拦截 run_id=%s ip=%s action=%s analysis=%s", state.run_id, ip, action, json_for_log(asdict(analysis)))
|
|
||||||
return state
|
|
||||||
self._save_checkpoint(state)
|
self._save_checkpoint(state)
|
||||||
logger.info("IP action 完成 run_id=%s ip=%s action=%s completed=%s", state.run_id, ip, action, completed_steps)
|
logger.info("IP action 完成 run_id=%s ip=%s action=%s completed=%s", state.run_id, ip, action, completed_steps)
|
||||||
return state
|
return state
|
||||||
@ -905,7 +908,7 @@ class PamDeployAgent:
|
|||||||
ip_state["log_file"] = str(values.get("LOG_FILE", ""))
|
ip_state["log_file"] = str(values.get("LOG_FILE", ""))
|
||||||
|
|
||||||
def _record_ip_failure(self, state: AgentState, ip: str, action: str, reason: str) -> None:
|
def _record_ip_failure(self, state: AgentState, ip: str, action: str, reason: str) -> None:
|
||||||
"""记录单 IP 失败,并保留失败 action 供 resume 重试。"""
|
"""记录单 IP 失败,并保留当前 action 供 resume 重试。"""
|
||||||
ip_state = state.ip_states[ip]
|
ip_state = state.ip_states[ip]
|
||||||
stop_first = action in ("start-ip", "verify-ip")
|
stop_first = action in ("start-ip", "verify-ip")
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -936,73 +939,6 @@ class PamDeployAgent:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _download_log_best_effort(self, state: AgentState, ip: str) -> None:
|
|
||||||
"""失败后尽力下载日志,日志失败不覆盖原失败原因。"""
|
|
||||||
backend = state.action_backends.get("download-log", "script")
|
|
||||||
logger.info("失败后尝试下载日志 run_id=%s ip=%s backend=%s", state.run_id, ip, backend)
|
|
||||||
self._emit_progress(
|
|
||||||
{
|
|
||||||
"type": "ACTION_START",
|
|
||||||
"stage": "download-log",
|
|
||||||
"backend": backend,
|
|
||||||
"ip": ip,
|
|
||||||
"message": "失败后尝试下载日志",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
result = self.router.run_action(state, "download-log", ip=ip)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.exception("失败后下载日志 action 调用异常 run_id=%s ip=%s backend=%s", state.run_id, ip, backend)
|
|
||||||
result = ActionResult(
|
|
||||||
action="download-log",
|
|
||||||
backend=backend,
|
|
||||||
ok=False,
|
|
||||||
error_summary=str(exc),
|
|
||||||
)
|
|
||||||
ip_state = state.ip_states[ip]
|
|
||||||
if result.ok:
|
|
||||||
ip_state["log_file"] = str(result.values.get("LOG_FILE", ""))
|
|
||||||
state.events.append(
|
|
||||||
{
|
|
||||||
"type": "ACTION_DONE",
|
|
||||||
"stage": "download-log",
|
|
||||||
"backend": result.backend,
|
|
||||||
"ip": ip,
|
|
||||||
"message": "已尽力下载日志",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self._emit_progress(
|
|
||||||
{
|
|
||||||
"type": "ACTION_DONE",
|
|
||||||
"stage": "download-log",
|
|
||||||
"backend": result.backend,
|
|
||||||
"ip": ip,
|
|
||||||
"message": result.values.get("MESSAGE", "已尽力下载日志"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
logger.info("失败后下载日志完成 run_id=%s ip=%s result=%s", state.run_id, ip, _action_result_for_log(result))
|
|
||||||
else:
|
|
||||||
state.events.append(
|
|
||||||
{
|
|
||||||
"type": "ACTION_FAIL",
|
|
||||||
"stage": "download-log",
|
|
||||||
"backend": result.backend,
|
|
||||||
"ip": ip,
|
|
||||||
"message": result.error_summary or "尽力下载日志失败",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self._emit_progress(
|
|
||||||
{
|
|
||||||
"type": "ACTION_FAIL",
|
|
||||||
"stage": "download-log",
|
|
||||||
"backend": result.backend,
|
|
||||||
"ip": ip,
|
|
||||||
"message": result.error_summary or "尽力下载日志失败",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
logger.info("失败后下载日志失败 run_id=%s ip=%s result=%s", state.run_id, ip, _action_result_for_log(result))
|
|
||||||
self._append_action_analysis(state, "download-log", result, ip=ip)
|
|
||||||
|
|
||||||
def _save_checkpoint(self, state: AgentState) -> None:
|
def _save_checkpoint(self, state: AgentState) -> None:
|
||||||
"""如果配置了 checkpoint 路径,则保存完整运行状态。"""
|
"""如果配置了 checkpoint 路径,则保存完整运行状态。"""
|
||||||
if state.checkpoint_path:
|
if state.checkpoint_path:
|
||||||
|
|||||||
@ -819,7 +819,7 @@ class InteractiveCliSession:
|
|||||||
if reason == "user_interrupted":
|
if reason == "user_interrupted":
|
||||||
self.output("输入 resume 可从当前 checkpoint 继续。")
|
self.output("输入 resume 可从当前 checkpoint 继续。")
|
||||||
elif reason == "llm_review_blocked":
|
elif reason == "llm_review_blocked":
|
||||||
self.output("请根据以上建议判断后续;如需继续,输入 resume。")
|
self.output("请根据以上建议判断后续;如需继续,输入 resume 重试当前 action。")
|
||||||
elif reason == "action_failed":
|
elif reason == "action_failed":
|
||||||
ip = context.get("ip")
|
ip = context.get("ip")
|
||||||
rollback_hint = f"rollback {ip}" if ip else "rollback <IP>"
|
rollback_hint = f"rollback {ip}" if ip else "rollback <IP>"
|
||||||
|
|||||||
@ -35,6 +35,26 @@ class BlockingReviewLlmClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BlockingOnceReviewLlmClient:
|
||||||
|
def __init__(self, blocked_action: str = "get-token") -> None:
|
||||||
|
self.blocked_action = blocked_action
|
||||||
|
self.blocked = False
|
||||||
|
|
||||||
|
def analyze_action_result(self, *, action, result, state_summary):
|
||||||
|
if action == self.blocked_action and not self.blocked:
|
||||||
|
self.blocked = True
|
||||||
|
return LlmActionAnalysis(
|
||||||
|
action=action,
|
||||||
|
has_anomaly=True,
|
||||||
|
severity="high",
|
||||||
|
possible_reason="review blocked once",
|
||||||
|
suggested_action="fix then retry current action",
|
||||||
|
requires_confirmation=True,
|
||||||
|
should_continue=False,
|
||||||
|
)
|
||||||
|
return LlmActionAnalysis(action=action)
|
||||||
|
|
||||||
|
|
||||||
class BrokenReviewLlmClient:
|
class BrokenReviewLlmClient:
|
||||||
def analyze_action_result(self, *, action, result, state_summary):
|
def analyze_action_result(self, *, action, result, state_summary):
|
||||||
raise RuntimeError("review transport failed")
|
raise RuntimeError("review transport failed")
|
||||||
@ -118,6 +138,7 @@ def test_run_deploy_flow_stops_on_verify_failure(tmp_path: Path):
|
|||||||
assert state.ip_states["192.168.1.10"]["rollback_status"] == "ROLLBACK_NOT_RUN"
|
assert state.ip_states["192.168.1.10"]["rollback_status"] == "ROLLBACK_NOT_RUN"
|
||||||
assert "192.168.1.11" not in state.ip_states
|
assert "192.168.1.11" not in state.ip_states
|
||||||
assert any(event["type"] == "ACTION_RETRY_REQUIRED" for event in state.events)
|
assert any(event["type"] == "ACTION_RETRY_REQUIRED" for event in state.events)
|
||||||
|
assert not any(call[0] == "download-log" for call in fake.calls)
|
||||||
|
|
||||||
|
|
||||||
def test_resume_retries_failed_ip_action_without_rollback(tmp_path: Path):
|
def test_resume_retries_failed_ip_action_without_rollback(tmp_path: Path):
|
||||||
@ -196,11 +217,35 @@ def test_successful_action_can_be_blocked_by_llm_review(tmp_path: Path):
|
|||||||
assert state.paused is True
|
assert state.paused is True
|
||||||
assert state.pause_reason == "llm_review_blocked"
|
assert state.pause_reason == "llm_review_blocked"
|
||||||
assert state.last_failed_step == "get-token"
|
assert state.last_failed_step == "get-token"
|
||||||
assert state.completed_global_steps == ["get-token"]
|
assert state.completed_global_steps == []
|
||||||
assert state.review_context["stage"] == "get-token"
|
assert state.review_context["stage"] == "get-token"
|
||||||
assert state.review_context["suggested_action"] == "stop and inspect"
|
assert state.review_context["suggested_action"] == "stop and inspect"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resume_retries_llm_blocked_global_action(tmp_path: Path):
|
||||||
|
fake = FakeActionRunner()
|
||||||
|
agent = PamDeployAgent(
|
||||||
|
fake_runner=fake,
|
||||||
|
llm_client=BlockingOnceReviewLlmClient(),
|
||||||
|
)
|
||||||
|
state = agent.create_state(
|
||||||
|
params=PARAMS,
|
||||||
|
execution_strategy="fake",
|
||||||
|
config_path=str(tmp_path / "config.txt"),
|
||||||
|
checkpoint_path=str(tmp_path / "checkpoint.json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
agent.run_deploy_flow(state)
|
||||||
|
agent.resume_state(state)
|
||||||
|
agent.run_deploy_flow(state)
|
||||||
|
|
||||||
|
called_actions = [call[0] for call in fake.calls]
|
||||||
|
assert called_actions[:2] == ["get-token", "get-token"]
|
||||||
|
assert called_actions.count("get-token") == 2
|
||||||
|
assert state.paused is False
|
||||||
|
assert state.completed_global_steps[0] == "get-token"
|
||||||
|
|
||||||
|
|
||||||
def test_action_review_failure_pauses_flow(tmp_path: Path):
|
def test_action_review_failure_pauses_flow(tmp_path: Path):
|
||||||
agent = PamDeployAgent(
|
agent = PamDeployAgent(
|
||||||
fake_runner=FakeActionRunner(),
|
fake_runner=FakeActionRunner(),
|
||||||
@ -219,6 +264,7 @@ def test_action_review_failure_pauses_flow(tmp_path: Path):
|
|||||||
assert state.pause_reason == "llm_review_blocked"
|
assert state.pause_reason == "llm_review_blocked"
|
||||||
assert state.review_context["stage"] == "get-token"
|
assert state.review_context["stage"] == "get-token"
|
||||||
assert "LLM 审核失败" in state.review_context["possible_reason"]
|
assert "LLM 审核失败" in state.review_context["possible_reason"]
|
||||||
|
assert state.completed_global_steps == []
|
||||||
assert any(event["type"] == "ACTION_ANALYSIS_FAIL" for event in state.events)
|
assert any(event["type"] == "ACTION_ANALYSIS_FAIL" for event in state.events)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user