From abeeccfcc8af40b3d8e6e8fff488220a145e0628 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Nov 2026 15:02:22 +0000 Subject: [PATCH] fix(mcp): name the unfinished-run tick fuse as loop_fuse Standing instructions and the fuse redirect_reason now match the #20 envelope, so a host can read loop_fuse without treating the bare code as a next_action or looking for it on error. Co-authored-by: Steper Lin --- AGENTS.md | 11 ++- CHANGELOG.md | 1 + DEBUG.md | 11 +++ README.md | 2 +- README.zh.md | 2 +- examples/swarm-quickstart/README.md | 2 +- src/evolver/mcp_server.py | 23 ++++-- src/evolver/swarm.py | 76 +++++++++++++------ .../test_mcp_host_usability_pending_smoke.py | 56 ++++++++++++++ tests/test_mcp_protocol.py | 5 ++ tests/test_mcp_server.py | 4 + 11 files changed, 156 insertions(+), 37 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ef14103..da1e5c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -405,9 +405,14 @@ boot 自动 ack 本 agent 的 `swarm.hello`。安装或拉代码后须重启 MCP Parent。此时 `boot_once` 为空。忽略 `next_action` 再调 `swarm_tick` 时默认不启新周期。 第一次 `ok=true`、`severity=soft`、`cycle_started=false`、`progress=false`、 `redirect_count=1`,`next_action` 仍是 `swarm_solidify`,不改 `last_run`。 -同一条 run 再 tick 返回 `ok=false`、`loop_fuse=pending_redirect_loop`(`severity=soft`, -`retryable=false`,没有 `error` 字段),仍不启周期;这不是门禁失败,也不是放弃, -软缺口留在 `warning`。两次返回都带 `preserve_pending=true`、`abandon_run=false`、 +同一条 run 再 tick 返回 `ok=false`、`loop_fuse=pending_redirect_loop`、 +`redirect_reason=pending_redirect_loop`(`severity=soft`,`retryable=false`, +没有 `error` 字段),仍不启周期;这不是门禁失败,也不是放弃,也不是另一个 +`next_action`。`next_action` 仍是 `swarm_solidify`。`dispatch_reason` 仍是 +`pending_solidify`。第一次重定向的 `redirect_reason` 仍是 `pending_solidify`, +没有 `loop_fuse`。常驻 instructions 写 `loop_fuse=pending_redirect_loop`, +不把裸码当成 `next_action`。软缺口留在 `warning`。两次返回都带 +`preserve_pending=true`、`abandon_run=false`、 `action_required=swarm_solidify`(boot/status 在仍要固化时同样带上)。有软缺口时, 第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true, 没有 `error`;`tick_again=false` 表示不要再 tick)。两次 tick 都带 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5b4fa..acf87ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **README 对齐软默认与 MCP 口味(英/中)**:soft-accept 为 MCP 默认路径(经验写入 `~/.evolver/workspaces//`,不自动 git commit,不写 Parent baseline;`no_baseline` / `pack_absent` / `unmeasured` / `drop` / `flat` / `regression` 为警告)。硬阻断仍为密钥、`~/.evolver/`、`val_seal`。补开箱 `setup` / `doctor`、soft T0、前 5 tick 核心循环、`pending_solidify`、安装后重启 MCP,以及本 fork 的 clone URL(`linkerlin/evolver.py`)。顺手修正仍写「缺基线即停 / 过门即 commit / drop 仍硬拒绝」的 quickstart、AGENTS.md 与教程句子。 ### Added +- **MCP 宿主可用性:保险丝码写在 `loop_fuse` 与 `redirect_reason` 上**:常驻 instructions 把裸 `pending_redirect_loop` 改成 `loop_fuse=pending_redirect_loop`,不再把它写成 `next_action` 的并列值,长度仍小于 1200。同一条未完成 run 再 tick 时 `redirect_reason=pending_redirect_loop`(无软缺口与有软缺口两条路径都写),`dispatch_reason` 仍是 `pending_solidify`,`next_action` 仍是 `swarm_solidify`,`ok=false`,没有 `error` 字段。第一次重定向的 `redirect_reason` 仍是 `pending_solidify`,没有 `loop_fuse`。只匹配 `error == pending_redirect_loop` 的宿主仍会错过;读 `loop_fuse` 或保险丝 `redirect_reason` 的宿主会去固化。不写 Parent baseline,不放宽固化门。DEBUG #61 的目录缓存没有做假修复。 - **MCP 宿主可用性:未完成 run 的 tick 重定向不再用失败字段表示停 tick**:软缺口上的两次默认 `swarm_tick` 不再写 `gate_soft_continue=false`,这个键缺席。缺席不是 true(只在 true 时再 tick 的宿主会停),也不是 false(只把严格 false 当成整段会话失败、且不读 `session_not_failed` 的宿主不会结束会话)。`tick_again=false` 仍表示不要再 tick。同一条 run 再 tick 时 `ok=false`,保险丝码改在 `loop_fuse=pending_redirect_loop`,`error` 字段不出现。只看 `ok` 的宿主会停;把任意 `error` 当放弃的宿主不会丢掉这条 run。`session_not_failed` 与 preserve 三元组仍在。boot / status 的 `gate_soft_continue` 仍为 true。不写 Parent baseline,不放宽固化门。 - **MCP 宿主可用性:未完成 run 的 tick 重定向标明会话没有失败**:两次默认 `swarm_tick`(第一次软重定向,以及 `error=pending_redirect_loop`)都带 `session_not_failed=true`。先读这个字段,再读 `gate_soft_continue` 和 `error`。true 表示会话没有失败:`gate_soft_continue=false` 只停 tick;保险丝上的 `pending_redirect_loop` 不放弃未完成 run,preserve 三元组仍指向 `swarm_solidify`。`ok` / `error` / `gate_soft_continue` 的原值不动。boot / status 不带这个字段。不写 Parent baseline,不放宽固化门。 - **MCP 宿主可用性:软缺口的第一次 tick 重定向不再邀请再 tick**:未完成 run 上带软缺口的 `swarm_tick`,第一次就把 `gate_soft_continue` 写成 false。`ok` 仍为 true,没有 `error`,`severity` 仍为 soft,`next_action` 仍是 `swarm_solidify`。false 只表示不要再 tick,会话没有失败,软缺口留在 `warning`。`next_steps` 第一条改为现在调用 `swarm_solidify`,不再写「再 swarm_tick」。boot / status 的 `gate_soft_continue` 仍为 true。只看 `ok` 的宿主再 tick 仍会碰到 `pending_redirect_loop`。不写 Parent baseline,不放宽固化门。 diff --git a/DEBUG.md b/DEBUG.md index 06899f1..dab8d48 100644 --- a/DEBUG.md +++ b/DEBUG.md @@ -1185,3 +1185,14 @@ - **根因**:`gate_soft_continue=true` 的原义是「软缺口,会话继续」;`error` 是硬停字段(`apply_soft_gap` 写明软缺口不得进 `error`)。#17–#19 把「不要再 tick」写进这两个失败形字段,再请宿主先读新旗标。不读新旗标的宿主按原义处理。 - **修复**:未完成 run 的 tick 重定向(第一次和保险丝)在软缺口上删掉 `gate_soft_continue`,不写成 false,也不留 true。缺键不是 true,只按 true 循环的宿主不再 tick;缺键不是 false,只把严格 false 当会话失败的宿主不会结束会话,哪怕它不读 `session_not_failed`。`tick_again=false` 仍表示不要再 tick。保险丝码从 `error` 挪到 `loop_fuse=pending_redirect_loop`,`error` 字段不出现,`ok` 仍为 false。只看 `ok` 的宿主会停;只因 `error` 放弃的宿主不会丢掉 run。`session_not_failed` 与 preserve 三元组仍在。boot / status 的 `gate_soft_continue` 仍为 true,且不带 `session_not_failed`。不写 Parent baseline,不放宽固化门。目录缓存(DEBUG #61)未做假修复。 - **经验**:**停 tick 不能借用失败字段。** `false` 写在名字像「继续」的旗标上,不读旁注的宿主会把 false 读成整段停。`error` 是硬停槽,保险丝码放进去就会被当成放弃。缺键同时躲开「只在 true 时 tick」和「只在 false 时判死」。仍把缺键当成「继续 tick」的宿主,以及把 `ok=false` 本身当成整段失败的宿主,这次覆盖不到。 + +### 68. 常驻说明把裸码当成 `next_action`,保险丝的 `redirect_reason` 仍写「待固化」(MCP 易用性,已修) + +- **症状**:#67 之后,保险丝码在 `loop_fuse=pending_redirect_loop`,`error` 字段不出现,`next_action` 仍是 `swarm_solidify`。连上时的常驻 instructions 仍写 `next_action=swarm_solidify or pending_redirect_loop`。只匹配 `error == pending_redirect_loop` 的宿主不再看见这个码,会跳过 `swarm_solidify`。把裸 `pending_redirect_loop` 当成 `next_action` 并列值的宿主也对不上 #20 的信封。保险丝返回的 `redirect_reason` 仍是 `pending_solidify`,机器读者没有一个非 `error` 的原因字段与保险丝码对齐。 +- **根因**:#20 把码从 `error` 挪到 `loop_fuse`,常驻那一句和 `redirect_reason` 留在旧形状上。裸码没有字段名,读起来像另一个 `next_action`。 +- **修复**:常驻 instructions 改成 `loop_fuse=pending_redirect_loop`,不写 `error=pending_redirect_loop`,也不把裸码当成 `next_action`,长度仍小于 1200。保险丝(无软缺口的早返回,以及有软缺口的返回)把 `redirect_reason` 写成 `pending_redirect_loop`。`dispatch_reason` 仍是 `pending_solidify`。`loop_fuse`、`ok=false`、`error` 缺席、`session_not_failed=true`、preserve 三元组、`tick_again=false` 不动。有软缺口时仍不写 `gate_soft_continue`。第一次重定向的 `redirect_reason` 仍是 `pending_solidify`,没有 `loop_fuse`。`next_action` 仍是 `swarm_solidify`。不写 Parent baseline,不放宽固化门。目录缓存(DEBUG #61)未做假修复。 +- **经验**:**字段名要和码一起出现。** 裸码会被读成 `next_action`。`error` 不能再写回这个码,否则「任意 error 即放弃」的宿主又会丢掉 run。非失败的原因字段是保险丝上的 `redirect_reason`。 +- **仍在的宿主摩擦**(#20 的 1、2、4,这次没有关): + 1. 把缺席的 `gate_soft_continue` 当成「继续 tick」(只在严格 false 时才停),并且不看 `tick_again` / `next_action` / `ok` 的宿主,仍可能再 tick。保险丝上 `ok=false` 仍能停住只看 `ok` 的宿主。 + 2. 把 `ok=false` 本身当成整段会话失败、且不读 `session_not_failed` 与 `loop_fuse` 的宿主,仍会在第二次 tick 上停掉会话。`ok=false` 留着,是因为只看 `ok` 的宿主靠它停止空转。 + 4. 宿主 `tools/list` 缓存滞后(DEBUG #61)没有做假修复。安全信号在返回体上。 diff --git a/README.md b/README.md index f5018d1..0b70f94 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ After `uv sync`, a pull, or any install, **restart the MCP server**. A long-live Live `tools/list` already publishes `swarm_tick` parameters `prompt_mode` (`full` or `short`) and `checklist_only`, with descriptions on those fields, and read-only `swarm_doctor`. A host catalog that still lists only `agent_name` and `include_prompt`, or that cannot find `swarm_doctor`, is a stale client session. Restarting the server reloads the code; start a new host session so the client reads `tools/list` again. -If a previous run is unfinished, `swarm_boot` keeps `next_action=swarm_solidify`, sets top-level `pending_solidify`, and adds `readonly_smoke` (`tools`: `swarm_doctor` and `swarm_status`, `settles_run`: false). `hint` says to solidify first. A soft gap (`no_baseline` / `pack_absent` / `unmeasured`) still attaches `warning` and `next_steps`, and it does not replace that hint or weaken the gate. A read-only smoke can call `swarm_doctor` or `swarm_status`; neither settles the run nor invents a Parent baseline. A `swarm_tick` in that state does not start a cycle. The first call stays soft (`ok` true, `severity` soft, `cycle_started` false, `progress` false, `redirect_count` 1, `next_action` still `swarm_solidify`, `last_run` unchanged). The same call again on that run returns `ok` false, `loop_fuse=pending_redirect_loop`, `severity` soft, `retryable` false, and no `error` field. It still does not start a cycle. `loop_fuse` is the tick-loop fuse, not a gate failure and not an abandon signal; a soft gap stays on `warning` and `next_steps`. Both returns set `preserve_pending` true, `abandon_run` false, and `action_required` to `swarm_solidify` (boot and status do too while solidify is still next). Do not drop the unfinished run. When a soft gap is present, `gate_soft_continue` is omitted on the first redirect and on the fuse (`tick_again` is false). Omitted is not false: the first redirect keeps `ok` true and sets no `error`, so a host that treats only a strict false as session death stays in the session and does not tick again. Both tick returns set `session_not_failed` true. Read that field before other flags: true means the session is not failed, and `loop_fuse` does not abandon the run when the preserve flags say to call `swarm_solidify`. `next_steps` starts with `swarm_solidify`. Boot and status keep `gate_soft_continue` true while a run is still unfinished, and they do not set `session_not_failed`. Pass `supersede_pending=true` to replace that run. Pause and the compaction checkpoint still refuse the cycle. +If a previous run is unfinished, `swarm_boot` keeps `next_action=swarm_solidify`, sets top-level `pending_solidify`, and adds `readonly_smoke` (`tools`: `swarm_doctor` and `swarm_status`, `settles_run`: false). `hint` says to solidify first. A soft gap (`no_baseline` / `pack_absent` / `unmeasured`) still attaches `warning` and `next_steps`, and it does not replace that hint or weaken the gate. A read-only smoke can call `swarm_doctor` or `swarm_status`; neither settles the run nor invents a Parent baseline. A `swarm_tick` in that state does not start a cycle. The first call stays soft (`ok` true, `severity` soft, `cycle_started` false, `progress` false, `redirect_count` 1, `next_action` still `swarm_solidify`, `last_run` unchanged). The same call again on that run returns `ok` false, `loop_fuse=pending_redirect_loop`, `redirect_reason=pending_redirect_loop`, `severity` soft, `retryable` false, and no `error` field. It still does not start a cycle. `loop_fuse` and that `redirect_reason` are the tick-loop fuse. The code is not an `error` and not a `next_action`. `next_action` stays `swarm_solidify`. `dispatch_reason` stays `pending_solidify`. The first redirect keeps `redirect_reason=pending_solidify` and does not set `loop_fuse`. Connect-time instructions name `loop_fuse=pending_redirect_loop` the same way. The fuse is not a gate failure and not an abandon signal; a soft gap stays on `warning` and `next_steps`. Both returns set `preserve_pending` true, `abandon_run` false, and `action_required` to `swarm_solidify` (boot and status do too while solidify is still next). Do not drop the unfinished run. When a soft gap is present, `gate_soft_continue` is omitted on the first redirect and on the fuse (`tick_again` is false). Omitted is not false: the first redirect keeps `ok` true and sets no `error`, so a host that treats only a strict false as session death stays in the session and does not tick again. Both tick returns set `session_not_failed` true. Read that field before other flags: true means the session is not failed, and `loop_fuse` does not abandon the run when the preserve flags say to call `swarm_solidify`. `next_steps` starts with `swarm_solidify`. Boot and status keep `gate_soft_continue` true while a run is still unfinished, and they do not set `session_not_failed`. Pass `supersede_pending=true` to replace that run. Pause and the compaction checkpoint still refuse the cycle. A cold `swarm_status` (this process has not called `swarm_boot`) returns `next_action=swarm_boot` and a short hint. After boot, status keeps `agent_name`, `source_revision`, and any soft `gap` / warnings. `tool_asset_search`, `tool_mailbox_poll`, `tool_cycle_timeline`, and `swarm_approvals` always return `{ok, items, hint}` — an empty match is not a blank body. `swarm_approvals` also keeps `pending` (the same rows as `items`) and `recent`. Read-only `swarm_doctor` mirrors `evolver doctor` without `--fix` (`ok`, soft warnings, command hints in `fixes`). `no_baseline` / `pack_absent` / `unmeasured` stay soft-continue. diff --git a/README.zh.md b/README.zh.md index aa82074..c4116d2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -277,7 +277,7 @@ uv run evolver migrate-state soft-accept 选用 minimal T0 探针(`determine_t0_mode` 返回 `minimal`;`skip` 也已实现)。验收钩子(`solidify_hook`)在本轮已标 `soft_accept` 时按这个深度跑。硬接受仍跑完整 T0。若结算裁决带了 skip 或 minimal 探针,warnings 里会有 `t0_skipped_soft` 或 `t0_probe=minimal`。 -`pending_solidify` 仅在 `last_run.run_id` 与 `last_solidify.run_id` 不同时为真。成功的 solidify(含 soft-accept)会写下 `last_solidify`,下一次 status 或 boot 报告 `pending_solidify: false`,开局动作回到 `swarm_tick`。仍有待固化 run 时,`boot_once` 为空,宿主先固化。此时再调 `swarm_tick` 默认不启新周期。第一次仍是软返回(`ok` 为 true,`severity=soft`,`cycle_started=false`,`progress=false`,`redirect_count=1`,`next_action` 仍是 `swarm_solidify`,不改 `last_run`)。同一条 run 再 tick 返回 `ok=false`、`loop_fuse=pending_redirect_loop`、`severity=soft`、`retryable=false`,并且没有 `error` 字段,仍然不启周期。`loop_fuse` 是 tick 空转保险丝,不是门禁失败,也不是放弃;软缺口留在 `warning` 与 `next_steps`。两次返回都带 `preserve_pending=true`、`abandon_run=false`、`action_required=swarm_solidify`(boot 与 status 在仍要固化时同样带上)。不要丢掉这条未完成 run。有软缺口时,第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 表示不要再 tick)。只把严格 false 当成整段会话失败的宿主因此不会停掉会话。两次 tick 都带 `session_not_failed=true`:先读这个字段,会话没有失败;`loop_fuse` 不在 `error` 里,preserve 三元组仍指向 `swarm_solidify`。`next_steps` 第一条是调用 `swarm_solidify`。boot 与 status 的 `gate_soft_continue` 仍为 true,且不带 `session_not_failed`。要替换这条 run 才传 `supersede_pending=true`。暂停与压缩检查点仍优先于这个参数。 +`pending_solidify` 仅在 `last_run.run_id` 与 `last_solidify.run_id` 不同时为真。成功的 solidify(含 soft-accept)会写下 `last_solidify`,下一次 status 或 boot 报告 `pending_solidify: false`,开局动作回到 `swarm_tick`。仍有待固化 run 时,`boot_once` 为空,宿主先固化。此时再调 `swarm_tick` 默认不启新周期。第一次仍是软返回(`ok` 为 true,`severity=soft`,`cycle_started=false`,`progress=false`,`redirect_count=1`,`next_action` 仍是 `swarm_solidify`,不改 `last_run`)。同一条 run 再 tick 返回 `ok=false`、`loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`、`severity=soft`、`retryable=false`,并且没有 `error` 字段,仍然不启周期。`loop_fuse` 与这个 `redirect_reason` 是 tick 空转保险丝,不是 `error`,也不是 `next_action`。`next_action` 仍是 `swarm_solidify`。`dispatch_reason` 仍是 `pending_solidify`。第一次重定向的 `redirect_reason` 仍是 `pending_solidify`,没有 `loop_fuse`。常驻 instructions 写 `loop_fuse=pending_redirect_loop`。这不是门禁失败,也不是放弃;软缺口留在 `warning` 与 `next_steps`。两次返回都带 `preserve_pending=true`、`abandon_run=false`、`action_required=swarm_solidify`(boot 与 status 在仍要固化时同样带上)。不要丢掉这条未完成 run。有软缺口时,第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 表示不要再 tick)。只把严格 false 当成整段会话失败的宿主因此不会停掉会话。两次 tick 都带 `session_not_failed=true`:先读这个字段,会话没有失败;`loop_fuse` 不在 `error` 里,preserve 三元组仍指向 `swarm_solidify`。`next_steps` 第一条是调用 `swarm_solidify`。boot 与 status 的 `gate_soft_continue` 仍为 true,且不带 `session_not_failed`。要替换这条 run 才传 `supersede_pending=true`。暂停与压缩检查点仍优先于这个参数。 下列情况仍硬停(不要重试同一次失败):diff 里的密钥或凭据、改写 `~/.evolver/` 下的引擎状态、`val_seal` 泄露。损坏的 bench 门(`gate_error`)以及验证面改动的 anchor 失败也会拒绝。没有假说时本轮仍被拒(`hypothesis_missing`),直到 `swarm_hypothesis` 成功。 diff --git a/examples/swarm-quickstart/README.md b/examples/swarm-quickstart/README.md index 8a282a1..d70dc4b 100644 --- a/examples/swarm-quickstart/README.md +++ b/examples/swarm-quickstart/README.md @@ -50,7 +50,7 @@ swarm_boot ─▶ swarm_status ─▶ swarm_hook_event(信号采集) | `claude-code.mcp.json` | Claude Code | 项目根 `.mcp.json` | | `cursor-mcp.json` | Cursor | `.cursor/mcp.json` | -连上之后,本条消息已有别的任务就做那件事。没有别的任务时宿主调用 `swarm_boot`,再用一句话说明实际状态。若只调了 `swarm_status` 且本进程还没 boot,`next_action` 是 `swarm_boot`。空的 `tool_asset_search` / `tool_mailbox_poll` / `tool_cycle_timeline` / `swarm_approvals` 返回 `{ok, items, hint}`(`swarm_approvals` 另含 `pending` 与 `recent`)。`swarm_tick` 的 `prompt_mode` 默认 `full`;短载荷须显式 `prompt_mode=short` 或 `checklist_only=true`,此时 `engine_log` 不再内联 GEP 全文。只读 `swarm_doctor` 等同 `evolver doctor`(不执行 `--fix`)。宿主工具目录若仍看不到 `prompt_mode` / `checklist_only` 或 `swarm_doctor`,那是客户端对 `tools/list` 的缓存:重启 MCP 进程后新开一次会话才会重读。有未完成 run 时 `swarm_boot` 的 `next_action` 仍是 `swarm_solidify`,`hint` 写明先固化,并带 `readonly_smoke`(`swarm_doctor` / `swarm_status` 不结算、不放宽门);软缺口留在 `warning`。此时直接 `swarm_tick` 不启新周期(第一次 `cycle_started=false`、`ok=true`、`progress=false`,不改 `last_run`;同一条 run 再 tick 得到 `ok=false`、`loop_fuse=pending_redirect_loop`,没有 `error` 字段,仍不启周期,不是门禁失败,也不是放弃;两次都带 `preserve_pending=true`、`abandon_run=false`,不要丢掉这条 run。有软缺口时第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 表示不要再 tick;两次都带 `session_not_failed=true`,先读它,会话没有失败,`loop_fuse` 不放弃这条 run;`next_steps` 第一条是 `swarm_solidify`)。boot / status 的 `gate_soft_continue` 仍为 true,且不带 `session_not_failed`)。要替换才传 `supersede_pending=true`。`boot_once` 只登记会话,不在开机时同步技能。冻结包未装或没有基线时 boot / `swarm_tick` **soft-continue**(带 warnings + next_steps,可进入循环);Accept/publish 可 soft-accept(不伪造 Parent baseline、不打开 val 密封材料)。已暂停时仍返回 `await_supervisor_resume`。用户说「停」,宿主调用 `swarm_supervise` action=pause;说「继续」,宿主转达 resume。也可以对它说「启动蜂群进化」。返回的 instrument 按步执行 `swarm_tick → 执行 → swarm_distill → swarm_hypothesis → swarm_solidify → swarm_feedback`,直到终止条件。`swarm_hypothesis` 可带 `expected_outcome` 写预计通过的检查,结算时引擎对照实测写 `prediction_delta`;上一轮输出被截断时按问题调用 `episode_inspect` 读回原文;收到压缩信号或连续 4 次固化失败时先调 `swarm_checkpoint_working` 写下 `WORKING_SESSION.md`。`dispatch_prompt` 标 **PROPOSAL REQUIRED** 时改走 `swarm_propose`,不得自由编辑。`next_action=await_human_approval` 停住:`swarm_approvals` 读给人,人决定后才 `swarm_approval_resolve`。 +连上之后,本条消息已有别的任务就做那件事。没有别的任务时宿主调用 `swarm_boot`,再用一句话说明实际状态。若只调了 `swarm_status` 且本进程还没 boot,`next_action` 是 `swarm_boot`。空的 `tool_asset_search` / `tool_mailbox_poll` / `tool_cycle_timeline` / `swarm_approvals` 返回 `{ok, items, hint}`(`swarm_approvals` 另含 `pending` 与 `recent`)。`swarm_tick` 的 `prompt_mode` 默认 `full`;短载荷须显式 `prompt_mode=short` 或 `checklist_only=true`,此时 `engine_log` 不再内联 GEP 全文。只读 `swarm_doctor` 等同 `evolver doctor`(不执行 `--fix`)。宿主工具目录若仍看不到 `prompt_mode` / `checklist_only` 或 `swarm_doctor`,那是客户端对 `tools/list` 的缓存:重启 MCP 进程后新开一次会话才会重读。有未完成 run 时 `swarm_boot` 的 `next_action` 仍是 `swarm_solidify`,`hint` 写明先固化,并带 `readonly_smoke`(`swarm_doctor` / `swarm_status` 不结算、不放宽门);软缺口留在 `warning`。此时直接 `swarm_tick` 不启新周期(第一次 `cycle_started=false`、`ok=true`、`progress=false`,不改 `last_run`;同一条 run 再 tick 得到 `ok=false`、`loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`,没有 `error` 字段,`next_action` 仍是 `swarm_solidify`(保险丝码不是 `next_action`;`dispatch_reason` 仍是 `pending_solidify`;第一次的 `redirect_reason` 仍是 `pending_solidify`),仍不启周期,不是门禁失败,也不是放弃;两次都带 `preserve_pending=true`、`abandon_run=false`,不要丢掉这条 run。有软缺口时第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 表示不要再 tick;两次都带 `session_not_failed=true`,先读它,会话没有失败,`loop_fuse` 不放弃这条 run;`next_steps` 第一条是 `swarm_solidify`)。boot / status 的 `gate_soft_continue` 仍为 true,且不带 `session_not_failed`)。要替换才传 `supersede_pending=true`。`boot_once` 只登记会话,不在开机时同步技能。冻结包未装或没有基线时 boot / `swarm_tick` **soft-continue**(带 warnings + next_steps,可进入循环);Accept/publish 可 soft-accept(不伪造 Parent baseline、不打开 val 密封材料)。已暂停时仍返回 `await_supervisor_resume`。用户说「停」,宿主调用 `swarm_supervise` action=pause;说「继续」,宿主转达 resume。也可以对它说「启动蜂群进化」。返回的 instrument 按步执行 `swarm_tick → 执行 → swarm_distill → swarm_hypothesis → swarm_solidify → swarm_feedback`,直到终止条件。`swarm_hypothesis` 可带 `expected_outcome` 写预计通过的检查,结算时引擎对照实测写 `prediction_delta`;上一轮输出被截断时按问题调用 `episode_inspect` 读回原文;收到压缩信号或连续 4 次固化失败时先调 `swarm_checkpoint_working` 写下 `WORKING_SESSION.md`。`dispatch_prompt` 标 **PROPOSAL REQUIRED** 时改走 `swarm_propose`,不得自由编辑。`next_action=await_human_approval` 停住:`swarm_approvals` 读给人,人决定后才 `swarm_approval_resolve`。 ## 开箱 / Open box(首次准备的一条命令) diff --git a/src/evolver/mcp_server.py b/src/evolver/mcp_server.py index 9e6fb6d..9701b99 100644 --- a/src/evolver/mcp_server.py +++ b/src/evolver/mcp_server.py @@ -316,8 +316,8 @@ def standing_instructions() -> str: " swarm_hypothesis → swarm_solidify → swarm_feedback (score or 0)\n" "PROPOSAL REQUIRED or proposal_required=true: swarm_propose that edit, " "no free edit.\n" - "next_action=swarm_solidify or pending_redirect_loop: call " - "swarm_solidify. session_not_failed=true: not failed. " + "next_action=swarm_solidify or loop_fuse=pending_redirect_loop: solidify. " + "session_not_failed=true: not failed. " "Do not tick or abandon. Soft gap: severity=soft.\n" "Grok Bot: /workspace root; src=/workspace/evolver-research/evolver.py; " "restart mcp after pull.\n" @@ -460,9 +460,12 @@ async def tool_swarm_tick( "tick_again=false, next_action=swarm_solidify, and last_run " "stays. A repeat on that same run returns ok=false, " "severity=soft, retryable=false, " - "loop_fuse=pending_redirect_loop, and no error field. " - "It still does not start a cycle. The fuse is not a gate " - "failure and not an abandon signal. Both returns set " + "loop_fuse=pending_redirect_loop, " + "redirect_reason=pending_redirect_loop, and no error field. " + "next_action stays swarm_solidify. The fuse code is not an " + "error and not a next_action value. It still does not start " + "a cycle. dispatch_reason stays pending_solidify. The fuse " + "is not a gate failure and not an abandon signal. Both returns set " "session_not_failed=true, preserve_pending=true, " "abandon_run=false, and action_required=swarm_solidify. " "A soft gap stays in warning. gate_soft_continue is omitted " @@ -488,9 +491,13 @@ async def tool_swarm_tick( If a run is unfinished, the first default call does not start a cycle (cycle_started=false, next_action=swarm_solidify, ok=true, severity=soft, progress=false, tick_again=false). A repeat on that run returns ok=false - and loop_fuse=pending_redirect_loop with no error field (severity=soft, - retryable=false) and still does not start a cycle. That is not a gate - failure and not an abandon signal. Both returns set + and loop_fuse=pending_redirect_loop with redirect_reason the same code + and no error field (severity=soft, retryable=false). next_action stays + swarm_solidify. The fuse code is not an error and not a next_action + value. It still does not start a cycle. dispatch_reason stays + pending_solidify. The first redirect keeps + redirect_reason=pending_solidify and sets no loop_fuse. That is not a + gate failure and not an abandon signal. Both returns set session_not_failed=true, preserve_pending=true, abandon_run=false, and action_required=swarm_solidify. Read session_not_failed before other flags. True means the session is not failed: do not drop the unfinished diff --git a/src/evolver/swarm.py b/src/evolver/swarm.py index 9022da8..892be36 100644 --- a/src/evolver/swarm.py +++ b/src/evolver/swarm.py @@ -305,9 +305,12 @@ def build_instrument_prompt(state: dict[str, Any]) -> str: 有软缺口时 `gate_soft_continue is omitted` (第一次也是,`ok` 仍为 true,没有 `error`):缺这个键不是 false, 不是失败的会话,软缺口仍在 `warning`。再 tick 才是 `ok=false`、 - `loop_fuse=pending_redirect_loop`、没有 `error` 字段、`severity=soft`、 - `retryable=false`:这不是门禁失败,也不放弃 run。preserve 三元组仍指向 - `swarm_solidify`。要换掉未完成 run 才传 + `loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`、 + 没有 `error` 字段、`next_action` 仍是 `swarm_solidify`(保险丝码不是 + `next_action`)、`severity=soft`、`retryable=false`:这不是门禁失败,也不放弃 + run。`dispatch_reason` 仍是 `pending_solidify`。第一次重定向的 + `redirect_reason` 仍是 `pending_solidify`,没有 `loop_fuse`。preserve + 三元组仍指向 `swarm_solidify`。要换掉未完成 run 才传 `supersede_pending=true`(会替换 `last_run`,不放宽固化门)。 2. 编辑 — 只改 `dispatch_prompt` 锚点所写的范围。工作区是 {workspace}。 3. `swarm_distill` — `response_text` 里要有下面这种围栏。 @@ -348,11 +351,12 @@ def build_instrument_prompt(state: dict[str, Any]) -> str: 人决定后才 `swarm_approval_resolve`。返回 `error=host_relay_blocked` 时请人执行 `evolver hitl approve` 或 `evolver hitl reject`。不要自己批准。 - 连续 3 次 `failure_mode.reasonClass` 相同:先 `swarm_report` 再停。 -- `loop_fuse=pending_redirect_loop`:`session_not_failed=true`。停止 tick,调用 - `swarm_solidify`。没有 `error` 字段。不是固化门失败,也不是整段会话失败。 - 先读 `session_not_failed`,再读 `preserve_pending=true`、`abandon_run=false`, - 不要放弃这条 run。有软缺口时第一次 tick 的 `gate_soft_continue is omitted`, - 不必再 tick 到这里。 +- `loop_fuse=pending_redirect_loop`(`redirect_reason=pending_redirect_loop`, + 不是 `error`,也不是 `next_action`):`session_not_failed=true`。停止 tick, + 调用 `swarm_solidify`。`next_action` 仍是 `swarm_solidify`。没有 `error` 字段。 + 不是固化门失败,也不是整段会话失败。先读 `session_not_failed`,再读 + `preserve_pending=true`、`abandon_run=false`,不要放弃这条 run。有软缺口时 + 第一次 tick 的 `gate_soft_continue is omitted`,不必再 tick 到这里。 `retryable=true` 的拒绝是稳态,回到步骤 1。 @@ -1027,12 +1031,14 @@ def _prefer_solidify_steps( "Omitted is not false, so the session is not failed. " "tick_again=false means do not tick. " "loop_fuse=pending_redirect_loop is not an error field. " + "redirect_reason=pending_redirect_loop is that fuse, not a next_action. " "Do not abandon the run." ) elif payload.get("session_not_failed") is True: lead += ( " session_not_failed=true: the session is not failed. " - "If loop_fuse is pending_redirect_loop, there is no error field. " + "If loop_fuse is pending_redirect_loop, redirect_reason is that same " + "code, there is no error field, and next_action stays swarm_solidify. " "Read session_not_failed and the preserve flags. Do not abandon the run." ) rewritten: list[str] = [] @@ -1059,6 +1065,7 @@ def _prefer_solidify_steps( "session_not_failed=true. " "gate_soft_continue is omitted: do not call swarm_tick. " "Call swarm_solidify. loop_fuse=pending_redirect_loop is not a failed session. " + "redirect_reason=pending_redirect_loop is that fuse, not a next_action. " "There is no error field." ) else: @@ -1098,8 +1105,9 @@ def _pending_redirect_note(count: int, closed: bool, *, gap: bool) -> str: f"{keep} " f"{omitted}" "Another swarm_tick on this same run returns " - "loop_fuse=pending_redirect_loop with ok false and no error field, " - "and still does not start a cycle. " + "loop_fuse=pending_redirect_loop, " + "redirect_reason=pending_redirect_loop, ok false, and no error field. " + "next_action stays swarm_solidify. It still does not start a cycle. " "Pass supersede_pending=true only to replace that run. " "The solidify gate is unchanged and no Parent baseline is written." ) @@ -1108,6 +1116,10 @@ def _pending_redirect_note(count: int, closed: bool, *, gap: bool) -> str: f"(redirect_count={count}, progress=false). " "ok is false so a host that only checks ok stops. " "loop_fuse=pending_redirect_loop is not a gate failure. " + "redirect_reason=pending_redirect_loop names that fuse. " + "It is not an error and not a next_action. " + "next_action stays swarm_solidify. " + "dispatch_reason stays pending_solidify. " "error is absent. severity stays soft. retryable=false. Do not tick again. " f"{keep} {omitted}" "Call swarm_solidify for this run_id. " @@ -1121,10 +1133,14 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: A tick writes a fresh ``last_run`` and would replace the run the host has not solidified. Default: do not start that cycle. The first call - keeps ``ok`` true. The same run's next call sets ``ok`` false with - ``loop_fuse=pending_redirect_loop`` and no ``error`` field, so a host - that only checks ``ok`` stops and a host that abandons on any ``error`` - keeps the run. That fuse is not a gate. Both calls set + keeps ``ok`` true and ``redirect_reason`` ``pending_solidify``. The same + run's next call sets ``ok`` false with ``loop_fuse=pending_redirect_loop``, + ``redirect_reason=pending_redirect_loop``, and no ``error`` field, so a + host that only checks ``ok`` stops, a host that reads the fuse code + solidifies, and a host that abandons on any ``error`` keeps the run. + ``next_action`` stays ``swarm_solidify``. ``dispatch_reason`` stays + ``pending_solidify``. That fuse is not a gate and not a ``next_action`` + value. Both calls set ``session_not_failed`` true, ``preserve_pending`` true, ``abandon_run`` false, and ``action_required`` to ``swarm_solidify``. Read ``session_not_failed`` before other flags. True means the session is @@ -1173,12 +1189,16 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: if closed: payload.pop("error", None) payload["loop_fuse"] = "pending_redirect_loop" + payload["redirect_reason"] = "pending_redirect_loop" payload["retryable"] = False payload["warnings"] = [ ( - "pending_redirect_loop: session_not_failed=true. " + "loop_fuse=pending_redirect_loop. " + "redirect_reason=pending_redirect_loop. " + "session_not_failed=true. " "Stop swarm_tick and call swarm_solidify. " - "loop_fuse is not an error field. " + "next_action stays swarm_solidify. " + "The fuse is not an error and not a next_action. " "Read session_not_failed and the preserve flags. " "Not a gate failure. The unfinished run is unchanged." ) @@ -1209,6 +1229,7 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: if closed: surfaced.pop("error", None) surfaced["loop_fuse"] = "pending_redirect_loop" + surfaced["redirect_reason"] = "pending_redirect_loop" surfaced["retryable"] = False else: surfaced.pop("error", None) @@ -1222,9 +1243,12 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: warnings.append(extra) if closed: loop_warn = ( - "pending_redirect_loop: session_not_failed=true. " + "loop_fuse=pending_redirect_loop. " + "redirect_reason=pending_redirect_loop. " + "session_not_failed=true. " "Stop swarm_tick and call swarm_solidify. " - "loop_fuse is not an error field. " + "next_action stays swarm_solidify. " + "The fuse is not an error and not a next_action. " "preserve_pending=true, abandon_run=false. Not a gate failure. " "gate_soft_continue is omitted. tick_again=false means do not tick again. " "The soft gap stays advisory." @@ -1292,7 +1316,11 @@ def _opening_action(state: dict[str, Any]) -> tuple[str, str]: "(`ok` 仍为 true,没有 `error`):缺这个键不是 false,不是失败的会话," "`tick_again=false` 表示不要再 tick。" "同一条 run 再 tick 得到 `loop_fuse=pending_redirect_loop`" - "(`ok=false`,没有 `error` 字段,仍不启周期,不是门禁失败)。" + "与 `redirect_reason=pending_redirect_loop`" + "(`ok=false`,没有 `error` 字段,`next_action` 仍是 `swarm_solidify`," + "保险丝码不是 `next_action`,仍不启周期,不是门禁失败)。" + "`dispatch_reason` 仍是 `pending_solidify`。" + "第一次的 `redirect_reason` 仍是 `pending_solidify`,没有 `loop_fuse`。" "`session_not_failed=true` 优先:返回带 " "`preserve_pending=true`、`abandon_run=false`,不要丢掉这条 run。" "要替换这条 run 才传 `supersede_pending=true`。" @@ -1640,9 +1668,11 @@ async def swarm_tick( An unfinished run (``pending_solidify``) makes a default tick return a soft redirect: the first call keeps ``ok`` true, ``cycle_started`` false, ``progress`` false, ``next_action`` ``swarm_solidify``. The same run's - next call returns ``ok`` false and ``loop_fuse=pending_redirect_loop`` - with no ``error`` field (``severity`` soft, ``retryable`` false). That - is not a gate failure and not an abandon signal. Both calls set + next call returns ``ok`` false, ``loop_fuse=pending_redirect_loop``, and + ``redirect_reason=pending_redirect_loop``, with no ``error`` field + (``severity`` soft, ``retryable`` false). ``next_action`` stays + ``swarm_solidify``. That is not a gate failure and not an abandon signal. + Both calls set ``session_not_failed`` true, ``preserve_pending`` true, and ``abandon_run`` false. Read ``session_not_failed`` before other flags. A soft gap omits ``gate_soft_continue`` on the first redirect and on diff --git a/tests/test_mcp_host_usability_pending_smoke.py b/tests/test_mcp_host_usability_pending_smoke.py index 359f830..a54ebb0 100644 --- a/tests/test_mcp_host_usability_pending_smoke.py +++ b/tests/test_mcp_host_usability_pending_smoke.py @@ -114,6 +114,26 @@ def _host_ends_session_on_soft_continue_false(payload: dict[str, Any]) -> bool: return payload.get("gate_soft_continue") is False +def _host_solidifies_only_if_error_is_the_fuse(payload: dict[str, Any]) -> bool: + """Solidify only when ``error`` is the old fuse code. + + ``loop_fuse``, ``redirect_reason``, and ``next_action`` are ignored. + The fuse no longer sets ``error``, so this host misses it. + """ + return payload.get("error") == "pending_redirect_loop" + + +def _host_solidifies_when_it_reads_the_fuse(payload: dict[str, Any]) -> bool: + """Solidify from ``loop_fuse`` or the fuse ``redirect_reason``. + + A bare ``pending_redirect_loop`` is not a ``next_action``. ``error`` is + not consulted. The first redirect has neither fuse field. + """ + if payload.get("loop_fuse") == "pending_redirect_loop": + return True + return payload.get("redirect_reason") == "pending_redirect_loop" + + def _host_abandons_error_before_preserve_triad(payload: dict[str, Any]) -> bool: """Any ``error`` drops the run. The preserve triad is not read by itself. @@ -196,20 +216,44 @@ def _assert_stop_is_not_failure(payload: dict[str, Any], *, closed: bool, gap: b assert _host_abandons_on_any_error(payload) is False assert _host_abandons_error_before_preserve_triad(payload) is False note = str(payload.get("note") or "") + assert payload["dispatch_reason"] == "pending_solidify" + assert payload["next_action"] == "swarm_solidify" + assert payload["next_action"] != "pending_redirect_loop" if closed: assert payload["ok"] is False assert payload["loop_fuse"] == "pending_redirect_loop" + assert payload["redirect_reason"] == "pending_redirect_loop" assert payload["retryable"] is False + assert "error" not in payload assert "loop_fuse" in note + assert "redirect_reason=pending_redirect_loop" in note assert "pending_redirect_loop" in note + assert _host_solidifies_only_if_error_is_the_fuse(payload) is False + assert _host_solidifies_when_it_reads_the_fuse(payload) is True else: assert payload["ok"] is True assert "loop_fuse" not in payload + assert payload["redirect_reason"] == "pending_solidify" + assert _host_solidifies_only_if_error_is_the_fuse(payload) is False + assert _host_solidifies_when_it_reads_the_fuse(payload) is False if gap: assert "gate_soft_continue is omitted" in note assert "not a failed session" in note.lower() +def test_standing_instructions_name_the_fuse_not_a_bare_code() -> None: + """Connect-time text names ``loop_fuse``, not a bare peer of ``next_action``.""" + from evolver.mcp_server import standing_instructions + + text = standing_instructions() + assert len(text) < 1200 + assert "loop_fuse=pending_redirect_loop" in text + assert " or pending_redirect_loop" not in text + assert "error=pending_redirect_loop" not in text + assert "next_action=pending_redirect_loop" not in text + assert "next_action=swarm_solidify or loop_fuse=pending_redirect_loop" in text + + class TestTickSchemaIsOnTheLiveList: def test_prompt_mode_and_doctor_are_listed_with_descriptions(self) -> None: from evolver.mcp_server import build_server @@ -230,6 +274,9 @@ def test_prompt_mode_and_doctor_are_listed_with_descriptions(self) -> None: assert "true" in supersede assert "pending_redirect_loop" in supersede assert "loop_fuse" in supersede + assert "redirect_reason=pending_redirect_loop" in supersede + assert "error=pending_redirect_loop" not in supersede + assert "next_action=pending_redirect_loop" not in supersede assert "preserve_pending" in supersede assert "abandon_run" in supersede assert "omitted" in supersede @@ -294,6 +341,10 @@ def test_pending_boot_keeps_solidify_and_names_a_read_only_smoke( assert status.get("gate_soft_continue") is True assert "session_not_failed" not in boot assert "session_not_failed" not in status + assert "loop_fuse" not in boot + assert "loop_fuse" not in status + assert boot.get("redirect_reason") != "pending_redirect_loop" + assert status.get("redirect_reason") != "pending_redirect_loop" assert boot.get("error") not in _SOFT_GAPS _assert_solidify_step_first(boot) _assert_solidify_step_first(status) @@ -346,8 +397,10 @@ def test_instrument_names_the_read_only_alternative_only_while_pending(self) -> assert "preserve_pending=true" in pending assert "abandon_run=false" in pending assert "loop_fuse=pending_redirect_loop" in pending + assert "redirect_reason=pending_redirect_loop" in pending assert "gate_soft_continue is omitted" in pending assert "error=pending_redirect_loop" not in pending + assert "next_action=pending_redirect_loop" not in pending assert "session_not_failed=true" in pending assert "swarm_solidify" in pending assert "await_supervisor_resume" not in pending @@ -705,6 +758,9 @@ def test_wire_tools_list_publishes_prompt_mode(tmp_path: Path) -> None: assert "cycle_started" in tick["supersede_pending"]["description"] assert "pending_redirect_loop" in tick["supersede_pending"]["description"] assert "loop_fuse" in tick["supersede_pending"]["description"] + assert "redirect_reason=pending_redirect_loop" in tick["supersede_pending"]["description"] + assert "error=pending_redirect_loop" not in tick["supersede_pending"]["description"] + assert "next_action=pending_redirect_loop" not in tick["supersede_pending"]["description"] assert "preserve_pending" in tick["supersede_pending"]["description"] assert "abandon_run" in tick["supersede_pending"]["description"] assert "omitted" in tick["supersede_pending"]["description"] diff --git a/tests/test_mcp_protocol.py b/tests/test_mcp_protocol.py index 8ec2afb..6269314 100644 --- a/tests/test_mcp_protocol.py +++ b/tests/test_mcp_protocol.py @@ -118,6 +118,11 @@ def test_instructions_carry_swarm_directive(self, client: _McpClient) -> None: assert "await_human_approval" in instructions assert "swarm_propose" in instructions assert "never retry" in instructions + assert "loop_fuse=pending_redirect_loop" in instructions + assert " or pending_redirect_loop" not in instructions + assert "error=pending_redirect_loop" not in instructions + assert "next_action=pending_redirect_loop" not in instructions + assert len(instructions) < 1200 assert "swarm_hook_event" not in instructions def test_tools_list_is_the_closed_surface(self, client: _McpClient) -> None: diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 21a79db..f798876 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -270,6 +270,10 @@ def test_instructions_are_a_short_loop(self) -> None: server = build_server() text = server.instructions or "" assert len(text) < 1200 + assert "loop_fuse=pending_redirect_loop" in text + assert " or pending_redirect_loop" not in text + assert "error=pending_redirect_loop" not in text + assert "next_action=pending_redirect_loop" not in text assert "swarm_boot" in text assert "THIS message" in text assert "never retry" in text