@team-agent/installer 0.1.11 → 0.2.1
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/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/src/team_agent/approvals/__init__.py +65 -0
- package/src/team_agent/approvals/constants.py +6 -0
- package/src/team_agent/approvals/parsing.py +176 -0
- package/src/team_agent/approvals/runtime_prompts.py +171 -0
- package/src/team_agent/approvals/status.py +165 -0
- package/src/team_agent/cli/__init__.py +137 -0
- package/src/team_agent/cli/commands.py +339 -0
- package/src/team_agent/cli/e2e.py +202 -0
- package/src/team_agent/cli/helpers.py +137 -0
- package/src/team_agent/cli/parser.py +477 -0
- package/src/team_agent/compiler.py +98 -33
- package/src/team_agent/coordinator/__init__.py +53 -0
- package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
- package/src/team_agent/coordinator/lifecycle.py +334 -0
- package/src/team_agent/coordinator/metadata.py +61 -0
- package/src/team_agent/coordinator/paths.py +17 -0
- package/src/team_agent/diagnose/__init__.py +48 -0
- package/src/team_agent/diagnose/checks.py +101 -0
- package/src/team_agent/diagnose/health.py +241 -0
- package/src/team_agent/diagnose/preflight.py +194 -0
- package/src/team_agent/diagnose/quick_start.py +233 -0
- package/src/team_agent/display/__init__.py +61 -0
- package/src/team_agent/display/close.py +147 -0
- package/src/team_agent/display/ghostty.py +77 -0
- package/src/team_agent/display/worker_window.py +110 -0
- package/src/team_agent/display/workspace.py +473 -0
- package/src/team_agent/launch/__init__.py +41 -0
- package/src/team_agent/launch/bootstrap.py +85 -0
- package/src/team_agent/launch/config.py +106 -0
- package/src/team_agent/launch/core.py +291 -0
- package/src/team_agent/launch/requirements.py +57 -0
- package/src/team_agent/leader/__init__.py +320 -0
- package/src/team_agent/lifecycle/__init__.py +5 -0
- package/src/team_agent/lifecycle/agents.py +226 -0
- package/src/team_agent/lifecycle/operations.py +321 -0
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +39 -0
- package/src/team_agent/lifecycle/start.py +363 -0
- package/src/team_agent/mcp_server/__init__.py +42 -0
- package/src/team_agent/mcp_server/__main__.py +7 -0
- package/src/team_agent/mcp_server/contracts.py +148 -0
- package/src/team_agent/mcp_server/normalize.py +257 -0
- package/src/team_agent/mcp_server/server.py +150 -0
- package/src/team_agent/mcp_server/tools.py +205 -0
- package/src/team_agent/message_store/__init__.py +23 -0
- package/src/team_agent/message_store/agent_health.py +109 -0
- package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
- package/src/team_agent/message_store/result_watchers.py +102 -0
- package/src/team_agent/message_store/schema.py +266 -0
- package/src/team_agent/messaging/__init__.py +1 -0
- package/src/team_agent/messaging/activity_detector.py +190 -0
- package/src/team_agent/messaging/delivery.py +138 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +323 -0
- package/src/team_agent/messaging/internal_delivery.py +46 -0
- package/src/team_agent/messaging/leader.py +317 -0
- package/src/team_agent/messaging/leader_panes.py +343 -0
- package/src/team_agent/messaging/owner_bypass.py +29 -0
- package/src/team_agent/messaging/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +428 -0
- package/src/team_agent/messaging/send.py +500 -0
- package/src/team_agent/messaging/session_drift.py +94 -0
- package/src/team_agent/messaging/tmux_io.py +337 -0
- package/src/team_agent/messaging/tmux_prompt.py +229 -0
- package/src/team_agent/orchestrator/__init__.py +376 -0
- package/src/team_agent/orchestrator/plan.py +122 -0
- package/src/team_agent/orchestrator/state.py +128 -0
- package/src/team_agent/profiles/__init__.py +82 -0
- package/src/team_agent/profiles/constants.py +19 -0
- package/src/team_agent/profiles/core.py +407 -0
- package/src/team_agent/profiles/helpers.py +69 -0
- package/src/team_agent/profiles/provider_env.py +188 -0
- package/src/team_agent/profiles/smoke.py +201 -0
- package/src/team_agent/provider_cli/__init__.py +43 -0
- package/src/team_agent/provider_cli/adapter.py +167 -0
- package/src/team_agent/provider_cli/base.py +48 -0
- package/src/team_agent/provider_cli/claude.py +457 -0
- package/src/team_agent/provider_cli/codex.py +319 -0
- package/src/team_agent/provider_cli/copilot.py +8 -0
- package/src/team_agent/provider_cli/fake.py +39 -0
- package/src/team_agent/provider_cli/gemini.py +95 -0
- package/src/team_agent/provider_cli/opencode.py +8 -0
- package/src/team_agent/provider_cli/prompt.py +62 -0
- package/src/team_agent/provider_cli/registry.py +18 -0
- package/src/team_agent/provider_cli/unsupported.py +32 -0
- package/src/team_agent/providers.py +67 -949
- package/src/team_agent/quality_gates.py +104 -0
- package/src/team_agent/restart/__init__.py +34 -0
- package/src/team_agent/restart/orchestration.py +328 -0
- package/src/team_agent/restart/selection.py +89 -0
- package/src/team_agent/restart/snapshot.py +70 -0
- package/src/team_agent/runtime.py +809 -5892
- package/src/team_agent/rust_core.py +22 -5
- package/src/team_agent/sessions/__init__.py +25 -0
- package/src/team_agent/sessions/capture.py +93 -0
- package/src/team_agent/sessions/inventory.py +44 -0
- package/src/team_agent/sessions/resume.py +135 -0
- package/src/team_agent/spec.py +3 -1
- package/src/team_agent/state.py +218 -4
- package/src/team_agent/status/__init__.py +63 -0
- package/src/team_agent/status/approvals.py +52 -0
- package/src/team_agent/status/compact.py +158 -0
- package/src/team_agent/status/constants.py +18 -0
- package/src/team_agent/status/inbox.py +28 -0
- package/src/team_agent/status/peek.py +117 -0
- package/src/team_agent/status/queries.py +168 -0
- package/src/team_agent/terminal.py +57 -0
- package/src/team_agent/cli.py +0 -858
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from contextlib import closing
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from team_agent.message_store.schema import utcnow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_result_watcher(
|
|
11
|
+
self,
|
|
12
|
+
task_id: str | None,
|
|
13
|
+
agent_id: str | None,
|
|
14
|
+
message_id: str | None,
|
|
15
|
+
leader_id: str = "leader",
|
|
16
|
+
owner_team_id: str | None = None,
|
|
17
|
+
) -> str:
|
|
18
|
+
watcher_id = f"watch_{uuid.uuid4().hex[:12]}"
|
|
19
|
+
with closing(self.connect()) as conn:
|
|
20
|
+
with conn:
|
|
21
|
+
conn.execute(
|
|
22
|
+
"""
|
|
23
|
+
insert into result_watchers(
|
|
24
|
+
watcher_id, owner_team_id, task_id, agent_id, message_id, leader_id, status, created_at
|
|
25
|
+
)
|
|
26
|
+
values (?, ?, ?, ?, ?, ?, 'pending', ?)
|
|
27
|
+
""",
|
|
28
|
+
(watcher_id, owner_team_id, task_id, agent_id, message_id, leader_id, utcnow()),
|
|
29
|
+
)
|
|
30
|
+
return watcher_id
|
|
31
|
+
|
|
32
|
+
def pending_result_watchers(self, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
33
|
+
with closing(self.connect()) as conn:
|
|
34
|
+
if owner_team_id is None:
|
|
35
|
+
rows = conn.execute("select * from result_watchers where status = 'pending' order by created_at").fetchall()
|
|
36
|
+
else:
|
|
37
|
+
rows = conn.execute(
|
|
38
|
+
"""
|
|
39
|
+
select * from result_watchers
|
|
40
|
+
where status = 'pending' and (owner_team_id = ? or owner_team_id is null)
|
|
41
|
+
order by created_at
|
|
42
|
+
""",
|
|
43
|
+
(owner_team_id,),
|
|
44
|
+
).fetchall()
|
|
45
|
+
return [dict(row) for row in rows]
|
|
46
|
+
|
|
47
|
+
def retryable_result_watchers(self) -> list[dict[str, Any]]:
|
|
48
|
+
with closing(self.connect()) as conn:
|
|
49
|
+
rows = conn.execute(
|
|
50
|
+
"select * from result_watchers where status in ('pending', 'notify_failed') order by created_at"
|
|
51
|
+
).fetchall()
|
|
52
|
+
return [dict(row) for row in rows]
|
|
53
|
+
|
|
54
|
+
def result_watchers(self, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
55
|
+
with closing(self.connect()) as conn:
|
|
56
|
+
if owner_team_id is None:
|
|
57
|
+
rows = conn.execute("select * from result_watchers order by created_at").fetchall()
|
|
58
|
+
else:
|
|
59
|
+
rows = conn.execute(
|
|
60
|
+
"select * from result_watchers where owner_team_id = ? or owner_team_id is null order by created_at",
|
|
61
|
+
(owner_team_id,),
|
|
62
|
+
).fetchall()
|
|
63
|
+
return [dict(row) for row in rows]
|
|
64
|
+
|
|
65
|
+
def mark_result_watcher(
|
|
66
|
+
self,
|
|
67
|
+
watcher_id: str,
|
|
68
|
+
status: str,
|
|
69
|
+
result_id: str | None = None,
|
|
70
|
+
notified_message_id: str | None = None,
|
|
71
|
+
error: str | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
with closing(self.connect()) as conn:
|
|
74
|
+
with conn:
|
|
75
|
+
conn.execute(
|
|
76
|
+
"""
|
|
77
|
+
update result_watchers
|
|
78
|
+
set status = ?,
|
|
79
|
+
completed_at = ?,
|
|
80
|
+
result_id = coalesce(?, result_id),
|
|
81
|
+
notified_message_id = coalesce(?, notified_message_id),
|
|
82
|
+
error = coalesce(?, error)
|
|
83
|
+
where watcher_id = ?
|
|
84
|
+
""",
|
|
85
|
+
(status, utcnow(), result_id, notified_message_id, error, watcher_id),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def requeue_delivery_exhausted_watchers(self) -> list[str]:
|
|
90
|
+
with closing(self.connect()) as conn:
|
|
91
|
+
with conn:
|
|
92
|
+
rows = conn.execute(
|
|
93
|
+
"select watcher_id from result_watchers where status = 'delivery_exhausted'"
|
|
94
|
+
).fetchall()
|
|
95
|
+
watcher_ids = [row[0] for row in rows]
|
|
96
|
+
if watcher_ids:
|
|
97
|
+
conn.execute(
|
|
98
|
+
"update result_watchers "
|
|
99
|
+
"set status = 'notify_failed', error = null, notified_message_id = null, completed_at = null "
|
|
100
|
+
"where status = 'delivery_exhausted'"
|
|
101
|
+
)
|
|
102
|
+
return watcher_ids
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
MESSAGE_COLUMNS = {
|
|
8
|
+
"owner_team_id",
|
|
9
|
+
"message_id",
|
|
10
|
+
"task_id",
|
|
11
|
+
"sender",
|
|
12
|
+
"recipient",
|
|
13
|
+
"reply_to",
|
|
14
|
+
"requires_ack",
|
|
15
|
+
"status",
|
|
16
|
+
"content",
|
|
17
|
+
"artifact_refs",
|
|
18
|
+
"created_at",
|
|
19
|
+
"updated_at",
|
|
20
|
+
"delivered_at",
|
|
21
|
+
"acknowledged_at",
|
|
22
|
+
"error",
|
|
23
|
+
"delivery_attempts",
|
|
24
|
+
}
|
|
25
|
+
RESULT_COLUMNS = {"result_id", "task_id", "agent_id", "envelope", "status", "created_at"}
|
|
26
|
+
SCHEDULED_EVENT_COLUMNS = {
|
|
27
|
+
"id",
|
|
28
|
+
"owner_team_id",
|
|
29
|
+
"due_at",
|
|
30
|
+
"target",
|
|
31
|
+
"kind",
|
|
32
|
+
"payload_json",
|
|
33
|
+
"status",
|
|
34
|
+
"created_at",
|
|
35
|
+
"fired_at",
|
|
36
|
+
"result_json",
|
|
37
|
+
}
|
|
38
|
+
DELIVERY_TOKEN_COLUMNS = {
|
|
39
|
+
"message_id",
|
|
40
|
+
"unique_token",
|
|
41
|
+
"injected_at",
|
|
42
|
+
"visible_at",
|
|
43
|
+
"consumed_at",
|
|
44
|
+
"failed_at",
|
|
45
|
+
"failure_reason",
|
|
46
|
+
}
|
|
47
|
+
AGENT_HEALTH_COLUMNS = {
|
|
48
|
+
"owner_team_id",
|
|
49
|
+
"agent_id",
|
|
50
|
+
"status",
|
|
51
|
+
"last_output_at",
|
|
52
|
+
"context_usage_pct",
|
|
53
|
+
"current_task_id",
|
|
54
|
+
"updated_at",
|
|
55
|
+
}
|
|
56
|
+
PEER_ALLOWLIST_COLUMNS = {"a", "b", "created_at"}
|
|
57
|
+
RESULT_WATCHER_COLUMNS = {
|
|
58
|
+
"owner_team_id",
|
|
59
|
+
"watcher_id",
|
|
60
|
+
"task_id",
|
|
61
|
+
"agent_id",
|
|
62
|
+
"message_id",
|
|
63
|
+
"leader_id",
|
|
64
|
+
"status",
|
|
65
|
+
"created_at",
|
|
66
|
+
"completed_at",
|
|
67
|
+
"result_id",
|
|
68
|
+
"notified_message_id",
|
|
69
|
+
"error",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def utcnow() -> str:
|
|
74
|
+
return datetime.now(timezone.utc).isoformat()
|
|
75
|
+
|
|
76
|
+
SCHEMA_VERSION = 3
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
80
|
+
return {row[1] for row in conn.execute(f"pragma table_info({table})").fetchall()}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _ensure_table_columns(
|
|
84
|
+
conn: sqlite3.Connection,
|
|
85
|
+
table: str,
|
|
86
|
+
required: set[str],
|
|
87
|
+
migrations: dict[str, str] | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
columns = _table_columns(conn, table)
|
|
90
|
+
missing = required - columns
|
|
91
|
+
migrations = migrations or {}
|
|
92
|
+
unsupported = missing - set(migrations)
|
|
93
|
+
if unsupported:
|
|
94
|
+
names = ", ".join(sorted(unsupported))
|
|
95
|
+
raise RuntimeError(f"team.db table {table} is missing required column(s): {names}")
|
|
96
|
+
for name in sorted(missing):
|
|
97
|
+
conn.execute(migrations[name])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def initialize_schema(conn: sqlite3.Connection) -> None:
|
|
101
|
+
with conn:
|
|
102
|
+
conn.execute(
|
|
103
|
+
"""
|
|
104
|
+
create table if not exists messages (
|
|
105
|
+
message_id text primary key,
|
|
106
|
+
owner_team_id text,
|
|
107
|
+
task_id text,
|
|
108
|
+
sender text,
|
|
109
|
+
recipient text,
|
|
110
|
+
reply_to text,
|
|
111
|
+
requires_ack integer,
|
|
112
|
+
status text,
|
|
113
|
+
content text,
|
|
114
|
+
artifact_refs text,
|
|
115
|
+
created_at text,
|
|
116
|
+
updated_at text,
|
|
117
|
+
delivered_at text,
|
|
118
|
+
acknowledged_at text,
|
|
119
|
+
error text,
|
|
120
|
+
delivery_attempts integer not null default 0
|
|
121
|
+
)
|
|
122
|
+
"""
|
|
123
|
+
)
|
|
124
|
+
conn.execute(
|
|
125
|
+
"""
|
|
126
|
+
create table if not exists results (
|
|
127
|
+
result_id text primary key,
|
|
128
|
+
task_id text not null,
|
|
129
|
+
agent_id text not null,
|
|
130
|
+
envelope text not null,
|
|
131
|
+
status text not null,
|
|
132
|
+
created_at text not null
|
|
133
|
+
)
|
|
134
|
+
"""
|
|
135
|
+
)
|
|
136
|
+
conn.execute(
|
|
137
|
+
"""
|
|
138
|
+
create table if not exists scheduled_events (
|
|
139
|
+
id integer primary key,
|
|
140
|
+
owner_team_id text,
|
|
141
|
+
due_at text not null,
|
|
142
|
+
target text not null,
|
|
143
|
+
kind text not null,
|
|
144
|
+
payload_json text not null,
|
|
145
|
+
status text not null,
|
|
146
|
+
created_at text not null,
|
|
147
|
+
fired_at text,
|
|
148
|
+
result_json text
|
|
149
|
+
)
|
|
150
|
+
"""
|
|
151
|
+
)
|
|
152
|
+
conn.execute(
|
|
153
|
+
"""
|
|
154
|
+
create table if not exists delivery_tokens (
|
|
155
|
+
message_id text primary key,
|
|
156
|
+
unique_token text not null,
|
|
157
|
+
injected_at text not null,
|
|
158
|
+
visible_at text,
|
|
159
|
+
consumed_at text,
|
|
160
|
+
failed_at text,
|
|
161
|
+
failure_reason text
|
|
162
|
+
)
|
|
163
|
+
"""
|
|
164
|
+
)
|
|
165
|
+
conn.execute(
|
|
166
|
+
"""
|
|
167
|
+
create table if not exists agent_health (
|
|
168
|
+
owner_team_id text,
|
|
169
|
+
agent_id text not null,
|
|
170
|
+
status text not null,
|
|
171
|
+
last_output_at text,
|
|
172
|
+
context_usage_pct integer,
|
|
173
|
+
current_task_id text,
|
|
174
|
+
updated_at text not null,
|
|
175
|
+
unique(owner_team_id, agent_id)
|
|
176
|
+
)
|
|
177
|
+
"""
|
|
178
|
+
)
|
|
179
|
+
conn.execute(
|
|
180
|
+
"""
|
|
181
|
+
create table if not exists peer_allowlist (
|
|
182
|
+
a text not null,
|
|
183
|
+
b text not null,
|
|
184
|
+
created_at text not null,
|
|
185
|
+
primary key (a, b)
|
|
186
|
+
)
|
|
187
|
+
"""
|
|
188
|
+
)
|
|
189
|
+
conn.execute(
|
|
190
|
+
"""
|
|
191
|
+
create table if not exists result_watchers (
|
|
192
|
+
watcher_id text primary key,
|
|
193
|
+
owner_team_id text,
|
|
194
|
+
task_id text,
|
|
195
|
+
agent_id text,
|
|
196
|
+
message_id text,
|
|
197
|
+
leader_id text not null,
|
|
198
|
+
status text not null,
|
|
199
|
+
created_at text not null,
|
|
200
|
+
completed_at text,
|
|
201
|
+
result_id text,
|
|
202
|
+
notified_message_id text,
|
|
203
|
+
error text
|
|
204
|
+
)
|
|
205
|
+
"""
|
|
206
|
+
)
|
|
207
|
+
_ensure_table_columns(
|
|
208
|
+
conn,
|
|
209
|
+
"messages",
|
|
210
|
+
MESSAGE_COLUMNS,
|
|
211
|
+
{
|
|
212
|
+
"delivery_attempts": (
|
|
213
|
+
"alter table messages add column delivery_attempts integer not null default 0"
|
|
214
|
+
),
|
|
215
|
+
"owner_team_id": "alter table messages add column owner_team_id text",
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
_ensure_table_columns(conn, "results", RESULT_COLUMNS)
|
|
219
|
+
_ensure_table_columns(
|
|
220
|
+
conn,
|
|
221
|
+
"scheduled_events",
|
|
222
|
+
SCHEDULED_EVENT_COLUMNS,
|
|
223
|
+
{"owner_team_id": "alter table scheduled_events add column owner_team_id text"},
|
|
224
|
+
)
|
|
225
|
+
_ensure_table_columns(conn, "delivery_tokens", DELIVERY_TOKEN_COLUMNS)
|
|
226
|
+
_migrate_agent_health_owner_team_id(conn)
|
|
227
|
+
_ensure_table_columns(conn, "peer_allowlist", PEER_ALLOWLIST_COLUMNS)
|
|
228
|
+
_ensure_table_columns(
|
|
229
|
+
conn,
|
|
230
|
+
"result_watchers",
|
|
231
|
+
RESULT_WATCHER_COLUMNS,
|
|
232
|
+
{"owner_team_id": "alter table result_watchers add column owner_team_id text"},
|
|
233
|
+
)
|
|
234
|
+
conn.execute("create index if not exists idx_messages_owner_team_id on messages(owner_team_id)")
|
|
235
|
+
conn.execute("create index if not exists idx_scheduled_events_owner_team_id on scheduled_events(owner_team_id)")
|
|
236
|
+
conn.execute("create index if not exists idx_agent_health_owner_team_id on agent_health(owner_team_id)")
|
|
237
|
+
conn.execute("create index if not exists idx_result_watchers_owner_team_id on result_watchers(owner_team_id)")
|
|
238
|
+
conn.execute(f"pragma user_version = {SCHEMA_VERSION}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _migrate_agent_health_owner_team_id(conn: sqlite3.Connection) -> None:
|
|
242
|
+
columns = _table_columns(conn, "agent_health")
|
|
243
|
+
if "owner_team_id" not in columns:
|
|
244
|
+
conn.execute(
|
|
245
|
+
"""
|
|
246
|
+
create table agent_health_new (
|
|
247
|
+
owner_team_id text,
|
|
248
|
+
agent_id text not null,
|
|
249
|
+
status text not null,
|
|
250
|
+
last_output_at text,
|
|
251
|
+
context_usage_pct integer,
|
|
252
|
+
current_task_id text,
|
|
253
|
+
updated_at text not null,
|
|
254
|
+
unique(owner_team_id, agent_id)
|
|
255
|
+
)
|
|
256
|
+
"""
|
|
257
|
+
)
|
|
258
|
+
conn.execute(
|
|
259
|
+
"""
|
|
260
|
+
insert into agent_health_new(owner_team_id, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at)
|
|
261
|
+
select null, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at from agent_health
|
|
262
|
+
"""
|
|
263
|
+
)
|
|
264
|
+
conn.execute("drop table agent_health")
|
|
265
|
+
conn.execute("alter table agent_health_new rename to agent_health")
|
|
266
|
+
_ensure_table_columns(conn, "agent_health", AGENT_HEALTH_COLUMNS)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Messaging/delivery/leader-receiver lane package."""
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from team_agent.events import EventLog
|
|
8
|
+
from team_agent.messaging.deps import datetime, save_runtime_state, team_state_key, timezone
|
|
9
|
+
|
|
10
|
+
_COMPACTION_RESET_THRESHOLD_DEFAULT = 3
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _compaction_reset_threshold(state: dict[str, Any]) -> int:
|
|
14
|
+
from team_agent.messaging.deps import load_spec
|
|
15
|
+
spec_path = state.get("spec_path")
|
|
16
|
+
if spec_path:
|
|
17
|
+
try:
|
|
18
|
+
spec = load_spec(spec_path)
|
|
19
|
+
except Exception:
|
|
20
|
+
spec = {}
|
|
21
|
+
runtime_cfg = spec.get("runtime", {}) if isinstance(spec, dict) else {}
|
|
22
|
+
raw = runtime_cfg.get("compaction_reset_threshold")
|
|
23
|
+
if raw is not None:
|
|
24
|
+
try:
|
|
25
|
+
value = int(raw)
|
|
26
|
+
if value > 0:
|
|
27
|
+
return value
|
|
28
|
+
except (TypeError, ValueError):
|
|
29
|
+
pass
|
|
30
|
+
return _COMPACTION_RESET_THRESHOLD_DEFAULT
|
|
31
|
+
_PROVIDER_COMMANDS = {"claude", "claude-code", "codex", "node", "node.exe", "claude.exe"}
|
|
32
|
+
_COMPACTION_PATTERNS = (
|
|
33
|
+
re.compile(r"context compacted", re.IGNORECASE),
|
|
34
|
+
re.compile(r"compaction occurred", re.IGNORECASE),
|
|
35
|
+
)
|
|
36
|
+
_IDLE_PROMPT_PATTERNS = (
|
|
37
|
+
re.compile(r"›\s*Find and fix a bug in @filename"),
|
|
38
|
+
re.compile(r"─\s*for agents"),
|
|
39
|
+
re.compile(r"^›[^\n]*\n(?:\s*\n){0,8}\s*gpt-[\w.-]+\s+\S+\s+·", re.MULTILINE),
|
|
40
|
+
)
|
|
41
|
+
_WORKING_PATTERNS = (
|
|
42
|
+
re.compile(r"\bWorking(?:\s*\((?P<working_seconds>\d+)s\))?", re.IGNORECASE),
|
|
43
|
+
re.compile(r"\bReticulating\b", re.IGNORECASE),
|
|
44
|
+
re.compile(r"\bBaked for (?P<baked_seconds>\d+)s\b", re.IGNORECASE),
|
|
45
|
+
re.compile(r"\bThinking\b", re.IGNORECASE),
|
|
46
|
+
re.compile(r"esc to interrupt", re.IGNORECASE),
|
|
47
|
+
re.compile(r"[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def classify_agent_activity(
|
|
52
|
+
agent_id: str,
|
|
53
|
+
provider: str,
|
|
54
|
+
last_output_at: str | None,
|
|
55
|
+
pane: dict[str, Any] | None,
|
|
56
|
+
scrollback: str,
|
|
57
|
+
*,
|
|
58
|
+
now: datetime | None = None,
|
|
59
|
+
stuck_timeout_sec: int = 300,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
_ = agent_id, provider
|
|
62
|
+
now = now or datetime.now(timezone.utc)
|
|
63
|
+
pane = pane or {}
|
|
64
|
+
pane_in_mode = str(pane.get("pane_in_mode") or "0")
|
|
65
|
+
if pane_in_mode != "0":
|
|
66
|
+
return {"status": "uncertain", "confidence": 0.9, "rationale": f"pane_in_mode={pane_in_mode}"}
|
|
67
|
+
command = str(pane.get("pane_current_command") or "").split("/")[-1]
|
|
68
|
+
if command and command not in _PROVIDER_COMMANDS:
|
|
69
|
+
return {"status": "uncertain", "confidence": 0.75, "rationale": f"unexpected pane current_command={command}"}
|
|
70
|
+
working = _latest_working_match(scrollback)
|
|
71
|
+
idle_pos = _latest_idle_prompt_position(scrollback)
|
|
72
|
+
if idle_pos is not None and (working is None or idle_pos > working[0]):
|
|
73
|
+
return {"status": "idle", "confidence": 0.9, "rationale": "provider idle prompt is the latest scrollback signal"}
|
|
74
|
+
if working:
|
|
75
|
+
_pos, label, elapsed = working
|
|
76
|
+
if elapsed is not None and elapsed >= stuck_timeout_sec:
|
|
77
|
+
return {"status": "stuck", "confidence": 0.85, "rationale": f"stale {label} indicator for {elapsed}s"}
|
|
78
|
+
return {"status": "working", "confidence": 0.9, "rationale": f"{label} indicator is the latest scrollback signal"}
|
|
79
|
+
age = _last_output_age_seconds(last_output_at, now)
|
|
80
|
+
if age is not None and age >= stuck_timeout_sec:
|
|
81
|
+
return {"status": "stuck", "confidence": 0.85, "rationale": "last_output_at exceeded timeout with no idle prompt"}
|
|
82
|
+
if age is not None and age <= 120 and (not command or command in _PROVIDER_COMMANDS):
|
|
83
|
+
return {"status": "working", "confidence": 0.7, "rationale": "recent output from provider command"}
|
|
84
|
+
return {"status": "uncertain", "confidence": 0.5, "rationale": "no decisive prompt or working signal"}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _latest_idle_prompt_position(scrollback: str) -> int | None:
|
|
88
|
+
best: int | None = None
|
|
89
|
+
for pattern in _IDLE_PROMPT_PATTERNS:
|
|
90
|
+
for match in pattern.finditer(scrollback):
|
|
91
|
+
if best is None or match.start() > best:
|
|
92
|
+
best = match.start()
|
|
93
|
+
return best
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def detect_compaction_degradation(
|
|
97
|
+
workspace: Path,
|
|
98
|
+
state: dict[str, Any],
|
|
99
|
+
event_log: EventLog,
|
|
100
|
+
*,
|
|
101
|
+
agent_id: str,
|
|
102
|
+
provider: str,
|
|
103
|
+
scrollback: str,
|
|
104
|
+
stuck_loop: bool = False,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
count = _count_compaction_markers(scrollback)
|
|
107
|
+
owner_team_id = team_state_key(state)
|
|
108
|
+
team_counts = state.setdefault("coordinator", {}).setdefault("compaction_counts", {}).setdefault(owner_team_id, {})
|
|
109
|
+
current = max(int(team_counts.get(agent_id) or 0), count)
|
|
110
|
+
team_counts[agent_id] = current
|
|
111
|
+
save_runtime_state(workspace, state)
|
|
112
|
+
if current <= 0:
|
|
113
|
+
return {"ok": True, "event": "compaction_threshold_crossed.none", "compaction_count": current}
|
|
114
|
+
event_log.write(
|
|
115
|
+
"coordinator.compaction_observed",
|
|
116
|
+
agent_id=agent_id,
|
|
117
|
+
provider=provider,
|
|
118
|
+
team=owner_team_id,
|
|
119
|
+
compaction_count=current,
|
|
120
|
+
stuck_loop=stuck_loop,
|
|
121
|
+
)
|
|
122
|
+
if provider != "codex":
|
|
123
|
+
event = "compaction_threshold_crossed.ignored_lossless_provider"
|
|
124
|
+
event_log.write(event, agent_id=agent_id, provider=provider, team=owner_team_id, compaction_count=current)
|
|
125
|
+
return {"ok": True, "event": event, "agent_id": agent_id, "provider": provider, "compaction_count": current}
|
|
126
|
+
threshold = _compaction_reset_threshold(state)
|
|
127
|
+
if current < threshold and not (current >= 1 and stuck_loop):
|
|
128
|
+
return {"ok": True, "event": "compaction_threshold_crossed.below_threshold", "agent_id": agent_id, "compaction_count": current, "threshold": threshold}
|
|
129
|
+
return _reset_or_recommend(workspace, state, event_log, agent_id, provider, owner_team_id, current, threshold)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _reset_or_recommend(
|
|
133
|
+
workspace: Path,
|
|
134
|
+
state: dict[str, Any],
|
|
135
|
+
event_log: EventLog,
|
|
136
|
+
agent_id: str,
|
|
137
|
+
provider: str,
|
|
138
|
+
owner_team_id: str,
|
|
139
|
+
compaction_count: int,
|
|
140
|
+
threshold: int,
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
from team_agent.runtime import reset_agent
|
|
143
|
+
reset = reset_agent(workspace, agent_id, discard_session=True)
|
|
144
|
+
if reset.get("ok"):
|
|
145
|
+
team_counts = state.setdefault("coordinator", {}).setdefault("compaction_counts", {}).setdefault(owner_team_id, {})
|
|
146
|
+
team_counts[agent_id] = 0
|
|
147
|
+
save_runtime_state(workspace, state)
|
|
148
|
+
event = "compaction_threshold_crossed.auto_reset"
|
|
149
|
+
event_log.write(event, agent_id=agent_id, provider=provider, team=owner_team_id, compaction_count=compaction_count, threshold=threshold)
|
|
150
|
+
return {"ok": True, "event": event, "agent_id": agent_id, "compaction_count": compaction_count, "threshold": threshold, "reset": reset}
|
|
151
|
+
event = "compaction_threshold_crossed.recommend_reset"
|
|
152
|
+
message = f"agent {agent_id} crossed Codex compaction threshold; run team-agent reset-agent {agent_id} --discard-session"
|
|
153
|
+
event_log.write(
|
|
154
|
+
event,
|
|
155
|
+
agent_id=agent_id,
|
|
156
|
+
provider=provider,
|
|
157
|
+
team=owner_team_id,
|
|
158
|
+
compaction_count=compaction_count,
|
|
159
|
+
threshold=threshold,
|
|
160
|
+
leader_visible_message=message,
|
|
161
|
+
reset_error=reset.get("error") or reset.get("reason"),
|
|
162
|
+
)
|
|
163
|
+
return {"ok": True, "event": event, "agent_id": agent_id, "compaction_count": compaction_count, "threshold": threshold, "leader_visible_message": message, "reset": reset}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _latest_working_match(scrollback: str) -> tuple[int, str, int | None] | None:
|
|
167
|
+
best: tuple[int, str, int | None] | None = None
|
|
168
|
+
for pattern in _WORKING_PATTERNS:
|
|
169
|
+
for match in pattern.finditer(scrollback):
|
|
170
|
+
elapsed_raw = match.groupdict().get("working_seconds") or match.groupdict().get("baked_seconds")
|
|
171
|
+
elapsed = int(elapsed_raw) if elapsed_raw else None
|
|
172
|
+
if best is None or match.start() > best[0]:
|
|
173
|
+
best = (match.start(), match.group(0), elapsed)
|
|
174
|
+
return best
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _last_output_age_seconds(last_output_at: str | None, now: datetime) -> float | None:
|
|
178
|
+
if not last_output_at:
|
|
179
|
+
return None
|
|
180
|
+
try:
|
|
181
|
+
last = datetime.fromisoformat(last_output_at)
|
|
182
|
+
except ValueError:
|
|
183
|
+
return None
|
|
184
|
+
if last.tzinfo is None:
|
|
185
|
+
last = last.replace(tzinfo=timezone.utc)
|
|
186
|
+
return max(0.0, (now - last).total_seconds())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _count_compaction_markers(scrollback: str) -> int:
|
|
190
|
+
return sum(len(pattern.findall(scrollback)) for pattern in _COMPACTION_PATTERNS)
|