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/subagent.py ADDED
@@ -0,0 +1,98 @@
1
+ import secrets
2
+ from collections.abc import Callable
3
+
4
+ from . import events, tools
5
+ from .config import Config
6
+ from .session import Message
7
+
8
+ CFG: Config | None = None
9
+ # Set by run_turn at the start of every turn, mirroring tools.set_tainted(False).
10
+ EMIT: Callable[[events.Event], None] | None = None
11
+
12
+ TIERS = {"fast": "subagent_fast", "mid": "subagent_mid", "verify": "subagent_mid"}
13
+
14
+ SYSTEM = """You are a research subagent. You have read-only tools.
15
+
16
+ Investigate and answer the task concretely -- real paths, real line numbers, real
17
+ symbol names. Do not speculate; if you could not determine something, say so.
18
+
19
+ Your reply is the ONLY thing the orchestrator sees, so it must stand alone. Be
20
+ dense and short: no preamble, no restating the task, no offers to help further."""
21
+
22
+ REVIEW_SYSTEM = """You are a verification subagent reviewing a just-completed
23
+ BUILD-mode turn. You have read-only tools.
24
+
25
+ You are given the user's original request and a diff of the changes made to
26
+ address it. Check the diff for: correctness against the ask, cases the diff
27
+ misses, and leftover debug code (stray prints, commented-out code, TODO
28
+ markers left by mistake). Read surrounding files with your tools when the
29
+ diff alone doesn't tell you enough.
30
+
31
+ Reply with exactly "OK" if the change looks right. Otherwise reply with a
32
+ short numbered list of concrete issues -- no preamble, no restating the diff."""
33
+
34
+
35
+ @tools.tool(
36
+ "subagent",
37
+ "Delegate a bounded read-only research task to a smaller, faster model. "
38
+ "Returns a dense summary instead of raw tool output, so use it for wide "
39
+ "searches, multi-file reading, and 'where is X' questions. "
40
+ "tier: 'fast' for lookups and greps, 'mid' for reasoning across files.",
41
+ {"task": {"type": "string"},
42
+ "tier": {"type": "string", "enum": ["fast", "mid"]}},
43
+ ["task"],
44
+ )
45
+ async def _subagent(task: str, tier: str = "fast") -> str:
46
+ from .loop import run_agent
47
+ if CFG is None:
48
+ return "error: subagent not wired"
49
+ role = TIERS.get(tier, "subagent_fast")
50
+ subagent_id = secrets.token_hex(3)
51
+ emit = EMIT or (lambda _e: None)
52
+
53
+ def forward(ev: events.Event) -> None:
54
+ # The subagent's prose is its return value, not transcript text --
55
+ # only its tool activity is visible upward. ModelUsed is also
56
+ # swallowed: it names the subagent's own tier model and would
57
+ # otherwise stomp the top-level turn's status-bar model on emit.
58
+ # Phase is swallowed too -- the subagent's own waiting/thinking/
59
+ # streaming churn is not the top-level turn's phase; the parent
60
+ # stays in "tools" for the whole duration of the subagent call.
61
+ if not isinstance(ev, events.TextDelta | events.Done | events.ModelUsed | events.Phase):
62
+ emit(ev)
63
+
64
+ emit(events.SubagentSpawned(subagent_id=subagent_id, tier=tier,
65
+ task_preview=" ".join(task.split())[:80]))
66
+ history: list[Message] = [{"role": "user", "content": task}]
67
+ result = await run_agent(CFG, role, SYSTEM, history,
68
+ tools.READ_ONLY - {"subagent"}, emit=forward,
69
+ max_rounds=12, subagent_id=subagent_id, tier=tier)
70
+ emit(events.SubagentDone(subagent_id=subagent_id,
71
+ summary_preview=" ".join(result.split())[:120]))
72
+ return result
73
+
74
+
75
+ async def review(cfg: Config, request: str, diff_text: str,
76
+ emit: Callable[[events.Event], None] | None = None) -> str:
77
+ """Invoked automatically by loop.run_agent at the end of a BUILD turn
78
+ that made a non-trivial change (see loop.py) -- not reachable from the
79
+ `subagent` tool's tier enum, which only offers "fast"/"mid" to the model.
80
+ Returns "OK" or a numbered list of issues; never raises."""
81
+ from .loop import run_agent
82
+ emit = emit or (lambda _e: None)
83
+ subagent_id = secrets.token_hex(3)
84
+
85
+ def forward(ev: events.Event) -> None:
86
+ if not isinstance(ev, events.TextDelta | events.Done | events.ModelUsed | events.Phase):
87
+ emit(ev)
88
+
89
+ emit(events.SubagentSpawned(subagent_id=subagent_id, tier="verify",
90
+ task_preview=" ".join(request.split())[:80]))
91
+ task = f"Original request:\n{request}\n\nDiff of changes made this turn:\n{diff_text}"
92
+ history: list[Message] = [{"role": "user", "content": task}]
93
+ result = await run_agent(cfg, TIERS["verify"], REVIEW_SYSTEM, history,
94
+ tools.READ_ONLY - {"subagent"}, emit=forward,
95
+ max_rounds=6, subagent_id=subagent_id, tier="verify")
96
+ emit(events.SubagentDone(subagent_id=subagent_id,
97
+ summary_preview=" ".join(result.split())[:120]))
98
+ return result
omega/tasks.py ADDED
@@ -0,0 +1,195 @@
1
+ """A Task pairs a session with a repo (optionally its own git worktree) and
2
+ the run state (status/phase/tokens/cost) the D1 daemon updates as a worker
3
+ process streams events -- see server/manager.py. Task id is always the
4
+ underlying session id, so `session.load(task.id)` recovers full history.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ import time
13
+ from dataclasses import asdict, dataclass
14
+ from pathlib import Path
15
+ from typing import Any, Literal
16
+
17
+ from . import gitlog, session
18
+
19
+ DIR = Path.home() / ".omega" / "tasks"
20
+ WORKTREES_DIR = Path.home() / ".omega" / "worktrees"
21
+
22
+ Status = Literal["idle", "running", "waiting_input", "done", "failed"]
23
+
24
+ # `gh pr view` is a network round trip; a WS overview push or a sidebar repaint
25
+ # must not pay for it every time -- see pr_info().
26
+ _PR_CACHE_TTL = 60.0
27
+ _pr_cache: dict[str, tuple[float, dict[str, Any] | None]] = {}
28
+
29
+
30
+ @dataclass
31
+ class Task:
32
+ id: str
33
+ title: str
34
+ repo: str
35
+ cwd: str
36
+ worktree: bool
37
+ branch: str | None = None
38
+ pr: dict[str, Any] | None = None
39
+ model: str | None = None
40
+ mode: str = "build"
41
+ status: Status = "idle"
42
+ phase: str = ""
43
+ created: float = 0.0
44
+ updated: float = 0.0
45
+ tokens_in: int = 0
46
+ tokens_out: int = 0
47
+ cost_usd: float | None = None
48
+ elapsed_s: float = 0.0
49
+
50
+ @property
51
+ def path(self) -> Path:
52
+ return DIR / f"{self.id}.json"
53
+
54
+ def save(self) -> None:
55
+ DIR.mkdir(parents=True, exist_ok=True)
56
+ self.updated = time.time()
57
+ tmp = self.path.with_suffix(".tmp")
58
+ tmp.write_text(json.dumps(asdict(self), indent=1))
59
+ tmp.replace(self.path)
60
+
61
+
62
+ def _slugify(text: str, limit: int = 40) -> str:
63
+ slug = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
64
+ return slug[:limit].strip("-") or "task"
65
+
66
+
67
+ def title_from_prompt(prompt: str, width: int = 80) -> str:
68
+ text = " ".join(prompt.split())
69
+ return text[: width - 1] + "…" if len(text) > width else text
70
+
71
+
72
+ def _run_git(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
73
+ return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, timeout=30)
74
+
75
+
76
+ def _repo_root(repo: str) -> Path | None:
77
+ path = Path(repo).expanduser().resolve()
78
+ result = _run_git(["rev-parse", "--show-toplevel"], path)
79
+ if result.returncode != 0:
80
+ return None
81
+ return Path(result.stdout.strip())
82
+
83
+
84
+ def create(repo: str, prompt: str | None = None, worktree: bool = True,
85
+ model: str | None = None, mode: str = "build") -> Task:
86
+ """Creates the underlying session first (its id becomes the task id), then
87
+ -- when `worktree` -- a git worktree branched from the repo's current HEAD
88
+ at `~/.omega/worktrees/<repo-basename>/<task-id>` and points the session's
89
+ cwd at it, so every tool the agent runs (bash's cwd included) stays inside
90
+ the isolated checkout."""
91
+ root = _repo_root(repo)
92
+ if worktree and root is None:
93
+ raise ValueError(f"{repo!r} is not inside a git repository")
94
+ root = root or Path(repo).expanduser().resolve()
95
+
96
+ sess = session.Session.new(cwd=str(root), mode=mode)
97
+ branch: str | None = None
98
+ cwd = str(root)
99
+
100
+ if worktree:
101
+ slug = _slugify(prompt) if prompt else sess.id
102
+ branch = f"omega/{slug}"
103
+ wt_path = WORKTREES_DIR / root.name / sess.id
104
+ wt_path.parent.mkdir(parents=True, exist_ok=True)
105
+ added = _run_git(["worktree", "add", str(wt_path), "-b", branch], root)
106
+ if added.returncode != 0:
107
+ raise RuntimeError(f"git worktree add failed: {added.stderr.strip()}")
108
+ cwd = str(wt_path)
109
+ sess.cwd = cwd
110
+ else:
111
+ branch = gitlog._branch(root) if (root / ".git").exists() else None
112
+ if branch == "?":
113
+ branch = None
114
+
115
+ sess.save()
116
+ task = Task(
117
+ id=sess.id, title=title_from_prompt(prompt) if prompt else "(no prompt yet)",
118
+ repo=str(root), cwd=cwd, worktree=worktree, branch=branch,
119
+ model=model, mode=mode, created=sess.created, updated=sess.created,
120
+ )
121
+ task.save()
122
+ return task
123
+
124
+
125
+ def _load(path: Path) -> Task | None:
126
+ try:
127
+ raw = json.loads(path.read_text())
128
+ except (OSError, json.JSONDecodeError):
129
+ return None
130
+ known = set(Task.__dataclass_fields__)
131
+ try:
132
+ return Task(**{k: v for k, v in raw.items() if k in known})
133
+ except TypeError:
134
+ return None
135
+
136
+
137
+ def list_tasks() -> list[Task]:
138
+ if not DIR.exists():
139
+ return []
140
+ found = [t for p in DIR.glob("*.json") if (t := _load(p)) is not None]
141
+ return sorted(found, key=lambda t: t.updated, reverse=True)
142
+
143
+
144
+ def get(task_id: str) -> Task | None:
145
+ path = DIR / f"{task_id}.json"
146
+ if path.exists():
147
+ return _load(path)
148
+ matches = sorted(DIR.glob(f"{task_id}*.json")) if DIR.exists() else []
149
+ return _load(matches[-1]) if matches else None
150
+
151
+
152
+ def update(task_id: str, **fields: Any) -> Task | None:
153
+ task = get(task_id)
154
+ if task is None:
155
+ return None
156
+ for key, value in fields.items():
157
+ if key in Task.__dataclass_fields__:
158
+ setattr(task, key, value)
159
+ task.save()
160
+ return task
161
+
162
+
163
+ def remove(task_id: str, delete_worktree: bool = False) -> bool:
164
+ task = get(task_id)
165
+ if task is None:
166
+ return False
167
+ if delete_worktree and task.worktree:
168
+ _run_git(["worktree", "remove", "--force", task.cwd], Path(task.repo))
169
+ task.path.unlink(missing_ok=True)
170
+ return True
171
+
172
+
173
+ def pr_info(task: Task) -> dict[str, Any] | None:
174
+ """`gh pr view` for this task's branch, cached for `_PR_CACHE_TTL` seconds.
175
+ Never raises: no branch, no `gh` on PATH, no PR yet, or a network hiccup
176
+ all just mean "no PR to show" rather than an error the caller must handle."""
177
+ if not task.branch or shutil.which("gh") is None:
178
+ return None
179
+ now = time.time()
180
+ cached = _pr_cache.get(task.id)
181
+ if cached is not None and now - cached[0] < _PR_CACHE_TTL:
182
+ return cached[1]
183
+
184
+ info: dict[str, Any] | None
185
+ try:
186
+ result = subprocess.run(
187
+ ["gh", "pr", "view", "--json", "number,url,state,statusCheckRollup,title",
188
+ "--head", task.branch],
189
+ cwd=task.repo, capture_output=True, text=True, timeout=15,
190
+ )
191
+ info = json.loads(result.stdout) if result.returncode == 0 and result.stdout.strip() else None
192
+ except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
193
+ info = None
194
+ _pr_cache[task.id] = (now, info)
195
+ return info