omega-code 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. omega/__init__.py +0 -0
  2. omega/__main__.py +589 -0
  3. omega/artifacts.py +151 -0
  4. omega/checkpoint.py +246 -0
  5. omega/compact.py +106 -0
  6. omega/config.py +285 -0
  7. omega/eval/__init__.py +3 -0
  8. omega/eval/cli.py +127 -0
  9. omega/eval/examples/plan-version-flag.yaml +11 -0
  10. omega/eval/examples/relative-age-negative-delta.yaml +14 -0
  11. omega/eval/examples/version-flag.yaml +10 -0
  12. omega/eval/manifest.py +129 -0
  13. omega/eval/prices.py +29 -0
  14. omega/eval/report.py +135 -0
  15. omega/eval/runner.py +199 -0
  16. omega/eval/tasks.py +97 -0
  17. omega/events.py +145 -0
  18. omega/export.py +80 -0
  19. omega/gitlog.py +229 -0
  20. omega/hooks.py +63 -0
  21. omega/instructions.py +103 -0
  22. omega/integrations.py +284 -0
  23. omega/keys.py +173 -0
  24. omega/llm.py +442 -0
  25. omega/loop.py +510 -0
  26. omega/mcp.py +490 -0
  27. omega/memory/__init__.py +5 -0
  28. omega/memory/consolidate.py +103 -0
  29. omega/memory/curate.py +69 -0
  30. omega/memory/store.py +321 -0
  31. omega/memory/tools.py +175 -0
  32. omega/migrate.py +40 -0
  33. omega/onboarding.py +242 -0
  34. omega/permissions.py +137 -0
  35. omega/secrets.py +173 -0
  36. omega/server/__init__.py +7 -0
  37. omega/server/__main__.py +18 -0
  38. omega/server/app.py +71 -0
  39. omega/server/auth.py +73 -0
  40. omega/server/manager.py +287 -0
  41. omega/server/models.py +123 -0
  42. omega/server/tasks_api.py +311 -0
  43. omega/server/terminals.py +245 -0
  44. omega/server/worker.py +186 -0
  45. omega/session.py +209 -0
  46. omega/setup.html +281 -0
  47. omega/setup_server.py +452 -0
  48. omega/skills.py +158 -0
  49. omega/subagent.py +98 -0
  50. omega/tasks.py +195 -0
  51. omega/tools.py +590 -0
  52. omega/trace.py +156 -0
  53. omega/trajectory.py +146 -0
  54. omega/ui/__init__.py +0 -0
  55. omega/ui/composer.py +140 -0
  56. omega/ui/format.py +708 -0
  57. omega/ui/plain.py +141 -0
  58. omega/ui/tui/__init__.py +9 -0
  59. omega/ui/tui/app.py +958 -0
  60. omega/ui/tui/history.py +50 -0
  61. omega/ui/tui/modals.py +292 -0
  62. omega/ui/tui/onboarding.py +367 -0
  63. omega/ui/tui/prefs.py +25 -0
  64. omega/ui/tui/sidebar.py +510 -0
  65. omega/ui/tui/status.py +115 -0
  66. omega/ui/tui/theme.py +91 -0
  67. omega/ui/tui/transcript.py +783 -0
  68. omega/verify.py +133 -0
  69. omega_code-0.4.0.dist-info/METADATA +479 -0
  70. omega_code-0.4.0.dist-info/RECORD +73 -0
  71. omega_code-0.4.0.dist-info/WHEEL +4 -0
  72. omega_code-0.4.0.dist-info/entry_points.txt +2 -0
  73. omega_code-0.4.0.dist-info/licenses/LICENSE +21 -0
omega/server/auth.py ADDED
@@ -0,0 +1,73 @@
1
+ """Bearer-token auth for the D1 daemon. The token is minted once and written
2
+ to `~/.omega/serve.json` (0600) by `app.main()` on startup, so the Mac app can
3
+ discover a running daemon (port, token, pid) and authenticate against it --
4
+ see Phase 9 of the plan. Bound to 127.0.0.1 only; this token is the only
5
+ access control, not a substitute for network isolation."""
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import secrets
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from fastapi import HTTPException, Request, WebSocket
15
+
16
+ SERVE_PATH = Path.home() / ".omega" / "serve.json"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ServeInfo:
21
+ port: int
22
+ token: str
23
+ pid: int
24
+
25
+
26
+ def generate_token() -> str:
27
+ return secrets.token_hex(32)
28
+
29
+
30
+ def write_serve_info(info: ServeInfo) -> None:
31
+ SERVE_PATH.parent.mkdir(parents=True, exist_ok=True)
32
+ tmp = SERVE_PATH.with_suffix(".tmp")
33
+ tmp.write_text(json.dumps({"port": info.port, "token": info.token, "pid": info.pid}))
34
+ tmp.replace(SERVE_PATH)
35
+ os.chmod(SERVE_PATH, 0o600)
36
+
37
+
38
+ def remove_serve_info() -> None:
39
+ SERVE_PATH.unlink(missing_ok=True)
40
+
41
+
42
+ def read_serve_info() -> ServeInfo | None:
43
+ if not SERVE_PATH.exists():
44
+ return None
45
+ try:
46
+ raw = json.loads(SERVE_PATH.read_text())
47
+ return ServeInfo(port=int(raw["port"]), token=str(raw["token"]), pid=int(raw["pid"]))
48
+ except (OSError, json.JSONDecodeError, KeyError, ValueError):
49
+ return None
50
+
51
+
52
+ def _bearer(header: str) -> str | None:
53
+ return header[7:] if header.lower().startswith("bearer ") else None
54
+
55
+
56
+ def require_token(request: Request) -> None:
57
+ """FastAPI dependency for every REST route -- raises 401 on a missing or
58
+ wrong token instead of letting the route run."""
59
+ expected: str = request.app.state.token
60
+ provided = _bearer(request.headers.get("authorization", ""))
61
+ if provided is None or not secrets.compare_digest(provided, expected):
62
+ raise HTTPException(status_code=401, detail="unauthorized")
63
+
64
+
65
+ def check_ws_token(websocket: WebSocket) -> bool:
66
+ """WebSocket routes call this themselves (before `accept()`) rather than
67
+ using it as a Depends(): raising out of a dependency during the WS
68
+ handshake closes the socket with an opaque 500, whereas the caller can
69
+ close(code=1008) with an explanation."""
70
+ expected: str = websocket.app.state.token
71
+ provided = (_bearer(websocket.headers.get("authorization", ""))
72
+ or websocket.query_params.get("token"))
73
+ return provided is not None and secrets.compare_digest(provided, expected)
@@ -0,0 +1,287 @@
1
+ """Owns the daemon's live task state: one child worker process per running
2
+ task (see worker.py for the child side), WebSocket subscriber fanout, and the
3
+ answer/confirm relay between HTTP callers and a worker blocked on
4
+ ask_user/confirm.
5
+
6
+ Why a child process per task at all: `tools.CONFIRM`/`ASK_USER`/`SESSION_ID`/
7
+ `TAINTED`, `subagent.EMIT` and `os.getcwd()` (bash's cwd) are process-wide
8
+ globals in omega's agent loop -- running two tasks in one process would have
9
+ them race and clobber each other. A worker is spawned with
10
+ `_worker_argv(task)` (overridable so tests can point it at a scripted stub
11
+ instead of the real `python -m omega.server.worker`) and talked to over
12
+ newline-delimited JSON on stdin/stdout.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import json
18
+ import secrets
19
+ import sys
20
+ import time
21
+ from collections.abc import Callable
22
+ from dataclasses import dataclass, field
23
+ from typing import Any
24
+
25
+ from .. import tasks
26
+ from ..eval import prices
27
+
28
+ WorkerArgv = Callable[[tasks.Task], list[str]]
29
+
30
+ # Event types that change something a client would want to see on the task
31
+ # list or overview -- persisted immediately. Everything else (TextDelta,
32
+ # ToolStart/End, ...) is still forwarded live to WS subscribers but does not
33
+ # hit disk on every line, which streaming token deltas would do many times a
34
+ # second.
35
+ _PERSIST_ON = {"Phase", "ModelUsed", "Usage", "Done", "Error", "Checkpoint",
36
+ "Verified", "ask_user_request", "confirm_request", "fatal"}
37
+
38
+
39
+ def default_worker_argv(task: tasks.Task) -> list[str]:
40
+ return [sys.executable, "-m", "omega.server.worker", task.id]
41
+
42
+
43
+ @dataclass
44
+ class RunningTask:
45
+ proc: asyncio.subprocess.Process
46
+ reader_task: asyncio.Task[None] | None = None
47
+ stderr_task: asyncio.Task[None] | None = None
48
+ turn_busy: bool = False
49
+ turn_started: float | None = None
50
+ last_alias: str | None = None
51
+ # Pending RPC-style requests (currently just "jobs") keyed by request_id --
52
+ # unlike prompt/answer/confirm these expect exactly one reply back.
53
+ pending_replies: dict[str, asyncio.Future[Any]] = field(default_factory=dict)
54
+
55
+
56
+ class TaskManager:
57
+ def __init__(self, worker_argv: WorkerArgv = default_worker_argv) -> None:
58
+ self._worker_argv = worker_argv
59
+ self._running: dict[str, RunningTask] = {}
60
+ self._task_subs: dict[str, set[asyncio.Queue[str]]] = {}
61
+ self._overview_subs: set[asyncio.Queue[str]] = set()
62
+
63
+ # -- subscriptions ----------------------------------------------------
64
+
65
+ def subscribe_task(self, task_id: str) -> asyncio.Queue[str]:
66
+ q: asyncio.Queue[str] = asyncio.Queue()
67
+ self._task_subs.setdefault(task_id, set()).add(q)
68
+ return q
69
+
70
+ def unsubscribe_task(self, task_id: str, q: asyncio.Queue[str]) -> None:
71
+ self._task_subs.get(task_id, set()).discard(q)
72
+
73
+ def subscribe_overview(self) -> asyncio.Queue[str]:
74
+ q: asyncio.Queue[str] = asyncio.Queue()
75
+ self._overview_subs.add(q)
76
+ return q
77
+
78
+ def unsubscribe_overview(self, q: asyncio.Queue[str]) -> None:
79
+ self._overview_subs.discard(q)
80
+
81
+ def _publish_task(self, task_id: str, line: str) -> None:
82
+ for q in self._task_subs.get(task_id, ()):
83
+ q.put_nowait(line)
84
+
85
+ def publish_overview_task(self, task: tasks.Task) -> None:
86
+ from .models import TaskOut
87
+ line = json.dumps({"type": "task", "task": TaskOut.from_task(task).model_dump()})
88
+ for q in self._overview_subs:
89
+ q.put_nowait(line)
90
+
91
+ def publish_overview_terminals(self, terminals: list[dict[str, Any]]) -> None:
92
+ line = json.dumps({"type": "terminals", "terminals": terminals})
93
+ for q in self._overview_subs:
94
+ q.put_nowait(line)
95
+
96
+ def is_running(self, task_id: str) -> bool:
97
+ running = self._running.get(task_id)
98
+ return running is not None and running.proc.returncode is None
99
+
100
+ # -- worker lifecycle ---------------------------------------------------
101
+
102
+ async def _ensure_started(self, task: tasks.Task) -> RunningTask:
103
+ running = self._running.get(task.id)
104
+ if running is not None and running.proc.returncode is None:
105
+ return running
106
+ argv = self._worker_argv(task)
107
+ proc = await asyncio.create_subprocess_exec(
108
+ *argv,
109
+ stdin=asyncio.subprocess.PIPE,
110
+ stdout=asyncio.subprocess.PIPE,
111
+ stderr=asyncio.subprocess.PIPE,
112
+ )
113
+ running = RunningTask(proc=proc)
114
+ self._running[task.id] = running
115
+ running.reader_task = asyncio.create_task(self._read_worker(task.id, running))
116
+ running.stderr_task = asyncio.create_task(self._drain_stderr(proc))
117
+ return running
118
+
119
+ async def _drain_stderr(self, proc: asyncio.subprocess.Process) -> None:
120
+ assert proc.stderr is not None
121
+ async for _line in proc.stderr:
122
+ pass # worker tracebacks land in the daemon's own log via app.py
123
+
124
+ async def _send(self, running: RunningTask, msg: dict[str, Any]) -> None:
125
+ assert running.proc.stdin is not None
126
+ running.proc.stdin.write((json.dumps(msg) + "\n").encode())
127
+ await running.proc.stdin.drain()
128
+
129
+ async def send_prompt(self, task: tasks.Task, text: str) -> None:
130
+ running = await self._ensure_started(task)
131
+ running.turn_started = time.time()
132
+ task.status = "running"
133
+ task.save()
134
+ self.publish_overview_task(task)
135
+ await self._send(running, {"cmd": "prompt", "text": text})
136
+
137
+ async def send_answer(self, task_id: str, request_id: str, answer: str) -> bool:
138
+ running = self._running.get(task_id)
139
+ if running is None:
140
+ return False
141
+ await self._send(running, {"cmd": "answer", "request_id": request_id, "answer": answer})
142
+ self._mark_resumed(task_id)
143
+ return True
144
+
145
+ async def send_confirm(self, task_id: str, request_id: str, allow: bool) -> bool:
146
+ running = self._running.get(task_id)
147
+ if running is None:
148
+ return False
149
+ await self._send(running, {"cmd": "confirm", "request_id": request_id, "allow": allow})
150
+ self._mark_resumed(task_id)
151
+ return True
152
+
153
+ def _mark_resumed(self, task_id: str) -> None:
154
+ task = tasks.get(task_id)
155
+ if task is not None and task.status == "waiting_input":
156
+ task.status = "running"
157
+ task.save()
158
+ self.publish_overview_task(task)
159
+
160
+ async def cancel(self, task_id: str) -> bool:
161
+ running = self._running.get(task_id)
162
+ if running is None:
163
+ return False
164
+ await self._send(running, {"cmd": "cancel"})
165
+ return True
166
+
167
+ async def set_model(self, task_id: str, model: str | None) -> None:
168
+ running = self._running.get(task_id)
169
+ if running is not None:
170
+ await self._send(running, {"cmd": "set_model", "model": model})
171
+
172
+ async def set_mode(self, task_id: str, mode: str) -> None:
173
+ running = self._running.get(task_id)
174
+ if running is not None:
175
+ await self._send(running, {"cmd": "set_mode", "mode": mode})
176
+
177
+ async def shutdown_task(self, task_id: str) -> None:
178
+ running = self._running.get(task_id)
179
+ if running is None:
180
+ return
181
+ try:
182
+ await self._send(running, {"cmd": "shutdown"})
183
+ except (BrokenPipeError, ConnectionResetError):
184
+ pass
185
+ try:
186
+ await asyncio.wait_for(running.proc.wait(), timeout=5)
187
+ except TimeoutError:
188
+ running.proc.kill()
189
+
190
+ async def shutdown_all(self) -> None:
191
+ await asyncio.gather(*(self.shutdown_task(tid) for tid in list(self._running)),
192
+ return_exceptions=True)
193
+
194
+ async def get_jobs(self, task_id: str, timeout: float = 5.0) -> list[dict[str, Any]]:
195
+ """Background `bash(..., background=True)` jobs live in the worker
196
+ process's `tools._JOBS`, not here -- ask it over the same stdio
197
+ channel and wait for the one "jobs" reply that request_id names."""
198
+ running = self._running.get(task_id)
199
+ if running is None:
200
+ return []
201
+ request_id = secrets.token_hex(6)
202
+ fut: asyncio.Future[list[dict[str, Any]]] = asyncio.get_event_loop().create_future()
203
+ running.pending_replies[request_id] = fut
204
+ await self._send(running, {"cmd": "jobs", "request_id": request_id})
205
+ try:
206
+ return await asyncio.wait_for(fut, timeout)
207
+ except TimeoutError:
208
+ return []
209
+ finally:
210
+ running.pending_replies.pop(request_id, None)
211
+
212
+ # -- event stream ---------------------------------------------------
213
+
214
+ async def _read_worker(self, task_id: str, running: RunningTask) -> None:
215
+ assert running.proc.stdout is not None
216
+ try:
217
+ async for raw in running.proc.stdout:
218
+ line = raw.decode(errors="replace").strip()
219
+ if not line:
220
+ continue
221
+ if self._resolve_reply(running, line):
222
+ continue
223
+ self._publish_task(task_id, line)
224
+ self._apply_event(task_id, running, line)
225
+ finally:
226
+ self._running.pop(task_id, None)
227
+ task = tasks.get(task_id)
228
+ if task is not None and task.status in ("running", "waiting_input"):
229
+ task.status = "failed"
230
+ task.save()
231
+ self.publish_overview_task(task)
232
+
233
+ def _resolve_reply(self, running: RunningTask, line: str) -> bool:
234
+ """Handles an RPC-style reply (currently only "jobs") -- these are not
235
+ `events.Event`s and must not be published to WS subscribers or
236
+ mistaken for task state."""
237
+ try:
238
+ obj = json.loads(line)
239
+ except json.JSONDecodeError:
240
+ return False
241
+ if obj.get("type") != "jobs":
242
+ return False
243
+ fut = running.pending_replies.get(obj.get("request_id"))
244
+ if fut is not None and not fut.done():
245
+ fut.set_result(obj.get("jobs", []))
246
+ return True
247
+
248
+ def _apply_event(self, task_id: str, running: RunningTask, line: str) -> None:
249
+ try:
250
+ obj = json.loads(line)
251
+ except json.JSONDecodeError:
252
+ return
253
+ kind = obj.get("type")
254
+ if kind not in _PERSIST_ON:
255
+ return
256
+ task = tasks.get(task_id)
257
+ if task is None:
258
+ return
259
+
260
+ if kind == "Phase":
261
+ state = str(obj.get("state", ""))
262
+ task.phase = state
263
+ if state != "idle" and task.status != "waiting_input":
264
+ task.status = "running"
265
+ elif kind == "ModelUsed":
266
+ running.last_alias = obj.get("alias")
267
+ task.model = running.last_alias or obj.get("model") or task.model
268
+ elif kind == "Usage":
269
+ prompt_tokens = int(obj.get("prompt_tokens") or 0)
270
+ completion_tokens = int(obj.get("completion_tokens") or 0)
271
+ task.tokens_in += prompt_tokens
272
+ task.tokens_out += completion_tokens
273
+ if running.last_alias:
274
+ delta = prices.estimate_cost(running.last_alias, prompt_tokens, completion_tokens)
275
+ if delta is not None:
276
+ task.cost_usd = (task.cost_usd or 0.0) + delta
277
+ elif kind in ("ask_user_request", "confirm_request"):
278
+ task.status = "waiting_input"
279
+ elif kind in ("Done", "Error", "fatal"):
280
+ task.status = "failed" if kind != "Done" else "done"
281
+ task.phase = "idle"
282
+ if running.turn_started is not None:
283
+ task.elapsed_s += time.time() - running.turn_started
284
+ running.turn_started = None
285
+
286
+ task.save()
287
+ self.publish_overview_task(task)
omega/server/models.py ADDED
@@ -0,0 +1,123 @@
1
+ """Pydantic request/response schemas for the D1 daemon's REST API. Field
2
+ names here ARE the wire contract the SwiftUI app is built against -- see
3
+ Phase 9 of the plan; keep them in lockstep with that spec, not with whatever
4
+ is convenient internally (that's what `TaskOut.from_task` adapts)."""
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from .. import tasks
12
+
13
+
14
+ class HealthOut(BaseModel):
15
+ status: Literal["ok"] = "ok"
16
+ version: str
17
+
18
+
19
+ class TaskCreateIn(BaseModel):
20
+ repo: str
21
+ prompt: str | None = None
22
+ worktree: bool = True
23
+ model: str | None = None
24
+ mode: str = "build"
25
+
26
+
27
+ class TaskOut(BaseModel):
28
+ id: str
29
+ title: str
30
+ repo: str
31
+ cwd: str
32
+ worktree: bool
33
+ branch: str | None
34
+ pr: dict[str, Any] | None
35
+ model: str | None
36
+ mode: str
37
+ status: str
38
+ phase: str
39
+ created: float
40
+ updated: float
41
+ tokens_in: int
42
+ tokens_out: int
43
+ cost_usd: float | None
44
+ elapsed_s: float
45
+
46
+ @classmethod
47
+ def from_task(cls, task: tasks.Task) -> TaskOut:
48
+ return cls(**{f: getattr(task, f) for f in TaskOut.model_fields})
49
+
50
+
51
+ class TaskDetailOut(TaskOut):
52
+ history: list[dict[str, Any]]
53
+
54
+
55
+ class PromptIn(BaseModel):
56
+ text: str
57
+
58
+
59
+ class AnswerIn(BaseModel):
60
+ request_id: str
61
+ answer: str
62
+
63
+
64
+ class ConfirmIn(BaseModel):
65
+ request_id: str
66
+ allow: bool
67
+
68
+
69
+ class ModelIn(BaseModel):
70
+ model: str
71
+
72
+
73
+ class ModeIn(BaseModel):
74
+ mode: str
75
+
76
+
77
+ class PrCreateIn(BaseModel):
78
+ title: str | None = None
79
+ body: str | None = None
80
+ draft: bool = False
81
+
82
+
83
+ class UndoIn(BaseModel):
84
+ steps: int = 1
85
+
86
+
87
+ class TerminalCreateIn(BaseModel):
88
+ task_id: str | None = None
89
+ cwd: str | None = None
90
+
91
+
92
+ class TerminalOut(BaseModel):
93
+ id: str
94
+ task_id: str | None
95
+ pid: int
96
+ cwd: str
97
+ created: float
98
+
99
+
100
+ class ModelCatalogEntry(BaseModel):
101
+ alias: str
102
+ model: str
103
+ provider: str
104
+ context: int
105
+
106
+
107
+ class ConnectionOut(BaseModel):
108
+ name: str
109
+ enabled: bool
110
+ state: str
111
+ tools: int
112
+ error: str | None
113
+ last_used: float | None
114
+
115
+
116
+ class MemoryHitOut(BaseModel):
117
+ id: str
118
+ scope: str
119
+ type: str
120
+ title: str
121
+ body: str
122
+ confidence: float
123
+ importance: float