@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,110 @@
|
|
|
1
|
+
"""RAGRouter — classifies a query and picks the right inner strategy, then
|
|
2
|
+
wraps the result in the Corrective envelope.
|
|
3
|
+
|
|
4
|
+
Routing happens in two steps:
|
|
5
|
+
1. A cheap heuristic ("does the query look relational? mention an image
|
|
6
|
+
or PDF? have a 'why/how' shape?") gives a default.
|
|
7
|
+
2. The provider (role "rag-route") gets a chance to override; its choice
|
|
8
|
+
wins when it picks one of the registered strategies.
|
|
9
|
+
|
|
10
|
+
The Corrective envelope wraps every dispatch — that's the project's stance:
|
|
11
|
+
"check / recheck until 100%" applied to retrieval (ARCHITECTURE.md §7).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
|
|
18
|
+
from omega_engine.provider import AgentProvider, AgentRequest
|
|
19
|
+
from omega_engine.rag.base import RetrievalResult, Retriever
|
|
20
|
+
from omega_engine.rag.corrective import CorrectiveRetriever
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_GRAPH_HINTS = re.compile(
|
|
24
|
+
r"\b(depend|related|connection|who|graph|owner|"
|
|
25
|
+
r"between|link|relation|adjacent|connected)\b",
|
|
26
|
+
re.IGNORECASE,
|
|
27
|
+
)
|
|
28
|
+
_MULTIMODAL_HINTS = re.compile(
|
|
29
|
+
r"\b(image|picture|screenshot|diagram|pdf|figure|chart|photo)\b",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
_AGENTIC_HINTS = re.compile(
|
|
33
|
+
r"\b(why|how|explain|compare|trace|step by step|deep dive)\b",
|
|
34
|
+
re.IGNORECASE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RAGRouter:
|
|
39
|
+
"""Routes a query to one of the registered strategies, then wraps in CRAG."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
strategies: dict[str, Retriever],
|
|
44
|
+
provider: AgentProvider,
|
|
45
|
+
*,
|
|
46
|
+
default: str = "hybrid",
|
|
47
|
+
corrective: bool = True,
|
|
48
|
+
threshold: float = 70.0,
|
|
49
|
+
max_retries: int = 2,
|
|
50
|
+
) -> None:
|
|
51
|
+
if not strategies:
|
|
52
|
+
raise ValueError("RAGRouter needs at least one strategy")
|
|
53
|
+
self.strategies = dict(strategies)
|
|
54
|
+
self.provider = provider
|
|
55
|
+
self.default = default if default in self.strategies else next(iter(self.strategies))
|
|
56
|
+
self.corrective = corrective
|
|
57
|
+
self.threshold = threshold
|
|
58
|
+
self.max_retries = max_retries
|
|
59
|
+
|
|
60
|
+
def classify(self, query: str) -> str:
|
|
61
|
+
"""Pick a strategy name. Heuristic first, provider may override."""
|
|
62
|
+
# 1. Cheap heuristic — works with no provider.
|
|
63
|
+
heuristic = self.default
|
|
64
|
+
if "multimodal" in self.strategies and _MULTIMODAL_HINTS.search(query):
|
|
65
|
+
heuristic = "multimodal"
|
|
66
|
+
elif "graph" in self.strategies and _GRAPH_HINTS.search(query):
|
|
67
|
+
heuristic = "graph"
|
|
68
|
+
elif "agentic" in self.strategies and _AGENTIC_HINTS.search(query):
|
|
69
|
+
heuristic = "agentic"
|
|
70
|
+
|
|
71
|
+
# 2. Provider override. If it returns a non-registered strategy, we
|
|
72
|
+
# ignore it (never trust a free-form provider blindly).
|
|
73
|
+
try:
|
|
74
|
+
result = self.provider.run(AgentRequest(
|
|
75
|
+
role="rag-route",
|
|
76
|
+
prompt=f"Classify retrieval strategy for: {query}",
|
|
77
|
+
context={
|
|
78
|
+
"query": query,
|
|
79
|
+
"available": sorted(self.strategies.keys()),
|
|
80
|
+
"heuristic": heuristic,
|
|
81
|
+
},
|
|
82
|
+
))
|
|
83
|
+
chosen = (result.artifacts or {}).get("strategy")
|
|
84
|
+
if isinstance(chosen, str) and chosen in self.strategies:
|
|
85
|
+
return chosen
|
|
86
|
+
except Exception:
|
|
87
|
+
# Provider misbehaved — keep the heuristic, never crash retrieval.
|
|
88
|
+
pass
|
|
89
|
+
return heuristic
|
|
90
|
+
|
|
91
|
+
def retrieve(self, query: str, k: int = 5) -> RetrievalResult:
|
|
92
|
+
name = self.classify(query)
|
|
93
|
+
inner = self.strategies[name]
|
|
94
|
+
if self.corrective:
|
|
95
|
+
wrapped = CorrectiveRetriever(
|
|
96
|
+
inner,
|
|
97
|
+
self.provider,
|
|
98
|
+
threshold=self.threshold,
|
|
99
|
+
max_retries=self.max_retries,
|
|
100
|
+
)
|
|
101
|
+
result = wrapped.retrieve(query, k=k)
|
|
102
|
+
# Surface the chosen inner strategy in the result for observability.
|
|
103
|
+
result.strategy = f"router({name})->{result.strategy}"
|
|
104
|
+
return result
|
|
105
|
+
result = inner.retrieve(query, k=k)
|
|
106
|
+
result.strategy = f"router({name})->{result.strategy}"
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
def names(self) -> Iterable[str]:
|
|
110
|
+
return self.strategies.keys()
|
|
@@ -63,14 +63,32 @@ def reduce(state: Optional[TaskState], event: Event) -> TaskState:
|
|
|
63
63
|
return _TRANSITIONS[key]
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def reduce_task(events: Iterable[Event]
|
|
66
|
+
def reduce_task(events: Iterable[Event],
|
|
67
|
+
initial: Optional[TaskState] = None) -> TaskState:
|
|
67
68
|
"""Fold an ordered event stream into the current task state.
|
|
68
69
|
|
|
69
|
-
The events MUST be ordered (the Event Store returns them by
|
|
70
|
+
The events MUST be ordered (the Event Store returns them by rowid).
|
|
71
|
+
`initial` lets a snapshot-aware caller start the fold from a known state
|
|
72
|
+
(`store.events_since_snapshot` + `snap.state`) instead of from genesis.
|
|
70
73
|
"""
|
|
71
|
-
state: Optional[TaskState] =
|
|
74
|
+
state: Optional[TaskState] = initial
|
|
72
75
|
for ev in events:
|
|
73
76
|
state = reduce(state, ev)
|
|
74
77
|
if state is None:
|
|
75
78
|
raise IllegalTransition("empty event stream — task never created")
|
|
76
79
|
return state
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def reduce_task_fast(store, task_id: str) -> TaskState:
|
|
83
|
+
"""Reduce a task using its latest snapshot if available, else full replay.
|
|
84
|
+
|
|
85
|
+
Bounded reduction time on a long event log. Correctness is identical to
|
|
86
|
+
`reduce_task(store.events_for(task_id))`.
|
|
87
|
+
"""
|
|
88
|
+
snap = store.latest_snapshot(task_id)
|
|
89
|
+
if snap is None:
|
|
90
|
+
return reduce_task(store.events_for(task_id))
|
|
91
|
+
events_after = store.events_since_snapshot(task_id)
|
|
92
|
+
if not events_after:
|
|
93
|
+
return snap["state"]
|
|
94
|
+
return reduce_task(events_after, initial=snap["state"])
|
|
@@ -47,6 +47,18 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
47
47
|
);
|
|
48
48
|
CREATE INDEX IF NOT EXISTS idx_events_task ON events(task_id, ts, id);
|
|
49
49
|
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts, id);
|
|
50
|
+
|
|
51
|
+
-- Snapshots: bounded reduction time on a growing log. A snapshot captures the
|
|
52
|
+
-- reduced state of a task at a given event rowid; later reductions can start
|
|
53
|
+
-- from the snapshot instead of replaying from genesis.
|
|
54
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
55
|
+
task_id TEXT NOT NULL,
|
|
56
|
+
state TEXT NOT NULL,
|
|
57
|
+
ts REAL NOT NULL,
|
|
58
|
+
last_event_rowid INTEGER NOT NULL,
|
|
59
|
+
PRIMARY KEY (task_id, ts)
|
|
60
|
+
);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_task ON snapshots(task_id, ts DESC);
|
|
50
62
|
"""
|
|
51
63
|
|
|
52
64
|
|
|
@@ -88,10 +100,58 @@ class SQLiteStore(EventStore):
|
|
|
88
100
|
cur = self._conn.execute("SELECT DISTINCT task_id FROM events")
|
|
89
101
|
return [r["task_id"] for r in cur.fetchall()]
|
|
90
102
|
|
|
103
|
+
# --- snapshots --------------------------------------------------------
|
|
104
|
+
# Snapshots are an optional optimisation. Correctness depends only on
|
|
105
|
+
# events; snapshots merely bound reduction time as the log grows.
|
|
106
|
+
|
|
107
|
+
def snapshot(self, task_id: str) -> dict | None:
|
|
108
|
+
"""Capture the current reduced state of a task at the latest event.
|
|
109
|
+
Returns the snapshot row, or None if the task has no events."""
|
|
110
|
+
cur = self._conn.execute(
|
|
111
|
+
"SELECT rowid FROM events WHERE task_id = ? ORDER BY rowid DESC LIMIT 1",
|
|
112
|
+
(task_id,),
|
|
113
|
+
)
|
|
114
|
+
row = cur.fetchone()
|
|
115
|
+
if not row:
|
|
116
|
+
return None
|
|
117
|
+
last_rowid = row[0]
|
|
118
|
+
# Local import to keep `store` importable without `reducer`-time deps.
|
|
119
|
+
from omega_engine.reducer import reduce_task
|
|
120
|
+
events = self.events_for(task_id)
|
|
121
|
+
state = reduce_task(events)
|
|
122
|
+
ts = events[-1].ts
|
|
123
|
+
self._conn.execute(
|
|
124
|
+
"INSERT OR REPLACE INTO snapshots (task_id, state, ts, last_event_rowid) "
|
|
125
|
+
"VALUES (?, ?, ?, ?)",
|
|
126
|
+
(task_id, state.value, ts, last_rowid),
|
|
127
|
+
)
|
|
128
|
+
return {"task_id": task_id, "state": state, "ts": ts,
|
|
129
|
+
"last_event_rowid": last_rowid}
|
|
130
|
+
|
|
131
|
+
def latest_snapshot(self, task_id: str) -> dict | None:
|
|
132
|
+
"""Latest snapshot for a task, or None."""
|
|
133
|
+
cur = self._conn.execute(
|
|
134
|
+
"SELECT state, ts, last_event_rowid FROM snapshots "
|
|
135
|
+
"WHERE task_id = ? ORDER BY ts DESC LIMIT 1",
|
|
136
|
+
(task_id,),
|
|
137
|
+
)
|
|
138
|
+
row = cur.fetchone()
|
|
139
|
+
if not row:
|
|
140
|
+
return None
|
|
141
|
+
from omega_engine.task import TaskState
|
|
142
|
+
return {"state": TaskState(row["state"]), "ts": row["ts"],
|
|
143
|
+
"last_event_rowid": row["last_event_rowid"]}
|
|
144
|
+
|
|
145
|
+
def events_since_snapshot(self, task_id: str) -> list[Event]:
|
|
146
|
+
"""Events appended since the latest snapshot, or all events if none."""
|
|
147
|
+
snap = self.latest_snapshot(task_id)
|
|
148
|
+
if not snap:
|
|
149
|
+
return self.events_for(task_id)
|
|
150
|
+
cur = self._conn.execute(
|
|
151
|
+
"SELECT * FROM events WHERE task_id = ? AND rowid > ? ORDER BY rowid",
|
|
152
|
+
(task_id, snap["last_event_rowid"]),
|
|
153
|
+
)
|
|
154
|
+
return [Event.from_row(dict(r)) for r in cur.fetchall()]
|
|
155
|
+
|
|
91
156
|
def close(self) -> None:
|
|
92
157
|
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,304 @@
|
|
|
1
|
+
"""SST → provider projection — the `omega sync` engine.
|
|
2
|
+
|
|
3
|
+
The SSOT is provider-neutral. Each provider adapter compiles
|
|
4
|
+
`Agentik_SSOT/` into that provider's native shape. Re-running `omega sync` is
|
|
5
|
+
idempotent: same SSOT in, same projection out — so the SSOT remains the only
|
|
6
|
+
hand-edited surface.
|
|
7
|
+
|
|
8
|
+
Today only the Claude Code adapter is fully wired. GLM / OpenAI / DeepSeek
|
|
9
|
+
providers consume the SSOT directly via API calls and have no on-disk native
|
|
10
|
+
layout, so their adapters log a no-op message. New providers add one adapter
|
|
11
|
+
file; the SSOT does not change.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Iterable, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _omega_home(explicit: str | Path | None = None) -> Path:
|
|
22
|
+
return Path(explicit or os.environ.get("OMEGA_HOME", str(Path.home() / "Omega")))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------------------------------
|
|
26
|
+
# Provider adapter contract
|
|
27
|
+
# --------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class ProviderAdapter(Protocol):
|
|
32
|
+
"""Each provider exposes a `project(omega_home)` that writes its native layout."""
|
|
33
|
+
|
|
34
|
+
id: str
|
|
35
|
+
|
|
36
|
+
def project(self, omega_home: Path) -> dict[str, Any]: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _StubAdapter:
|
|
40
|
+
"""Adapter that has no on-disk native layout — logs and returns metadata."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, provider_id: str, reason: str) -> None:
|
|
43
|
+
self.id = provider_id
|
|
44
|
+
self._reason = reason
|
|
45
|
+
|
|
46
|
+
def project(self, omega_home: Path) -> dict[str, Any]:
|
|
47
|
+
msg = f"[sync] {self.id}: {self._reason}"
|
|
48
|
+
print(msg)
|
|
49
|
+
return {"provider": self.id, "projected": False, "reason": self._reason}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --------------------------------------------------------------------------
|
|
53
|
+
# Claude Code adapter — the only fully wired one for now
|
|
54
|
+
# --------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ClaudeCodeAdapter:
|
|
58
|
+
"""Project the SSOT into Claude Code's native `.claude/` layout.
|
|
59
|
+
|
|
60
|
+
Native target tree (idempotent):
|
|
61
|
+
<target>/skills/<id>/SKILL.md
|
|
62
|
+
<target>/commands/<id>.md
|
|
63
|
+
<target>/agents/<id>.md
|
|
64
|
+
<target>/.mcp.json { "mcpServers": { ... } }
|
|
65
|
+
<target>/settings.json { ..., "hooks": {...} }
|
|
66
|
+
|
|
67
|
+
The SSOT is never modified — the adapter is one-way (SSOT → projection).
|
|
68
|
+
Default target: `<omega_home>/Agentik_AI/providers/claude-code/.claude/`.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
id = "claude-code"
|
|
72
|
+
|
|
73
|
+
def __init__(self, target: str | Path | None = None) -> None:
|
|
74
|
+
self._explicit_target = Path(target) if target else None
|
|
75
|
+
|
|
76
|
+
# ----- helpers -------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def target_dir(self, omega_home: Path) -> Path:
|
|
79
|
+
if self._explicit_target is not None:
|
|
80
|
+
return self._explicit_target
|
|
81
|
+
return omega_home / "Agentik_AI" / "providers" / "claude-code" / ".claude"
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _ensure(d: Path) -> Path:
|
|
85
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
return d
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _stem(path: Path) -> str:
|
|
90
|
+
return path.stem
|
|
91
|
+
|
|
92
|
+
# ----- skills / commands / agents (markdown surfaces) ----------------
|
|
93
|
+
|
|
94
|
+
def _project_markdown_dir(
|
|
95
|
+
self,
|
|
96
|
+
src: Path,
|
|
97
|
+
out_root: Path,
|
|
98
|
+
layout: str, # "skill" | "command" | "agent"
|
|
99
|
+
) -> list[str]:
|
|
100
|
+
"""Copy `src/*.md` to the native layout. Returns list of written paths."""
|
|
101
|
+
if not src.is_dir():
|
|
102
|
+
return []
|
|
103
|
+
out_root = self._ensure(out_root)
|
|
104
|
+
written: list[str] = []
|
|
105
|
+
for md in sorted(src.glob("*.md")):
|
|
106
|
+
stem = self._stem(md)
|
|
107
|
+
if layout == "skill":
|
|
108
|
+
dest_dir = self._ensure(out_root / stem)
|
|
109
|
+
dest = dest_dir / "SKILL.md"
|
|
110
|
+
else:
|
|
111
|
+
dest = out_root / f"{stem}.md"
|
|
112
|
+
dest.write_text(md.read_text())
|
|
113
|
+
written.append(str(dest.relative_to(out_root.parent)))
|
|
114
|
+
return written
|
|
115
|
+
|
|
116
|
+
# ----- MCP config (.mcp.json) ---------------------------------------
|
|
117
|
+
|
|
118
|
+
def _project_mcp(self, omega_home: Path, out_root: Path) -> dict[str, Any]:
|
|
119
|
+
"""Compile mcp-config.yaml + the tool registry → Claude Code .mcp.json.
|
|
120
|
+
|
|
121
|
+
Claude Code expects: { "mcpServers": { "<id>": { "command": "...",
|
|
122
|
+
"args": [...],
|
|
123
|
+
"env": {...} } } }
|
|
124
|
+
We resolve `command` from the tool registry invoke path when available.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
import yaml
|
|
128
|
+
except ImportError:
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
cfg_path = omega_home / "Agentik_SSOT" / "mcp" / "mcp-config.yaml"
|
|
132
|
+
catalog_path = omega_home / "Agentik_SSOT" / "mcp" / "mcp-catalog.yaml"
|
|
133
|
+
|
|
134
|
+
cfg: dict[str, Any] = {}
|
|
135
|
+
if cfg_path.exists():
|
|
136
|
+
cfg = yaml.safe_load(cfg_path.read_text()) or {}
|
|
137
|
+
|
|
138
|
+
catalog_by_id: dict[str, dict[str, Any]] = {}
|
|
139
|
+
if catalog_path.exists():
|
|
140
|
+
catalog = yaml.safe_load(catalog_path.read_text()) or {}
|
|
141
|
+
for e in catalog.get("catalog") or []:
|
|
142
|
+
catalog_by_id[str(e.get("id"))] = e
|
|
143
|
+
|
|
144
|
+
# The tool registry tells us where the binary is for each MCP id.
|
|
145
|
+
from omega_engine.tools import ToolRegistry # local import — avoid cycle
|
|
146
|
+
reg = ToolRegistry.load(omega_home)
|
|
147
|
+
tool_by_id = {t.name: t for t in reg.list()}
|
|
148
|
+
|
|
149
|
+
mcp_servers: dict[str, Any] = {}
|
|
150
|
+
for server in cfg.get("servers") or []:
|
|
151
|
+
sid = str(server.get("id", "")).strip()
|
|
152
|
+
if not sid or not server.get("enabled", True):
|
|
153
|
+
continue
|
|
154
|
+
entry: dict[str, Any] = {}
|
|
155
|
+
tool = tool_by_id.get(sid)
|
|
156
|
+
cat = catalog_by_id.get(sid, {})
|
|
157
|
+
install = cat.get("install") or {}
|
|
158
|
+
|
|
159
|
+
# Prefer the locally installed binary; fall back to `npx <package>`.
|
|
160
|
+
if tool and tool.invoke:
|
|
161
|
+
entry["command"] = str(omega_home / tool.invoke)
|
|
162
|
+
entry["args"] = list(server.get("args") or [])
|
|
163
|
+
elif install.get("method") == "npx" and install.get("package"):
|
|
164
|
+
entry["command"] = "npx"
|
|
165
|
+
entry["args"] = ["-y", str(install["package"]), *(server.get("args") or [])]
|
|
166
|
+
else:
|
|
167
|
+
# nothing to invoke — skip rather than write a half-broken entry
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
from_server = list(server.get("secret_refs") or [])
|
|
171
|
+
from_tool = list(tool.secret_refs) if tool else []
|
|
172
|
+
secret_refs = from_server or from_tool
|
|
173
|
+
if secret_refs:
|
|
174
|
+
# Claude Code's .mcp.json supports an `env` block; we project
|
|
175
|
+
# secret refs by name (the value is filled at runtime from the
|
|
176
|
+
# vault, never written to disk in plaintext).
|
|
177
|
+
entry["env"] = {ref: f"${{{ref}}}" for ref in secret_refs}
|
|
178
|
+
|
|
179
|
+
mcp_servers[sid] = entry
|
|
180
|
+
|
|
181
|
+
out_root = self._ensure(out_root)
|
|
182
|
+
target = out_root / ".mcp.json"
|
|
183
|
+
payload = {"mcpServers": mcp_servers}
|
|
184
|
+
target.write_text(json.dumps(payload, indent=2) + "\n")
|
|
185
|
+
return payload
|
|
186
|
+
|
|
187
|
+
# ----- hooks → settings.json -----------------------------------------
|
|
188
|
+
|
|
189
|
+
def _project_hooks(self, omega_home: Path, out_root: Path) -> dict[str, Any]:
|
|
190
|
+
"""Merge `Agentik_SSOT/hooks/*` into <target>/settings.json `hooks`.
|
|
191
|
+
|
|
192
|
+
Each file under hooks/ is one of:
|
|
193
|
+
- *.json — already in Claude Code hook shape; merged shallow into hooks
|
|
194
|
+
- *.yaml/*.yml — same shape, parsed as YAML
|
|
195
|
+
Unknown extensions are ignored.
|
|
196
|
+
"""
|
|
197
|
+
hooks_dir = omega_home / "Agentik_SSOT" / "hooks"
|
|
198
|
+
out_root = self._ensure(out_root)
|
|
199
|
+
settings_file = out_root / "settings.json"
|
|
200
|
+
|
|
201
|
+
settings: dict[str, Any] = {}
|
|
202
|
+
if settings_file.exists():
|
|
203
|
+
try:
|
|
204
|
+
settings = json.loads(settings_file.read_text() or "{}")
|
|
205
|
+
except json.JSONDecodeError:
|
|
206
|
+
settings = {}
|
|
207
|
+
|
|
208
|
+
hooks_block: dict[str, Any] = dict(settings.get("hooks") or {})
|
|
209
|
+
|
|
210
|
+
if hooks_dir.is_dir():
|
|
211
|
+
import yaml
|
|
212
|
+
for path in sorted(hooks_dir.iterdir()):
|
|
213
|
+
if not path.is_file():
|
|
214
|
+
continue
|
|
215
|
+
if path.suffix == ".json":
|
|
216
|
+
try:
|
|
217
|
+
data = json.loads(path.read_text() or "{}")
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
continue
|
|
220
|
+
elif path.suffix in {".yaml", ".yml"}:
|
|
221
|
+
data = yaml.safe_load(path.read_text() or "") or {}
|
|
222
|
+
else:
|
|
223
|
+
continue
|
|
224
|
+
if not isinstance(data, dict):
|
|
225
|
+
continue
|
|
226
|
+
for k, v in data.items():
|
|
227
|
+
hooks_block[k] = v
|
|
228
|
+
|
|
229
|
+
settings["hooks"] = hooks_block
|
|
230
|
+
settings_file.write_text(json.dumps(settings, indent=2) + "\n")
|
|
231
|
+
return hooks_block
|
|
232
|
+
|
|
233
|
+
# ----- entry point ---------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def project(self, omega_home: Path) -> dict[str, Any]:
|
|
236
|
+
target = self.target_dir(omega_home)
|
|
237
|
+
self._ensure(target)
|
|
238
|
+
ssot = omega_home / "Agentik_SSOT"
|
|
239
|
+
|
|
240
|
+
skills = self._project_markdown_dir(ssot / "skills", target / "skills", "skill")
|
|
241
|
+
commands = self._project_markdown_dir(ssot / "commands", target / "commands", "command")
|
|
242
|
+
agents = self._project_markdown_dir(ssot / "agents", target / "agents", "agent")
|
|
243
|
+
mcp = self._project_mcp(omega_home, target)
|
|
244
|
+
hooks = self._project_hooks(omega_home, target)
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
"provider": self.id,
|
|
248
|
+
"projected": True,
|
|
249
|
+
"target": str(target),
|
|
250
|
+
"skills_written": len(skills),
|
|
251
|
+
"commands_written": len(commands),
|
|
252
|
+
"agents_written": len(agents),
|
|
253
|
+
"mcp_servers": len(mcp.get("mcpServers", {})) if isinstance(mcp, dict) else 0,
|
|
254
|
+
"hooks": list(hooks.keys()),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# --------------------------------------------------------------------------
|
|
259
|
+
# Sync engine
|
|
260
|
+
# --------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _active_providers(omega_home: Path) -> list[str]:
|
|
264
|
+
"""Read Agentik_Providers/registry.yaml; default to [claude] if missing."""
|
|
265
|
+
try:
|
|
266
|
+
import yaml
|
|
267
|
+
except ImportError:
|
|
268
|
+
return ["claude"]
|
|
269
|
+
path = omega_home / "Agentik_Providers" / "registry.yaml"
|
|
270
|
+
if not path.exists():
|
|
271
|
+
return ["claude"]
|
|
272
|
+
data = yaml.safe_load(path.read_text()) or {}
|
|
273
|
+
return [str(p.get("id")) for p in data.get("providers") or [] if p.get("id")]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class SyncEngine:
|
|
277
|
+
"""Run every active provider's adapter against the SSOT."""
|
|
278
|
+
|
|
279
|
+
def __init__(self, adapters: Iterable[ProviderAdapter] | None = None) -> None:
|
|
280
|
+
self._explicit = list(adapters) if adapters is not None else None
|
|
281
|
+
|
|
282
|
+
def adapters_for(self, omega_home: Path) -> list[ProviderAdapter]:
|
|
283
|
+
if self._explicit is not None:
|
|
284
|
+
return list(self._explicit)
|
|
285
|
+
active = _active_providers(omega_home)
|
|
286
|
+
result: list[ProviderAdapter] = []
|
|
287
|
+
for pid in active:
|
|
288
|
+
if pid == "claude":
|
|
289
|
+
result.append(ClaudeCodeAdapter())
|
|
290
|
+
else:
|
|
291
|
+
# GLM / OpenAI / DeepSeek don't have a native on-disk format;
|
|
292
|
+
# MCP/skills/commands are passed via API. Document this.
|
|
293
|
+
result.append(_StubAdapter(
|
|
294
|
+
pid,
|
|
295
|
+
"no native projection — used directly via API",
|
|
296
|
+
))
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
def sync_all(self, omega_home: str | Path | None = None) -> list[dict[str, Any]]:
|
|
300
|
+
home = _omega_home(omega_home)
|
|
301
|
+
outcomes: list[dict[str, Any]] = []
|
|
302
|
+
for adapter in self.adapters_for(home):
|
|
303
|
+
outcomes.append(adapter.project(home))
|
|
304
|
+
return outcomes
|