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.
- omega/__init__.py +0 -0
- omega/__main__.py +589 -0
- omega/artifacts.py +151 -0
- omega/checkpoint.py +246 -0
- omega/compact.py +106 -0
- omega/config.py +285 -0
- omega/eval/__init__.py +3 -0
- omega/eval/cli.py +127 -0
- omega/eval/examples/plan-version-flag.yaml +11 -0
- omega/eval/examples/relative-age-negative-delta.yaml +14 -0
- omega/eval/examples/version-flag.yaml +10 -0
- omega/eval/manifest.py +129 -0
- omega/eval/prices.py +29 -0
- omega/eval/report.py +135 -0
- omega/eval/runner.py +199 -0
- omega/eval/tasks.py +97 -0
- omega/events.py +145 -0
- omega/export.py +80 -0
- omega/gitlog.py +229 -0
- omega/hooks.py +63 -0
- omega/instructions.py +103 -0
- omega/integrations.py +284 -0
- omega/keys.py +173 -0
- omega/llm.py +442 -0
- omega/loop.py +510 -0
- omega/mcp.py +490 -0
- omega/memory/__init__.py +5 -0
- omega/memory/consolidate.py +103 -0
- omega/memory/curate.py +69 -0
- omega/memory/store.py +321 -0
- omega/memory/tools.py +175 -0
- omega/migrate.py +40 -0
- omega/onboarding.py +242 -0
- omega/permissions.py +137 -0
- omega/secrets.py +173 -0
- omega/server/__init__.py +7 -0
- omega/server/__main__.py +18 -0
- omega/server/app.py +71 -0
- omega/server/auth.py +73 -0
- omega/server/manager.py +287 -0
- omega/server/models.py +123 -0
- omega/server/tasks_api.py +311 -0
- omega/server/terminals.py +245 -0
- omega/server/worker.py +186 -0
- omega/session.py +209 -0
- omega/setup.html +281 -0
- omega/setup_server.py +452 -0
- omega/skills.py +158 -0
- omega/subagent.py +98 -0
- omega/tasks.py +195 -0
- omega/tools.py +590 -0
- omega/trace.py +156 -0
- omega/trajectory.py +146 -0
- omega/ui/__init__.py +0 -0
- omega/ui/composer.py +140 -0
- omega/ui/format.py +708 -0
- omega/ui/plain.py +141 -0
- omega/ui/tui/__init__.py +9 -0
- omega/ui/tui/app.py +958 -0
- omega/ui/tui/history.py +50 -0
- omega/ui/tui/modals.py +292 -0
- omega/ui/tui/onboarding.py +367 -0
- omega/ui/tui/prefs.py +25 -0
- omega/ui/tui/sidebar.py +510 -0
- omega/ui/tui/status.py +115 -0
- omega/ui/tui/theme.py +91 -0
- omega/ui/tui/transcript.py +783 -0
- omega/verify.py +133 -0
- omega_code-0.4.0.dist-info/METADATA +479 -0
- omega_code-0.4.0.dist-info/RECORD +73 -0
- omega_code-0.4.0.dist-info/WHEEL +4 -0
- omega_code-0.4.0.dist-info/entry_points.txt +2 -0
- 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
|