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/trace.py ADDED
@@ -0,0 +1,156 @@
1
+ """Per-session JSONL event trace: every `events.Event` a session emits is
2
+ appended as one line to `~/.omega/sessions/<id>/trace.jsonl`, independent of
3
+ what either UI chooses to render -- so `omega trace <id>` can replay a
4
+ session's full timeline (including events a UI drops on the floor) after the
5
+ fact, and price it against `omega.eval.prices`."""
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import time
10
+ from dataclasses import asdict, is_dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from . import events, session
15
+
16
+ _TOOL_TYPES = {"ToolStart", "ToolEnd"}
17
+
18
+ _GLYPH = {
19
+ "ToolStart": "●", "ToolEnd": "└", "SubagentSpawned": "●", "SubagentDone": "✓",
20
+ "Compacted": "⏺", "MemoryWrite": "◆", "MemoryConsolidated": "◆", "Error": "✗",
21
+ "Done": "●", "Fallback": "⇄", "ModelUsed": "◇", "Usage": "◇", "Phase": "·",
22
+ "Checkpoint": "⎘", "Verified": "✓", "JobStarted": "⟳", "JobFinished": "✓",
23
+ }
24
+
25
+
26
+ def trace_path(session_id: str) -> Path:
27
+ return session.DIR / session_id / "trace.jsonl"
28
+
29
+
30
+ def append(session_id: str, ev: events.Event, turn: int) -> None:
31
+ """Fire-and-forget: observability must never be able to break a turn, so
32
+ any failure here (disk full, permissions, an event with no dataclass
33
+ fields) is swallowed rather than raised."""
34
+ try:
35
+ if not is_dataclass(ev):
36
+ return
37
+ path = trace_path(session_id)
38
+ path.parent.mkdir(parents=True, exist_ok=True)
39
+ line = {"t": time.time(), "turn": turn, "type": type(ev).__name__, **asdict(ev)}
40
+ with path.open("a") as f:
41
+ f.write(json.dumps(line) + "\n")
42
+ except Exception:
43
+ pass
44
+
45
+
46
+ def _read(session_id: str) -> list[dict[str, Any]]:
47
+ """Tolerates a truncated trailing line the same way `session._read_jsonl`
48
+ does -- appends are not fsynced, so a crash mid-write leaves one."""
49
+ path = trace_path(session_id)
50
+ if not path.exists():
51
+ return []
52
+ rows: list[dict[str, Any]] = []
53
+ for line in path.read_text().splitlines():
54
+ line = line.strip()
55
+ if not line:
56
+ continue
57
+ try:
58
+ rows.append(json.loads(line))
59
+ except json.JSONDecodeError:
60
+ break
61
+ return rows
62
+
63
+
64
+ def _summary(row: dict[str, Any]) -> str:
65
+ t = row.get("type", "")
66
+ if t == "ToolStart":
67
+ return f"{row.get('name', '')} {row.get('args_preview', '')}"
68
+ if t == "ToolEnd":
69
+ dur = float(row.get("duration_s") or 0.0)
70
+ return f"{row.get('name', '')} {row.get('outcome', '')} ({dur:.2f}s)".strip()
71
+ if t == "Done":
72
+ return " ".join(str(row.get("text", "")).split())[:80]
73
+ if t == "Error":
74
+ message = str(row.get("message", ""))
75
+ return message.splitlines()[0] if message else ""
76
+ if t == "SubagentSpawned":
77
+ return f"subagent({row.get('tier', '')}) {row.get('task_preview', '')}"
78
+ if t == "SubagentDone":
79
+ return f"subagent {row.get('subagent_id', '')} done"
80
+ if t == "Compacted":
81
+ return str(row.get("note", ""))
82
+ if t == "MemoryWrite":
83
+ return f"{row.get('type', '')} '{row.get('title', '')}' ({row.get('scope', '')})"
84
+ if t == "MemoryConsolidated":
85
+ return str(row.get("summary", ""))
86
+ if t == "ModelUsed":
87
+ alias = row.get("alias") or ""
88
+ return f"{alias} {row.get('model', '')}".strip()
89
+ if t == "Usage":
90
+ return f"{row.get('prompt_tokens', 0)} in / {row.get('completion_tokens', 0)} out"
91
+ if t == "Fallback":
92
+ return f"{row.get('from_model', '')} -> {row.get('to_model', '')}: {row.get('reason', '')}"
93
+ if t == "Phase":
94
+ return str(row.get("state", ""))
95
+ if t == "Checkpoint":
96
+ return f"checkpoint {row.get('id', '')}"
97
+ if t == "Verified":
98
+ return str(row.get("results_summary", ""))
99
+ if t == "JobStarted":
100
+ return f"{row.get('id', '')} {row.get('command', '')}"
101
+ if t == "JobFinished":
102
+ return f"{row.get('id', '')} exit {row.get('exit_code', '')}"
103
+ return ""
104
+
105
+
106
+ def _turn_totals(rows: list[dict[str, Any]]) -> dict[int, tuple[int, int, float | None]]:
107
+ """`turn -> (tokens_in, tokens_out, cost_usd)`, summed from `Usage` rows
108
+ and priced against whichever alias the last `ModelUsed` row announced --
109
+ `cost_usd` is None once that model is absent from `eval.prices.PRICES`."""
110
+ from .eval import prices
111
+
112
+ totals: dict[int, tuple[int, int, float | None]] = {}
113
+ alias: str | None = None
114
+ for row in rows:
115
+ if row.get("type") == "ModelUsed":
116
+ alias = row.get("alias")
117
+ elif row.get("type") == "Usage":
118
+ turn = int(row.get("turn", 0))
119
+ prev_in, prev_out, _ = totals.get(turn, (0, 0, None))
120
+ tin = prev_in + int(row.get("prompt_tokens", 0))
121
+ tout = prev_out + int(row.get("completion_tokens", 0))
122
+ cost = prices.estimate_cost(alias, tin, tout) if alias else None
123
+ totals[turn] = (tin, tout, cost)
124
+ return totals
125
+
126
+
127
+ def render_timeline(session_id: str, *, tools_only: bool = False, raw_json: bool = False) -> str:
128
+ rows = _read(session_id)
129
+ if raw_json:
130
+ return "\n".join(json.dumps(r) for r in rows)
131
+ if not rows:
132
+ return f"no trace for session {session_id}"
133
+
134
+ totals = _turn_totals(rows)
135
+ view = [r for r in rows if r.get("type") in _TOOL_TYPES] if tools_only else rows
136
+ t0 = float(rows[0].get("t") or 0.0)
137
+
138
+ lines = []
139
+ for row in view:
140
+ t = str(row.get("type", ""))
141
+ if t == "TextDelta":
142
+ continue
143
+ offset = float(row.get("t") or t0) - t0
144
+ turn = row.get("turn", 0)
145
+ glyph = _GLYPH.get(t, "·")
146
+ lines.append(f"+{offset:7.2f}s turn {turn:<3} {glyph} {t:<18}{_summary(row)}")
147
+
148
+ if totals:
149
+ lines.append("")
150
+ lines.append("per-turn totals:")
151
+ for turn in sorted(totals):
152
+ tin, tout, cost = totals[turn]
153
+ cost_text = f"${cost:.4f}" if cost is not None else "unknown"
154
+ lines.append(f" turn {turn}: {tin} in / {tout} out tokens · {cost_text}")
155
+
156
+ return "\n".join(lines)
omega/trajectory.py ADDED
@@ -0,0 +1,146 @@
1
+ """The trajectory ledger: a deterministic, non-LLM record of every tool call
2
+ made this session, derived by walking `history` rather than kept as separate
3
+ state -- that way it survives session resume for free and can never drift
4
+ from what actually happened.
5
+
6
+ Two consumers:
7
+ - `render()` -- the live "## Recent actions" block `loop.run_turn` injects
8
+ into the volatile half of the system prompt every round.
9
+ - `compaction_lines()` -- the full-fidelity, uncapped-per-field lines
10
+ `compact.py` folds into its deterministic ledger for a dropped range.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+ from . import tools
19
+ from .session import Message
20
+ from .ui import format
21
+
22
+ MAX_ENTRIES = 50
23
+ ACTION_LINE_CAP = 25
24
+ BLOCK_MAX_CHARS = 2000
25
+ ARGS_DIGEST_CHARS = 64
26
+ SUMMARY_CHARS = 120
27
+
28
+ _DEDUPE_NOTE = ("If an identical call already appears above, do not re-run it "
29
+ "-- call fetch_result on its recorded artifact instead.")
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Entry:
34
+ tool: str
35
+ args_digest: str
36
+ summary: str
37
+ artifact_id: str | None
38
+ call_id: str
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class _Raw:
43
+ tool: str
44
+ args: dict[str, Any]
45
+ result: str
46
+ artifact_id: str | None
47
+ call_id: str
48
+
49
+
50
+ def _flatten(text: str) -> str:
51
+ return " ".join(text.split())
52
+
53
+
54
+ def _cap(text: str, limit: int) -> str:
55
+ text = _flatten(text)
56
+ return text if len(text) <= limit else text[:limit - 1] + "…"
57
+
58
+
59
+ def _describe_args(name: str, args: dict[str, Any]) -> str:
60
+ try:
61
+ described = format.describe_call(name, args)
62
+ except Exception:
63
+ described = ""
64
+ if described.startswith(name):
65
+ described = described[len(name):].strip()
66
+ if not described:
67
+ described = json.dumps(args, default=str)
68
+ return _flatten(described)
69
+
70
+
71
+ def _tool_calls_by_id(history: list[Message]) -> dict[str, tuple[str, str]]:
72
+ out: dict[str, tuple[str, str]] = {}
73
+ for m in history:
74
+ if m.get("role") != "assistant":
75
+ continue
76
+ for tc in m.get("tool_calls") or []:
77
+ fn = tc.get("function") or {}
78
+ out[tc["id"]] = (fn.get("name", ""), fn.get("arguments", "{}"))
79
+ return out
80
+
81
+
82
+ def _raw_entries(history: list[Message]) -> list[_Raw]:
83
+ calls = _tool_calls_by_id(history)
84
+ out: list[_Raw] = []
85
+ for m in history:
86
+ if m.get("role") != "tool":
87
+ continue
88
+ call_id = str(m.get("tool_call_id") or "")
89
+ name, args_json = calls.get(call_id, ("", "{}"))
90
+ if not name:
91
+ continue
92
+ try:
93
+ args = dict(json.loads(args_json or "{}"))
94
+ except json.JSONDecodeError:
95
+ args = {}
96
+ text = str(m.get("content") or "")
97
+ _offloaded, artifact_id = tools.offload_info(text)
98
+ out.append(_Raw(tool=name, args=args, result=text,
99
+ artifact_id=artifact_id, call_id=call_id))
100
+ return out
101
+
102
+
103
+ def build(history: list[Message]) -> list[Entry]:
104
+ """The last `MAX_ENTRIES` tool calls this session, oldest dropped."""
105
+ raws = _raw_entries(history)[-MAX_ENTRIES:]
106
+ return [Entry(tool=r.tool,
107
+ args_digest=_cap(_describe_args(r.tool, r.args), ARGS_DIGEST_CHARS),
108
+ summary=_cap(r.result, SUMMARY_CHARS),
109
+ artifact_id=r.artifact_id, call_id=r.call_id)
110
+ for r in raws]
111
+
112
+
113
+ def _line(tool: str, args_text: str, summary_text: str, artifact_id: str | None) -> str:
114
+ suffix = f" [artifact {artifact_id}]" if artifact_id else ""
115
+ return f"{tool}({args_text}) → {summary_text}{suffix}"
116
+
117
+
118
+ def render(history: list[Message]) -> str:
119
+ """`## Recent actions` block for the volatile half of the system prompt.
120
+ Empty string when there is nothing to show yet."""
121
+ entries = build(history)[-ACTION_LINE_CAP:]
122
+ if not entries:
123
+ return ""
124
+ header = "## Recent actions\n"
125
+ footer = "\n\n" + _DEDUPE_NOTE
126
+ lines = [f"- {_line(e.tool, e.args_digest, e.summary, e.artifact_id)}" for e in entries]
127
+ # Oldest-first drop keeps the most recent (most relevant) actions when the
128
+ # block would otherwise blow the char budget.
129
+ while lines and len(header) + len("\n".join(lines)) + len(footer) > BLOCK_MAX_CHARS:
130
+ lines.pop(0)
131
+ if not lines:
132
+ return ""
133
+ return header + "\n".join(lines) + footer
134
+
135
+
136
+ def compaction_lines(history: list[Message], entry_char_cap: int) -> list[str]:
137
+ """Every tool call in `history` (typically the dropped range of a
138
+ compaction), one deterministic line each -- no LLM involved, so it cannot
139
+ hallucinate. Unlike `render()`, nothing here is capped to the live
140
+ ledger's tight per-field limits; only the whole line is capped."""
141
+ lines = []
142
+ for r in _raw_entries(history):
143
+ args_text = _describe_args(r.tool, r.args)
144
+ line = _line(r.tool, args_text, _flatten(r.result), r.artifact_id)
145
+ lines.append(_cap(line, entry_char_cap) if len(line) > entry_char_cap else line)
146
+ return lines
omega/ui/__init__.py ADDED
File without changes
omega/ui/composer.py ADDED
@@ -0,0 +1,140 @@
1
+ """Shared composer helpers for the TUI (and any other Python front-end).
2
+
3
+ Mirrors the Swift `ComposerPrompt` helpers: `@mention` parsing, workspace
4
+ file ranking, and flattening dropped paths into the text-only prompt the
5
+ daemon accepts.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ _TOKEN = re.compile(r"[A-Za-z0-9._\-++/\\]")
15
+ _SKIP_DIRS = {
16
+ ".git", ".hg", ".svn",
17
+ "node_modules", ".build", "DerivedData", ".swiftpm",
18
+ "dist", "build", "Pods", "Carthage",
19
+ ".venv", "venv", "__pycache__", ".mypy_cache", ".pytest_cache",
20
+ ".next", ".nuxt", "target", ".omega", ".idea", ".vscode",
21
+ "xcuserdata",
22
+ }
23
+ _MAX_FILES = 8_000
24
+ _MAX_DEPTH = 10
25
+ _IMAGE_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".tif", ".tiff", ".heic", ".bmp"}
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class MentionQuery:
30
+ query: str
31
+ start: int # character offset of '@'
32
+ end: int # character offset of the cursor (exclusive of the token)
33
+
34
+
35
+ def mention_query(text: str, cursor: int) -> MentionQuery | None:
36
+ """If `cursor` sits inside an `@token` at a word boundary, return it."""
37
+ if not text:
38
+ return None
39
+ cursor = max(0, min(cursor, len(text)))
40
+ start = cursor
41
+ while start > 0 and _TOKEN.match(text[start - 1]):
42
+ start -= 1
43
+ if start == 0 or text[start - 1] != "@":
44
+ return None
45
+ at = start - 1
46
+ if at > 0 and not text[at - 1].isspace():
47
+ return None
48
+ return MentionQuery(query=text[start:cursor], start=at, end=cursor)
49
+
50
+
51
+ def rank_files(files: list[str], query: str, limit: int = 12) -> list[str]:
52
+ q = query.strip().lower()
53
+ if not q:
54
+ return sorted(files, key=lambda p: (p.count("/"), p.lower()))[:limit]
55
+
56
+ hits: list[tuple[int, int, str, str]] = []
57
+ for path in files:
58
+ filename = path.rsplit("/", 1)[-1]
59
+ path_l, file_l = path.lower(), filename.lower()
60
+ if file_l == q:
61
+ score = 0
62
+ elif file_l.startswith(q):
63
+ score = 1
64
+ elif path_l.startswith(q):
65
+ score = 2
66
+ elif q in file_l:
67
+ score = 3
68
+ elif q in path_l:
69
+ score = 4
70
+ else:
71
+ continue
72
+ hits.append((score, len(filename), file_l, path))
73
+ hits.sort()
74
+ return [h[3] for h in hits[:limit]]
75
+
76
+
77
+ def insert_mention(text: str, cursor: int, path: str) -> tuple[str, int]:
78
+ """Replace the active `@token` (or append) with `` `path` ``. Returns (text, cursor)."""
79
+ q = mention_query(text, cursor)
80
+ replacement = f"`{path}` "
81
+ if q is None:
82
+ new = text + replacement
83
+ return new, len(new)
84
+ new = text[:q.start] + replacement + text[q.end:]
85
+ return new, q.start + len(replacement)
86
+
87
+
88
+ def build_prompt(text: str, attachment_paths: list[str]) -> str:
89
+ trimmed = text.strip()
90
+ paths = [p.strip() for p in attachment_paths if p.strip()]
91
+ if not paths:
92
+ return trimmed
93
+ lines: list[str] = []
94
+ if trimmed:
95
+ lines.append(trimmed)
96
+ lines.append("")
97
+ lines.append("Attached:")
98
+ lines.extend(f"- {p}" for p in paths)
99
+ return "\n".join(lines)
100
+
101
+
102
+ def display_path(path: str, workspace_root: str | None) -> str:
103
+ if not workspace_root:
104
+ return path
105
+ root = os.path.realpath(workspace_root)
106
+ file = os.path.realpath(path)
107
+ if file == root:
108
+ return "."
109
+ prefix = root if root.endswith(os.sep) else root + os.sep
110
+ if file.startswith(prefix):
111
+ return file[len(prefix):]
112
+ return path
113
+
114
+
115
+ def is_image_path(path: str) -> bool:
116
+ return Path(path).suffix.lower() in _IMAGE_EXT
117
+
118
+
119
+ def list_workspace_files(root: str) -> list[str]:
120
+ root_path = Path(root).expanduser().resolve()
121
+ if not root_path.is_dir():
122
+ return []
123
+ files: list[str] = []
124
+ root_s = str(root_path)
125
+ prefix = root_s if root_s.endswith(os.sep) else root_s + os.sep
126
+ for dirpath, dirnames, filenames in os.walk(root_path):
127
+ rel_dir = dirpath[len(prefix):] if dirpath.startswith(prefix) else ""
128
+ depth = 0 if not rel_dir else rel_dir.count(os.sep) + 1
129
+ if depth > _MAX_DEPTH:
130
+ dirnames[:] = []
131
+ continue
132
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS and not d.startswith(".")]
133
+ for name in filenames:
134
+ if name.startswith("."):
135
+ continue
136
+ rel = name if not rel_dir else f"{rel_dir}/{name}".replace("\\", "/")
137
+ files.append(rel)
138
+ if len(files) >= _MAX_FILES:
139
+ return files
140
+ return files