@team-agent/installer 0.1.10 → 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 (111) 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/skills/team-agent/SKILL.md +1 -1
  5. package/src/team_agent/approvals/__init__.py +65 -0
  6. package/src/team_agent/approvals/constants.py +6 -0
  7. package/src/team_agent/approvals/parsing.py +176 -0
  8. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  9. package/src/team_agent/approvals/status.py +165 -0
  10. package/src/team_agent/cli/__init__.py +135 -0
  11. package/src/team_agent/cli/commands.py +335 -0
  12. package/src/team_agent/cli/e2e.py +202 -0
  13. package/src/team_agent/cli/helpers.py +137 -0
  14. package/src/team_agent/cli/parser.py +470 -0
  15. package/src/team_agent/compiler.py +98 -33
  16. package/src/team_agent/coordinator/__init__.py +53 -0
  17. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  18. package/src/team_agent/coordinator/lifecycle.py +319 -0
  19. package/src/team_agent/coordinator/metadata.py +61 -0
  20. package/src/team_agent/coordinator/paths.py +17 -0
  21. package/src/team_agent/diagnose/__init__.py +48 -0
  22. package/src/team_agent/diagnose/checks.py +101 -0
  23. package/src/team_agent/diagnose/health.py +241 -0
  24. package/src/team_agent/diagnose/preflight.py +194 -0
  25. package/src/team_agent/diagnose/quick_start.py +233 -0
  26. package/src/team_agent/display/__init__.py +61 -0
  27. package/src/team_agent/display/close.py +147 -0
  28. package/src/team_agent/display/ghostty.py +77 -0
  29. package/src/team_agent/display/worker_window.py +110 -0
  30. package/src/team_agent/display/workspace.py +473 -0
  31. package/src/team_agent/launch/__init__.py +41 -0
  32. package/src/team_agent/launch/bootstrap.py +85 -0
  33. package/src/team_agent/launch/config.py +106 -0
  34. package/src/team_agent/launch/core.py +291 -0
  35. package/src/team_agent/launch/requirements.py +57 -0
  36. package/src/team_agent/leader/__init__.py +320 -0
  37. package/src/team_agent/lifecycle/__init__.py +5 -0
  38. package/src/team_agent/lifecycle/agents.py +226 -0
  39. package/src/team_agent/lifecycle/operations.py +321 -0
  40. package/src/team_agent/lifecycle/start.py +360 -0
  41. package/src/team_agent/mcp_server/__init__.py +42 -0
  42. package/src/team_agent/mcp_server/__main__.py +7 -0
  43. package/src/team_agent/mcp_server/contracts.py +148 -0
  44. package/src/team_agent/mcp_server/normalize.py +257 -0
  45. package/src/team_agent/mcp_server/server.py +150 -0
  46. package/src/team_agent/mcp_server/tools.py +205 -0
  47. package/src/team_agent/message_store/__init__.py +23 -0
  48. package/src/team_agent/message_store/agent_health.py +109 -0
  49. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  50. package/src/team_agent/message_store/result_watchers.py +102 -0
  51. package/src/team_agent/message_store/schema.py +266 -0
  52. package/src/team_agent/messaging/__init__.py +1 -0
  53. package/src/team_agent/messaging/activity_detector.py +190 -0
  54. package/src/team_agent/messaging/delivery.py +128 -0
  55. package/src/team_agent/messaging/deps.py +263 -0
  56. package/src/team_agent/messaging/idle_alerts.py +217 -0
  57. package/src/team_agent/messaging/internal_delivery.py +46 -0
  58. package/src/team_agent/messaging/leader.py +317 -0
  59. package/src/team_agent/messaging/leader_panes.py +343 -0
  60. package/src/team_agent/messaging/result_delivery.py +300 -0
  61. package/src/team_agent/messaging/results.py +456 -0
  62. package/src/team_agent/messaging/scheduler.py +418 -0
  63. package/src/team_agent/messaging/send.py +493 -0
  64. package/src/team_agent/messaging/tmux_io.py +337 -0
  65. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  66. package/src/team_agent/orchestrator/__init__.py +376 -0
  67. package/src/team_agent/orchestrator/plan.py +122 -0
  68. package/src/team_agent/orchestrator/state.py +128 -0
  69. package/src/team_agent/profiles/__init__.py +82 -0
  70. package/src/team_agent/profiles/constants.py +19 -0
  71. package/src/team_agent/profiles/core.py +407 -0
  72. package/src/team_agent/profiles/helpers.py +69 -0
  73. package/src/team_agent/profiles/provider_env.py +188 -0
  74. package/src/team_agent/profiles/smoke.py +201 -0
  75. package/src/team_agent/provider_cli/__init__.py +43 -0
  76. package/src/team_agent/provider_cli/adapter.py +167 -0
  77. package/src/team_agent/provider_cli/base.py +48 -0
  78. package/src/team_agent/provider_cli/claude.py +457 -0
  79. package/src/team_agent/provider_cli/codex.py +319 -0
  80. package/src/team_agent/provider_cli/copilot.py +8 -0
  81. package/src/team_agent/provider_cli/fake.py +39 -0
  82. package/src/team_agent/provider_cli/gemini.py +95 -0
  83. package/src/team_agent/provider_cli/opencode.py +8 -0
  84. package/src/team_agent/provider_cli/prompt.py +62 -0
  85. package/src/team_agent/provider_cli/registry.py +18 -0
  86. package/src/team_agent/provider_cli/unsupported.py +32 -0
  87. package/src/team_agent/providers.py +67 -949
  88. package/src/team_agent/quality_gates.py +104 -0
  89. package/src/team_agent/restart/__init__.py +34 -0
  90. package/src/team_agent/restart/orchestration.py +328 -0
  91. package/src/team_agent/restart/selection.py +89 -0
  92. package/src/team_agent/restart/snapshot.py +70 -0
  93. package/src/team_agent/runtime.py +802 -5740
  94. package/src/team_agent/rust_core.py +22 -5
  95. package/src/team_agent/sessions/__init__.py +25 -0
  96. package/src/team_agent/sessions/capture.py +93 -0
  97. package/src/team_agent/sessions/inventory.py +44 -0
  98. package/src/team_agent/sessions/resume.py +135 -0
  99. package/src/team_agent/spec.py +3 -1
  100. package/src/team_agent/state.py +204 -4
  101. package/src/team_agent/status/__init__.py +63 -0
  102. package/src/team_agent/status/approvals.py +52 -0
  103. package/src/team_agent/status/compact.py +158 -0
  104. package/src/team_agent/status/constants.py +18 -0
  105. package/src/team_agent/status/inbox.py +28 -0
  106. package/src/team_agent/status/peek.py +117 -0
  107. package/src/team_agent/status/queries.py +168 -0
  108. package/src/team_agent/terminal.py +57 -0
  109. package/src/team_agent/cli.py +0 -857
  110. package/src/team_agent/mcp_server.py +0 -579
  111. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ from typing import Any
7
+
8
+
9
+ DANGEROUS_LEADER_FLAGS = (
10
+ ("claude", "--dangerously-skip-permissions"),
11
+ ("claude", "--dangerously-skip-permission"),
12
+ ("codex", "--dangerously-bypass-approvals-and-sandbox"),
13
+ )
14
+
15
+
16
+ def effective_runtime_config(runtime_cfg: dict[str, Any]) -> dict[str, Any]:
17
+ # Route via the runtime alias surface so tests patching
18
+ # team_agent.runtime._detect_inherited_dangerous_permissions still take effect.
19
+ from team_agent.runtime import _detect_inherited_dangerous_permissions
20
+ effective = dict(runtime_cfg)
21
+ if effective.get("dangerous_auto_approve"):
22
+ effective["dangerous_auto_approve_source"] = "runtime_config"
23
+ effective["dangerous_auto_approve_inherited"] = False
24
+ return effective
25
+ inherited = _detect_inherited_dangerous_permissions()
26
+ if not inherited.get("enabled"):
27
+ effective["dangerous_auto_approve"] = False
28
+ effective["dangerous_auto_approve_source"] = "disabled"
29
+ effective["dangerous_auto_approve_inherited"] = False
30
+ return effective
31
+ effective["dangerous_auto_approve"] = True
32
+ effective["dangerous_auto_approve_source"] = "leader_process"
33
+ effective["dangerous_auto_approve_inherited"] = True
34
+ effective["dangerous_auto_approve_provider"] = inherited.get("provider")
35
+ effective["dangerous_auto_approve_flag"] = inherited.get("flag")
36
+ return effective
37
+
38
+
39
+ def requires_direct_leader_receiver(spec: dict[str, Any], runtime_cfg: dict[str, Any]) -> bool:
40
+ if runtime_cfg.get("require_leader_receiver") is not None:
41
+ return bool(runtime_cfg.get("require_leader_receiver"))
42
+ return any(agent.get("provider") != "fake" for agent in spec.get("agents", []))
43
+
44
+
45
+ def detect_inherited_dangerous_permissions() -> dict[str, Any]:
46
+ # Route via runtime alias so existing patches of
47
+ # team_agent.runtime._process_ancestry take effect at call time.
48
+ from team_agent.runtime import _process_ancestry
49
+ for proc in _process_ancestry(os.getpid()):
50
+ command = str(proc.get("command") or "")
51
+ for provider, flag in DANGEROUS_LEADER_FLAGS:
52
+ if command_has_flag(command, flag):
53
+ return {
54
+ "enabled": True,
55
+ "provider": provider,
56
+ "flag": flag,
57
+ "pid": proc.get("pid"),
58
+ }
59
+ return {"enabled": False}
60
+
61
+
62
+ def command_has_flag(command: str, flag: str) -> bool:
63
+ return re.search(rf"(?<!\S){re.escape(flag)}(?!\S)", command) is not None
64
+
65
+
66
+ def process_ancestry(pid: int, max_depth: int = 12) -> list[dict[str, Any]]:
67
+ ancestry: list[dict[str, Any]] = []
68
+ current = pid
69
+ seen: set[int] = set()
70
+ for _ in range(max_depth):
71
+ if current in seen or current <= 0:
72
+ break
73
+ seen.add(current)
74
+ info = process_info(current)
75
+ if not info:
76
+ break
77
+ ancestry.append(info)
78
+ parent = info.get("ppid")
79
+ if not isinstance(parent, int) or parent <= 1 or parent == current:
80
+ break
81
+ current = parent
82
+ return ancestry
83
+
84
+
85
+ def process_info(pid: int) -> dict[str, Any] | None:
86
+ try:
87
+ proc = subprocess.run(
88
+ ["ps", "-p", str(pid), "-o", "ppid=", "-o", "command="],
89
+ text=True,
90
+ capture_output=True,
91
+ timeout=2,
92
+ check=False,
93
+ )
94
+ except (OSError, subprocess.TimeoutExpired):
95
+ return None
96
+ if proc.returncode != 0:
97
+ return None
98
+ line = proc.stdout.strip()
99
+ if not line:
100
+ return None
101
+ parts = line.split(None, 1)
102
+ try:
103
+ ppid = int(parts[0])
104
+ except (IndexError, ValueError):
105
+ return None
106
+ return {"pid": pid, "ppid": ppid, "command": parts[1] if len(parts) > 1 else ""}
@@ -0,0 +1,291 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from team_agent.events import EventLog
8
+ from team_agent.launch.bootstrap import (
9
+ attach_team_profile_dirs,
10
+ spec_team_dir,
11
+ tmux_session_conflict_error,
12
+ )
13
+ from team_agent.launch.config import effective_runtime_config, requires_direct_leader_receiver
14
+ from team_agent.launch.requirements import ensure_agent_start_requirements
15
+ from team_agent.message_store import MessageStore
16
+ from team_agent.permissions import resolve_permissions
17
+ from team_agent.routing import route_task
18
+ from team_agent.spec import load_spec, workspace_from_spec
19
+ from team_agent.state import (
20
+ load_runtime_state,
21
+ merge_workspace_team_state,
22
+ populate_team_owner_from_env,
23
+ save_runtime_state,
24
+ write_team_state,
25
+ )
26
+
27
+
28
+ def launch(
29
+ spec_path: Path,
30
+ dry_run: bool = False,
31
+ auto_approve: bool = False,
32
+ skip_profile_smoke: bool = False,
33
+ ) -> dict[str, Any]:
34
+ from team_agent.runtime import (
35
+ GHOSTTY_DISPLAY_BACKENDS,
36
+ RuntimeError,
37
+ _attach_leader_to_state,
38
+ _capture_agent_session,
39
+ _enable_codex_fast_mode,
40
+ _open_worker_displays,
41
+ _save_team_runtime_snapshot,
42
+ _tmux_session_exists,
43
+ ensure_workspace_dirs,
44
+ get_adapter,
45
+ get_adapter_or_raise,
46
+ run_cmd,
47
+ shell_command_for_agent,
48
+ )
49
+
50
+ spec = load_spec(spec_path)
51
+ workspace = workspace_from_spec(spec, spec_path)
52
+ team_dir = spec_team_dir(spec_path, workspace)
53
+ attach_team_profile_dirs(spec, spec_path, workspace, team_dir)
54
+ ensure_workspace_dirs(workspace)
55
+ event_log = EventLog(workspace)
56
+ session_name = spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
57
+ state = {
58
+ "spec_path": str(spec_path.resolve()),
59
+ "workspace": str(workspace),
60
+ "team_dir": str(team_dir),
61
+ "session_name": session_name,
62
+ "leader": spec.get("leader"),
63
+ "agents": {},
64
+ "tasks": [dict(task) for task in spec.get("tasks", [])],
65
+ "display_backend": spec.get("runtime", {}).get("display_backend", "none"),
66
+ }
67
+ runtime_cfg = effective_runtime_config(spec.get("runtime", {}))
68
+ dangerous_auto_approve = bool(runtime_cfg.get("dangerous_auto_approve"))
69
+ dangerous_inherited = bool(runtime_cfg.get("dangerous_auto_approve_inherited"))
70
+
71
+ routing_decisions: list[dict[str, Any]] = []
72
+ for task in state["tasks"]:
73
+ route = route_task(spec, task)
74
+ task["assignee"] = route["agent_id"]
75
+ decision = {
76
+ "source": "launch",
77
+ "task_id": task.get("id"),
78
+ "selected_agent": route["agent_id"],
79
+ "reason": route["reason"],
80
+ "manual_override": False,
81
+ }
82
+ routing_decisions.append(decision)
83
+ event_log.write("routing.decision", **decision)
84
+
85
+ permission_summary = [resolve_permissions(agent) for agent in spec.get("agents", [])]
86
+ event_log.write(
87
+ "launch.permissions_resolved",
88
+ permissions=permission_summary,
89
+ dangerous_auto_approve=dangerous_auto_approve,
90
+ dangerous_auto_approve_source=runtime_cfg.get("dangerous_auto_approve_source"),
91
+ )
92
+ if dry_run:
93
+ return {
94
+ "ok": True,
95
+ "dry_run": True,
96
+ "session_name": session_name,
97
+ "permissions": permission_summary,
98
+ "routes": routing_decisions,
99
+ "safety": {
100
+ "dangerous_auto_approve": dangerous_auto_approve,
101
+ "dangerous_auto_approve_source": runtime_cfg.get("dangerous_auto_approve_source"),
102
+ "dangerous_auto_approve_inherited": dangerous_inherited,
103
+ "requires_explicit_yes": dangerous_auto_approve and not dangerous_inherited,
104
+ },
105
+ }
106
+ if dangerous_auto_approve:
107
+ event_log.write(
108
+ "launch.dangerous_auto_approve_requested",
109
+ reason="provider may bypass approvals or sandbox",
110
+ source=runtime_cfg.get("dangerous_auto_approve_source"),
111
+ inherited=dangerous_inherited,
112
+ inherited_provider=runtime_cfg.get("dangerous_auto_approve_provider"),
113
+ inherited_flag=runtime_cfg.get("dangerous_auto_approve_flag"),
114
+ )
115
+ if dangerous_auto_approve and not dangerous_inherited and not auto_approve:
116
+ raise RuntimeError("dangerous_auto_approve requires explicit --yes after reviewing launch risk")
117
+ if runtime_cfg.get("require_user_approval_before_launch", True) and not auto_approve:
118
+ raise RuntimeError("launch requires approval; rerun with --yes after reviewing resolved permissions")
119
+
120
+ tmux = get_adapter_or_raise("tmux")
121
+ _ = tmux
122
+ if _tmux_session_exists(session_name):
123
+ event_log.write(
124
+ "launch.session_conflict",
125
+ session=session_name,
126
+ action="use a different team name or runtime.session_name; do not terminate existing tmux sessions from startup",
127
+ )
128
+ raise RuntimeError(tmux_session_conflict_error(session_name))
129
+ ensure_agent_start_requirements(
130
+ workspace,
131
+ spec.get("agents", []),
132
+ event_log,
133
+ "launch",
134
+ skip_profile_smoke=skip_profile_smoke,
135
+ )
136
+
137
+ leader_receiver = None
138
+ leader_provider = state.get("leader", {}).get("provider")
139
+ require_leader_receiver = requires_direct_leader_receiver(spec, runtime_cfg)
140
+ if runtime_cfg.get("auto_attach_leader", True) and leader_provider != "fake":
141
+ try:
142
+ leader_receiver, _ = _attach_leader_to_state(
143
+ workspace,
144
+ state,
145
+ pane=None,
146
+ provider=leader_provider,
147
+ event_log=event_log,
148
+ source="launch",
149
+ require_current=require_leader_receiver,
150
+ )
151
+ except RuntimeError as exc:
152
+ event_log.write(
153
+ "leader_receiver.auto_attach_skipped",
154
+ provider=leader_provider,
155
+ reason=str(exc),
156
+ required=require_leader_receiver,
157
+ suggestion="Start the leader with `team-agent codex` or run quick-start from an existing tmux pane.",
158
+ )
159
+ if require_leader_receiver:
160
+ raise
161
+
162
+ first = True
163
+ started: list[dict[str, Any]] = []
164
+ display_jobs: list[tuple[str, dict[str, Any]]] = []
165
+ for agent in spec.get("agents", []):
166
+ if agent.get("paused"):
167
+ state["agents"][agent["id"]] = {"status": "paused", "provider": agent["provider"]}
168
+ continue
169
+ adapter = get_adapter(agent["provider"])
170
+ if not adapter.is_installed():
171
+ event_log.write(
172
+ "launch.provider_missing",
173
+ agent_id=agent["id"],
174
+ provider=agent["provider"],
175
+ command=adapter.command_name,
176
+ )
177
+ raise RuntimeError(
178
+ f"Provider {agent['provider']} command {adapter.command_name!r} not found for agent {agent['id']}"
179
+ )
180
+ mcp_config = adapter.mcp_config(workspace, agent["id"])
181
+ mcp_path = adapter.install_mcp(workspace, agent["id"], mcp_config)
182
+ command_agent = dict(agent)
183
+ command_agent["_runtime"] = runtime_cfg
184
+ command = shell_command_for_agent(command_agent, workspace, mcp_config)
185
+ spawn_time = datetime.now(timezone.utc)
186
+ event_log.write(
187
+ "launch.agent_start",
188
+ agent_id=agent["id"],
189
+ provider=agent["provider"],
190
+ session=session_name,
191
+ window=agent["id"],
192
+ command=command,
193
+ mcp_config=str(mcp_path),
194
+ )
195
+ if first:
196
+ proc = run_cmd(["tmux", "new-session", "-d", "-s", session_name, "-n", agent["id"], "sh", "-lc", command])
197
+ first = False
198
+ else:
199
+ proc = run_cmd(["tmux", "new-window", "-t", session_name, "-n", agent["id"], "sh", "-lc", command])
200
+ if proc.returncode != 0:
201
+ try:
202
+ adapter.cleanup_mcp(workspace, agent["id"], mcp_path)
203
+ except Exception as exc:
204
+ event_log.write(
205
+ "launch.mcp_cleanup_failed",
206
+ agent_id=agent["id"],
207
+ provider=agent["provider"],
208
+ mcp_config=str(mcp_path),
209
+ error=str(exc),
210
+ )
211
+ event_log.write(
212
+ "launch.agent_failed",
213
+ agent_id=agent["id"],
214
+ stderr=proc.stderr,
215
+ stdout=proc.stdout,
216
+ )
217
+ raise RuntimeError(f"Failed to start agent {agent['id']}: {proc.stderr.strip()}")
218
+ handled_prompts = adapter.handle_startup_prompts(session_name, agent["id"], checks=1, sleep_s=0.0)
219
+ for prompt_event in handled_prompts:
220
+ event_log.write(
221
+ "launch.startup_prompt_handled",
222
+ agent_id=agent["id"],
223
+ provider=agent["provider"],
224
+ **prompt_event,
225
+ )
226
+ if runtime_cfg.get("fast") and agent.get("provider") == "codex":
227
+ fast_result = _enable_codex_fast_mode(session_name, agent["id"])
228
+ event_log.write("launch.codex_fast_mode", agent_id=agent["id"], **fast_result)
229
+ state["agents"][agent["id"]] = {
230
+ "status": "running",
231
+ "provider": agent["provider"],
232
+ "agent_id": agent["id"],
233
+ "model": agent.get("model"),
234
+ "auth_mode": agent.get("auth_mode"),
235
+ "profile": agent.get("profile"),
236
+ "window": agent["id"],
237
+ "mcp_config": str(mcp_path),
238
+ "permissions": resolve_permissions(agent),
239
+ "session_id": None,
240
+ "rollout_path": None,
241
+ "captured_at": None,
242
+ "captured_via": None,
243
+ "attribution_confidence": None,
244
+ "spawn_cwd": str(workspace),
245
+ "spawned_at": spawn_time.isoformat(),
246
+ }
247
+ profile_launch = command_agent.get("_provider_profile") or {}
248
+ if profile_launch.get("claude_projects_root"):
249
+ state["agents"][agent["id"]]["claude_projects_root"] = profile_launch["claude_projects_root"]
250
+ if command_agent.get("_session_id"):
251
+ state["agents"][agent["id"]]["_pending_session_id"] = command_agent["_session_id"]
252
+ known_session_ids = {
253
+ str(item.get("session_id"))
254
+ for aid, item in state.get("agents", {}).items()
255
+ if aid != agent["id"] and item.get("session_id")
256
+ }
257
+ _capture_agent_session(
258
+ workspace,
259
+ agent["id"],
260
+ state["agents"][agent["id"]],
261
+ event_log,
262
+ timeout_s=1.5,
263
+ exclude_session_ids=known_session_ids,
264
+ )
265
+ if state.get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
266
+ display_jobs.append((agent["id"], agent))
267
+ started.append({"agent_id": agent["id"], "provider": agent["provider"], "window": agent["id"]})
268
+ for agent_id, display in _open_worker_displays(
269
+ workspace,
270
+ session_name,
271
+ display_jobs,
272
+ event_log,
273
+ state.get("display_backend", "none"),
274
+ ).items():
275
+ if agent_id in state["agents"]:
276
+ state["agents"][agent_id]["display"] = display
277
+ populate_team_owner_from_env(state, source="launch")
278
+ workspace_state = merge_workspace_team_state(load_runtime_state(workspace), state)
279
+ save_runtime_state(workspace, workspace_state)
280
+ _save_team_runtime_snapshot(workspace, state)
281
+ MessageStore(workspace)
282
+ write_team_state(workspace, spec, state)
283
+ event_log.write("launch.complete", session=session_name, started=started)
284
+ return {
285
+ "ok": True,
286
+ "session_name": session_name,
287
+ "agents": started,
288
+ "permissions": permission_summary,
289
+ "routes": routing_decisions,
290
+ "leader_receiver": leader_receiver,
291
+ }
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from team_agent.diagnose import (
7
+ compact_model_checks,
8
+ format_model_check_failures,
9
+ format_profile_check_failures,
10
+ format_profile_smoke_failures,
11
+ model_checks_for_agents,
12
+ profile_checks_for_agents,
13
+ profile_smoke_checks_for_agents,
14
+ )
15
+ from team_agent.events import EventLog
16
+ from team_agent.profiles import compact_profile_check
17
+
18
+
19
+ def ensure_agent_start_requirements(
20
+ workspace: Path,
21
+ agents: list[dict[str, Any]],
22
+ event_log: EventLog,
23
+ event_prefix: str,
24
+ skip_profile_smoke: bool = False,
25
+ ) -> None:
26
+ from team_agent.runtime import RuntimeError, get_adapter
27
+ active_agents = [agent for agent in agents if not agent.get("paused")]
28
+ for agent in active_agents:
29
+ adapter = get_adapter(agent["provider"])
30
+ if not adapter.is_installed():
31
+ event_log.write(
32
+ f"{event_prefix}.provider_missing",
33
+ agent_id=agent["id"],
34
+ provider=agent["provider"],
35
+ command=adapter.command_name,
36
+ )
37
+ raise RuntimeError(
38
+ f"Provider {agent['provider']} command {adapter.command_name!r} not found for agent {agent['id']}"
39
+ )
40
+ profile_checks = profile_checks_for_agents(workspace, active_agents)
41
+ profile_failures = [item for item in profile_checks if item.get("ok") is False]
42
+ event_log.write(f"{event_prefix}.profile_check", ok=not profile_failures, checks=[compact_profile_check(item) for item in profile_checks])
43
+ if profile_failures:
44
+ raise RuntimeError(format_profile_check_failures(profile_failures))
45
+ if skip_profile_smoke:
46
+ event_log.write(f"{event_prefix}.profile_smoke_check", ok=True, skipped=True, reason="already_checked")
47
+ else:
48
+ smoke_checks = profile_smoke_checks_for_agents(workspace, active_agents)
49
+ smoke_failures = [item for item in smoke_checks if item.get("ok") is False]
50
+ event_log.write(f"{event_prefix}.profile_smoke_check", ok=not smoke_failures, checks=[compact_profile_check(item) for item in smoke_checks])
51
+ if smoke_failures:
52
+ raise RuntimeError(format_profile_smoke_failures(smoke_failures))
53
+ checks = model_checks_for_agents(active_agents, workspace)
54
+ failures = [item for item in checks if item.get("ok") is False]
55
+ event_log.write(f"{event_prefix}.model_check", ok=not failures, checks=compact_model_checks(checks))
56
+ if failures:
57
+ raise RuntimeError(format_model_check_failures(failures))