@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,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
|
+
)
|