55from typing import Any
66
77from ..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
1711GOAL_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
2126def 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-
22865def 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