Skip to content
Merged
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
29 changes: 29 additions & 0 deletions docs/reference/protocols/goal-vision-replan-contract-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 3 additions & 0 deletions loopx/control_plane/quota/turn_envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion loopx/control_plane/work_items/progress_observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion loopx/control_plane/work_items/replan_semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ const FRESH_PATH_DISPOSITIONS = new Set(["continue", "no_change", "replan"]);
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>";
const VISION_CLI_ARGS = "--agent-vision-json '<path-to-evidence-linked-goal-vision-replan-contract-v0.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 : {};
}
Expand Down Expand Up @@ -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");
Expand Down
50 changes: 49 additions & 1 deletion tests/control_plane/test_replan_host_context_projection.py
Original file line number Diff line number Diff line change
@@ -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,
)
Expand Down Expand Up @@ -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"],
Expand All @@ -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"]
15 changes: 15 additions & 0 deletions tests/control_plane_ts/replan_semantics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Loading