Skip to content

Commit 496166c

Browse files
author
mac
committed
fix(goal): type fallback declaration-to-successor relations and share frontier items
Add a structured fallback declaration-to-successor contract to resolve fallback directions without guessing from generic todo_delta actions. An unrelated create/reopen action no longer suppresses declared fallback gaps, and generic primary-path retain actions no longer trigger false positive fallback warnings. Derive both agent_scoped_selectable_advancement_todo_ids and todo_advancement_frontier_counts from a single authoritative helper todo_advancement_frontier_items in projection.py, eliminating duplicate precedence and predicate logic. Signed-off-by: mac <bishopapril850965@yahoo.com>
1 parent d7985ff commit 496166c

5 files changed

Lines changed: 455 additions & 122 deletions

File tree

‎loopx/control_plane/goals/goal_frontier/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
)
5050
from .fallback_disposition import (
5151
VISION_FRONTIER_TODO_DELTA_ACTIONS, # noqa: F401
52+
FallbackDeclaration, # noqa: F401
5253
agent_scoped_selectable_advancement_todo_ids, # noqa: F401
5354
declared_fallback_gap_from_agent_vision,
55+
parse_fallback_declarations, # noqa: F401
5456
parse_vision_todo_delta_entries,
5557
)
5658
from .long_todo_chain import (

‎loopx/control_plane/goals/goal_frontier/fallback_disposition.py‎

Lines changed: 202 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
from __future__ import annotations
22

3+
from dataclasses import dataclass
34
from typing import Any
45

56
from ...todos.contract import (
6-
TODO_TASK_CLASS_ADVANCEMENT,
7-
normalize_todo_claimed_by,
87
normalize_todo_id,
98
)
109
from ...todos.deferred_resume import todo_summary_blocked_successor_items
1110
from ...todos.projection import (
12-
todo_item_excludes_agent,
13-
todo_item_is_actionable_open,
14-
todo_item_task_class,
11+
agent_scoped_selectable_advancement_todo_ids,
1512
)
1613
from ..goal_vision_state import goal_vision_state_is_closed
1714

@@ -40,6 +37,31 @@
4037
)
4138

4239

40+
@dataclass(frozen=True)
41+
class FallbackDeclaration:
42+
"""Structured declaration of a fallback direction and its associated work."""
43+
44+
declaration_id: str
45+
target_todo_id: str | None = None
46+
successor_todo_id: str | None = None
47+
48+
@property
49+
def candidate_todo_ids(self) -> set[str]:
50+
return {
51+
todo_id
52+
for todo_id in (
53+
self.target_todo_id,
54+
self.successor_todo_id,
55+
self.declaration_id,
56+
)
57+
if todo_id
58+
}
59+
60+
@property
61+
def unresolved_todo_id(self) -> str:
62+
return self.target_todo_id or self.declaration_id
63+
64+
4365
def _compact_text(value: Any, *, limit: int) -> str:
4466
return " ".join(str(value or "").strip().split())[:limit]
4567

@@ -63,50 +85,133 @@ def parse_vision_todo_delta_entries(entries: Any) -> list[tuple[str, str]]:
6385
return parsed
6486

6587

66-
def agent_scoped_selectable_advancement_todo_ids(
67-
agent_todo_summary: dict[str, Any] | None,
68-
*,
69-
agent_id: str | None,
70-
) -> set[str]:
71-
"""Return the ids the agent-scoped selectable advancement frontier holds.
88+
def parse_fallback_declarations(
89+
agent_vision: dict[str, Any] | None,
90+
agent_todo_summary: dict[str, Any] | None = None,
91+
) -> list[FallbackDeclaration]:
92+
"""Extract structured fallback declarations from vision and linkage contracts."""
7293

73-
Mirrors the slot order and item predicates of the authoritative
74-
``todo_advancement_frontier_counts`` counter (executable backlog first,
75-
unclaimed priority plus agent-claimed advancement items as the slotless
76-
fallback), including claim ownership: peer-claimed advancement work is
77-
intentionally not part of this agent's selectable frontier.
78-
"""
94+
declarations: list[FallbackDeclaration] = []
95+
seen: set[tuple[str, str | None, str | None]] = set()
7996

80-
if not isinstance(agent_todo_summary, dict):
81-
return set()
82-
normalized_agent_id = normalize_todo_claimed_by(agent_id)
83-
executable_items = agent_todo_summary.get("executable_backlog_items")
84-
if isinstance(executable_items, list):
85-
slots: tuple[Any, ...] = (executable_items,)
86-
else:
87-
slots = (
88-
agent_todo_summary.get("unclaimed_priority_open_items"),
89-
agent_todo_summary.get("claimed_advancement_open_items"),
90-
)
91-
selectable: set[str] = set()
92-
for items in slots:
93-
if not isinstance(items, list):
94-
continue
95-
for item in items:
96-
if not isinstance(item, dict):
97-
continue
98-
if not todo_item_is_actionable_open(item):
99-
continue
100-
if todo_item_task_class(item) != TODO_TASK_CLASS_ADVANCEMENT:
101-
continue
102-
claimed_by = normalize_todo_claimed_by(item.get("claimed_by"))
103-
if normalized_agent_id and claimed_by and claimed_by != normalized_agent_id:
97+
def add_declaration(
98+
declaration_id: str,
99+
target_todo_id: str | None = None,
100+
successor_todo_id: str | None = None,
101+
) -> None:
102+
key = (declaration_id, target_todo_id, successor_todo_id)
103+
if key not in seen:
104+
seen.add(key)
105+
declarations.append(
106+
FallbackDeclaration(
107+
declaration_id=declaration_id,
108+
target_todo_id=target_todo_id,
109+
successor_todo_id=successor_todo_id,
110+
)
111+
)
112+
113+
if isinstance(agent_vision, dict):
114+
patch = agent_vision.get("vision_patch")
115+
patch = patch if isinstance(patch, dict) else {}
116+
for source in (
117+
agent_vision.get("fallback_declarations"),
118+
agent_vision.get("fallback_relationships"),
119+
agent_vision.get("fallbacks"),
120+
patch.get("fallback_declarations"),
121+
patch.get("fallback_relationships"),
122+
patch.get("fallbacks"),
123+
):
124+
if not isinstance(source, list):
104125
continue
105-
if todo_item_excludes_agent(item, agent_id=normalized_agent_id):
126+
for raw in source:
127+
if isinstance(raw, dict):
128+
raw_decl_id = (
129+
raw.get("declaration_id")
130+
or raw.get("fallback_id")
131+
or raw.get("id")
132+
or raw.get("name")
133+
or raw.get("fallback_todo_id")
134+
or raw.get("todo_id")
135+
)
136+
declaration_id = _compact_text(
137+
raw_decl_id,
138+
limit=VISION_TODO_DELTA_ID_LIMIT,
139+
)
140+
target_todo_id = normalize_todo_id(
141+
raw.get("target_todo_id")
142+
or raw.get("fallback_todo_id")
143+
or raw.get("todo_id")
144+
)
145+
successor_todo_id = normalize_todo_id(
146+
raw.get("successor_todo_id")
147+
or raw.get("successor_id")
148+
)
149+
if not declaration_id and target_todo_id:
150+
declaration_id = target_todo_id
151+
if not target_todo_id and not successor_todo_id and declaration_id:
152+
target_todo_id = normalize_todo_id(declaration_id)
153+
if declaration_id:
154+
add_declaration(
155+
declaration_id=declaration_id,
156+
target_todo_id=target_todo_id,
157+
successor_todo_id=successor_todo_id,
158+
)
159+
elif isinstance(raw, str):
160+
text = _compact_text(raw, limit=VISION_TODO_DELTA_ID_LIMIT)
161+
if not text:
162+
continue
163+
if "->" in text:
164+
decl, _, succ = text.partition("->")
165+
d_id = decl.strip()
166+
s_id = normalize_todo_id(succ.strip())
167+
add_declaration(
168+
declaration_id=d_id,
169+
target_todo_id=normalize_todo_id(d_id),
170+
successor_todo_id=s_id,
171+
)
172+
elif ":" in text and not text.startswith(("todo_", "task_")):
173+
decl, _, succ = text.partition(":")
174+
d_id = decl.strip()
175+
s_id = normalize_todo_id(succ.strip())
176+
add_declaration(
177+
declaration_id=d_id,
178+
target_todo_id=normalize_todo_id(d_id),
179+
successor_todo_id=s_id,
180+
)
181+
else:
182+
t_id = normalize_todo_id(text) or text
183+
add_declaration(
184+
declaration_id=text,
185+
target_todo_id=t_id,
186+
successor_todo_id=t_id,
187+
)
188+
189+
# Check linkage contract on blocked successor items in agent_todo_summary
190+
if isinstance(agent_todo_summary, dict):
191+
for slot in (
192+
"deferred_items",
193+
"deferred_resume_candidates",
194+
"current_agent_blocker_items",
195+
):
196+
items = agent_todo_summary.get(slot)
197+
if not isinstance(items, list):
106198
continue
107-
if todo_id := normalize_todo_id(item.get("todo_id")):
108-
selectable.add(todo_id)
109-
return selectable
199+
for item in items:
200+
if not isinstance(item, dict):
201+
continue
202+
if fallback_todo_id := normalize_todo_id(item.get("fallback_todo_id")):
203+
add_declaration(
204+
declaration_id=fallback_todo_id,
205+
target_todo_id=fallback_todo_id,
206+
successor_todo_id=fallback_todo_id,
207+
)
208+
if fallback_succ_id := normalize_todo_id(item.get("fallback_successor_todo_id")):
209+
add_declaration(
210+
declaration_id=fallback_succ_id,
211+
successor_todo_id=fallback_succ_id,
212+
)
213+
214+
return declarations
110215

111216

112217
def _blocked_successor_todo_ids(
@@ -173,16 +278,19 @@ def declared_fallback_gap_from_agent_vision(
173278
) -> dict[str, Any] | None:
174279
"""Project one advisory gap for an unresolved declared fallback.
175280
176-
A fallback direction is declared structurally: the vision's todo_delta
177-
links it to existing Todos via activate/resume/retain entries. Prose
178-
mentions never declare a fallback, so negated or non-English vision text
179-
cannot invent a gap. The declared direction is resolved when one of:
180-
a linked Todo sits on the authoritative agent-scoped selectable
181-
advancement frontier (peer-claimed primary-path Todos do not), the
182-
todo_delta declares a bounded create/reopen successor, or the vision
183-
records an explicit terminal disposition (closed-family state or
184-
path_delta.outcome=stop). The primary path's own blocked successors are
185-
the wait state itself, not fallback declarations, and are excluded.
281+
A fallback direction is declared structurally via the agent vision's
282+
``fallback_declarations`` / ``fallback_relationships`` contract or the
283+
task linkage contract on blocked successor items. Prose mentions never
284+
declare a fallback, and generic ``todo_delta`` actions are not fallback
285+
declarations on their own.
286+
287+
The declared direction is resolved when one of:
288+
1. A linked Todo sits on the authoritative agent-scoped selectable
289+
advancement frontier (peer-claimed primary-path Todos do not);
290+
2. A bounded successor Todo is created or reopened specifically for
291+
this fallback direction; or
292+
3. The vision records an explicit terminal disposition (closed-family state
293+
or path_delta.outcome=stop).
186294
187295
When the primary path is blocked and none of the resolutions holds, the
188296
declared fallback would otherwise disappear silently behind the
@@ -200,26 +308,53 @@ def declared_fallback_gap_from_agent_vision(
200308
agent_id=agent_id,
201309
):
202310
return None
203-
todo_delta = parse_vision_todo_delta_entries(agent_vision.get("todo_delta"))
204-
if {action for action, _ in todo_delta} & VISION_TODO_DELTA_SUCCESSOR_ACTIONS:
311+
312+
declarations = parse_fallback_declarations(
313+
agent_vision,
314+
agent_todo_summary=agent_todo_summary,
315+
)
316+
if not declarations:
205317
return None
318+
319+
selectable_ids = agent_scoped_selectable_advancement_todo_ids(
320+
agent_todo_summary,
321+
agent_id=agent_id,
322+
)
206323
waiting_todo_ids = _blocked_successor_todo_ids(
207324
agent_todo_summary,
208325
agent_id=agent_id,
209326
)
210-
declared_linkage_todo_ids = {
327+
todo_delta = parse_vision_todo_delta_entries(agent_vision.get("todo_delta"))
328+
created_or_reopened_ids = {
211329
todo_id
212330
for action, todo_id in todo_delta
213-
if action in VISION_TODO_DELTA_LINKAGE_ACTIONS
214-
and todo_id not in waiting_todo_ids
331+
if action in VISION_TODO_DELTA_SUCCESSOR_ACTIONS
215332
}
216-
if not declared_linkage_todo_ids:
217-
return None
218-
if declared_linkage_todo_ids & agent_scoped_selectable_advancement_todo_ids(
219-
agent_todo_summary,
220-
agent_id=agent_id,
221-
):
333+
334+
unresolved_ids: set[str] = set()
335+
for declaration in declarations:
336+
candidate_ids = declaration.candidate_todo_ids - waiting_todo_ids
337+
if not candidate_ids:
338+
continue
339+
# Disposition 1: Runnable on authoritative selectable advancement frontier
340+
if candidate_ids & selectable_ids:
341+
continue
342+
# Disposition 2: Bounded successor created/reopened specifically for this fallback
343+
if candidate_ids & created_or_reopened_ids:
344+
continue
345+
if (
346+
declaration.successor_todo_id
347+
and declaration.successor_todo_id in created_or_reopened_ids
348+
):
349+
continue
350+
351+
unresolved_id = declaration.unresolved_todo_id
352+
if unresolved_id not in waiting_todo_ids:
353+
unresolved_ids.add(unresolved_id)
354+
355+
if not unresolved_ids:
222356
return None
357+
223358
gap: dict[str, Any] = {
224359
"kind": VISION_FALLBACK_GAP_TRIGGER,
225360
"source": "latest_agent_vision",
@@ -229,7 +364,7 @@ def declared_fallback_gap_from_agent_vision(
229364
"recommended_action": VISION_FALLBACK_RECOMMENDED_ACTION,
230365
}
231366
unresolved_todo_ids = [
232-
todo_id for todo_id in sorted(declared_linkage_todo_ids) if todo_id
367+
todo_id for todo_id in sorted(unresolved_ids) if todo_id
233368
][:VISION_FALLBACK_RUNNABLE_ITEM_LIMIT]
234369
if unresolved_todo_ids:
235370
gap["unresolved_todo_ids"] = unresolved_todo_ids

‎loopx/control_plane/goals/goal_frontier/semantic_history.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ def latest_agent_vision_from_runs(
167167
}
168168
if isinstance(vision.get("path_delta"), dict):
169169
result["path_delta"] = vision["path_delta"]
170+
if isinstance(vision.get("fallback_declarations"), list):
171+
result["fallback_declarations"] = vision["fallback_declarations"]
172+
if isinstance(vision.get("fallback_relationships"), list):
173+
result["fallback_relationships"] = vision["fallback_relationships"]
170174
return result
171175
return None
172176

0 commit comments

Comments
 (0)