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/agent-loop-effect-interpreter-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,16 @@ commands bind the original actor and route. Receipt repair reuses the existing
idempotent writer. This is a bounded M7.4 adoption with no shared executor or
new authority store; it does not certify terminal Todo or Goal acceptance.

The R5 compact projection also retains the existing CLI settlement plan intact,
including its effect identity, ordered conditional steps and host handoff. Action
signature coverage v5 detects removal or mutation of that plan; packets without
one retain their historical coverage. Real CLI validation exercises premature
spend rejection and one original-Turn writeback/spend with idempotent replay.
This closes a projection omission, not the short-context rollout: same-Turn
cached detail delivery, normal/replan context selection and measured model
behavior remain unqualified. The 8 KiB target and delivery growth checks stay
unchanged. See [TurnEnvelope](../../reference/protocols/turn-envelope-v0.md).

### What Is Missing

- A generic shared executor is deliberately absent. The current adapters share
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ M7 只有在至少产生一个下列最终 effect 时才有理由存在:

配额收尾适配现在消费 TS readback 按回执归约的结算进度,不再独立把存在 spend run 当作已结算。正常刷新、重放和扣款响应共享该投影;可执行命令绑定原 Agent 和路由。补回执复用现有幂等 writer。这是有界的 M7.4 接入,没有增加共享 executor 或 authority store,也不证明 Todo 终态或 Goal 验收完成。

R5 短包投影也完整保留已有 CLI 结算计划,包括 effect identity、带条件的步骤顺序和
宿主交接。签名覆盖 v5 能检测计划被删除或修改;没有计划的输入保留原覆盖版本。
真实 CLI 验证覆盖提前扣额度被拒绝、原 Turn 写回和结算一次,以及幂等重放。
这补上了投影缺口;同 Turn 缓存详情、普通轮与 replan 上下文选择、模型行为收益
尚未验收,8 KiB 目标和交付时增长检查保持不变。见
[TurnEnvelope](../../reference/protocols/turn-envelope-v0.md)。

### 还缺什么

- 通用共享 executor 被有意保留为空。当前 adapter 共享 plan/receipt algebra,却拥有不同的执行边界,因此 M7.3 应以 no-follow-up 关闭,而不是用推测性 framework 填充。
Expand Down
22 changes: 20 additions & 2 deletions docs/reference/protocols/turn-envelope-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ payload.
Preview it explicitly:

```bash
loopx quota should-run --goal-id <goal-id> --agent-id <agent-id> --turn-envelope
loopx --format json quota should-run --goal-id <goal-id> --agent-id <agent-id> --turn-envelope
```

The envelope flag selects a projection of the full decision. The original v0
Expand Down Expand Up @@ -43,13 +43,31 @@ Action-signature coverage is versioned independently from the envelope schema.
`turn_envelope_action_dimensions_v1` additionally covers a blocking user
gate's `response_plan`; `turn_envelope_action_dimensions_v2` additionally signs
`action.action_portfolio`; `turn_envelope_action_dimensions_v3` additionally
signs `action.planning_horizon`. Base/head qualification accepts a declared
signs `action.planning_horizon`; v4 additionally signs capability `agent_context`;
`turn_envelope_action_dimensions_v5` additionally preserves the canonical
`writeback.settlement_plan`. Base/head qualification accepts a declared
coverage migration as a review signal. The bounded, JSON-only v2 and v3
migration budgets apply only to their named schema transitions; ordinary
growth limits resume once the new version is the baseline. A digest change
without a supported coverage migration, or a projection above its one-version
budget, still fails closed.

For a decision carrying a settlement plan, the opt-in envelope now transports
that plan intact: effect identity, ordered steps, command conditions, expected
receipts and the host-owned handoff. It also keeps the corresponding next CLI
commands untruncated. This repairs the earlier preview that could report matching
action hashes while omitting the settlement plan. The plan remains owned by the
shared settlement algebra; the envelope neither rebuilds it nor grants authority
to execute an unadmitted step. Packets without a plan keep their previous coverage
and do not acquire one from a historical heartbeat receipt. Stored v0–v4 signatures
are not rewritten. The v5 migration is an explicit semantic review signal and
has **no additional size allowance**; overflow still requires the existing budget
analysis. Default full quota output and settlement rules are unchanged.

中文:短包现在完整保留已有结算计划及执行命令,签名 v5 覆盖结算身份、步骤顺序、
条件和宿主边界;没有计划的输入不会凭空获得结算权限。旧签名保留,默认完整输出
不变,大小预算不放宽。此修复是短上下文实验的前置条件,尚不证明模型收益。

`quota_planning_horizon_v0` remains advisory even when carried by the envelope.
Its `selection_contract` points back to `selected_todo` and `action_portfolio`,
and `horizon_changes_selection=false`. Effect Program transports this
Expand Down
1 change: 1 addition & 0 deletions loopx/control_plane/quota/turn_envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ACTION_SIGNATURE_COVERAGE_V2 = "turn_envelope_action_dimensions_v2"
ACTION_SIGNATURE_COVERAGE_V3 = "turn_envelope_action_dimensions_v3"
ACTION_SIGNATURE_COVERAGE_V4 = "turn_envelope_action_dimensions_v4"
ACTION_SIGNATURE_COVERAGE_V5 = "turn_envelope_action_dimensions_v5"
ACTION_SIGNATURE_COVERAGE = ACTION_SIGNATURE_COVERAGE_V0
PLANNING_HORIZON_DETAIL_REFS_REF = "$.detail_ref"

Expand Down
11 changes: 10 additions & 1 deletion loopx/control_plane/quota/turn_envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const ACTION_SIGNATURE_COVERAGE_V1 = "turn_envelope_action_dimensions_v1"
export const ACTION_SIGNATURE_COVERAGE_V2 = "turn_envelope_action_dimensions_v2";
export const ACTION_SIGNATURE_COVERAGE_V3 = "turn_envelope_action_dimensions_v3";
export const ACTION_SIGNATURE_COVERAGE_V4 = "turn_envelope_action_dimensions_v4";
export const ACTION_SIGNATURE_COVERAGE_V5 = "turn_envelope_action_dimensions_v5";
export const ACTION_SIGNATURE_COVERAGE = ACTION_SIGNATURE_COVERAGE_V0;

const EXECUTABLE_CLI_ARGS_MAX_ITEMS = 64;
Expand Down Expand Up @@ -640,6 +641,8 @@ function actionProjection(payload: JsonObject, protocolActionFields: JsonObject)
if (nextCliActions.length === 0 && Array.isArray(cliChannel.next_cli_actions)) {
nextCliActions = [...cliChannel.next_cli_actions].map(pythonString);
}
const settlementPlan = object(cliChannel.settlement_plan);
const hasSettlementPlan = Object.keys(settlementPlan).length > 0;
let preserveBoundReplanCommands = replanSettlementOnly;
if (replanPacket && !replanSettlementOnly) {
const writebackContract = object(object(payload.replan_action_packet).writeback_contract);
Expand Down Expand Up @@ -675,10 +678,15 @@ function actionProjection(payload: JsonObject, protocolActionFields: JsonObject)
} else {
// Original-Turn identities often follow an absolute runtime path. Cutting
// a closeout command into display text can erase its binding or execute flag.
writeback.next_cli_actions = preserveBoundReplanCommands
writeback.next_cli_actions = hasSettlementPlan
? nextCliActions.map(command => scalarString(command, "settlement command"))
: preserveBoundReplanCommands
? nextCliActions.slice(0, 5).map(command => scalarString(command, "bound replan closeout command"))
: textList(nextCliActions, 5, 420);
}
// Transport the canonical plan intact. Reconstructing it from command previews
// loses the effect identity, conditional closeout and host/agent boundary.
if (hasSettlementPlan) writeback.settlement_plan = settlementPlan;
for (const field of ["replan_settlement_contract", "delivery_workspace_causality"]) {
const value = object(cliChannel[field]);
if (Object.keys(value).length > 0) writeback[field] = value;
Expand Down Expand Up @@ -763,6 +771,7 @@ function turnActionProjection(payload: JsonObject, protocolActionFields: JsonObj
}

function signatureCoverage(envelope: JsonObject, responsePlanValue: unknown): string {
if (Object.keys(object(object(envelope.writeback).settlement_plan)).length > 0) return ACTION_SIGNATURE_COVERAGE_V5;
if (Object.keys(object(envelope.agent_context)).length > 0) return ACTION_SIGNATURE_COVERAGE_V4;
const action = object(envelope.action);
if (Object.keys(object(action.planning_horizon)).length > 0) return ACTION_SIGNATURE_COVERAGE_V3;
Expand Down
6 changes: 6 additions & 0 deletions loopx/control_plane/testing/cli_output_differential.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ACTION_SIGNATURE_COVERAGE_V2,
ACTION_SIGNATURE_COVERAGE_V3,
ACTION_SIGNATURE_COVERAGE_V4,
ACTION_SIGNATURE_COVERAGE_V5,
)


Expand Down Expand Up @@ -402,6 +403,11 @@ def _action_signature_migration(
(ACTION_SIGNATURE_COVERAGE_V1, ACTION_SIGNATURE_COVERAGE_V4),
(ACTION_SIGNATURE_COVERAGE_V2, ACTION_SIGNATURE_COVERAGE_V4),
(ACTION_SIGNATURE_COVERAGE_V3, ACTION_SIGNATURE_COVERAGE_V4),
(ACTION_SIGNATURE_COVERAGE_V0, ACTION_SIGNATURE_COVERAGE_V5),
(ACTION_SIGNATURE_COVERAGE_V1, ACTION_SIGNATURE_COVERAGE_V5),
(ACTION_SIGNATURE_COVERAGE_V2, ACTION_SIGNATURE_COVERAGE_V5),
(ACTION_SIGNATURE_COVERAGE_V3, ACTION_SIGNATURE_COVERAGE_V5),
(ACTION_SIGNATURE_COVERAGE_V4, ACTION_SIGNATURE_COVERAGE_V5),
}
if not (
isinstance(base_coverages, list)
Expand Down
18 changes: 17 additions & 1 deletion tests/control_plane/test_cli_output_differential.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ def test_unknown_action_portfolio_schema_migration_fails_closed() -> None:
def test_unknown_action_signature_coverage_migration_fails_closed() -> None:
candidate = _row(
action_signature_sha256="unknown-semantic-signature",
action_signature_coverages=["turn_envelope_action_dimensions_v5"],
action_signature_coverages=["turn_envelope_action_dimensions_v999"],
)

result = compare_cli_output_receipts(_receipt(_row()), _receipt(candidate))
Expand Down Expand Up @@ -1212,3 +1212,19 @@ def test_only_retired_evidence_command_with_real_replacement_is_allowed():
assert not compare_cli_output_receipts(_receipt(base), _receipt())['ok']
assert not compare_cli_output_receipts(_receipt(base), _receipt({**replacement, 'json_shape_paths': []}))['ok']
assert not compare_cli_output_receipts(_receipt({**base, 'row_id': 'surface/status/small/json'}), _receipt(replacement))['ok']


@pytest.mark.parametrize("previous", range(5))
def test_settlement_v5_migration_does_not_waive_output_growth(previous):
base = _row(action_signature_coverages=[f"turn_envelope_action_dimensions_v{previous}"])
candidate = {**base, "action_signature_sha256": "settlement-signature",
"action_signature_coverages": ["turn_envelope_action_dimensions_v5"]}
result = compare_cli_output_receipts(_receipt(base), _receipt(candidate))
assert result["ok"] and result["review_required"]
assert not compare_cli_output_receipts(
_receipt(base), _receipt({**candidate, "chars": 50_000}),
)["ok"]
assert not compare_cli_output_receipts(_receipt(candidate), _receipt(base))["ok"]
assert not compare_cli_output_receipts(
_receipt(candidate), _receipt({**candidate, "action_signature_sha256": "lost-identity"}),
)["ok"]
125 changes: 125 additions & 0 deletions tests/control_plane/test_turn_envelope_settlement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Settlement remains executable and signed through the opt-in compact view."""
from copy import deepcopy

import pytest

from loopx.control_plane.quota.turn_envelope import (
build_turn_envelope,
quota_action_signature_document,
turn_envelope_action_signature_document,
)
from tests.test_turn_envelope import _full_decision
from tests.control_plane.test_quota_settlement_cli import (
AGENT_ID, GOAL_ID, TODO_ID, TURN_ID,
_configure_read_only_todo, _run_cli, _run_generated_cli,
_spend_run_count, _write_fixture,
)


def _plan():
# Independent wire example, not generated by the projection under test.
return {
"schema_version": "quota_settlement_plan_v1",
"identity": {
"schema_version": "quota_settlement_identity_v0",
"goal_id": "fixture-goal", "agent_id": "codex-fixture",
"todo_id": "todo_fixture0001", "turn_instance_id": "turn-original",
"effect_id": "fixture-goal:codex-fixture:todo_fixture0001:turn-original",
},
"ordered_steps": [
{"kind": "validation", "owner": "agent", "command_condition": "todo_deliverable_complete"},
{"kind": "durable_writeback", "owner": "agent", "precondition": "validation succeeded"},
{"kind": "quota_spend", "owner": "agent", "expected_receipt": "quota_spend_receipt"},
{"kind": "terminal_closeout", "owner": "agent", "conditional": True},
],
"host_handoff": {"owner": "host", "kind": "scheduler_handoff", "inside_agent_settlement": False},
}


def test_plan_and_long_commands_survive_without_mutating_cached_packet():
source = _full_decision()
cli = source["interaction_contract"]["cli_channel"]
cli["settlement_plan"] = _plan()
command = "loopx --runtime-root /" + "nested/" * 80 + " quota spend-slot --turn-instance-id turn-original --execute"
cli["next_cli_actions"] = [command]
cli["settlement_plan"]["ordered_steps"][2]["command_template"] = command
before = deepcopy(source)
envelope = build_turn_envelope(source)
assert source == before
assert envelope["writeback"]["settlement_plan"] == before["interaction_contract"]["cli_channel"]["settlement_plan"]
assert envelope["writeback"]["next_cli_actions"] == [command]
assert envelope["action_signature"]["coverage"] == "turn_envelope_action_dimensions_v5"
assert quota_action_signature_document(source) == turn_envelope_action_signature_document(envelope)


@pytest.mark.parametrize("mutation", ["identity", "order", "condition", "receipt", "handoff", "remove"])
def test_signature_rejects_settlement_semantic_loss(mutation):
source = _full_decision()
source["interaction_contract"]["cli_channel"]["settlement_plan"] = _plan()
envelope = build_turn_envelope(source)
plan = envelope["writeback"]["settlement_plan"]
if mutation == "identity":
plan["identity"]["effect_id"] = "another-turn"
elif mutation == "order":
plan["ordered_steps"].reverse()
elif mutation == "condition":
plan["ordered_steps"][-1]["conditional"] = False
elif mutation == "receipt":
plan["ordered_steps"][2]["expected_receipt"] = "validation_receipt"
elif mutation == "handoff":
plan["host_handoff"]["inside_agent_settlement"] = True
else:
del envelope["writeback"]["settlement_plan"]
assert quota_action_signature_document(source) != turn_envelope_action_signature_document(envelope)


def test_absent_plan_is_not_invented_for_historical_or_unbound_packets():
source = _full_decision()
before = build_turn_envelope(source)
assert "settlement_plan" not in before["writeback"]
assert before["action_signature"]["coverage"] == "turn_envelope_action_dimensions_v0"
source["interaction_contract"]["cli_channel"].update(
selection_required=True, next_cli_actions=[], spend_after_validation=False,
)
envelope = build_turn_envelope(source)
assert "settlement_plan" not in envelope["writeback"]
assert envelope["writeback"]["spend_after_validation"] is False


def test_real_cli_envelope_settles_original_turn_once(tmp_path):
project, runtime, registry = _write_fixture(tmp_path)
_configure_read_only_todo(project)
args = ("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID,
"--agent-id", AGENT_ID, "--todo-id", TODO_ID,
"--turn-instance-id", TURN_ID, "--scan-path", str(project))
rc, full = _run_cli(registry, runtime, *args)
assert rc == 0, full
rc, envelope = _run_cli(registry, runtime, *args, "--turn-envelope")
assert rc == 0, envelope
plan = envelope["writeback"]["settlement_plan"]
assert plan == full["interaction_contract"]["cli_channel"]["settlement_plan"]
assert plan["identity"] == full["heartbeat_receipt"]["settlement_identity"]
assert [step["kind"] for step in plan["ordered_steps"]] == [
"validation", "durable_writeback", "quota_spend", "terminal_closeout",
]
assert plan["ordered_steps"][-1]["conditional"] is True
assert plan["host_handoff"]["inside_agent_settlement"] is False
spend_command = plan["ordered_steps"][2]["command_template"].replace("loopx ", "loopx --format json ", 1)
rc, premature = _run_generated_cli(spend_command, registry_path=registry)
assert rc != 0, premature
assert _spend_run_count(runtime) == 0
writeback = plan["ordered_steps"][1]["command_template"].replace("loopx ", "loopx --format json ", 1)
writeback = writeback.replace("<validated_progress>", "validated_progress").replace(
"<scale>", "single_surface").replace("<outcome>", "outcome_progress")
rc, refreshed = _run_generated_cli(
writeback + " --delivery-boundary in_flight_continuation --no-global-sync --suppress-external-sinks",
registry_path=registry,
)
assert rc == 0, refreshed
for replay in (False, True):
rc, spent = _run_generated_cli(spend_command, registry_path=registry)
assert rc == 0, spent
assert spent["settlement_result"]["ok"] is True
if replay:
assert spent["idempotent_replay"] is True
assert _spend_run_count(runtime) == 1
Loading