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
24 changes: 19 additions & 5 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,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 @@ -510,12 +512,24 @@ 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.

The finite refresh/checkpoint CLI and standalone replan qualification reuse the
existing managed-runtime request scope. It pins only the code revision: source
and provider reads, canonical writes, error ordering and Turn receipts remain
live and unchanged. Complete File/SQLite CLI qualification is required alongside
the native request measurements; fewer fingerprint scans alone are not payoff
evidence. Keep earlier tail failures and same-code A/A variability separate from
the current qualification in the PR validation record.

Replan decision context now follows the same boundary: `work_items/replan_context.ts`
owns scoping, chronology, repetition reduction, result/route diversity and bounded
coverage projection. Its Python codec reads the existing Goal and compact index,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,12 +375,24 @@ 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 规则仍由既有边界负责,
不新增 capability、provider 或设置。

有限的 refresh/checkpoint CLI 与独立 replan 资格判断复用既有 managed-runtime
request scope。它只固定代码版本;source/provider 读取、canonical 写入、错误顺序与
Turn 回执仍实时执行,保持原语义。晋升同时要求完整 File/SQLite CLI 资格与原生请求
测量;减少指纹扫描本身不能证明收益。既往尾部失败、相同代码的 A/A 波动与当前资格
分别保留在 PR 验证记录中。

交付历史边界将 `classification`、`health_check` 与 `recommended_action` 视为
叙述文本。它们不能生成或解除 follow-through obligation,不能证明 outcome,也
不能判定交付规模。例如,`unblocked after dependency update` 不构成 blocker
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: 2 additions & 0 deletions loopx/cli_commands/project_lifecycle_refresh_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
runtime_capabilities_for_cli_projection,
)
from ..control_plane.capability_hooks import PostWritebackHookRegistration
from ..control_plane.effect_runtime import effect_runtime_request_scope
from ..control_plane.goals.goal_vision_policy import (
GOAL_VISION_ADVANCEMENT_POLICY_CHOICES,
)
Expand Down Expand Up @@ -420,6 +421,7 @@ def register_refresh_state_command(
)


@effect_runtime_request_scope()
def handle_refresh_state_command(
args: argparse.Namespace,
*,
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 @@ -786,7 +786,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",
};
}
Loading
Loading