@agentikos/omega-os 0.1.0 → 0.2.0
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.
- package/README.md +25 -13
- package/bootstrap/lib/steps.sh +214 -9
- package/bootstrap/manifest.example.yaml +6 -1
- package/docs/COMPLETION-PLAN.md +48 -0
- package/omega/Agentik_Engine/README.md +25 -10
- package/omega/Agentik_Engine/omega_engine/__init__.py +66 -2
- package/omega/Agentik_Engine/omega_engine/account.py +505 -0
- package/omega/Agentik_Engine/omega_engine/autonomous.py +538 -0
- package/omega/Agentik_Engine/omega_engine/cli.py +467 -29
- package/omega/Agentik_Engine/omega_engine/daemons/__init__.py +14 -0
- package/omega/Agentik_Engine/omega_engine/daemons/autonomous.py +56 -0
- package/omega/Agentik_Engine/omega_engine/daemons/engine.py +187 -0
- package/omega/Agentik_Engine/omega_engine/daemons/telegram.py +231 -0
- package/omega/Agentik_Engine/omega_engine/educators/__init__.py +51 -0
- package/omega/Agentik_Engine/omega_engine/educators/artifact.py +65 -0
- package/omega/Agentik_Engine/omega_engine/educators/automation.py +76 -0
- package/omega/Agentik_Engine/omega_engine/educators/base.py +327 -0
- package/omega/Agentik_Engine/omega_engine/educators/claudecode.py +71 -0
- package/omega/Agentik_Engine/omega_engine/educators/connection.py +75 -0
- package/omega/Agentik_Engine/omega_engine/educators/coworker.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/loop.py +82 -0
- package/omega/Agentik_Engine/omega_engine/educators/prompt.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/skill.py +69 -0
- package/omega/Agentik_Engine/omega_engine/executor.py +46 -6
- package/omega/Agentik_Engine/omega_engine/mission.py +13 -1
- package/omega/Agentik_Engine/omega_engine/provider.py +247 -1
- package/omega/Agentik_Engine/omega_engine/rag/__init__.py +21 -0
- package/omega/Agentik_Engine/omega_engine/rag/agentic.py +83 -0
- package/omega/Agentik_Engine/omega_engine/rag/base.py +42 -0
- package/omega/Agentik_Engine/omega_engine/rag/corrective.py +119 -0
- package/omega/Agentik_Engine/omega_engine/rag/graph.py +169 -0
- package/omega/Agentik_Engine/omega_engine/rag/hybrid.py +205 -0
- package/omega/Agentik_Engine/omega_engine/rag/multimodal.py +136 -0
- package/omega/Agentik_Engine/omega_engine/rag/router.py +110 -0
- package/omega/Agentik_Engine/omega_engine/reducer.py +21 -3
- package/omega/Agentik_Engine/omega_engine/store.py +65 -5
- package/omega/Agentik_Engine/omega_engine/sync.py +304 -0
- package/omega/Agentik_Engine/omega_engine/tools.py +272 -0
- package/omega/Agentik_Engine/pyproject.toml +1 -1
- package/omega/Agentik_Engine/tests/test_account.py +333 -0
- package/omega/Agentik_Engine/tests/test_autonomous.py +361 -0
- package/omega/Agentik_Engine/tests/test_educators.py +233 -0
- package/omega/Agentik_Engine/tests/test_rag.py +287 -0
- package/omega/Agentik_Engine/tests/test_snapshot_partial.py +172 -0
- package/omega/Agentik_Engine/tests/test_tools_and_sync.py +312 -0
- package/omega/Agentik_SSOT/skills/rag-route.md +73 -0
- package/package.json +1 -1
- package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit_arsenal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/barrier.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/bus.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/events.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/progress.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/report.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/supervisor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/task.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/telegram.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_report.cpython-313.pyc +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Tests for the multi-RAG subsystem — five real retrievers + router.
|
|
2
|
+
|
|
3
|
+
Every retriever is exercised end-to-end on real data:
|
|
4
|
+
|
|
5
|
+
* HybridRetriever indexes a small corpus into a temp SQLite WAL store and
|
|
6
|
+
the right doc surfaces for a known-good query.
|
|
7
|
+
* GraphRetriever adds typed edges and asserts depth-limited expansion.
|
|
8
|
+
* AgenticRetriever multi-hops on top of HybridRetriever, terminates within
|
|
9
|
+
`max_hops`, and accumulates docs without duplicates.
|
|
10
|
+
* CorrectiveRetriever exercises the refine-on-low-score path (the
|
|
11
|
+
MockProvider returns 40 then 90, so a retry MUST happen).
|
|
12
|
+
* RAGRouter classifies, picks an inner strategy, wraps it in CRAG, and
|
|
13
|
+
returns a `RetrievalResult` with a strategy string that names the pick.
|
|
14
|
+
|
|
15
|
+
Standalone runner: `python3 tests/test_rag.py`. Temp dir cleans up at end.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import shutil
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
25
|
+
|
|
26
|
+
from omega_engine.provider import MockProvider # noqa: E402
|
|
27
|
+
from omega_engine.rag import ( # noqa: E402
|
|
28
|
+
AgenticRetriever,
|
|
29
|
+
CorrectiveRetriever,
|
|
30
|
+
Document,
|
|
31
|
+
GraphRetriever,
|
|
32
|
+
HybridRetriever,
|
|
33
|
+
RAGRouter,
|
|
34
|
+
RetrievalResult,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Three docs whose lexical and semantic separation is clear enough that a
|
|
39
|
+
# simple BM25+cosine blend should rank the right one first for a focused query.
|
|
40
|
+
_CORPUS: list[Document] = [
|
|
41
|
+
Document(
|
|
42
|
+
id="payments",
|
|
43
|
+
text=("The pricing service computes invoice totals and applies "
|
|
44
|
+
"promotional discounts before sending receipts."),
|
|
45
|
+
metadata={"topic": "billing"},
|
|
46
|
+
),
|
|
47
|
+
Document(
|
|
48
|
+
id="auth",
|
|
49
|
+
text=("The authentication module validates JWT tokens, manages "
|
|
50
|
+
"session refresh, and enforces role-based access control."),
|
|
51
|
+
metadata={"topic": "auth"},
|
|
52
|
+
),
|
|
53
|
+
Document(
|
|
54
|
+
id="telemetry",
|
|
55
|
+
text=("The telemetry layer emits structured events to the SQLite "
|
|
56
|
+
"WAL store and exposes a Prometheus endpoint."),
|
|
57
|
+
metadata={"topic": "ops"},
|
|
58
|
+
),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _hybrid(tmp_dir: Path, alpha: float = 0.5) -> HybridRetriever:
|
|
63
|
+
"""Helper — build a HybridRetriever with the corpus already indexed."""
|
|
64
|
+
h = HybridRetriever(tmp_dir / "rag.db", alpha=alpha)
|
|
65
|
+
n = h.index(_CORPUS)
|
|
66
|
+
assert n == len(_CORPUS), f"indexed {n}, expected {len(_CORPUS)}"
|
|
67
|
+
return h
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_hybrid_surfaces_the_right_doc():
|
|
71
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
72
|
+
h = _hybrid(tmp)
|
|
73
|
+
try:
|
|
74
|
+
result = h.retrieve("how is the JWT session refresh handled?", k=3)
|
|
75
|
+
assert isinstance(result, RetrievalResult)
|
|
76
|
+
assert result.strategy == "hybrid"
|
|
77
|
+
assert result.documents, "expected at least one document"
|
|
78
|
+
# The auth doc is the only one that talks about JWT/session.
|
|
79
|
+
top_ids = [d.id for d in result.documents]
|
|
80
|
+
assert top_ids[0] == "auth", f"got {top_ids}"
|
|
81
|
+
# And every result carries its blended score in metadata.
|
|
82
|
+
assert "score" in result.documents[0].metadata
|
|
83
|
+
# k=3 means we should see up to 3 — never more.
|
|
84
|
+
assert len(result.documents) <= 3
|
|
85
|
+
finally:
|
|
86
|
+
h.close()
|
|
87
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_hybrid_dense_and_sparse_both_contribute():
|
|
91
|
+
"""At alpha=1 (dense only) and alpha=0 (sparse only) the top doc for an
|
|
92
|
+
auth-flavoured query stays the auth doc — both legs are real.
|
|
93
|
+
|
|
94
|
+
Uses dim=4096 because the hashing-trick collision rate at the default
|
|
95
|
+
dim=256 is too high for short documents to reliably separate. In a
|
|
96
|
+
real corpus you'd pick dim per the expected vocabulary size; for a
|
|
97
|
+
3-doc test we want headroom.
|
|
98
|
+
"""
|
|
99
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
100
|
+
try:
|
|
101
|
+
for a in (0.0, 1.0):
|
|
102
|
+
h = HybridRetriever(tmp / f"rag_{a}.db", alpha=a, dim=4096)
|
|
103
|
+
h.index(_CORPUS)
|
|
104
|
+
r = h.retrieve("JWT token validation", k=1)
|
|
105
|
+
assert r.documents, f"alpha={a}: empty result"
|
|
106
|
+
assert r.documents[0].id == "auth", (
|
|
107
|
+
f"alpha={a}: top doc was {r.documents[0].id}"
|
|
108
|
+
)
|
|
109
|
+
h.close()
|
|
110
|
+
finally:
|
|
111
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_graph_expansion_and_persistence():
|
|
115
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
116
|
+
try:
|
|
117
|
+
g = GraphRetriever(tmp / "graph.json")
|
|
118
|
+
g.add_node("auth", text="authentication service",
|
|
119
|
+
metadata={"owner": "alice"})
|
|
120
|
+
g.add_node("payments", text="payments service",
|
|
121
|
+
metadata={"owner": "bob"})
|
|
122
|
+
g.add_node("billing", text="billing dashboard")
|
|
123
|
+
g.add_node("telemetry", text="metrics + tracing")
|
|
124
|
+
g.add_edge("auth", "payments", "depends_on")
|
|
125
|
+
g.add_edge("payments", "billing", "renders_into")
|
|
126
|
+
g.add_edge("billing", "telemetry", "emits_to")
|
|
127
|
+
|
|
128
|
+
# Depth 1 from auth = direct neighbours only.
|
|
129
|
+
n1 = g.neighbors("auth", depth=1)
|
|
130
|
+
assert "payments" in n1 and "billing" not in n1, n1
|
|
131
|
+
# Depth 2 reaches one more hop.
|
|
132
|
+
n2 = g.neighbors("auth", depth=2)
|
|
133
|
+
assert "billing" in n2 and "telemetry" not in n2, n2
|
|
134
|
+
# Depth 3 spans the whole graph.
|
|
135
|
+
n3 = g.neighbors("auth", depth=3)
|
|
136
|
+
assert {"payments", "billing", "telemetry"}.issubset(set(n3)), n3
|
|
137
|
+
|
|
138
|
+
# Query path: seed picks `auth` (token match), depth=2 expansion.
|
|
139
|
+
r = g.retrieve("what does auth depend on?", k=4, depth=2)
|
|
140
|
+
ids = [d.id for d in r.documents]
|
|
141
|
+
assert "auth" in ids and "payments" in ids, ids
|
|
142
|
+
assert r.strategy == "graph"
|
|
143
|
+
|
|
144
|
+
# Persistence: a fresh GraphRetriever on the same path should see
|
|
145
|
+
# every edge we just wrote.
|
|
146
|
+
g2 = GraphRetriever(tmp / "graph.json")
|
|
147
|
+
assert "billing" in g2.neighbors("payments", depth=1)
|
|
148
|
+
finally:
|
|
149
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_agentic_multihop_terminates():
|
|
153
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
154
|
+
h = _hybrid(tmp)
|
|
155
|
+
try:
|
|
156
|
+
provider = MockProvider() # rag-agent terminates after 2 calls
|
|
157
|
+
a = AgenticRetriever(h, provider, max_hops=3, k_per_hop=2)
|
|
158
|
+
r = a.retrieve("explain authentication", k=4)
|
|
159
|
+
assert r.strategy == "agentic"
|
|
160
|
+
assert r.documents, "agentic returned no docs"
|
|
161
|
+
# Hop metadata stamped on every doc.
|
|
162
|
+
hops = {d.metadata.get("hop") for d in r.documents}
|
|
163
|
+
# Must have at least one hop and never more than max_hops - 1
|
|
164
|
+
# (since hop indices are 0-based).
|
|
165
|
+
assert hops, "no hop metadata"
|
|
166
|
+
assert max(hops) <= 2, f"hops={hops}"
|
|
167
|
+
# De-duplication: every id appears once.
|
|
168
|
+
ids = [d.id for d in r.documents]
|
|
169
|
+
assert len(ids) == len(set(ids)), f"duplicates in {ids}"
|
|
170
|
+
finally:
|
|
171
|
+
h.close()
|
|
172
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_corrective_grades_and_retries_on_low_score():
|
|
176
|
+
"""MockProvider returns 40 (below threshold) then 90 — the Corrective
|
|
177
|
+
envelope MUST call the inner retriever twice."""
|
|
178
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
179
|
+
h = _hybrid(tmp)
|
|
180
|
+
try:
|
|
181
|
+
provider = MockProvider()
|
|
182
|
+
c = CorrectiveRetriever(h, provider, threshold=70.0, max_retries=2)
|
|
183
|
+
r = c.retrieve("auth", k=3)
|
|
184
|
+
assert r.documents, "corrective returned no docs"
|
|
185
|
+
assert r.strategy.startswith("corrective+"), r.strategy
|
|
186
|
+
# The grader stamps `grader_avg` on each document; final value must
|
|
187
|
+
# be the SECOND call's score (90), proving the retry happened.
|
|
188
|
+
avgs = {d.metadata.get("grader_avg") for d in r.documents}
|
|
189
|
+
assert avgs == {90.0}, f"grader_avg={avgs} (retry never fired?)"
|
|
190
|
+
# Provider call counter is the most direct proof.
|
|
191
|
+
assert provider._grader_calls == 2, provider._grader_calls
|
|
192
|
+
finally:
|
|
193
|
+
h.close()
|
|
194
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_corrective_settles_quickly_when_score_is_already_high():
|
|
198
|
+
"""If the first grade is already above threshold, no retry happens."""
|
|
199
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
200
|
+
h = _hybrid(tmp)
|
|
201
|
+
try:
|
|
202
|
+
provider = MockProvider()
|
|
203
|
+
# threshold below the FIRST grade (40) → corrective is satisfied
|
|
204
|
+
# on the first try and never refines.
|
|
205
|
+
c = CorrectiveRetriever(h, provider, threshold=30.0, max_retries=2)
|
|
206
|
+
r = c.retrieve("auth", k=3)
|
|
207
|
+
assert r.documents
|
|
208
|
+
assert provider._grader_calls == 1, provider._grader_calls
|
|
209
|
+
assert r.strategy.startswith("corrective+"), r.strategy
|
|
210
|
+
finally:
|
|
211
|
+
h.close()
|
|
212
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_router_classifies_and_wraps_in_corrective():
|
|
216
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
217
|
+
h = _hybrid(tmp)
|
|
218
|
+
try:
|
|
219
|
+
g = GraphRetriever(tmp / "graph.json")
|
|
220
|
+
g.add_edge("auth", "payments", "depends_on")
|
|
221
|
+
provider = MockProvider()
|
|
222
|
+
router = RAGRouter(
|
|
223
|
+
strategies={"hybrid": h, "graph": g},
|
|
224
|
+
provider=provider,
|
|
225
|
+
default="hybrid",
|
|
226
|
+
threshold=70.0,
|
|
227
|
+
max_retries=2,
|
|
228
|
+
)
|
|
229
|
+
r = router.retrieve("JWT session refresh", k=3)
|
|
230
|
+
assert isinstance(r, RetrievalResult)
|
|
231
|
+
assert r.documents, "router returned no docs"
|
|
232
|
+
# Strategy string names the inner pick AND the corrective wrap.
|
|
233
|
+
assert r.strategy.startswith("router(")
|
|
234
|
+
assert "corrective+" in r.strategy, r.strategy
|
|
235
|
+
# Provider override returned "hybrid", and the heuristic also
|
|
236
|
+
# defaults to hybrid for this query — so the inner pick is hybrid.
|
|
237
|
+
assert "router(hybrid)" in r.strategy, r.strategy
|
|
238
|
+
finally:
|
|
239
|
+
h.close()
|
|
240
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_router_heuristic_picks_graph_for_relational_query():
|
|
244
|
+
tmp = Path(tempfile.mkdtemp(prefix="omega_rag_"))
|
|
245
|
+
h = _hybrid(tmp)
|
|
246
|
+
try:
|
|
247
|
+
g = GraphRetriever(tmp / "graph.json")
|
|
248
|
+
g.add_edge("auth", "payments", "depends_on")
|
|
249
|
+
|
|
250
|
+
# Stub provider that returns no override → heuristic decides.
|
|
251
|
+
class HeuristicOnlyProvider(MockProvider):
|
|
252
|
+
def run(self, req): # type: ignore[override]
|
|
253
|
+
if req.role == "rag-route":
|
|
254
|
+
# Return empty artifact so router falls back to heuristic.
|
|
255
|
+
from omega_engine.provider import AgentResult
|
|
256
|
+
return AgentResult(text="no override", claimed_done=True,
|
|
257
|
+
artifacts={})
|
|
258
|
+
return super().run(req)
|
|
259
|
+
|
|
260
|
+
provider = HeuristicOnlyProvider()
|
|
261
|
+
router = RAGRouter(
|
|
262
|
+
strategies={"hybrid": h, "graph": g},
|
|
263
|
+
provider=provider,
|
|
264
|
+
corrective=False, # disable corrective for cleaner assertion
|
|
265
|
+
)
|
|
266
|
+
# Query has "depend" and "between" — graph heuristic must fire.
|
|
267
|
+
chosen = router.classify("what does auth depend on?")
|
|
268
|
+
assert chosen == "graph", chosen
|
|
269
|
+
finally:
|
|
270
|
+
h.close()
|
|
271
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _run_all() -> bool:
|
|
275
|
+
tests = [v for k, v in sorted(globals().items())
|
|
276
|
+
if k.startswith("test_") and callable(v)]
|
|
277
|
+
passed = 0
|
|
278
|
+
for t in tests:
|
|
279
|
+
t()
|
|
280
|
+
print(f" PASS {t.__name__}")
|
|
281
|
+
passed += 1
|
|
282
|
+
print(f"\n{passed}/{len(tests)} rag tests passed")
|
|
283
|
+
return passed == len(tests)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
sys.exit(0 if _run_all() else 1)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Snapshotting (bounded reduction) + PARTIAL policy (per-topology join handling).
|
|
2
|
+
|
|
3
|
+
Standalone: python3 tests/test_snapshot_partial.py
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
11
|
+
|
|
12
|
+
from omega_engine.audit import AuditGate # noqa: E402
|
|
13
|
+
from omega_engine.bus import EventBus # noqa: E402
|
|
14
|
+
from omega_engine.events import Event, EventType # noqa: E402
|
|
15
|
+
from omega_engine.executor import Executor # noqa: E402
|
|
16
|
+
from omega_engine.provider import AgentRequest, AgentResult # noqa: E402
|
|
17
|
+
from omega_engine.reducer import reduce_task, reduce_task_fast # noqa: E402
|
|
18
|
+
from omega_engine.router import ModelRouter # noqa: E402
|
|
19
|
+
from omega_engine.store import SQLiteStore # noqa: E402
|
|
20
|
+
from omega_engine.task import Kind, TaskState # noqa: E402
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ----- snapshotting ---------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def test_snapshot_round_trip():
|
|
26
|
+
"""A snapshot captures the reduced state; latest_snapshot returns it."""
|
|
27
|
+
db = tempfile.mktemp(suffix=".db")
|
|
28
|
+
store = SQLiteStore(db)
|
|
29
|
+
for et in (EventType.CREATED, EventType.DISPATCHED, EventType.STARTED):
|
|
30
|
+
store.append(Event(task_id="t1", type=et))
|
|
31
|
+
snap = store.snapshot("t1")
|
|
32
|
+
assert snap is not None
|
|
33
|
+
assert snap["state"] is TaskState.RUNNING
|
|
34
|
+
|
|
35
|
+
again = store.latest_snapshot("t1")
|
|
36
|
+
assert again is not None and again["state"] is TaskState.RUNNING
|
|
37
|
+
store.close()
|
|
38
|
+
os.remove(db)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_snapshot_makes_reduce_correct_and_short():
|
|
42
|
+
"""reduce_task_fast skips events covered by the snapshot."""
|
|
43
|
+
db = tempfile.mktemp(suffix=".db")
|
|
44
|
+
store = SQLiteStore(db)
|
|
45
|
+
seq = [EventType.CREATED, EventType.DISPATCHED, EventType.STARTED]
|
|
46
|
+
for et in seq:
|
|
47
|
+
store.append(Event(task_id="t1", type=et))
|
|
48
|
+
store.snapshot("t1") # state == RUNNING, captured at the 3rd event
|
|
49
|
+
|
|
50
|
+
# 0 events after the snapshot -> fast path returns the snapshot state
|
|
51
|
+
assert reduce_task_fast(store, "t1") is TaskState.RUNNING
|
|
52
|
+
|
|
53
|
+
# add more events; fast == full
|
|
54
|
+
for et in (EventType.CLAIMED_DONE, EventType.VERIFYING, EventType.VERIFIED,
|
|
55
|
+
EventType.COMPLETED):
|
|
56
|
+
store.append(Event(task_id="t1", type=et))
|
|
57
|
+
|
|
58
|
+
full = reduce_task(store.events_for("t1"))
|
|
59
|
+
fast = reduce_task_fast(store, "t1")
|
|
60
|
+
assert full is TaskState.COMPLETED
|
|
61
|
+
assert fast is TaskState.COMPLETED
|
|
62
|
+
# only events AFTER the snapshot should be folded by the fast path
|
|
63
|
+
assert len(store.events_since_snapshot("t1")) == 4
|
|
64
|
+
store.close()
|
|
65
|
+
os.remove(db)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_snapshot_on_unknown_task_returns_none():
|
|
69
|
+
db = tempfile.mktemp(suffix=".db")
|
|
70
|
+
store = SQLiteStore(db)
|
|
71
|
+
assert store.snapshot("ghost") is None
|
|
72
|
+
assert store.latest_snapshot("ghost") is None
|
|
73
|
+
store.close()
|
|
74
|
+
os.remove(db)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ----- PARTIAL policy -------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
class _PartialProvider:
|
|
80
|
+
"""3-worker plan; selected indices fail their runtime audit (verify_cmd=false)."""
|
|
81
|
+
id = "test-partial"
|
|
82
|
+
|
|
83
|
+
def __init__(self, fail_indices) -> None:
|
|
84
|
+
self._fail = set(fail_indices)
|
|
85
|
+
|
|
86
|
+
def run(self, req: AgentRequest) -> AgentResult:
|
|
87
|
+
if req.role in ("oracle", "manager", "aisb"):
|
|
88
|
+
plan = []
|
|
89
|
+
for i in range(3):
|
|
90
|
+
cmd = "false" if i in self._fail else "true"
|
|
91
|
+
plan.append({"role": "worker",
|
|
92
|
+
"spec": {"task": f"t{i}", "verify_cmd": cmd}})
|
|
93
|
+
return AgentResult(text="planned", claimed_done=True, plan=plan)
|
|
94
|
+
if req.role == "worker":
|
|
95
|
+
return AgentResult(
|
|
96
|
+
text="done", claimed_done=True,
|
|
97
|
+
artifacts={"files": ["x.py"], "summary": "done"},
|
|
98
|
+
)
|
|
99
|
+
if req.role in ("verifier", "audit"):
|
|
100
|
+
return AgentResult(
|
|
101
|
+
text="ok", claimed_done=True,
|
|
102
|
+
artifacts={"verdict": {"score": 95, "verified": True,
|
|
103
|
+
"confidence": "high",
|
|
104
|
+
"summary": "ok",
|
|
105
|
+
"findings": [], "fix_plan": []}})
|
|
106
|
+
return AgentResult(text="ok", claimed_done=True)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _engine(provider, partial_policy="fail_up"):
|
|
110
|
+
db = tempfile.mktemp(suffix=".db")
|
|
111
|
+
store = SQLiteStore(db)
|
|
112
|
+
bus = EventBus(store)
|
|
113
|
+
router = ModelRouter.single(provider)
|
|
114
|
+
executor = Executor(store, bus, router, AuditGate(),
|
|
115
|
+
partial_policy=partial_policy)
|
|
116
|
+
return store, executor, db
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_partial_fail_up_default():
|
|
120
|
+
"""Default policy: a PARTIAL scope fails the dispatcher."""
|
|
121
|
+
store, ex, db = _engine(_PartialProvider(fail_indices=[1]))
|
|
122
|
+
result = ex.run_mission("partial mission")
|
|
123
|
+
assert result.final_state is TaskState.FAILED, result.final_state
|
|
124
|
+
store.close(); os.remove(db)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_partial_accept_partial():
|
|
128
|
+
"""accept_partial: the dispatcher completes despite a failed child."""
|
|
129
|
+
store, ex, db = _engine(_PartialProvider(fail_indices=[1]),
|
|
130
|
+
partial_policy="accept_partial")
|
|
131
|
+
result = ex.run_mission("partial mission")
|
|
132
|
+
assert result.final_state is TaskState.COMPLETED, result.final_state
|
|
133
|
+
store.close(); os.remove(db)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_partial_retry_failed_spawns_extra_children():
|
|
137
|
+
"""retry_failed: a replacement task is spawned for each failed child."""
|
|
138
|
+
store, ex, db = _engine(_PartialProvider(fail_indices=[1]),
|
|
139
|
+
partial_policy="retry_failed")
|
|
140
|
+
result = ex.run_mission("partial mission")
|
|
141
|
+
workers = [t for t in result.tasks.values() if t.kind is Kind.EXECUTOR]
|
|
142
|
+
# original 3 + 1 retry replacement = 4 worker tasks (the failing index
|
|
143
|
+
# was retried once; under this provider it fails again, so the final
|
|
144
|
+
# state is FAILED — but the retry attempt is observable)
|
|
145
|
+
assert len(workers) == 4, f"retry did not spawn a replacement: {len(workers)}"
|
|
146
|
+
assert result.final_state is TaskState.FAILED # retry exhausted -> fail_up
|
|
147
|
+
store.close(); os.remove(db)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_unknown_partial_policy_raises():
|
|
151
|
+
raised = False
|
|
152
|
+
try:
|
|
153
|
+
_engine(_PartialProvider([]), partial_policy="ignore")
|
|
154
|
+
except ValueError:
|
|
155
|
+
raised = True
|
|
156
|
+
assert raised
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _run_all() -> bool:
|
|
160
|
+
tests = [v for k, v in sorted(globals().items())
|
|
161
|
+
if k.startswith("test_") and callable(v)]
|
|
162
|
+
passed = 0
|
|
163
|
+
for t in tests:
|
|
164
|
+
t()
|
|
165
|
+
print(f" PASS {t.__name__}")
|
|
166
|
+
passed += 1
|
|
167
|
+
print(f"\n{passed}/{len(tests)} snapshot+partial tests passed")
|
|
168
|
+
return passed == len(tests)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
sys.exit(0 if _run_all() else 1)
|