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
58 changes: 53 additions & 5 deletions docs/reference/protocols/turn-envelope-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,56 @@ the default quota output.

Large todo summaries, frontier diagnostics, readiness history, compatibility
fields, and warning collections stay on the referenced full-decision/status
cold paths. The envelope has an 8 KiB JSON budget and reports its measured
source/envelope byte counts.
cold paths. The envelope has an **8 KiB compact UTF-8 JSON performance target**,
not an execution-admission limit. `compaction.envelope_utf8_bytes` measures the
final packet, including diagnostics. The historical `source_json_bytes` and
`envelope_json_bytes` fields still count Unicode code points for v0 compatibility;
do not use them as wire-byte measurements.

### Budget warnings and allocation

Oversize valid envelopes keep their normal Turn plan/controller route. They
report `compaction.within_budget=false` and a structured
`warning.code=turn_envelope_budget_exceeded`, with `excess_bytes`, additive
`section_bytes` and `over_target_sections`. JSON carries this through the Turn
plan and host request; Markdown plan/envelope output calls out the warning.
Schema, signatures, identity, permissions, receipt validation and execution
quota are still hard gates. This changes previous behavior for **all Turn hosts**:
packet growth alone no longer produces `contract_error` or stops a Turn loop.

The TypeScript owner keeps review allocations totaling 8,192 bytes. These are
diagnostic targets, not permission to truncate fields or hard per-section caps:

| Section | Target bytes | Included fields |
| --- | ---: | --- |
| action | 800 | action, user, required reads, replan packet, response plan |
| boundary | 2,000 | boundary and execution policy |
| writeback | 600 | validation/settlement commands and policy |
| scheduler | 600 | scheduler action and acknowledgement |
| contracts | 1,800 | contract capsule |
| context | 1,400 | capability context and task orchestration |
| transport | 992 | identity/metadata, signatures, cold-read commands, diagnostics |

Counts include JSON property names, delimiters and UTF-8 text. Their sum equals
the measured final packet; dividing each by `envelope_utf8_bytes` gives its
share. Diagnostic detail is emitted only on overflow, not every normal Turn.
Use the existing `quota should-run --turn-envelope` or `turn plan` JSON output
to inspect the breakdown. Record a public-safe reproduction and compare each
section with the same fixture on the baseline before changing its owner.
First remove repeated presentation or move non-actionable detail to an existing
cold read. Never trim write scope, executable arguments, signatures or required
reads to silence a warning, and do not simply raise the target. The cold-read
commands remain; their redundant human-readable `contains` inventory is retired.

Repository size/parity canaries remain blocking **delivery-time regression
checks**, independent of runtime warning semantics. Representative fixtures must
still fit the target. A warning is a performance investigation signal, not an
automatic Todo, new authority, or permission to spend an extra Turn.

中文:TurnEnvelope 超出 8 KiB 后产生可分析的 warning,不再仅因大小中断合法
Turn。按最终 UTF-8 字节数统计各部分占比,先压缩重复展示内容,再检查对应规则
所属模块;不得截断权限、签名或执行指令,也不应单纯提高预算掩盖增长。
身份、权限、签名和执行配额仍是硬门禁;仓库的体积与语义回归检查仍阻止交付。

Hot-path fields may use explicit references when the inline value would only
repeat another authoritative field. In particular,
Expand All @@ -136,9 +184,9 @@ blocked, and throttled decisions. Every case must preserve the canonical action
signature, reconstruct `protocol_action_packet`, and remain within the 8 KiB
budget.

The current matrix produces envelopes from 4,866 to 5,602 bytes, with 66.44% to
69.36% reduction from the full synthetic decision. This is sufficient to keep
the projection available as an opt-in host view. It is not sufficient to change
The matrix records exact measurements in validation rather than treating a
dated size range as the contract. This keeps the projection available as an
opt-in host view. It is not sufficient to change
the default CLI response: default promotion still requires shadow parity from a
real host integration, no consumer regression with the full decision available
as a cold path, and explicit compatibility acceptance for the default-view
Expand Down
8 changes: 8 additions & 0 deletions loopx/cli_commands/turn_rendering.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from __future__ import annotations

from ..presentation.renderers.turn_envelope_markdown import (
turn_envelope_budget_warning_lines,
)


def render_loopx_turn_plan_markdown(payload: dict[str, object]) -> str:
if not payload.get("ok"):
error = payload.get("error") or "invalid TurnEnvelope contract"
return f"LoopX Turn plan failed: {error}"
host = payload.get("host") if isinstance(payload.get("host"), dict) else {}
route = payload.get("route") if isinstance(payload.get("route"), dict) else {}
envelope = payload.get("turn_envelope")
return "\n".join(
[
"# LoopX Turn Plan",
Expand All @@ -15,6 +20,9 @@ def render_loopx_turn_plan_markdown(payload: dict[str, object]) -> str:
f"- route: {route.get('kind')}",
f"- would_invoke_host: {route.get('would_invoke_host')}",
"- side_effects: none",
*turn_envelope_budget_warning_lines(
envelope if isinstance(envelope, dict) else {}
),
]
)

Expand Down
37 changes: 3 additions & 34 deletions loopx/control_plane/quota/turn_envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {
} from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { requireJsonObject } from "../runtime_decode.ts";
import { measureTurnEnvelope, TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts";
export { TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts";

export const TURN_ENVELOPE_SCHEMA_VERSION = "loopx_turn_envelope_v0";
export const TURN_ENVELOPE_BUDGET_BYTES = 8_192;
export const CONTRACT_CAPSULE_SCHEMA_VERSION = "loopx_contract_capsule_v0";
export const ACTION_SIGNATURE_SCHEMA_VERSION = "loopx_action_signature_v0";
export const ACTION_SIGNATURE_COVERAGE_V0 = "turn_envelope_action_dimensions_v0";
Expand Down Expand Up @@ -178,10 +179,6 @@ function comparePythonUnicode(left: string, right: string): number {
return leftPoints.length - rightPoints.length;
}

function compactJson(value: unknown): string {
return JSON.stringify(value);
}

function canonicalHash(value: unknown): string {
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalValue(value)), "utf8").digest("hex")}`;
}
Expand Down Expand Up @@ -722,14 +719,6 @@ function coldPath(
: "rerun the typed quota_guard from the current host packet",
todo_detail: `${prefix} --format json todo list --goal-id ${goalId}`,
status_detail: `${prefix} --format json status --goal-id ${goalId}`,
contains: [
"quota accounting detail",
"goal frontier and route diagnostics",
"full todo summaries",
"handoff and readiness diagnostics",
"promotion, archive, and projection warnings",
"scheduler runtime detail",
],
};
}

Expand Down Expand Up @@ -774,27 +763,7 @@ export function buildTurnEnvelope(value: unknown): JsonObject {
matches: JSON.stringify(sourceSignature) === JSON.stringify(envelopeSignature),
source_decision_hash: canonicalHash(payload),
};
// Preserve the versioned v0 metric: the historical Python owner counted
// Unicode code points even though the public field is named *_json_bytes.
const sourceBytes = [...compactJson(payload)].length;
envelope.compaction = {
source_json_bytes: sourceBytes,
envelope_json_bytes: 0,
byte_reduction_ratio: 0,
budget_bytes: TURN_ENVELOPE_BUDGET_BYTES,
within_budget: true,
};
for (let index = 0; index < 3; index += 1) {
const envelopeBytes = [...compactJson(envelope)].length;
envelope.compaction = {
...object(envelope.compaction),
envelope_json_bytes: envelopeBytes,
byte_reduction_ratio: sourceBytes
? Math.round((1 - envelopeBytes / sourceBytes) * 10_000) / 10_000
: 0,
within_budget: envelopeBytes <= TURN_ENVELOPE_BUDGET_BYTES,
};
}
measureTurnEnvelope(envelope, payload);
return envelope;
}

Expand Down
76 changes: 76 additions & 0 deletions loopx/control_plane/quota/turn_envelope_budget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/** Performance diagnostics, never Turn admission or execution authority. */
import type { JsonObject } from "../effect_program.ts";

export const TURN_ENVELOPE_BUDGET_BYTES = 8_192;

// Review allocations, not truncation limits. Preserve authority even on overflow.
export const TURN_ENVELOPE_SECTION_TARGETS = {
action: 800, boundary: 2_000, writeback: 600, scheduler: 600,
contracts: 1_800, context: 1_400, transport: 992,
} as const;
type Section = keyof typeof TURN_ENVELOPE_SECTION_TARGETS;
const SECTION_FIELDS: Record<string, Section> = {
action: "action", user: "action", required_reads: "action",
replan_action_packet: "action", response_plan: "action",
boundary: "boundary", execution_policy: "boundary", writeback: "writeback",
scheduler: "scheduler", contract_capsule: "contracts",
agent_context: "context", task_orchestration_contract: "context",
};

function sectionBytes(envelope: JsonObject): Record<Section, number> {
const sizes = Object.fromEntries(
Object.keys(TURN_ENVELOPE_SECTION_TARGETS).map((key) => [key, 0]),
) as Record<Section, number>;
// Include property names, separators and braces; totals equal wire bytes.
sizes.transport = 1;
for (const [key, value] of Object.entries(envelope)) {
sizes[SECTION_FIELDS[key] ?? "transport"] +=
Buffer.byteLength(JSON.stringify(key) + ":" + JSON.stringify(value), "utf8") + 1;
}
return sizes;
}

export function measureTurnEnvelope(envelope: JsonObject, source: JsonObject): void {
// Keep v0 *_json_bytes code-point metrics for compatibility. New diagnostics
// and the performance target use actual compact JSON UTF-8 bytes.
const sourceChars = [...JSON.stringify(source)].length;
envelope.compaction = {
source_json_bytes: sourceChars, envelope_json_bytes: 0,
byte_reduction_ratio: 0, budget_bytes: TURN_ENVELOPE_BUDGET_BYTES,
within_budget: true, envelope_utf8_bytes: 0,
};
// Measurements include their own serialized metadata. Recompute to a fixed
// point (decimal widths and the four-place ratio stabilize after a few passes).
const seen = new Set<string>();
let ratioLocked = false;
for (let pass = 0; pass < 16; pass += 1) {
const before = JSON.stringify(envelope);
// Rounding can alternate between e.g. 0.54 and 0.5401, changing its own
// width. Freeze that approximate ratio on a cycle; byte counts stay exact.
if (seen.has(before)) ratioLocked = true;
seen.add(before);
const chars = [...before].length;
const bytes = Buffer.byteLength(before, "utf8");
const metric: JsonObject = {
source_json_bytes: sourceChars, envelope_json_bytes: chars,
byte_reduction_ratio: ratioLocked
? (envelope.compaction as JsonObject).byte_reduction_ratio : sourceChars
? Math.round((1 - chars / sourceChars) * 10_000) / 10_000 : 0,

Check warning on line 58 in loopx/control_plane/quota/turn_envelope_budget.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCT_02S5avs-bjFm3BQ&open=AaCT_02S5avs-bjFm3BQ&pullRequest=4268
budget_bytes: TURN_ENVELOPE_BUDGET_BYTES,
within_budget: bytes <= TURN_ENVELOPE_BUDGET_BYTES,
envelope_utf8_bytes: bytes,
};
if (bytes > TURN_ENVELOPE_BUDGET_BYTES) {
const sections = sectionBytes(envelope);
metric.warning = {
code: "turn_envelope_budget_exceeded", severity: "warning",
excess_bytes: bytes - TURN_ENVELOPE_BUDGET_BYTES,
section_bytes: sections,
over_target_sections: (Object.keys(sections) as Section[])
.filter((key) => sections[key] > TURN_ENVELOPE_SECTION_TARGETS[key]),
};
}
envelope.compaction = metric;
if (JSON.stringify(envelope) === before) break;
}
}
5 changes: 2 additions & 3 deletions loopx/control_plane/turn_driver/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ def _typed_route(envelope: Mapping[str, Any]) -> LoopXTurnRoute:
or source_hash != envelope_hash
):
return LoopXTurnRoute.CONTRACT_ERROR
compaction = _mapping(envelope.get("compaction"))
if compaction.get("within_budget") is not True:
return LoopXTurnRoute.CONTRACT_ERROR
# Packet size is a performance warning, not execution authority. Keep the
# diagnostics in the envelope; schema/signature/lineage remain hard gates.

action = _mapping(envelope.get("action"))
user = _mapping(envelope.get("user"))
Expand Down
6 changes: 3 additions & 3 deletions loopx/control_plane/turn_driver/loop_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ def _envelope_route(decision: Mapping[str, Any]) -> LoopXTurnRoute:
"""Return the shared typed route for a fresh quota/scheduler decision.

Reuses the Turn plan driver's ``_typed_route`` contract, which requires a
matching action signature with non-empty equal hashes and an in-budget
compaction. A projected user action outranks delivery, so it is resolved
matching action signature with non-empty equal hashes. Compaction budget
warnings are diagnostic only. A projected user action outranks delivery, so it is resolved
before the typed delivery route. Raises ``ValueError`` when the envelope
fails the shared contract instead of accepting a forged or truncated
decision.
Expand All @@ -114,7 +114,7 @@ def _envelope_route(decision: Mapping[str, Any]) -> LoopXTurnRoute:
if route is LoopXTurnRoute.CONTRACT_ERROR:
raise ValueError(
"quota decision failed the shared envelope contract "
"(schema, signature hashes, or compaction budget)"
"(schema or signature hashes)"
)
user = _mapping(decision.get("user"))
if user.get("action_required") is True:
Expand Down
19 changes: 19 additions & 0 deletions loopx/presentation/renderers/turn_envelope_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
from typing import Any


def turn_envelope_budget_warning_lines(payload: dict[str, Any]) -> list[str]:
compaction = payload.get("compaction") or {}
warning = compaction.get("warning") or {}
if warning.get("code") != "turn_envelope_budget_exceeded":
return []
sections = warning.get("section_bytes") or {}
return [
"- WARNING: TurnEnvelope exceeds its performance target by "
f"{warning.get('excess_bytes')} UTF-8 bytes; Turn routing is unchanged.",
"- section_bytes: "
+ ", ".join(f"{key}={value}" for key, value in sections.items()),
"- Review over-target sections: "
+ ", ".join(warning.get("over_target_sections") or []),
"- Compress duplicate presentation or move detail to cold reads; "
"do not truncate authority or raise the target to hide growth.",
]


def render_turn_envelope_markdown(payload: dict[str, Any]) -> str:
action_value = payload.get("action")
user_value = payload.get("user")
Expand All @@ -29,5 +47,6 @@ def render_turn_envelope_markdown(payload: dict[str, Any]) -> str:
f"- scheduler: `{scheduler.get('action')}`",
f"- envelope_bytes: `{compaction.get('envelope_json_bytes')}`",
f"- within_budget: `{compaction.get('within_budget')}`",
*turn_envelope_budget_warning_lines(payload),
]
return "\n".join(lines)
19 changes: 11 additions & 8 deletions skills/loopx-project/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1064,14 +1064,17 @@ ids, or raw local evidence in public repo docs or examples.
## Capability Context And Child Models

Read `interaction_contract.agent_context` at planning time (or
`turn_envelope.agent_context` in LoopX Turn). Enabled capabilities contribute
bounded guidance for the coordinator, including independent evidence questions,
the work to retain locally, and parent validation obligations. Consider useful
read-heavy delegation within a single Todo; do not manufacture persistent Todos
or duplicate research to trigger parallelism. Respect the existing admission
and authorization boundaries.

When using native child tools outside LoopX Turn, read the same capability
`turn_envelope.agent_context` in LoopX Turn, resolving its detail reference when
compacted). If the host supplies neither, use the read-only
`loopx agent-context --goal-id <goal> --agent-id <agent> --phase before_plan`.
If context is absent, disabled, or the read fails, preserve the existing single
agent workflow: do not seek delegation splits or invoke child tools because of
this capability. Tool availability and installed skills do not activate it.
Only apply delegation guidance from a non-null, current-scope enabled context;
the capability provider owns that policy. Context never grants spawn authority.

When enabled context and separate authorization allow native child tools
outside LoopX Turn, read the same capability
context at each boundary, using the current registry, Goal and Agent:

```bash
Expand Down
Loading