From f574ec73fc0a7bdca7ee0e2993359ec87ea72686 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:19:50 +0800 Subject: [PATCH] refactor(todos): retire unused decision-scope scalar crossings Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../10-autonomous-agent-quality-gates.md | 53 +++++++++---------- docs/reference/protocols/decision-scope-v0.md | 17 +++++- loopx/control_plane/todos/decision_scope.py | 29 ---------- loopx/control_plane/todos/decision_scope.ts | 8 ++- ...test_quota_authority_settlement_journey.py | 43 +++++++++++++-- ...st_todo_decision_scope_runtime_boundary.py | 28 +++++----- tests/control_plane_ts/decision_scope.test.ts | 13 ++++- 7 files changed, 110 insertions(+), 81 deletions(-) diff --git a/docs/development/control-plane-course/10-autonomous-agent-quality-gates.md b/docs/development/control-plane-course/10-autonomous-agent-quality-gates.md index 0d29f63f16..1f483a44e0 100644 --- a/docs/development/control-plane-course/10-autonomous-agent-quality-gates.md +++ b/docs/development/control-plane-course/10-autonomous-agent-quality-gates.md @@ -409,38 +409,33 @@ compatibility flag,都不能改变当前 agent 的最终 decision。 ### 核心代码领读:可见提醒为什么不是 Authority -入口是 -`loopx/control_plane/todos/decision_scope.py::build_required_decision_scope_consistency`: - -```python -gates = [item for item in user_items if is_user_gate_todo_item(item)] -user_actions = [item for item in user_items if not is_user_gate_todo_item(item)] - -matching_gates = [ - gate - for gate in gates - if decision_scope_covers(gate.get("decision_scope"), required_scope) -] -compatible_gates = [ - gate - for gate in matching_gates - if _gate_owner_compatible(gate, agent_id=effective_owner) -] -if compatible_gates: - continue +Python 入口 `decision_scope.py::build_required_decision_scope_consistency` 只负责 +输入 codec 和返回 schema 检查。规则 owner 是同目录 +`decision_scope.ts::decisionScopeConsistency`;它先区分 live gate 和非阻塞 action, +再检查 scope、owner 和精确目标: + +```ts +const matching = gates.filter(gate => decisionScopeCovers(gate.decision_scope, scope)); +const compatible = matching.filter(gate => addressed(gate, owner)); +const conflicting = compatible.filter(gate => + todoGateRelation(gate, item)?.state === "projection_repair_required"); +if (conflicting.length) { + // 记录 required_decision_scope_target_mismatch;不得由另一 gate 掩盖。 + continue; +} +if (compatible.length) continue; ``` -只有 task class 正确、scope 覆盖且 owner compatible 的 gate 才满足依赖。随后 -`matching_actions` 只用于产出错误归因: +只有 task class、scope、owner 和精确目标一致的 gate 才满足依赖;依赖一致不等于 +批准或 lease。随后 `matchingActions` 只用于错误归因 +`non_blocking_user_action_scope_collision`,不能成为 authority。只有不兼容 gate 时 +归因 `required_decision_scope_gate_owner_mismatch`;没有匹配来源时归因 +`dangling_required_decision_scope`。standing authority 的冲突与拒绝/取消后的阻塞要求 +也由这个 TS owner 检查。 -```python -if matching_actions: - reason_code = "non_blocking_user_action_scope_collision" -elif matching_gates: - reason_code = "required_decision_scope_gate_owner_mismatch" -else: - reason_code = "dangling_required_decision_scope" -``` +覆盖范围和精确目标的内部 TS 函数仍被组合规则使用。它们已无生产调用方的三个 +Python scalar adapter 与对应 RPC 操作已退役;生产调用继续使用 consistency、 +组合 relation、批量 relations 和 gate scope projection,不能另建 Python 决策源。 沿 `build_required_decision_scope_repair_hint` 再读一步:repair 可以修 projection,但 `user_action remains non-blocking`,因此 repair route 也没有获得受限 delivery authority。 diff --git a/docs/reference/protocols/decision-scope-v0.md b/docs/reference/protocols/decision-scope-v0.md index b4ff01c345..7aec00be05 100644 --- a/docs/reference/protocols/decision-scope-v0.md +++ b/docs/reference/protocols/decision-scope-v0.md @@ -9,8 +9,8 @@ whether a safe fallback may continue, or whether the projection itself needs repair. This contract turns the interaction catalog's Decision Scope Model into a -machine-facing schema. It does not implement the runtime migration by itself; -CLI/state/status/quota consumers should use this shape as the migration target. +machine-facing schema. The shared TypeScript decision-dependency owner evaluates +it for status/quota; Python adapters normalize inputs and validate responses. ## Fields @@ -110,6 +110,19 @@ comparison and notification behavior. ## Status And Quota Rules +### Typed transport boundary + +The internal `todo.decision_scope.evaluate` transport retains `consistency`, +`standing`, `relation`, `relations`, `fallback`, and `gate_scopes`. The unused +scalar operations `covers`, `scope_relation`, and `exact_relation`, and their +Python adapters, have been removed; those operation requests fail explicitly. +The underlying TypeScript coverage and exact-target rules remain part of the +combined evaluation, including rejection of conflicting exact targets. + +This retirement changes an internal transport surface, not Todo metadata, +user-facing CLI operations, provider defaults, permissions, or approval +consumption. Existing input codecs and response schema/cardinality checks remain. + Status and quota should read decision scopes in this order: 1. explicit `decision_scope`, `required_decision_scopes`, and `safety_class`; diff --git a/loopx/control_plane/todos/decision_scope.py b/loopx/control_plane/todos/decision_scope.py index 1966f9b4ac..0a96f738a4 100644 --- a/loopx/control_plane/todos/decision_scope.py +++ b/loopx/control_plane/todos/decision_scope.py @@ -296,35 +296,6 @@ def build_required_decision_scope_repair_hint( return result -def decision_scope_covers(gate_scope: Any, required_scope: Any) -> bool: - gate = normalize_todo_decision_scope(gate_scope) - required = normalize_todo_decision_scope(required_scope) - if not gate or not required: - return False - result = _evaluate("covers", gate_scope=gate, required_scope=required) - if not isinstance(result, bool): - raise TypeError("invalid typed decision scope covers projection") - return result - - -def decision_scope_gate_relation(gate: dict[str, Any], agent_item: dict[str, Any]) -> dict[str, Any] | None: - return _projection( - "scope_relation", - _evaluate("scope_relation", gate=_facts(gate), item=_facts(agent_item)), - schema_versions=frozenset({DECISION_SCOPE_RELATION_SCHEMA_VERSION}), - nullable=True, - ) - - -def exact_todo_gate_relation(gate: dict[str, Any], agent_item: dict[str, Any]) -> dict[str, Any] | None: - return _projection( - "exact_relation", - _evaluate("exact_relation", gate=_facts(gate), item=_facts(agent_item)), - schema_versions=frozenset({TODO_GATE_RELATION_SCHEMA_VERSION}), - nullable=True, - ) - - def todo_gate_relation(gate: dict[str, Any], agent_item: dict[str, Any]) -> dict[str, Any] | None: return _optional_relation( "relation", diff --git a/loopx/control_plane/todos/decision_scope.ts b/loopx/control_plane/todos/decision_scope.ts index ab5ab73bec..085a5cd588 100644 --- a/loopx/control_plane/todos/decision_scope.ts +++ b/loopx/control_plane/todos/decision_scope.ts @@ -1,6 +1,7 @@ /** Read-only decision dependency rules over one complete source snapshot. * A consistent dependency is not approval, a lease, or a mutation receipt. */ import type {JsonObject} from "../effect_program.ts"; +import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import {requireJsonObject, optionalNonEmptyString, requireBoolean, requireInteger} from "../runtime_decode.ts"; import {gateAddressesAgent} from "./agent_scope.ts"; import { @@ -227,22 +228,19 @@ export function projectTodoGateScopes(request: JsonObject): JsonObject { export function evaluateDecisionScope(value: unknown): JsonObject { const request = requireJsonObject(value, "decision scope request"); if (request.schema_version !== DECISION_SCOPE_REQUEST_SCHEMA) throw new TypeError("decision scope request schema mismatch"); - let result: JsonObject | boolean | null | (JsonObject | null)[][]; + let result: JsonObject | null | (JsonObject | null)[][]; switch (request.operation) { case "fallback": result = selectScopedGateFallback(request); break; case "gate_scopes": result = projectTodoGateScopes(request); break; case "consistency": result = decisionScopeConsistency(request); break; case "standing": result = scopeStandingAuthority(request.authority, optionalNonEmptyString(request.agent_id, "agent_id")); break; - case "covers": result = decisionScopeCovers(request.gate_scope, request.required_scope); break; case "relations": { const items = rows(request.items); result = rows(request.gates).map(gate => items.map(item => todoGateRelation(gate, item))); break; } case "relation": result = todoGateRelation(requireJsonObject(request.gate, "gate"), requireJsonObject(request.item, "item")); break; - case "scope_relation": result = decisionScopeRelation(requireJsonObject(request.gate, "gate"), requireJsonObject(request.item, "item")); break; - case "exact_relation": result = exactTodoGateRelation(requireJsonObject(request.gate, "gate"), requireJsonObject(request.item, "item")); break; - default: throw new TypeError("unsupported decision scope operation"); + default: throw new EffectRuntimeRequestError("unsupported decision scope operation"); } return {schema_version: "todo_decision_scope_result_v0", result}; } diff --git a/tests/control_plane/test_quota_authority_settlement_journey.py b/tests/control_plane/test_quota_authority_settlement_journey.py index f2911530ba..1fd95e47d8 100644 --- a/tests/control_plane/test_quota_authority_settlement_journey.py +++ b/tests/control_plane/test_quota_authority_settlement_journey.py @@ -27,11 +27,14 @@ def _row(todo_id: str, *, status: str = "open", extra: str = "") -> str: def _source(root: Path, *, provider: str, status: str = "open", extra: str = "", empty: bool = False, - handoff_mode: str = "soft_claim"): + handoff_mode: str = "soft_claim", user_rows: str = ""): project, runtime, registry = cli._write_fixture(root) goal = json.loads(registry.read_text())["goals"][0] path = project / goal["state_file"] - prefix = path.read_text().split("## Agent Todo")[0] + "## Agent Todo\n\n" + prefix = path.read_text().split("## Agent Todo")[0] + if user_rows: + prefix += "## User Todo / Owner Review Reading Queue\n\n" + user_rows + "\n" + prefix += "## Agent Todo\n\n" rows = "".join(_row(f"todo_ready_{i}") for i in range(35)) path.write_text(prefix + ("" if empty else rows + _row(cli.TODO_ID, status=status, extra=extra))) if provider != "legacy": @@ -39,7 +42,9 @@ def _source(root: Path, *, provider: str, status: str = "open", extra: str = "", initialize_canonical_authority( runtime, cli.GOAL_ID, build_todo_runtime_shadow_projection( - goal_id=cli.GOAL_ID, todos=fields["agent_todos"]["items"], handoff_mode=handoff_mode, + goal_id=cli.GOAL_ID, + todos=fields["agent_todos"]["items"] + fields.get("user_todos", {}).get("items", []), + handoff_mode=handoff_mode, ), state_path=path, provider=provider, ) @@ -82,6 +87,38 @@ def test_exact_selection_reaches_work_beyond_display_limits(tmp_path, provider): assert guard["heartbeat_receipt"]["settlement_identity"]["todo_id"] == cli.TODO_ID +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("conflicting_target", [False, True]) +def test_canonical_decision_gate_cannot_be_bypassed_by_exact_selection( + tmp_path, provider, conflicting_target, +): + target = "todo_other" if conflicting_target else cli.TODO_ID + user_rows = ( + "- [ ] [P0] Decide whether the selected write may proceed.\n" + f" \n" + ) + project, runtime, registry, path, prefix = _source( + tmp_path, provider=provider, + extra=f"claimed_by={cli.AGENT_ID} required_decision_scopes=write_scope:action:release", + user_rows=user_rows, + ) + # An apparently unblocked display cannot erase the canonical gate/requirement. + path.write_text(prefix.split("## User Todo")[0] + "## Agent Todo\n\n" + _row(cli.TODO_ID)) + _, guard = _guard(project, runtime, registry) + assert guard["normal_delivery_allowed"] is False, guard + if conflicting_target: + consistency = guard["todo_decision_scope_consistency"] + assert consistency["ok"] is False + assert any(error["reason_code"] == "required_decision_scope_target_mismatch" + for error in consistency["errors"]) + current = list_goal_todos(registry_path=registry, goal_id=cli.GOAL_ID, + runtime_root_arg=str(runtime), todo_id=cli.TODO_ID)["todo"] + assert current["required_decision_scopes"][0]["scope_key"] == "release" + assert cli._spend_run_count(runtime) == 0 + + @pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) @pytest.mark.parametrize("extra", [f"excluded_agents={cli.AGENT_ID}", "claimed_by=agent-other", "required_capabilities=production_access"]) diff --git a/tests/control_plane/test_todo_decision_scope_runtime_boundary.py b/tests/control_plane/test_todo_decision_scope_runtime_boundary.py index f3af89d842..4f57254799 100644 --- a/tests/control_plane/test_todo_decision_scope_runtime_boundary.py +++ b/tests/control_plane/test_todo_decision_scope_runtime_boundary.py @@ -5,6 +5,7 @@ import pytest from loopx.control_plane.todos import decision_scope +from loopx.control_plane.effect_runtime import EffectRuntimeRejected, effect_runtime_result SCOPE = { "schema_version": "decision_scope_v0", @@ -38,7 +39,6 @@ def _response(value: object) -> dict[str, object]: @pytest.mark.parametrize( ("invoke", "value"), [ - (lambda: decision_scope.decision_scope_covers(SCOPE, SCOPE), 1), ( lambda: decision_scope.build_required_decision_scope_consistency( {"first_open_items": [AGENT_ITEM]}, @@ -54,14 +54,7 @@ def _response(value: object) -> dict[str, object]: ), {"schema_version": "wrong_standing_v0"}, ), - ( - lambda: decision_scope.decision_scope_gate_relation(GATE, AGENT_ITEM), - {"schema_version": "todo_gate_relation_v0"}, - ), - ( - lambda: decision_scope.exact_todo_gate_relation(GATE, AGENT_ITEM), - {"schema_version": "decision_scope_relation_v0"}, - ), + (lambda: decision_scope.todo_gate_relation(GATE, AGENT_ITEM), True), ( lambda: decision_scope.todo_gate_relation(GATE, AGENT_ITEM), {"schema_version": "unknown_relation_v0"}, @@ -123,7 +116,7 @@ def test_outer_runtime_envelope_fails_closed( ) with pytest.raises(TypeError, match="invalid typed decision scope projection"): - decision_scope.decision_scope_covers(SCOPE, SCOPE) + decision_scope.todo_gate_relation(GATE, AGENT_ITEM) def test_nullable_operations_still_accept_explicit_null( @@ -135,8 +128,6 @@ def test_nullable_operations_still_accept_explicit_null( lambda *_args, **_kwargs: _response(None), ) - assert decision_scope.decision_scope_gate_relation(GATE, AGENT_ITEM) is None - assert decision_scope.exact_todo_gate_relation(GATE, AGENT_ITEM) is None assert decision_scope.todo_gate_relation(GATE, AGENT_ITEM) is None assert decision_scope.select_scoped_gate_fallback( [GATE], [AGENT_ITEM], agent_id="agent-a", allow_unrelated_gate=True, @@ -152,3 +143,16 @@ def test_fallback_runtime_result_fails_closed(monkeypatch, value): [GATE], [AGENT_ITEM], agent_id="agent-a", allow_unrelated_gate=True, monitor_debt_backoff_active=False, ) + + +@pytest.mark.parametrize("operation", ["covers", "scope_relation", "exact_relation"]) +def test_retired_scalar_operations_are_rejected_by_real_runtime(operation: str) -> None: + with pytest.raises(EffectRuntimeRejected, match="unsupported decision scope operation"): + effect_runtime_result("todo.decision_scope.evaluate", { + "schema_version": "todo_decision_scope_request_v0", + "operation": operation, + "gate_scope": SCOPE, + "required_scope": SCOPE, + "gate": {**GATE, "is_gate": True}, + "item": AGENT_ITEM, + }) diff --git a/tests/control_plane_ts/decision_scope.test.ts b/tests/control_plane_ts/decision_scope.test.ts index 6582a8a564..29d3154c73 100644 --- a/tests/control_plane_ts/decision_scope.test.ts +++ b/tests/control_plane_ts/decision_scope.test.ts @@ -67,10 +67,21 @@ test("complete production-scale history preserves the conflict beyond display li test("batched relations preserve pair ordering and fail on unknown protocol operations", () => { const result = evaluateDecisionScope({schema_version: "todo_decision_scope_request_v0", operation: "relations", gates: [gate], items: [item, {...item, required_decision_scopes: []}]}); - assert.deepEqual(result.result, [[todoGateRelation(gate, item), todoGateRelation(gate, {...item, required_decision_scopes: []})]]); + const matrix = result.result as JsonObject[][]; + assert.deepEqual(matrix.map(row => row.map(relation => relation.state)), [["gate_covers_action", "independent"]]); + assert.deepEqual(matrix.map(row => row.map(relation => relation.agent_todo_id)), [["todo_work", "todo_work"]]); assert.throws(() => evaluateDecisionScope({schema_version: "todo_decision_scope_request_v0", operation: "approve"}), /unsupported/); }); +test("retired scalar transport operations fail closed while internal rules remain available", () => { + for (const operation of ["covers", "scope_relation", "exact_relation"]) { + assert.throws(() => evaluateDecisionScope({schema_version: "todo_decision_scope_request_v0", + operation, gate_scope: scope, required_scope: scope, gate, item}), /unsupported decision scope operation/); + } + assert.equal(decisionScopeCovers(scope, scope), true); + assert.equal(todoGateRelation({...gate, unblocks_todo_id: "todo_other"}, item)?.state, "projection_repair_required"); +}); + test("gate scope projection qualifies every addressed live dependency without granting authority", () => { const independent = {...gate, decision_scope: undefined, unblocks_todo_id: "todo_other"};