Skip to content
Open
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
80 changes: 79 additions & 1 deletion docs/reference/protocols/turn-envelope-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,14 +617,92 @@ capsule witnesses. Packet-free v0 envelopes remain supported. The envelope keeps
- validation/writeback and quota-spend policy;
- the current scheduler action and cadence acknowledgement command.

The envelope also carries a bounded `contract_capsule` for interaction mode,
The envelope also carries a `contract_capsule` for interaction mode,
work-lane and execution obligations, successor/replan duties, automation
liveness, vision/handoff state, and actionable warning references. A canonical
`action_signature` is independently built from the full decision and from the
envelope; matching hashes prove the covered action dimensions agree for that
projection. They do not prove that every possible quota state has test
coverage.

The five execution-contract objects `work_lane_contract`,
`execution_obligation`, `autonomous_replan_scope`, `agent_scope_frontier`, and
`automation_liveness` retain their complete source JSON, including nested data,
ordered lists, empty values and unrecognized metadata. Their clauses are not
subject to display truncation. Both quota-envelope emission and direct Turn
composition use this shared producer. The shared structured Host prompt now
forwards the remaining signed capsule for ordinary execution as well as staged
replanning; previously the ordinary path omitted it. Retaining metadata does
not interpret it as a new execution permission or support a new required
semantic. Existing schema/signature coverage and historical captured envelopes
remain readable without rewriting them. Re-projecting an older raw decision
with the corrected producer can produce a different signature; validate an
existing envelope against its saved content and digest. Raw quota output and admission,
scheduler and settlement decisions are unchanged. Larger capsule bytes remain
visible in the existing diagnostic budget warning; this correction does not
claim lower token cost or complete controller-contract unification.

中文:上述五份执行契约完整保留源 JSON,包括嵌套数据、有序列表、空值和未知
元数据,不按展示预算截断条款。quota 短包与直接 Turn 共用该生产者;共享结构化
Host 在普通执行和分阶段重规划中都传递剩余的已签名 capsule,修复普通执行
原先遗漏的问题。保留元数据不表示解释新权限或支持新的必需语义。既有 schema、
签名覆盖与历史捕获内容继续可读,不改写旧记录。用修正后的生产者重新投影旧
原始决定可能得到不同签名;既有 envelope 应按保存内容与摘要校验。原始 quota
输出、准入、调度和
结算决定不变。增长的字节量仍通过既有预算诊断可见;这项修正不证明 token
成本下降,也不表示 controller 契约已完全统一。

Live Turn planning, native Codex prompt construction and structured Host
consumption share the TypeScript owner's version gate. The current consumer
accepts `loopx_turn_envelope_v0`, action-signature schema v0 and the registered
v0–v5 coverage versions. An explicitly unknown or null version is rejected
before Host launch, even when its unchanged action digest still matches. The
historical digest document does not sign these version declarations, so digest
equality alone cannot establish consumer compatibility. Existing v0 callers
that omit signature-version metadata retain their prior behavior; digest,
lineage, current authority and settlement checks remain separate. Saved capture
bytes and old signature documents are not rewritten. This gate does not qualify
a future unified schema or interpret unknown required nested semantics.

中文:实时 Turn 规划、原生 Codex 提示构造和结构化 Host 消费共用 TypeScript
owner 的版本门。当前 consumer 接受 envelope v0、action-signature schema v0
及已注册的 v0–v5 coverage;显式未知或 null 版本即使仍携带匹配的旧行动摘要,
也会在 Host 启动前拒绝。历史摘要文档不签署这些版本声明,摘要相等不能证明
消费者兼容。省略签名版本元数据的既有 v0 调用方保持原行为;摘要、谱系、当前
权限和结算校验仍各自负责。既有捕获字节和历史签名文档不改写。此门不表示未来
统一 schema 已获得资格,也不负责解释未知的必需嵌套语义。

Ordinary structured Host prompts also retain the signed `user`,
`execution_policy`, `scheduler`, `response_plan`, and
`task_orchestration_contract` sections, plus the existing captured-decision
reference when present. These sections are no longer conditional on a staged
replan reader. Notification policy does not grant or cancel execution authority;
carrying surrounding Turn plans does not delegate their effects. The prompt
keeps state writeback, quota and scheduler operations with the outer Turn
executor and asks the work segment to return its semantic result.

中文:普通结构化 Host 还保留已签名的用户通知、执行策略、调度、响应计划、
任务编排五个区段,以及已有的捕获决定引用,不再以分阶段 replan reader
是否存在为条件。通知策略不授予或取消执行权限;传递外围 Turn 计划不等于
委派其副作用。提示明确由外围 Turn executor 负责状态写回、quota 和调度操作,
工作段返回语义结果。

The structured Host also receives signed `agent_id`, `decision`, `should_run`,
`effective_action` and `state`. Its `action` object retains execution flags,
action choices and capability intent while the existing task/planning carriers
keep their established locations. `recommended_action` remains a display
fallback, omitted from this Host projection because it can retain an older
recommendation than the authoritative `primary_action`. Neither a suggested
alternative nor a transported intent overrides admission, scope or effect
ownership. This is input preservation, not a new admission decision or the
unified-contract writer transition.

中文:结构化 Host 同时收到已签名的 Agent 身份、决定、是否运行、有效行动和
状态;`action` 保留执行标记、行动选项及能力意图,任务和规划字段沿用已有位置。
`recommended_action` 是可能落后于权威 `primary_action` 的展示回退,仍不放入
此 Host 投影。建议选项和传递的意图不覆盖准入、作用域或副作用归属。这是输入
保真修复,不增加准入决策,也不表示统一契约 writer 已切换。

Required reads use the authoritative `interaction_contract.agent_channel` list,
including an explicit empty list. Historical interaction-root and payload lists
remain fallbacks when that carrier is absent. Commands retain their exact bytes;
Expand Down
5 changes: 5 additions & 0 deletions loopx/control_plane/quota/turn_envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def _turn_envelope_result(
return dict(result)


def assert_turn_envelope_consumer_version(envelope: Mapping[str, Any]) -> None:
"""Use the typed wire-version gate before any Host consumes this packet."""
_turn_envelope_result("consumer_version", envelope=dict(envelope))


def turn_envelope_action_signature_document(
envelope: Mapping[str, Any],
) -> dict[str, Any]:
Expand Down
79 changes: 38 additions & 41 deletions loopx/control_plane/quota/turn_envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,32 +108,10 @@ const ACTIONABLE_WARNING_FIELDS = [

const CONTRACT_CAPSULE_FIELDS: Readonly<Record<string, readonly string[]>> = {
interaction_contract: ["schema_version", "mode"],
work_lane_contract: [
"schema_version", "lane", "monitor_kind", "next_lane", "obligation",
"must_attempt_work", "reason_codes", "monitor_policy", "selected_todo_id",
"selected_next_due_at", "non_runnable_non_monitor_count",
"material_transition", "action",
],
execution_obligation: [
"kind", "contract", "contract_obligation", "must_attempt_work",
"notify_is_execution_gate", "delivery_allowed", "reason",
],
goal_route_hint: [
"schema_version", "kind", "route_decision", "preserves_goal_next_action",
"goal_next_action_mutation",
],
autonomous_replan_scope: [
"schema_version", "required", "applies", "scope", "owner_agent_ids",
"selected_peer_agent",
],
agent_scope_frontier: [
"schema_version", "action", "effective_action", "blocks_delivery",
"quiet_noop_allowed", "requires_replan", "recommended_action", "spend_policy",
],
automation_liveness: [
"schema_version", "keep_active", "pause_allowed", "pause_policy",
"automation_action",
],
handoff_readiness: [
"ready", "codex_ready", "handoff_status", "post_handoff_run_seen",
],
Expand All @@ -143,13 +121,6 @@ const CONTRACT_CAPSULE_FIELDS: Readonly<Record<string, readonly string[]>> = {
],
};

const CONTRACT_CAPSULE_TEXT_LIMITS: Readonly<Record<string, Readonly<Record<string, number>>>> = {
work_lane_contract: { action: 300, material_transition: 240 },
execution_obligation: { reason: 240 },
agent_scope_frontier: { recommended_action: 320, spend_policy: 220 },
automation_liveness: { pause_policy: 260 },
};

function object(value: unknown): JsonObject {
return typeof value === "object" && value !== null && !Array.isArray(value)
? { ...(value as JsonObject) }
Expand Down Expand Up @@ -750,10 +721,21 @@ function contractCapsule(payload: JsonObject): JsonObject {
};
for (const [sourceKey, fields] of Object.entries(CONTRACT_CAPSULE_FIELDS)) {
const compact = compactFields(
payload[sourceKey], fields, CONTRACT_CAPSULE_TEXT_LIMITS[sourceKey] || {},
payload[sourceKey], fields, {},
);
if (Object.keys(compact).length > 0) capsule[sourceKey] = compact;
}
// These source owners provide execution contracts, not display summaries.
// Preserve clauses and nested/unknown data without interpreting new fields.
// The existing signature covers the complete retained JSON; budget overflow
// remains diagnostic rather than permission to truncate an obligation.
for (const field of [
"work_lane_contract", "execution_obligation", "autonomous_replan_scope",
"agent_scope_frontier", "automation_liveness",
]) {
const contract = object(payload[field]);
if (Object.keys(contract).length > 0) capsule[field] = structuredClone(contract);
}
const unavailableContext = projectTurnStartUnavailableContext(payload.turn_start_capability_hook_dispatch);
if (unavailableContext) capsule.unavailable_context = unavailableContext;
// Work qualification is behavioral context, including its outcome and
Expand Down Expand Up @@ -797,17 +779,6 @@ function contractCapsule(payload: JsonObject): JsonObject {
}
const taskScope = text(payload.task_scope, 80);
if (taskScope) capsule.task_scope = taskScope;
const workLane = object(payload.work_lane_contract);
const outcome = object(workLane.outcome_followthrough);
if (Object.keys(outcome).length > 0) {
const compactLane = object(capsule.work_lane_contract);
compactLane.outcome_followthrough = compactFields(
outcome,
["required", "obligation", "accepted_resolution_kinds", "spend_policy"],
{ spend_policy: 220 },
);
capsule.work_lane_contract = compactLane;
}
const warnings = ACTIONABLE_WARNING_FIELDS.filter((field) => Boolean(payload[field]));
if (warnings.length > 0) capsule.actionable_warning_refs = warnings;
return capsule;
Expand Down Expand Up @@ -1071,6 +1042,28 @@ function signatureCoverage(envelope: JsonObject, responsePlanValue: unknown): st
return ACTION_SIGNATURE_COVERAGE_V0;
}

/** Live consumers must understand the declared wire versions before dispatch.
* Historical v0 callers may omit signature metadata; digest verification stays
* with its existing owner. Explicit unknown/null declarations are never v0.
* This does not make the bounded envelope a complete future work contract.
*/
export function assertTurnEnvelopeConsumerVersion(envelope: JsonObject): void {
assertSupportedQuotaPacket(envelope);
if (envelope.schema_version !== TURN_ENVELOPE_SCHEMA_VERSION) {
throw new EffectRuntimeRequestError("unsupported TurnEnvelope schema_version; use a compatible consumer");
}
const signature = object(envelope.action_signature);
if (Object.hasOwn(signature, "schema_version") && signature.schema_version !== ACTION_SIGNATURE_SCHEMA_VERSION) {
throw new EffectRuntimeRequestError("unsupported TurnEnvelope action signature schema_version; use a compatible consumer");
}
if (Object.hasOwn(signature, "coverage") && (typeof signature.coverage !== "string" || ![
ACTION_SIGNATURE_COVERAGE_V0, ACTION_SIGNATURE_COVERAGE_V1, ACTION_SIGNATURE_COVERAGE_V2,
ACTION_SIGNATURE_COVERAGE_V3, ACTION_SIGNATURE_COVERAGE_V4, ACTION_SIGNATURE_COVERAGE_V5,
].includes(signature.coverage))) {
throw new EffectRuntimeRequestError("unsupported TurnEnvelope action signature coverage; use a compatible consumer");
}
}

export function turnEnvelopeActionSignatureDocument(value: unknown): JsonObject {
const envelope = requireJsonObject(value, "turn_envelope");
assertSupportedQuotaPacket(envelope);
Expand Down Expand Up @@ -1275,6 +1268,10 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject {
autonomous_replan_obligation: {...obligation, context_read: reads.review},
replan_action_packet: replanActionPacket(payload)};
}
if (operation === "consumer_version") {
assertTurnEnvelopeConsumerVersion(requireJsonObject(request.envelope, "turn_envelope"));
return {ok: true};
}
if (operation === "build") return buildTurnEnvelope(request);
if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS);
if (operation === "native_context_lifecycle") return updateNativeContextLifecycle(request);
Expand Down
3 changes: 3 additions & 0 deletions loopx/control_plane/turn_driver/codex_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,10 @@ def codex_cli_result_schema(


def _prompt(request: Mapping[str, Any]) -> str:
from ..quota.turn_envelope import assert_turn_envelope_consumer_version

envelope = _mapping(request.get("turn_envelope"))
assert_turn_envelope_consumer_version(envelope)
staged = _mapping(_mapping(envelope.get("detail_ref")).get("replan_action_packet")).get("review")
model_request = request
if staged:
Expand Down
15 changes: 9 additions & 6 deletions loopx/control_plane/turn_driver/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
build_subagent_execution_topology,
)
from ..quota.turn_envelope import (
TURN_ENVELOPE_SCHEMA_VERSION,
assert_turn_envelope_consumer_version,
turn_envelope_action_signature_document,
turn_envelope_replan_settlement_identity,
)
Expand Down Expand Up @@ -138,8 +138,6 @@ def _mapping(value: Any) -> dict[str, Any]:


def _typed_route(envelope: Mapping[str, Any]) -> LoopXTurnRoute:
if envelope.get("schema_version") != TURN_ENVELOPE_SCHEMA_VERSION:
return LoopXTurnRoute.CONTRACT_ERROR
signature = _mapping(envelope.get("action_signature"))
source_hash = str(signature.get("source_hash") or "")
envelope_hash = str(signature.get("envelope_hash") or "")
Expand Down Expand Up @@ -509,9 +507,14 @@ def build_loopx_turn_plan(
scheduler_owner=scheduler_owner,
)
envelope = dict(turn_envelope)
contract_error = None
try:
assert_turn_envelope_consumer_version(envelope)
except (ValueError, RuntimeError) as exc:
contract_error = str(exc)
route = (
_typed_route(envelope)
if execution_context.ok
if execution_context.ok and contract_error is None
else LoopXTurnRoute.CONTRACT_ERROR
)
selected_todo = selected_turn_todo(envelope)
Expand Down Expand Up @@ -609,8 +612,8 @@ def build_loopx_turn_plan(
"opaque_session_handle_omitted": True,
},
**(
{"error": session_error or "; ".join(execution_context.errors)}
if session_error or not execution_context.ok
{"error": contract_error or session_error or "; ".join(execution_context.errors)}
if contract_error or session_error or not execution_context.ok
else {}
),
}
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/turn_driver/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def build_loopx_turn_host_request(plan: Mapping[str, Any]) -> dict[str, Any]:
raise ValueError("LoopX Turn plan has no valid transaction turn_key")
route = plan.get("route") if isinstance(plan.get("route"), dict) else {}
if route.get("would_invoke_host") is not True:
raise ValueError("LoopX Turn route is not host executable")
raise ValueError(str(plan.get("error") or "LoopX Turn route is not host executable"))
request = {
"schema_version": LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION,
"turn_key": turn_key,
Expand Down
Loading
Loading