@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,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))
|