Skip to content

Commit e4b8d59

Browse files
authored
Merge pull request #5132 from loopx-project/codex/replan-goal-fidelity
2 parents fdec859 + 1e53876 commit e4b8d59

6 files changed

Lines changed: 109 additions & 3 deletions

File tree

‎docs/reference/protocols/goal-vision-replan-contract-v0.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,35 @@ the same `agent_id` as the refresh run. This keeps `research-executor`,
131131
`evaluator-promoter`, and other roles from overwriting or satisfying each
132132
other's active vision.
133133

134+
### Replan planning guidance
135+
136+
The shared `replan_action_packet.planning_guidance` carries two short Agent
137+
instructions through full/compact quota and the host Turn envelope:
138+
139+
- Preserve the requested end state under current user direction. A bounded
140+
slice retains outstanding requirements; easier tests cannot redefine
141+
acceptance. User-authorized scope changes, permissions, budgets and stop
142+
conditions remain authoritative.
143+
- Before claiming Goal achievement, check every current requirement and
144+
deliverable against authoritative evidence of the actual current state.
145+
Missing, stale or indirect evidence leaves completion unproven. An empty Todo
146+
list, a passing subset or a settled replan is insufficient; blocked, exhausted
147+
and superseded outcomes remain distinct from achievement.
148+
149+
This guidance is included by default whenever a replan action packet is
150+
projected. It is Agent judgment guidance, not a new machine-enforced acceptance
151+
gate or a permission grant. Typed semantic outcomes, lifecycle transitions and
152+
non-replan turns are unchanged. The existing TypeScript replan owner supplies
153+
the text; CLI and host projections preserve it without a new setting or editor.
154+
Transport tests prove delivery and unchanged gates, not improved model behavior.
155+
156+
共享 replan action packet 默认携带两条简短指引,并在完整/精简 quota 与
157+
Turn envelope 中保留:不能为了容易通过测试而悄悄缩小目标;宣布 Goal 达成前,
158+
须逐项核对当前要求与实际状态的权威证据。阶段成果保留剩余要求,证据缺失、
159+
过期或间接时保留未证实的缺口;阻塞、探索耗尽及被替代不等于达成。
160+
用户授权的范围调整、权限、预算和停止条件仍有效。这是 Agent 判断指引,
161+
不新增机器验收门禁或配置,不改变已有语义写回规则;投影测试不代表模型效果提升。
162+
134163
### Path Delta
135164

136165
A vision packet may include a top-level `path_delta` object; `goal_path_delta_v0`

‎loopx/control_plane/quota/turn_envelope.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ function replanActionPacket(payload: JsonObject): JsonObject | null {
257257
"schema_version", "decision", "obligation_id", "uncovered_frontier",
258258
"required_outcome", "allowed_terminal", "bounded_frontier",
259259
]);
260+
// The typed replan owner supplies bounded instructions; do not truncate their
261+
// authority/stop qualifiers through generic diagnostic compaction.
262+
if (Array.isArray(source.planning_guidance)) compact.planning_guidance = source.planning_guidance;
260263
const writeback = object(source.writeback_contract);
261264
if (writeback.vision_authoring) compact.writeback_contract = writeback;
262265
return Object.keys(compact).length > 0 ? compact : null;

‎loopx/control_plane/work_items/progress_observation.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,8 @@ def build_replan_action_packet(
563563
"explore_result_node_refs"
564564
),
565565
)
566-
writeback_contract = replan_writeback_requirements(obligation)["writeback_contract"]
566+
requirements = replan_writeback_requirements(obligation)
567+
writeback_contract = requirements["writeback_contract"]
567568
successor_summary = str(
568569
selected_gap_values.get("successor_summary") or ""
569570
).strip()[:240]
@@ -612,6 +613,7 @@ def build_replan_action_packet(
612613
"obligation_id": obligation.get("obligation_id"),
613614
"uncovered_frontier": context.get("uncovered_frontier"),
614615
"required_outcome": "semantic_delta",
616+
"planning_guidance": requirements["planning_guidance"],
615617
"writeback_contract": writeback_contract,
616618
"allowed_terminal": [
617619
ProgressResultClass.EXPLORATION_EXHAUSTED.value,

‎loopx/control_plane/work_items/replan_semantics.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ const FRESH_PATH_DISPOSITIONS = new Set(["continue", "no_change", "replan"]);
2727
const PROGRESS_CLI_ARGS = "--progress-result-class <advanced|blocked|exploration_exhausted|no_followup> --progress-surface-id <surface-id> --progress-hypothesis-id <hypothesis-id> --progress-probe-kind <probe-kind> --progress-evidence-id <evidence-id>";
2828
const VISION_CLI_ARGS = "--agent-vision-json '<path-to-evidence-linked-goal-vision-replan-contract-v0.json>'";
2929

30+
// Agent guidance only: the typed outcome/authority gates below remain the owner.
31+
const REPLAN_PLANNING_GUIDANCE = [
32+
"Never shrink requested goals for easier tests. Retain unmet requirements; " +
33+
"honor user scope, authority, budget and stops.",
34+
"Claim achieved only with current authoritative evidence for every requirement. " +
35+
"Empty Todos/replan closure is not proof; unproven/blocked/exhausted/superseded is not achieved.",
36+
];
37+
3038
function object(value: unknown): JsonObject {
3139
return value && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {};
3240
}
@@ -104,7 +112,8 @@ export function projectReplanSemantics(value: unknown): JsonObject {
104112
const required = requiredSemanticOutcomes(obligation);
105113
const externalReview = isExternalReview(obligation);
106114
if (request.operation === "requirements") {
107-
return {required_any_of: required, ...writebackProjection(required, externalReview)};
115+
return {required_any_of: required, planning_guidance: [...REPLAN_PLANNING_GUIDANCE],
116+
...writebackProjection(required, externalReview)};
108117
}
109118
if (request.operation !== "qualify") {
110119
throw new EffectRuntimeRequestError("replan semantics operation must be requirements or qualify");

‎tests/control_plane/test_replan_host_context_projection.py‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
from __future__ import annotations
22

3+
import copy
4+
5+
import pytest
6+
7+
from loopx.control_plane.quota.cli_projection import compact_quota_should_run_cli_payload
8+
from loopx.control_plane.quota.turn_envelope import (
9+
build_turn_envelope, quota_action_signature_document,
10+
turn_envelope_action_signature_document,
11+
)
12+
from loopx.control_plane.testing.control_plane_composition_scenarios import (
13+
_required_vision_replan_source,
14+
)
15+
316
from loopx.control_plane.scheduler.execution_context import (
417
GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT,
518
)
@@ -114,7 +127,8 @@ def test_quota_delivers_coverage_context_and_minimal_replan_action() -> None:
114127
"delivered_by": "quota_host_projection",
115128
}
116129
assert context["coverage_ledger"][0]["surface_id"] == "surface-existing"
117-
assert action == {
130+
assert len(action["planning_guidance"]) == 2
131+
assert {key: value for key, value in action.items() if key != "planning_guidance"} == {
118132
"schema_version": "replan_action_packet_v0",
119133
"decision": "replan_required",
120134
"obligation_id": obligation["obligation_id"],
@@ -138,3 +152,37 @@ def test_manual_evidence_read_receipt_cannot_close_replan() -> None:
138152
assert payload["decision"] == "autonomous_replan_required"
139153
assert payload["autonomous_replan_obligation"]["required"] is True
140154
assert payload.get("replan_ack_feedback") is None
155+
156+
157+
@pytest.mark.parametrize("vision_gap", [False, True])
158+
def test_replan_guidance_survives_cli_and_host_compaction_without_new_authority(vision_gap: bool) -> None:
159+
source = (_required_vision_replan_source(goal_id=GOAL_ID, agent_id=AGENT_ID)
160+
if vision_gap else _quota_payload())
161+
guidance = source["replan_action_packet"]["planning_guidance"]
162+
# These are delivery assertions, not a claim that a model follows the advice.
163+
assert len(guidance) == 2
164+
assert "Never shrink requested goals" in guidance[0]
165+
assert "honor user scope, authority, budget and stops" in guidance[0]
166+
assert "current authoritative evidence for every requirement" in guidance[1]
167+
assert "unproven" in guidance[1]
168+
assert "blocked/exhausted/superseded is not achieved" in guidance[1]
169+
compact = compact_quota_should_run_cli_payload(source)
170+
envelope = build_turn_envelope(source)
171+
for packet in (compact, envelope):
172+
assert packet["replan_action_packet"]["planning_guidance"] == guidance
173+
174+
baseline = copy.deepcopy(source)
175+
baseline["replan_action_packet"].pop("planning_guidance")
176+
old_envelope = build_turn_envelope(baseline)
177+
# Prompt advice cannot change executable actions, authority or settlement.
178+
assert envelope["action"] == old_envelope["action"]
179+
signature = quota_action_signature_document(source)
180+
assert turn_envelope_action_signature_document(envelope) == signature
181+
signature["replan_action_packet"].pop("planning_guidance")
182+
assert signature == quota_action_signature_document(baseline)
183+
# Preserve an existing budget warning; bounded advice adds no more than 360 bytes.
184+
assert envelope["compaction"]["within_budget"] == old_envelope["compaction"]["within_budget"]
185+
assert (envelope["compaction"]["envelope_json_bytes"]
186+
- old_envelope["compaction"]["envelope_json_bytes"]) <= 360
187+
envelope["replan_action_packet"].pop("planning_guidance")
188+
assert envelope["replan_action_packet"] == old_envelope["replan_action_packet"]

‎tests/control_plane_ts/replan_semantics.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,18 @@ test("acceptance holds use one typed recovery policy even without a projected ou
181181
observation_delta: {delta_kinds: ["new_surface"]}}).accepted, false);
182182
}
183183
});
184+
185+
186+
test("planning advice cannot discharge a replan or widen source-specific exits", () => {
187+
for (const kind of ["typed_progress_repeat", "vision_acceptance_gap", "long_todo_chain",
188+
"external_progress_review_drift", "goal_acceptance_stale"]) {
189+
const source = {triggers: [{kind}]};
190+
const projection = projectReplanSemantics({operation: "requirements", obligation: source});
191+
assert.equal((projection.planning_guidance as string[]).length, 2);
192+
const refusal = projectReplanSemantics({operation: "qualify", obligation: source,
193+
planning_guidance: projection.planning_guidance});
194+
assert.equal(refusal.accepted, false);
195+
assert.deepEqual(refusal.required_any_of, requiredSemanticOutcomes(source));
196+
assert.equal(refusal.planning_guidance, undefined);
197+
}
198+
});

0 commit comments

Comments
 (0)