@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.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/omega-os.js +48 -0
- package/bootstrap/lib/common.sh +73 -0
- package/bootstrap/lib/steps.sh +153 -0
- package/bootstrap/manifest.example.yaml +45 -0
- package/docs/ACCOUNT-AND-BILLING.md +95 -0
- package/docs/ARCHITECTURE.md +225 -0
- package/docs/AUTONOMOUS-AGENTS.md +128 -0
- package/docs/ENGINE-SPEC.md +174 -0
- package/docs/INSTALL.md +106 -0
- package/docs/MCP-AND-PLUGINS.md +121 -0
- package/docs/RUNTIME-PLAN.md +63 -0
- package/install.sh +54 -0
- package/omega/Agentik_Coding/README.md +21 -0
- package/omega/Agentik_Engine/README.md +58 -0
- package/omega/Agentik_Engine/omega_engine/__init__.py +58 -0
- 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/omega_engine/audit.py +96 -0
- package/omega/Agentik_Engine/omega_engine/audit_arsenal.py +314 -0
- package/omega/Agentik_Engine/omega_engine/barrier.py +45 -0
- package/omega/Agentik_Engine/omega_engine/bus.py +45 -0
- package/omega/Agentik_Engine/omega_engine/cli.py +158 -0
- package/omega/Agentik_Engine/omega_engine/events.py +60 -0
- package/omega/Agentik_Engine/omega_engine/executor.py +167 -0
- package/omega/Agentik_Engine/omega_engine/mission.py +145 -0
- package/omega/Agentik_Engine/omega_engine/progress.py +75 -0
- package/omega/Agentik_Engine/omega_engine/project.py +92 -0
- package/omega/Agentik_Engine/omega_engine/provider.py +139 -0
- package/omega/Agentik_Engine/omega_engine/reducer.py +76 -0
- package/omega/Agentik_Engine/omega_engine/report.py +146 -0
- package/omega/Agentik_Engine/omega_engine/router.py +34 -0
- package/omega/Agentik_Engine/omega_engine/store.py +97 -0
- package/omega/Agentik_Engine/omega_engine/supervisor.py +69 -0
- package/omega/Agentik_Engine/omega_engine/task.py +91 -0
- package/omega/Agentik_Engine/omega_engine/telegram.py +115 -0
- package/omega/Agentik_Engine/pyproject.toml +31 -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
- package/omega/Agentik_Engine/tests/test_audit_arsenal.py +80 -0
- package/omega/Agentik_Engine/tests/test_executor.py +96 -0
- package/omega/Agentik_Engine/tests/test_mission.py +64 -0
- package/omega/Agentik_Engine/tests/test_progress.py +69 -0
- package/omega/Agentik_Engine/tests/test_project.py +61 -0
- package/omega/Agentik_Engine/tests/test_reducer.py +144 -0
- package/omega/Agentik_Engine/tests/test_report.py +88 -0
- package/omega/Agentik_Extra/README.md +37 -0
- package/omega/Agentik_Extra/etc/agentik.env.example +19 -0
- package/omega/Agentik_Extra/etc/structure.yaml +46 -0
- package/omega/Agentik_Orchestration/README.md +43 -0
- package/omega/Agentik_Orchestration/autonomous/README.md +29 -0
- package/omega/Agentik_Orchestration/autonomous/example-agents.yaml +85 -0
- package/omega/Agentik_Orchestration/educators/README.md +55 -0
- package/omega/Agentik_Orchestration/topologies/aisb-oracle-worker.yaml +42 -0
- package/omega/Agentik_Orchestration/verifier/audit-router.yaml +26 -0
- package/omega/Agentik_Providers/README.md +62 -0
- package/omega/Agentik_Providers/claude/accounts.example.yaml +28 -0
- package/omega/Agentik_Providers/registry.yaml +30 -0
- package/omega/Agentik_Runtime/README.md +30 -0
- package/omega/Agentik_SSOT/README.md +36 -0
- package/omega/Agentik_SSOT/VERSION +1 -0
- package/omega/Agentik_SSOT/audits/a11yaudit.yaml +69 -0
- package/omega/Agentik_SSOT/audits/apiaudit.yaml +71 -0
- package/omega/Agentik_SSOT/audits/automationaudit.yaml +77 -0
- package/omega/Agentik_SSOT/audits/codeaudit.yaml +63 -0
- package/omega/Agentik_SSOT/audits/copyaudit.yaml +68 -0
- package/omega/Agentik_SSOT/audits/dataaudit.yaml +76 -0
- package/omega/Agentik_SSOT/audits/debugaudit.yaml +75 -0
- package/omega/Agentik_SSOT/audits/dxaudit.yaml +78 -0
- package/omega/Agentik_SSOT/audits/featureaudit.yaml +73 -0
- package/omega/Agentik_SSOT/audits/flowaudit.yaml +72 -0
- package/omega/Agentik_SSOT/audits/logicaudit.yaml +75 -0
- package/omega/Agentik_SSOT/audits/motionaudit.yaml +67 -0
- package/omega/Agentik_SSOT/audits/perfaudit.yaml +71 -0
- package/omega/Agentik_SSOT/audits/refontaudit.yaml +77 -0
- package/omega/Agentik_SSOT/audits/retentionaudit.yaml +84 -0
- package/omega/Agentik_SSOT/audits/secaudit.yaml +73 -0
- package/omega/Agentik_SSOT/audits/seoaudit.yaml +75 -0
- package/omega/Agentik_SSOT/audits/uiuxaudit.yaml +61 -0
- package/omega/Agentik_SSOT/mcp/mcp-catalog.yaml +136 -0
- package/omega/Agentik_SSOT/rules/constitution.md +44 -0
- package/omega/Agentik_SSOT/schemas/event.schema.json +45 -0
- package/omega/Agentik_SSOT/schemas/task.schema.json +54 -0
- package/omega/Agentik_Tools/README.md +42 -0
- package/omega/Agentik_Tools/registry.json +15 -0
- 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"]
|