@team-agent/installer 0.1.0
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.
- package/README.md +201 -0
- package/crates/team-agent-core/Cargo.toml +12 -0
- package/crates/team-agent-core/src/lib.rs +287 -0
- package/crates/team-agent-core/src/main.rs +152 -0
- package/examples/team.spec.yaml +206 -0
- package/examples/team_state.md +35 -0
- package/npm/install.mjs +266 -0
- package/package.json +28 -0
- package/pyproject.toml +18 -0
- package/schemas/result-envelope.schema.json +76 -0
- package/schemas/team.schema.json +241 -0
- package/scripts/install.py +88 -0
- package/scripts/run_regression_tests.py +79 -0
- package/skills/team-agent/SKILL.md +173 -0
- package/src/team_agent/__init__.py +3 -0
- package/src/team_agent/__main__.py +5 -0
- package/src/team_agent/cli.py +857 -0
- package/src/team_agent/compiler.py +269 -0
- package/src/team_agent/coordinator.py +62 -0
- package/src/team_agent/errors.py +10 -0
- package/src/team_agent/events.py +37 -0
- package/src/team_agent/fake_worker.py +80 -0
- package/src/team_agent/mcp_server.py +579 -0
- package/src/team_agent/message_store.py +497 -0
- package/src/team_agent/paths.py +45 -0
- package/src/team_agent/permissions.py +123 -0
- package/src/team_agent/profiles.py +882 -0
- package/src/team_agent/providers.py +1045 -0
- package/src/team_agent/routing.py +84 -0
- package/src/team_agent/runtime.py +5213 -0
- package/src/team_agent/rust_core.py +156 -0
- package/src/team_agent/simple_yaml.py +236 -0
- package/src/team_agent/spec.py +308 -0
- package/src/team_agent/state.py +112 -0
- package/src/team_agent/task_graph.py +80 -0
- package/templates/team_state.md +32 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import uuid
|
|
6
|
+
from contextlib import closing
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from team_agent.paths import runtime_dir
|
|
12
|
+
from team_agent.spec import validate_result_envelope
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def utcnow() -> str:
|
|
16
|
+
return datetime.now(timezone.utc).isoformat()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MessageStore:
|
|
20
|
+
def __init__(self, workspace: Path):
|
|
21
|
+
self.workspace = workspace
|
|
22
|
+
self.path = runtime_dir(workspace) / "team.db"
|
|
23
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
self._init()
|
|
25
|
+
|
|
26
|
+
def connect(self) -> sqlite3.Connection:
|
|
27
|
+
conn = sqlite3.connect(self.path)
|
|
28
|
+
conn.row_factory = sqlite3.Row
|
|
29
|
+
return conn
|
|
30
|
+
|
|
31
|
+
def _init(self) -> None:
|
|
32
|
+
with closing(self.connect()) as conn:
|
|
33
|
+
with conn:
|
|
34
|
+
conn.execute(
|
|
35
|
+
"""
|
|
36
|
+
create table if not exists messages (
|
|
37
|
+
message_id text primary key,
|
|
38
|
+
task_id text,
|
|
39
|
+
sender text,
|
|
40
|
+
recipient text,
|
|
41
|
+
reply_to text,
|
|
42
|
+
requires_ack integer,
|
|
43
|
+
status text,
|
|
44
|
+
content text,
|
|
45
|
+
artifact_refs text,
|
|
46
|
+
created_at text,
|
|
47
|
+
updated_at text,
|
|
48
|
+
delivered_at text,
|
|
49
|
+
acknowledged_at text,
|
|
50
|
+
error text
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
)
|
|
54
|
+
conn.execute(
|
|
55
|
+
"""
|
|
56
|
+
create table if not exists results (
|
|
57
|
+
result_id text primary key,
|
|
58
|
+
task_id text not null,
|
|
59
|
+
agent_id text not null,
|
|
60
|
+
envelope text not null,
|
|
61
|
+
status text not null,
|
|
62
|
+
created_at text not null
|
|
63
|
+
)
|
|
64
|
+
"""
|
|
65
|
+
)
|
|
66
|
+
conn.execute(
|
|
67
|
+
"""
|
|
68
|
+
create table if not exists scheduled_events (
|
|
69
|
+
id integer primary key,
|
|
70
|
+
due_at text not null,
|
|
71
|
+
target text not null,
|
|
72
|
+
kind text not null,
|
|
73
|
+
payload_json text not null,
|
|
74
|
+
status text not null,
|
|
75
|
+
created_at text not null,
|
|
76
|
+
fired_at text,
|
|
77
|
+
result_json text
|
|
78
|
+
)
|
|
79
|
+
"""
|
|
80
|
+
)
|
|
81
|
+
conn.execute(
|
|
82
|
+
"""
|
|
83
|
+
create table if not exists delivery_tokens (
|
|
84
|
+
message_id text primary key,
|
|
85
|
+
unique_token text not null,
|
|
86
|
+
injected_at text not null,
|
|
87
|
+
visible_at text,
|
|
88
|
+
consumed_at text,
|
|
89
|
+
failed_at text,
|
|
90
|
+
failure_reason text
|
|
91
|
+
)
|
|
92
|
+
"""
|
|
93
|
+
)
|
|
94
|
+
conn.execute(
|
|
95
|
+
"""
|
|
96
|
+
create table if not exists agent_health (
|
|
97
|
+
agent_id text primary key,
|
|
98
|
+
status text not null,
|
|
99
|
+
last_output_at text,
|
|
100
|
+
context_usage_pct integer,
|
|
101
|
+
current_task_id text,
|
|
102
|
+
updated_at text not null
|
|
103
|
+
)
|
|
104
|
+
"""
|
|
105
|
+
)
|
|
106
|
+
conn.execute(
|
|
107
|
+
"""
|
|
108
|
+
create table if not exists peer_allowlist (
|
|
109
|
+
a text not null,
|
|
110
|
+
b text not null,
|
|
111
|
+
created_at text not null,
|
|
112
|
+
primary key (a, b)
|
|
113
|
+
)
|
|
114
|
+
"""
|
|
115
|
+
)
|
|
116
|
+
conn.execute(
|
|
117
|
+
"""
|
|
118
|
+
create table if not exists result_watchers (
|
|
119
|
+
watcher_id text primary key,
|
|
120
|
+
task_id text,
|
|
121
|
+
agent_id text,
|
|
122
|
+
message_id text,
|
|
123
|
+
leader_id text not null,
|
|
124
|
+
status text not null,
|
|
125
|
+
created_at text not null,
|
|
126
|
+
completed_at text,
|
|
127
|
+
result_id text,
|
|
128
|
+
notified_message_id text,
|
|
129
|
+
error text
|
|
130
|
+
)
|
|
131
|
+
"""
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def create_message(
|
|
135
|
+
self,
|
|
136
|
+
task_id: str | None,
|
|
137
|
+
sender: str,
|
|
138
|
+
recipient: str,
|
|
139
|
+
content: str,
|
|
140
|
+
reply_to: str | None = None,
|
|
141
|
+
requires_ack: bool = True,
|
|
142
|
+
artifact_refs: list[dict[str, Any]] | None = None,
|
|
143
|
+
) -> str:
|
|
144
|
+
message_id = f"msg_{uuid.uuid4().hex[:12]}"
|
|
145
|
+
now = utcnow()
|
|
146
|
+
with closing(self.connect()) as conn:
|
|
147
|
+
with conn:
|
|
148
|
+
conn.execute(
|
|
149
|
+
"""
|
|
150
|
+
insert into messages values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
151
|
+
""",
|
|
152
|
+
(
|
|
153
|
+
message_id,
|
|
154
|
+
task_id,
|
|
155
|
+
sender,
|
|
156
|
+
recipient,
|
|
157
|
+
reply_to,
|
|
158
|
+
int(requires_ack),
|
|
159
|
+
"accepted",
|
|
160
|
+
content,
|
|
161
|
+
json.dumps(artifact_refs or []),
|
|
162
|
+
now,
|
|
163
|
+
now,
|
|
164
|
+
None,
|
|
165
|
+
None,
|
|
166
|
+
None,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
return message_id
|
|
170
|
+
|
|
171
|
+
def mark(self, message_id: str, status: str, error: str | None = None) -> None:
|
|
172
|
+
now = utcnow()
|
|
173
|
+
delivered_at = now if status in {"injected", "visible", "submitted", "submitted_unverified", "delivered"} else None
|
|
174
|
+
acknowledged_at = now if status == "acknowledged" else None
|
|
175
|
+
with closing(self.connect()) as conn:
|
|
176
|
+
with conn:
|
|
177
|
+
conn.execute(
|
|
178
|
+
"""
|
|
179
|
+
update messages
|
|
180
|
+
set status = ?,
|
|
181
|
+
updated_at = ?,
|
|
182
|
+
delivered_at = coalesce(?, delivered_at),
|
|
183
|
+
acknowledged_at = coalesce(?, acknowledged_at),
|
|
184
|
+
error = coalesce(?, error)
|
|
185
|
+
where message_id = ?
|
|
186
|
+
""",
|
|
187
|
+
(status, now, delivered_at, acknowledged_at, error, message_id),
|
|
188
|
+
)
|
|
189
|
+
if status in {"injected", "visible", "submitted", "submitted_unverified", "injected_unverified", "failed"}:
|
|
190
|
+
conn.execute(
|
|
191
|
+
"""
|
|
192
|
+
insert into delivery_tokens(
|
|
193
|
+
message_id, unique_token, injected_at, visible_at, failed_at, failure_reason
|
|
194
|
+
)
|
|
195
|
+
values (?, ?, ?, ?, ?, ?)
|
|
196
|
+
on conflict(message_id) do update set
|
|
197
|
+
visible_at = coalesce(excluded.visible_at, delivery_tokens.visible_at),
|
|
198
|
+
failed_at = coalesce(excluded.failed_at, delivery_tokens.failed_at),
|
|
199
|
+
failure_reason = coalesce(excluded.failure_reason, delivery_tokens.failure_reason)
|
|
200
|
+
""",
|
|
201
|
+
(
|
|
202
|
+
message_id,
|
|
203
|
+
message_id,
|
|
204
|
+
now,
|
|
205
|
+
now if status in {"visible", "submitted"} else None,
|
|
206
|
+
now if status in {"failed", "injected_unverified"} else None,
|
|
207
|
+
error if status in {"failed", "injected_unverified"} else None,
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
if status == "acknowledged":
|
|
211
|
+
conn.execute(
|
|
212
|
+
"update delivery_tokens set consumed_at = coalesce(consumed_at, ?) where message_id = ?",
|
|
213
|
+
(now, message_id),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def claim_for_delivery(self, message_id: str) -> bool:
|
|
217
|
+
now = utcnow()
|
|
218
|
+
with closing(self.connect()) as conn:
|
|
219
|
+
with conn:
|
|
220
|
+
result = conn.execute(
|
|
221
|
+
"""
|
|
222
|
+
update messages
|
|
223
|
+
set status = 'target_resolved',
|
|
224
|
+
updated_at = ?
|
|
225
|
+
where message_id = ?
|
|
226
|
+
and status in ('pending', 'accepted')
|
|
227
|
+
""",
|
|
228
|
+
(now, message_id),
|
|
229
|
+
)
|
|
230
|
+
return result.rowcount == 1
|
|
231
|
+
|
|
232
|
+
def fail_timeouts(self, timeout_sec: int) -> list[str]:
|
|
233
|
+
cutoff = datetime.now(timezone.utc) - timedelta(seconds=timeout_sec)
|
|
234
|
+
failed: list[str] = []
|
|
235
|
+
with closing(self.connect()) as conn:
|
|
236
|
+
with conn:
|
|
237
|
+
rows = conn.execute(
|
|
238
|
+
"select message_id, updated_at from messages where requires_ack = 1 and status in ('pending','accepted','target_resolved','injected','visible','submitted','delivered')"
|
|
239
|
+
).fetchall()
|
|
240
|
+
for row in rows:
|
|
241
|
+
try:
|
|
242
|
+
updated = datetime.fromisoformat(row["updated_at"])
|
|
243
|
+
except ValueError:
|
|
244
|
+
continue
|
|
245
|
+
if updated < cutoff:
|
|
246
|
+
failed.append(row["message_id"])
|
|
247
|
+
conn.execute(
|
|
248
|
+
"update messages set status = ?, updated_at = ?, error = ? where message_id = ?",
|
|
249
|
+
("failed", utcnow(), f"ack timeout after {timeout_sec}s", row["message_id"]),
|
|
250
|
+
)
|
|
251
|
+
return failed
|
|
252
|
+
|
|
253
|
+
def messages(self) -> list[dict[str, Any]]:
|
|
254
|
+
with closing(self.connect()) as conn:
|
|
255
|
+
rows = conn.execute("select * from messages order by created_at").fetchall()
|
|
256
|
+
return [dict(row) for row in rows]
|
|
257
|
+
|
|
258
|
+
def inbox(self, agent_id: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
259
|
+
with closing(self.connect()) as conn:
|
|
260
|
+
rows = conn.execute(
|
|
261
|
+
"""
|
|
262
|
+
select * from messages
|
|
263
|
+
where sender = ? or recipient = ?
|
|
264
|
+
order by created_at desc
|
|
265
|
+
limit ?
|
|
266
|
+
""",
|
|
267
|
+
(agent_id, agent_id, limit),
|
|
268
|
+
).fetchall()
|
|
269
|
+
return [dict(row) for row in reversed(rows)]
|
|
270
|
+
|
|
271
|
+
def delivery_tokens(self) -> list[dict[str, Any]]:
|
|
272
|
+
with closing(self.connect()) as conn:
|
|
273
|
+
rows = conn.execute("select * from delivery_tokens order by injected_at").fetchall()
|
|
274
|
+
return [dict(row) for row in rows]
|
|
275
|
+
|
|
276
|
+
def upsert_agent_health(
|
|
277
|
+
self,
|
|
278
|
+
agent_id: str,
|
|
279
|
+
status: str,
|
|
280
|
+
last_output_at: str | None = None,
|
|
281
|
+
context_usage_pct: int | None = None,
|
|
282
|
+
current_task_id: str | None = None,
|
|
283
|
+
) -> None:
|
|
284
|
+
now = utcnow()
|
|
285
|
+
with closing(self.connect()) as conn:
|
|
286
|
+
with conn:
|
|
287
|
+
conn.execute(
|
|
288
|
+
"""
|
|
289
|
+
insert into agent_health(agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at)
|
|
290
|
+
values (?, ?, ?, ?, ?, ?)
|
|
291
|
+
on conflict(agent_id) do update set
|
|
292
|
+
status = excluded.status,
|
|
293
|
+
last_output_at = coalesce(excluded.last_output_at, agent_health.last_output_at),
|
|
294
|
+
context_usage_pct = excluded.context_usage_pct,
|
|
295
|
+
current_task_id = excluded.current_task_id,
|
|
296
|
+
updated_at = excluded.updated_at
|
|
297
|
+
""",
|
|
298
|
+
(agent_id, status, last_output_at, context_usage_pct, current_task_id, now),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def agent_health(self) -> dict[str, dict[str, Any]]:
|
|
302
|
+
with closing(self.connect()) as conn:
|
|
303
|
+
rows = conn.execute("select * from agent_health order by agent_id").fetchall()
|
|
304
|
+
return {row["agent_id"]: dict(row) for row in rows}
|
|
305
|
+
|
|
306
|
+
def add_scheduled_event(self, due_at: str, target: str, kind: str, payload: dict[str, Any]) -> int:
|
|
307
|
+
with closing(self.connect()) as conn:
|
|
308
|
+
with conn:
|
|
309
|
+
cur = conn.execute(
|
|
310
|
+
"""
|
|
311
|
+
insert into scheduled_events(due_at, target, kind, payload_json, status, created_at)
|
|
312
|
+
values (?, ?, ?, ?, 'pending', ?)
|
|
313
|
+
""",
|
|
314
|
+
(due_at, target, kind, json.dumps(payload, ensure_ascii=False), utcnow()),
|
|
315
|
+
)
|
|
316
|
+
return int(cur.lastrowid)
|
|
317
|
+
|
|
318
|
+
def due_scheduled_events(self, now: str | None = None) -> list[dict[str, Any]]:
|
|
319
|
+
with closing(self.connect()) as conn:
|
|
320
|
+
rows = conn.execute(
|
|
321
|
+
"""
|
|
322
|
+
select * from scheduled_events
|
|
323
|
+
where status = 'pending' and due_at <= ?
|
|
324
|
+
order by due_at, id
|
|
325
|
+
""",
|
|
326
|
+
(now or utcnow(),),
|
|
327
|
+
).fetchall()
|
|
328
|
+
return [dict(row) for row in rows]
|
|
329
|
+
|
|
330
|
+
def mark_scheduled_event(self, event_id: int, status: str, result: dict[str, Any]) -> None:
|
|
331
|
+
with closing(self.connect()) as conn:
|
|
332
|
+
with conn:
|
|
333
|
+
conn.execute(
|
|
334
|
+
"""
|
|
335
|
+
update scheduled_events
|
|
336
|
+
set status = ?, fired_at = ?, result_json = ?
|
|
337
|
+
where id = ?
|
|
338
|
+
""",
|
|
339
|
+
(status, utcnow(), json.dumps(result, ensure_ascii=False), event_id),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def allow_peer(self, a: str, b: str) -> None:
|
|
343
|
+
now = utcnow()
|
|
344
|
+
pairs = [(a, b), (b, a)]
|
|
345
|
+
with closing(self.connect()) as conn:
|
|
346
|
+
with conn:
|
|
347
|
+
conn.executemany(
|
|
348
|
+
"insert or ignore into peer_allowlist(a, b, created_at) values (?, ?, ?)",
|
|
349
|
+
[(left, right, now) for left, right in pairs],
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def peer_allowed(self, a: str, b: str) -> bool:
|
|
353
|
+
with closing(self.connect()) as conn:
|
|
354
|
+
row = conn.execute("select 1 from peer_allowlist where a = ? and b = ?", (a, b)).fetchone()
|
|
355
|
+
return row is not None
|
|
356
|
+
|
|
357
|
+
def message_counts(self) -> dict[str, int]:
|
|
358
|
+
counts: dict[str, int] = {}
|
|
359
|
+
with closing(self.connect()) as conn:
|
|
360
|
+
rows = conn.execute("select status, count(*) as n from messages group by status").fetchall()
|
|
361
|
+
for row in rows:
|
|
362
|
+
counts[row["status"]] = row["n"]
|
|
363
|
+
return counts
|
|
364
|
+
|
|
365
|
+
def result_counts(self) -> dict[str, Any]:
|
|
366
|
+
counts: dict[str, Any] = {"total": 0, "uncollected": 0, "collected": 0, "invalid": 0, "by_status": {}}
|
|
367
|
+
with closing(self.connect()) as conn:
|
|
368
|
+
rows = conn.execute("select status, count(*) as n from results group by status").fetchall()
|
|
369
|
+
for row in rows:
|
|
370
|
+
status = row["status"]
|
|
371
|
+
n = row["n"]
|
|
372
|
+
counts["total"] += n
|
|
373
|
+
if status == "collected":
|
|
374
|
+
counts["collected"] += n
|
|
375
|
+
elif status == "invalid":
|
|
376
|
+
counts["invalid"] += n
|
|
377
|
+
else:
|
|
378
|
+
counts["uncollected"] += n
|
|
379
|
+
counts["by_status"][status] = n
|
|
380
|
+
return counts
|
|
381
|
+
|
|
382
|
+
def create_result_watcher(
|
|
383
|
+
self,
|
|
384
|
+
task_id: str | None,
|
|
385
|
+
agent_id: str | None,
|
|
386
|
+
message_id: str | None,
|
|
387
|
+
leader_id: str = "leader",
|
|
388
|
+
) -> str:
|
|
389
|
+
watcher_id = f"watch_{uuid.uuid4().hex[:12]}"
|
|
390
|
+
with closing(self.connect()) as conn:
|
|
391
|
+
with conn:
|
|
392
|
+
conn.execute(
|
|
393
|
+
"""
|
|
394
|
+
insert into result_watchers(
|
|
395
|
+
watcher_id, task_id, agent_id, message_id, leader_id, status, created_at
|
|
396
|
+
)
|
|
397
|
+
values (?, ?, ?, ?, ?, 'pending', ?)
|
|
398
|
+
""",
|
|
399
|
+
(watcher_id, task_id, agent_id, message_id, leader_id, utcnow()),
|
|
400
|
+
)
|
|
401
|
+
return watcher_id
|
|
402
|
+
|
|
403
|
+
def pending_result_watchers(self) -> list[dict[str, Any]]:
|
|
404
|
+
with closing(self.connect()) as conn:
|
|
405
|
+
rows = conn.execute(
|
|
406
|
+
"select * from result_watchers where status = 'pending' order by created_at"
|
|
407
|
+
).fetchall()
|
|
408
|
+
return [dict(row) for row in rows]
|
|
409
|
+
|
|
410
|
+
def result_watchers(self) -> list[dict[str, Any]]:
|
|
411
|
+
with closing(self.connect()) as conn:
|
|
412
|
+
rows = conn.execute("select * from result_watchers order by created_at").fetchall()
|
|
413
|
+
return [dict(row) for row in rows]
|
|
414
|
+
|
|
415
|
+
def mark_result_watcher(
|
|
416
|
+
self,
|
|
417
|
+
watcher_id: str,
|
|
418
|
+
status: str,
|
|
419
|
+
result_id: str | None = None,
|
|
420
|
+
notified_message_id: str | None = None,
|
|
421
|
+
error: str | None = None,
|
|
422
|
+
) -> None:
|
|
423
|
+
with closing(self.connect()) as conn:
|
|
424
|
+
with conn:
|
|
425
|
+
conn.execute(
|
|
426
|
+
"""
|
|
427
|
+
update result_watchers
|
|
428
|
+
set status = ?,
|
|
429
|
+
completed_at = ?,
|
|
430
|
+
result_id = coalesce(?, result_id),
|
|
431
|
+
notified_message_id = coalesce(?, notified_message_id),
|
|
432
|
+
error = coalesce(?, error)
|
|
433
|
+
where watcher_id = ?
|
|
434
|
+
""",
|
|
435
|
+
(status, utcnow(), result_id, notified_message_id, error, watcher_id),
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def add_result(self, envelope: dict[str, Any]) -> str:
|
|
439
|
+
validate_result_envelope(envelope)
|
|
440
|
+
result_id = f"res_{uuid.uuid4().hex[:12]}"
|
|
441
|
+
with closing(self.connect()) as conn:
|
|
442
|
+
with conn:
|
|
443
|
+
conn.execute(
|
|
444
|
+
"insert into results values (?, ?, ?, ?, ?, ?)",
|
|
445
|
+
(
|
|
446
|
+
result_id,
|
|
447
|
+
envelope["task_id"],
|
|
448
|
+
envelope["agent_id"],
|
|
449
|
+
json.dumps(envelope, ensure_ascii=False),
|
|
450
|
+
envelope["status"],
|
|
451
|
+
utcnow(),
|
|
452
|
+
),
|
|
453
|
+
)
|
|
454
|
+
return result_id
|
|
455
|
+
|
|
456
|
+
def acknowledge_task_messages(self, task_id: str, agent_id: str) -> list[str]:
|
|
457
|
+
now = utcnow()
|
|
458
|
+
with closing(self.connect()) as conn:
|
|
459
|
+
with conn:
|
|
460
|
+
rows = conn.execute(
|
|
461
|
+
"""
|
|
462
|
+
select message_id from messages
|
|
463
|
+
where task_id = ? and recipient = ? and status in ('pending', 'accepted', 'target_resolved', 'injected', 'visible', 'submitted', 'delivered')
|
|
464
|
+
""",
|
|
465
|
+
(task_id, agent_id),
|
|
466
|
+
).fetchall()
|
|
467
|
+
ids = [row["message_id"] for row in rows]
|
|
468
|
+
conn.execute(
|
|
469
|
+
"""
|
|
470
|
+
update messages
|
|
471
|
+
set status = 'acknowledged', acknowledged_at = ?, updated_at = ?
|
|
472
|
+
where task_id = ? and recipient = ? and status in ('pending', 'accepted', 'target_resolved', 'injected', 'visible', 'submitted', 'delivered')
|
|
473
|
+
""",
|
|
474
|
+
(now, now, task_id, agent_id),
|
|
475
|
+
)
|
|
476
|
+
return ids
|
|
477
|
+
|
|
478
|
+
def results(self, uncollected_only: bool = False) -> list[dict[str, Any]]:
|
|
479
|
+
query = "select * from results order by created_at"
|
|
480
|
+
if uncollected_only:
|
|
481
|
+
query = "select * from results where status not in ('collected', 'invalid') order by created_at"
|
|
482
|
+
with closing(self.connect()) as conn:
|
|
483
|
+
rows = conn.execute(query).fetchall()
|
|
484
|
+
return [dict(row) for row in rows]
|
|
485
|
+
|
|
486
|
+
def mark_result_collected(self, result_id: str) -> None:
|
|
487
|
+
with closing(self.connect()) as conn:
|
|
488
|
+
with conn:
|
|
489
|
+
conn.execute("update results set status = 'collected' where result_id = ?", (result_id,))
|
|
490
|
+
|
|
491
|
+
def mark_result_invalid(self, result_id: str, error: str) -> None:
|
|
492
|
+
with closing(self.connect()) as conn:
|
|
493
|
+
with conn:
|
|
494
|
+
conn.execute(
|
|
495
|
+
"update results set status = 'invalid' where result_id = ?",
|
|
496
|
+
(result_id,),
|
|
497
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def repo_root() -> Path:
|
|
10
|
+
return PACKAGE_ROOT
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def schema_path(name: str) -> Path:
|
|
14
|
+
return repo_root() / "schemas" / name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def template_path(name: str) -> Path:
|
|
18
|
+
return repo_root() / "templates" / name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def example_path(name: str) -> Path:
|
|
22
|
+
return repo_root() / "examples" / name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def runtime_dir(workspace: Path) -> Path:
|
|
26
|
+
return workspace / ".team" / "runtime"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def logs_dir(workspace: Path) -> Path:
|
|
30
|
+
return workspace / ".team" / "logs"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def artifacts_dir(workspace: Path) -> Path:
|
|
34
|
+
return workspace / ".team" / "artifacts"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def messages_dir(workspace: Path) -> Path:
|
|
38
|
+
return workspace / ".team" / "messages"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def team_workspace(team_dir: Path) -> Path:
|
|
42
|
+
team_dir = team_dir.resolve()
|
|
43
|
+
if team_dir.parent.name == ".team":
|
|
44
|
+
return team_dir.parent.parent
|
|
45
|
+
return team_dir.parent
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
CANONICAL_TOOLS = {
|
|
6
|
+
"fs_read",
|
|
7
|
+
"fs_write",
|
|
8
|
+
"fs_list",
|
|
9
|
+
"execute_bash",
|
|
10
|
+
"git_diff",
|
|
11
|
+
"network",
|
|
12
|
+
"mcp_team",
|
|
13
|
+
"provider_builtin",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
ROLE_DEFAULTS = {
|
|
17
|
+
"leader": ["fs_read", "fs_list", "mcp_team", "provider_builtin"],
|
|
18
|
+
"supervisor": ["fs_read", "fs_list", "mcp_team", "provider_builtin"],
|
|
19
|
+
"implementation_engineer": [
|
|
20
|
+
"fs_read",
|
|
21
|
+
"fs_write",
|
|
22
|
+
"fs_list",
|
|
23
|
+
"execute_bash",
|
|
24
|
+
"git_diff",
|
|
25
|
+
"mcp_team",
|
|
26
|
+
"provider_builtin",
|
|
27
|
+
],
|
|
28
|
+
"developer": [
|
|
29
|
+
"fs_read",
|
|
30
|
+
"fs_write",
|
|
31
|
+
"fs_list",
|
|
32
|
+
"execute_bash",
|
|
33
|
+
"git_diff",
|
|
34
|
+
"mcp_team",
|
|
35
|
+
"provider_builtin",
|
|
36
|
+
],
|
|
37
|
+
"researcher": ["fs_read", "fs_list", "network", "mcp_team", "provider_builtin"],
|
|
38
|
+
"reviewer": ["fs_read", "fs_list", "git_diff", "mcp_team", "provider_builtin"],
|
|
39
|
+
"code_reviewer": ["fs_read", "fs_list", "git_diff", "mcp_team", "provider_builtin"],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
PROVIDER_ENFORCEMENT = {
|
|
43
|
+
"claude_code": {
|
|
44
|
+
"fs_read": "hard",
|
|
45
|
+
"fs_write": "hard",
|
|
46
|
+
"fs_list": "hard",
|
|
47
|
+
"execute_bash": "hard",
|
|
48
|
+
"git_diff": "hard",
|
|
49
|
+
"network": "prompt_only",
|
|
50
|
+
"mcp_team": "hard",
|
|
51
|
+
"provider_builtin": "hard",
|
|
52
|
+
},
|
|
53
|
+
"codex": {tool: "prompt_only" for tool in CANONICAL_TOOLS},
|
|
54
|
+
"gemini_cli": {
|
|
55
|
+
"fs_read": "hard",
|
|
56
|
+
"fs_write": "hard",
|
|
57
|
+
"fs_list": "hard",
|
|
58
|
+
"execute_bash": "hard",
|
|
59
|
+
"git_diff": "hard",
|
|
60
|
+
"network": "prompt_only",
|
|
61
|
+
"mcp_team": "prompt_only",
|
|
62
|
+
"provider_builtin": "hard",
|
|
63
|
+
},
|
|
64
|
+
"fake": {tool: "hard" for tool in CANONICAL_TOOLS},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def expand_tools(tools: list[str]) -> list[str]:
|
|
69
|
+
expanded: list[str] = []
|
|
70
|
+
for tool in tools:
|
|
71
|
+
if tool == "fs_*":
|
|
72
|
+
expanded.extend(["fs_read", "fs_write", "fs_list"])
|
|
73
|
+
elif tool == "@builtin":
|
|
74
|
+
expanded.append("provider_builtin")
|
|
75
|
+
elif tool in {"@team-orchestrator", "@cao-mcp-server"}:
|
|
76
|
+
expanded.append("mcp_team")
|
|
77
|
+
elif tool == "*":
|
|
78
|
+
expanded.extend(sorted(CANONICAL_TOOLS))
|
|
79
|
+
else:
|
|
80
|
+
expanded.append(tool)
|
|
81
|
+
return sorted(set(expanded))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def default_tools_for_role(role: str) -> list[str]:
|
|
85
|
+
return list(ROLE_DEFAULTS.get(role, ROLE_DEFAULTS["developer"]))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def resolve_permissions(agent: dict[str, Any]) -> dict[str, Any]:
|
|
89
|
+
provider = agent["provider"]
|
|
90
|
+
tools = agent.get("tools") or default_tools_for_role(agent.get("role", "developer"))
|
|
91
|
+
resolved = expand_tools(tools)
|
|
92
|
+
enforcement_map = PROVIDER_ENFORCEMENT.get(provider, {})
|
|
93
|
+
entries = [
|
|
94
|
+
{
|
|
95
|
+
"tool": tool,
|
|
96
|
+
"enforcement": enforcement_map.get(tool, "prompt_only"),
|
|
97
|
+
}
|
|
98
|
+
for tool in resolved
|
|
99
|
+
]
|
|
100
|
+
return {
|
|
101
|
+
"agent_id": agent.get("id"),
|
|
102
|
+
"provider": provider,
|
|
103
|
+
"tools": resolved,
|
|
104
|
+
"resolved_tools": entries,
|
|
105
|
+
"has_prompt_only": any(e["enforcement"] == "prompt_only" for e in entries),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def task_required_tools(task: dict[str, Any]) -> list[str]:
|
|
110
|
+
required = list(task.get("requires_tools", []))
|
|
111
|
+
task_type = task.get("type")
|
|
112
|
+
if task_type in {"implementation", "bug_fix", "test"}:
|
|
113
|
+
required.extend(["fs_write", "execute_bash"])
|
|
114
|
+
if task_type in {"review", "risk_check"}:
|
|
115
|
+
required.extend(["fs_read", "git_diff"])
|
|
116
|
+
if task_type in {"research", "architecture"}:
|
|
117
|
+
required.extend(["fs_read"])
|
|
118
|
+
return expand_tools(required)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def missing_tools(agent: dict[str, Any], task: dict[str, Any]) -> list[str]:
|
|
122
|
+
allowed = set(resolve_permissions(agent)["tools"])
|
|
123
|
+
return [tool for tool in task_required_tools(task) if tool not in allowed]
|