@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.
- package/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/skills/team-agent/SKILL.md +1 -1
- 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 +135 -0
- package/src/team_agent/cli/commands.py +335 -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 +470 -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 +319 -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/start.py +360 -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 +128 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +217 -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/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +418 -0
- package/src/team_agent/messaging/send.py +493 -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 +802 -5740
- 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 +204 -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 -857
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import hashlib
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from team_agent import runtime as _runtime
|
|
10
|
+
from team_agent.errors import RuntimeError
|
|
11
|
+
from team_agent.events import EventLog
|
|
12
|
+
from team_agent.spec import load_spec, validate_spec
|
|
13
|
+
from team_agent.state import (
|
|
14
|
+
SESSION_CAPTURE_FIELDS,
|
|
15
|
+
check_team_owner,
|
|
16
|
+
load_runtime_state,
|
|
17
|
+
resolve_team_scoped_state,
|
|
18
|
+
save_runtime_state,
|
|
19
|
+
save_team_scoped_state,
|
|
20
|
+
write_spec,
|
|
21
|
+
write_team_state,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_RUNTIME_SYMBOLS = (
|
|
26
|
+
"_capture_agent_session",
|
|
27
|
+
"_close_ghostty_display",
|
|
28
|
+
"_close_ghostty_workspace_slot",
|
|
29
|
+
"_effective_runtime_config",
|
|
30
|
+
"_find_agent",
|
|
31
|
+
"_handle_startup_prompts_and_verify_window",
|
|
32
|
+
"_open_ghostty_worker_window",
|
|
33
|
+
"_open_ghostty_workspace_agent_display",
|
|
34
|
+
"_running_agent_state",
|
|
35
|
+
"_runtime_lock",
|
|
36
|
+
"_save_team_runtime_snapshot",
|
|
37
|
+
"_spec_team_dir",
|
|
38
|
+
"_tmux_start_command_for_agent_window",
|
|
39
|
+
"_tmux_window_exists",
|
|
40
|
+
"ensure_workspace_dirs",
|
|
41
|
+
"get_adapter",
|
|
42
|
+
"run_cmd",
|
|
43
|
+
"shell_fork_command_for_agent",
|
|
44
|
+
"start_agent",
|
|
45
|
+
"start_coordinator",
|
|
46
|
+
)
|
|
47
|
+
for _name in _RUNTIME_SYMBOLS:
|
|
48
|
+
if not hasattr(_runtime, _name):
|
|
49
|
+
raise ImportError(f"team_agent.runtime missing lifecycle operation dependency: {_name}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _runtime_proxy(name: str):
|
|
53
|
+
def proxy(*args: Any, **kwargs: Any) -> Any:
|
|
54
|
+
return getattr(_runtime, name)(*args, **kwargs)
|
|
55
|
+
|
|
56
|
+
return proxy
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
globals().update({_name: _runtime_proxy(_name) for _name in _RUNTIME_SYMBOLS})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def stop_agent(workspace: Path, agent_id: str, *, team: str | None = None) -> dict[str, Any]:
|
|
63
|
+
with _runtime_lock(workspace, "stop-agent"):
|
|
64
|
+
state, refusal = resolve_team_scoped_state(workspace, team)
|
|
65
|
+
if refusal:
|
|
66
|
+
return refusal
|
|
67
|
+
gate = check_team_owner(state)
|
|
68
|
+
if gate:
|
|
69
|
+
return gate
|
|
70
|
+
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
71
|
+
spec = load_spec(spec_path)
|
|
72
|
+
agent = _find_agent(spec, agent_id)
|
|
73
|
+
if not agent or spec.get("leader", {}).get("id") == agent_id:
|
|
74
|
+
raise RuntimeError(f"unknown worker agent id: {agent_id}")
|
|
75
|
+
ensure_workspace_dirs(workspace)
|
|
76
|
+
event_log = EventLog(workspace)
|
|
77
|
+
session_name = state.get("session_name") or spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
|
|
78
|
+
agent_state = dict(state.get("agents", {}).get(agent_id) or {"provider": agent["provider"], "agent_id": agent_id})
|
|
79
|
+
window = str(agent_state.get("window") or agent_id)
|
|
80
|
+
target = f"{session_name}:{window}"
|
|
81
|
+
stopped = False
|
|
82
|
+
if _tmux_window_exists(session_name, window):
|
|
83
|
+
proc = run_cmd(["tmux", "kill-window", "-t", target], timeout=10)
|
|
84
|
+
if proc.returncode != 0:
|
|
85
|
+
event_log.write("stop_agent.window_stop_failed", agent_id=agent_id, target=target, stderr=proc.stderr.strip())
|
|
86
|
+
raise RuntimeError(f"failed to stop agent {agent_id}: {proc.stderr.strip()}")
|
|
87
|
+
stopped = True
|
|
88
|
+
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
89
|
+
display = agent_state.get("display") or {}
|
|
90
|
+
if display.get("backend") == "ghostty_workspace":
|
|
91
|
+
_close_ghostty_workspace_slot(agent_id, display, event_log)
|
|
92
|
+
agent_state["display"] = display
|
|
93
|
+
agent_state.update({"status": "stopped", "provider": agent["provider"], "agent_id": agent_id, "window": window})
|
|
94
|
+
state.setdefault("agents", {})[agent_id] = agent_state
|
|
95
|
+
save_team_scoped_state(workspace, state)
|
|
96
|
+
_save_team_runtime_snapshot(workspace, state)
|
|
97
|
+
state_path = write_team_state(workspace, spec, state)
|
|
98
|
+
event_log.write("stop_agent.complete", agent_id=agent_id, target=target, stopped=stopped)
|
|
99
|
+
return {"ok": True, "agent_id": agent_id, "status": "stopped", "target": target, "stopped": stopped, "state_file": str(state_path)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def reset_agent(workspace: Path, agent_id: str, *, discard_session: bool = False, open_display: bool = True, team: str | None = None) -> dict[str, Any]:
|
|
103
|
+
if not discard_session:
|
|
104
|
+
return {"ok": False, "agent_id": agent_id, "status": "refused", "reason": "discard_session_required"}
|
|
105
|
+
state, refusal = resolve_team_scoped_state(workspace, team)
|
|
106
|
+
if refusal:
|
|
107
|
+
return refusal
|
|
108
|
+
gate = check_team_owner(state)
|
|
109
|
+
if gate:
|
|
110
|
+
return gate
|
|
111
|
+
stopped = stop_agent(workspace, agent_id, team=team)
|
|
112
|
+
state, refusal = resolve_team_scoped_state(workspace, team)
|
|
113
|
+
if refusal:
|
|
114
|
+
return refusal
|
|
115
|
+
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
116
|
+
spec = load_spec(spec_path)
|
|
117
|
+
agent_state = dict(state.get("agents", {}).get(agent_id) or {})
|
|
118
|
+
discarded_session_id = agent_state.get("session_id")
|
|
119
|
+
for key in [*SESSION_CAPTURE_FIELDS, "_pending_session_id"]:
|
|
120
|
+
agent_state.pop(key, None)
|
|
121
|
+
agent_state["status"] = "stopped"
|
|
122
|
+
state.setdefault("agents", {})[agent_id] = agent_state
|
|
123
|
+
EventLog(workspace).write("discard.session_tombstone", agent_id=agent_id, discarded_session_id=discarded_session_id)
|
|
124
|
+
save_team_scoped_state(workspace, state)
|
|
125
|
+
write_team_state(workspace, spec, state)
|
|
126
|
+
started = start_agent(workspace, agent_id, force=True, open_display=open_display, allow_fresh=True, team=team)
|
|
127
|
+
EventLog(workspace).write("reset_agent.complete", agent_id=agent_id, stopped=stopped, started=started)
|
|
128
|
+
return {"ok": True, "agent_id": agent_id, "status": "running", "stopped": stopped, "started": started}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def add_agent(workspace: Path, agent_id: str, *, role_file_path: str, open_display: bool = True, team: str | None = None) -> dict[str, Any]:
|
|
132
|
+
from team_agent.compiler import compile_role_doc_agent
|
|
133
|
+
|
|
134
|
+
state, refusal = resolve_team_scoped_state(workspace, team)
|
|
135
|
+
if refusal:
|
|
136
|
+
return refusal
|
|
137
|
+
gate = check_team_owner(state)
|
|
138
|
+
if gate:
|
|
139
|
+
return gate
|
|
140
|
+
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
141
|
+
spec = load_spec(spec_path)
|
|
142
|
+
if _find_agent(spec, agent_id):
|
|
143
|
+
raise RuntimeError(f"agent id already exists: {agent_id}")
|
|
144
|
+
team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
|
|
145
|
+
role_file = Path(role_file_path)
|
|
146
|
+
if not role_file.is_absolute():
|
|
147
|
+
role_file = workspace / role_file
|
|
148
|
+
if not role_file.is_file():
|
|
149
|
+
raise RuntimeError(f"role file not found: {role_file}")
|
|
150
|
+
role_bytes = role_file.read_bytes()
|
|
151
|
+
role_sha = hashlib.sha256(role_bytes).hexdigest()
|
|
152
|
+
dynamic_dir = workspace / ".team" / "dynamic-role-files"
|
|
153
|
+
dynamic_path = dynamic_dir / f"{agent_id}.md"
|
|
154
|
+
old_spec_text = spec_path.read_text(encoding="utf-8")
|
|
155
|
+
old_state = copy.deepcopy(state)
|
|
156
|
+
old_dynamic = dynamic_path.read_bytes() if dynamic_path.exists() else None
|
|
157
|
+
event_log = EventLog(workspace)
|
|
158
|
+
try:
|
|
159
|
+
dynamic_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
dynamic_path.write_bytes(role_bytes)
|
|
161
|
+
agent = compile_role_doc_agent(dynamic_path, team_dir, agent_id)
|
|
162
|
+
spec.setdefault("agents", []).append(agent)
|
|
163
|
+
spec.setdefault("runtime", {}).setdefault("startup_order", []).append(agent_id)
|
|
164
|
+
validate_spec(spec, base_dir=spec_path.parent)
|
|
165
|
+
write_spec(spec_path, spec)
|
|
166
|
+
write_team_state(workspace, spec, state)
|
|
167
|
+
started = start_agent(workspace, agent_id, open_display=open_display, allow_fresh=True, team=team)
|
|
168
|
+
state, _refusal_after = resolve_team_scoped_state(workspace, team)
|
|
169
|
+
state["agents"][agent_id]["dynamic_role_file"] = str(dynamic_path.relative_to(workspace))
|
|
170
|
+
state["agents"][agent_id]["role_file_sha"] = role_sha
|
|
171
|
+
save_team_scoped_state(workspace, state)
|
|
172
|
+
state_path = write_team_state(workspace, spec, state)
|
|
173
|
+
except Exception:
|
|
174
|
+
spec_path.write_text(old_spec_text, encoding="utf-8")
|
|
175
|
+
save_team_scoped_state(workspace, old_state)
|
|
176
|
+
if old_dynamic is None:
|
|
177
|
+
dynamic_path.unlink(missing_ok=True)
|
|
178
|
+
else:
|
|
179
|
+
dynamic_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
dynamic_path.write_bytes(old_dynamic)
|
|
181
|
+
raise
|
|
182
|
+
event_log.write("add_agent.complete", agent_id=agent_id, role_file=str(dynamic_path), role_file_sha=role_sha, started=started)
|
|
183
|
+
return {
|
|
184
|
+
"ok": True,
|
|
185
|
+
"agent_id": agent_id,
|
|
186
|
+
"new_agent_id": agent_id,
|
|
187
|
+
"status": "running",
|
|
188
|
+
"role_file": str(dynamic_path),
|
|
189
|
+
"role_file_sha": role_sha,
|
|
190
|
+
"started": started,
|
|
191
|
+
"state_file": str(state_path),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def fork_agent(
|
|
196
|
+
workspace: Path,
|
|
197
|
+
source_agent_id: str,
|
|
198
|
+
*,
|
|
199
|
+
as_agent_id: str,
|
|
200
|
+
label: str | None = None,
|
|
201
|
+
open_display: bool = True,
|
|
202
|
+
team: str | None = None,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
state, refusal = resolve_team_scoped_state(workspace, team)
|
|
205
|
+
if refusal:
|
|
206
|
+
return refusal
|
|
207
|
+
gate = check_team_owner(state)
|
|
208
|
+
if gate:
|
|
209
|
+
return gate
|
|
210
|
+
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
211
|
+
spec = load_spec(spec_path)
|
|
212
|
+
if _find_agent(spec, as_agent_id):
|
|
213
|
+
raise RuntimeError(f"agent id already exists: {as_agent_id}")
|
|
214
|
+
source_agent = _find_agent(spec, source_agent_id)
|
|
215
|
+
if not source_agent or spec.get("leader", {}).get("id") == source_agent_id:
|
|
216
|
+
raise RuntimeError(f"unknown worker agent id: {source_agent_id}")
|
|
217
|
+
source_state = state.get("agents", {}).get(source_agent_id) or {}
|
|
218
|
+
source_session_id = str(source_state.get("session_id") or "")
|
|
219
|
+
if not source_session_id:
|
|
220
|
+
raise RuntimeError(f"cannot fork {source_agent_id}: source session_id is missing")
|
|
221
|
+
session_name = state.get("session_name") or spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
|
|
222
|
+
if _tmux_window_exists(session_name, as_agent_id):
|
|
223
|
+
raise RuntimeError(f"tmux window already exists for fork target: {session_name}:{as_agent_id}")
|
|
224
|
+
new_agent = copy.deepcopy(source_agent)
|
|
225
|
+
new_agent["id"] = as_agent_id
|
|
226
|
+
new_agent["role"] = str(label or new_agent.get("role") or as_agent_id)
|
|
227
|
+
new_agent["forked_from"] = source_agent_id
|
|
228
|
+
new_agent["preferred_for"] = [as_agent_id, new_agent["role"]]
|
|
229
|
+
old_spec_text = spec_path.read_text(encoding="utf-8")
|
|
230
|
+
old_state = copy.deepcopy(state)
|
|
231
|
+
event_log = EventLog(workspace)
|
|
232
|
+
mcp_path: Path | None = None
|
|
233
|
+
try:
|
|
234
|
+
spec.setdefault("agents", []).append(new_agent)
|
|
235
|
+
spec.setdefault("runtime", {}).setdefault("startup_order", []).append(as_agent_id)
|
|
236
|
+
validate_spec(spec, base_dir=spec_path.parent)
|
|
237
|
+
write_spec(spec_path, spec)
|
|
238
|
+
runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
|
|
239
|
+
adapter = get_adapter(new_agent["provider"])
|
|
240
|
+
if not adapter.supports_session_fork(new_agent):
|
|
241
|
+
raise RuntimeError(f"{new_agent['provider']} does not support native session fork")
|
|
242
|
+
mcp_config = adapter.mcp_config(workspace, as_agent_id)
|
|
243
|
+
mcp_path = adapter.install_mcp(workspace, as_agent_id, mcp_config)
|
|
244
|
+
command_agent = copy.deepcopy(new_agent)
|
|
245
|
+
command_agent["_runtime"] = runtime_cfg
|
|
246
|
+
command = shell_fork_command_for_agent(command_agent, source_session_id, workspace, mcp_config)
|
|
247
|
+
tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, as_agent_id, command)
|
|
248
|
+
event_log.write(
|
|
249
|
+
"fork_agent.agent_start",
|
|
250
|
+
source_agent_id=source_agent_id,
|
|
251
|
+
new_agent_id=as_agent_id,
|
|
252
|
+
provider=new_agent["provider"],
|
|
253
|
+
source_session_id=source_session_id,
|
|
254
|
+
tmux_start_mode=tmux_start_mode,
|
|
255
|
+
command=command,
|
|
256
|
+
mcp_config=str(mcp_path),
|
|
257
|
+
)
|
|
258
|
+
proc = run_cmd(tmux_cmd)
|
|
259
|
+
if proc.returncode != 0:
|
|
260
|
+
raise RuntimeError(f"failed to fork agent {source_agent_id}: {proc.stderr.strip()}")
|
|
261
|
+
if not _handle_startup_prompts_and_verify_window(
|
|
262
|
+
adapter, event_log, "fork_agent", as_agent_id, new_agent["provider"], session_name, "forked"
|
|
263
|
+
):
|
|
264
|
+
raise RuntimeError(f"Failed to fork agent {as_agent_id}: tmux window exited after start")
|
|
265
|
+
spawn_time = datetime.now(timezone.utc)
|
|
266
|
+
agent_state = _running_agent_state(workspace, new_agent, {})
|
|
267
|
+
agent_state.update(
|
|
268
|
+
{
|
|
269
|
+
"mcp_config": str(mcp_path),
|
|
270
|
+
"session_name": session_name,
|
|
271
|
+
"spawned_at": spawn_time.isoformat(),
|
|
272
|
+
"forked_from": source_agent_id,
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
if command_agent.get("_session_id"):
|
|
276
|
+
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
277
|
+
_capture_agent_session(
|
|
278
|
+
workspace,
|
|
279
|
+
as_agent_id,
|
|
280
|
+
agent_state,
|
|
281
|
+
event_log,
|
|
282
|
+
timeout_s=1.5,
|
|
283
|
+
exclude_session_ids={source_session_id},
|
|
284
|
+
)
|
|
285
|
+
if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
|
|
286
|
+
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, as_agent_id, new_agent, event_log)
|
|
287
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
288
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(session_name, as_agent_id, new_agent, {}, event_log)
|
|
289
|
+
state.setdefault("agents", {})[as_agent_id] = agent_state
|
|
290
|
+
save_team_scoped_state(workspace, state)
|
|
291
|
+
_save_team_runtime_snapshot(workspace, state)
|
|
292
|
+
state_path = write_team_state(workspace, spec, state)
|
|
293
|
+
coordinator = start_coordinator(workspace)
|
|
294
|
+
except Exception:
|
|
295
|
+
if _tmux_window_exists(session_name, as_agent_id):
|
|
296
|
+
run_cmd(["tmux", "kill-window", "-t", f"{session_name}:{as_agent_id}"], timeout=10)
|
|
297
|
+
if mcp_path is not None:
|
|
298
|
+
try:
|
|
299
|
+
get_adapter(new_agent["provider"]).cleanup_mcp(workspace, as_agent_id, mcp_path)
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
event_log.write("fork_agent.mcp_cleanup_failed", new_agent_id=as_agent_id, error=str(exc))
|
|
302
|
+
spec_path.write_text(old_spec_text, encoding="utf-8")
|
|
303
|
+
save_team_scoped_state(workspace, old_state)
|
|
304
|
+
raise
|
|
305
|
+
event_log.write(
|
|
306
|
+
"fork_agent.complete",
|
|
307
|
+
source_agent_id=source_agent_id,
|
|
308
|
+
new_agent_id=as_agent_id,
|
|
309
|
+
session_id=state["agents"][as_agent_id].get("session_id"),
|
|
310
|
+
coordinator=coordinator,
|
|
311
|
+
)
|
|
312
|
+
return {
|
|
313
|
+
"ok": True,
|
|
314
|
+
"source_agent_id": source_agent_id,
|
|
315
|
+
"new_agent_id": as_agent_id,
|
|
316
|
+
"agent_id": as_agent_id,
|
|
317
|
+
"status": "running",
|
|
318
|
+
"session_id": state["agents"][as_agent_id].get("session_id"),
|
|
319
|
+
"state_file": str(state_path),
|
|
320
|
+
"coordinator": coordinator,
|
|
321
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
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
|
+
tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, agent_id, command)
|
|
223
|
+
event_log.write(
|
|
224
|
+
"start_agent.agent_start",
|
|
225
|
+
agent_id=agent_id,
|
|
226
|
+
provider=agent["provider"],
|
|
227
|
+
start_mode=start_mode,
|
|
228
|
+
session_id=previous.get("session_id"),
|
|
229
|
+
session=session_name,
|
|
230
|
+
window=agent_id,
|
|
231
|
+
tmux_start_mode=tmux_start_mode,
|
|
232
|
+
command=command,
|
|
233
|
+
mcp_config=str(mcp_path),
|
|
234
|
+
)
|
|
235
|
+
proc = run_cmd(tmux_cmd)
|
|
236
|
+
if proc.returncode != 0:
|
|
237
|
+
try:
|
|
238
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
241
|
+
event_log.write("start_agent.agent_failed", agent_id=agent_id, stderr=proc.stderr, stdout=proc.stdout)
|
|
242
|
+
raise RuntimeError(f"Failed to start agent {agent_id}: {proc.stderr.strip()}")
|
|
243
|
+
|
|
244
|
+
if not _handle_startup_prompts_and_verify_window(
|
|
245
|
+
adapter, event_log, "start_agent", agent_id, agent["provider"], session_name, start_mode
|
|
246
|
+
):
|
|
247
|
+
if start_mode != "resumed":
|
|
248
|
+
try:
|
|
249
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
252
|
+
raise RuntimeError(f"Failed to start agent {agent_id}: tmux window exited after start")
|
|
253
|
+
if not allow_fresh:
|
|
254
|
+
try:
|
|
255
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
256
|
+
except Exception as cleanup_exc:
|
|
257
|
+
event_log.write(
|
|
258
|
+
"start_agent.mcp_cleanup_failed",
|
|
259
|
+
agent_id=agent_id,
|
|
260
|
+
provider=agent["provider"],
|
|
261
|
+
mcp_config=str(mcp_path),
|
|
262
|
+
error=str(cleanup_exc),
|
|
263
|
+
)
|
|
264
|
+
raise RuntimeError(
|
|
265
|
+
f"Cannot resume agent {agent_id}: resume window exited or did not become visible. "
|
|
266
|
+
"Use team-agent start-agent --allow-fresh only if losing that worker context is acceptable."
|
|
267
|
+
)
|
|
268
|
+
event_log.write(
|
|
269
|
+
"start_agent.resume_window_missing_fallback_fresh",
|
|
270
|
+
agent_id=agent_id,
|
|
271
|
+
provider=agent["provider"],
|
|
272
|
+
session_id=previous.get("session_id"),
|
|
273
|
+
)
|
|
274
|
+
command = shell_command_for_agent(command_agent, workspace, mcp_config)
|
|
275
|
+
start_mode = "fresh_after_missing_rollout" if missing_resume_rollout else "fresh"
|
|
276
|
+
tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, agent_id, command)
|
|
277
|
+
event_log.write(
|
|
278
|
+
"start_agent.agent_start",
|
|
279
|
+
agent_id=agent_id,
|
|
280
|
+
provider=agent["provider"],
|
|
281
|
+
start_mode=start_mode,
|
|
282
|
+
session_id=None,
|
|
283
|
+
session=session_name,
|
|
284
|
+
window=agent_id,
|
|
285
|
+
tmux_start_mode=tmux_start_mode,
|
|
286
|
+
command=command,
|
|
287
|
+
mcp_config=str(mcp_path),
|
|
288
|
+
)
|
|
289
|
+
proc = run_cmd(tmux_cmd)
|
|
290
|
+
if proc.returncode != 0:
|
|
291
|
+
try:
|
|
292
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
293
|
+
except Exception as exc:
|
|
294
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
295
|
+
event_log.write("start_agent.agent_failed", agent_id=agent_id, stderr=proc.stderr, stdout=proc.stdout)
|
|
296
|
+
raise RuntimeError(f"Failed to start agent {agent_id} fresh after resume exit: {proc.stderr.strip()}")
|
|
297
|
+
if not _handle_startup_prompts_and_verify_window(
|
|
298
|
+
adapter, event_log, "start_agent", agent_id, agent["provider"], session_name, start_mode
|
|
299
|
+
):
|
|
300
|
+
try:
|
|
301
|
+
adapter.cleanup_mcp(workspace, agent_id, mcp_path)
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
event_log.write("start_agent.mcp_cleanup_failed", agent_id=agent_id, provider=agent["provider"], error=str(exc))
|
|
304
|
+
raise RuntimeError(f"Failed to start agent {agent_id} fresh: tmux window exited after start")
|
|
305
|
+
if runtime_cfg.get("fast") and agent.get("provider") == "codex":
|
|
306
|
+
fast_result = _enable_codex_fast_mode(session_name, agent_id)
|
|
307
|
+
event_log.write("start_agent.codex_fast_mode", agent_id=agent_id, **fast_result)
|
|
308
|
+
|
|
309
|
+
spawn_time = datetime.now(timezone.utc)
|
|
310
|
+
agent_state = _running_agent_state(workspace, agent, previous)
|
|
311
|
+
agent_state.update({"mcp_config": str(mcp_path), "session_name": session_name, "spawned_at": spawn_time.isoformat()})
|
|
312
|
+
profile_launch = command_agent.get("_provider_profile") or {}
|
|
313
|
+
if profile_launch.get("claude_projects_root"):
|
|
314
|
+
agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
|
|
315
|
+
if start_mode in {"fresh", "fresh_after_missing_rollout"}:
|
|
316
|
+
_clear_session_capture_fields(agent_state)
|
|
317
|
+
if command_agent.get("_session_id"):
|
|
318
|
+
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
319
|
+
_capture_agent_session(workspace, agent_id, agent_state, event_log, timeout_s=1.5, exclude_session_ids=known_session_ids)
|
|
320
|
+
if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
|
|
321
|
+
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
322
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
323
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(
|
|
324
|
+
session_name,
|
|
325
|
+
agent_id,
|
|
326
|
+
agent,
|
|
327
|
+
previous.get("display") or {},
|
|
328
|
+
event_log,
|
|
329
|
+
)
|
|
330
|
+
state["agents"][agent_id] = agent_state
|
|
331
|
+
save_team_scoped_state(workspace, state)
|
|
332
|
+
store = MessageStore(workspace)
|
|
333
|
+
delivered_messages: list[str] = []
|
|
334
|
+
for row in store.messages():
|
|
335
|
+
if row["recipient"] == agent_id and row["status"] in {"pending", "accepted"}:
|
|
336
|
+
delivered = _deliver_pending_message(workspace, state, row["message_id"], wait_visible=True, timeout=30.0)
|
|
337
|
+
if delivered.get("ok"):
|
|
338
|
+
delivered_messages.append(row["message_id"])
|
|
339
|
+
event_log.write("send.pending_delivered", message_id=row["message_id"], agent_id=agent_id, source="start_agent")
|
|
340
|
+
write_team_state(workspace, spec, state)
|
|
341
|
+
coordinator = start_coordinator(workspace)
|
|
342
|
+
event_log.write(
|
|
343
|
+
"start_agent.complete",
|
|
344
|
+
agent_id=agent_id,
|
|
345
|
+
session=session_name,
|
|
346
|
+
start_mode=start_mode,
|
|
347
|
+
delivered_messages=delivered_messages,
|
|
348
|
+
coordinator=coordinator,
|
|
349
|
+
)
|
|
350
|
+
return {
|
|
351
|
+
"ok": True,
|
|
352
|
+
"agent_id": agent_id,
|
|
353
|
+
"status": "running",
|
|
354
|
+
"start_mode": start_mode,
|
|
355
|
+
"session_id": agent_state.get("session_id"),
|
|
356
|
+
"target": target,
|
|
357
|
+
"display_target": agent_state.get("display"),
|
|
358
|
+
"delivered_messages": delivered_messages,
|
|
359
|
+
"coordinator": coordinator,
|
|
360
|
+
}
|