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/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)
|
omega/server/manager.py
ADDED
|
@@ -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
|