@team-agent/installer 0.1.11 → 0.2.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.
Files changed (110) hide show
  1. package/crates/team-agent-core/src/lib.rs +50 -5
  2. package/package.json +1 -1
  3. package/schemas/team.schema.json +1 -0
  4. package/src/team_agent/approvals/__init__.py +65 -0
  5. package/src/team_agent/approvals/constants.py +6 -0
  6. package/src/team_agent/approvals/parsing.py +176 -0
  7. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  8. package/src/team_agent/approvals/status.py +165 -0
  9. package/src/team_agent/cli/__init__.py +135 -0
  10. package/src/team_agent/cli/commands.py +335 -0
  11. package/src/team_agent/cli/e2e.py +202 -0
  12. package/src/team_agent/cli/helpers.py +137 -0
  13. package/src/team_agent/cli/parser.py +470 -0
  14. package/src/team_agent/compiler.py +98 -33
  15. package/src/team_agent/coordinator/__init__.py +53 -0
  16. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  17. package/src/team_agent/coordinator/lifecycle.py +319 -0
  18. package/src/team_agent/coordinator/metadata.py +61 -0
  19. package/src/team_agent/coordinator/paths.py +17 -0
  20. package/src/team_agent/diagnose/__init__.py +48 -0
  21. package/src/team_agent/diagnose/checks.py +101 -0
  22. package/src/team_agent/diagnose/health.py +241 -0
  23. package/src/team_agent/diagnose/preflight.py +194 -0
  24. package/src/team_agent/diagnose/quick_start.py +233 -0
  25. package/src/team_agent/display/__init__.py +61 -0
  26. package/src/team_agent/display/close.py +147 -0
  27. package/src/team_agent/display/ghostty.py +77 -0
  28. package/src/team_agent/display/worker_window.py +110 -0
  29. package/src/team_agent/display/workspace.py +473 -0
  30. package/src/team_agent/launch/__init__.py +41 -0
  31. package/src/team_agent/launch/bootstrap.py +85 -0
  32. package/src/team_agent/launch/config.py +106 -0
  33. package/src/team_agent/launch/core.py +291 -0
  34. package/src/team_agent/launch/requirements.py +57 -0
  35. package/src/team_agent/leader/__init__.py +320 -0
  36. package/src/team_agent/lifecycle/__init__.py +5 -0
  37. package/src/team_agent/lifecycle/agents.py +226 -0
  38. package/src/team_agent/lifecycle/operations.py +321 -0
  39. package/src/team_agent/lifecycle/start.py +360 -0
  40. package/src/team_agent/mcp_server/__init__.py +42 -0
  41. package/src/team_agent/mcp_server/__main__.py +7 -0
  42. package/src/team_agent/mcp_server/contracts.py +148 -0
  43. package/src/team_agent/mcp_server/normalize.py +257 -0
  44. package/src/team_agent/mcp_server/server.py +150 -0
  45. package/src/team_agent/mcp_server/tools.py +205 -0
  46. package/src/team_agent/message_store/__init__.py +23 -0
  47. package/src/team_agent/message_store/agent_health.py +109 -0
  48. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  49. package/src/team_agent/message_store/result_watchers.py +102 -0
  50. package/src/team_agent/message_store/schema.py +266 -0
  51. package/src/team_agent/messaging/__init__.py +1 -0
  52. package/src/team_agent/messaging/activity_detector.py +190 -0
  53. package/src/team_agent/messaging/delivery.py +128 -0
  54. package/src/team_agent/messaging/deps.py +263 -0
  55. package/src/team_agent/messaging/idle_alerts.py +217 -0
  56. package/src/team_agent/messaging/internal_delivery.py +46 -0
  57. package/src/team_agent/messaging/leader.py +317 -0
  58. package/src/team_agent/messaging/leader_panes.py +343 -0
  59. package/src/team_agent/messaging/result_delivery.py +300 -0
  60. package/src/team_agent/messaging/results.py +456 -0
  61. package/src/team_agent/messaging/scheduler.py +418 -0
  62. package/src/team_agent/messaging/send.py +493 -0
  63. package/src/team_agent/messaging/tmux_io.py +337 -0
  64. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  65. package/src/team_agent/orchestrator/__init__.py +376 -0
  66. package/src/team_agent/orchestrator/plan.py +122 -0
  67. package/src/team_agent/orchestrator/state.py +128 -0
  68. package/src/team_agent/profiles/__init__.py +82 -0
  69. package/src/team_agent/profiles/constants.py +19 -0
  70. package/src/team_agent/profiles/core.py +407 -0
  71. package/src/team_agent/profiles/helpers.py +69 -0
  72. package/src/team_agent/profiles/provider_env.py +188 -0
  73. package/src/team_agent/profiles/smoke.py +201 -0
  74. package/src/team_agent/provider_cli/__init__.py +43 -0
  75. package/src/team_agent/provider_cli/adapter.py +167 -0
  76. package/src/team_agent/provider_cli/base.py +48 -0
  77. package/src/team_agent/provider_cli/claude.py +457 -0
  78. package/src/team_agent/provider_cli/codex.py +319 -0
  79. package/src/team_agent/provider_cli/copilot.py +8 -0
  80. package/src/team_agent/provider_cli/fake.py +39 -0
  81. package/src/team_agent/provider_cli/gemini.py +95 -0
  82. package/src/team_agent/provider_cli/opencode.py +8 -0
  83. package/src/team_agent/provider_cli/prompt.py +62 -0
  84. package/src/team_agent/provider_cli/registry.py +18 -0
  85. package/src/team_agent/provider_cli/unsupported.py +32 -0
  86. package/src/team_agent/providers.py +67 -949
  87. package/src/team_agent/quality_gates.py +104 -0
  88. package/src/team_agent/restart/__init__.py +34 -0
  89. package/src/team_agent/restart/orchestration.py +328 -0
  90. package/src/team_agent/restart/selection.py +89 -0
  91. package/src/team_agent/restart/snapshot.py +70 -0
  92. package/src/team_agent/runtime.py +802 -5893
  93. package/src/team_agent/rust_core.py +22 -5
  94. package/src/team_agent/sessions/__init__.py +25 -0
  95. package/src/team_agent/sessions/capture.py +93 -0
  96. package/src/team_agent/sessions/inventory.py +44 -0
  97. package/src/team_agent/sessions/resume.py +135 -0
  98. package/src/team_agent/spec.py +3 -1
  99. package/src/team_agent/state.py +204 -4
  100. package/src/team_agent/status/__init__.py +63 -0
  101. package/src/team_agent/status/approvals.py +52 -0
  102. package/src/team_agent/status/compact.py +158 -0
  103. package/src/team_agent/status/constants.py +18 -0
  104. package/src/team_agent/status/inbox.py +28 -0
  105. package/src/team_agent/status/peek.py +117 -0
  106. package/src/team_agent/status/queries.py +168 -0
  107. package/src/team_agent/terminal.py +57 -0
  108. package/src/team_agent/cli.py +0 -858
  109. package/src/team_agent/mcp_server.py +0 -579
  110. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from team_agent.events import EventLog
8
+ from team_agent.message_store import MessageStore
9
+ from team_agent.messaging.deps import send_message
10
+ from team_agent.messaging.internal_delivery import deliver_stored_message
11
+
12
+ _RESULT_DELIVERY_MAX_ATTEMPTS = 5
13
+ _DELIVERED_RESULT_MESSAGE_STATUSES = {"visible", "submitted", "submitted_unverified", "delivered", "acknowledged"}
14
+
15
+
16
+ def retry_result_deliveries(workspace: Path, event_log: EventLog) -> list[dict[str, Any]]:
17
+ store = MessageStore(workspace)
18
+ notified: list[dict[str, Any]] = []
19
+ for watcher in store.retryable_result_watchers():
20
+ if watcher.get("status") != "notify_failed" or not watcher.get("result_id"):
21
+ continue
22
+ row = store.result_by_id(str(watcher["result_id"]))
23
+ if not row:
24
+ continue
25
+ notified.extend(notify_result_watchers(workspace, _result_entry_from_row(row), event_log, watchers=[watcher]))
26
+ return notified
27
+
28
+
29
+ def notify_result_watchers(
30
+ workspace: Path,
31
+ result: dict[str, Any],
32
+ event_log: EventLog,
33
+ watchers: list[dict[str, Any]] | None = None,
34
+ ) -> list[dict[str, Any]]:
35
+ store = MessageStore(workspace)
36
+ candidates = [
37
+ watcher
38
+ for watcher in (watchers if watchers is not None else store.pending_result_watchers())
39
+ if watcher_matches_result(watcher, result)
40
+ ]
41
+ if not candidates:
42
+ return []
43
+ primary, superseded = _dedupe_watchers_for_result(candidates)
44
+ notified: list[dict[str, Any]] = []
45
+ for stale in superseded:
46
+ store.mark_result_watcher(
47
+ stale["watcher_id"],
48
+ "superseded",
49
+ result_id=result.get("result_id"),
50
+ error="superseded by earlier watcher for same (task_id, agent_id, result_id)",
51
+ )
52
+ event_log.write(
53
+ "result_watcher.superseded",
54
+ watcher_id=stale["watcher_id"],
55
+ result_id=result.get("result_id"),
56
+ task_id=result.get("task_id"),
57
+ agent_id=result.get("agent_id"),
58
+ primary_watcher_id=primary["watcher_id"],
59
+ )
60
+ notified.append(
61
+ {
62
+ "watcher_id": stale["watcher_id"],
63
+ "result_id": result.get("result_id"),
64
+ "ok": False,
65
+ "status": "superseded",
66
+ "primary_watcher_id": primary["watcher_id"],
67
+ }
68
+ )
69
+ attempts = result_delivery_attempts(event_log, primary["watcher_id"], str(result.get("result_id") or ""))
70
+ existing = delivered_result_message(
71
+ store,
72
+ str(result.get("result_id") or ""),
73
+ task_id=result.get("task_id"),
74
+ owner_team_id=primary.get("owner_team_id"),
75
+ )
76
+ if existing:
77
+ notified.append(_mark_watcher_already_delivered(store, event_log, primary, result, attempts, existing))
78
+ return notified
79
+ if attempts >= _RESULT_DELIVERY_MAX_ATTEMPTS:
80
+ notified.append(_mark_delivery_exhausted(store, event_log, primary, result, attempts))
81
+ else:
82
+ notified.append(_deliver_result_to_watcher(workspace, store, event_log, primary, result, attempts))
83
+ return notified
84
+
85
+
86
+ def _dedupe_watchers_for_result(
87
+ watchers: list[dict[str, Any]],
88
+ ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
89
+ ordered = sorted(watchers, key=lambda w: (str(w.get("created_at") or ""), str(w.get("watcher_id") or "")))
90
+ return ordered[0], ordered[1:]
91
+
92
+
93
+ def _deliver_result_to_watcher(
94
+ workspace: Path,
95
+ store: MessageStore,
96
+ event_log: EventLog,
97
+ watcher: dict[str, Any],
98
+ result: dict[str, Any],
99
+ attempts: int,
100
+ ) -> dict[str, Any]:
101
+ try:
102
+ deliver = deliver_stored_message if watcher.get("owner_team_id") else send_message
103
+ delivery = deliver(
104
+ workspace,
105
+ watcher.get("leader_id") or "leader",
106
+ format_result_watcher_notification(result),
107
+ task_id=result.get("task_id"),
108
+ sender="coordinator",
109
+ requires_ack=False,
110
+ wait_visible=False,
111
+ team=watcher.get("owner_team_id"),
112
+ )
113
+ except Exception as exc:
114
+ return _mark_delivery_failed(store, event_log, watcher, result, attempts, str(exc))
115
+ status = "notified" if delivery.get("ok") else "notify_failed"
116
+ error = delivery.get("reason") or delivery.get("error")
117
+ store.mark_result_watcher(
118
+ watcher["watcher_id"],
119
+ status,
120
+ result_id=result.get("result_id"),
121
+ notified_message_id=delivery.get("message_id"),
122
+ error=error,
123
+ )
124
+ event_log.write(
125
+ "result_watcher.notified",
126
+ watcher_id=watcher["watcher_id"],
127
+ result_id=result.get("result_id"),
128
+ task_id=result.get("task_id"),
129
+ agent_id=result.get("agent_id"),
130
+ ok=bool(delivery.get("ok")),
131
+ delivery_status=delivery.get("status"),
132
+ message_id=delivery.get("message_id"),
133
+ error=error,
134
+ attempt=attempts + 1,
135
+ )
136
+ return {
137
+ "watcher_id": watcher["watcher_id"],
138
+ "result_id": result.get("result_id"),
139
+ "ok": bool(delivery.get("ok")),
140
+ "message_id": delivery.get("message_id"),
141
+ }
142
+
143
+
144
+ def _mark_delivery_failed(
145
+ store: MessageStore,
146
+ event_log: EventLog,
147
+ watcher: dict[str, Any],
148
+ result: dict[str, Any],
149
+ attempts: int,
150
+ error: str,
151
+ ) -> dict[str, Any]:
152
+ store.mark_result_watcher(watcher["watcher_id"], "notify_failed", result_id=result.get("result_id"), error=error)
153
+ event_log.write(
154
+ "result_watcher.notify_failed",
155
+ watcher_id=watcher["watcher_id"],
156
+ result_id=result.get("result_id"),
157
+ attempt=attempts + 1,
158
+ error=error,
159
+ )
160
+ return {"watcher_id": watcher["watcher_id"], "result_id": result.get("result_id"), "ok": False, "error": error}
161
+
162
+
163
+ def _mark_watcher_already_delivered(
164
+ store: MessageStore,
165
+ event_log: EventLog,
166
+ watcher: dict[str, Any],
167
+ result: dict[str, Any],
168
+ attempts: int,
169
+ message: dict[str, Any],
170
+ ) -> dict[str, Any]:
171
+ store.mark_result_watcher(
172
+ watcher["watcher_id"],
173
+ "notified",
174
+ result_id=result.get("result_id"),
175
+ notified_message_id=message.get("message_id"),
176
+ )
177
+ event_log.write(
178
+ "result_watcher.notified",
179
+ watcher_id=watcher["watcher_id"],
180
+ result_id=result.get("result_id"),
181
+ task_id=result.get("task_id"),
182
+ agent_id=result.get("agent_id"),
183
+ ok=True,
184
+ delivery_status="already_delivered",
185
+ message_id=message.get("message_id"),
186
+ deduped=True,
187
+ attempt=attempts,
188
+ )
189
+ return {
190
+ "watcher_id": watcher["watcher_id"],
191
+ "result_id": result.get("result_id"),
192
+ "ok": True,
193
+ "message_id": message.get("message_id"),
194
+ "deduped": True,
195
+ }
196
+
197
+
198
+ def _mark_delivery_exhausted(
199
+ store: MessageStore,
200
+ event_log: EventLog,
201
+ watcher: dict[str, Any],
202
+ result: dict[str, Any],
203
+ attempts: int,
204
+ ) -> dict[str, Any]:
205
+ error = "result delivery retry budget exhausted"
206
+ store.mark_result_watcher(watcher["watcher_id"], "delivery_exhausted", result_id=result.get("result_id"), error=error)
207
+ event_log.write(
208
+ "result_delivery_exhausted",
209
+ watcher_id=watcher["watcher_id"],
210
+ result_id=result.get("result_id"),
211
+ task_id=result.get("task_id"),
212
+ agent_id=result.get("agent_id"),
213
+ attempts=attempts,
214
+ last_error=watcher.get("error"),
215
+ )
216
+ return {"watcher_id": watcher["watcher_id"], "result_id": result.get("result_id"), "ok": False, "error": error}
217
+
218
+
219
+ def _result_entry_from_row(row: dict[str, Any]) -> dict[str, Any]:
220
+ envelope = json.loads(row["envelope"])
221
+ return {
222
+ "result_id": row["result_id"],
223
+ "task_id": envelope.get("task_id"),
224
+ "agent_id": envelope.get("agent_id"),
225
+ "status": envelope.get("status"),
226
+ "summary": envelope.get("summary"),
227
+ "tests": envelope.get("tests", []),
228
+ "created_at": row.get("created_at"),
229
+ "scope": "task",
230
+ }
231
+
232
+
233
+ def result_delivery_attempts(event_log: EventLog, watcher_id: str, result_id: str) -> int:
234
+ attempts = 0
235
+ for event in event_log.tail(500):
236
+ if event.get("watcher_id") != watcher_id:
237
+ continue
238
+ if event.get("event") == "result_watcher.requeued":
239
+ attempts = 0
240
+ continue
241
+ if event.get("result_id") != result_id:
242
+ continue
243
+ if event.get("event") in {"result_watcher.notified", "result_watcher.notify_failed"}:
244
+ attempts += 1
245
+ return attempts
246
+
247
+
248
+ def delivered_result_message(
249
+ store: MessageStore,
250
+ result_id: str,
251
+ *,
252
+ task_id: str | None = None,
253
+ owner_team_id: str | None = None,
254
+ ) -> dict[str, Any] | None:
255
+ if not result_id:
256
+ return None
257
+ for message in reversed(store.messages(owner_team_id=owner_team_id)):
258
+ if message.get("recipient") != "leader":
259
+ continue
260
+ if task_id and message.get("task_id") != task_id:
261
+ continue
262
+ if message.get("status") not in _DELIVERED_RESULT_MESSAGE_STATUSES:
263
+ continue
264
+ if f"Result id: {result_id}" in str(message.get("content") or ""):
265
+ return message
266
+ return None
267
+
268
+
269
+ def result_id_from_text(content: str) -> str | None:
270
+ for line in content.splitlines():
271
+ if line.startswith("Result id: "):
272
+ return line.removeprefix("Result id: ").strip() or None
273
+ return None
274
+
275
+
276
+ def watcher_matches_result(watcher: dict[str, Any], result: dict[str, Any]) -> bool:
277
+ task_id = watcher.get("task_id")
278
+ agent_id = watcher.get("agent_id")
279
+ return (not task_id or task_id == result.get("task_id")) and (not agent_id or agent_id == result.get("agent_id"))
280
+
281
+
282
+ def format_result_watcher_notification(result: dict[str, Any]) -> str:
283
+ task_id = result.get("task_id") or "unknown task"
284
+ agent_id = result.get("agent_id") or "unknown agent"
285
+ status = result.get("status") or "unknown"
286
+ summary = result.get("summary") or "completed"
287
+ lines = [
288
+ f"Task {task_id} reported {status} from {agent_id}: {summary}",
289
+ "Team Agent has collected this result and updated team_state.md. No manual polling is needed.",
290
+ ]
291
+ if result.get("result_id"):
292
+ lines.insert(1, f"Result id: {result['result_id']}")
293
+ rendered_tests = [
294
+ f"{test.get('command') or 'test'}={test.get('status') or 'unknown'}"
295
+ for test in (result.get("tests") or [])[:3]
296
+ if isinstance(test, dict)
297
+ ]
298
+ if rendered_tests:
299
+ lines.insert(1, "Tests: " + "; ".join(rendered_tests))
300
+ return "\n".join(lines)