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
16 changes: 11 additions & 5 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,8 +734,10 @@ qualify that guidance seam; avoided model retries and sustained cost remain open
The replan obligation outcome policy now lives in
`work_items/replan_semantics.ts`: required-outcome selection, vision-path and
terminal consistency matching, and the matching refresh input projection share
one owner. Python retains progress normalization/novelty and persistence adapters,
but no longer duplicates obligation-to-outcome matching. This is a bounded rule
one owner. `work_items/progress_delta.ts` now derives semantic dimensions,
terminal coverage and evidence/blocker novelty inside that same qualification
request. Python retains shared observation/fingerprint normalization, history
codecs and persistence adapters; its delta and terminal-coverage rules are removed. This is a bounded rule
convergence, not a settlement-writer or store migration. Existing outcome
characterization precedes the move; the intentional correction is executable
authoring for all vision triggers, with real bound CLI closeout/readback and
Expand All @@ -744,9 +746,13 @@ remain in their existing owners; no new capability, provider or setting is added

Known blocker qualification also consumes the host-projected coverage ledger.
Changing a Turn/work-item binding or refreshing evidence cannot make the same
blocker a new replan outcome. Python normalizes the observations and reports
identity reuse; the TypeScript outcome owner rejects that claim for every
replan source while retaining independently evidenced vision paths. This
blocker a new replan outcome. Python normalizes the observations; TypeScript
compares the complete normalized window and rejects identity reuse for every
replan source while retaining independently evidenced vision paths. Expanded
qualification inputs use the existing private snapshot transport without
truncating old claims. The qualify request count and input-error order remain
unchanged. Shared codecs retire with refresh writers, replan history/context
readers, external review and Explore evidence consumers. This
corrects false advancement admission; it does not add a blocked-wait settlement
route or certify Goal completion.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,13 @@ token/重试成本、source-mode 修复和常规短上下文/replan 完整

Replan 的义务结果规则现收敛到 `work_items/replan_semantics.ts`:接受结果选择、
vision path/terminal 一致性校验与对应 refresh 输入投影共用同一 owner。
Python 保留 progress 归一化/新颖性与持久化适配,不再重复义务匹配规则。
`work_items/progress_delta.ts` 现在在同一资格请求内推导语义维度、终态覆盖及
证据/blocker 新颖性;删除 Python 的 delta 与终态覆盖规则。Python 保留共享的
observation/fingerprint 归一化、历史 codec 和持久化适配。TS 对完整的归一化窗口
比较身份复用;更大的资格输入复用既有私有快照传输,不截断旧声明。更换 Turn/
work-item 或刷新证据不能让旧 blocker 成为新结果,独立的 Vision 证据路径仍可使用。
资格请求次数与输入错误顺序不变。共享 codec 随 refresh writer、replan history/
context reader、external review 与 Explore evidence 消费者一起迁移后退役。
这是有界规则收敛,不是 settlement writer 或存储迁移。先刻画既有接受语义,
再修正所有 vision trigger 的可执行写入投影,并验证真实绑定 CLI 闭环、回读及
资格范围错配反例。Checkpoint 恢复与 in-flight 规则仍由既有边界负责,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -699,17 +699,25 @@ work effect

### 1. Replan 不能靠 classification 或 ACK,必须带 typed semantic delta

`loopx/control_plane/work_items/progress_observation.py` 从显式 enum 与稳定 ID
推导语义变化,不从 prose 猜测:
`loopx/control_plane/work_items/progress_delta.ts` 从显式 enum、稳定 ID 与完整
证据窗口推导语义变化,再由既有 replan owner 判断是否满足当前义务。Python
facade 保留输入与历史 codec;调用完整资格入口,不能用预计算 outcome 代替:

```python
observation = normalize_progress_observation(progress_observation)
delta = semantic_progress_delta(
observation,
baseline=obligation.get("progress_baseline"),
from loopx.control_plane.work_items.progress_observation import semantic_delta_from_writeback

delta = semantic_delta_from_writeback(
obligation=obligation,
progress_observation=progress_observation,
history_runs=newest_first_runs,
)
if not delta["accepted"]:
print(delta["reason"])
```

仅刷新 evidence ID 不构成新语义维度;旧 blocker 不因更换 work item 而成为新结果。
若 context 已截断 coverage,必须提供完整历史,缺失时资格入口拒绝写回。

成功 receipt 必须携带当前 `obligation_id`,且 outcome 属于该 obligation 的
`required_any_of`。`new_runnable_successor` 不走 `refresh-state` 的 successor 声明;
它由带 `--action-kind`、稳定 target 和
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ export function createEffectRuntimeHandlers(
["work_item.context.plan", lazyHandler(() => import("./work_items/interaction_contract.ts"), ({planInteractionWorkContext}) => planInteractionWorkContext)],
["work_item.context.project", lazyHandler(() => import("./work_items/interaction_contract.ts"), ({projectInteractionWorkContext}) => projectInteractionWorkContext)],
["work_item.replan_settlement.project", lazyHandler(() => import("./work_items/replan_settlement.ts"), ({projectReplanSettlementContract}) => projectReplanSettlementContract)],
["work_item.replan_semantics.project", lazyHandler(() => import("./work_items/replan_semantics.ts"), ({projectReplanSemantics}) => projectReplanSemantics)],
["work_item.replan_semantics.project", lazyHandler(() => import("./work_items/replan_semantics.ts"), ({projectReplanWriteback}) => projectReplanWriteback)],
["work_item.replan_context.project", lazyHandler(() => import("./work_items/replan_context.ts"), ({projectReplanContext}) => projectReplanContext)],
["work_item.replan_context.project_snapshot", lazyHandler(() => import("./work_items/replan_context.ts"), ({projectReplanContextSnapshot}) => projectReplanContextSnapshot)],
["explore.configuration.resolve", lazyHandler(() => import("./capabilities/explore_configuration.ts"), ({resolveExploreConfiguration}) => resolveExploreConfiguration)],
Expand Down
113 changes: 113 additions & 0 deletions loopx/control_plane/work_items/progress_delta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { JsonObject } from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import {
requireBoolean, requireJsonObject, requireNonEmptyString, requireStringArray,
requireStringLiteral,
} from "../runtime_decode.ts";
import { PROGRESS_RESULT_CLASSES } from "./replan_history.ts";

const PROGRESS_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;

interface Observation {
result: typeof PROGRESS_RESULT_CLASSES[number];
fingerprint: string;
surface_id?: string;
hypothesis_id?: string;
probe_kind?: string;
blocker_id?: string;
coverage_scope_id?: string;
coverage_complete?: boolean;
evidence: readonly string[];
}

function identifier(source: JsonObject, field: string): string | undefined {
if (!Object.hasOwn(source, field)) return undefined;
const identifier = requireNonEmptyString(source[field], field);
if (!PROGRESS_IDENTIFIER.test(identifier)) {
throw new EffectRuntimeRequestError(`${field} must be a normalized progress identifier`);
}
return identifier;
}

function evidenceIdentifiers(source: JsonObject): string[] {
if (!Object.hasOwn(source, "evidence_ids")) return [];
const identifiers = requireStringArray(source.evidence_ids, "evidence_ids");
if (!identifiers.length || identifiers.length > 12 ||
identifiers.some(identifier => !PROGRESS_IDENTIFIER.test(identifier))) {
throw new EffectRuntimeRequestError("evidence_ids must contain 1-12 normalized progress identifiers");
}
return identifiers;
}

function decodeObservation(value: unknown): Observation {
const source = requireJsonObject(value, "progress observation");
requireStringLiteral(source.schema_version, ["typed_progress_observation_v0"], "progress schema");
const fingerprint = requireNonEmptyString(source.fingerprint, "fingerprint");
if (!/^[a-f0-9]{16}$/.test(fingerprint)) {
throw new EffectRuntimeRequestError("fingerprint must be a normalized progress digest");
}
identifier(source, "work_item_id");
const observation = {
result: requireStringLiteral(source.result_class, PROGRESS_RESULT_CLASSES, "result_class"),
fingerprint, surface_id: identifier(source, "surface_id"),
hypothesis_id: identifier(source, "hypothesis_id"), probe_kind: identifier(source, "probe_kind"),
blocker_id: identifier(source, "blocker_id"), coverage_scope_id: identifier(source, "coverage_scope_id"),
coverage_complete: Object.hasOwn(source, "coverage_complete")
? requireBoolean(source.coverage_complete, "coverage_complete") : undefined,
evidence: evidenceIdentifiers(source),
};
if (!["work_item_id", "surface_id", "hypothesis_id", "probe_kind", "blocker_id",
"coverage_scope_id", "evidence_ids", "coverage_complete"].some(field => Object.hasOwn(source, field))) {
throw new EffectRuntimeRequestError("progress observation requires a stable work, coverage or evidence field");
}
return observation;
}

function newTerminalCoverage(current: Observation, prior: Observation | null): boolean {
if (prior === null || current.result !== prior.result ||
current.coverage_scope_id !== prior.coverage_scope_id) return true;
return current.result === "exploration_exhausted" && current.coverage_complete !== prior.coverage_complete;
}

function deltaKinds(current: Observation, prior: Observation | null): string[] {
if (!current.evidence.length) return [];
if (current.result === "advanced") {
const dimensions = [
["surface_id", "new_surface"], ["hypothesis_id", "new_hypothesis"], ["probe_kind", "new_probe_family"],
] as const;
return dimensions.filter(([field]) => current[field] && current[field] !== prior?.[field])
.map(([, outcome]) => outcome);
}
if (current.result === "blocked") {
return current.blocker_id && current.blocker_id !== prior?.blocker_id ? ["new_concrete_blocker"] : [];
}
if (!current.coverage_scope_id || !newTerminalCoverage(current, prior)) return [];
if (current.result === "exploration_exhausted" && current.coverage_complete === true) {
return ["coverage_backed_exploration_exhausted"];
}
return current.result === "no_followup" ? ["coverage_backed_no_followup"] : [];
}

/** Python supplies normalized source facts; only this owner qualifies their delta. */
export function projectProgressDelta(value: unknown): JsonObject {
if (value === null) return {accepted: false, reason: "typed progress observation missing"};
const source = requireJsonObject(value, "progress_source");
const current = decodeObservation(source.current);
const prior = source.baseline === null ? null : decodeObservation(source.baseline);
if (!Array.isArray(source.window)) throw new EffectRuntimeRequestError("progress window must be an array");
const claimed = source.window.map(decodeObservation);
const known = prior === null ? claimed : [prior, ...claimed];
const knownEvidence = new Set(known.flatMap(observation => observation.evidence));
const kinds = deltaKinds(current, prior);
return {
schema_version: "replan_semantic_delta_v0", accepted: kinds.length > 0, delta_kinds: kinds,
evidence_novel: current.evidence.some(evidence => !knownEvidence.has(evidence)),
observation_repeated: known.some(observation => observation.fingerprint === current.fingerprint),
blocker_repeated: !!current.blocker_id && known.some(observation =>
observation.result === "blocked" && observation.blocker_id === current.blocker_id),
window_size: claimed.length, observation_fingerprint: current.fingerprint,
baseline_fingerprint: prior?.fingerprint ?? null,
reason: kinds.length ? "typed observation changes the replan frontier"
: "typed observation does not change an accepted semantic dimension",
};
}
125 changes: 8 additions & 117 deletions loopx/control_plane/work_items/progress_observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,24 +200,6 @@ def typed_progress_repeat_trigger(
)


def _has_new_terminal_coverage(
current: Mapping[str, Any],
prior: Mapping[str, Any] | None,
) -> bool:
"""Compare terminal coverage semantics without treating proof churn as novelty."""

if prior is None:
return True
result_class = current["result_class"]
if result_class != prior.get("result_class"):
return True
if current.get("coverage_scope_id") != prior.get("coverage_scope_id"):
return True
if result_class == ProgressResultClass.EXPLORATION_EXHAUSTED.value:
return current.get("coverage_complete") != prior.get("coverage_complete")
return False


def _normalized_window(
window: Iterable[Mapping[str, Any]] | None,
) -> list[dict[str, Any]]:
Expand All @@ -234,99 +216,6 @@ def _normalized_window(
return normalized


def semantic_progress_delta(
observation: Mapping[str, Any] | None,
*,
baseline: Mapping[str, Any] | None,
window: Iterable[Mapping[str, Any]] | None = None,
) -> dict[str, Any]:
"""Qualify a typed observation as a replan-closing semantic delta.

`baseline` is the observation the delta kinds are computed against.
`window` lists every typed observation already claimed while the
obligation formed; the codec reports novelty facts against the whole
window so an outcome owner can refuse a replayed claim.
"""

if not isinstance(observation, Mapping):
return {"accepted": False, "reason": "typed progress observation missing"}
current = normalize_progress_observation(observation)
prior = (
normalize_progress_observation(baseline)
if isinstance(baseline, Mapping)
else None
)
claimed = _normalized_window(window)
result_class = current["result_class"]
delta_kinds: list[str] = []
if result_class == ProgressResultClass.ADVANCED.value:
dimension_delta_names = {
"surface_id": "new_surface",
"hypothesis_id": "new_hypothesis",
"probe_kind": "new_probe_family",
}
if current.get("evidence_ids"):
for field, delta_kind in dimension_delta_names.items():
value = current.get(field)
if value and (prior is None or value != prior.get(field)):
delta_kinds.append(delta_kind)
elif result_class == ProgressResultClass.BLOCKED.value:
blocker_id = current.get("blocker_id")
if (
blocker_id
and current.get("evidence_ids")
and (prior is None or blocker_id != prior.get("blocker_id"))
):
delta_kinds.append("new_concrete_blocker")
elif result_class == ProgressResultClass.EXPLORATION_EXHAUSTED.value:
if (
current.get("coverage_complete") is True
and current.get("coverage_scope_id")
and current.get("evidence_ids")
and _has_new_terminal_coverage(current, prior)
):
delta_kinds.append("coverage_backed_exploration_exhausted")
elif result_class == ProgressResultClass.NO_FOLLOWUP.value:
if (
current.get("coverage_scope_id")
and current.get("evidence_ids")
and _has_new_terminal_coverage(current, prior)
):
delta_kinds.append("coverage_backed_no_followup")
# Novelty facts are computed here against the baseline and every claim in
# the obligation window; which obligation sources require them behind a
# renamed surface, hypothesis or probe family is decided by the TypeScript
# outcome owner (work_item.replan_semantics).
known_evidence: set[str] = set(prior.get("evidence_ids") or []) if prior else set()
known_fingerprints: set[str] = {prior["fingerprint"]} if prior else set()
for item in claimed:
known_evidence.update(item.get("evidence_ids") or [])
known_fingerprints.add(item["fingerprint"])
evidence_novel = bool(set(current.get("evidence_ids") or []) - known_evidence)
observation_repeated = current["fingerprint"] in known_fingerprints
blocker_repeated = bool(current.get("blocker_id")) and any(
item.get("result_class") == ProgressResultClass.BLOCKED.value
and item.get("blocker_id") == current.get("blocker_id")
for item in ([prior] if prior else []) + claimed
)
return {
"schema_version": "replan_semantic_delta_v0",
"accepted": bool(delta_kinds),
"delta_kinds": delta_kinds,
"evidence_novel": evidence_novel,
"observation_repeated": observation_repeated,
"blocker_repeated": blocker_repeated,
"window_size": len(claimed),
"observation_fingerprint": current["fingerprint"],
"baseline_fingerprint": prior.get("fingerprint") if prior else None,
"reason": (
"typed observation changes the replan frontier"
if delta_kinds
else "typed observation does not change an accepted semantic dimension"
),
}


def replan_writeback_requirements(
obligation: Mapping[str, Any],
) -> dict[str, Any]:
Expand Down Expand Up @@ -474,18 +363,20 @@ def semantic_delta_from_writeback(
)
elif context is not None and context.get("coverage_truncated"):
raise ValueError("replan writeback requires the complete evidence history when coverage is truncated")
observation_delta = semantic_progress_delta(
progress_observation,
baseline=baseline if isinstance(baseline, Mapping) else None,
window=claimed,
)
progress_source = None
if isinstance(progress_observation, Mapping):
progress_source = {
"current": normalize_progress_observation(progress_observation),
"baseline": normalize_progress_observation(baseline) if isinstance(baseline, Mapping) else None,
"window": _normalized_window(claimed),
}
vision = dict(agent_vision) if isinstance(agent_vision, Mapping) else {}
if vision:
vision["state"] = normalize_goal_vision_state(vision.get("state"))
try:
result = project_replan_semantics({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Complete the accepted production-caller performance qualification before promotion

Trigger: Bound public refresh-state first successful writeback on current full history path. Accepted §6 payoff promotion is not qualified: current File full CLI p95 +27.287ms and SQLite refusal +38.985ms exceeds unexplained25ms additive target; cold/warm/RSS complete attribution and evidence-backed cost decision remain missing.

Minimum repair: Complete current same-workload layered performance qualification and explain/repair the cost or support a documented budget decision; preserve full history, semantic parity and original failed samples.

Regression: Rerun public File/SQLite successful/refusal/replay same-fixture paired workloads plus declared cold/warm/request/RSS boundaries on the exact new head.

"operation": "qualify", "obligation": dict(obligation),
"observation_delta": observation_delta, "agent_vision": vision,
"progress_source": progress_source, "agent_vision": vision,
})
except EffectRuntimeRejected as exc:
raise ValueError(str(exc)) from None
Expand Down
8 changes: 6 additions & 2 deletions loopx/control_plane/work_items/replan_history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
} from "../runtime_decode.ts";
import { projectTodoResumePlanning } from "../todos/resume_planning.ts";

export const PROGRESS_RESULT_CLASSES = [
"advanced", "unchanged", "blocked", "exploration_exhausted", "no_followup",
] as const;

interface Progress {
fingerprint: string;
result: "advanced" | "unchanged" | "blocked" | "exploration_exhausted" | "no_followup";
result: typeof PROGRESS_RESULT_CLASSES[number];
payload: JsonObject;
}
interface Monitor {
Expand Down Expand Up @@ -102,7 +106,7 @@ function decodeProgress(value: unknown): Progress | null {
return {
payload, fingerprint: requireNonEmptyString(payload.fingerprint, "fingerprint"),
result: requireStringLiteral(payload.result_class,
["advanced", "unchanged", "blocked", "exploration_exhausted", "no_followup"], "result_class"),
PROGRESS_RESULT_CLASSES, "result_class"),
};
}
function decode(value: unknown): Request {
Expand Down
Loading
Loading