@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,363 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from team_agent import runtime as _runtime
|
|
9
|
+
from team_agent.errors import RuntimeError
|
|
10
|
+
from team_agent.events import EventLog
|
|
11
|
+
from team_agent.message_store import MessageStore
|
|
12
|
+
from team_agent.providers import ResumeUnavailable
|
|
13
|
+
from team_agent.spec import load_spec
|
|
14
|
+
from team_agent.state import (
|
|
15
|
+
check_team_owner,
|
|
16
|
+
load_runtime_state,
|
|
17
|
+
resolve_team_scoped_state,
|
|
18
|
+
save_runtime_state,
|
|
19
|
+
save_team_scoped_state,
|
|
20
|
+
write_team_state,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_RUNTIME_SYMBOLS = (
|
|
25
|
+
"_attach_profile_resume_root",
|
|
26
|
+
"_attach_team_profile_dirs",
|
|
27
|
+
"_capture_agent_session",
|
|
28
|
+
"_clear_session_capture_fields",
|
|
29
|
+
"_deliver_pending_message",
|
|
30
|
+
"_effective_runtime_config",
|
|
31
|
+
"_enable_codex_fast_mode",
|
|
32
|
+
"_ensure_agent_start_requirements",
|
|
33
|
+
"_find_agent",
|
|
34
|
+
"_handle_startup_prompts_and_verify_window",
|
|
35
|
+
"_open_ghostty_worker_window",
|
|
36
|
+
"_open_ghostty_workspace_agent_display",
|
|
37
|
+
"_prepare_resume_state",
|
|
38
|
+
"_running_agent_state",
|
|
39
|
+
"_runtime_lock",
|
|
40
|
+
"_spec_team_dir",
|
|
41
|
+
"_tmux_start_command_for_agent_window",
|
|
42
|
+
"_tmux_window_exists",
|
|
43
|
+
"ensure_workspace_dirs",
|
|
44
|
+
"get_adapter",
|
|
45
|
+
"run_cmd",
|
|
46
|
+
"shell_command_for_agent",
|
|
47
|
+
"shell_resume_command_for_agent",
|
|
48
|
+
"start_coordinator",
|
|
49
|
+
)
|
|
50
|
+
for _name in _RUNTIME_SYMBOLS:
|
|
51
|
+
if not hasattr(_runtime, _name):
|
|
52
|
+
raise ImportError(f"team_agent.runtime missing lifecycle start dependency: {_name}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _runtime_proxy(name: str):
|
|
56
|
+
def proxy(*args: Any, **kwargs: Any) -> Any:
|
|
57
|
+
return getattr(_runtime, name)(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
return proxy
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
globals().update({_name: _runtime_proxy(_name) for _name in _RUNTIME_SYMBOLS})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resume_rollout_missing(agent: dict[str, Any], previous: dict[str, Any]) -> bool:
|
|
66
|
+
if agent.get("provider") != "codex" or not previous.get("session_id"):
|
|
67
|
+
return False
|
|
68
|
+
rollout_path = previous.get("rollout_path")
|
|
69
|
+
return not rollout_path or not Path(str(rollout_path)).exists()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def start_agent(
|
|
73
|
+
workspace: Path,
|
|
74
|
+
agent_id: str,
|
|
75
|
+
force: bool = False,
|
|
76
|
+
open_display: bool = True,
|
|
77
|
+
allow_fresh: bool = False,
|
|
78
|
+
team: str | None = None,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
with _runtime_lock(workspace, "start-agent"):
|
|
81
|
+
return _start_agent_unlocked(workspace, agent_id, force=force, open_display=open_display, allow_fresh=allow_fresh, team=team)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_display: bool, allow_fresh: bool, team: str | None = None) -> dict[str, Any]:
|
|
85
|
+
state, refusal = resolve_team_scoped_state(workspace, team)
|
|
86
|
+
if refusal:
|
|
87
|
+
return refusal
|
|
88
|
+
gate = check_team_owner(state)
|
|
89
|
+
if gate:
|
|
90
|
+
return gate
|
|
91
|
+
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
92
|
+
if not spec_path.exists():
|
|
93
|
+
raise RuntimeError(f"missing spec for start-agent: {spec_path}")
|
|
94
|
+
spec = load_spec(spec_path)
|
|
95
|
+
team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
|
|
96
|
+
_attach_team_profile_dirs(spec, spec_path, workspace, team_dir)
|
|
97
|
+
agent = _find_agent(spec, agent_id)
|
|
98
|
+
if not agent or spec.get("leader", {}).get("id") == agent_id:
|
|
99
|
+
raise RuntimeError(f"unknown worker agent id: {agent_id}")
|
|
100
|
+
if agent.get("paused"):
|
|
101
|
+
return {"ok": False, "status": "paused", "agent_id": agent_id, "reason": "agent_paused"}
|
|
102
|
+
ensure_workspace_dirs(workspace)
|
|
103
|
+
event_log = EventLog(workspace)
|
|
104
|
+
runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
|
|
105
|
+
session_name = state.get("session_name") or spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
|
|
106
|
+
state["session_name"] = session_name
|
|
107
|
+
state.setdefault("workspace", str(workspace))
|
|
108
|
+
state.setdefault("team_dir", str(team_dir))
|
|
109
|
+
state.setdefault("spec_path", str(spec_path.resolve()))
|
|
110
|
+
state.setdefault("leader", spec.get("leader"))
|
|
111
|
+
state.setdefault("tasks", [dict(task) for task in spec.get("tasks", [])])
|
|
112
|
+
state.setdefault("agents", {})
|
|
113
|
+
state["display_backend"] = spec.get("runtime", {}).get("display_backend", state.get("display_backend") or "none")
|
|
114
|
+
|
|
115
|
+
previous = state.get("agents", {}).get(agent_id, {})
|
|
116
|
+
target = f"{session_name}:{agent_id}"
|
|
117
|
+
window_present = _tmux_window_exists(session_name, agent_id)
|
|
118
|
+
if window_present and not force:
|
|
119
|
+
agent_state = _running_agent_state(workspace, agent, previous)
|
|
120
|
+
agent_state["session_name"] = session_name
|
|
121
|
+
if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
|
|
122
|
+
display = agent_state.get("display") or {}
|
|
123
|
+
if display.get("status") != "opened":
|
|
124
|
+
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
125
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
126
|
+
display = agent_state.get("display") or {}
|
|
127
|
+
if display.get("status") != "opened":
|
|
128
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(session_name, agent_id, agent, display, event_log)
|
|
129
|
+
state["agents"][agent_id] = agent_state
|
|
130
|
+
save_team_scoped_state(workspace, state)
|
|
131
|
+
write_team_state(workspace, spec, state)
|
|
132
|
+
coordinator = start_coordinator(workspace)
|
|
133
|
+
event_log.write("start_agent.noop", agent_id=agent_id, target=target, coordinator=coordinator)
|
|
134
|
+
return {"ok": True, "agent_id": agent_id, "status": "running", "start_mode": "noop", "target": target, "coordinator": coordinator}
|
|
135
|
+
|
|
136
|
+
if window_present and force:
|
|
137
|
+
proc = run_cmd(["tmux", "kill-window", "-t", target], timeout=10)
|
|
138
|
+
if proc.returncode != 0:
|
|
139
|
+
raise RuntimeError(f"failed to replace existing agent window {target}: {proc.stderr.strip()}")
|
|
140
|
+
|
|
141
|
+
_ensure_agent_start_requirements(workspace, [agent], event_log, "start_agent")
|
|
142
|
+
adapter = get_adapter(agent["provider"])
|
|
143
|
+
if not adapter.is_installed():
|
|
144
|
+
event_log.write("start_agent.provider_missing", agent_id=agent_id, provider=agent["provider"], command=adapter.command_name)
|
|
145
|
+
raise RuntimeError(f"Provider {agent['provider']} command {adapter.command_name!r} not found for agent {agent_id}")
|
|
146
|
+
mcp_config = adapter.mcp_config(workspace, agent_id)
|
|
147
|
+
mcp_path = adapter.install_mcp(workspace, agent_id, mcp_config)
|
|
148
|
+
command_agent = copy.deepcopy(agent)
|
|
149
|
+
command_agent["_runtime"] = runtime_cfg
|
|
150
|
+
previous = _attach_profile_resume_root(workspace, command_agent, previous)
|
|
151
|
+
known_session_ids = {
|
|
152
|
+
str(item.get("session_id"))
|
|
153
|
+
for aid, item in state.get("agents", {}).items()
|
|
154
|
+
if aid != agent_id and item.get("session_id")
|
|
155
|
+
}
|
|
156
|
+
try:
|
|
157
|
+
previous = _prepare_resume_state(
|
|
158
|
+
workspace,
|
|
159
|
+
agent_id,
|
|
160
|
+
previous,
|
|
161
|
+
adapter,
|
|
162
|
+
event_log,
|
|
163
|
+
known_session_ids,
|
|
164
|
+
allow_fresh_on_resume_failure=allow_fresh,
|
|
165
|
+
)
|
|
166
|
+
except ResumeUnavailable as exc:
|
|
167
|
+
try:
|
|
168
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
169
|
+
except Exception as cleanup_exc:
|
|
170
|
+
event_log.write(
|
|
171
|
+
"start_agent.mcp_cleanup_failed",
|
|
172
|
+
agent_id=agent_id,
|
|
173
|
+
provider=agent["provider"],
|
|
174
|
+
mcp_config=str(mcp_path),
|
|
175
|
+
error=str(cleanup_exc),
|
|
176
|
+
)
|
|
177
|
+
raise RuntimeError(str(exc)) from exc
|
|
178
|
+
missing_resume_rollout = _resume_rollout_missing(agent, previous)
|
|
179
|
+
start_mode = "resumed" if previous.get("session_id") else "fresh"
|
|
180
|
+
if missing_resume_rollout and allow_fresh:
|
|
181
|
+
event_log.write(
|
|
182
|
+
"start_agent.resume_window_missing_fallback_fresh",
|
|
183
|
+
agent_id=agent_id,
|
|
184
|
+
provider=agent["provider"],
|
|
185
|
+
session_id=previous.get("session_id"),
|
|
186
|
+
reason="rollout_missing",
|
|
187
|
+
)
|
|
188
|
+
start_mode = "fresh_after_missing_rollout"
|
|
189
|
+
previous = dict(previous)
|
|
190
|
+
previous["session_id"] = None
|
|
191
|
+
if start_mode == "resumed":
|
|
192
|
+
try:
|
|
193
|
+
command = shell_resume_command_for_agent(command_agent, previous, workspace, mcp_config)
|
|
194
|
+
except ResumeUnavailable as exc:
|
|
195
|
+
event_log.write("start_agent.resume_unavailable", agent_id=agent_id, error=str(exc))
|
|
196
|
+
if not allow_fresh:
|
|
197
|
+
try:
|
|
198
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
199
|
+
except Exception as cleanup_exc:
|
|
200
|
+
event_log.write(
|
|
201
|
+
"start_agent.mcp_cleanup_failed",
|
|
202
|
+
agent_id=agent_id,
|
|
203
|
+
provider=agent["provider"],
|
|
204
|
+
mcp_config=str(mcp_path),
|
|
205
|
+
error=str(cleanup_exc),
|
|
206
|
+
)
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Cannot resume agent {agent_id}: {exc}. "
|
|
209
|
+
"Use team-agent start-agent --allow-fresh only if losing that worker context is acceptable."
|
|
210
|
+
) from exc
|
|
211
|
+
command = shell_command_for_agent(command_agent, workspace, mcp_config)
|
|
212
|
+
start_mode = "fresh"
|
|
213
|
+
else:
|
|
214
|
+
command = shell_command_for_agent(command_agent, workspace, mcp_config)
|
|
215
|
+
event_log.write(
|
|
216
|
+
"start_agent.fresh_spawn",
|
|
217
|
+
agent_id=agent_id,
|
|
218
|
+
provider=agent["provider"],
|
|
219
|
+
reason="rollout_missing" if start_mode == "fresh_after_missing_rollout" else "session_id_missing",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
from team_agent.lifecycle.paste_buffer_hygiene import cleanup_stale_team_agent_buffers
|
|
223
|
+
cleanup_stale_team_agent_buffers(workspace, event_log, context=f"start_agent:{agent_id}")
|
|
224
|
+
tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, agent_id, command)
|
|
225
|
+
event_log.write(
|
|
226
|
+
"start_agent.agent_start",
|
|
227
|
+
agent_id=agent_id,
|
|
228
|
+
provider=agent["provider"],
|
|
229
|
+
start_mode=start_mode,
|
|
230
|
+
session_id=previous.get("session_id"),
|
|
231
|
+
session=session_name,
|
|
232
|
+
window=agent_id,
|
|
233
|
+
tmux_start_mode=tmux_start_mode,
|
|
234
|
+
command=command,
|
|
235
|
+
mcp_config=str(mcp_path),
|
|
236
|
+
)
|
|
237
|
+
proc = run_cmd(tmux_cmd)
|
|
238
|
+
if proc.returncode != 0:
|
|
239
|
+
try:
|
|
240
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
243
|
+
event_log.write("start_agent.agent_failed", agent_id=agent_id, stderr=proc.stderr, stdout=proc.stdout)
|
|
244
|
+
raise RuntimeError(f"Failed to start agent {agent_id}: {proc.stderr.strip()}")
|
|
245
|
+
|
|
246
|
+
if not _handle_startup_prompts_and_verify_window(
|
|
247
|
+
adapter, event_log, "start_agent", agent_id, agent["provider"], session_name, start_mode
|
|
248
|
+
):
|
|
249
|
+
if start_mode != "resumed":
|
|
250
|
+
try:
|
|
251
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
254
|
+
raise RuntimeError(f"Failed to start agent {agent_id}: tmux window exited after start")
|
|
255
|
+
if not allow_fresh:
|
|
256
|
+
try:
|
|
257
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
258
|
+
except Exception as cleanup_exc:
|
|
259
|
+
event_log.write(
|
|
260
|
+
"start_agent.mcp_cleanup_failed",
|
|
261
|
+
agent_id=agent_id,
|
|
262
|
+
provider=agent["provider"],
|
|
263
|
+
mcp_config=str(mcp_path),
|
|
264
|
+
error=str(cleanup_exc),
|
|
265
|
+
)
|
|
266
|
+
raise RuntimeError(
|
|
267
|
+
f"Cannot resume agent {agent_id}: resume window exited or did not become visible. "
|
|
268
|
+
"Use team-agent start-agent --allow-fresh only if losing that worker context is acceptable."
|
|
269
|
+
)
|
|
270
|
+
event_log.write(
|
|
271
|
+
"start_agent.resume_window_missing_fallback_fresh",
|
|
272
|
+
agent_id=agent_id,
|
|
273
|
+
provider=agent["provider"],
|
|
274
|
+
session_id=previous.get("session_id"),
|
|
275
|
+
)
|
|
276
|
+
command = shell_command_for_agent(command_agent, workspace, mcp_config)
|
|
277
|
+
start_mode = "fresh_after_missing_rollout" if missing_resume_rollout else "fresh"
|
|
278
|
+
cleanup_stale_team_agent_buffers(workspace, event_log, context=f"start_agent_fallback:{agent_id}")
|
|
279
|
+
tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, agent_id, command)
|
|
280
|
+
event_log.write(
|
|
281
|
+
"start_agent.agent_start",
|
|
282
|
+
agent_id=agent_id,
|
|
283
|
+
provider=agent["provider"],
|
|
284
|
+
start_mode=start_mode,
|
|
285
|
+
session_id=None,
|
|
286
|
+
session=session_name,
|
|
287
|
+
window=agent_id,
|
|
288
|
+
tmux_start_mode=tmux_start_mode,
|
|
289
|
+
command=command,
|
|
290
|
+
mcp_config=str(mcp_path),
|
|
291
|
+
)
|
|
292
|
+
proc = run_cmd(tmux_cmd)
|
|
293
|
+
if proc.returncode != 0:
|
|
294
|
+
try:
|
|
295
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
296
|
+
except Exception as exc:
|
|
297
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
298
|
+
event_log.write("start_agent.agent_failed", agent_id=agent_id, stderr=proc.stderr, stdout=proc.stdout)
|
|
299
|
+
raise RuntimeError(f"Failed to start agent {agent_id} fresh after resume exit: {proc.stderr.strip()}")
|
|
300
|
+
if not _handle_startup_prompts_and_verify_window(
|
|
301
|
+
adapter, event_log, "start_agent", agent_id, agent["provider"], session_name, start_mode
|
|
302
|
+
):
|
|
303
|
+
try:
|
|
304
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
307
|
+
raise RuntimeError(f"Failed to start agent {agent_id} fresh: tmux window exited after start")
|
|
308
|
+
if runtime_cfg.get("fast") and agent.get("provider") == "codex":
|
|
309
|
+
fast_result = _enable_codex_fast_mode(session_name, agent_id)
|
|
310
|
+
event_log.write("start_agent.codex_fast_mode", agent_id=agent_id, **fast_result)
|
|
311
|
+
|
|
312
|
+
spawn_time = datetime.now(timezone.utc)
|
|
313
|
+
agent_state = _running_agent_state(workspace, agent, previous)
|
|
314
|
+
agent_state.update({"mcp_config": str(mcp_path), "session_name": session_name, "spawned_at": spawn_time.isoformat()})
|
|
315
|
+
profile_launch = command_agent.get("_provider_profile") or {}
|
|
316
|
+
if profile_launch.get("claude_projects_root"):
|
|
317
|
+
agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
|
|
318
|
+
if start_mode in {"fresh", "fresh_after_missing_rollout"}:
|
|
319
|
+
_clear_session_capture_fields(agent_state)
|
|
320
|
+
if command_agent.get("_session_id"):
|
|
321
|
+
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
322
|
+
_capture_agent_session(workspace, agent_id, agent_state, event_log, timeout_s=1.5, exclude_session_ids=known_session_ids)
|
|
323
|
+
if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
|
|
324
|
+
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
325
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
326
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(
|
|
327
|
+
session_name,
|
|
328
|
+
agent_id,
|
|
329
|
+
agent,
|
|
330
|
+
previous.get("display") or {},
|
|
331
|
+
event_log,
|
|
332
|
+
)
|
|
333
|
+
state["agents"][agent_id] = agent_state
|
|
334
|
+
save_team_scoped_state(workspace, state)
|
|
335
|
+
store = MessageStore(workspace)
|
|
336
|
+
delivered_messages: list[str] = []
|
|
337
|
+
for row in store.messages():
|
|
338
|
+
if row["recipient"] == agent_id and row["status"] in {"pending", "accepted"}:
|
|
339
|
+
delivered = _deliver_pending_message(workspace, state, row["message_id"], wait_visible=True, timeout=30.0)
|
|
340
|
+
if delivered.get("ok"):
|
|
341
|
+
delivered_messages.append(row["message_id"])
|
|
342
|
+
event_log.write("send.pending_delivered", message_id=row["message_id"], agent_id=agent_id, source="start_agent")
|
|
343
|
+
write_team_state(workspace, spec, state)
|
|
344
|
+
coordinator = start_coordinator(workspace)
|
|
345
|
+
event_log.write(
|
|
346
|
+
"start_agent.complete",
|
|
347
|
+
agent_id=agent_id,
|
|
348
|
+
session=session_name,
|
|
349
|
+
start_mode=start_mode,
|
|
350
|
+
delivered_messages=delivered_messages,
|
|
351
|
+
coordinator=coordinator,
|
|
352
|
+
)
|
|
353
|
+
return {
|
|
354
|
+
"ok": True,
|
|
355
|
+
"agent_id": agent_id,
|
|
356
|
+
"status": "running",
|
|
357
|
+
"start_mode": start_mode,
|
|
358
|
+
"session_id": agent_state.get("session_id"),
|
|
359
|
+
"target": target,
|
|
360
|
+
"display_target": agent_state.get("display"),
|
|
361
|
+
"delivered_messages": delivered_messages,
|
|
362
|
+
"coordinator": coordinator,
|
|
363
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent import runtime
|
|
4
|
+
from team_agent.mcp_server.contracts import TOOLS
|
|
5
|
+
from team_agent.mcp_server.normalize import (
|
|
6
|
+
_compact_tool_result,
|
|
7
|
+
_normalize_report_envelope,
|
|
8
|
+
_items,
|
|
9
|
+
_text,
|
|
10
|
+
_first_text,
|
|
11
|
+
_normalize_result_status,
|
|
12
|
+
_normalize_changes,
|
|
13
|
+
_normalize_change_kind,
|
|
14
|
+
_normalize_tests,
|
|
15
|
+
_normalize_test_status,
|
|
16
|
+
_normalize_risks,
|
|
17
|
+
_normalize_artifacts,
|
|
18
|
+
_normalize_next_actions,
|
|
19
|
+
)
|
|
20
|
+
from team_agent.mcp_server.server import dispatch, handle_mcp, main
|
|
21
|
+
from team_agent.mcp_server.tools import TeamOrchestratorTools
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'TeamOrchestratorTools',
|
|
25
|
+
'TOOLS',
|
|
26
|
+
'dispatch',
|
|
27
|
+
'handle_mcp',
|
|
28
|
+
'main',
|
|
29
|
+
'_compact_tool_result',
|
|
30
|
+
'_normalize_report_envelope',
|
|
31
|
+
'_items',
|
|
32
|
+
'_text',
|
|
33
|
+
'_first_text',
|
|
34
|
+
'_normalize_result_status',
|
|
35
|
+
'_normalize_changes',
|
|
36
|
+
'_normalize_change_kind',
|
|
37
|
+
'_normalize_tests',
|
|
38
|
+
'_normalize_test_status',
|
|
39
|
+
'_normalize_risks',
|
|
40
|
+
'_normalize_artifacts',
|
|
41
|
+
'_normalize_next_actions',
|
|
42
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
TOOLS = [
|
|
5
|
+
{
|
|
6
|
+
"name": "assign_task",
|
|
7
|
+
"description": "Add or update a task in the team graph and deliver it to its assignee.",
|
|
8
|
+
"inputSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": ["task"],
|
|
11
|
+
"properties": {
|
|
12
|
+
"task": {"type": "object"},
|
|
13
|
+
"message": {"type": "string"},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "send_message",
|
|
19
|
+
"description": "Send a message to a teammate, the leader, or '*' for all other team members. Provide only target and content; Team Agent fills sender, task id, ack policy, and delivery metadata.",
|
|
20
|
+
"inputSchema": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["to", "content"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"to": {
|
|
25
|
+
"oneOf": [
|
|
26
|
+
{"type": "string"},
|
|
27
|
+
{"type": "array", "items": {"type": "string"}, "minItems": 1},
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"content": {"type": "string"},
|
|
31
|
+
},
|
|
32
|
+
"additionalProperties": False,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "report_result",
|
|
37
|
+
"description": "Report task completion. Provide a short summary and optional status/details; Team Agent fills schema_version, task_id, and agent_id, and normalizes common change/test field aliases.",
|
|
38
|
+
"inputSchema": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["summary"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"summary": {"type": "string"},
|
|
43
|
+
"status": {"type": "string", "enum": ["success", "blocked", "failed", "partial"]},
|
|
44
|
+
"changes": {"type": "array", "items": {"type": "object"}},
|
|
45
|
+
"tests": {"type": "array", "items": {"type": "object"}},
|
|
46
|
+
"risks": {"type": "array", "items": {"type": "object"}},
|
|
47
|
+
"artifacts": {"type": "array", "items": {"type": "object"}},
|
|
48
|
+
"next_actions": {"type": "array", "items": {"type": "object"}},
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": False,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "update_state",
|
|
55
|
+
"description": "Append a note to team state and rewrite team_state.md.",
|
|
56
|
+
"inputSchema": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["note"],
|
|
59
|
+
"properties": {"note": {"type": "string"}},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "get_team_status",
|
|
64
|
+
"description": "Return machine-readable team status.",
|
|
65
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "stop_agent",
|
|
69
|
+
"description": "Hard-stop one running worker while preserving its session for start_agent resume.",
|
|
70
|
+
"inputSchema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"required": ["agent_id"],
|
|
73
|
+
"properties": {"agent_id": {"type": "string"}},
|
|
74
|
+
"additionalProperties": False,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "reset_agent",
|
|
79
|
+
"description": "Reset one worker to a fresh session. discard_session must be true.",
|
|
80
|
+
"inputSchema": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"required": ["agent_id", "discard_session"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"agent_id": {"type": "string"},
|
|
85
|
+
"discard_session": {"type": "boolean"},
|
|
86
|
+
},
|
|
87
|
+
"additionalProperties": False,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "add_agent",
|
|
92
|
+
"description": "Add a first-class worker from a workspace-relative role file.",
|
|
93
|
+
"inputSchema": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"required": ["new_agent_id", "role_file_path"],
|
|
96
|
+
"properties": {
|
|
97
|
+
"new_agent_id": {"type": "string"},
|
|
98
|
+
"role_file_path": {"type": "string"},
|
|
99
|
+
},
|
|
100
|
+
"additionalProperties": False,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "fork_agent",
|
|
105
|
+
"description": "Fork a running worker using the provider's native branch/fork support.",
|
|
106
|
+
"inputSchema": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"required": ["source_agent_id", "as_agent_id"],
|
|
109
|
+
"properties": {
|
|
110
|
+
"source_agent_id": {"type": "string"},
|
|
111
|
+
"as_agent_id": {"type": "string"},
|
|
112
|
+
"label": {"type": "string"},
|
|
113
|
+
},
|
|
114
|
+
"additionalProperties": False,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "request_human",
|
|
119
|
+
"description": "Ask the leader/user for human input.",
|
|
120
|
+
"inputSchema": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"required": ["question"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"question": {"type": "string"},
|
|
125
|
+
"task_id": {"type": "string"},
|
|
126
|
+
"agent_id": {"type": "string"},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "stuck_list",
|
|
132
|
+
"description": "List manually suppressed idle-triggered alerts for this team.",
|
|
133
|
+
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "stuck_cancel",
|
|
137
|
+
"description": "Suppress repeated stuck/idle alerts for one agent until meaningful agent state changes.",
|
|
138
|
+
"inputSchema": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"required": ["agent_id"],
|
|
141
|
+
"properties": {
|
|
142
|
+
"agent_id": {"type": "string"},
|
|
143
|
+
"alert_type": {"type": "string", "enum": ["stuck", "idle_fallback", "all"]},
|
|
144
|
+
},
|
|
145
|
+
"additionalProperties": False,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]
|