From 9e232552459f154d2a7e01352a97e018da6173a3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 12 Nov 2026 03:55:02 +0800 Subject: [PATCH 1/2] Show the bound Yardstick reversal in replan opening Use one scoped cue for the complete first read and staged review. Expose the authoritative comparison candidate and source finding only for a Todo bound to that replan, without another read or a new decision. Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/protocols/turn-envelope-v0.md | 11 +++ .../quota/progressive_context.ts | 36 ++++++++- loopx/control_plane/quota/turn_envelope.ts | 9 ++- .../control_plane/test_work_yardstick_cli.py | 75 +++++++++++++++++++ tests/control_plane_ts/turn_envelope.test.ts | 67 ++++++++++++++++- 5 files changed, 193 insertions(+), 5 deletions(-) diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 9474a88164..bd0f3327da 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -250,6 +250,17 @@ reader; a missing source is never replaced by a fresh guard. The existing generi followthrough is now available with stride off; its result-class counts and observations remain evidence, not successful-work or goal-gain verdicts. +When the bound Todo's captured Yardstick shows a proxy ordering opposite to the +authoritative measurement, the first complete Turn read and staged `review` +show the conflict, the preferred candidate, and the finding that supports it. +The pinned Explore read retains the measurement scope and evidence. Unbound +reviews keep navigation until the Todo is selected. The read grants only +evidence access. + +已绑定 Todo 的标尺若与正式测量反向,完整 Turn 的首读和分段 `review` 会显示 +分歧、正式测量更优的候选和支持它的发现。原捕获的 Explore 读取保留测量范围和证据。 +待选择的复盘保留证据入口,绑定 Todo 后才交付该任务的提示。读取只提供证据访问。 + Captured replans also reference Explore attachment syntax already present in the same captured writer instead of repeating it in the entry work context. This applies to default CLI/full recovery as well as optional delta delivery. Evidence, diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index 2997fdd69f..ab01080c8e 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -8,7 +8,7 @@ import {completeGoalNarrative} from "../work_items/goal_context_source.ts"; import type {JsonObject} from "../effect_program.ts"; import {jsonObject, requireBoolean, requireJsonObject} from "../runtime_decode.ts"; import {GOAL_OUTCOMES_SCHEMA, OUTCOME_CONTEXT_NAVIGATION_SCHEMA} from "../goals/outcome_requirements.ts"; -import {FULL_OUTCOME_READ_GUIDANCE, PROGRESSIVE_OUTCOME_READ_GUIDANCE} +import {FULL_OUTCOME_READ_GUIDANCE, PROGRESSIVE_OUTCOME_READ_GUIDANCE, selectedWorkTodoId} from "../work_items/interaction_contract.ts"; import {quotaSummaryReferences, contextSourceIdentity, contextProjectionChanges, type ProjectionSourceValues} from "./diagnostic_projection.ts"; @@ -27,6 +27,7 @@ type ReusableReadField = { export type ReusableExactBody = {path: Path; sourceIdentity: string}; const MIN_REUSABLE_BODY_BYTES = 1024; const SETTLEMENT_PLAN_PATH = ["interaction_contract", "cli_channel", "settlement_plan"]; +const REPLAN_CONTEXT_PATH = ["autonomous_replan_obligation", "replan_context"]; const pointer = (path: Path): string => path.length ? "/" + path.map(key => String(key).replaceAll("~", "~0").replaceAll("/", "~1")).join("/") : ""; const at = (root: unknown, path: Path): unknown => path.reduce((row, key) => @@ -833,6 +834,35 @@ export function initialContextReadPointers(context: JsonObject, decisionFields: return [...opening, ...remaining]; } +/** A suggested Todo in an unbound guard cannot supply the next decision's cue. */ +export function scopedReplanYardstickReversal(decision: JsonObject) { + const packet = jsonObject(decision.replan_action_packet); + const review = jsonObject(at(decision, REPLAN_CONTEXT_PATH)); + const explore = jsonObject(review?.explore_basis); + const cli = jsonObject(jsonObject(decision.interaction_contract)?.cli_channel); + const identity = jsonObject(jsonObject(cli?.settlement_plan)?.identity); + const todo = selectedWorkTodoId(decision); + const agent = decision.agent_id ?? jsonObject(decision.agent_identity)?.agent_id; + if (typeof packet?.obligation_id !== "string" || packet.obligation_id !== review?.obligation_id + || packet.settlement_only === true || explore?.availability !== "available" + || todo === null || cli?.selection_required === true || identity?.todo_id !== todo + || typeof decision.goal_id !== "string" || identity.goal_id !== decision.goal_id + || typeof agent !== "string" || identity.agent_id !== agent + || !Array.isArray(explore.work_yardsticks)) return undefined; + + const index = explore.work_yardsticks.findIndex(value => { + const cue = jsonObject(value); + return cue?.todo_id === todo && cue.proxy_order_reversed === true + && typeof cue.source_finding_id === "string" && typeof cue.comparison_candidate_ref === "string"; + }); + if (index < 0) return undefined; + const cue = explore.work_yardsticks[index] as JsonObject; + const fields = ["todo_id", "source_finding_id", "comparison_candidate_ref", "proxy_order_reversed", + "fidelity_status", "paired_candidates"].filter(field => + ["string", "boolean", "number"].includes(typeof cue[field])); + return {cue, fields, path: [...REPLAN_CONTEXT_PATH, "explore_basis", "work_yardsticks", index]}; +} + /** A pending Manager Inbox read already delivers these scoped request rows. */ function managerAgendaOpeningFields(value: unknown, requiredReads: unknown[]): string[] | undefined { const {content, ...read} = jsonObject(value) ?? {}; @@ -918,6 +948,10 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly include("autonomous_replan_obligation", "replan_context", "context_id"); if (Array.isArray(obligation.triggers)) obligation.triggers.forEach((_trigger, index) => include("autonomous_replan_obligation", "triggers", index, "kind")); + const reversal = scopedReplanYardstickReversal(context); + if (reversal) { + for (const field of reversal.fields) include(...reversal.path, field); + } } } include("agent_lane_next_action", "next_step"); diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 5e6ec826e3..3ee7c81864 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1,7 +1,8 @@ import {EXECUTION_PREPARATION_SCHEMA_VERSION} from "../work_items/execution_preparation.ts"; import {capturedContextSelection} from "./context_selection.ts"; import {contextReadSectionDigests, initialContextReadPointers, prepareProgressiveContext, prepareProgressiveContextDelivery, - progressiveContextPlan, type PreparationScopeDetail, type ReusableExactBody} from "./progressive_context.ts"; + progressiveContextPlan, scopedReplanYardstickReversal, + type PreparationScopeDetail, type ReusableExactBody} from "./progressive_context.ts"; import type { LoopXTurnRouteValue } from "../turn_driver/turn_contract_generated.ts"; import { EFFECTIVE_ACTIONS, EffectiveAction, type QuotaEffectiveActionValue } from "./effective_action.generated.ts"; import { AGENT_SCOPE_FRONTIER_ACTIONS } from "../agents/agent_scope_frontier.generated.ts"; @@ -967,6 +968,8 @@ function projectReplanView(request: JsonObject, payload: JsonObject, sourceHash: retain(follow, originalFollow, [["recorded_progress_count", "observation_count"]]); } else retain(follow, originalFollow, followFields); const explore = jsonObject(context.explore_basis) ?? {}; + const yardstickCount = Array.isArray(explore.work_yardsticks) ? explore.work_yardsticks.length : 0; + const reversal = reads ? scopedReplanYardstickReversal(payload) : undefined; const decisionValueReview = jsonObject(jsonObject(packet.stride_control)?.decision_value_review); const review = retain({...common, core_goal: core, @@ -983,8 +986,8 @@ function projectReplanView(request: JsonObject, payload: JsonObject, sourceHash: availability: explore.availability, revision: explore.revision, ...(Array.isArray(explore.measurement_feedback) && explore.measurement_feedback.length ? {measurement_count: explore.measurement_feedback.length} : {}), - ...(Array.isArray(explore.work_yardsticks) && explore.work_yardsticks.length - ? {yardstick_count: explore.work_yardsticks.length} : {}), + ...(yardstickCount ? {yardstick_count: yardstickCount} : {}), + ...(reversal ? {work_yardstick_reversal: retain(compactFields(reversal.cue, reversal.fields), reversal.cue)} : {}), read_action: reads?.explore ?? null, }, explore, ["availability", "revision"])}} : {}), evidence: (Array.isArray(context.evidence) ? context.evidence : []).map(value => { diff --git a/tests/control_plane/test_work_yardstick_cli.py b/tests/control_plane/test_work_yardstick_cli.py index 4518912179..b756a94366 100644 --- a/tests/control_plane/test_work_yardstick_cli.py +++ b/tests/control_plane/test_work_yardstick_cli.py @@ -107,6 +107,81 @@ def starting_page(project, runtime, path, turn, saved, monkeypatch): return page +@pytest.mark.parametrize("native_session", [False, True]) +def test_bound_benchmark_replan_opening_shows_scoped_reversal(tmp_path, monkeypatch, native_session): + from loopx.control_plane.turn_driver.codex_context import codex_context_hook + + project, runtime, path = setup(tmp_path, active=True) + run(project, runtime, path, "configure-goal", "--goal-id", GOAL_ID, + "--execution-replan-after-turns", "1", + "--execution-stride-strategy", "evidence_conditioned_v1", "--execute") + finding = attachment() + profiles = finding["work_yardstick_decision"] + finding["measurements"] = [ + {**profiles[role], "candidate_ref": f"candidate:{name}", "evidence_ref": f"result:{role}-{name}", "value": value} + for role, scores in (("authoritative", (8, 4)), ("proxy", (12, 15))) + for name, value in zip(("a", "b"), scores) + ] + finding["evidence_refs"].extend(row["evidence_ref"] for row in finding["measurements"]) + evidence = tmp_path / "finding.json" + evidence.write_text(json.dumps(finding)) + first = refresh(project, runtime, path, "prepare-reversal", evidence, + "--delivery-boundary", "in_flight_continuation") + assert first["explore_result_delivery"]["search_comparison"]["proxy_order_reversed"] + run(project, runtime, path, *_projected_cli_args(first["settlement_owed"]["command"], + turn_instance_id="prepare-reversal")) + + native = tmp_path / "native" + native.mkdir(mode=0o700) + monkeypatch.setenv("LOOPX_EXECUTION_MODE", "heartbeat") + monkeypatch.setenv("LOOPX_CONTEXT_DELIVERY", "on") + if native_session: + monkeypatch.setenv("CODEX_THREAD_ID", "fixture-benchmark") + assert codex_context_hook(str(native), {"session_id": "fixture-benchmark", "cwd": str(project), + "hook_event_name": "SessionStart", "source": "startup"}) == {} + else: + monkeypatch.delenv("CODEX_THREAD_ID", raising=False) + capture = tmp_path / "captures" + capture.mkdir(mode=0o700) + opening = capture / "decision-original" + turn = "periodic-review" + prepared = run(project, runtime, path, "quota", "should-run", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--turn-instance-id", turn, "--runtime-profile", "generic_cli", + "--available-capability", "shell", "--available-capability", "filesystem_write", + "--decision-output-dir", str(opening), "--prepared-capture-reference", "--complete-turn", + "--context-delivery", "on", "--codex-context-dir", str(native), "--defer-context-presentation") + original = opening / "decision.json" + original_bytes = original.read_bytes() + assert prepared["capture_sha256"] == hashlib.sha256(original_bytes).hexdigest() + unbound = json.loads(original_bytes) + unbound_cli = unbound["interaction_contract"]["cli_channel"] + assert unbound_cli["selection_required"] is True + selection = shlex.split(unbound_cli["selection_command"]["command_args_template"].replace("{todo_id}", TODO_ID)) + bound = run(project, runtime, path, *selection) + [selected] = [file for file in capture.glob("decision-*/decision.json") if file != original] + guard = json.loads(selected.read_bytes()) + assert guard["schema_version"] == "loopx_turn_envelope_v1" + identity = guard["heartbeat_receipt"]["settlement_identity"] + assert identity["todo_id"] == TODO_ID and identity["turn_instance_id"] == turn + assert guard["effective_action"] == "autonomous_replan_required" + obligation = "autonomous_replan_obligation" + assert guard[obligation]["obligation_id"] == unbound[obligation]["obligation_id"] + [cue] = guard[obligation]["replan_context"]["explore_basis"]["work_yardsticks"] + assert cue["proxy_order_reversed"] is True + assert original.read_bytes() == original_bytes + + command = shlex.split(bound["first_read_command"]) + page = run(project, runtime, path, *command[command.index("quota"):]) + basis = f"/{obligation}/replan_context/explore_basis/work_yardsticks/0/" + for field, expected in (("proxy_order_reversed", True), ("comparison_candidate_ref", "candidate:a"), + ("source_finding_id", first["explore_result_delivery"]["finding_id"])): + marker = json.dumps(basis + field) + ',"value":' + offset = page["content"].index(marker) + len(marker) + observed, _ = json.JSONDecoder().raw_decode(page["content"], offset) + assert observed == expected + assert _spend_run_count(runtime) == 1 + + def wide_write_scope(): return [ f"modules/component-{i:03d}/experiments/artifact-encodings/public-validation/" diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index 3cb9cc55e9..7b237c77ca 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -18,12 +18,13 @@ import { turnEnvelopeScopedSettlementIdentity, } from "../../loopx/control_plane/quota/turn_envelope.ts"; import { EffectRuntimeRequestError } from "../../loopx/control_plane/effect_runtime_errors.ts"; -import {settlementIdentityPayload, type JsonObject} from "../../loopx/control_plane/effect_program.ts"; +import {settlementIdentityPayload, settlementPlanPayload, type JsonObject} from "../../loopx/control_plane/effect_program.ts"; import { TURN_ENVELOPE_SECTION_TARGETS } from "../../loopx/control_plane/quota/turn_envelope_budget.ts"; import { projectPeerOrchestration } from "../../loopx/control_plane/quota/peer_orchestration.ts"; import {projectQuotaDiagnostics, referenceQuotaSummaryLanes, resolveQuotaSummaryLanes} from "../../loopx/control_plane/quota/diagnostic_projection.ts"; import { controllerRequest } from "../../loopx/control_plane/turn_driver/loop_controller.ts"; +import {turnScopedCliSettlementPlan} from "../../loopx/control_plane/quota/settlement_plan.ts"; test("complete captures reuse progressive presentation only after source validation", () => { const source = payload(); @@ -332,6 +333,70 @@ function completeAcceptanceRead(source: JsonObject, outcomeRef = ""): JsonObject return outcomeRef ? read : {outcomes: (read.acceptance_index as JsonObject | undefined)?.outcomes}; } +test("both replan readers surface an opted-in reversal for the bound task only", () => { + const conflict = {todo_id: "todo-1", source_finding_id: "finding-1", + comparison_candidate_ref: "candidate:outcome-best", proxy_order_reversed: true, + fidelity_status: "order_reversed", paired_candidates: 4, counterevidence_refs: ["result:counter"]}; + const other = {...conflict, todo_id: "todo-foreign", comparison_candidate_ref: "candidate:foreign"}; + const cases = [ + {cues: [other, conflict], scoped: true}, + {cues: [other], scoped: false}, + {cues: [{...conflict, proxy_order_reversed: false}], scoped: false}, + {cues: [], scoped: false}, + {cues: [conflict], selectionRequired: true, scoped: false}, + {cues: [conflict], unbound: true, scoped: false}, + {cues: [conflict], unavailable: true, scoped: false}, + ]; + for (const {cues, scoped, selectionRequired, unbound, unavailable} of cases) { + const source = payload(), interaction = source.interaction_contract as JsonObject; + const cli = interaction.cli_channel as JsonObject; + if (!unbound) cli.settlement_plan = settlementPlanPayload(turnScopedCliSettlementPlan({ + identity: {goal_id: source.goal_id, agent_id: source.agent_id, todo_id: "todo-1", turn_instance_id: "turn"}, + delivery_batch_scale_choices: ["implementation"], command_templates: { + todo_claim: "claim", todo_completion: "completion", durable_writeback: "writeback", + quota_spend: "spend", terminal_closeout: "terminal", + }, + })); + cli.selection_required = selectionRequired ?? false; + Object.assign(source, { + effective_action: "autonomous_replan_required", normal_delivery_allowed: false, + replan_read_path: "/capture/original.json", + replan_action_packet: {schema_version: "replan_action_packet_v0", decision: "replan_required", + obligation_id: "original-obligation"}, + autonomous_replan_obligation: {schema_version: "autonomous_replan_obligation_v0", + obligation_id: "original-obligation", replan_context: {schema_version: "replan_context_v0", + obligation_id: "original-obligation", context_id: "original-context", + explore_basis: {availability: unavailable ? "unavailable" : "available", + revision: "captured", work_yardsticks: cues}, + }}, + }); + const captured = evaluateTurnEnvelope({operation: "compose_complete", payload: source, scheduler_execution_args: ""}); + const before = structuredClone(captured); + const source_hash = (captured.contract_binding as JsonObject).content_hash; + const read = evaluateTurnEnvelope({operation: "replan_view", payload: captured, source_hash, stage: "review"}); + const explore = (read.capabilities as JsonObject).explore as JsonObject; + const opening = evaluateTurnEnvelope({operation: "context_select", context: captured, initial_read: true}); + const cuesInOpening = (opening.selections as {pointer: string}[]) + .filter(row => row.pointer.includes("/explore_basis/work_yardsticks/")); + if (scoped) { + assert.deepEqual(explore.work_yardstick_reversal, { + todo_id: "todo-1", source_finding_id: "finding-1", comparison_candidate_ref: "candidate:outcome-best", + proxy_order_reversed: true, fidelity_status: "order_reversed", paired_candidates: 4, + }); + assert.match(String(explore.read_action), /--replan-stage explore$/); + const prefix = "/autonomous_replan_obligation/replan_context/explore_basis/work_yardsticks/1/"; + assert.deepEqual(cuesInOpening.map(row => row.pointer), + Object.keys(explore.work_yardstick_reversal as JsonObject).map(field => prefix + field)); + const detail = evaluateTurnEnvelope({operation: "replan_view", payload: captured, source_hash, stage: "explore"}); + assert.deepEqual((detail.explore_basis as JsonObject).work_yardsticks, cues); + } else { + assert.ok(!Object.hasOwn(explore, "work_yardstick_reversal")); + assert.equal(cuesInOpening.length, 0); + } + assert.deepEqual(captured, before); + } +}); + test("complete acceptance lookup shares exact content across registered carriers and retains revisions", () => { const one = {id: "shared", owner_agent_id: "peer", objective: "Original", revision: 1, requirements: [{id: "a", planning_satisfied: false, status: "unverified", description: "Full a"}, From e3a82fba290e3adb69778f875559f04265e15850 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 12 Nov 2026 09:57:06 +0800 Subject: [PATCH 2/2] Keep bound Yardstick evidence in the replan opening Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/protocols/turn-envelope-v0.md | 12 ++- loopx/capabilities/explore/turn_context.py | 11 ++- .../capabilities/explore_turn_context.ts | 16 +++- loopx/control_plane/quota/live_decision.py | 1 + .../quota/progressive_context.ts | 15 ++- .../work_items/yardstick_decision.ts | 5 +- loopx/quota.py | 10 +- .../control_plane/test_work_yardstick_cli.py | 94 ++++++++++++++++--- tests/control_plane_ts/turn_envelope.test.ts | 4 +- 9 files changed, 137 insertions(+), 31 deletions(-) diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index bd0f3327da..ef7488f0b6 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -252,13 +252,15 @@ observations remain evidence, not successful-work or goal-gain verdicts. When the bound Todo's captured Yardstick shows a proxy ordering opposite to the authoritative measurement, the first complete Turn read and staged `review` -show the conflict, the preferred candidate, and the finding that supports it. -The pinned Explore read retains the measurement scope and evidence. Unbound -reviews keep navigation until the Todo is selected. The read grants only -evidence access. +show the conflict, the preferred candidate, the proxy's comparison scope, and +the finding IDs for the measured pair alongside the Yardstick declaration. +The selected Todo's card takes precedence within the bounded read. The pinned +Explore read retains the full evidence. Unbound reviews keep navigation until +the Todo is selected. The read grants only evidence access. 已绑定 Todo 的标尺若与正式测量反向,完整 Turn 的首读和分段 `review` 会显示 -分歧、正式测量更优的候选和支持它的发现。原捕获的 Explore 读取保留测量范围和证据。 +分歧、正式测量更优的候选、代理指标的比较范围,以及实际测量发现和原标尺声明各自的 ID。 +已选 Todo 的提示在有上限的读取中优先交付,原捕获的 Explore 读取保留完整证据。 待选择的复盘保留证据入口,绑定 Todo 后才交付该任务的提示。读取只提供证据访问。 Captured replans also reference Explore attachment syntax already present in the diff --git a/loopx/capabilities/explore/turn_context.py b/loopx/capabilities/explore/turn_context.py index 51a3f50995..639f9981f3 100644 --- a/loopx/capabilities/explore/turn_context.py +++ b/loopx/capabilities/explore/turn_context.py @@ -49,7 +49,7 @@ def _policy(registry_path: Path, goal_id: str, runtime_root: Path | None = None) def read_replan_explore_context(*, source_status: Mapping[str, Any], goal_id: str, agent_id: str | None, - review_pages: Any = None) -> dict[str, Any] | None: + review_pages: Any = None, preferred_yardstick_todo_id: str | None = None) -> dict[str, Any] | None: """Outer read adapter; the replan owner invokes it only for enabled reviews. Unavailable IO is distinct from an empty or disabled evidence source. No @@ -61,6 +61,7 @@ def read_replan_explore_context(*, source_status: Mapping[str, Any], goal_id: st return explore_turn_context( registry_path=Path(source_status["registry"]), runtime_root=Path(source_status["runtime_root"]), goal_id=goal_id, agent_id=agent_id, include_finding_digests=True, + preferred_yardstick_todo_id=preferred_yardstick_todo_id, **({"review_pages": review_pages} if review_pages is not None else {})) except (OSError, ValueError, RuntimeError): return {"ok": False, "goal_id": goal_id, "agent_id": agent_id} @@ -70,6 +71,7 @@ def project_work_yardstick_sources( *, events: list[dict[str, Any]], projection: Mapping[str, Any], goal_id: str, agent_id: str, work_todos: list[dict[str, Any]], enabled: bool, todo_id: str | None = None, result_node: str | None = None, + preferred_yardstick_todo_id: str | None = None, ) -> list[dict[str, Any]]: """Replay typed-owner-selected prefixes from one already-read canonical log. @@ -82,6 +84,7 @@ def project_work_yardstick_sources( requests = effect_runtime_result("work_item.yardstick.sources", { "yardstick_enabled": enabled, "work_todos": work_todos, "goal_id": goal_id, "agent_id": agent_id, "todo_id": todo_id, "result_node": result_node, + "preferred_yardstick_todo_id": preferred_yardstick_todo_id, "findings": projection.get("findings", []), "source_events": [{"goal_id": row["goal_id"], "event_id": row["event_id"]} for row in events], }) @@ -108,6 +111,7 @@ def explore_turn_context( result_node: str | None = None, result_revision: str | None = None, include_finding_digests: bool = False, review_pages: Any = None, todo_id: str | None = None, + preferred_yardstick_todo_id: str | None = None, ) -> dict[str, Any]: if type(include_finding_digests) is not bool: raise ValueError("include_finding_digests must be an explicit boolean") @@ -165,7 +169,7 @@ def explore_turn_context( if yardstick and graph: sources = project_work_yardstick_sources(events=events, projection=projection, goal_id=goal_id, agent_id=agent_id, work_todos=work_todos, enabled=yardstick, - todo_id=todo_id, result_node=result_node) + todo_id=todo_id, result_node=result_node, preferred_yardstick_todo_id=preferred_yardstick_todo_id) return cast(dict[str, Any], effect_runtime_result( "explore.turn_context", { @@ -175,7 +179,8 @@ def explore_turn_context( "harness_gate": gate, "projection": projection, "plan": plan, - **({"yardstick_enabled": True, "work_todos": work_todos, "yardstick_sources": sources} if yardstick else {}), + **({"yardstick_enabled": True, "work_todos": work_todos, "yardstick_sources": sources, + "preferred_yardstick_todo_id": preferred_yardstick_todo_id} if yardstick else {}), **focus, **({"review_todos": [scopes[key] for key in sorted(page_tasks) if key in scopes]} if page_tasks else {}), "route": route, diff --git a/loopx/control_plane/capabilities/explore_turn_context.ts b/loopx/control_plane/capabilities/explore_turn_context.ts index 1dd47d1f55..edf8e85c3d 100644 --- a/loopx/control_plane/capabilities/explore_turn_context.ts +++ b/loopx/control_plane/capabilities/explore_turn_context.ts @@ -10,7 +10,7 @@ import {PLANNING_TODO_ID} from "../work_items/planning_relations.ts"; import {EXPLORE_WRITEBACK_SUMMARY_LIMIT, exploreBeforeWorkGuidance, exploreInTurnFeedbackGuidance, exploreResultWritebackAffordance} from "./explore_result_writeback.ts"; import {EXPLORE_RESULT_PAGE_MAX, normalizeExploreResultPages} from "./explore_result_page.ts"; import {projectWorkMeasurementFacts, projectWorkYardstickCue, workYardstickTodos, WORK_YARDSTICK_DECISION_SCHEMA} from "../work_items/yardstick_decision.ts"; -import {projectExploreMeasurementFeedback} from "./explore_measurements.ts"; +import {projectExploreMeasurementFeedback, type ExploreMeasurementPoint} from "./explore_measurements.ts"; /** Reusable evidence and instructions in this owner's read model. Current * selection, eligibility, source revisions and pagination stay outside this @@ -374,13 +374,23 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { ...(workCues.length ? {work_yardsticks: workCues.slice(0, 3).map(cue => { if (resultNode !== null) return cue; const comparison = cue.search_comparison as JsonObject | undefined; + const goalPair = comparison?.goal as JsonObject | undefined; + const proxyPair = comparison?.proxy as JsonObject | undefined; + const supportingFindings = comparison?.proxy_order_reversed === true && goalPair && proxyPair + ? [...new Set([goalPair.previous_best, goalPair.latest, proxyPair.previous_best, proxyPair.latest] + .map(point => (point as ExploreMeasurementPoint).finding_id))] + : undefined; return {source_finding_id: cue.source_finding_id, node_id: cue.node_id, todo_id: (cue.binding as JsonObject).todo_id, target: (cue.target as JsonObject).summary, cue: (cue.next_action as JsonObject).reason, counterevidence_refs: cue.counterevidence_refs, ...(comparison ? {comparison_cue: comparison.guidance, - comparison_candidate_ref: (comparison.goal as JsonObject).preferred_candidate_ref, + comparison_candidate_ref: goalPair?.preferred_candidate_ref, ...(typeof comparison.proxy_order_reversed === "boolean" - ? {proxy_order_reversed: comparison.proxy_order_reversed} : {})} : {}), + ? {proxy_order_reversed: comparison.proxy_order_reversed} : {}), + ...(supportingFindings ? { + comparison_finding_ids: supportingFindings, + proxy_comparison_scope: (cue.proxy as JsonObject).comparison_scope, + } : {})} : {}), details_command: resultRead("--result-node", String(cue.node_id))}; }), omitted_work_yardsticks: Math.max(0, workCues.length - 3), work_yardstick_command: command("turn-context", "--agent-id", agent, "--todo-id", "")} : {}), diff --git a/loopx/control_plane/quota/live_decision.py b/loopx/control_plane/quota/live_decision.py index c4b598c681..5498b6ea28 100644 --- a/loopx/control_plane/quota/live_decision.py +++ b/loopx/control_plane/quota/live_decision.py @@ -825,6 +825,7 @@ def build_live_quota_should_run_decision( ), receipt_bound_todo_id=receipt_bound_todo_id, requested_action_todo_id=requested_action_todo_id, + retained_action_selection_todo_id=retained_action_selection_todo_id, receipt_bound_monitor_phase=receipt_bound_monitor_phase, receipt_bound_replay_phase=receipt_bound_replay_phase, receipt_bound_replan_obligation_id=receipt_bound_replan_obligation_id, diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index ab01080c8e..2d3bedadac 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -857,9 +857,13 @@ export function scopedReplanYardstickReversal(decision: JsonObject) { }); if (index < 0) return undefined; const cue = explore.work_yardsticks[index] as JsonObject; - const fields = ["todo_id", "source_finding_id", "comparison_candidate_ref", "proxy_order_reversed", + const fields = ["todo_id", "source_finding_id", "comparison_candidate_ref", "proxy_order_reversed", "proxy_comparison_scope", "fidelity_status", "paired_candidates"].filter(field => ["string", "boolean", "number"].includes(typeof cue[field])); + if (Array.isArray(cue.comparison_finding_ids) && cue.comparison_finding_ids.length > 0 + && cue.comparison_finding_ids.length <= 4 && cue.comparison_finding_ids.every(id => typeof id === "string")) { + fields.push("comparison_finding_ids"); + } return {cue, fields, path: [...REPLAN_CONTEXT_PATH, "explore_basis", "work_yardsticks", index]}; } @@ -923,6 +927,7 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly // A staged replan has already replaced inline acceptance with a pinned // review reader. Deliver its first action and ordering without writeback. const replan = object(context.replan_action_packet); + let replanReversal: ReturnType; if (context.effective_action === EffectiveAction.AUTONOMOUS_REPLAN_REQUIRED && replan?.schema_version === "replan_action_packet_v0" && replan.decision === "replan_required" && replan.settlement_only !== true && typeof replan.review === "string" && replan.review) { @@ -948,10 +953,7 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly include("autonomous_replan_obligation", "replan_context", "context_id"); if (Array.isArray(obligation.triggers)) obligation.triggers.forEach((_trigger, index) => include("autonomous_replan_obligation", "triggers", index, "kind")); - const reversal = scopedReplanYardstickReversal(context); - if (reversal) { - for (const field of reversal.fields) include(...reversal.path, field); - } + replanReversal = scopedReplanYardstickReversal(context); } } include("agent_lane_next_action", "next_step"); @@ -986,6 +988,9 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly // Deliver the opted-in decision cue before work, while the candidate or // test can still change. The template and evidence stay on their owner read. const cueStart = pointers.length; + if (replanReversal) { + for (const field of replanReversal.fields) include(...replanReversal.path, field); + } if (context.effective_action === EffectiveAction.NORMAL_RUN && context.normal_delivery_allowed === true && typeof selectedTodo?.todo_id === "string" && typeof context.goal_id === "string" && typeof agentId === "string" && (identityAgent === undefined || identityAgent === agentId)) { diff --git a/loopx/control_plane/work_items/yardstick_decision.ts b/loopx/control_plane/work_items/yardstick_decision.ts index 1be5dad3d2..2ae386512c 100644 --- a/loopx/control_plane/work_items/yardstick_decision.ts +++ b/loopx/control_plane/work_items/yardstick_decision.ts @@ -103,10 +103,13 @@ export function normalizeWorkYardstickSourceSnapshot(value: unknown): NonNullabl } /** Share current-work eligibility between the read plan and its final projection. */ export function workYardstickTodos(params: JsonObject): JsonObject[] { - return params.yardstick_enabled === true && Array.isArray(params.work_todos) ? (params.work_todos as JsonObject[]) + const todos = params.yardstick_enabled === true && Array.isArray(params.work_todos) ? (params.work_todos as JsonObject[]) .filter(todo => todo.role === "agent" && !todo.done && todo.claimed_by === params.agent_id && ["open", "pending", "in_progress", "blocked"].includes(String(todo.status)) && (params.todo_id == null || todo.todo_id === params.todo_id)) : []; + const preferred = todos.findIndex(todo => todo.todo_id === params.preferred_yardstick_todo_id); + if (preferred > 0) todos.unshift(...todos.splice(preferred, 1)); + return todos; } function belongsToWork(row: JsonObject, binding: JsonObject): boolean { const capture = row.work_yardstick as WorkYardstickCapture | undefined; diff --git a/loopx/quota.py b/loopx/quota.py index 1ce7e8fa87..eded708cd4 100644 --- a/loopx/quota.py +++ b/loopx/quota.py @@ -1041,6 +1041,7 @@ def build_quota_should_run( operator_inbox_urgency_projector: Callable[..., dict[str, Any]] | None = None, receipt_bound_todo_id: str | None = None, requested_action_todo_id: str | None = None, + retained_action_selection_todo_id: str | None = None, receipt_bound_monitor_phase: ReceiptBoundMonitorPhase | None = None, receipt_bound_replay_phase: ReceiptBoundReplayPhase | None = None, receipt_bound_terminal_phase: ReceiptBoundTerminalPhase | None = None, @@ -1060,10 +1061,17 @@ def build_quota_should_run( build_quota_should_run as _build_quota_should_run, ) + # Inline reentry can retain an explicit choice before creating a receipt. + # This only orders the agent's eligible cards within the existing read cap. + preferred_yardstick_todo_id = receipt_bound_todo_id or requested_action_todo_id or retained_action_selection_todo_id + def read_explore_context(read_goal_id: str, read_agent_id: str | None) -> dict[str, Any] | None: from .capabilities.explore.turn_context import read_replan_explore_context - return read_replan_explore_context(source_status=status_payload, goal_id=read_goal_id, agent_id=read_agent_id) + return read_replan_explore_context( + source_status=status_payload, goal_id=read_goal_id, agent_id=read_agent_id, + preferred_yardstick_todo_id=preferred_yardstick_todo_id, + ) return _build_quota_should_run( status_payload, diff --git a/tests/control_plane/test_work_yardstick_cli.py b/tests/control_plane/test_work_yardstick_cli.py index b756a94366..1bfba7d982 100644 --- a/tests/control_plane/test_work_yardstick_cli.py +++ b/tests/control_plane/test_work_yardstick_cli.py @@ -108,29 +108,74 @@ def starting_page(project, runtime, path, turn, saved, monkeypatch): @pytest.mark.parametrize("native_session", [False, True]) -def test_bound_benchmark_replan_opening_shows_scoped_reversal(tmp_path, monkeypatch, native_session): +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_bound_benchmark_replan_opening_shows_scoped_reversal(tmp_path, monkeypatch, native_session, provider): from loopx.control_plane.turn_driver.codex_context import codex_context_hook - project, runtime, path = setup(tmp_path, active=True) + scope = wide_write_scope() + project, runtime, path = setup(tmp_path, active=True, provider=provider, write_scope=scope) run(project, runtime, path, "configure-goal", "--goal-id", GOAL_ID, "--execution-replan-after-turns", "1", "--execution-stride-strategy", "evidence_conditioned_v1", "--execute") finding = attachment() profiles = finding["work_yardstick_decision"] - finding["measurements"] = [ + profiles["proxy"]["comparison_scope"] = "validation:screen-slice" + measurements = [ {**profiles[role], "candidate_ref": f"candidate:{name}", "evidence_ref": f"result:{role}-{name}", "value": value} for role, scores in (("authoritative", (8, 4)), ("proxy", (12, 15))) for name, value in zip(("a", "b"), scores) ] - finding["evidence_refs"].extend(row["evidence_ref"] for row in finding["measurements"]) + prepared_turn = binding("prepare-reversal") + run(project, runtime, path, "quota", "should-run", "--codex-app", *prepared_turn) evidence = tmp_path / "finding.json" - evidence.write_text(json.dumps(finding)) - first = refresh(project, runtime, path, "prepare-reversal", evidence, - "--delivery-boundary", "in_flight_continuation") - assert first["explore_result_delivery"]["search_comparison"]["proxy_order_reversed"] + + def record(payload): + evidence.write_text(json.dumps(payload)) + return run(project, runtime, path, "explore", "finding", *prepared_turn, "--result-json", str(evidence)) + + def record_measurement(measurement, **location): + return record({ + "schema_version": finding["schema_version"], "input_revision": finding["input_revision"], + "status": "confirmed", "observation": f"Observed {measurement['evidence_ref']}", + "interpretation": "Use the matched measurement for this candidate.", + "evidence_refs": [measurement["evidence_ref"]], "measurements": [measurement], **location, + }) + + proxy_a = record_measurement(measurements[2], question=finding["question"], applicability=finding["applicability"]) + node_id = proxy_a["node_id"] + proxy_b = record_measurement(measurements[3], node_id=node_id) + profiles["counterevidence_refs"] = [row["evidence_ref"] for row in measurements[2:]] + finding["evidence_refs"].extend(profiles["counterevidence_refs"]) + declaration = record({**finding, "node_id": node_id}) + assert "search_comparison" not in declaration + official = [record_measurement(row, node_id=node_id) for row in measurements[:2]] + finding_ids = [result["finding_id"] for result in [*official, proxy_a, proxy_b]] + assert len(set(finding_ids)) == 4 and declaration["finding_id"] not in finding_ids + assert official[-1]["search_comparison"]["proxy_order_reversed"] + first = run(project, runtime, path, "refresh-state", *prepared_turn, "--classification", "validated_change", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--delivery-boundary", "in_flight_continuation", "--no-global-sync", "--suppress-external-sinks") run(project, runtime, path, *_projected_cli_args(first["settlement_owed"]["command"], turn_instance_id="prepare-reversal")) + log = explore_result_log_path(runtime, GOAL_ID) + for index in range(3): + node = f"competing-question-{index}" + added = run(project, runtime, path, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--claimed-by", AGENT_ID, "--priority", "P0", + "--text", f"Inspect independent evidence {index}", "--task-class", "advancement_task", + "--action-kind", "validate", "--required-capability", "network", "--target-key", node) + other = added["todo_id"] + run(project, runtime, path, "todo", "update", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", other, "--explore-result-node-ref", node) + append_explore_result_event(log, build_explore_node_event(goal_id=GOAL_ID, agent_id=AGENT_ID, + node_id=node, title=node, node_kind="question", status="exploring")) + other_work = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, todo_id=other, turn_instance_id=f"other-{index}") + append_explore_result_event(log, build_explore_finding_event(goal_id=GOAL_ID, agent_id=AGENT_ID, + node_id=node, title=f"Adopt the scope for {node}", status="confirmed", + evidence_refs=finding["evidence_refs"], work_binding=other_work, + work_yardstick={"decision": {**profiles, "counterevidence_refs": []}, "binding": other_work})) + native = tmp_path / "native" native.mkdir(mode=0o700) monkeypatch.setenv("LOOPX_EXECUTION_MODE", "heartbeat") @@ -156,6 +201,11 @@ def test_bound_benchmark_replan_opening_shows_scoped_reversal(tmp_path, monkeypa unbound = json.loads(original_bytes) unbound_cli = unbound["interaction_contract"]["cli_channel"] assert unbound_cli["selection_required"] is True + obligation = "autonomous_replan_obligation" + unbound_explore = unbound[obligation]["replan_context"]["explore_basis"] + assert len(unbound_explore["work_yardsticks"]) == 3 + assert all(card["todo_id"] != TODO_ID for card in unbound_explore["work_yardsticks"]) + assert unbound_explore["omitted_work_yardsticks"] == 1 selection = shlex.split(unbound_cli["selection_command"]["command_args_template"].replace("{todo_id}", TODO_ID)) bound = run(project, runtime, path, *selection) [selected] = [file for file in capture.glob("decision-*/decision.json") if file != original] @@ -164,21 +214,41 @@ def test_bound_benchmark_replan_opening_shows_scoped_reversal(tmp_path, monkeypa identity = guard["heartbeat_receipt"]["settlement_identity"] assert identity["todo_id"] == TODO_ID and identity["turn_instance_id"] == turn assert guard["effective_action"] == "autonomous_replan_required" - obligation = "autonomous_replan_obligation" assert guard[obligation]["obligation_id"] == unbound[obligation]["obligation_id"] - [cue] = guard[obligation]["replan_context"]["explore_basis"]["work_yardsticks"] + explore = guard[obligation]["replan_context"]["explore_basis"] + assert explore["revision"] == unbound_explore["revision"] + cue, *others = explore["work_yardsticks"] + assert cue["todo_id"] == TODO_ID and len(others) == 2 and explore["omitted_work_yardsticks"] == 1 assert cue["proxy_order_reversed"] is True assert original.read_bytes() == original_bytes + staged = run(project, runtime, path, "quota", "replan-view", "--decision-file", str(selected), + "--source-hash", guard["contract_binding"]["content_hash"], "--replan-stage", "review") + staged_cue = staged["capabilities"]["explore"]["work_yardstick_reversal"] command = shlex.split(bound["first_read_command"]) page = run(project, runtime, path, *command[command.index("quota"):]) + assert page["offset"] == 0 and not page["eof"] + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 12000 basis = f"/{obligation}/replan_context/explore_basis/work_yardsticks/0/" for field, expected in (("proxy_order_reversed", True), ("comparison_candidate_ref", "candidate:a"), - ("source_finding_id", first["explore_result_delivery"]["finding_id"])): + ("source_finding_id", declaration["finding_id"]), + ("proxy_comparison_scope", profiles["proxy"]["comparison_scope"]), + ("comparison_finding_ids", finding_ids)): marker = json.dumps(basis + field) + ',"value":' offset = page["content"].index(marker) + len(marker) observed, _ = json.JSONDecoder().raw_decode(page["content"], offset) - assert observed == expected + assert observed == staged_cue[field] == expected + _, reference = read_context_read_reference(page["reference_file"]) + assert reference["sha256"] == hashlib.sha256(selected.read_bytes()).hexdigest() + chunks = [page["content"]] + while not page["eof"]: + command = shlex.split(page["next_command"]) + page = run(project, runtime, path, *command[command.index("quota"):]) + _, follow = read_context_read_reference(page["reference_file"]) + assert follow["path"] == reference["path"] and follow["sha256"] == reference["sha256"] + chunks.append(page["content"]) + values = {row["pointer"]: row["value"] for row in json.loads("".join(chunks))["selections"]} + assert values["/goal_boundary"]["write_scope"] == scope assert _spend_run_count(runtime) == 1 diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index 7b237c77ca..1871b1a7f7 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -336,6 +336,7 @@ function completeAcceptanceRead(source: JsonObject, outcomeRef = ""): JsonObject test("both replan readers surface an opted-in reversal for the bound task only", () => { const conflict = {todo_id: "todo-1", source_finding_id: "finding-1", comparison_candidate_ref: "candidate:outcome-best", proxy_order_reversed: true, + proxy_comparison_scope: "validation:screen-slice", comparison_finding_ids: ["finding-2", "finding-3"], fidelity_status: "order_reversed", paired_candidates: 4, counterevidence_refs: ["result:counter"]}; const other = {...conflict, todo_id: "todo-foreign", comparison_candidate_ref: "candidate:foreign"}; const cases = [ @@ -381,7 +382,8 @@ test("both replan readers surface an opted-in reversal for the bound task only", if (scoped) { assert.deepEqual(explore.work_yardstick_reversal, { todo_id: "todo-1", source_finding_id: "finding-1", comparison_candidate_ref: "candidate:outcome-best", - proxy_order_reversed: true, fidelity_status: "order_reversed", paired_candidates: 4, + proxy_order_reversed: true, proxy_comparison_scope: "validation:screen-slice", + fidelity_status: "order_reversed", paired_candidates: 4, comparison_finding_ids: ["finding-2", "finding-3"], }); assert.match(String(explore.read_action), /--replan-stage explore$/); const prefix = "/autonomous_replan_obligation/replan_context/explore_basis/work_yardsticks/1/";