@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,167 @@
|
|
|
1
|
+
"""The topology executor — what makes OmegaOS run a mission.
|
|
2
|
+
|
|
3
|
+
It walks a dispatcher/executor tree. A dispatcher calls its provider for a plan
|
|
4
|
+
and recurses into children; a worker runs its provider, then the audit gate
|
|
5
|
+
verifies it. The join barrier governs every dispatcher — it can only complete
|
|
6
|
+
when all children are terminal and none failed.
|
|
7
|
+
|
|
8
|
+
Every state change is an event on the bus. The reducer derives state; the
|
|
9
|
+
executor never writes a state directly.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from omega_engine.audit import AuditGate
|
|
17
|
+
from omega_engine.barrier import ScopeStatus, scope_status
|
|
18
|
+
from omega_engine.bus import EventBus
|
|
19
|
+
from omega_engine.events import Event, EventType
|
|
20
|
+
from omega_engine.provider import AgentRequest
|
|
21
|
+
from omega_engine.reducer import reduce_task
|
|
22
|
+
from omega_engine.router import ModelRouter
|
|
23
|
+
from omega_engine.store import EventStore
|
|
24
|
+
from omega_engine.task import Kind, Task, TaskState
|
|
25
|
+
|
|
26
|
+
#: Roles that are leaf executors. Anything else is a dispatcher.
|
|
27
|
+
EXECUTOR_ROLES = {"worker"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class MissionResult:
|
|
32
|
+
mission_id: str
|
|
33
|
+
intent: str
|
|
34
|
+
root: Task
|
|
35
|
+
tasks: dict[str, Task]
|
|
36
|
+
final_state: TaskState
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def verified(self) -> bool:
|
|
40
|
+
return self.final_state is TaskState.COMPLETED
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Executor:
|
|
44
|
+
"""Runs a mission graph to completion, with verified completion throughout."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
store: EventStore,
|
|
49
|
+
bus: EventBus,
|
|
50
|
+
router: ModelRouter,
|
|
51
|
+
audit: AuditGate,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._store = store
|
|
54
|
+
self._bus = bus
|
|
55
|
+
self._router = router
|
|
56
|
+
self._audit = audit
|
|
57
|
+
self._tasks: dict[str, Task] = {}
|
|
58
|
+
|
|
59
|
+
# -- public ---------------------------------------------------------------
|
|
60
|
+
def run_mission(self, intent: str, root_role: str = "oracle") -> MissionResult:
|
|
61
|
+
"""Run a mission from a natural-language intent. Synchronous, correct."""
|
|
62
|
+
mission_id = "m-" + uuid.uuid4().hex[:8]
|
|
63
|
+
root = self._new_task(
|
|
64
|
+
kind=Kind.DISPATCHER, role=root_role,
|
|
65
|
+
spec={"intent": intent}, parent_id=None, mission_id=mission_id,
|
|
66
|
+
)
|
|
67
|
+
self._run(root, mission_id)
|
|
68
|
+
return MissionResult(
|
|
69
|
+
mission_id=mission_id, intent=intent, root=root,
|
|
70
|
+
tasks=dict(self._tasks), final_state=self._state(root.id),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# -- internals ------------------------------------------------------------
|
|
74
|
+
def _new_task(self, *, kind: Kind, role: str, spec: dict,
|
|
75
|
+
parent_id: str | None, mission_id: str) -> Task:
|
|
76
|
+
prefix = "t-" if kind is Kind.EXECUTOR else "d-"
|
|
77
|
+
tid = prefix + uuid.uuid4().hex[:8]
|
|
78
|
+
task = Task(
|
|
79
|
+
id=tid, kind=kind, role=role,
|
|
80
|
+
spec=dict(spec, mission_id=mission_id), parent_id=parent_id,
|
|
81
|
+
)
|
|
82
|
+
self._tasks[tid] = task
|
|
83
|
+
return task
|
|
84
|
+
|
|
85
|
+
def _emit(self, task_id: str, etype: EventType, payload: dict | None = None) -> None:
|
|
86
|
+
self._bus.publish(Event(task_id=task_id, type=etype, payload=payload or {}))
|
|
87
|
+
|
|
88
|
+
def _state(self, task_id: str) -> TaskState:
|
|
89
|
+
return reduce_task(self._store.events_for(task_id))
|
|
90
|
+
|
|
91
|
+
def _run(self, task: Task, mission_id: str) -> None:
|
|
92
|
+
self._emit(task.id, EventType.CREATED,
|
|
93
|
+
{"role": task.role, "kind": task.kind.value, "mission_id": mission_id})
|
|
94
|
+
self._emit(task.id, EventType.DISPATCHED)
|
|
95
|
+
self._emit(task.id, EventType.STARTED)
|
|
96
|
+
if task.kind is Kind.EXECUTOR:
|
|
97
|
+
self._run_worker(task)
|
|
98
|
+
else:
|
|
99
|
+
self._run_dispatcher(task, mission_id)
|
|
100
|
+
task.state = self._state(task.id)
|
|
101
|
+
|
|
102
|
+
def _run_dispatcher(self, task: Task, mission_id: str) -> None:
|
|
103
|
+
provider = self._router.resolve(task.role)
|
|
104
|
+
prompt = task.spec.get("intent") or task.spec.get("task") or task.role
|
|
105
|
+
result = provider.run(
|
|
106
|
+
AgentRequest(role=task.role, prompt=str(prompt), context=task.spec)
|
|
107
|
+
)
|
|
108
|
+
children: list[Task] = []
|
|
109
|
+
for item in result.plan:
|
|
110
|
+
child_role = item.get("role", "worker")
|
|
111
|
+
child_kind = (
|
|
112
|
+
Kind.EXECUTOR if child_role in EXECUTOR_ROLES else Kind.DISPATCHER
|
|
113
|
+
)
|
|
114
|
+
child = self._new_task(
|
|
115
|
+
kind=child_kind, role=child_role,
|
|
116
|
+
spec=item.get("spec", {}), parent_id=task.id, mission_id=mission_id,
|
|
117
|
+
)
|
|
118
|
+
self._run(child, mission_id)
|
|
119
|
+
children.append(child)
|
|
120
|
+
|
|
121
|
+
status = scope_status([self._state(c.id) for c in children])
|
|
122
|
+
if status is ScopeStatus.JOINABLE:
|
|
123
|
+
# the barrier resolved — the dispatcher may now close
|
|
124
|
+
self._emit(task.id, EventType.SCOPE_JOINABLE, {"children": len(children)})
|
|
125
|
+
self._emit(task.id, EventType.CLAIMED_DONE)
|
|
126
|
+
self._emit(task.id, EventType.VERIFYING)
|
|
127
|
+
self._emit(task.id, EventType.VERIFIED, {"reason": "all children verified"})
|
|
128
|
+
self._emit(task.id, EventType.COMPLETED)
|
|
129
|
+
else:
|
|
130
|
+
# PARTIAL — a child failed. on_partial = fail_up (children already
|
|
131
|
+
# retried within their own budget). Honest partial failure.
|
|
132
|
+
failed = [c.id for c in children
|
|
133
|
+
if self._state(c.id) is TaskState.FAILED]
|
|
134
|
+
self._emit(task.id, EventType.FAILED,
|
|
135
|
+
{"reason": "partial scope", "failed_children": failed})
|
|
136
|
+
|
|
137
|
+
def _run_worker(self, task: Task) -> None:
|
|
138
|
+
provider = self._router.resolve(task.role)
|
|
139
|
+
for attempt in range(task.budget.max_iterations):
|
|
140
|
+
result = provider.run(AgentRequest(
|
|
141
|
+
role=task.role,
|
|
142
|
+
prompt=str(task.spec.get("task", task.role)),
|
|
143
|
+
context=task.spec,
|
|
144
|
+
))
|
|
145
|
+
self._emit(task.id, EventType.CLAIMED_DONE, {"attempt": attempt + 1})
|
|
146
|
+
self._emit(task.id, EventType.VERIFYING)
|
|
147
|
+
verdict = self._audit.verify({
|
|
148
|
+
"artifacts": result.artifacts,
|
|
149
|
+
"runtime_cmd": task.spec.get("verify_cmd"),
|
|
150
|
+
"role": task.role,
|
|
151
|
+
"changed": result.artifacts.get("files", []),
|
|
152
|
+
"path": str(task.spec.get("path", ".")),
|
|
153
|
+
})
|
|
154
|
+
if verdict.verified:
|
|
155
|
+
self._emit(task.id, EventType.VERIFIED, {
|
|
156
|
+
"score": verdict.score,
|
|
157
|
+
"findings": [f.audit for f in verdict.findings],
|
|
158
|
+
})
|
|
159
|
+
self._emit(task.id, EventType.COMPLETED)
|
|
160
|
+
return
|
|
161
|
+
self._emit(task.id, EventType.REJECTED,
|
|
162
|
+
{"score": verdict.score, "attempt": attempt + 1})
|
|
163
|
+
if attempt + 1 < task.budget.max_iterations:
|
|
164
|
+
self._emit(task.id, EventType.DISPATCHED, {"retry": attempt + 2})
|
|
165
|
+
self._emit(task.id, EventType.STARTED)
|
|
166
|
+
# budget exhausted — honest failure, the deadman clause terminates it
|
|
167
|
+
self._emit(task.id, EventType.FAILED, {"reason": "audit budget exhausted"})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Mission coordinator — the entry point that ties the runtime together.
|
|
2
|
+
|
|
3
|
+
`run_mission()` executes a mission, tracks progress live to a Telegram topic
|
|
4
|
+
(a progress bar that updates in place, throttled to respect flood control), and
|
|
5
|
+
ALWAYS produces the Oracle's whitepaper PDF report — delivered into the topic.
|
|
6
|
+
|
|
7
|
+
Telegram is best-effort: a delivery hiccup never fails a mission that ran fine.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from omega_engine.audit import AuditGate
|
|
17
|
+
from omega_engine.bus import EventBus
|
|
18
|
+
from omega_engine.executor import Executor, MissionResult
|
|
19
|
+
from omega_engine.progress import ProgressTracker
|
|
20
|
+
from omega_engine.provider import ClaudeProvider, MockProvider
|
|
21
|
+
from omega_engine.report import generate_mission_report
|
|
22
|
+
from omega_engine.router import ModelRouter
|
|
23
|
+
from omega_engine.store import SQLiteStore
|
|
24
|
+
|
|
25
|
+
#: minimum seconds between live progress edits — Telegram flood control
|
|
26
|
+
_EDIT_THROTTLE_S = 3.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _omega_home(explicit: str | Path | None = None) -> Path:
|
|
30
|
+
return Path(explicit or os.environ.get("OMEGA_HOME", str(Path.home() / "Omega")))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_router() -> ModelRouter:
|
|
34
|
+
"""Real Claude when a key is present, else the deterministic MockProvider —
|
|
35
|
+
so `omega run` always works, with or without credentials."""
|
|
36
|
+
use_claude = (
|
|
37
|
+
os.environ.get("ANTHROPIC_API_KEY")
|
|
38
|
+
and os.environ.get("OMEGA_PROVIDER", "") != "mock"
|
|
39
|
+
)
|
|
40
|
+
if use_claude:
|
|
41
|
+
try:
|
|
42
|
+
import anthropic # noqa: F401
|
|
43
|
+
return ModelRouter.single(ClaudeProvider())
|
|
44
|
+
except ImportError:
|
|
45
|
+
pass
|
|
46
|
+
return ModelRouter.single(MockProvider())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class MissionOutcome:
|
|
51
|
+
result: MissionResult
|
|
52
|
+
report_pdf: Path | None
|
|
53
|
+
progress_pct: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_mission(
|
|
57
|
+
intent: str,
|
|
58
|
+
*,
|
|
59
|
+
omega_home: str | Path | None = None,
|
|
60
|
+
topic_id: int | None = None,
|
|
61
|
+
telegram: object | None = None,
|
|
62
|
+
report_dir: str | Path | None = None,
|
|
63
|
+
) -> MissionOutcome:
|
|
64
|
+
"""Run a mission end-to-end. Always emits the Oracle's PDF report."""
|
|
65
|
+
home = _omega_home(omega_home)
|
|
66
|
+
store = SQLiteStore(home / "Agentik_Runtime" / "eventlog" / "omega.db")
|
|
67
|
+
bus = EventBus(store)
|
|
68
|
+
tracker = ProgressTracker()
|
|
69
|
+
bus.subscribe(tracker.on_event)
|
|
70
|
+
|
|
71
|
+
# live Telegram progress — a message that edits itself as work advances,
|
|
72
|
+
# throttled so a fast mission cannot trip flood control.
|
|
73
|
+
progress_msg_id: int | None = None
|
|
74
|
+
last_edit = [0.0]
|
|
75
|
+
if telegram is not None and topic_id is not None:
|
|
76
|
+
try:
|
|
77
|
+
progress_msg_id = telegram.post( # type: ignore[attr-defined]
|
|
78
|
+
topic_id, "Omega OS - mission starting...")
|
|
79
|
+
except Exception as exc: # noqa: BLE001
|
|
80
|
+
print(f"[mission] telegram post failed: {exc}")
|
|
81
|
+
|
|
82
|
+
def _live(event) -> None:
|
|
83
|
+
if event.type.value not in ("task.verified", "task.failed"):
|
|
84
|
+
return
|
|
85
|
+
if progress_msg_id is None:
|
|
86
|
+
return
|
|
87
|
+
now = time.time()
|
|
88
|
+
if now - last_edit[0] < _EDIT_THROTTLE_S:
|
|
89
|
+
return
|
|
90
|
+
last_edit[0] = now
|
|
91
|
+
missions = tracker.all()
|
|
92
|
+
if not missions:
|
|
93
|
+
return
|
|
94
|
+
try:
|
|
95
|
+
telegram.edit( # type: ignore[attr-defined]
|
|
96
|
+
progress_msg_id,
|
|
97
|
+
missions[0].render(title="Omega OS - mission in progress"))
|
|
98
|
+
except Exception: # noqa: BLE001 — progress is best-effort
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
bus.subscribe(_live)
|
|
102
|
+
|
|
103
|
+
# the verification gate: the full Quality Arsenal when audit definitions are
|
|
104
|
+
# deployed (Agentik_SSOT/audits/), else the simple runtime gate.
|
|
105
|
+
router = build_router()
|
|
106
|
+
audits_dir = home / "Agentik_SSOT" / "audits"
|
|
107
|
+
if audits_dir.is_dir() and any(audits_dir.glob("*.yaml")):
|
|
108
|
+
from omega_engine.audit_arsenal import ArsenalGate, AuditRegistry
|
|
109
|
+
gate: object = ArsenalGate(AuditRegistry.load(audits_dir), router)
|
|
110
|
+
else:
|
|
111
|
+
gate = AuditGate()
|
|
112
|
+
executor = Executor(store, bus, router, gate)
|
|
113
|
+
result = executor.run_mission(intent)
|
|
114
|
+
|
|
115
|
+
# the Oracle ALWAYS produces a report
|
|
116
|
+
rdir = Path(report_dir or (home / "Agentik_Runtime" / "verdicts"))
|
|
117
|
+
report_pdf: Path | None
|
|
118
|
+
try:
|
|
119
|
+
report_pdf = generate_mission_report(result, store, rdir)
|
|
120
|
+
except Exception as exc: # noqa: BLE001
|
|
121
|
+
report_pdf = None
|
|
122
|
+
print(f"[mission] report generation failed: {exc}")
|
|
123
|
+
|
|
124
|
+
prog = tracker.progress(result.mission_id)
|
|
125
|
+
pct = prog.pct if prog else 0
|
|
126
|
+
|
|
127
|
+
if telegram is not None and topic_id is not None:
|
|
128
|
+
verdict = "VERIFIED" if result.verified else "PARTIAL / FAILED"
|
|
129
|
+
if progress_msg_id is not None and prog is not None:
|
|
130
|
+
try:
|
|
131
|
+
telegram.edit( # type: ignore[attr-defined]
|
|
132
|
+
progress_msg_id,
|
|
133
|
+
prog.render(title=f"{verdict} - mission complete"))
|
|
134
|
+
except Exception as exc: # noqa: BLE001
|
|
135
|
+
print(f"[mission] final progress edit failed: {exc}")
|
|
136
|
+
if report_pdf is not None:
|
|
137
|
+
try:
|
|
138
|
+
telegram.send_document( # type: ignore[attr-defined]
|
|
139
|
+
topic_id, report_pdf,
|
|
140
|
+
caption=f"{verdict} - mission report - {intent[:80]}")
|
|
141
|
+
except Exception as exc: # noqa: BLE001
|
|
142
|
+
print(f"[mission] report delivery failed (report is on disk): {exc}")
|
|
143
|
+
|
|
144
|
+
store.close()
|
|
145
|
+
return MissionOutcome(result=result, report_pdf=report_pdf, progress_pct=pct)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Live progress tracking — turn the event stream into a progress bar.
|
|
2
|
+
|
|
3
|
+
`ProgressTracker` is an EventBus subscriber. It keeps one `MissionProgress` per
|
|
4
|
+
mission, derived purely from events — so progress, like completion, is never
|
|
5
|
+
self-declared.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from omega_engine.events import Event, EventType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class MissionProgress:
|
|
16
|
+
mission_id: str
|
|
17
|
+
total_tasks: int = 0
|
|
18
|
+
verified: int = 0
|
|
19
|
+
failed: int = 0
|
|
20
|
+
last_event: str = ""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def done(self) -> int:
|
|
24
|
+
return self.verified + self.failed
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def pct(self) -> int:
|
|
28
|
+
return round(100 * self.done / self.total_tasks) if self.total_tasks else 0
|
|
29
|
+
|
|
30
|
+
def bar(self, width: int = 20) -> str:
|
|
31
|
+
filled = round(width * self.pct / 100)
|
|
32
|
+
return "█" * filled + "░" * (width - filled)
|
|
33
|
+
|
|
34
|
+
def render(self, title: str = "") -> str:
|
|
35
|
+
"""A Telegram-ready progress block."""
|
|
36
|
+
head = f"{title}\n" if title else ""
|
|
37
|
+
return (
|
|
38
|
+
f"{head}{self.bar()} {self.pct}%\n"
|
|
39
|
+
f"{self.verified} verified · {self.failed} failed "
|
|
40
|
+
f"· {self.total_tasks} tasks\n"
|
|
41
|
+
f"last: {self.last_event}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProgressTracker:
|
|
46
|
+
"""Subscribe `.on_event` to an EventBus; read `.progress(mission_id)`."""
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
self._missions: dict[str, MissionProgress] = {}
|
|
50
|
+
self._task_mission: dict[str, str] = {}
|
|
51
|
+
|
|
52
|
+
def on_event(self, event: Event) -> None:
|
|
53
|
+
if event.type is EventType.CREATED:
|
|
54
|
+
mid = str(event.payload.get("mission_id", ""))
|
|
55
|
+
if mid:
|
|
56
|
+
self._task_mission[event.task_id] = mid
|
|
57
|
+
prog = self._missions.setdefault(mid, MissionProgress(mid))
|
|
58
|
+
prog.total_tasks += 1
|
|
59
|
+
|
|
60
|
+
mid = self._task_mission.get(event.task_id)
|
|
61
|
+
if not mid:
|
|
62
|
+
return
|
|
63
|
+
prog = self._missions[mid]
|
|
64
|
+
prog.last_event = f"{event.type.value} {event.task_id}"
|
|
65
|
+
if event.type is EventType.VERIFIED:
|
|
66
|
+
prog.verified += 1
|
|
67
|
+
elif event.type is EventType.FAILED:
|
|
68
|
+
prog.failed += 1
|
|
69
|
+
|
|
70
|
+
def progress(self, mission_id: str) -> MissionProgress | None:
|
|
71
|
+
return self._missions.get(mission_id)
|
|
72
|
+
|
|
73
|
+
def all(self) -> list[MissionProgress]:
|
|
74
|
+
"""Every mission's progress — used by the live Telegram reporter."""
|
|
75
|
+
return list(self._missions.values())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Project creation — structure, registry entry, and a bound Telegram topic.
|
|
2
|
+
|
|
3
|
+
Creating a project builds its folder under Agentik_Coding/projects/, registers
|
|
4
|
+
it in the canonical project registry (the project<->topic map that routes
|
|
5
|
+
Telegram messages), and — if a Telegram bridge is given — creates its forum
|
|
6
|
+
topic.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def slugify(name: str) -> str:
|
|
19
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
20
|
+
return slug or "project"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Project:
|
|
25
|
+
name: str
|
|
26
|
+
slug: str
|
|
27
|
+
path: str
|
|
28
|
+
topic_id: int | None = None
|
|
29
|
+
created_at: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _omega_home(explicit: str | Path | None = None) -> Path:
|
|
33
|
+
return Path(explicit or os.environ.get("OMEGA_HOME", str(Path.home() / "Omega")))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _registry_file(home: Path) -> Path:
|
|
37
|
+
return home / "Agentik_Coding" / "projects.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_projects(omega_home: str | Path | None = None) -> list[Project]:
|
|
41
|
+
"""The canonical project registry — the project <-> Telegram-topic map."""
|
|
42
|
+
fpath = _registry_file(_omega_home(omega_home))
|
|
43
|
+
if not fpath_exists(fpath):
|
|
44
|
+
return []
|
|
45
|
+
rows = json.loads(fpath.read_text() or "[]")
|
|
46
|
+
return [Project(**row) for row in rows]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fpath_exists(p: Path) -> bool:
|
|
50
|
+
return p.exists()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _save(home: Path, projects: list[Project]) -> None:
|
|
54
|
+
fpath = _registry_file(home)
|
|
55
|
+
fpath.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
fpath.write_text(
|
|
57
|
+
json.dumps([asdict(p) for p in projects], indent=2, ensure_ascii=False)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_project(name: str, omega_home: str | Path | None = None,
|
|
62
|
+
telegram: object | None = None) -> Project:
|
|
63
|
+
"""Create a project: folder + README + registry entry + Telegram topic.
|
|
64
|
+
|
|
65
|
+
`telegram` is an optional TelegramBridge — when given, a forum topic is
|
|
66
|
+
created and bound to the project.
|
|
67
|
+
"""
|
|
68
|
+
home = _omega_home(omega_home)
|
|
69
|
+
slug = slugify(name)
|
|
70
|
+
projects = load_projects(home)
|
|
71
|
+
if any(p.slug == slug for p in projects):
|
|
72
|
+
raise ValueError(f"project '{slug}' already exists")
|
|
73
|
+
|
|
74
|
+
path = home / "Agentik_Coding" / "projects" / slug
|
|
75
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
(path / "README.md").write_text(
|
|
77
|
+
f"# {name}\n\n"
|
|
78
|
+
f"Project managed by Omega OS. Missions addressed to this project "
|
|
79
|
+
f"route to its bound Telegram topic.\n"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
topic_id: int | None = None
|
|
83
|
+
if telegram is not None:
|
|
84
|
+
topic_id = telegram.create_topic(name) # type: ignore[attr-defined]
|
|
85
|
+
|
|
86
|
+
project = Project(
|
|
87
|
+
name=name, slug=slug, path=str(path), topic_id=topic_id,
|
|
88
|
+
created_at=time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
89
|
+
)
|
|
90
|
+
projects.append(project)
|
|
91
|
+
_save(home, projects)
|
|
92
|
+
return project
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""The provider abstraction — one contract, any LLM.
|
|
2
|
+
|
|
3
|
+
The executor talks only to `AgentProvider`. `MockProvider` makes the whole
|
|
4
|
+
orchestration testable with no network. `ClaudeProvider` is the real adapter.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AgentRequest:
|
|
16
|
+
role: str # "aisb" | "oracle" | "worker" | ...
|
|
17
|
+
prompt: str
|
|
18
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class AgentResult:
|
|
23
|
+
text: str
|
|
24
|
+
claimed_done: bool = False
|
|
25
|
+
artifacts: dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
plan: list[dict[str, Any]] = field(default_factory=list) # dispatcher roles only
|
|
27
|
+
usage: dict[str, int] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class AgentProvider(Protocol):
|
|
32
|
+
"""Every LLM sits behind this. ~200-400 lines per real adapter."""
|
|
33
|
+
|
|
34
|
+
id: str
|
|
35
|
+
|
|
36
|
+
def run(self, req: AgentRequest) -> AgentResult: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MockProvider:
|
|
40
|
+
"""Deterministic provider — makes the executor fully testable with no API.
|
|
41
|
+
|
|
42
|
+
Dispatcher roles (oracle/manager) return a `plan`; workers return artifacts
|
|
43
|
+
and `claimed_done`; the verifier returns a passing score.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
id = "mock"
|
|
47
|
+
|
|
48
|
+
def __init__(self, plan_size: int = 2) -> None:
|
|
49
|
+
self._plan_size = plan_size
|
|
50
|
+
|
|
51
|
+
def run(self, req: AgentRequest) -> AgentResult:
|
|
52
|
+
role = req.role
|
|
53
|
+
if role in ("oracle", "manager", "aisb"):
|
|
54
|
+
plan = [
|
|
55
|
+
{"role": "worker",
|
|
56
|
+
"spec": {"task": f"subtask {i + 1}", "verify_cmd": "true"}}
|
|
57
|
+
for i in range(self._plan_size)
|
|
58
|
+
]
|
|
59
|
+
return AgentResult(text=f"planned {len(plan)} subtasks",
|
|
60
|
+
claimed_done=True, plan=plan)
|
|
61
|
+
if role == "worker":
|
|
62
|
+
task = req.context.get("task", "task")
|
|
63
|
+
return AgentResult(
|
|
64
|
+
text=f"completed: {task}",
|
|
65
|
+
claimed_done=True,
|
|
66
|
+
artifacts={"summary": f"completed: {task}", "files": ["mock_change.py"]},
|
|
67
|
+
usage={"input_tokens": 100, "output_tokens": 200},
|
|
68
|
+
)
|
|
69
|
+
if role in ("verifier", "audit"):
|
|
70
|
+
# a structured forensic verdict — what the Quality Arsenal expects
|
|
71
|
+
return AgentResult(
|
|
72
|
+
text="audit pass", claimed_done=True,
|
|
73
|
+
artifacts={"verdict": {
|
|
74
|
+
"score": 95, "verified": True, "confidence": "high",
|
|
75
|
+
"summary": "mock forensic audit — no falsifiable claims found",
|
|
76
|
+
"findings": [], "fix_plan": [],
|
|
77
|
+
}})
|
|
78
|
+
return AgentResult(text="ok", claimed_done=True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ClaudeProvider:
|
|
82
|
+
"""Real provider — wraps the Anthropic SDK.
|
|
83
|
+
|
|
84
|
+
Needs `ANTHROPIC_API_KEY` in the environment (or passed) and the `anthropic`
|
|
85
|
+
package: `pip install omega-engine[providers]`. The credential resolves
|
|
86
|
+
through the Account pool (Agentik_Providers/claude/accounts.yaml).
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
id = "claude"
|
|
90
|
+
|
|
91
|
+
def __init__(self, model: str = "claude-opus-4-7", api_key: str | None = None) -> None:
|
|
92
|
+
self._model = model
|
|
93
|
+
self._api_key = api_key
|
|
94
|
+
|
|
95
|
+
def run(self, req: AgentRequest) -> AgentResult:
|
|
96
|
+
try:
|
|
97
|
+
import anthropic
|
|
98
|
+
except ImportError as exc: # pragma: no cover - depends on optional extra
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
"ClaudeProvider needs the `anthropic` package — "
|
|
101
|
+
"pip install omega-engine[providers]"
|
|
102
|
+
) from exc
|
|
103
|
+
|
|
104
|
+
client = anthropic.Anthropic(api_key=self._api_key) # reads env if None
|
|
105
|
+
prompt = req.prompt
|
|
106
|
+
if req.role in ("oracle", "manager", "aisb"):
|
|
107
|
+
prompt += (
|
|
108
|
+
"\n\nRespond with a JSON array of subtasks, each "
|
|
109
|
+
'{"role":"worker","spec":{"task":"..."}}. JSON only.'
|
|
110
|
+
)
|
|
111
|
+
msg = client.messages.create(
|
|
112
|
+
model=self._model,
|
|
113
|
+
max_tokens=4096,
|
|
114
|
+
messages=[{"role": "user", "content": prompt}],
|
|
115
|
+
)
|
|
116
|
+
text = "".join(
|
|
117
|
+
b.text for b in msg.content if getattr(b, "type", "") == "text"
|
|
118
|
+
)
|
|
119
|
+
return AgentResult(
|
|
120
|
+
text=text,
|
|
121
|
+
claimed_done=True,
|
|
122
|
+
plan=_extract_plan(text) if req.role in ("oracle", "manager", "aisb") else [],
|
|
123
|
+
usage={
|
|
124
|
+
"input_tokens": msg.usage.input_tokens,
|
|
125
|
+
"output_tokens": msg.usage.output_tokens,
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _extract_plan(text: str) -> list[dict[str, Any]]:
|
|
131
|
+
"""Pull a JSON subtask array out of an LLM response. Empty list if none."""
|
|
132
|
+
match = re.search(r"\[.*\]", text, re.DOTALL)
|
|
133
|
+
if not match:
|
|
134
|
+
return []
|
|
135
|
+
try:
|
|
136
|
+
data = json.loads(match.group(0))
|
|
137
|
+
return data if isinstance(data, list) else []
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
return []
|