@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.
Files changed (73) hide show
  1. package/README.md +25 -13
  2. package/bootstrap/lib/steps.sh +214 -9
  3. package/bootstrap/manifest.example.yaml +6 -1
  4. package/docs/COMPLETION-PLAN.md +48 -0
  5. package/omega/Agentik_Engine/README.md +25 -10
  6. package/omega/Agentik_Engine/omega_engine/__init__.py +66 -2
  7. package/omega/Agentik_Engine/omega_engine/account.py +505 -0
  8. package/omega/Agentik_Engine/omega_engine/autonomous.py +538 -0
  9. package/omega/Agentik_Engine/omega_engine/cli.py +467 -29
  10. package/omega/Agentik_Engine/omega_engine/daemons/__init__.py +14 -0
  11. package/omega/Agentik_Engine/omega_engine/daemons/autonomous.py +56 -0
  12. package/omega/Agentik_Engine/omega_engine/daemons/engine.py +187 -0
  13. package/omega/Agentik_Engine/omega_engine/daemons/telegram.py +231 -0
  14. package/omega/Agentik_Engine/omega_engine/educators/__init__.py +51 -0
  15. package/omega/Agentik_Engine/omega_engine/educators/artifact.py +65 -0
  16. package/omega/Agentik_Engine/omega_engine/educators/automation.py +76 -0
  17. package/omega/Agentik_Engine/omega_engine/educators/base.py +327 -0
  18. package/omega/Agentik_Engine/omega_engine/educators/claudecode.py +71 -0
  19. package/omega/Agentik_Engine/omega_engine/educators/connection.py +75 -0
  20. package/omega/Agentik_Engine/omega_engine/educators/coworker.py +68 -0
  21. package/omega/Agentik_Engine/omega_engine/educators/loop.py +82 -0
  22. package/omega/Agentik_Engine/omega_engine/educators/prompt.py +68 -0
  23. package/omega/Agentik_Engine/omega_engine/educators/skill.py +69 -0
  24. package/omega/Agentik_Engine/omega_engine/executor.py +46 -6
  25. package/omega/Agentik_Engine/omega_engine/mission.py +13 -1
  26. package/omega/Agentik_Engine/omega_engine/provider.py +247 -1
  27. package/omega/Agentik_Engine/omega_engine/rag/__init__.py +21 -0
  28. package/omega/Agentik_Engine/omega_engine/rag/agentic.py +83 -0
  29. package/omega/Agentik_Engine/omega_engine/rag/base.py +42 -0
  30. package/omega/Agentik_Engine/omega_engine/rag/corrective.py +119 -0
  31. package/omega/Agentik_Engine/omega_engine/rag/graph.py +169 -0
  32. package/omega/Agentik_Engine/omega_engine/rag/hybrid.py +205 -0
  33. package/omega/Agentik_Engine/omega_engine/rag/multimodal.py +136 -0
  34. package/omega/Agentik_Engine/omega_engine/rag/router.py +110 -0
  35. package/omega/Agentik_Engine/omega_engine/reducer.py +21 -3
  36. package/omega/Agentik_Engine/omega_engine/store.py +65 -5
  37. package/omega/Agentik_Engine/omega_engine/sync.py +304 -0
  38. package/omega/Agentik_Engine/omega_engine/tools.py +272 -0
  39. package/omega/Agentik_Engine/pyproject.toml +1 -1
  40. package/omega/Agentik_Engine/tests/test_account.py +333 -0
  41. package/omega/Agentik_Engine/tests/test_autonomous.py +361 -0
  42. package/omega/Agentik_Engine/tests/test_educators.py +233 -0
  43. package/omega/Agentik_Engine/tests/test_rag.py +287 -0
  44. package/omega/Agentik_Engine/tests/test_snapshot_partial.py +172 -0
  45. package/omega/Agentik_Engine/tests/test_tools_and_sync.py +312 -0
  46. package/omega/Agentik_SSOT/skills/rag-route.md +73 -0
  47. package/package.json +1 -1
  48. package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
  49. package/omega/Agentik_Engine/omega_engine/__pycache__/audit.cpython-313.pyc +0 -0
  50. package/omega/Agentik_Engine/omega_engine/__pycache__/audit_arsenal.cpython-313.pyc +0 -0
  51. package/omega/Agentik_Engine/omega_engine/__pycache__/barrier.cpython-313.pyc +0 -0
  52. package/omega/Agentik_Engine/omega_engine/__pycache__/bus.cpython-313.pyc +0 -0
  53. package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
  54. package/omega/Agentik_Engine/omega_engine/__pycache__/events.cpython-313.pyc +0 -0
  55. package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
  56. package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
  57. package/omega/Agentik_Engine/omega_engine/__pycache__/progress.cpython-313.pyc +0 -0
  58. package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
  59. package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
  60. package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
  61. package/omega/Agentik_Engine/omega_engine/__pycache__/report.cpython-313.pyc +0 -0
  62. package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
  63. package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
  64. package/omega/Agentik_Engine/omega_engine/__pycache__/supervisor.cpython-313.pyc +0 -0
  65. package/omega/Agentik_Engine/omega_engine/__pycache__/task.cpython-313.pyc +0 -0
  66. package/omega/Agentik_Engine/omega_engine/__pycache__/telegram.cpython-313.pyc +0 -0
  67. package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313.pyc +0 -0
  68. package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313.pyc +0 -0
  69. package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313.pyc +0 -0
  70. package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313.pyc +0 -0
  71. package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313.pyc +0 -0
  72. package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313.pyc +0 -0
  73. 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)