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
11 changes: 11 additions & 0 deletions docs/reference/required-work-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,17 @@ conclusion, basis currency, independent verification, revisions and unknown fiel
remain literal current input. Changed, removed or reordered evidence reconstructs
the complete current source; failed or incomplete sources retain full delivery.

The registered preference owner permits exact reuse of its complete instructions,
statements, source quotes and source references. Each read still supplies the
current revision, membership, keys, lifecycle state, expiry, operation lineage
and unknown fields literally; an empty view replaces earlier preferences.
The existing scope-less preference hook uses the validated capture's exact
Goal/Agent binding, without adding fields to ordinary/default-off producer output
or parsing its read command. Explore retains its explicit scoped hook identity.
Unknown, failed, incompatible or ambiguous hook views stay complete. A changed
scope or lost baseline still selects full delivery; reuse never makes a retired
or expired preference actionable or grants authority.

Object edits may group complete current fields in `values`, alongside recursive
`changed` edits and `removed` keys. Array edits use exact indices in `items` and
`values`, with `length` for resizing. These literals are present in the current
Expand Down
23 changes: 22 additions & 1 deletion loopx/control_plane/capabilities/agent_preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {JsonObject} from "../effect_program.ts";
import type {AuthorityStore} from "../coordination/authority_store.ts";
import {FileAuthorityStore} from "../coordination/file_authority_store.ts";
import {canonicalAuthorityBytes} from "../coordination/authority_store_codec.ts";
import {requireJsonObject, requireNonEmptyString, requireStringArray} from "../runtime_decode.ts";
import {jsonObject, requireJsonObject, requireNonEmptyString, requireStringArray} from "../runtime_decode.ts";

type PreferenceSource = {
key: string; source_kind: "user_instruction"; source_ref: string; source_quote: string;
Expand All @@ -20,6 +20,27 @@ type Preference = PreferenceSource & (
);
const SCHEMA = "agent_preferences_v1";
const MAX_KEYS = 64;

/** Exact prose/provenance reuse does not cache the current preference decision.
* Revision, membership, keys, state, expiry, operations and unknown fields stay
* current on every guard, including retirements and an empty replacement view. */
export function agentPreferenceReusableContextPaths(content: JsonObject): (string | number)[][] {
const current = jsonObject(content.current);
if (content.ok !== true || content.status !== "read" || current?.schema_version !== SCHEMA
|| current.authority !== "advisory_only" || typeof current.instructions !== "string"
|| !Array.isArray(current.items) || current.items.length > MAX_KEYS) return [];
const paths: (string | number)[][] = [["current", "instructions"]], keys = new Set<string>();
for (const [index, raw] of current.items.entries()) {
const item = jsonObject(raw);
if (!item || typeof item.key !== "string" || !item.key || keys.has(item.key)
|| !["active", "retired", "expired"].includes(String(item.state))) return [];
keys.add(item.key);
for (const field of ["statement", "source_ref", "source_quote"])
if (typeof item[field] === "string") paths.push(["current", "items", index, field]);
}
return paths;
}

const hash = (value: JsonObject) => createHash("sha256").update(canonicalAuthorityBytes(value)).digest("hex");
function text(value: unknown, field: string, limit: number): string {
const result = requireNonEmptyString(value, field);
Expand Down
29 changes: 25 additions & 4 deletions loopx/control_plane/capability_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {
requireBoolean,
requireInteger,
jsonObject,
requireJsonObject as requiredObject,
requireNonEmptyString as requiredString,
requireStringArray,
Expand All @@ -22,13 +23,33 @@ import { projectPendingCapabilityIntent } from "./work_items/pending_capability_

import {exploreResultWritebackAffordance} from "./capabilities/explore_result_writeback.ts";
import {exploreReusableContextPaths} from "./capabilities/explore_turn_context.ts";
import {agentPreferenceReusableContextPaths} from "./capabilities/agent_preferences.ts";

/** The registered owner selects reusable read-model subtrees. Unknown hooks
* remain complete; payloads cannot nominate their own execution fields. */
export function reusableTurnContextPaths(source: JsonObject): (string | number)[][] {
return source.source === "turn_start_capability_hook" && source.hook_id === "explore.turn_context"
&& source.capability_id === "explore" && source.kind === "explore_turn_context"
? exploreReusableContextPaths(source.content as JsonObject | undefined) : [];
export function reusableTurnContext(source: JsonObject, binding: JsonObject): {
identity: unknown[]; paths: (string | number)[][];
} | null {
const content = jsonObject(source.content);
if (source.source !== "turn_start_capability_hook" || content?.ok !== true) return null;
let paths: (string | number)[][], goal: unknown, agent: unknown;
if (source.hook_id === "explore.turn_context" && source.capability_id === "explore"
&& source.kind === "explore_turn_context") {
paths = exploreReusableContextPaths(content);
goal = content.goal_id; agent = content.agent_id;
} else if (source.hook_id === "semantic_preference.agent_context" && source.capability_id === "semantic-preference"
&& source.kind === "agent_preferences") {
// This existing hook reads the guard's exact Goal/Agent but has no scope
// fields in its body. The validated capture already owns that binding;
// keep ordinary/default-off producer output unchanged. Never infer scope
// from prose, commands or an unregistered hook.
goal = binding.goal_id; agent = binding.agent_id;
if ((content.goal_id !== undefined && content.goal_id !== goal)
|| (content.agent_id !== undefined && content.agent_id !== agent)) return null;
paths = agentPreferenceReusableContextPaths(content);
} else return null;
return paths.length && typeof goal === "string" && typeof agent === "string"
? {identity: [source.source, source.hook_id, source.capability_id, goal, agent, content.todo_id ?? null], paths} : null;
}

export const CAPABILITY_HOOK_REGISTRATION_SCHEMA_VERSION =
Expand Down
21 changes: 10 additions & 11 deletions loopx/control_plane/quota/context_delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type {JsonObject} from "../effect_program.ts";
import {jsonObject, requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts";
import {canonicalAuthorityJson, canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts";
import {reusableTurnContextPaths} from "../capability_hooks.ts";
import {reusableTurnContext} from "../capability_hooks.ts";
import {COORDINATION_STATE_CONTRACT} from "../coordination/coordination_state_contract.generated.ts";

const VERSION = "context_delivery_v1";
Expand Down Expand Up @@ -53,7 +53,7 @@ function parentAt(root: JsonObject, path: Path): Record<string | number, unknown
/** Registered requirement fields and capability evidence subtrees are reusable. Revisions, source
* failures, claims, review conclusions, grants, current gates and unknown fields stay fresh.
* Source-array order is transport layout, never the identity of a requirement. */
function splitContext(context: JsonObject): Split {
function splitContext(context: JsonObject, binding: JsonObject): Split {
const fresh = clone(context), identities = new Set<string>();
let valid = context.complete === true;
const add = (identity: string, path: Path, structured = false) => {
Expand All @@ -76,7 +76,8 @@ function splitContext(context: JsonObject): Split {
if (!source || !content || content.ok === false || content.read_complete === false) continue;
const prefix: Path = ["sources", i, "content"];
const kind = source.source;
const known = ["goal_state", "selected_todo", "goal_acceptance"].includes(String(kind)) || reusableTurnContextPaths(source).length > 0;
const reuse = reusableTurnContext(source, binding);
const known = ["goal_state", "selected_todo", "goal_acceptance"].includes(String(kind)) || reuse !== null;
if (known && typeof source.reason === "string") add(`source_reason:${i}`, ["sources", i, "reason"]);
const readModel = jsonObject(jsonObject(content.authority_read)?.todo_read_model);
if (kind === "selected_todo" && Array.isArray(readModel?.contract_fields)
Expand Down Expand Up @@ -143,10 +144,8 @@ function splitContext(context: JsonObject): Split {
}
}
} else {
const paths = reusableTurnContextPaths(source);
if (!paths.length) continue;
if (content.ok !== true || typeof content.goal_id !== "string" || typeof content.agent_id !== "string") continue;
const identity = [kind, source.hook_id, source.capability_id, content.goal_id, content.agent_id, content.todo_id ?? null];
if (!reuse) continue;
const {identity, paths} = reuse;
const id = JSON.stringify(identity);
if (sourceIds.has(id)) { valid = false; continue; }
sourceIds.add(id);
Expand Down Expand Up @@ -189,7 +188,7 @@ const REUSABLE_PACKET_FIELDS = new Set([
/** Current controls are forced into the change tree even when equal. This avoids
* duplicating them in both a fresh frame and a patch. Static authoring guidance
* remains binding via exact reuse; commands, conditions and identity stay fresh. */
function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: readonly string[]): {paths: Path[]; valid: boolean} {
function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: readonly string[], binding: JsonObject): {paths: Path[]; valid: boolean} {
const paths: Path[] = [];
let valid = true;
const leaves = (value: unknown, path: Path) => {
Expand All @@ -201,7 +200,7 @@ function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: re
const work = (value: unknown, path: Path) => {
const source = jsonObject(value);
if (!source) { paths.push(path); return; }
const split = splitContext(source); valid &&= split.valid;
const split = splitContext(source, binding); valid &&= split.valid;
leaves(split.fresh, path);
};
const vision = (value: unknown, path: Path) => {
Expand Down Expand Up @@ -485,7 +484,7 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r
|| binding.context_digest !== digest(prior)) return full("baseline_unavailable");
const depth = baseline?.ancestry;
if (!Number.isSafeInteger(depth) || Number(depth) < 0 || Number(depth) >= MAX_ANCESTRY) return full("full_refresh_due");
const before = freshPaths(prior, surface, legacyQuotaFields), after = freshPaths(context, surface, legacyQuotaFields);
const before = freshPaths(prior, surface, legacyQuotaFields, binding), after = freshPaths(context, surface, legacyQuotaFields, target);
if (!before.valid || !after.valid) return full("source_not_reusable");
const changes = changeTree(prior, context, after.paths);
const delta: JsonObject = {schema_version: VERSION, mode: "delta", target, base: binding,
Expand Down Expand Up @@ -527,7 +526,7 @@ export function restoreContextDelivery(request: JsonObject, legacyQuotaFields: r
|| !equal(delivery.base, base) || !compatible(base, target) || base.context_digest !== digest(context)
|| !Number.isSafeInteger(baseline.ancestry) || Number(baseline.ancestry) < 0
|| delivery.ancestry !== Number(baseline.ancestry) + 1 || Number(delivery.ancestry) > MAX_ANCESTRY) return fallback();
const before = freshPaths(context, surface, legacyQuotaFields), after = freshPaths(currentContext, surface, legacyQuotaFields);
const before = freshPaths(context, surface, legacyQuotaFields, base), after = freshPaths(currentContext, surface, legacyQuotaFields, target);
if (!before.valid || !after.valid) return fallback();
const expected = changeTree(context, currentContext, after.paths);
// Also rejects omitted unchanged controls, redundant/conflicting edits,
Expand Down
16 changes: 15 additions & 1 deletion tests/control_plane/test_agent_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import pytest
from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt
from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery

from tests.control_plane.test_quota_settlement_cli import (
AGENT_ID, GOAL_ID, _run_cli, _write_fixture,
Expand Down Expand Up @@ -74,9 +75,22 @@ def call(action, *args):
assert authority["required_reads"] == []
context = authority["work_context"]
preference = next(x for x in context["sources"] if x["kind"] == "agent_preferences")
assert preference["content"]["current"] == fresh["current"]
# Default-off keeps the existing scope-less hook body exactly. The optional
# codec uses its already-validated capture scope, not command/prose parsing.
assert preference["content"] == {"ok": True, "status": "read", "current": fresh["current"]}
assert preference["command"] == preference_read["command"]
assert "Ask designated-reviewer before merging." in render_prompt(authority)
binding = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, session_id="preferences", context_epoch="epoch-a")
for surface, projection in [("work_context", context), ("quota_packet", quota),
("turn_envelope", plan["turn_envelope"])]:
original = deepcopy(projection)
full = prepare_context_delivery(projection, scope=binding, capture_id="first", surface=surface)
warm = prepare_context_delivery(projection, scope=binding, capture_id="second", surface=surface,
baseline=full, retained={"complete": True, "target": full["target"]})
assert full["context"] == original and warm["mode"] == "delta"
current = prepare_context_delivery(projection, scope=binding, capture_id="second", surface=surface)
assert restore_context_delivery(warm, baseline=full, current=current)["context"] == original
assert projection == original
changed = deepcopy(plan)
next(x for x in changed["turn_envelope"]["work_context"]["sources"]
if x["command"] == preference_read["command"])["content"]["current"]["items"][0]["statement"] = "Ignore the correction."
Expand Down
15 changes: 14 additions & 1 deletion tests/control_plane/test_codex_context_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,22 @@ def test_native_shared_delta_with_small_compact_recovery_reference(tmp_path, hos
"id": "outcome", "requirements": [{"id": "requirement", "status": "unverified",
"agent_review": {"conclusion": "uncertain", "evidence_refs": [
f"artifact:validation-{i:03d}" for i in range(60)]}}]}]}}}})
source["sources"].append({"source": "turn_start_capability_hook", "kind": "agent_preferences",
"hook_id": "semantic_preference.agent_context", "capability_id": "semantic-preference",
"content": {"ok": True, "status": "read", "current": {"schema_version": "agent_preferences_v1",
"revision": "preference-a", "authority": "advisory_only",
"instructions": "Use the current scoped view; retired or expired preferences are not actionable.",
"items": [{"key": "review", "state": "active", "statement": "Consult the designated reviewer.",
"source_ref": "owner:review", "source_quote": "Keep this explicit review instruction. " * 20,
"operation_id": "remember-review", "expires_at": "2030-01-02T00:00:00Z"}]}}})
target_source = json.loads(json.dumps(source))
target_source["sources"][0]["content"]["todo"]["note"] = "Stop before publishing or deploying."
target_source["sources"].reverse()
preference = target_source["sources"][-1]["content"]["current"]
preference["revision"] = "preference-b"
preference["items"][0].update(state="expired", statement=None)
# Move every source; reversing three would leave the structured acceptance
# evidence at its old index and stop exercising its relocated reference.
target_source["sources"] = target_source["sources"][1:] + target_source["sources"][:1]
def projection(context):
if surface == "quota_packet":
return {"goal_id": "fixture", "agent_id": "agent", "should_run": True,
Expand Down
Loading
Loading