Skip to content

Commit d8daa76

Browse files
committed
refactor(goals): converge Vision wait projection in TypeScript
Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com>
1 parent af68b0f commit d8daa76

7 files changed

Lines changed: 276 additions & 304 deletions

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ Python retains registry, document/material IO, independent Vision-wait consumers
191191
and compatibility exports. Its replaced summary, filter and result coordination
192192
is retired. Persisted completion spellings use shared token data and compatibility codecs
193193
without scalar RPCs;
194+
Vision-wait state projection now composes typed resume planning, current-agent
195+
blocker eligibility, causal lineage coverage and result assembly in one TypeScript
196+
effect. Python retains source-snapshot encoding, result validation, historical
197+
wait-state decoding and independent consumers.
194198
`todo.summary.project`, `todo.completion_state.normalize` and
195199
`todo.completion_state.require_metadata` are retired after their production
196200
callers exit. Existing semantic owners execute in process inside the transaction.

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ Host fence 和真实 provider IO。有界扫描只提供读取提示,写入锁
155155
依赖与闭合判断仍使用完整源。
156156
Python 保留 registry、文档/材料 IO、独立的 Vision-wait 消费者及兼容导出。
157157
被替代的摘要、筛选与结果协调已退役。持久化 completion 拼写使用共享 token 数据与兼容 codec,
158-
不再逐标量调用 RPC;实际生产调用者退出后,退役 `todo.summary.project`、
158+
不再逐标量调用 RPC;
159+
Vision-wait 状态投影现在由一次 TypeScript effect 组合类型化 resume planning、当前 Agent blocker
160+
资格、因果血缘覆盖和结果组装。Python 保留源快照编码、结果校验、历史等待态解码及独立消费者。
161+
实际生产调用者退出后,退役 `todo.summary.project`、
159162
`todo.completion_state.normalize` 与 `todo.completion_state.require_metadata`。
160163
事务在进程内执行既有语义 owner。
161164

‎loopx/control_plane/effect_runtime_handlers.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ export function createEffectRuntimeHandlers(
366366
["goal.checkpoint_read_context.commit", lazyHandler(() => import("./goals/checkpoint_commit.ts"), ({commitCheckpoint}) => commitCheckpoint)],
367367
["goal.checkpoint_read_context.inspect_replay", lazyHandler(() => import("./goals/checkpoint_commit.ts"), ({inspectCheckpointReplay}) => inspectCheckpointReplay)],
368368
["goal.vision_wait.coverage", lazyHandler(() => import("./goals/vision_wait_coverage.ts"), ({projectVisionWaitCoverage}) => projectVisionWaitCoverage)],
369+
["goal.vision_wait.project", lazyHandler(() => import("./goals/vision_wait_coverage.ts"), ({projectGoalVisionWaitState}) => projectGoalVisionWaitState)],
369370
["goal.shared_goal_alignment.project", lazyHandler(() => import("./goals/shared_goal_alignment.ts"), ({projectSharedGoalAlignment}) => projectSharedGoalAlignment)],
370371
["goal.operator_actions.project", lazyHandler(() => import("./goals/operator_actions.ts"), ({projectGoalOperatorActions}) => projectGoalOperatorActions)],
371372
["goal.amendment.execute", lazyHandler(() => import("./goals/goal_amendment_authority.ts"), ({goalAmendmentLocal}) => goalAmendmentLocal)],

‎loopx/control_plane/goals/goal_vision_wait.py‎

Lines changed: 43 additions & 270 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@
55
from typing import Any
66

77
from ..effect_runtime import effect_runtime_result
8-
from ..todos.todo_semantics import todo_item_excludes_agent
9-
from ..todos.contract import (
10-
TODO_TASK_CLASS_BLOCKER,
11-
normalize_todo_claimed_by,
12-
normalize_todo_id,
13-
normalize_todo_status,
14-
)
15-
from ..todos.resume_planning import project_todo_resume_planning
8+
from ..todos.contract import normalize_todo_id
9+
from ..todos.resume_planning import build_todo_resume_planning_request
1610

1711
GOAL_VISION_WAIT_STATE_SCHEMA_VERSION = "goal_vision_wait_state_v0"
18-
VISION_ACCEPTANCE_GAP_KIND = "vision_acceptance_gap"
12+
13+
14+
def _vision_wait_blocker_rows(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
15+
fields = ("todo_id", "task_class", "status", "claimed_by", "excluded_agents", "reason")
16+
return [{key: item[key] for key in fields if key in item} for item in items if isinstance(item, dict)]
17+
18+
19+
def _vision_wait_lineage_rows(items: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None:
20+
if items is None:
21+
return None
22+
fields = ("todo_id", "successor_todo_ids", "resume_condition")
23+
return [{key: item[key] for key in fields if key in item} for item in items if isinstance(item, dict)]
1924

2025

2126
def exact_blocked_successor_wait_state(value: Any) -> dict[str, Any]:
@@ -57,174 +62,6 @@ def exact_blocked_successor_frontier_identity(value: Any) -> str | None:
5762
).hexdigest()[:16]
5863

5964

60-
def _compact_resume_condition(value: Any) -> dict[str, Any]:
61-
if not isinstance(value, dict):
62-
return {}
63-
compact = {
64-
key: value.get(key)
65-
for key in (
66-
"schema_version",
67-
"resume_when",
68-
"satisfied",
69-
"kind",
70-
"target",
71-
"target_todo_id",
72-
"target_status",
73-
"target_archive_state",
74-
"target_source_section",
75-
"target_task_class",
76-
"target_claimed_by",
77-
"target_next_due_at",
78-
"capability",
79-
"provider",
80-
"scheduled_for",
81-
"evaluated_at",
82-
"clock_provider",
83-
"material_change",
84-
"material_change_generation",
85-
"baseline_generation",
86-
"invalid_state",
87-
)
88-
if value.get(key) is not None
89-
}
90-
if "target_archive_state" in value and value["target_archive_state"] is None:
91-
# An explicitly unarchived source is positive evidence. An omitted
92-
# source field must remain omitted for the typed resume owner.
93-
compact["target_archive_state"] = None
94-
if "target_expires_at" in value:
95-
compact["target_expires_at"] = value["target_expires_at"]
96-
if value.get("kind") == "resume_at":
97-
# Keep the complete positive clock proof for the typed interaction
98-
# owner, including the explicit absence of an already-triggered receipt.
99-
compact["resume_receipt"] = value.get("resume_receipt")
100-
compact["satisfied"] = False
101-
return compact
102-
103-
104-
def _acceptance_gap_causal_todo_ids(
105-
gaps: list[dict[str, Any]],
106-
) -> set[str]:
107-
todo_ids: set[str] = set()
108-
for gap in gaps:
109-
for key in (
110-
"todo_id",
111-
"current_todo_id",
112-
"blocked_todo_id",
113-
"completed_todo_id",
114-
"successor_todo_id",
115-
):
116-
todo_id = normalize_todo_id(gap.get(key))
117-
if todo_id:
118-
todo_ids.add(todo_id)
119-
for key in (
120-
"vision_todo_ids",
121-
"lineage_todo_ids",
122-
"successor_todo_ids",
123-
"completed_todo_ids",
124-
):
125-
raw_values = gap.get(key)
126-
values = raw_values if isinstance(raw_values, list) else []
127-
todo_ids.update(
128-
todo_id for value in values if (todo_id := normalize_todo_id(value))
129-
)
130-
return todo_ids
131-
132-
133-
def _wait_lineage_edges(items: list[dict[str, Any]]) -> list[list[str]]:
134-
edges: set[tuple[str, str]] = set()
135-
for item in items:
136-
todo_id = normalize_todo_id(item.get("todo_id"))
137-
if not todo_id:
138-
continue
139-
for value in item.get("successor_todo_ids") or []:
140-
successor = normalize_todo_id(value)
141-
if successor:
142-
edges.add((todo_id, successor))
143-
condition = item.get("resume_condition")
144-
if isinstance(condition, dict) and condition.get("satisfied") is False:
145-
target = normalize_todo_id(condition.get("target_todo_id"))
146-
if target:
147-
# A prerequisite can explain its waiting successor, never the
148-
# reverse: shared prerequisites do not cover unrelated siblings.
149-
edges.add((target, todo_id))
150-
return [list(edge) for edge in sorted(edges)]
151-
152-
153-
def _covered_wait_items(
154-
*,
155-
agent_todo_summary: dict[str, Any] | None,
156-
agent_id: str | None,
157-
causal_todo_ids: set[str],
158-
source_items: list[dict[str, Any]] | None,
159-
lineage_source_items: list[dict[str, Any]] | None,
160-
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
161-
if source_items is not None:
162-
# Source rows have already passed the canonical resume evaluator. Never
163-
# rebuild them from the quota's bounded deferred/backlog display lanes.
164-
agent_todo_summary = {
165-
"items": source_items,
166-
"deferred_items": [
167-
i for i in source_items if i.get("status") == "deferred"
168-
],
169-
"resume_blocked_items": [
170-
i
171-
for i in source_items
172-
if i.get("resume_ready") is False and i.get("status") == "open"
173-
],
174-
"current_agent_blocker_items": source_items,
175-
}
176-
177-
raw_blockers = (agent_todo_summary or {}).get("current_agent_blocker_items")
178-
blocker_items = raw_blockers if isinstance(raw_blockers, list) else []
179-
safe_agent_id = normalize_todo_claimed_by(agent_id)
180-
blocker_items = [
181-
item
182-
for item in blocker_items
183-
if isinstance(item, dict)
184-
and safe_agent_id is not None
185-
and item.get("task_class") == TODO_TASK_CLASS_BLOCKER
186-
and normalize_todo_status(item.get("status")) == "blocked"
187-
and str(item.get("reason") or "").strip()
188-
and normalize_todo_claimed_by(item.get("claimed_by")) == safe_agent_id
189-
and not todo_item_excludes_agent(item, agent_id=safe_agent_id)
190-
]
191-
candidates = project_todo_resume_planning(
192-
agent_todo_summary or {}, agent_id=agent_id
193-
)["blocked_successor_items"]
194-
coverage = effect_runtime_result(
195-
"goal.vision_wait.coverage",
196-
{
197-
"causal_todo_ids": sorted(causal_todo_ids),
198-
"waiting_todo_ids": [i["todo_id"] for i in candidates if i.get("todo_id")],
199-
"blocker_todo_ids": [
200-
i["todo_id"] for i in blocker_items if i.get("todo_id")
201-
],
202-
"edges": _wait_lineage_edges(
203-
lineage_source_items
204-
if lineage_source_items is not None
205-
else source_items
206-
if source_items is not None
207-
else candidates
208-
),
209-
},
210-
)
211-
if (
212-
not isinstance(coverage, dict)
213-
or coverage.get("schema_version") != "vision_wait_coverage_v0"
214-
):
215-
raise RuntimeError("TypeScript vision wait coverage shape mismatch")
216-
if coverage.get("covered") is not True:
217-
return [], []
218-
witnesses = set(coverage["witness_todo_ids"])
219-
blocker_items = [i for i in blocker_items if i.get("todo_id") in witnesses]
220-
candidates = (
221-
[]
222-
if blocker_items
223-
else [i for i in candidates if i.get("todo_id") in witnesses]
224-
)
225-
return blocker_items, candidates
226-
227-
22865
def build_goal_vision_wait_state(
22966
*,
23067
agent_todo_summary: dict[str, Any] | None,
@@ -241,98 +78,34 @@ def build_goal_vision_wait_state(
24178
checkpoints and closed-stage successor requirements remain strict.
24279
"""
24380

244-
gaps = [gap for gap in (acceptance_gaps or []) if isinstance(gap, dict)]
245-
if not gaps or any(gap.get("kind") != VISION_ACCEPTANCE_GAP_KIND for gap in gaps):
246-
return None
247-
if selectable_advancement_count > 0:
248-
return None
249-
250-
# A gap with no causal link cannot borrow another gap's wait witness.
251-
if any(not _acceptance_gap_causal_todo_ids([gap]) for gap in gaps):
252-
return None
253-
causal_todo_ids = _acceptance_gap_causal_todo_ids(gaps)
254-
if agent_todo_summary:
255-
for proof in (agent_todo_summary or {}).get("vision_wait_states") or []:
256-
if (
257-
isinstance(proof, dict)
258-
and proof.get("schema_version") == GOAL_VISION_WAIT_STATE_SCHEMA_VERSION
259-
and proof.get("state") == "waiting"
260-
and proof.get("agent_id") == agent_id
261-
and proof.get("causal_todo_ids") == sorted(causal_todo_ids)
262-
):
263-
return dict(proof)
264-
blocker_items, candidates = _covered_wait_items(
265-
agent_todo_summary=agent_todo_summary,
266-
agent_id=agent_id,
267-
causal_todo_ids=causal_todo_ids,
268-
source_items=source_items,
269-
lineage_source_items=lineage_source_items,
270-
)
271-
if candidates:
272-
selected = candidates[0]
273-
waiting_todo_ids = [
274-
todo_id
275-
for todo_id in (
276-
normalize_todo_id(item.get("todo_id")) for item in candidates[:5]
277-
)
278-
if todo_id
279-
]
280-
payload: dict[str, Any] = {
281-
"schema_version": GOAL_VISION_WAIT_STATE_SCHEMA_VERSION,
282-
"state": "waiting",
283-
"reason_code": "exact_blocked_successor",
284-
"agent_id": agent_id,
285-
"waiting_todo_count": len(candidates),
286-
"waiting_todo_ids": waiting_todo_ids,
287-
"selected_todo_id": normalize_todo_id(selected.get("todo_id")),
288-
"selected_todo_status": selected.get("status"),
289-
"selected_todo_priority": selected.get("priority"),
290-
"selected_todo_claimed_by": selected.get("claimed_by"),
291-
"resume_when": selected.get("resume_when"),
292-
"resume_monitor_generation": selected.get("resume_monitor_generation"),
293-
"resume_condition": _compact_resume_condition(
294-
selected.get("resume_condition")
295-
),
296-
"causal_todo_ids": sorted(causal_todo_ids),
297-
"deferred_acceptance_gap_count": len(gaps),
298-
"deferred_acceptance_gap_kinds": [VISION_ACCEPTANCE_GAP_KIND],
299-
"automatic_resume": True,
300-
"resume_behavior": (
301-
"when resume_ready becomes true, restore ordinary open-todo or "
302-
"deferred-successor routing without closing the active vision"
303-
),
81+
summary = agent_todo_summary if isinstance(agent_todo_summary, dict) else {}
82+
planning_input = summary
83+
if source_items is not None:
84+
planning_input = {
85+
"items": source_items,
86+
"deferred_items": [item for item in source_items if item.get("status") == "deferred"],
87+
"resume_blocked_items": [
88+
item for item in source_items
89+
if item.get("resume_ready") is False and item.get("status") == "open"
90+
],
91+
"current_agent_blocker_items": source_items,
30492
}
305-
return {key: value for key, value in payload.items() if value is not None}
306-
307-
if not blocker_items:
308-
return None
309-
selected = blocker_items[0]
310-
waiting_todo_ids = [
311-
todo_id
312-
for todo_id in (
313-
normalize_todo_id(item.get("todo_id")) for item in blocker_items[:5]
314-
)
315-
if todo_id
316-
]
317-
payload = {
318-
"schema_version": GOAL_VISION_WAIT_STATE_SCHEMA_VERSION,
319-
"state": "waiting",
320-
"reason_code": "current_agent_blocker",
93+
blocker_items = source_items if source_items is not None else summary.get("current_agent_blocker_items")
94+
wait_summary = {"vision_wait_states": summary.get("vision_wait_states"),
95+
"current_agent_blocker_items": _vision_wait_blocker_rows(blocker_items or [])}
96+
lineage_items = lineage_source_items if lineage_source_items is not None else source_items
97+
result = effect_runtime_result("goal.vision_wait.project", {
98+
"agent_todo_summary": wait_summary,
32199
"agent_id": agent_id,
322-
"waiting_todo_count": len(blocker_items),
323-
"waiting_todo_ids": waiting_todo_ids,
324-
"selected_todo_id": normalize_todo_id(selected.get("todo_id")),
325-
"selected_todo_status": selected.get("status"),
326-
"selected_todo_priority": selected.get("priority"),
327-
"selected_todo_claimed_by": selected.get("claimed_by"),
328-
"blocker_reason": str(selected.get("reason") or "").strip(),
329-
"causal_todo_ids": sorted(causal_todo_ids),
330-
"deferred_acceptance_gap_count": len(gaps),
331-
"deferred_acceptance_gap_kinds": [VISION_ACCEPTANCE_GAP_KIND],
332-
"automatic_resume": False,
333-
"resume_behavior": (
334-
"re-evaluate the active vision when the blocker is resolved or "
335-
"superseded, or when runnable scoped advancement appears"
336-
),
337-
}
338-
return {key: value for key, value in payload.items() if value is not None}
100+
"acceptance_gaps": acceptance_gaps or [],
101+
"selectable_advancement_count": selectable_advancement_count,
102+
"lineage_source_items": _vision_wait_lineage_rows(lineage_items),
103+
"resume_planning_request": build_todo_resume_planning_request(planning_input, agent_id=agent_id),
104+
})
105+
if result is not None and (
106+
not isinstance(result, dict)
107+
or result.get("schema_version") != GOAL_VISION_WAIT_STATE_SCHEMA_VERSION
108+
or result.get("state") != "waiting"
109+
):
110+
raise RuntimeError("TypeScript vision wait state shape mismatch")
111+
return result

0 commit comments

Comments
 (0)