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
10 changes: 10 additions & 0 deletions docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,16 @@ with invented evidence. The field never qualifies execution, scientific novelty,
utility, effective-Turn counts or acceptance; these remain independent checks.
No new scheduler, selection rule, quota debit or per-result settlement is added.

The history reader covers both legacy Todo-bound decisions and autonomous
replan decisions. For an autonomous decision, the recorded Goal, Agent, Turn,
obligation, settlement identity and accepted acknowledgement must agree; the
derived decision reference includes that obligation instead of inventing a
Todo. The existing settlement owner defines the binding, while the Python
codec only transports its facts. Enabled reviews therefore receive the prior
decision through the same `previous_review` requirement. Feature-off context
and legacy decision references remain unchanged. This repairs feedback
delivery; execution quality and goal benefit still require separate evidence.

#### An iterative strategy and engineering boundary

The first strategy is **model-guided, evidence-conditioned work planning**.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,13 @@ replan 复查这个向量,不成为新的权限 owner。在授权目标内改
替代证据。此字段不资格化执行、科学创新、收益、有效 Turn 计数或验收,后者仍
独立验证;不增加 scheduler、选择规则、quota 消耗或逐结果结算。

历史读取同时覆盖旧 Todo 绑定决策和自主 replan 决策。自主决策的 Goal、Agent、
Turn、义务、settlement identity 与已接受的回执必须一致;派生决策引用包含该
义务,不虚构 Todo。绑定由现有 settlement owner 定义,Python codec 只传输
事实。启用策略后,上一决策进入同一个 `previous_review` 要求;关闭策略的
context 和历史决策引用保持兼容。这里修复的是反馈送达,执行质量和目标收益
仍需独立证据。

#### 可迭代策略与工程边界

第一版策略是**模型依据证据规划完整工作**:显式选择 grow(扩大有支持的路线)、
Expand Down
38 changes: 31 additions & 7 deletions loopx/control_plane/work_items/replan_context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Bounded evidence for the existing replan boundary; no settlement authority. */
import {createHash} from "node:crypto";
import type {JsonObject} from "../effect_program.ts";
import {settlementIdentityPayload, SETTLEMENT_IDENTITY_SCHEMA_VERSION,
SCOPED_SETTLEMENT_IDENTITY_SCHEMA_VERSION, type JsonObject} from "../effect_program.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import {BARE_SHA256_PATTERN} from "../content_digest.ts";
import {requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts";
Expand Down Expand Up @@ -104,16 +105,33 @@ function coreGoal(value: unknown): JsonObject {
objective_missing: objective === null, acceptance_contract: acceptance};
}

/** Qualify recorded bindings using the settlement owner, without settling work.
* Keep historical Todo evidence eligible under its existing read contract.
*/
function decisionBindingMatches(row: JsonObject, source: JsonObject, delta: JsonObject | null): boolean {
const identity = objectOrNull(source.settlement_identity);
if (!identity || identity.turn_instance_id !== source.turn_instance_id ||
typeof identity.turn_instance_id !== "string" || !ID.test(identity.turn_instance_id)) return false;
if (identity.schema_version === SETTLEMENT_IDENTITY_SCHEMA_VERSION) {
return ["goal_id", "agent_id", "todo_id"].every(key => identity[key] === row[key]);
}
if (identity.schema_version !== SCOPED_SETTLEMENT_IDENTITY_SCHEMA_VERSION ||
typeof row.goal_id !== "string" || typeof row.agent_id !== "string" ||
[row.todo_id, source.todo_id, identity.todo_id].some(value => value != null && value !== "") ||
["goal_id", "agent_id"].some(key => source[key] !== row[key]) ||
typeof source.replan_obligation_id !== "string" || !ID.test(source.replan_obligation_id) ||
delta?.obligation_id !== source.replan_obligation_id) return false;
const expected = settlementIdentityPayload({goal_id: row.goal_id, agent_id: row.agent_id,
turn_instance_id: identity.turn_instance_id, replan_obligation_id: source.replan_obligation_id});
return Object.entries(expected).every(([key, value]) => identity[key] === value);
}

function acceptedDecisionSource(row: JsonObject): boolean {
const source = objectOrNull(row._source_facts);
const identity = objectOrNull(source?.settlement_identity);
const vision = objectOrNull(source?.agent_vision);
const ack = objectOrNull(source?.autonomous_replan_ack);
const delta = objectOrNull(ack?.semantic_delta);
return !!source && identity?.schema_version === "quota_settlement_identity_v0" &&
["goal_id", "agent_id", "todo_id"].every(key => identity[key] === row[key]) &&
identity.turn_instance_id === source.turn_instance_id &&
typeof identity.turn_instance_id === "string" && ID.test(identity.turn_instance_id) &&
return !!source && decisionBindingMatches(row, source, delta) &&
!objectOrNull(source.quota_spend_commit) &&
vision?.schema_version === "goal_vision_replan_contract_v0" && vision.agent_id === row.agent_id &&
(vision.goal_id == null || vision.goal_id === row.goal_id) &&
Expand All @@ -137,8 +155,10 @@ function recordedDecision(row: JsonObject): JsonObject | null {
let stride: JsonObject | null;
try { stride = normalizeRecordedStrideDecision(objectOrNull(source.agent_vision)?.stride_decision); }
catch { return null; }
const autonomous = objectOrNull(source.settlement_identity)?.schema_version === SCOPED_SETTLEMENT_IDENTITY_SCHEMA_VERSION;
const decision = {history_evidence_ref: "replan-evidence-" + digest(publicEvidenceRow(row)),
generated_at: row.generated_at, todo_id: row.todo_id, turn_instance_id: source.turn_instance_id,
generated_at: row.generated_at, todo_id: autonomous ? null : row.todo_id, turn_instance_id: source.turn_instance_id,
...(autonomous ? {replan_obligation_id: source.replan_obligation_id} : {}),
goal_ref: ref ? {goal_id: ref.goal_id, goal_instance_id: ref.goal_instance_id} : null,
path_delta: path, ...(stride ? {stride_decision: stride} : {})};
// Existing history refs keep their identity. This derived ref additionally
Expand Down Expand Up @@ -235,6 +255,10 @@ export function validateReplanContext(value: unknown): JsonObject {
identifier(previous.evidence_ref, "previous_decision.evidence_ref");
identifier(previous.history_evidence_ref, "previous_decision.history_evidence_ref");
identifier(previous.turn_instance_id, "previous_decision.turn_instance_id");
if (previous.replan_obligation_id !== undefined) {
identifier(previous.replan_obligation_id, "previous_decision.replan_obligation_id");
if (previous.todo_id !== null) throw new EffectRuntimeRequestError("previous decision cannot bind both Todo and autonomous replan");
}
normalizeGoalPathDelta(previous.path_delta);
normalizeRecordedStrideDecision(previous.stride_decision);
}
Expand Down
4 changes: 3 additions & 1 deletion loopx/control_plane/work_items/replan_context_codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ def replan_evidence_rows(runs: Iterable[Mapping[str, Any]], *, goal_id: str, age
source["goal_ref"] = run.get("goal_ref")
source["agent_vision"] = run.get("agent_vision")
source["autonomous_replan_ack"] = run.get("autonomous_replan_ack")
source["replan_obligation_id"] = run.get("replan_obligation_id")
for key, fields in (("settlement_identity", (
"schema_version", "goal_id", "agent_id", "todo_id", "turn_instance_id", "effect_id")),
"schema_version", "goal_id", "agent_id", "todo_id", "turn_instance_id", "effect_id",
*(("binding_kind", "binding_id", "replan_obligation_id") if include_decisions else ()))),
("quota_spend_commit", ("schema_version", "effect_id"))):
value = run.get(key)
source[key] = {field: value.get(field) for field in fields} if isinstance(value, Mapping) else None
Expand Down
68 changes: 66 additions & 2 deletions tests/control_plane/test_effective_turn_replan.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ def test_due_stride_review_survives_successor_transition_until_decision(
isolate_sqlite_runtime(tmp_path, monkeypatch)
project, runtime, registry = _write_fixture(tmp_path)
data = json.loads(registry.read_text())
data["goals"][0]["quota"]["allowed_slots"] = 4
data["goals"][0]["quota"]["allowed_slots"] = 8
registry.write_text(json.dumps(data))
rc, listed = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID)
assert rc == 0, listed
Expand All @@ -321,7 +321,7 @@ def test_due_stride_review_survives_successor_transition_until_decision(

def call(*args: str, expected_code: int = 0) -> dict:
code, result = _run_cli(registry, runtime, *args, cwd=project)
assert code == expected_code, result
assert code == expected_code, json.dumps(result, indent=2)
return result

call("configure-goal", "--goal-id", GOAL_ID, "--execution-replan-after-turns", "2",
Expand Down Expand Up @@ -438,6 +438,70 @@ def call(*args: str, expected_code: int = 0) -> dict:
assert accepted["autonomous_replan_ack"]["semantic_delta"]["accepted"] is True
persisted = json.loads(Path(accepted["json_path"]).read_text())
assert persisted["agent_vision"]["stride_decision"] == decision
assert persisted["settlement_identity"]["schema_version"] == "quota_settlement_identity_v1"
assert persisted["replan_obligation_id"] == obligation["obligation_id"]
call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID,
"--replan-obligation-id", obligation["obligation_id"], "--turn-instance-id", "stride-due-review",
"--slots", "1", "--source", "heartbeat", "--execute")

# Continue through real authority/history, rather than injecting a crafted
# context. The next periodic review must see the accepted autonomous decision.
for index in range(2):
turn = f"stride-followthrough-{index}"
call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID,
"--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", turn,
"--scan-path", str(project))
call("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID,
"--todo-id", TODO_ID, "--turn-instance-id", turn,
"--classification", "validated_progress", "--delivery-batch-scale", "implementation",
"--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation",
"--no-global-sync", "--suppress-external-sinks")
call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID,
"--todo-id", TODO_ID, "--turn-instance-id", turn,
"--slots", "1", "--source", "heartbeat", "--execute")
next_guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID,
"--agent-id", AGENT_ID,
"--turn-instance-id", "stride-next-review", "--scan-path", str(project))
next_obligation = next_guard["autonomous_replan_obligation"]
next_successor = call("todo", "add", "--goal-id", GOAL_ID, "--role", "agent",
"--claimed-by", AGENT_ID, "--text", "Resolve the remaining source uncertainty",
"--task-class", "advancement_task", "--action-kind", "validate",
"--target-key", "source-uncertainty", "--replan-obligation-id", next_obligation["obligation_id"])
next_guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID,
"--agent-id", AGENT_ID, "--todo-id", next_successor["todo_id"],
"--turn-instance-id", "stride-next-review", "--scan-path", str(project))
next_obligation = next_guard["autonomous_replan_obligation"]
next_control = next_guard["replan_action_packet"]["stride_control"]
follow = next_obligation["replan_context"]["decision_followthrough"]
previous = follow["previous_decision"]
assert previous["todo_id"] is None
assert previous["replan_obligation_id"] == obligation["obligation_id"]
assert previous["stride_decision"] == decision
assert follow["execution_and_goal_gain"] == "unknown"
assert next_control["previous_decision_ref"] == previous["evidence_ref"]
resolved = call("history", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID,
"--evidence-ref", previous["evidence_ref"])
assert resolved["evidence"]["stride_decision"] == decision
next_decision = {**decision, "reference_context_id": next_control["reference_context_id"],
"reference_obligation_id": next_obligation["obligation_id"]}
refresh_args = ("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID,
"--replan-obligation-id", next_obligation["obligation_id"], "--turn-instance-id", "stride-next-review",
"--agent-vision-json", str(decision_path), "--classification", "bounded_replan_progress",
"--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress",
"--no-global-sync", "--suppress-external-sinks")
decision_path.write_text(json.dumps({**vision_packet, "stride_decision": next_decision}))
missing_review = call(*refresh_args, expected_code=1)
assert missing_review.get("appended") is not True
assert "previous" in missing_review["error"].lower()
next_decision["previous_review"] = {"reference_decision_ref": previous["evidence_ref"],
"adoption": "unknown", "information_gain": "unknown", "goal_gain": "unknown",
"evidence_refs": [previous["history_evidence_ref"]],
"lesson": "The decision is recorded; subsequent work does not yet prove target gain."}
decision_path.write_text(json.dumps({**vision_packet, "stride_decision": next_decision}))
next_accepted = call(*refresh_args)
assert next_accepted["autonomous_replan_ack"]["semantic_delta"]["accepted"] is True
replay = call(*refresh_args)
assert replay.get("appended") is not True


@pytest.mark.parametrize("invalid", [0, 7, True, 2.5, "2"])
Expand Down
58 changes: 57 additions & 1 deletion tests/control_plane_ts/replan_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {createHash} from "node:crypto";
import {projectReplanContext, validateReplanContext} from "../../loopx/control_plane/work_items/replan_context.ts";
import type {JsonObject} from "../../loopx/control_plane/effect_program.ts";
import {settlementIdentityPayload, type JsonObject} from "../../loopx/control_plane/effect_program.ts";

const progress = {schema_version: "typed_progress_observation_v0", result_class: "unchanged",
surface_id: "surface-a", work_item_id: "todo-a", fingerprint: "same-work"};
Expand Down Expand Up @@ -130,6 +130,62 @@ function subsequent(position: number): JsonObject {

const strideRequest = {...request, stride_strategy: "evidence_conditioned_v1"};

function autonomousReviewed(position = 0, obligation = "replan-first"): JsonObject {
const legacy = reviewed(position);
const source = legacy._source_facts as JsonObject;
const ack = source.autonomous_replan_ack as JsonObject;
const identity = settlementIdentityPayload({goal_id: "goal-a", agent_id: "agent-a",
turn_instance_id: String(source.turn_instance_id), replan_obligation_id: obligation});
return {...legacy, todo_id: undefined, _source_facts: {...source, todo_id: null,
replan_obligation_id: obligation, settlement_identity: identity,
autonomous_replan_ack: {...ack, semantic_delta: {...ack.semantic_delta, obligation_id: obligation}}}};
}

test("autonomous decisions return through their own binding and resolve without a fabricated Todo", () => {
const latest = autonomousReviewed(2);
const rows = [reviewed(), latest, subsequent(3)];
const context = projectReplanContext({...strideRequest, rows});
const follow = context.decision_followthrough as JsonObject;
assert.equal(follow.previous_decision.turn_instance_id, "turn-3");
assert.equal(follow.previous_decision.replan_obligation_id, "replan-first");
assert.equal(follow.previous_decision.todo_id, null);
assert.equal(follow.recorded_progress_count, 1);
assert.equal(follow.execution_and_goal_gain, "unknown");
assert.deepEqual(validateReplanContext(context), context);
const resolve = {...request, operation: "resolve", evidence_ref: follow.previous_decision.evidence_ref, rows};
assert.deepEqual(projectReplanContext(resolve).evidence, follow.previous_decision);
assert.throws(() => projectReplanContext({...resolve, rows: [autonomousReviewed(2, "replan-other")]}), /unavailable/);
const ordinary = projectReplanContext({...request, rows});
assert.deepEqual(projectReplanContext({...request, rows, stride_strategy: "off"}), ordinary);
assert.equal(ordinary.decision_followthrough, undefined);
});

test("autonomous followthrough rejects conflicting identities, bindings and acknowledgements", () => {
const valid = autonomousReviewed();
const source = valid._source_facts as JsonObject;
const identity = source.settlement_identity as JsonObject;
const ack = source.autonomous_replan_ack as JsonObject;
const corruptions = [
...["schema_version", "goal_id", "agent_id", "turn_instance_id", "effect_id", "binding_kind",
"binding_id", "replan_obligation_id"].map(key => ({settlement_identity: {...identity, [key]: "wrong"}})),
{settlement_identity: {...identity, todo_id: "todo-conflict"}},
{todo_id: "todo-conflict"}, {goal_id: "wrong"}, {agent_id: "wrong"}, {turn_instance_id: "wrong"},
{replan_obligation_id: null}, {replan_obligation_id: "wrong"},
{autonomous_replan_ack: {...ack, recorded: false}},
{autonomous_replan_ack: {...ack, semantic_delta: {...ack.semantic_delta, obligation_id: "wrong"}}},
{autonomous_replan_ack: {...ack, semantic_delta: {...ack.semantic_delta, accepted: false}}},
{quota_spend_commit: {schema_version: "quota_spend_commit_receipt_v0", effect_id: identity.effect_id + "#quota_spend"}},
];
for (const patch of corruptions) {
assert.equal(projectReplanContext({...strideRequest,
rows: [{...valid, _source_facts: {...source, ...patch}}]}).decision_followthrough, undefined, JSON.stringify(patch));
}
assert.equal(projectReplanContext({...strideRequest, rows: [{...valid, todo_id: "todo-conflict"}]}).decision_followthrough, undefined);
const corruptPath = autonomousReviewed(2);
corruptPath._source_facts.agent_vision.path_delta = {outcome: "replan"};
assert.equal(projectReplanContext({...strideRequest, rows: [reviewed(), corruptPath]}).decision_followthrough, undefined);
});

test("a prior accepted path returns with attributable later observations, never a utility verdict", () => {
const decision = reviewed();
const later = subsequent(1);
Expand Down
Loading