@agentikos/omega-os 0.1.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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/bin/omega-os.js +48 -0
  4. package/bootstrap/lib/common.sh +73 -0
  5. package/bootstrap/lib/steps.sh +153 -0
  6. package/bootstrap/manifest.example.yaml +45 -0
  7. package/docs/ACCOUNT-AND-BILLING.md +95 -0
  8. package/docs/ARCHITECTURE.md +225 -0
  9. package/docs/AUTONOMOUS-AGENTS.md +128 -0
  10. package/docs/ENGINE-SPEC.md +174 -0
  11. package/docs/INSTALL.md +106 -0
  12. package/docs/MCP-AND-PLUGINS.md +121 -0
  13. package/docs/RUNTIME-PLAN.md +63 -0
  14. package/install.sh +54 -0
  15. package/omega/Agentik_Coding/README.md +21 -0
  16. package/omega/Agentik_Engine/README.md +58 -0
  17. package/omega/Agentik_Engine/omega_engine/__init__.py +58 -0
  18. package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/omega/Agentik_Engine/omega_engine/__pycache__/audit.cpython-313.pyc +0 -0
  20. package/omega/Agentik_Engine/omega_engine/__pycache__/audit_arsenal.cpython-313.pyc +0 -0
  21. package/omega/Agentik_Engine/omega_engine/__pycache__/barrier.cpython-313.pyc +0 -0
  22. package/omega/Agentik_Engine/omega_engine/__pycache__/bus.cpython-313.pyc +0 -0
  23. package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
  24. package/omega/Agentik_Engine/omega_engine/__pycache__/events.cpython-313.pyc +0 -0
  25. package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
  26. package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
  27. package/omega/Agentik_Engine/omega_engine/__pycache__/progress.cpython-313.pyc +0 -0
  28. package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
  29. package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
  30. package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
  31. package/omega/Agentik_Engine/omega_engine/__pycache__/report.cpython-313.pyc +0 -0
  32. package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
  33. package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
  34. package/omega/Agentik_Engine/omega_engine/__pycache__/supervisor.cpython-313.pyc +0 -0
  35. package/omega/Agentik_Engine/omega_engine/__pycache__/task.cpython-313.pyc +0 -0
  36. package/omega/Agentik_Engine/omega_engine/__pycache__/telegram.cpython-313.pyc +0 -0
  37. package/omega/Agentik_Engine/omega_engine/audit.py +96 -0
  38. package/omega/Agentik_Engine/omega_engine/audit_arsenal.py +314 -0
  39. package/omega/Agentik_Engine/omega_engine/barrier.py +45 -0
  40. package/omega/Agentik_Engine/omega_engine/bus.py +45 -0
  41. package/omega/Agentik_Engine/omega_engine/cli.py +158 -0
  42. package/omega/Agentik_Engine/omega_engine/events.py +60 -0
  43. package/omega/Agentik_Engine/omega_engine/executor.py +167 -0
  44. package/omega/Agentik_Engine/omega_engine/mission.py +145 -0
  45. package/omega/Agentik_Engine/omega_engine/progress.py +75 -0
  46. package/omega/Agentik_Engine/omega_engine/project.py +92 -0
  47. package/omega/Agentik_Engine/omega_engine/provider.py +139 -0
  48. package/omega/Agentik_Engine/omega_engine/reducer.py +76 -0
  49. package/omega/Agentik_Engine/omega_engine/report.py +146 -0
  50. package/omega/Agentik_Engine/omega_engine/router.py +34 -0
  51. package/omega/Agentik_Engine/omega_engine/store.py +97 -0
  52. package/omega/Agentik_Engine/omega_engine/supervisor.py +69 -0
  53. package/omega/Agentik_Engine/omega_engine/task.py +91 -0
  54. package/omega/Agentik_Engine/omega_engine/telegram.py +115 -0
  55. package/omega/Agentik_Engine/pyproject.toml +31 -0
  56. package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313.pyc +0 -0
  57. package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313.pyc +0 -0
  58. package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313.pyc +0 -0
  59. package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313.pyc +0 -0
  60. package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313.pyc +0 -0
  61. package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313.pyc +0 -0
  62. package/omega/Agentik_Engine/tests/__pycache__/test_report.cpython-313.pyc +0 -0
  63. package/omega/Agentik_Engine/tests/test_audit_arsenal.py +80 -0
  64. package/omega/Agentik_Engine/tests/test_executor.py +96 -0
  65. package/omega/Agentik_Engine/tests/test_mission.py +64 -0
  66. package/omega/Agentik_Engine/tests/test_progress.py +69 -0
  67. package/omega/Agentik_Engine/tests/test_project.py +61 -0
  68. package/omega/Agentik_Engine/tests/test_reducer.py +144 -0
  69. package/omega/Agentik_Engine/tests/test_report.py +88 -0
  70. package/omega/Agentik_Extra/README.md +37 -0
  71. package/omega/Agentik_Extra/etc/agentik.env.example +19 -0
  72. package/omega/Agentik_Extra/etc/structure.yaml +46 -0
  73. package/omega/Agentik_Orchestration/README.md +43 -0
  74. package/omega/Agentik_Orchestration/autonomous/README.md +29 -0
  75. package/omega/Agentik_Orchestration/autonomous/example-agents.yaml +85 -0
  76. package/omega/Agentik_Orchestration/educators/README.md +55 -0
  77. package/omega/Agentik_Orchestration/topologies/aisb-oracle-worker.yaml +42 -0
  78. package/omega/Agentik_Orchestration/verifier/audit-router.yaml +26 -0
  79. package/omega/Agentik_Providers/README.md +62 -0
  80. package/omega/Agentik_Providers/claude/accounts.example.yaml +28 -0
  81. package/omega/Agentik_Providers/registry.yaml +30 -0
  82. package/omega/Agentik_Runtime/README.md +30 -0
  83. package/omega/Agentik_SSOT/README.md +36 -0
  84. package/omega/Agentik_SSOT/VERSION +1 -0
  85. package/omega/Agentik_SSOT/audits/a11yaudit.yaml +69 -0
  86. package/omega/Agentik_SSOT/audits/apiaudit.yaml +71 -0
  87. package/omega/Agentik_SSOT/audits/automationaudit.yaml +77 -0
  88. package/omega/Agentik_SSOT/audits/codeaudit.yaml +63 -0
  89. package/omega/Agentik_SSOT/audits/copyaudit.yaml +68 -0
  90. package/omega/Agentik_SSOT/audits/dataaudit.yaml +76 -0
  91. package/omega/Agentik_SSOT/audits/debugaudit.yaml +75 -0
  92. package/omega/Agentik_SSOT/audits/dxaudit.yaml +78 -0
  93. package/omega/Agentik_SSOT/audits/featureaudit.yaml +73 -0
  94. package/omega/Agentik_SSOT/audits/flowaudit.yaml +72 -0
  95. package/omega/Agentik_SSOT/audits/logicaudit.yaml +75 -0
  96. package/omega/Agentik_SSOT/audits/motionaudit.yaml +67 -0
  97. package/omega/Agentik_SSOT/audits/perfaudit.yaml +71 -0
  98. package/omega/Agentik_SSOT/audits/refontaudit.yaml +77 -0
  99. package/omega/Agentik_SSOT/audits/retentionaudit.yaml +84 -0
  100. package/omega/Agentik_SSOT/audits/secaudit.yaml +73 -0
  101. package/omega/Agentik_SSOT/audits/seoaudit.yaml +75 -0
  102. package/omega/Agentik_SSOT/audits/uiuxaudit.yaml +61 -0
  103. package/omega/Agentik_SSOT/mcp/mcp-catalog.yaml +136 -0
  104. package/omega/Agentik_SSOT/rules/constitution.md +44 -0
  105. package/omega/Agentik_SSOT/schemas/event.schema.json +45 -0
  106. package/omega/Agentik_SSOT/schemas/task.schema.json +54 -0
  107. package/omega/Agentik_Tools/README.md +42 -0
  108. package/omega/Agentik_Tools/registry.json +15 -0
  109. package/package.json +43 -0
@@ -0,0 +1,76 @@
1
+ """The reducer — the anti-hallucination core of Omega OS.
2
+
3
+ A pure function. No agent ever writes a state; the engine REPLAYS events to
4
+ derive it. An illegal transition raises an exception — it never corrupts state.
5
+
6
+ This is the mechanical answer to "the agent said done and it wasn't": the only
7
+ way to reach VERIFIED is the event `task.verified`, and only the independent
8
+ verifier is allowed to emit that event.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Iterable, Optional
13
+
14
+ from omega_engine.events import Event, EventType
15
+ from omega_engine.task import TERMINAL, TaskState
16
+
17
+
18
+ class IllegalTransition(Exception):
19
+ """Raised when an event would drive an illegal FSM transition."""
20
+
21
+
22
+ # (current_state, event_type) -> next_state. Anything not listed is illegal.
23
+ # `None` is the pre-creation state.
24
+ _TRANSITIONS: dict[tuple[Optional[TaskState], EventType], TaskState] = {
25
+ (None, EventType.CREATED): TaskState.PENDING,
26
+ (TaskState.PENDING, EventType.DISPATCHED): TaskState.DISPATCHED,
27
+ (TaskState.DISPATCHED, EventType.STARTED): TaskState.RUNNING,
28
+ (TaskState.RUNNING, EventType.CLAIMED_DONE): TaskState.CLAIMED_DONE,
29
+ (TaskState.CLAIMED_DONE, EventType.VERIFYING): TaskState.VERIFYING,
30
+ (TaskState.VERIFYING, EventType.VERIFIED): TaskState.VERIFIED,
31
+ (TaskState.VERIFYING, EventType.REJECTED): TaskState.REJECTED,
32
+ (TaskState.VERIFIED, EventType.COMPLETED): TaskState.COMPLETED,
33
+ (TaskState.REJECTED, EventType.DISPATCHED): TaskState.DISPATCHED, # retry
34
+ }
35
+
36
+
37
+ def reduce(state: Optional[TaskState], event: Event) -> TaskState:
38
+ """Apply one event to one state. Pure. Raises IllegalTransition on a bad move."""
39
+ # Heartbeats are liveness pings, never transitions.
40
+ if event.type is EventType.HEARTBEAT:
41
+ if state is None:
42
+ raise IllegalTransition("heartbeat before task.created")
43
+ return state
44
+
45
+ # Scope events are not task-state events — the barrier consumes them.
46
+ if event.type is EventType.SCOPE_JOINABLE:
47
+ if state is None:
48
+ raise IllegalTransition("scope.joinable before task.created")
49
+ return state
50
+
51
+ # The deadman: a `task.failed` event terminates ANY live task. This is what
52
+ # guarantees the join barrier always resolves — no task can hang forever.
53
+ # `failed` on an already-terminal task remains illegal (handled below).
54
+ if event.type is EventType.FAILED and state is not None and state not in TERMINAL:
55
+ return TaskState.FAILED
56
+
57
+ key = (state, event.type)
58
+ if key not in _TRANSITIONS:
59
+ cur = state.value if state is not None else "<none>"
60
+ raise IllegalTransition(
61
+ f"illegal transition: {cur} --{event.type.value}--> ?"
62
+ )
63
+ return _TRANSITIONS[key]
64
+
65
+
66
+ def reduce_task(events: Iterable[Event]) -> TaskState:
67
+ """Fold an ordered event stream into the current task state.
68
+
69
+ The events MUST be ordered (the Event Store returns them by `ts, id`).
70
+ """
71
+ state: Optional[TaskState] = None
72
+ for ev in events:
73
+ state = reduce(state, ev)
74
+ if state is None:
75
+ raise IllegalTransition("empty event stream — task never created")
76
+ return state
@@ -0,0 +1,146 @@
1
+ """Mission reports — every finished mission produces a whitepaper PDF.
2
+
3
+ `build_report()` turns a MissionResult + the event log into the pdfgen whitepaper
4
+ schema; `render_pdf()` shells out to the pdfgen tool. An Oracle finishing ALWAYS
5
+ emits one of these (wired in omega_engine.mission).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ import tempfile
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from omega_engine.executor import MissionResult
19
+ from omega_engine.reducer import reduce_task
20
+ from omega_engine.store import EventStore
21
+ from omega_engine.task import Kind, TaskState
22
+
23
+
24
+ def _resolve_pdfgen() -> str:
25
+ """Locate the pdfgen tool: OMEGA_PDFGEN env -> Agentik_Tools -> PATH."""
26
+ env = os.environ.get("OMEGA_PDFGEN")
27
+ if env and Path(env).exists():
28
+ return env
29
+ home = Path(os.environ.get("OMEGA_HOME", str(Path.home() / "Omega")))
30
+ tool = home / "Agentik_Tools" / "pdfgen" / "bin" / "pdfgen"
31
+ if tool.exists():
32
+ return str(tool)
33
+ on_path = shutil.which("pdfgen")
34
+ if on_path:
35
+ return on_path
36
+ raise RuntimeError(
37
+ "pdfgen not found — install it into Agentik_Tools/ "
38
+ "(see omega/Agentik_Tools/README.md) or set $OMEGA_PDFGEN"
39
+ )
40
+
41
+
42
+ def _state(store: EventStore, task_id: str) -> TaskState:
43
+ return reduce_task(store.events_for(task_id))
44
+
45
+
46
+ def build_report(result: MissionResult, store: EventStore) -> dict[str, Any]:
47
+ """Turn a finished mission into the pdfgen whitepaper schema."""
48
+ workers = [t for t in result.tasks.values() if t.kind is Kind.EXECUTOR]
49
+ verified = sum(1 for w in workers if _state(store, w.id) is TaskState.COMPLETED)
50
+ verdict = "VERIFIED" if result.verified else "FAILED / PARTIAL"
51
+
52
+ done_body: list[str] = []
53
+ for i, w in enumerate(workers, 1):
54
+ evs = store.events_for(w.id)
55
+ vr = next((e for e in evs if e.type.value == "task.verified"), None)
56
+ score = vr.payload.get("score", "—") if vr else "—"
57
+ done_body.append(
58
+ f"### Worker {i} — {w.spec.get('task', w.role)}\n\n"
59
+ f"- Final state: **{_state(store, w.id).value}**\n"
60
+ f"- Audit score: **{score}/100** (verified by live flow)\n"
61
+ )
62
+ if not done_body:
63
+ done_body = ["No worker tasks were spawned for this mission."]
64
+
65
+ test_body = ["The verifier already ran each worker's live flow. To re-check "
66
+ "manually, run each command below — exit 0 means the flow passes.\n"]
67
+ for i, w in enumerate(workers, 1):
68
+ test_body.append(f"- Worker {i}: `{w.spec.get('verify_cmd', '(none declared)')}`")
69
+
70
+ timeline = "\n".join(
71
+ f"{e.type.value:<22} {e.task_id}" for e in store.all_events()
72
+ )
73
+
74
+ sections = [
75
+ {"index": "01", "eyebrow": "Mission", "title": "Mission overview",
76
+ "lead": f"Verdict — {verdict}.",
77
+ "body": (f"**Intent.** {result.intent}\n\n"
78
+ f"**Mission id.** `{result.mission_id}`\n\n"
79
+ f"**Result.** {verified} of {len(workers)} worker task(s) "
80
+ f"verified. Root final state: **{result.final_state.value}**.\n\n"
81
+ f"Completion here is *derived* — the Oracle could not declare "
82
+ f"itself done; the join barrier and the audit gate decided it.")},
83
+ {"index": "02", "eyebrow": "Work", "title": "What was done",
84
+ "lead": "Every worker, its final state, its audit score.",
85
+ "body": "\n".join(done_body)},
86
+ {"index": "03", "eyebrow": "Verification", "title": "What to test",
87
+ "lead": "The verifier ran the live flow — here is how to re-check it.",
88
+ "body": "\n".join(test_body)},
89
+ {"index": "04", "eyebrow": "Trace", "title": "Event timeline",
90
+ "lead": "The append-only event log — this mission, fully replayable.",
91
+ "body": "```\n" + (timeline or "(no events)") + "\n```"},
92
+ ]
93
+
94
+ return {
95
+ "template": "whitepaper",
96
+ "theme": "agentik",
97
+ "eyebrow": "OMEGA OS - MISSION REPORT",
98
+ "title": f"Mission Report - {result.intent[:56]}",
99
+ "subtitle": f"Verdict: {verdict}. {verified}/{len(workers)} tasks verified.",
100
+ "author": "Omega OS - Oracle",
101
+ "date": time.strftime("%d %B %Y"),
102
+ "docId": result.mission_id.upper(),
103
+ "brand": "AGENTIK",
104
+ "abstract": (
105
+ f"This report documents the Omega OS mission \"{result.intent}\". "
106
+ f"The Oracle planned the work; {len(workers)} worker task(s) executed "
107
+ f"it; the audit gate verified each one by running its live flow. "
108
+ f"{verified} of {len(workers)} were verified. The mission's final "
109
+ f"state is {result.final_state.value}. Every figure below is derived "
110
+ f"from the append-only event log — nothing here is self-declared."
111
+ ),
112
+ "sections": sections,
113
+ }
114
+
115
+
116
+ def render_pdf(report_data: dict[str, Any], out_path: str | Path,
117
+ pdfgen_bin: str | None = None) -> Path:
118
+ """Render the whitepaper JSON to a PDF via the pdfgen tool."""
119
+ out = Path(out_path)
120
+ out.parent.mkdir(parents=True, exist_ok=True)
121
+ binary = pdfgen_bin or _resolve_pdfgen()
122
+ fh = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
123
+ try:
124
+ json.dump(report_data, fh, ensure_ascii=False)
125
+ fh.close()
126
+ proc = subprocess.run(
127
+ [binary, "--template=whitepaper", f"--data={fh.name}",
128
+ "--theme=agentik", f"--out={out}"],
129
+ capture_output=True, text=True, timeout=600,
130
+ )
131
+ finally:
132
+ os.unlink(fh.name)
133
+ if proc.returncode != 0 or not out.exists():
134
+ raise RuntimeError(
135
+ f"pdfgen failed: {(proc.stderr or proc.stdout)[-400:]}"
136
+ )
137
+ return out
138
+
139
+
140
+ def generate_mission_report(result: MissionResult, store: EventStore,
141
+ out_dir: str | Path,
142
+ pdfgen_bin: str | None = None) -> Path:
143
+ """Build + render the mission report PDF. Returns the PDF path."""
144
+ data = build_report(result, store)
145
+ out = Path(out_dir) / f"mission-report-{result.mission_id}.pdf"
146
+ return render_pdf(data, out, pdfgen_bin=pdfgen_bin)
@@ -0,0 +1,34 @@
1
+ """The model router — resolve a task role to a provider.
2
+
3
+ The router reasons in roles: a cheap model for triage, Claude for code, a
4
+ different model for audit. Backed by Agentik_Providers/registry.yaml.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from omega_engine.provider import AgentProvider
9
+
10
+
11
+ class ModelRouter:
12
+ """Maps a task role to a concrete provider instance."""
13
+
14
+ def __init__(
15
+ self,
16
+ providers: dict[str, AgentProvider],
17
+ role_provider: dict[str, str],
18
+ default: str,
19
+ ) -> None:
20
+ if default not in providers:
21
+ raise ValueError(f"default provider '{default}' is not registered")
22
+ self._providers = providers
23
+ self._role_provider = role_provider
24
+ self._default = default
25
+
26
+ def resolve(self, role: str) -> AgentProvider:
27
+ """Return the provider for a role, falling back to the default."""
28
+ pid = self._role_provider.get(role, self._default)
29
+ return self._providers.get(pid, self._providers[self._default])
30
+
31
+ @classmethod
32
+ def single(cls, provider: AgentProvider) -> "ModelRouter":
33
+ """A router that sends every role to one provider — used for tests."""
34
+ return cls({provider.id: provider}, {}, provider.id)
@@ -0,0 +1,97 @@
1
+ """The Event Store — append-only, the single source of runtime truth.
2
+
3
+ SQLite in WAL mode. `EventStore` is the interface; `SQLiteStore` is the default
4
+ implementation — simple, ample for a VPS. A `RedisStreamStore` can be dropped in
5
+ later behind the same interface if you ever run hundreds of concurrent workers.
6
+
7
+ Append-only by contract: there is no UPDATE and no DELETE. State is always a
8
+ fold over events (see omega_engine.reducer).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import sqlite3
13
+ from abc import ABC, abstractmethod
14
+ from pathlib import Path
15
+ from typing import Iterator
16
+
17
+ from omega_engine.events import Event
18
+
19
+
20
+ class EventStore(ABC):
21
+ """The store interface. Swap the implementation, keep the contract."""
22
+
23
+ @abstractmethod
24
+ def append(self, event: Event) -> None:
25
+ """Append one immutable event."""
26
+
27
+ @abstractmethod
28
+ def events_for(self, task_id: str) -> list[Event]:
29
+ """All events for one task, ordered by (ts, id)."""
30
+
31
+ @abstractmethod
32
+ def all_events(self, since_ts: float = 0.0) -> Iterator[Event]:
33
+ """Every event since `since_ts`, ordered — for the bus / projections."""
34
+
35
+ @abstractmethod
36
+ def task_ids(self) -> list[str]:
37
+ """Every distinct task id seen in the log."""
38
+
39
+
40
+ _SCHEMA = """
41
+ CREATE TABLE IF NOT EXISTS events (
42
+ id TEXT PRIMARY KEY,
43
+ task_id TEXT NOT NULL,
44
+ type TEXT NOT NULL,
45
+ ts REAL NOT NULL,
46
+ payload TEXT NOT NULL DEFAULT '{}'
47
+ );
48
+ CREATE INDEX IF NOT EXISTS idx_events_task ON events(task_id, ts, id);
49
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts, id);
50
+ """
51
+
52
+
53
+ class SQLiteStore(EventStore):
54
+ """Append-only event log on SQLite (WAL). The default store for a VPS."""
55
+
56
+ def __init__(self, db_path: str | Path):
57
+ self.db_path = str(db_path)
58
+ Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
59
+ self._conn = sqlite3.connect(self.db_path, isolation_level=None)
60
+ self._conn.row_factory = sqlite3.Row
61
+ self._conn.execute("PRAGMA journal_mode=WAL;")
62
+ self._conn.execute("PRAGMA synchronous=NORMAL;")
63
+ self._conn.executescript(_SCHEMA)
64
+
65
+ def append(self, event: Event) -> None:
66
+ self._conn.execute(
67
+ "INSERT INTO events (id, task_id, type, ts, payload) "
68
+ "VALUES (:id, :task_id, :type, :ts, :payload)",
69
+ event.to_row(),
70
+ )
71
+
72
+ def events_for(self, task_id: str) -> list[Event]:
73
+ # ORDER BY rowid = insertion order — the only correct order for a reducer.
74
+ # Ordering by `ts` is unsafe: rapid emits can share a timestamp.
75
+ cur = self._conn.execute(
76
+ "SELECT * FROM events WHERE task_id = ? ORDER BY rowid", (task_id,)
77
+ )
78
+ return [Event.from_row(dict(r)) for r in cur.fetchall()]
79
+
80
+ def all_events(self, since_ts: float = 0.0) -> Iterator[Event]:
81
+ cur = self._conn.execute(
82
+ "SELECT * FROM events WHERE ts >= ? ORDER BY rowid", (since_ts,)
83
+ )
84
+ for r in cur.fetchall():
85
+ yield Event.from_row(dict(r))
86
+
87
+ def task_ids(self) -> list[str]:
88
+ cur = self._conn.execute("SELECT DISTINCT task_id FROM events")
89
+ return [r["task_id"] for r in cur.fetchall()]
90
+
91
+ def close(self) -> None:
92
+ self._conn.close()
93
+
94
+ # NOTE — snapshotting. An append-only log grows without bound. A future
95
+ # `snapshots` table (state of every task at a checkpoint ts) lets `reduce_task`
96
+ # start from the last snapshot instead of replaying from genesis. Snapshots live
97
+ # in Agentik_Runtime/snapshots/. Not required for correctness — only for scale.
@@ -0,0 +1,69 @@
1
+ """The supervisor — deadman + watchdog.
2
+
3
+ Every task carries a Budget (max iterations, wall deadline, heartbeat interval).
4
+ The supervisor scans live tasks and, for any that breached its budget — silent
5
+ past `heartbeat_interval_s`, past `deadline_epoch`, or past `max_iterations`
6
+ retries — emits a `task.failed` event with a reason.
7
+
8
+ The reducer's deadman clause turns that event into a terminal FAILED state,
9
+ which is what guarantees every join barrier eventually resolves: no task, and
10
+ therefore no scope, can hang forever.
11
+
12
+ The cron is only a cold backstop; the live path is the event bus.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from dataclasses import dataclass
18
+
19
+ from omega_engine.events import Event, EventType
20
+ from omega_engine.reducer import reduce_task
21
+ from omega_engine.store import EventStore
22
+ from omega_engine.task import TERMINAL, Budget
23
+
24
+
25
+ @dataclass
26
+ class StallReport:
27
+ task_id: str
28
+ reason: str # "heartbeat" | "deadline" | "max_iterations"
29
+
30
+
31
+ def find_stalled(
32
+ store: EventStore,
33
+ budgets: dict[str, Budget],
34
+ now: float | None = None,
35
+ ) -> list[StallReport]:
36
+ """Scan live tasks; report those that breached their budget.
37
+
38
+ Read-only: returns reports, emits nothing. Safe to dry-run. The caller
39
+ decides whether to append the resulting `task.failed` events.
40
+ """
41
+ now = now if now is not None else time.time()
42
+ reports: list[StallReport] = []
43
+ for task_id in store.task_ids():
44
+ events = store.events_for(task_id)
45
+ if not events:
46
+ continue
47
+ if reduce_task(events) in TERMINAL:
48
+ continue
49
+ budget = budgets.get(task_id, Budget())
50
+ last_ts = max(e.ts for e in events)
51
+ if now - last_ts > budget.heartbeat_interval_s:
52
+ reports.append(StallReport(task_id, "heartbeat"))
53
+ continue
54
+ if budget.deadline_epoch is not None and now > budget.deadline_epoch:
55
+ reports.append(StallReport(task_id, "deadline"))
56
+ continue
57
+ retries = sum(1 for e in events if e.type is EventType.REJECTED)
58
+ if retries >= budget.max_iterations:
59
+ reports.append(StallReport(task_id, "max_iterations"))
60
+ return reports
61
+
62
+
63
+ def deadman_event(report: StallReport) -> Event:
64
+ """Build the terminal `task.failed` event for a stalled task."""
65
+ return Event(
66
+ task_id=report.task_id,
67
+ type=EventType.FAILED,
68
+ payload={"reason": f"deadman:{report.reason}"},
69
+ )
@@ -0,0 +1,91 @@
1
+ """Core task model for the Omega OS orchestration engine.
2
+
3
+ Everything the engine executes — AISB, oracle, worker, audit, autonomous agent —
4
+ is a Task. The only thing that differs is `kind`.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from typing import Any, Optional
11
+
12
+
13
+ class TaskState(str, Enum):
14
+ """The completion FSM. See docs/ENGINE-SPEC.md.
15
+
16
+ The crux: a task is never trusted at CLAIMED_DONE. Only VERIFIED — set by an
17
+ independent verifier that ran the real flow — counts for a parent's barrier.
18
+ """
19
+
20
+ PENDING = "pending"
21
+ DISPATCHED = "dispatched"
22
+ RUNNING = "running"
23
+ CLAIMED_DONE = "claimed_done" # the executor *thinks* it finished — NOT trusted
24
+ VERIFYING = "verifying" # the audit gate is running
25
+ VERIFIED = "verified" # validated by an independent third party
26
+ REJECTED = "rejected" # audits failed → retry within budget
27
+ COMPLETED = "completed" # terminal — success
28
+ FAILED = "failed" # terminal — budget exhausted / stall / honest block
29
+
30
+
31
+ #: The only states from which nothing further happens.
32
+ TERMINAL: frozenset[TaskState] = frozenset({TaskState.COMPLETED, TaskState.FAILED})
33
+
34
+
35
+ class Kind(str, Enum):
36
+ """What role a node plays in the graph. The engine treats all kinds uniformly
37
+ through the same reducer; only the orchestration layer assigns behaviour."""
38
+
39
+ DISPATCHER = "dispatcher" # owns a scope, spawns children (AISB, oracle)
40
+ EXECUTOR = "executor" # does bounded work (worker)
41
+ VERIFIER = "verifier" # runs the audit gate — independent of the executor
42
+
43
+
44
+ class Lifecycle(str, Enum):
45
+ """Ephemeral tasks end. Persistent tasks are autonomous agents."""
46
+
47
+ EPHEMERAL = "ephemeral"
48
+ PERSISTENT = "persistent"
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Trigger:
53
+ """What wakes a task. Mission tasks have no trigger; autonomous agents do."""
54
+
55
+ type: str # "cron" | "event" | "webhook" | "channel"
56
+ config: dict[str, Any] = field(default_factory=dict)
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class Budget:
61
+ """Every task is bounded — no infinite loops, no infinite hangs (principle 6)."""
62
+
63
+ max_iterations: int = 3
64
+ deadline_epoch: Optional[float] = None # wall-clock hard stop
65
+ heartbeat_interval_s: int = 120 # silence longer than this → deadman
66
+
67
+
68
+ @dataclass
69
+ class Task:
70
+ """A unit of execution at any level of the graph.
71
+
72
+ `state` here is a CACHE for convenience. The authoritative state is always
73
+ `reduce_task(events_for(task.id))` — events are the source of truth.
74
+ """
75
+
76
+ id: str
77
+ kind: Kind
78
+ role: str # "aisb" | "oracle" | "worker" | ...
79
+ spec: dict[str, Any] # the brief
80
+ parent_id: Optional[str] = None
81
+ state: TaskState = TaskState.PENDING
82
+ lifecycle: Lifecycle = Lifecycle.EPHEMERAL
83
+ trigger: Optional[Trigger] = None
84
+ budget: Budget = field(default_factory=Budget)
85
+ provider: Optional[str] = None # resolved by the model router
86
+ iteration: int = 0 # incremented on each retry
87
+ created_at: Optional[str] = None
88
+ updated_at: Optional[str] = None
89
+
90
+ def is_terminal(self) -> bool:
91
+ return self.state in TERMINAL
@@ -0,0 +1,115 @@
1
+ """Telegram bridge — live progress in topics, PDF delivery, topic creation.
2
+
3
+ Each project maps to a forum topic. A mission posts a progress message into its
4
+ topic and edits it as work advances; the Oracle's final mission-report PDF is
5
+ delivered into the same topic. Creating a project creates its topic.
6
+
7
+ All calls go through `curl` — reliable, no extra dependency.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import time
15
+ from pathlib import Path
16
+
17
+
18
+ class TelegramError(RuntimeError):
19
+ pass
20
+
21
+
22
+ class TelegramBridge:
23
+ API = "https://api.telegram.org"
24
+
25
+ def __init__(self, token: str, group_chat_id: str) -> None:
26
+ self._token = token
27
+ self._group = str(group_chat_id)
28
+
29
+ @classmethod
30
+ def from_vault(cls, omega_home: str | Path | None = None) -> "TelegramBridge":
31
+ """Build the bridge from Agentik_Extra/etc/secrets/telegram.env."""
32
+ home = Path(omega_home or os.environ.get(
33
+ "OMEGA_HOME", str(Path.home() / "Omega")))
34
+ env_file = home / "Agentik_Extra" / "etc" / "secrets" / "telegram.env"
35
+ if not env_file.exists():
36
+ raise TelegramError(f"telegram secret not found: {env_file}")
37
+ vals: dict[str, str] = {}
38
+ for line in env_file.read_text().splitlines():
39
+ line = line.strip()
40
+ if "=" in line and not line.startswith("#"):
41
+ key, _, value = line.partition("=")
42
+ vals[key.strip()] = value.strip()
43
+ token = vals.get("TELEGRAM_TOKEN")
44
+ if not token:
45
+ raise TelegramError("TELEGRAM_TOKEN missing from the vault")
46
+ return cls(token, vals.get("TELEGRAM_GROUP_CHAT_ID", ""))
47
+
48
+ # -- low level ------------------------------------------------------------
49
+ def _call(self, method: str, fields: dict,
50
+ files: dict | None = None, _retry: bool = True) -> dict:
51
+ cmd = ["curl", "-s", "-X", "POST",
52
+ f"{self.API}/bot{self._token}/{method}"]
53
+ for key, value in fields.items():
54
+ cmd += ["-F", f"{key}={value}"]
55
+ for key, path in (files or {}).items():
56
+ cmd += ["-F", f"{key}=@{path}"]
57
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
58
+ try:
59
+ data = json.loads(proc.stdout or "{}")
60
+ except json.JSONDecodeError as exc:
61
+ raise TelegramError(
62
+ f"bad Telegram response: {proc.stdout[:200]}") from exc
63
+ if not data.get("ok"):
64
+ # respect Telegram flood control: on 429, wait the advised delay
65
+ # and retry exactly once.
66
+ if data.get("error_code") == 429 and _retry:
67
+ wait = int(data.get("parameters", {}).get("retry_after", 5)) + 1
68
+ time.sleep(min(wait, 60))
69
+ return self._call(method, fields, files, _retry=False)
70
+ raise TelegramError(
71
+ f"{method}: {data.get('description', proc.stdout[:200])}")
72
+ return data["result"]
73
+
74
+ # -- public ---------------------------------------------------------------
75
+ def check(self) -> str:
76
+ """Validate the token; return the bot username."""
77
+ return self._call("getMe", {})["username"]
78
+
79
+ def post(self, topic_id: int, text: str) -> int:
80
+ """Post a message into a forum topic; return the message id."""
81
+ result = self._call("sendMessage", {
82
+ "chat_id": self._group,
83
+ "message_thread_id": topic_id,
84
+ "text": text,
85
+ })
86
+ return result["message_id"]
87
+
88
+ def edit(self, message_id: int, text: str) -> None:
89
+ """Edit a posted message — the live progress-bar update."""
90
+ self._call("editMessageText", {
91
+ "chat_id": self._group,
92
+ "message_id": message_id,
93
+ "text": text,
94
+ })
95
+
96
+ def delete(self, message_id: int) -> None:
97
+ self._call("deleteMessage",
98
+ {"chat_id": self._group, "message_id": message_id})
99
+
100
+ def send_document(self, topic_id: int, path: str | Path,
101
+ caption: str = "") -> int:
102
+ """Deliver a file (the mission-report PDF) into a topic."""
103
+ result = self._call(
104
+ "sendDocument",
105
+ {"chat_id": self._group, "message_thread_id": topic_id,
106
+ "caption": caption},
107
+ files={"document": str(path)},
108
+ )
109
+ return result["message_id"]
110
+
111
+ def create_topic(self, name: str) -> int:
112
+ """Create a forum topic — used when a new project is created."""
113
+ result = self._call("createForumTopic",
114
+ {"chat_id": self._group, "name": name})
115
+ return result["message_thread_id"]
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "omega-engine"
3
+ version = "0.1.0"
4
+ description = "The Omega OS orchestration engine — event-sourced, verified-completion agent graphs."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ dependencies = [
9
+ "pyyaml>=6.0",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ providers = [
14
+ "anthropic>=0.40",
15
+ ]
16
+ dev = [
17
+ "pytest>=8.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ omega = "omega_engine.cli:main"
22
+
23
+ [build-system]
24
+ requires = ["hatchling"]
25
+ build-backend = "hatchling.build"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["omega_engine"]
29
+
30
+ [tool.pytest.ini_options]
31
+ testpaths = ["tests"]