aetherorchestrate 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ """
2
+ AetherOrchestrate v2.0.0 — Orchestration for the Aether family.
3
+
4
+ Built through a seven-phase methodology:
5
+ 1. Architectural Specification
6
+ 2. Invariant Definition
7
+ 3. Test Design
8
+ 4. Simulator Implementation
9
+ 5. Test Execution
10
+ 6. Experiments
11
+ 7. Release
12
+ """
13
+
14
+ from .__version__ import __version__
15
+
16
+ __all__ = ["__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
@@ -0,0 +1,3 @@
1
+ """L2 — Assignment layer."""
2
+ from .assigner import assign, AssignmentResult, eligible, preferred_assignee
3
+ __all__ = ["assign", "AssignmentResult", "eligible", "preferred_assignee"]
@@ -0,0 +1,58 @@
1
+ """L2 — Assignment (task → agent)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import dataclass, field
7
+ from ..models import Task, Claim, CapabilityRecord
8
+ from datetime import datetime, timedelta
9
+
10
+
11
+ @dataclass
12
+ class AssignmentResult:
13
+ claim: Claim
14
+ valid: bool = False
15
+ note: str = ""
16
+
17
+
18
+ def _hash(task_id: str, agent: str) -> str:
19
+ return hashlib.sha256(f"{task_id}|{agent}".encode()).hexdigest()
20
+
21
+
22
+ def eligible(task: Task, agents: list[str], capabilities: list[CapabilityRecord],
23
+ revoked: set[str] = frozenset()) -> list[str]:
24
+ """Return the list of eligible agents for a task."""
25
+ capable = set()
26
+ for cap in capabilities:
27
+ if cap.capability in task.requires:
28
+ capable.add(cap.agent)
29
+ eligible_set = [a for a in agents if a in capable or not task.requires]
30
+ return [a for a in eligible_set if a not in revoked]
31
+
32
+
33
+ def preferred_assignee(task: Task, agents: list[str], capabilities: list[CapabilityRecord],
34
+ revoked: set[str] = frozenset()) -> str:
35
+ """Return the preferred assignee (deterministic, view-sensitive)."""
36
+ elig = eligible(task, agents, capabilities, revoked)
37
+ if not elig:
38
+ return ""
39
+ return min(elig, key=lambda a: _hash(task.id, a))
40
+
41
+
42
+ def assign(task: Task, agents: list[str], capabilities: list[CapabilityRecord],
43
+ revoked: set[str] = frozenset(), lease_seconds: int = 60) -> AssignmentResult:
44
+ """Assign a task to its preferred assignee."""
45
+ agent = preferred_assignee(task, agents, capabilities, revoked)
46
+ if not agent:
47
+ return AssignmentResult(
48
+ claim=Claim(task_id=task.id, agent="", lease_until=datetime.utcnow()),
49
+ valid=False,
50
+ note="no eligible agent",
51
+ )
52
+ claim = Claim(
53
+ task_id=task.id,
54
+ agent=agent,
55
+ lease_until=datetime.utcnow() + timedelta(seconds=lease_seconds),
56
+ signature=f"sig:{agent}:{task.id}",
57
+ )
58
+ return AssignmentResult(claim=claim, valid=True, note=f"assigned to {agent}")
@@ -0,0 +1,33 @@
1
+ """AetherOrchestrate CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from .__version__ import __version__
7
+
8
+
9
+ def main() -> int:
10
+ args = sys.argv[1:]
11
+ if not args or args[0] == "info":
12
+ print(f"AetherOrchestrate v{__version__}")
13
+ print("Status: Phase 4 (Simulator Implementation)")
14
+ print("Source paper: DOI 10.5281/zenodo.24095399")
15
+ print("Seven-phase methodology:")
16
+ print(" 1. Architectural Specification ✅")
17
+ print(" 2. Invariant Definition ✅")
18
+ print(" 3. Test Design ✅")
19
+ print(" 4. Simulator Implementation ⏳ (in progress)")
20
+ print(" 5. Test Execution ⏳")
21
+ print(" 6. Experiments ⏳")
22
+ print(" 7. Release ⏳")
23
+ return 0
24
+ if args[0] == "verify":
25
+ from . import __version__ as v
26
+ print(f"✅ aetherorchestrate {v} importable")
27
+ return 0
28
+ print(f"Unknown command: {args[0]}")
29
+ return 1
30
+
31
+
32
+ if __name__ == "__main__":
33
+ sys.exit(main())
@@ -0,0 +1,3 @@
1
+ """L1 — Decomposition layer."""
2
+ from .decomposer import decompose, DecompositionResult
3
+ __all__ = ["decompose", "DecompositionResult"]
@@ -0,0 +1,32 @@
1
+ """L1 — Decomposition (goal → tasks)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from ..models import Goal, Task, Decomposition
7
+
8
+
9
+ @dataclass
10
+ class DecompositionResult:
11
+ goal: Goal
12
+ decomposition: Decomposition
13
+ valid: bool = False
14
+ note: str = ""
15
+
16
+
17
+ def decompose(goal: Goal, hint: str = "") -> DecompositionResult:
18
+ """Decompose a goal into tasks (trivial decomposition)."""
19
+ task = Task(goal_id=goal.id, requires=frozenset(), acceptance=goal.acceptance)
20
+ decomp = Decomposition(
21
+ goal_id=goal.id,
22
+ tasks=[task],
23
+ edges=[],
24
+ kappa={task.id: frozenset()},
25
+ acceptance=goal.acceptance,
26
+ )
27
+ return DecompositionResult(
28
+ goal=goal,
29
+ decomposition=decomp,
30
+ valid=decomp.is_acyclic(),
31
+ note="trivial decomposition" if not hint else "trivial (hint ignored)",
32
+ )
@@ -0,0 +1,3 @@
1
+ """L4 — Dependency tracking layer."""
2
+ from .graph import build_graph, materialize, DependencyGraph
3
+ __all__ = ["build_graph", "materialize", "DependencyGraph"]
@@ -0,0 +1,64 @@
1
+ """L4 — Dependency tracking (task DAG)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from collections import defaultdict
7
+ from ..models import DependencyEdge
8
+
9
+
10
+ @dataclass
11
+ class DependencyGraph:
12
+ nodes: set[str] = field(default_factory=set)
13
+ edges: list[DependencyEdge] = field(default_factory=list)
14
+
15
+ def add_edge(self, edge: DependencyEdge) -> None:
16
+ self.nodes.add(edge.from_task)
17
+ self.nodes.add(edge.to_task)
18
+ self.edges.append(edge)
19
+
20
+
21
+ def build_graph(edges: list[DependencyEdge]) -> DependencyGraph:
22
+ g = DependencyGraph()
23
+ for e in edges:
24
+ g.add_edge(e)
25
+ return g
26
+
27
+
28
+ def _has_cycle(nodes: set[str], edges: list[DependencyEdge]) -> bool:
29
+ adj = defaultdict(list)
30
+ indeg = {n: 0 for n in nodes}
31
+ for e in edges:
32
+ adj[e.from_task].append(e.to_task)
33
+ indeg[e.to_task] = indeg.get(e.to_task, 0) + 1
34
+ queue = [n for n, d in indeg.items() if d == 0]
35
+ visited = 0
36
+ while queue:
37
+ cur = queue.pop()
38
+ visited += 1
39
+ for nxt in adj[cur]:
40
+ indeg[nxt] -= 1
41
+ if indeg[nxt] == 0:
42
+ queue.append(nxt)
43
+ return visited != len(nodes)
44
+
45
+
46
+ def materialize(g: DependencyGraph) -> DependencyGraph:
47
+ """Return the acyclic materialization of g (deterministic cycle-breaking).
48
+
49
+ Rule: repeatedly remove the edge with greatest id inside a cycle,
50
+ until no cycle remains. Deterministic function of the edge set.
51
+ """
52
+ edges = list(g.edges)
53
+ while _has_cycle(g.nodes, edges):
54
+ # Find edges that participate in a cycle, remove the one with max id
55
+ cyclic_edges = [e for e in edges if _has_cycle(g.nodes, edges)]
56
+ if not cyclic_edges:
57
+ break
58
+ to_remove = max(cyclic_edges, key=lambda e: e.id)
59
+ edges = [e for e in edges if e.id != to_remove.id]
60
+
61
+ result = DependencyGraph(nodes=set(g.nodes))
62
+ for e in edges:
63
+ result.add_edge(e)
64
+ return result
@@ -0,0 +1,190 @@
1
+ """AetherOrchestrate v2.0.0 — Core data models.
2
+
3
+ Implements the 7 entities from SPECIFICATION.md §2.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import Optional
12
+ import hashlib
13
+ import uuid
14
+
15
+
16
+ # ═══════════════════════════════════════════════════════════════════════════
17
+ # Enums
18
+ # ═══════════════════════════════════════════════════════════════════════════
19
+
20
+
21
+ class TaskStatus(str, Enum):
22
+ PENDING = "pending"
23
+ ASSIGNED = "assigned"
24
+ RUNNING = "running"
25
+ COMPLETED = "completed"
26
+ FAILED = "failed"
27
+ CANCELLED = "cancelled"
28
+
29
+
30
+ class DependencyType(str, Enum):
31
+ FINISH_TO_START = "finish-to-start"
32
+ START_TO_START = "start-to-start"
33
+ DATA = "data"
34
+
35
+
36
+ # ═══════════════════════════════════════════════════════════════════════════
37
+ # Entities
38
+ # ═══════════════════════════════════════════════════════════════════════════
39
+
40
+
41
+ def _content_id(*parts: str) -> str:
42
+ """Content-addressed id from parts."""
43
+ h = hashlib.sha256("|".join(parts).encode()).hexdigest()
44
+ return h[:16]
45
+
46
+
47
+ @dataclass
48
+ class Goal:
49
+ """E1 — Goal."""
50
+ id: str = ""
51
+ description: str = ""
52
+ authority: str = ""
53
+ created_at: datetime = field(default_factory=datetime.utcnow)
54
+ acceptance: str = ""
55
+
56
+ def __post_init__(self):
57
+ if not self.id:
58
+ self.id = _content_id("goal", self.description, self.authority)
59
+
60
+
61
+ @dataclass
62
+ class Task:
63
+ """E2 — Task."""
64
+ id: str = ""
65
+ goal_id: str = ""
66
+ requires: frozenset[str] = frozenset()
67
+ acceptance: str = ""
68
+ duration: Optional[float] = None
69
+ label: str = ""
70
+
71
+ def __post_init__(self):
72
+ if not self.id:
73
+ self.id = _content_id(
74
+ "task",
75
+ self.goal_id,
76
+ self.label,
77
+ str(sorted(self.requires)),
78
+ )
79
+
80
+
81
+ @dataclass
82
+ class Decomposition:
83
+ """E3 — Decomposition: (T, <, κ, Acc)."""
84
+ id: str = ""
85
+ goal_id: str = ""
86
+ tasks: list[Task] = field(default_factory=list)
87
+ edges: list[tuple[str, str]] = field(default_factory=list) # (from_id, to_id)
88
+ kappa: dict[str, frozenset[str]] = field(default_factory=dict)
89
+ acceptance: str = ""
90
+
91
+ def __post_init__(self):
92
+ if not self.id:
93
+ task_ids = sorted(t.id for t in self.tasks)
94
+ self.id = _content_id("decomp", self.goal_id, ",".join(task_ids))
95
+
96
+ def is_acyclic(self) -> bool:
97
+ """Check that the < relation is acyclic (I2)."""
98
+ from collections import defaultdict, deque
99
+ adj = defaultdict(list)
100
+ indeg = {t.id: 0 for t in self.tasks}
101
+ for a, b in self.edges:
102
+ adj[a].append(b)
103
+ indeg[b] = indeg.get(b, 0) + 1
104
+ queue = deque([t for t, d in indeg.items() if d == 0])
105
+ visited = 0
106
+ while queue:
107
+ cur = queue.popleft()
108
+ visited += 1
109
+ for nxt in adj[cur]:
110
+ indeg[nxt] -= 1
111
+ if indeg[nxt] == 0:
112
+ queue.append(nxt)
113
+ return visited == len(indeg)
114
+
115
+
116
+ @dataclass
117
+ class DependencyEdge:
118
+ """E4 — Dependency Edge."""
119
+ id: str = ""
120
+ from_task: str = ""
121
+ to_task: str = ""
122
+ type: DependencyType = DependencyType.FINISH_TO_START
123
+
124
+ def __post_init__(self):
125
+ if not self.id:
126
+ self.id = _content_id("edge", self.from_task, self.to_task, self.type.value)
127
+
128
+
129
+ @dataclass
130
+ class Claim:
131
+ """E5 — Claim."""
132
+ id: str = ""
133
+ task_id: str = ""
134
+ agent: str = ""
135
+ lease_until: datetime = field(default_factory=datetime.utcnow)
136
+ signature: str = ""
137
+
138
+ def __post_init__(self):
139
+ if not self.id:
140
+ self.id = _content_id("claim", self.task_id, self.agent, self.lease_until.isoformat())
141
+
142
+ def is_active(self, now: Optional[datetime] = None) -> bool:
143
+ now = now or datetime.utcnow()
144
+ return now < self.lease_until
145
+
146
+
147
+ @dataclass
148
+ class Completion:
149
+ """E6 — Completion."""
150
+ id: str = ""
151
+ task_id: str = ""
152
+ agent: str = ""
153
+ result_hash: str = ""
154
+ evidence: Optional[str] = None
155
+ completed_at: datetime = field(default_factory=datetime.utcnow)
156
+
157
+ def __post_init__(self):
158
+ if not self.id:
159
+ self.id = _content_id("completion", self.task_id, self.agent, self.result_hash)
160
+
161
+
162
+ @dataclass
163
+ class CapabilityRecord:
164
+ """E7 — Capability Record."""
165
+ id: str = ""
166
+ agent: str = ""
167
+ capability: str = ""
168
+ authority: str = ""
169
+ valid_until: Optional[datetime] = None
170
+
171
+ def __post_init__(self):
172
+ if not self.id:
173
+ self.id = _content_id("cap", self.agent, self.capability, self.authority)
174
+
175
+
176
+ # ═══════════════════════════════════════════════════════════════════════════
177
+ # Public API
178
+ # ═══════════════════════════════════════════════════════════════════════════
179
+
180
+ __all__ = [
181
+ "TaskStatus",
182
+ "DependencyType",
183
+ "Goal",
184
+ "Task",
185
+ "Decomposition",
186
+ "DependencyEdge",
187
+ "Claim",
188
+ "Completion",
189
+ "CapabilityRecord",
190
+ ]
@@ -0,0 +1,3 @@
1
+ """L5 — Routing layer."""
2
+ from .router import route, RouteResult
3
+ __all__ = ["route", "RouteResult"]
@@ -0,0 +1,27 @@
1
+ """L5 — Routing (task → executor)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from ..models import Task, Claim
7
+
8
+
9
+ @dataclass
10
+ class RouteResult:
11
+ delivered: dict[str, bool] = field(default_factory=dict)
12
+ complete: bool = False
13
+ note: str = ""
14
+
15
+
16
+ def route(tasks: list[Task], claims: list[Claim]) -> RouteResult:
17
+ """Route tasks to their assignees (direct routing)."""
18
+ claim_by_task = {c.task_id: c for c in claims if c.is_active()}
19
+ delivered = {}
20
+ for t in tasks:
21
+ c = claim_by_task.get(t.id)
22
+ delivered[t.id] = c is not None and c.agent != ""
23
+ return RouteResult(
24
+ delivered=delivered,
25
+ complete=all(delivered.values()) if delivered else False,
26
+ note="direct routing",
27
+ )
@@ -0,0 +1,3 @@
1
+ """L3 — Scheduling layer."""
2
+ from .scheduler import schedule, ScheduleResult
3
+ __all__ = ["schedule", "ScheduleResult"]
@@ -0,0 +1,41 @@
1
+ """L3 — Scheduling (when tasks run)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from ..models import Task, DependencyEdge, DependencyType
7
+
8
+
9
+ @dataclass
10
+ class ScheduleResult:
11
+ order: list[str] = field(default_factory=list)
12
+ feasible: bool = False
13
+ note: str = ""
14
+
15
+
16
+ def schedule(tasks: list[Task], edges: list[DependencyEdge]) -> ScheduleResult:
17
+ """Compute a schedule hint (deterministic function of S)."""
18
+ task_ids = {t.id for t in tasks}
19
+ adj: dict[str, list[str]] = {tid: [] for tid in task_ids}
20
+ indeg: dict[str, int] = {tid: 0 for tid in task_ids}
21
+ for e in edges:
22
+ if e.from_task in task_ids and e.to_task in task_ids:
23
+ adj[e.from_task].append(e.to_task)
24
+ indeg[e.to_task] += 1
25
+
26
+ queue = sorted([t for t, d in indeg.items() if d == 0])
27
+ order = []
28
+ while queue:
29
+ cur = queue.pop(0)
30
+ order.append(cur)
31
+ for nxt in sorted(adj[cur]):
32
+ indeg[nxt] -= 1
33
+ if indeg[nxt] == 0:
34
+ queue.append(nxt)
35
+ queue.sort()
36
+ feasible = len(order) == len(task_ids)
37
+ return ScheduleResult(
38
+ order=order,
39
+ feasible=feasible,
40
+ note="feasible" if feasible else "cycle detected",
41
+ )
@@ -0,0 +1,5 @@
1
+ """Simulation layer."""
2
+ from .network import Network
3
+ from .node import Node
4
+ from .testbed import Testbed
5
+ __all__ = ["Network", "Node", "Testbed"]
@@ -0,0 +1,36 @@
1
+ """Simulated network."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class Network:
10
+ nodes: list[str] = field(default_factory=list)
11
+ partitions: dict[str, set[str]] = field(default_factory=dict)
12
+
13
+ def add_node(self, node_id: str) -> None:
14
+ if node_id in self.nodes:
15
+ return
16
+ self.nodes.append(node_id)
17
+ # كل العقد الموجودة يمكنها الوصول إلى العقدة الجديدة
18
+ for existing in self.nodes:
19
+ if existing == node_id:
20
+ continue
21
+ self.partitions.setdefault(existing, set()).add(node_id)
22
+ # العقدة الجديدة يمكنها الوصول إلى كل العقد الموجودة
23
+ self.partitions[node_id] = set(self.nodes) - {node_id}
24
+
25
+ def can_reach(self, src: str, dst: str) -> bool:
26
+ return dst in self.partitions.get(src, set())
27
+
28
+ def partition(self, group_a: set[str], group_b: set[str]) -> None:
29
+ for a in group_a:
30
+ self.partitions[a] = set(group_a) - {a}
31
+ for b in group_b:
32
+ self.partitions[b] = set(group_b) - {b}
33
+
34
+ def heal(self) -> None:
35
+ for n in self.nodes:
36
+ self.partitions[n] = set(self.nodes) - {n}
@@ -0,0 +1,21 @@
1
+ """Simulated node."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+
8
+
9
+ @dataclass
10
+ class Node:
11
+ id: str
12
+ admitted: set[str] = field(default_factory=set) # delta ids
13
+ claims: list[str] = field(default_factory=list)
14
+ revocations: set[str] = field(default_factory=set)
15
+ is_byzantine: bool = False
16
+
17
+ def admit(self, delta_id: str) -> None:
18
+ self.admitted.add(delta_id)
19
+
20
+ def has(self, delta_id: str) -> bool:
21
+ return delta_id in self.admitted
@@ -0,0 +1,35 @@
1
+ """Simulation testbed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from .network import Network
7
+ from .node import Node
8
+
9
+
10
+ @dataclass
11
+ class Testbed:
12
+ network: Network = field(default_factory=Network)
13
+ nodes: dict[str, Node] = field(default_factory=dict)
14
+
15
+ def add_node(self, node_id: str, byzantine: bool = False) -> Node:
16
+ node = Node(id=node_id, is_byzantine=byzantine)
17
+ self.nodes[node_id] = node
18
+ self.network.add_node(node_id)
19
+ return node
20
+
21
+ def broadcast(self, src: str, delta_id: str) -> int:
22
+ count = 0
23
+ for dst, node in self.nodes.items():
24
+ if dst == src:
25
+ continue
26
+ if self.network.can_reach(src, dst):
27
+ node.admit(delta_id)
28
+ count += 1
29
+ return count
30
+
31
+ def converge(self) -> bool:
32
+ if not self.nodes:
33
+ return True
34
+ first = next(iter(self.nodes.values()))
35
+ return all(n.admitted == first.admitted for n in self.nodes.values())