trinity-lite 0.1.1__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.
- trinity_lite/__init__.py +3 -0
- trinity_lite/__main__.py +4 -0
- trinity_lite/adapters.py +136 -0
- trinity_lite/bus.py +229 -0
- trinity_lite/cli.py +178 -0
- trinity_lite/config.py +16 -0
- trinity_lite/doctor.py +182 -0
- trinity_lite/guard.py +111 -0
- trinity_lite/orchestrator.py +97 -0
- trinity_lite/paths.py +30 -0
- trinity_lite/router.py +134 -0
- trinity_lite/validation.py +152 -0
- trinity_lite/worker.py +42 -0
- trinity_lite-0.1.1.dist-info/METADATA +254 -0
- trinity_lite-0.1.1.dist-info/RECORD +19 -0
- trinity_lite-0.1.1.dist-info/WHEEL +5 -0
- trinity_lite-0.1.1.dist-info/entry_points.txt +2 -0
- trinity_lite-0.1.1.dist-info/licenses/LICENSE +21 -0
- trinity_lite-0.1.1.dist-info/top_level.txt +1 -0
trinity_lite/__init__.py
ADDED
trinity_lite/__main__.py
ADDED
trinity_lite/adapters.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Agent adapters for Trinity Lite workers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .config import string_list
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AgentSpec:
|
|
18
|
+
agent_id: str
|
|
19
|
+
mode: str = "mock"
|
|
20
|
+
command: list[str] | None = None
|
|
21
|
+
timeout: int = 1800
|
|
22
|
+
roles: list[str] | None = None
|
|
23
|
+
capabilities: list[str] | None = None
|
|
24
|
+
priority: int = 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AdapterError(RuntimeError):
|
|
28
|
+
"""Raised when an agent adapter fails."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseAdapter:
|
|
32
|
+
def __init__(self, spec: AgentSpec) -> None:
|
|
33
|
+
self.spec = spec
|
|
34
|
+
|
|
35
|
+
def run(self, task: dict[str, Any]) -> str:
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MockAdapter(BaseAdapter):
|
|
40
|
+
def run(self, task: dict[str, Any]) -> str:
|
|
41
|
+
prompt = task["prompt"].strip().splitlines()[0][:120]
|
|
42
|
+
return (
|
|
43
|
+
f"[mock:{self.spec.agent_id}] completed task {task['id']} "
|
|
44
|
+
f"({task.get('task_type') or 'unspecified'}): {prompt}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CommandAdapter(BaseAdapter):
|
|
49
|
+
def run(self, task: dict[str, Any]) -> str:
|
|
50
|
+
if not self.spec.command:
|
|
51
|
+
raise AdapterError(f"agent {self.spec.agent_id} has no command")
|
|
52
|
+
command = [self._format_arg(arg, task) for arg in self.spec.command]
|
|
53
|
+
executable = command[0]
|
|
54
|
+
if shutil.which(executable) is None and not Path(executable).exists():
|
|
55
|
+
raise AdapterError(f"executable not found for {self.spec.agent_id}: {executable}")
|
|
56
|
+
uses_prompt_placeholder = any("{prompt}" in arg for arg in self.spec.command)
|
|
57
|
+
completed = subprocess.run(
|
|
58
|
+
command,
|
|
59
|
+
cwd=task["cwd"],
|
|
60
|
+
input=None if uses_prompt_placeholder else task["prompt"],
|
|
61
|
+
text=True,
|
|
62
|
+
capture_output=True,
|
|
63
|
+
timeout=self.spec.timeout,
|
|
64
|
+
shell=False,
|
|
65
|
+
check=False,
|
|
66
|
+
)
|
|
67
|
+
output = (completed.stdout or "").strip()
|
|
68
|
+
stderr = (completed.stderr or "").strip()
|
|
69
|
+
if completed.returncode != 0:
|
|
70
|
+
detail = stderr or output or f"exit code {completed.returncode}"
|
|
71
|
+
raise AdapterError(f"{self.spec.agent_id} failed: {detail}")
|
|
72
|
+
return output or stderr or f"{self.spec.agent_id} completed without output"
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _format_arg(arg: str, task: dict[str, Any]) -> str:
|
|
76
|
+
return (
|
|
77
|
+
arg.replace("{prompt}", task["prompt"])
|
|
78
|
+
.replace("{cwd}", task["cwd"])
|
|
79
|
+
.replace("{task_id}", task["id"])
|
|
80
|
+
.replace("{task_type}", task.get("task_type") or "")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def default_specs() -> dict[str, AgentSpec]:
|
|
85
|
+
return {
|
|
86
|
+
"codex": AgentSpec(
|
|
87
|
+
agent_id="codex",
|
|
88
|
+
roles=["primary_engineer"],
|
|
89
|
+
capabilities=[
|
|
90
|
+
"architecture_design",
|
|
91
|
+
"code_edit",
|
|
92
|
+
"documentation",
|
|
93
|
+
"project_audit",
|
|
94
|
+
"test_run",
|
|
95
|
+
],
|
|
96
|
+
priority=80,
|
|
97
|
+
),
|
|
98
|
+
"claude_code": AgentSpec(
|
|
99
|
+
agent_id="claude_code",
|
|
100
|
+
roles=["reviewer"],
|
|
101
|
+
capabilities=["code_review", "risk_check", "source_scan"],
|
|
102
|
+
priority=70,
|
|
103
|
+
),
|
|
104
|
+
"hermes": AgentSpec(
|
|
105
|
+
agent_id="hermes",
|
|
106
|
+
roles=["orchestrator", "acceptance"],
|
|
107
|
+
capabilities=["acceptance", "orchestration", "verification"],
|
|
108
|
+
priority=60,
|
|
109
|
+
),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_specs(path: str | os.PathLike[str] | None = None) -> dict[str, AgentSpec]:
|
|
114
|
+
if path is None:
|
|
115
|
+
return default_specs()
|
|
116
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
117
|
+
specs: dict[str, AgentSpec] = {}
|
|
118
|
+
for agent_id, raw in data.get("agents", {}).items():
|
|
119
|
+
specs[agent_id] = AgentSpec(
|
|
120
|
+
agent_id=agent_id,
|
|
121
|
+
mode=raw.get("mode", "mock"),
|
|
122
|
+
command=raw.get("command"),
|
|
123
|
+
timeout=int(raw.get("timeout", 1800)),
|
|
124
|
+
roles=string_list(raw.get("roles")),
|
|
125
|
+
capabilities=string_list(raw.get("capabilities")),
|
|
126
|
+
priority=int(raw.get("priority", 0)),
|
|
127
|
+
)
|
|
128
|
+
return specs or default_specs()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_adapter(spec: AgentSpec) -> BaseAdapter:
|
|
132
|
+
if spec.mode == "mock":
|
|
133
|
+
return MockAdapter(spec)
|
|
134
|
+
if spec.mode == "command":
|
|
135
|
+
return CommandAdapter(spec)
|
|
136
|
+
raise AdapterError(f"unknown adapter mode for {spec.agent_id}: {spec.mode}")
|
trinity_lite/bus.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""SQLite task and message bus for Trinity Lite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .guard import GuardError, ensure_inside_roots
|
|
13
|
+
from .paths import default_allowed_roots, default_db_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MAX_DEPTH = 2
|
|
17
|
+
TERMINAL_STATUSES = {"completed", "failed", "cancelled"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def utc_now() -> str:
|
|
21
|
+
return datetime.now(timezone.utc).isoformat()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TrinityBus:
|
|
25
|
+
"""Persistent task bus backed by SQLite."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
db_path: str | os.PathLike[str] | None = None,
|
|
30
|
+
allowed_roots: list[Path] | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.db_path = Path(db_path).expanduser() if db_path else default_db_path()
|
|
33
|
+
self.allowed_roots = allowed_roots or default_allowed_roots()
|
|
34
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
self._init_db()
|
|
36
|
+
|
|
37
|
+
def connect(self) -> sqlite3.Connection:
|
|
38
|
+
conn = sqlite3.connect(self.db_path)
|
|
39
|
+
conn.row_factory = sqlite3.Row
|
|
40
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
41
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
42
|
+
return conn
|
|
43
|
+
|
|
44
|
+
def _init_db(self) -> None:
|
|
45
|
+
with self.connect() as conn:
|
|
46
|
+
conn.execute(
|
|
47
|
+
"""
|
|
48
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
source_agent TEXT NOT NULL,
|
|
51
|
+
target_agent TEXT NOT NULL,
|
|
52
|
+
task_type TEXT,
|
|
53
|
+
prompt TEXT NOT NULL,
|
|
54
|
+
cwd TEXT NOT NULL,
|
|
55
|
+
status TEXT NOT NULL,
|
|
56
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
result TEXT,
|
|
58
|
+
error TEXT,
|
|
59
|
+
created_at TEXT NOT NULL,
|
|
60
|
+
started_at TEXT,
|
|
61
|
+
finished_at TEXT,
|
|
62
|
+
heartbeat_at TEXT
|
|
63
|
+
)
|
|
64
|
+
"""
|
|
65
|
+
)
|
|
66
|
+
conn.execute(
|
|
67
|
+
"""
|
|
68
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
source_agent TEXT NOT NULL,
|
|
71
|
+
target_agent TEXT NOT NULL,
|
|
72
|
+
task_id TEXT,
|
|
73
|
+
message TEXT NOT NULL,
|
|
74
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
created_at TEXT NOT NULL
|
|
76
|
+
)
|
|
77
|
+
"""
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def submit_task(
|
|
81
|
+
self,
|
|
82
|
+
source_agent: str,
|
|
83
|
+
target_agent: str,
|
|
84
|
+
prompt: str,
|
|
85
|
+
task_type: str | None = None,
|
|
86
|
+
cwd: str | os.PathLike[str] | None = None,
|
|
87
|
+
depth: int = 0,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
if source_agent == target_agent:
|
|
90
|
+
raise GuardError("self-delegation is not allowed")
|
|
91
|
+
if depth > MAX_DEPTH:
|
|
92
|
+
raise GuardError(f"delegation depth exceeds max depth {MAX_DEPTH}")
|
|
93
|
+
workdir = ensure_inside_roots(cwd or os.getcwd(), self.allowed_roots)
|
|
94
|
+
task_id = uuid.uuid4().hex[:12]
|
|
95
|
+
now = utc_now()
|
|
96
|
+
with self.connect() as conn:
|
|
97
|
+
conn.execute(
|
|
98
|
+
"""
|
|
99
|
+
INSERT INTO tasks (
|
|
100
|
+
id, source_agent, target_agent, task_type, prompt, cwd,
|
|
101
|
+
status, depth, created_at, heartbeat_at
|
|
102
|
+
)
|
|
103
|
+
VALUES (?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?)
|
|
104
|
+
""",
|
|
105
|
+
(
|
|
106
|
+
task_id,
|
|
107
|
+
source_agent,
|
|
108
|
+
target_agent,
|
|
109
|
+
task_type,
|
|
110
|
+
prompt,
|
|
111
|
+
str(workdir),
|
|
112
|
+
depth,
|
|
113
|
+
now,
|
|
114
|
+
now,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
return self.get_task(task_id)
|
|
118
|
+
|
|
119
|
+
def get_task(self, task_id: str) -> dict[str, Any]:
|
|
120
|
+
with self.connect() as conn:
|
|
121
|
+
row = conn.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
|
|
122
|
+
if row is None:
|
|
123
|
+
raise KeyError(f"task not found: {task_id}")
|
|
124
|
+
return dict(row)
|
|
125
|
+
|
|
126
|
+
def list_tasks(self, agent: str | None = None, limit: int = 20) -> list[dict[str, Any]]:
|
|
127
|
+
sql = "SELECT * FROM tasks"
|
|
128
|
+
params: list[Any] = []
|
|
129
|
+
if agent:
|
|
130
|
+
sql += " WHERE source_agent = ? OR target_agent = ?"
|
|
131
|
+
params.extend([agent, agent])
|
|
132
|
+
sql += " ORDER BY created_at DESC LIMIT ?"
|
|
133
|
+
params.append(limit)
|
|
134
|
+
with self.connect() as conn:
|
|
135
|
+
rows = conn.execute(sql, params).fetchall()
|
|
136
|
+
return [dict(r) for r in rows]
|
|
137
|
+
|
|
138
|
+
def task_for_worker(self, target_agent: str) -> dict[str, Any] | None:
|
|
139
|
+
now = utc_now()
|
|
140
|
+
with self.connect() as conn:
|
|
141
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
142
|
+
row = conn.execute(
|
|
143
|
+
"""
|
|
144
|
+
SELECT * FROM tasks
|
|
145
|
+
WHERE target_agent = ? AND status = 'queued'
|
|
146
|
+
ORDER BY created_at ASC
|
|
147
|
+
LIMIT 1
|
|
148
|
+
""",
|
|
149
|
+
(target_agent,),
|
|
150
|
+
).fetchone()
|
|
151
|
+
if row is None:
|
|
152
|
+
conn.commit()
|
|
153
|
+
return None
|
|
154
|
+
conn.execute(
|
|
155
|
+
"""
|
|
156
|
+
UPDATE tasks
|
|
157
|
+
SET status = 'running', started_at = ?, heartbeat_at = ?
|
|
158
|
+
WHERE id = ? AND status = 'queued'
|
|
159
|
+
""",
|
|
160
|
+
(now, now, row["id"]),
|
|
161
|
+
)
|
|
162
|
+
conn.commit()
|
|
163
|
+
return self.get_task(row["id"])
|
|
164
|
+
|
|
165
|
+
def finish_worker(
|
|
166
|
+
self,
|
|
167
|
+
task_id: str,
|
|
168
|
+
result: str | None = None,
|
|
169
|
+
error: str | None = None,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
status = "failed" if error else "completed"
|
|
172
|
+
now = utc_now()
|
|
173
|
+
with self.connect() as conn:
|
|
174
|
+
conn.execute(
|
|
175
|
+
"""
|
|
176
|
+
UPDATE tasks
|
|
177
|
+
SET status = ?, result = ?, error = ?, finished_at = ?, heartbeat_at = ?
|
|
178
|
+
WHERE id = ?
|
|
179
|
+
""",
|
|
180
|
+
(status, result, error, now, now, task_id),
|
|
181
|
+
)
|
|
182
|
+
return self.get_task(task_id)
|
|
183
|
+
|
|
184
|
+
def send_message(
|
|
185
|
+
self,
|
|
186
|
+
source_agent: str,
|
|
187
|
+
target_agent: str,
|
|
188
|
+
message: str,
|
|
189
|
+
task_id: str | None = None,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
if source_agent == target_agent:
|
|
192
|
+
raise GuardError("self-messaging is not allowed")
|
|
193
|
+
msg_id = uuid.uuid4().hex[:12]
|
|
194
|
+
with self.connect() as conn:
|
|
195
|
+
conn.execute(
|
|
196
|
+
"""
|
|
197
|
+
INSERT INTO messages (
|
|
198
|
+
id, source_agent, target_agent, task_id, message, read, created_at
|
|
199
|
+
)
|
|
200
|
+
VALUES (?, ?, ?, ?, ?, 0, ?)
|
|
201
|
+
""",
|
|
202
|
+
(msg_id, source_agent, target_agent, task_id, message, utc_now()),
|
|
203
|
+
)
|
|
204
|
+
row = conn.execute("SELECT * FROM messages WHERE id = ?", (msg_id,)).fetchone()
|
|
205
|
+
return dict(row)
|
|
206
|
+
|
|
207
|
+
def inbox(
|
|
208
|
+
self,
|
|
209
|
+
agent: str,
|
|
210
|
+
unread_only: bool = True,
|
|
211
|
+
mark_read: bool = False,
|
|
212
|
+
limit: int = 20,
|
|
213
|
+
) -> list[dict[str, Any]]:
|
|
214
|
+
sql = "SELECT * FROM messages WHERE target_agent = ?"
|
|
215
|
+
params: list[Any] = [agent]
|
|
216
|
+
if unread_only:
|
|
217
|
+
sql += " AND read = 0"
|
|
218
|
+
sql += " ORDER BY created_at DESC LIMIT ?"
|
|
219
|
+
params.append(limit)
|
|
220
|
+
with self.connect() as conn:
|
|
221
|
+
rows = conn.execute(sql, params).fetchall()
|
|
222
|
+
messages = [dict(r) for r in rows]
|
|
223
|
+
if mark_read and messages:
|
|
224
|
+
for message in messages:
|
|
225
|
+
conn.execute(
|
|
226
|
+
"UPDATE messages SET read = 1 WHERE id = ?",
|
|
227
|
+
(message["id"],),
|
|
228
|
+
)
|
|
229
|
+
return messages
|
trinity_lite/cli.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Command line interface for Trinity Lite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .bus import TrinityBus
|
|
11
|
+
from .doctor import run_doctor
|
|
12
|
+
from .orchestrator import run_review_flow
|
|
13
|
+
from .router import resolve_route
|
|
14
|
+
from .worker import run_loop, run_once
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def print_json(data: Any) -> None:
|
|
18
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
22
|
+
parser = argparse.ArgumentParser(prog="trinity-lite")
|
|
23
|
+
parser.add_argument("--db", default=None, help="SQLite database path")
|
|
24
|
+
parser.add_argument("--routes", default=None, help="routes JSON path")
|
|
25
|
+
parser.add_argument("--agents", default=None, help="agents JSON path")
|
|
26
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
27
|
+
common.add_argument("--db", default=argparse.SUPPRESS, help="SQLite database path")
|
|
28
|
+
common.add_argument("--routes", default=argparse.SUPPRESS, help="routes JSON path")
|
|
29
|
+
common.add_argument("--agents", default=argparse.SUPPRESS, help="agents JSON path")
|
|
30
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
31
|
+
|
|
32
|
+
route = sub.add_parser("route", parents=[common], help="resolve a route without dispatching")
|
|
33
|
+
route.add_argument("task")
|
|
34
|
+
route.add_argument("--type", dest="task_type")
|
|
35
|
+
route.add_argument("--previous-agent")
|
|
36
|
+
|
|
37
|
+
dispatch = sub.add_parser("dispatch", parents=[common], help="dispatch to an explicit agent")
|
|
38
|
+
dispatch.add_argument("target_agent")
|
|
39
|
+
dispatch.add_argument("task")
|
|
40
|
+
dispatch.add_argument("--source", default="user")
|
|
41
|
+
dispatch.add_argument("--type", dest="task_type")
|
|
42
|
+
dispatch.add_argument("--cwd", default=os.getcwd())
|
|
43
|
+
|
|
44
|
+
auto = sub.add_parser("dispatch-auto", parents=[common], help="resolve route then dispatch")
|
|
45
|
+
auto.add_argument("task")
|
|
46
|
+
auto.add_argument("--source", default="user")
|
|
47
|
+
auto.add_argument("--type", dest="task_type")
|
|
48
|
+
auto.add_argument("--previous-agent")
|
|
49
|
+
auto.add_argument("--cwd", default=os.getcwd())
|
|
50
|
+
|
|
51
|
+
orchestrate = sub.add_parser("orchestrate", parents=[common], help="dispatch and run a primary task with optional review")
|
|
52
|
+
orchestrate.add_argument("task")
|
|
53
|
+
orchestrate.add_argument("--source", default="user")
|
|
54
|
+
orchestrate.add_argument("--type", dest="task_type")
|
|
55
|
+
orchestrate.add_argument("--previous-agent")
|
|
56
|
+
orchestrate.add_argument("--cwd", default=os.getcwd())
|
|
57
|
+
orchestrate.add_argument("--no-run", action="store_true", help="dispatch the primary task without running workers")
|
|
58
|
+
|
|
59
|
+
status = sub.add_parser("status", parents=[common], help="show task status")
|
|
60
|
+
status.add_argument("task_id")
|
|
61
|
+
|
|
62
|
+
tasks = sub.add_parser("tasks", parents=[common], help="list recent tasks")
|
|
63
|
+
tasks.add_argument("--agent")
|
|
64
|
+
tasks.add_argument("--limit", type=int, default=20)
|
|
65
|
+
|
|
66
|
+
worker = sub.add_parser("worker", parents=[common], help="run a worker for one agent")
|
|
67
|
+
worker.add_argument("agent")
|
|
68
|
+
worker.add_argument("--once", action="store_true")
|
|
69
|
+
worker.add_argument("--poll", type=float, default=2.0)
|
|
70
|
+
|
|
71
|
+
send = sub.add_parser("send", parents=[common], help="send a durable message")
|
|
72
|
+
send.add_argument("target_agent")
|
|
73
|
+
send.add_argument("message")
|
|
74
|
+
send.add_argument("--source", default="user")
|
|
75
|
+
send.add_argument("--task-id")
|
|
76
|
+
|
|
77
|
+
inbox = sub.add_parser("inbox", parents=[common], help="read durable messages")
|
|
78
|
+
inbox.add_argument("agent")
|
|
79
|
+
inbox.add_argument("--all", action="store_true")
|
|
80
|
+
inbox.add_argument("--mark-read", action="store_true")
|
|
81
|
+
inbox.add_argument("--limit", type=int, default=20)
|
|
82
|
+
|
|
83
|
+
doctor = sub.add_parser("doctor", parents=[common], help="run environment checks")
|
|
84
|
+
doctor.add_argument("--scan-root")
|
|
85
|
+
doctor.add_argument("--runtime-root", help="runtime state directory for hygiene checks")
|
|
86
|
+
doctor.add_argument(
|
|
87
|
+
"--retired-port",
|
|
88
|
+
action="append",
|
|
89
|
+
type=int,
|
|
90
|
+
default=[],
|
|
91
|
+
help="TCP port that should not be listening, repeatable",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return parser
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main(argv: list[str] | None = None) -> int:
|
|
98
|
+
args = build_parser().parse_args(argv)
|
|
99
|
+
try:
|
|
100
|
+
return run_command(args)
|
|
101
|
+
except (KeyError, OSError, ValueError) as exc:
|
|
102
|
+
print_json({"error": str(exc)})
|
|
103
|
+
return 2
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_command(args: argparse.Namespace) -> int:
|
|
107
|
+
bus = TrinityBus(args.db)
|
|
108
|
+
|
|
109
|
+
if args.command == "route":
|
|
110
|
+
print_json(resolve_route(args.task, args.task_type, args.previous_agent, args.routes, args.agents))
|
|
111
|
+
return 0
|
|
112
|
+
if args.command == "dispatch":
|
|
113
|
+
print_json(bus.submit_task(
|
|
114
|
+
source_agent=args.source,
|
|
115
|
+
target_agent=args.target_agent,
|
|
116
|
+
prompt=args.task,
|
|
117
|
+
task_type=args.task_type,
|
|
118
|
+
cwd=args.cwd,
|
|
119
|
+
))
|
|
120
|
+
return 0
|
|
121
|
+
if args.command == "dispatch-auto":
|
|
122
|
+
route = resolve_route(args.task, args.task_type, args.previous_agent, args.routes, args.agents)
|
|
123
|
+
task = bus.submit_task(
|
|
124
|
+
source_agent=args.source,
|
|
125
|
+
target_agent=route["agent"],
|
|
126
|
+
prompt=args.task,
|
|
127
|
+
task_type=route["task_type"],
|
|
128
|
+
cwd=args.cwd,
|
|
129
|
+
)
|
|
130
|
+
task["route"] = route
|
|
131
|
+
print_json(task)
|
|
132
|
+
return 0
|
|
133
|
+
if args.command == "orchestrate":
|
|
134
|
+
print_json(run_review_flow(
|
|
135
|
+
args.task,
|
|
136
|
+
bus,
|
|
137
|
+
args.routes,
|
|
138
|
+
args.agents,
|
|
139
|
+
args.source,
|
|
140
|
+
args.task_type,
|
|
141
|
+
args.previous_agent,
|
|
142
|
+
args.cwd,
|
|
143
|
+
run_workers=not args.no_run,
|
|
144
|
+
))
|
|
145
|
+
return 0
|
|
146
|
+
if args.command == "status":
|
|
147
|
+
print_json(bus.get_task(args.task_id))
|
|
148
|
+
return 0
|
|
149
|
+
if args.command == "tasks":
|
|
150
|
+
print_json(bus.list_tasks(args.agent, args.limit))
|
|
151
|
+
return 0
|
|
152
|
+
if args.command == "worker":
|
|
153
|
+
if args.once:
|
|
154
|
+
print_json(run_once(args.agent, bus, args.agents))
|
|
155
|
+
return 0
|
|
156
|
+
run_loop(args.agent, bus, args.agents, args.poll)
|
|
157
|
+
return 0
|
|
158
|
+
if args.command == "send":
|
|
159
|
+
print_json(bus.send_message(args.source, args.target_agent, args.message, args.task_id))
|
|
160
|
+
return 0
|
|
161
|
+
if args.command == "inbox":
|
|
162
|
+
print_json(bus.inbox(args.agent, unread_only=not args.all, mark_read=args.mark_read, limit=args.limit))
|
|
163
|
+
return 0
|
|
164
|
+
if args.command == "doctor":
|
|
165
|
+
print_json(run_doctor(
|
|
166
|
+
args.db,
|
|
167
|
+
args.routes,
|
|
168
|
+
args.agents,
|
|
169
|
+
args.scan_root,
|
|
170
|
+
args.runtime_root,
|
|
171
|
+
args.retired_port,
|
|
172
|
+
))
|
|
173
|
+
return 0
|
|
174
|
+
raise AssertionError(f"unhandled command: {args.command}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
raise SystemExit(main())
|
trinity_lite/config.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Configuration normalization helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def string_list(value: Any) -> list[str]:
|
|
9
|
+
"""Return a string list from a string, list, or absent value."""
|
|
10
|
+
if value is None:
|
|
11
|
+
return []
|
|
12
|
+
if isinstance(value, str):
|
|
13
|
+
return [value]
|
|
14
|
+
if isinstance(value, list):
|
|
15
|
+
return [str(item) for item in value]
|
|
16
|
+
raise ValueError(f"expected string or list of strings, got {type(value).__name__}")
|