From 1e5387641d893022601dd29a31cbb620fbb560a7 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:35:18 +0800 Subject: [PATCH] fix(replan): preserve goal fidelity and completion evidence guidance Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../goal-vision-replan-contract-v0.md | 29 +++++++++++ loopx/control_plane/quota/turn_envelope.ts | 3 ++ .../work_items/progress_observation.py | 4 +- .../work_items/replan_semantics.ts | 11 +++- .../test_replan_host_context_projection.py | 50 ++++++++++++++++++- .../control_plane_ts/replan_semantics.test.ts | 15 ++++++ 6 files changed, 109 insertions(+), 3 deletions(-) diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index 40a7a22a59..cb4994562e 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -131,6 +131,35 @@ the same `agent_id` as the refresh run. This keeps `research-executor`, `evaluator-promoter`, and other roles from overwriting or satisfying each other's active vision. +### Replan planning guidance + +The shared `replan_action_packet.planning_guidance` carries two short Agent +instructions through full/compact quota and the host Turn envelope: + +- Preserve the requested end state under current user direction. A bounded + slice retains outstanding requirements; easier tests cannot redefine + acceptance. User-authorized scope changes, permissions, budgets and stop + conditions remain authoritative. +- Before claiming Goal achievement, check every current requirement and + deliverable against authoritative evidence of the actual current state. + Missing, stale or indirect evidence leaves completion unproven. An empty Todo + list, a passing subset or a settled replan is insufficient; blocked, exhausted + and superseded outcomes remain distinct from achievement. + +This guidance is included by default whenever a replan action packet is +projected. It is Agent judgment guidance, not a new machine-enforced acceptance +gate or a permission grant. Typed semantic outcomes, lifecycle transitions and +non-replan turns are unchanged. The existing TypeScript replan owner supplies +the text; CLI and host projections preserve it without a new setting or editor. +Transport tests prove delivery and unchanged gates, not improved model behavior. + +共享 replan action packet 默认携带两条简短指引,并在完整/精简 quota 与 +Turn envelope 中保留:不能为了容易通过测试而悄悄缩小目标;宣布 Goal 达成前, +须逐项核对当前要求与实际状态的权威证据。阶段成果保留剩余要求,证据缺失、 +过期或间接时保留未证实的缺口;阻塞、探索耗尽及被替代不等于达成。 +用户授权的范围调整、权限、预算和停止条件仍有效。这是 Agent 判断指引, +不新增机器验收门禁或配置,不改变已有语义写回规则;投影测试不代表模型效果提升。 + ### Path Delta A vision packet may include a top-level `path_delta` object; `goal_path_delta_v0` diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index c2658f4491..607e4c4749 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -257,6 +257,9 @@ function replanActionPacket(payload: JsonObject): JsonObject | null { "schema_version", "decision", "obligation_id", "uncovered_frontier", "required_outcome", "allowed_terminal", "bounded_frontier", ]); + // The typed replan owner supplies bounded instructions; do not truncate their + // authority/stop qualifiers through generic diagnostic compaction. + if (Array.isArray(source.planning_guidance)) compact.planning_guidance = source.planning_guidance; const writeback = object(source.writeback_contract); if (writeback.vision_authoring) compact.writeback_contract = writeback; return Object.keys(compact).length > 0 ? compact : null; diff --git a/loopx/control_plane/work_items/progress_observation.py b/loopx/control_plane/work_items/progress_observation.py index 764ea165ef..977559edf1 100644 --- a/loopx/control_plane/work_items/progress_observation.py +++ b/loopx/control_plane/work_items/progress_observation.py @@ -563,7 +563,8 @@ def build_replan_action_packet( "explore_result_node_refs" ), ) - writeback_contract = replan_writeback_requirements(obligation)["writeback_contract"] + requirements = replan_writeback_requirements(obligation) + writeback_contract = requirements["writeback_contract"] successor_summary = str( selected_gap_values.get("successor_summary") or "" ).strip()[:240] @@ -612,6 +613,7 @@ def build_replan_action_packet( "obligation_id": obligation.get("obligation_id"), "uncovered_frontier": context.get("uncovered_frontier"), "required_outcome": "semantic_delta", + "planning_guidance": requirements["planning_guidance"], "writeback_contract": writeback_contract, "allowed_terminal": [ ProgressResultClass.EXPLORATION_EXHAUSTED.value, diff --git a/loopx/control_plane/work_items/replan_semantics.ts b/loopx/control_plane/work_items/replan_semantics.ts index 4fc28c3a21..464adb93fe 100644 --- a/loopx/control_plane/work_items/replan_semantics.ts +++ b/loopx/control_plane/work_items/replan_semantics.ts @@ -27,6 +27,14 @@ const FRESH_PATH_DISPOSITIONS = new Set(["continue", "no_change", "replan"]); const PROGRESS_CLI_ARGS = "--progress-result-class --progress-surface-id --progress-hypothesis-id --progress-probe-kind --progress-evidence-id "; const VISION_CLI_ARGS = "--agent-vision-json ''"; +// Agent guidance only: the typed outcome/authority gates below remain the owner. +const REPLAN_PLANNING_GUIDANCE = [ + "Never shrink requested goals for easier tests. Retain unmet requirements; " + + "honor user scope, authority, budget and stops.", + "Claim achieved only with current authoritative evidence for every requirement. " + + "Empty Todos/replan closure is not proof; unproven/blocked/exhausted/superseded is not achieved.", +]; + function object(value: unknown): JsonObject { return value && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {}; } @@ -104,7 +112,8 @@ export function projectReplanSemantics(value: unknown): JsonObject { const required = requiredSemanticOutcomes(obligation); const externalReview = isExternalReview(obligation); if (request.operation === "requirements") { - return {required_any_of: required, ...writebackProjection(required, externalReview)}; + return {required_any_of: required, planning_guidance: [...REPLAN_PLANNING_GUIDANCE], + ...writebackProjection(required, externalReview)}; } if (request.operation !== "qualify") { throw new EffectRuntimeRequestError("replan semantics operation must be requirements or qualify"); diff --git a/tests/control_plane/test_replan_host_context_projection.py b/tests/control_plane/test_replan_host_context_projection.py index 967ce87b7d..e63843ab46 100644 --- a/tests/control_plane/test_replan_host_context_projection.py +++ b/tests/control_plane/test_replan_host_context_projection.py @@ -1,5 +1,18 @@ from __future__ import annotations +import copy + +import pytest + +from loopx.control_plane.quota.cli_projection import compact_quota_should_run_cli_payload +from loopx.control_plane.quota.turn_envelope import ( + build_turn_envelope, quota_action_signature_document, + turn_envelope_action_signature_document, +) +from loopx.control_plane.testing.control_plane_composition_scenarios import ( + _required_vision_replan_source, +) + from loopx.control_plane.scheduler.execution_context import ( GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) @@ -114,7 +127,8 @@ def test_quota_delivers_coverage_context_and_minimal_replan_action() -> None: "delivered_by": "quota_host_projection", } assert context["coverage_ledger"][0]["surface_id"] == "surface-existing" - assert action == { + assert len(action["planning_guidance"]) == 2 + assert {key: value for key, value in action.items() if key != "planning_guidance"} == { "schema_version": "replan_action_packet_v0", "decision": "replan_required", "obligation_id": obligation["obligation_id"], @@ -138,3 +152,37 @@ def test_manual_evidence_read_receipt_cannot_close_replan() -> None: assert payload["decision"] == "autonomous_replan_required" assert payload["autonomous_replan_obligation"]["required"] is True assert payload.get("replan_ack_feedback") is None + + +@pytest.mark.parametrize("vision_gap", [False, True]) +def test_replan_guidance_survives_cli_and_host_compaction_without_new_authority(vision_gap: bool) -> None: + source = (_required_vision_replan_source(goal_id=GOAL_ID, agent_id=AGENT_ID) + if vision_gap else _quota_payload()) + guidance = source["replan_action_packet"]["planning_guidance"] + # These are delivery assertions, not a claim that a model follows the advice. + assert len(guidance) == 2 + assert "Never shrink requested goals" in guidance[0] + assert "honor user scope, authority, budget and stops" in guidance[0] + assert "current authoritative evidence for every requirement" in guidance[1] + assert "unproven" in guidance[1] + assert "blocked/exhausted/superseded is not achieved" in guidance[1] + compact = compact_quota_should_run_cli_payload(source) + envelope = build_turn_envelope(source) + for packet in (compact, envelope): + assert packet["replan_action_packet"]["planning_guidance"] == guidance + + baseline = copy.deepcopy(source) + baseline["replan_action_packet"].pop("planning_guidance") + old_envelope = build_turn_envelope(baseline) + # Prompt advice cannot change executable actions, authority or settlement. + assert envelope["action"] == old_envelope["action"] + signature = quota_action_signature_document(source) + assert turn_envelope_action_signature_document(envelope) == signature + signature["replan_action_packet"].pop("planning_guidance") + assert signature == quota_action_signature_document(baseline) + # Preserve an existing budget warning; bounded advice adds no more than 360 bytes. + assert envelope["compaction"]["within_budget"] == old_envelope["compaction"]["within_budget"] + assert (envelope["compaction"]["envelope_json_bytes"] + - old_envelope["compaction"]["envelope_json_bytes"]) <= 360 + envelope["replan_action_packet"].pop("planning_guidance") + assert envelope["replan_action_packet"] == old_envelope["replan_action_packet"] diff --git a/tests/control_plane_ts/replan_semantics.test.ts b/tests/control_plane_ts/replan_semantics.test.ts index 6b87bcdd9c..0821050b9e 100644 --- a/tests/control_plane_ts/replan_semantics.test.ts +++ b/tests/control_plane_ts/replan_semantics.test.ts @@ -181,3 +181,18 @@ test("acceptance holds use one typed recovery policy even without a projected ou observation_delta: {delta_kinds: ["new_surface"]}}).accepted, false); } }); + + +test("planning advice cannot discharge a replan or widen source-specific exits", () => { + for (const kind of ["typed_progress_repeat", "vision_acceptance_gap", "long_todo_chain", + "external_progress_review_drift", "goal_acceptance_stale"]) { + const source = {triggers: [{kind}]}; + const projection = projectReplanSemantics({operation: "requirements", obligation: source}); + assert.equal((projection.planning_guidance as string[]).length, 2); + const refusal = projectReplanSemantics({operation: "qualify", obligation: source, + planning_guidance: projection.planning_guidance}); + assert.equal(refusal.accepted, false); + assert.deepEqual(refusal.required_any_of, requiredSemanticOutcomes(source)); + assert.equal(refusal.planning_guidance, undefined); + } +});