@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,187 @@
1
+ """The engine daemon — keeps the event store alive 24/7.
2
+
3
+ What it actually does:
4
+
5
+ * opens ``$OMEGA_HOME/Agentik_Runtime/eventlog/omega.db`` (WAL-mode SQLite)
6
+ * periodically appends a ``system.heartbeat`` event so the store always has a
7
+ fresh "this engine is alive" anchor
8
+ * exposes a small stdlib HTTP API on ``127.0.0.1:OMEGA_ENGINE_PORT`` so the
9
+ other daemons (telegram, autonomous) can ask the engine to run a mission
10
+ inside *one* process tree (avoids each daemon spinning its own duplicate
11
+ executor stack)
12
+ * exits cleanly on SIGTERM / SIGINT (systemd `Restart=always` handles the rest)
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import os
19
+ import signal
20
+ import threading
21
+ import time
22
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
23
+ from pathlib import Path
24
+
25
+ from omega_engine.bus import EventBus
26
+ from omega_engine.events import Event, EventType
27
+ from omega_engine.store import SQLiteStore
28
+
29
+ logger = logging.getLogger("omega.daemon.engine")
30
+
31
+ # the heartbeat event lives outside the task FSM, so we reuse the HEARTBEAT
32
+ # event type but with a system-level task id.
33
+ _SYSTEM_TASK_ID = "system.engine"
34
+ _HEARTBEAT_PERIOD_S = 60.0
35
+
36
+
37
+ def _omega_home() -> Path:
38
+ return Path(os.environ.get("OMEGA_HOME", str(Path.home() / "Omega")))
39
+
40
+
41
+ def _store_path(home: Path) -> Path:
42
+ return home / "Agentik_Runtime" / "eventlog" / "omega.db"
43
+
44
+
45
+ # ─────────────────────────────────────────────────────────────────────────────
46
+ # HTTP API — local-only mission dispatch
47
+ # ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ class _EngineHandler(BaseHTTPRequestHandler):
50
+ """A tiny REST-ish surface. JSON in/out. Loopback only.
51
+
52
+ Endpoints:
53
+
54
+ * ``GET /health`` -- liveness
55
+ * ``POST /mission`` -- body: ``{"intent": "..."}``; runs a mission
56
+ synchronously and returns the result. Used
57
+ by the telegram daemon for project-bound
58
+ topics.
59
+ """
60
+
61
+ # silence stock access log → use our logger
62
+ def log_message(self, fmt: str, *args) -> None: # noqa: A003
63
+ logger.debug("http: " + fmt, *args)
64
+
65
+ def _reply(self, status: int, payload: dict) -> None:
66
+ body = json.dumps(payload).encode()
67
+ self.send_response(status)
68
+ self.send_header("Content-Type", "application/json")
69
+ self.send_header("Content-Length", str(len(body)))
70
+ self.end_headers()
71
+ self.wfile.write(body)
72
+
73
+ def do_GET(self) -> None: # noqa: N802 — required by BaseHTTPRequestHandler
74
+ if self.path == "/health":
75
+ self._reply(200, {"ok": True, "ts": time.time()})
76
+ return
77
+ self._reply(404, {"error": "not found"})
78
+
79
+ def do_POST(self) -> None: # noqa: N802
80
+ if self.path != "/mission":
81
+ self._reply(404, {"error": "not found"})
82
+ return
83
+ length = int(self.headers.get("Content-Length", "0"))
84
+ raw = self.rfile.read(length).decode() if length else "{}"
85
+ try:
86
+ body = json.loads(raw) if raw else {}
87
+ except json.JSONDecodeError:
88
+ self._reply(400, {"error": "invalid json"})
89
+ return
90
+ intent = body.get("intent")
91
+ topic_id = body.get("topic_id")
92
+ if not intent:
93
+ self._reply(400, {"error": "missing intent"})
94
+ return
95
+ try:
96
+ from omega_engine.mission import run_mission
97
+ outcome = run_mission(
98
+ intent=intent,
99
+ topic_id=topic_id,
100
+ telegram=None, # the telegram daemon supplies its own bridge
101
+ )
102
+ self._reply(200, {
103
+ "mission_id": outcome.result.mission_id,
104
+ "verified": outcome.result.verified,
105
+ "final_state": outcome.result.final_state.value,
106
+ "progress_pct": outcome.progress_pct,
107
+ "report_pdf": (
108
+ str(outcome.report_pdf) if outcome.report_pdf else None
109
+ ),
110
+ })
111
+ except Exception as exc: # noqa: BLE001
112
+ logger.exception("engine HTTP: mission failed")
113
+ self._reply(500, {"error": str(exc)})
114
+
115
+
116
+ def _serve_http(port: int, stop: threading.Event) -> None:
117
+ """Run the HTTP API until ``stop`` is set."""
118
+ httpd = ThreadingHTTPServer(("127.0.0.1", port), _EngineHandler)
119
+ httpd.timeout = 1.0
120
+ logger.info("engine HTTP API on 127.0.0.1:%d", port)
121
+ while not stop.is_set():
122
+ httpd.handle_request()
123
+ httpd.server_close()
124
+
125
+
126
+ # ─────────────────────────────────────────────────────────────────────────────
127
+ # Main loop
128
+ # ─────────────────────────────────────────────────────────────────────────────
129
+
130
+ def main() -> int:
131
+ logging.basicConfig(
132
+ level=os.environ.get("OMEGA_LOG_LEVEL", "INFO"),
133
+ format="%(asctime)s %(name)s %(levelname)s %(message)s",
134
+ )
135
+ home = _omega_home()
136
+ db_path = _store_path(home)
137
+ db_path.parent.mkdir(parents=True, exist_ok=True)
138
+
139
+ store = SQLiteStore(db_path)
140
+ bus = EventBus(store)
141
+ logger.info("engine daemon online — store=%s", db_path)
142
+
143
+ stop = threading.Event()
144
+ signal.signal(signal.SIGTERM, lambda *_: stop.set())
145
+ signal.signal(signal.SIGINT, lambda *_: stop.set())
146
+
147
+ port = int(os.environ.get("OMEGA_ENGINE_PORT", "8731"))
148
+ http_thread = threading.Thread(
149
+ target=_serve_http, args=(port, stop), daemon=True, name="omega-engine-http",
150
+ )
151
+ http_thread.start()
152
+
153
+ # first heartbeat at startup so `omega status` immediately shows the engine
154
+ bus.publish(Event(
155
+ task_id=_SYSTEM_TASK_ID,
156
+ type=EventType.HEARTBEAT,
157
+ payload={"daemon": "engine", "event": "start"},
158
+ ))
159
+
160
+ last_beat = time.time()
161
+ try:
162
+ while not stop.is_set():
163
+ now = time.time()
164
+ if now - last_beat >= _HEARTBEAT_PERIOD_S:
165
+ try:
166
+ bus.publish(Event(
167
+ task_id=_SYSTEM_TASK_ID,
168
+ type=EventType.HEARTBEAT,
169
+ payload={"daemon": "engine", "ts": now},
170
+ ))
171
+ except Exception: # noqa: BLE001 — never die on a heartbeat
172
+ logger.exception("engine daemon: heartbeat publish failed")
173
+ last_beat = now
174
+ stop.wait(1.0)
175
+ finally:
176
+ bus.publish(Event(
177
+ task_id=_SYSTEM_TASK_ID,
178
+ type=EventType.HEARTBEAT,
179
+ payload={"daemon": "engine", "event": "stop"},
180
+ ))
181
+ store.close()
182
+ logger.info("engine daemon stopped")
183
+ return 0
184
+
185
+
186
+ if __name__ == "__main__": # pragma: no cover
187
+ raise SystemExit(main())
@@ -0,0 +1,231 @@
1
+ """The Telegram daemon — listens to the bot, routes messages.
2
+
3
+ Long-polls ``getUpdates`` (HTTP via ``curl``, the same pattern
4
+ :class:`omega_engine.telegram.TelegramBridge` uses) and routes every inbound
5
+ message:
6
+
7
+ 1. If the message is in a forum topic that an autonomous charter is bound to
8
+ → :meth:`AutonomousSupervisor.on_channel_message` (the agent handles it).
9
+ 2. Else, if the topic is a *project* topic registered in
10
+ ``Agentik_Coding/projects.json`` → :func:`run_mission` with that topic id.
11
+ 3. Else → log and ignore.
12
+
13
+ The daemon shares no in-process state with the engine daemon — it talks to it
14
+ through the engine's HTTP API when a mission has to run (avoids two parallel
15
+ SQLite writers). Telegram is best-effort: a network blip never kills the loop.
16
+
17
+ On SIGTERM/SIGINT the daemon stops cleanly.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import os
24
+ import signal
25
+ import subprocess
26
+ import time
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from omega_engine.autonomous import build_supervisor_from_home
31
+
32
+ logger = logging.getLogger("omega.daemon.telegram")
33
+
34
+ _POLL_TIMEOUT_S = 25 # Telegram long-poll
35
+ _BACKOFF_S = 5.0 # on network / API errors
36
+ _API = "https://api.telegram.org"
37
+
38
+
39
+ def _omega_home() -> Path:
40
+ return Path(os.environ.get("OMEGA_HOME", str(Path.home() / "Omega")))
41
+
42
+
43
+ # ─────────────────────────────────────────────────────────────────────────────
44
+ # Telegram bot — getUpdates loop (curl-based, no extra deps)
45
+ # ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ def _read_token(home: Path) -> str:
48
+ env_file = home / "Agentik_Extra" / "etc" / "secrets" / "telegram.env"
49
+ if not env_file.exists():
50
+ raise RuntimeError(f"telegram secret not found: {env_file}")
51
+ for line in env_file.read_text().splitlines():
52
+ line = line.strip()
53
+ if line.startswith("TELEGRAM_TOKEN="):
54
+ return line.split("=", 1)[1].strip()
55
+ raise RuntimeError("TELEGRAM_TOKEN missing from the vault")
56
+
57
+
58
+ def _curl_json(url: str, *, timeout: int) -> dict[str, Any]:
59
+ """Issue one curl call; return the parsed JSON or ``{}`` on hard failure."""
60
+ cmd = ["curl", "-s", "--max-time", str(timeout + 5), url]
61
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 10)
62
+ if proc.returncode != 0:
63
+ raise RuntimeError(f"curl exit {proc.returncode}: {proc.stderr[:200]}")
64
+ try:
65
+ return json.loads(proc.stdout or "{}")
66
+ except json.JSONDecodeError as exc:
67
+ raise RuntimeError(f"bad JSON: {proc.stdout[:200]}") from exc
68
+
69
+
70
+ def _get_updates(token: str, offset: int) -> list[dict[str, Any]]:
71
+ """One long-poll cycle. Returns the new updates (possibly empty)."""
72
+ url = (f"{_API}/bot{token}/getUpdates?"
73
+ f"timeout={_POLL_TIMEOUT_S}&offset={offset}")
74
+ data = _curl_json(url, timeout=_POLL_TIMEOUT_S)
75
+ if not data.get("ok"):
76
+ raise RuntimeError(f"getUpdates: {data.get('description', 'unknown')}")
77
+ return data.get("result", []) or []
78
+
79
+
80
+ # ─────────────────────────────────────────────────────────────────────────────
81
+ # Routing
82
+ # ─────────────────────────────────────────────────────────────────────────────
83
+
84
+ def _project_topic_map(home: Path) -> dict[int, str]:
85
+ """Read the project registry. Returns ``{topic_id: slug}``.
86
+
87
+ A project owns a forum topic; messages in that topic are addressed to the
88
+ project (the missions the user types there run against it).
89
+ """
90
+ registry = home / "Agentik_Coding" / "projects.json"
91
+ if not registry.exists():
92
+ return {}
93
+ try:
94
+ rows = json.loads(registry.read_text() or "[]")
95
+ except json.JSONDecodeError:
96
+ return {}
97
+ out: dict[int, str] = {}
98
+ for row in rows:
99
+ topic = row.get("topic_id")
100
+ slug = row.get("slug")
101
+ if topic is not None and slug:
102
+ out[int(topic)] = str(slug)
103
+ return out
104
+
105
+
106
+ def _extract_message_fields(update: dict[str, Any]) -> tuple[int | None, int | None, str | None]:
107
+ """Pull (topic_id, message_id, text) from an update — None if not relevant."""
108
+ msg = update.get("message") or update.get("edited_message") or {}
109
+ text = msg.get("text")
110
+ if not text:
111
+ return None, None, None
112
+ topic_id = msg.get("message_thread_id")
113
+ message_id = msg.get("message_id")
114
+ return topic_id, message_id, text
115
+
116
+
117
+ def _route_one(
118
+ *,
119
+ topic_id: int,
120
+ text: str,
121
+ supervisor,
122
+ project_map: dict[int, str],
123
+ home: Path,
124
+ telegram_bridge,
125
+ ) -> str:
126
+ """Route one inbound message; return a short tag for the log."""
127
+ # 1. autonomous-agent channel match wins
128
+ fired = supervisor.on_channel_message(topic_id, text)
129
+ if fired:
130
+ return f"autonomous:{','.join(fired)}"
131
+ # 2. project topic → run_mission
132
+ slug = project_map.get(topic_id)
133
+ if slug:
134
+ # we use run_mission directly here (in-process); the engine daemon's
135
+ # HTTP API exists for *cross-process* dispatch, not for the daemon that
136
+ # already lives in the right process.
137
+ from omega_engine.mission import run_mission
138
+ try:
139
+ run_mission(
140
+ intent=text,
141
+ omega_home=home,
142
+ topic_id=topic_id,
143
+ telegram=telegram_bridge,
144
+ )
145
+ except Exception: # noqa: BLE001
146
+ logger.exception("telegram daemon: project mission failed")
147
+ return f"project:{slug}:error"
148
+ return f"project:{slug}"
149
+ return "ignored"
150
+
151
+
152
+ # ─────────────────────────────────────────────────────────────────────────────
153
+ # Main loop
154
+ # ─────────────────────────────────────────────────────────────────────────────
155
+
156
+ def main() -> int:
157
+ logging.basicConfig(
158
+ level=os.environ.get("OMEGA_LOG_LEVEL", "INFO"),
159
+ format="%(asctime)s %(name)s %(levelname)s %(message)s",
160
+ )
161
+ home = _omega_home()
162
+ try:
163
+ token = _read_token(home)
164
+ except RuntimeError as exc:
165
+ logger.error("telegram daemon: %s", exc)
166
+ return 1
167
+
168
+ # the supervisor stays loaded (charters discovered at startup); we DO NOT
169
+ # call .run() — we just use it as a router. Channel-trigger firings will
170
+ # be dispatched via its callable runner (defaults to run_mission).
171
+ supervisor = build_supervisor_from_home(home)
172
+ project_map = _project_topic_map(home)
173
+ logger.info("telegram daemon: %d charter(s) bound, %d project(s) mapped",
174
+ len(supervisor.charters()), len(project_map))
175
+
176
+ # an outbound bridge for project missions (progress edits + PDF delivery)
177
+ telegram_bridge = None
178
+ try:
179
+ from omega_engine.telegram import TelegramBridge
180
+ telegram_bridge = TelegramBridge.from_vault(home)
181
+ except Exception as exc: # noqa: BLE001
182
+ logger.warning("telegram daemon: outbound bridge disabled (%s)", exc)
183
+
184
+ stop = [False]
185
+ signal.signal(signal.SIGTERM, lambda *_: stop.__setitem__(0, True))
186
+ signal.signal(signal.SIGINT, lambda *_: stop.__setitem__(0, True))
187
+
188
+ offset = 0 # only positive update_ids; 0 = "give me everything"
189
+ backoff = 0.0
190
+ while not stop[0]:
191
+ if backoff > 0:
192
+ # use a *short* sleep loop so SIGTERM still wakes us promptly
193
+ slept = 0.0
194
+ while slept < backoff and not stop[0]:
195
+ time.sleep(min(1.0, backoff - slept))
196
+ slept += 1.0
197
+ backoff = 0.0
198
+ if stop[0]:
199
+ break
200
+
201
+ try:
202
+ updates = _get_updates(token, offset)
203
+ except Exception as exc: # noqa: BLE001
204
+ logger.warning("telegram daemon: poll failed (%s) — backing off", exc)
205
+ backoff = _BACKOFF_S
206
+ continue
207
+
208
+ for update in updates:
209
+ offset = max(offset, int(update.get("update_id", 0)) + 1)
210
+ topic_id, _message_id, text = _extract_message_fields(update)
211
+ if topic_id is None or not text:
212
+ continue
213
+ # refresh the project map periodically so newly created projects
214
+ # become routable without restarting the daemon
215
+ project_map = _project_topic_map(home)
216
+ try:
217
+ tag = _route_one(
218
+ topic_id=topic_id, text=text, supervisor=supervisor,
219
+ project_map=project_map, home=home,
220
+ telegram_bridge=telegram_bridge,
221
+ )
222
+ logger.info("telegram in: topic=%s text=%r -> %s",
223
+ topic_id, text[:80], tag)
224
+ except Exception: # noqa: BLE001
225
+ logger.exception("telegram daemon: routing failed")
226
+ logger.info("telegram daemon stopped")
227
+ return 0
228
+
229
+
230
+ if __name__ == "__main__": # pragma: no cover
231
+ raise SystemExit(main())
@@ -0,0 +1,51 @@
1
+ """The educators — the self-improving layer of OmegaOS.
2
+
3
+ Eight generators that produce SSOT artifacts under the same quality gate that
4
+ guards code. The framework lives in ``base``; each domain has its own focused
5
+ module. None of them write to the SSOT directly — every proposal goes through
6
+ ``StagingPipeline``.
7
+
8
+ Usage::
9
+
10
+ from omega_engine.educators import EducatorRegistry, StagingPipeline
11
+ from omega_engine.provider import MockProvider
12
+
13
+ registry = EducatorRegistry.default()
14
+ staging = StagingPipeline("/path/to/Agentik_Extra/staging/promotion")
15
+ proposal = registry.get("prompt").generate(
16
+ intent="oracle dispatches to a fresh worker",
17
+ context={"source": "oracle", "target": "worker"},
18
+ provider=MockProvider(),
19
+ )
20
+ record = staging.stage(proposal)
21
+ """
22
+ from omega_engine.educators.artifact import ArtifactEducator
23
+ from omega_engine.educators.automation import AutomationEducator
24
+ from omega_engine.educators.base import (
25
+ Artifact,
26
+ Educator,
27
+ EducatorProposal,
28
+ EducatorRegistry,
29
+ StagingPipeline,
30
+ StagingRecord,
31
+ build_education_prompt,
32
+ parse_proposal,
33
+ run_education,
34
+ )
35
+ from omega_engine.educators.claudecode import ClaudecodeEducator
36
+ from omega_engine.educators.connection import ConnectionEducator
37
+ from omega_engine.educators.coworker import CoworkerEducator
38
+ from omega_engine.educators.loop import LoopEducator
39
+ from omega_engine.educators.prompt import PromptEducator
40
+ from omega_engine.educators.skill import SkillEducator
41
+
42
+ __all__ = [
43
+ # framework
44
+ "Artifact", "Educator", "EducatorProposal", "EducatorRegistry",
45
+ "StagingPipeline", "StagingRecord",
46
+ "build_education_prompt", "parse_proposal", "run_education",
47
+ # the eight
48
+ "PromptEducator", "ArtifactEducator", "SkillEducator", "CoworkerEducator",
49
+ "ConnectionEducator", "AutomationEducator", "ClaudecodeEducator",
50
+ "LoopEducator",
51
+ ]
@@ -0,0 +1,65 @@
1
+ """`artifact-educator` — generates templates for deliverables.
2
+
3
+ The signature: produces a reusable template for a class of artifacts that
4
+ workers ship at the end of a mission — mission reports, audit reports, doc
5
+ pages, component scaffolds, PR descriptions, ADRs.
6
+
7
+ Target SSOT path: ``Agentik_SSOT/templates/<kind>.md``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from omega_engine.educators.base import EducatorProposal, run_education
14
+ from omega_engine.provider import AgentProvider
15
+
16
+ _INSTRUCTIONS = """\
17
+ Produce ONE artifact TEMPLATE — a reusable scaffold that workers fill in when
18
+ they ship work of this kind. The template MUST:
19
+
20
+ 1. Open with a one-line statement of what THIS template is for.
21
+ 2. Define the required sections (use H2 headings) in fill-in order — every
22
+ section title MUST be the noun-phrase the worker uses ("Summary",
23
+ "Changes", "Tests", "Verify Command", "Screenshots"), never a generic
24
+ "details" or "info".
25
+ 3. Mark required-vs-optional sections explicitly (e.g. "[required]").
26
+ 4. Provide ONE-LINE placeholders in each section, in the form
27
+ `<short prompt to the worker, in imperative voice>` — never lorem ipsum.
28
+ 5. End with a quality checklist (3-7 boxes) the worker self-checks before
29
+ declaring done — the items must be falsifiable, not feel-good.
30
+
31
+ The template MUST NOT:
32
+ - assume the worker has unbounded time or tokens (sections must be terse)
33
+ - omit a verification section (every deliverable shows HOW it was verified)
34
+ - duplicate work that other templates already handle — state the boundary
35
+ if there is overlap.
36
+
37
+ Format the artifact as Markdown.
38
+ """
39
+
40
+
41
+ class ArtifactEducator:
42
+ """Generates artifact templates for worker deliverables."""
43
+
44
+ name = "artifact"
45
+ domain = "deliverable-templates"
46
+
47
+ def generate(
48
+ self,
49
+ intent: str,
50
+ context: dict[str, Any],
51
+ provider: AgentProvider,
52
+ ) -> EducatorProposal:
53
+ kind = str(context.get("artifact_kind", "report"))
54
+ target_path = f"Agentik_SSOT/templates/{kind}.md"
55
+ return run_education(
56
+ educator_name=self.name,
57
+ domain=self.domain,
58
+ intent=intent,
59
+ target_kind="template",
60
+ target_path=target_path,
61
+ domain_instructions=_INSTRUCTIONS,
62
+ context={**context, "artifact_kind": kind},
63
+ provider=provider,
64
+ artifact_id=kind,
65
+ )
@@ -0,0 +1,76 @@
1
+ """`automation-educator` — generates hooks, crons, reactors.
2
+
3
+ The signature: an automation declares a TRIGGER → ACTION binding the supervisor
4
+ runs unattended. Crons (time), hooks (event), reactors (state change). Every
5
+ automation is auditable: it logs to the event store and inherits the deadman.
6
+
7
+ Target SSOT path: ``Agentik_SSOT/automations/<id>.yaml``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from omega_engine.educators.base import EducatorProposal, run_education
14
+ from omega_engine.provider import AgentProvider
15
+
16
+ _INSTRUCTIONS = """\
17
+ Produce ONE automation config in YAML. The config MUST contain these fields:
18
+
19
+ id: <stable identifier, lowercase, kebab-case>
20
+ kind: cron | hook | reactor
21
+ description: <one line>
22
+ trigger:
23
+ # for cron: schedule: "<cron expression>" + timezone: <IANA tz>
24
+ # for hook: event: <event.type from the event store>
25
+ # for reactor: state_from: <FSM state> state_to: <FSM state>
26
+ # kind_filter: <task kind | "*">
27
+ action:
28
+ skill: <skill id, OR>
29
+ command: <shell command, OR>
30
+ dispatch_role: <role from Agentik_SSOT/agents/>
31
+ budget:
32
+ max_iterations: <int>
33
+ wall_timeout_sec: <int>
34
+ cost_cap_usd: <number | null>
35
+ guards:
36
+ - <a pre-condition predicate the supervisor evaluates before firing>
37
+ on_failure: log_only | retry_n | escalate
38
+ observability:
39
+ heartbeat_sec: <int> # missed heartbeat = deadman terminal
40
+ emits: [ <event.type the action emits> ]
41
+
42
+ The automation MUST NOT:
43
+ - fire without a heartbeat (silent automations are a debug nightmare).
44
+ - bypass the audit gate when its action produces artifacts.
45
+ - declare more than one of (skill, command, dispatch_role) — exactly one.
46
+ - omit `on_failure` (default behaviour is too important to leave implicit).
47
+
48
+ Format the artifact as valid YAML — strict.
49
+ """
50
+
51
+
52
+ class AutomationEducator:
53
+ """Generates automation specs (cron / hook / reactor) for the SSOT."""
54
+
55
+ name = "automation"
56
+ domain = "automations"
57
+
58
+ def generate(
59
+ self,
60
+ intent: str,
61
+ context: dict[str, Any],
62
+ provider: AgentProvider,
63
+ ) -> EducatorProposal:
64
+ automation_id = str(context.get("automation_id", "new-automation"))
65
+ target_path = f"Agentik_SSOT/automations/{automation_id}.yaml"
66
+ return run_education(
67
+ educator_name=self.name,
68
+ domain=self.domain,
69
+ intent=intent,
70
+ target_kind="automation",
71
+ target_path=target_path,
72
+ domain_instructions=_INSTRUCTIONS,
73
+ context={**context, "automation_id": automation_id},
74
+ provider=provider,
75
+ artifact_id=automation_id,
76
+ )