@team-agent/installer 0.3.1 → 0.3.2
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +38 -7
- package/crates/team-agent/src/cli/emit.rs +7 -6
- package/crates/team-agent/src/cli/mod.rs +623 -21
- package/crates/team-agent/src/cli/status_port.rs +170 -44
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/health.rs +9 -0
- package/crates/team-agent/src/lifecycle/launch.rs +271 -58
- package/crates/team-agent/src/lifecycle/restart/common.rs +65 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +57 -15
- package/crates/team-agent/src/lifecycle/restart/remove.rs +5 -1
- package/crates/team-agent/src/lifecycle/restart.rs +20 -0
- package/crates/team-agent/src/messaging/delivery.rs +397 -36
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/messaging/results.rs +200 -47
- package/crates/team-agent/src/provider/adapter.rs +95 -10
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/state/persist.rs +113 -1
- package/crates/team-agent/src/state/projection.rs +127 -3
- package/crates/team-agent/src/tmux_backend.rs +66 -6
- package/package.json +4 -4
|
@@ -44,6 +44,26 @@ pub fn launch_with_transport(
|
|
|
44
44
|
auto_approve: bool,
|
|
45
45
|
skip_profile_smoke: bool,
|
|
46
46
|
transport: &dyn Transport,
|
|
47
|
+
) -> Result<LaunchReport, LifecycleError> {
|
|
48
|
+
let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
|
|
49
|
+
let workspace = team_workspace(team_dir);
|
|
50
|
+
launch_with_transport_in_workspace(
|
|
51
|
+
&workspace,
|
|
52
|
+
spec_path,
|
|
53
|
+
dry_run,
|
|
54
|
+
auto_approve,
|
|
55
|
+
skip_profile_smoke,
|
|
56
|
+
transport,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn launch_with_transport_in_workspace(
|
|
61
|
+
workspace: &Path,
|
|
62
|
+
spec_path: &Path,
|
|
63
|
+
dry_run: bool,
|
|
64
|
+
auto_approve: bool,
|
|
65
|
+
skip_profile_smoke: bool,
|
|
66
|
+
transport: &dyn Transport,
|
|
47
67
|
) -> Result<LaunchReport, LifecycleError> {
|
|
48
68
|
let _ = skip_profile_smoke;
|
|
49
69
|
if !spec_path.exists() {
|
|
@@ -76,13 +96,13 @@ pub fn launch_with_transport(
|
|
|
76
96
|
raw: serde_json::json!({"source": "compiled_spec"}),
|
|
77
97
|
})
|
|
78
98
|
.collect::<Vec<_>>();
|
|
79
|
-
write_launch_permission_audit(
|
|
99
|
+
write_launch_permission_audit(workspace, &safety)?;
|
|
80
100
|
let routes = spec_routes(&spec);
|
|
81
101
|
let started = if dry_run {
|
|
82
102
|
Vec::new()
|
|
83
103
|
} else {
|
|
84
|
-
let started = spawn_agents(spec_path, &spec, &session_name, &safety, transport)?;
|
|
85
|
-
persist_spawn_agent_state(spec_path, &spec, &session_name, transport, &started)?;
|
|
104
|
+
let started = spawn_agents(workspace, spec_path, &spec, &session_name, &safety, transport)?;
|
|
105
|
+
persist_spawn_agent_state(workspace, spec_path, &spec, &session_name, transport, &started)?;
|
|
86
106
|
started
|
|
87
107
|
};
|
|
88
108
|
Ok(LaunchReport {
|
|
@@ -97,6 +117,7 @@ pub fn launch_with_transport(
|
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
fn spawn_agents(
|
|
120
|
+
workspace: &Path,
|
|
100
121
|
spec_path: &Path,
|
|
101
122
|
spec: &Value,
|
|
102
123
|
session_name: &SessionName,
|
|
@@ -104,7 +125,6 @@ fn spawn_agents(
|
|
|
104
125
|
transport: &dyn Transport,
|
|
105
126
|
) -> Result<Vec<StartedAgent>, LifecycleError> {
|
|
106
127
|
let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
|
|
107
|
-
let workspace = team_workspace(team_dir);
|
|
108
128
|
let mut started = Vec::new();
|
|
109
129
|
for agent in spec_agent_values(spec) {
|
|
110
130
|
let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
|
|
@@ -135,13 +155,12 @@ fn spawn_agents(
|
|
|
135
155
|
let tools = worker_tool_refs(agent_tool_strings(agent), safety);
|
|
136
156
|
let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
137
157
|
let mcp_team_id =
|
|
138
|
-
runtime_active_team_key_for_spawn(
|
|
139
|
-
let process_team_id = process_team_id_for_spawn(&workspace, spec);
|
|
158
|
+
runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
|
|
140
159
|
let mcp_config = adapter
|
|
141
160
|
.mcp_config(auth_mode)
|
|
142
161
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
143
|
-
let mcp_config = resolve_mcp_config(mcp_config,
|
|
144
|
-
let mcp_config_path = write_worker_mcp_config(
|
|
162
|
+
let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
|
|
163
|
+
let mcp_config_path = write_worker_mcp_config(workspace, agent_id_raw, &mcp_config)?;
|
|
145
164
|
let mut argv = adapter
|
|
146
165
|
.build_command_with_tools(
|
|
147
166
|
auth_mode,
|
|
@@ -154,15 +173,15 @@ fn spawn_agents(
|
|
|
154
173
|
point_native_mcp_config_at_file(&mut argv, provider, &mcp_config_path);
|
|
155
174
|
fill_spawn_placeholders_full(
|
|
156
175
|
&mut argv,
|
|
157
|
-
|
|
176
|
+
workspace,
|
|
158
177
|
agent_id_raw,
|
|
159
|
-
|
|
178
|
+
Some(&mcp_team_id),
|
|
160
179
|
);
|
|
161
180
|
let window = WindowName::new(agent_id_raw);
|
|
162
181
|
let env = inherited_env_with_team_overrides(
|
|
163
|
-
|
|
182
|
+
workspace,
|
|
164
183
|
agent_id_raw,
|
|
165
|
-
|
|
184
|
+
Some(&mcp_team_id),
|
|
166
185
|
);
|
|
167
186
|
let spawn = if started.is_empty() {
|
|
168
187
|
transport.spawn_first(session_name, &window, &argv, team_dir, &env)
|
|
@@ -194,15 +213,14 @@ fn spawn_agents(
|
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
fn persist_spawn_agent_state(
|
|
216
|
+
workspace: &Path,
|
|
197
217
|
spec_path: &Path,
|
|
198
218
|
spec: &Value,
|
|
199
219
|
session_name: &SessionName,
|
|
200
220
|
transport: &dyn Transport,
|
|
201
221
|
started: &[StartedAgent],
|
|
202
222
|
) -> Result<(), LifecycleError> {
|
|
203
|
-
let
|
|
204
|
-
let workspace = team_workspace(team_dir);
|
|
205
|
-
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
223
|
+
let state_path = crate::state::persist::runtime_state_path(workspace);
|
|
206
224
|
let mut state = if state_path.exists() {
|
|
207
225
|
let text = std::fs::read_to_string(&state_path)
|
|
208
226
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", state_path.display())))?;
|
|
@@ -213,7 +231,7 @@ fn persist_spawn_agent_state(
|
|
|
213
231
|
};
|
|
214
232
|
let team_id = explicit_active_team_key(&state)
|
|
215
233
|
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
|
|
216
|
-
let worker_tmux_socket = launched_worker_tmux_socket(transport,
|
|
234
|
+
let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
|
|
217
235
|
drop_worker_pane_seeded_owner(
|
|
218
236
|
&mut state,
|
|
219
237
|
&team_id,
|
|
@@ -231,6 +249,7 @@ fn persist_spawn_agent_state(
|
|
|
231
249
|
.iter()
|
|
232
250
|
.map(|agent| agent.agent_id.as_str().to_string())
|
|
233
251
|
.collect();
|
|
252
|
+
let pane_pids_by_agent = pane_pids_by_started_agent(transport, started);
|
|
234
253
|
let mut agents = serde_json::Map::new();
|
|
235
254
|
let spawned_at = spawn_timestamp();
|
|
236
255
|
for agent in spec_agent_values(spec) {
|
|
@@ -265,9 +284,10 @@ fn persist_spawn_agent_state(
|
|
|
265
284
|
agents.insert(id.to_string(), serde_json::Value::Object(failed));
|
|
266
285
|
continue;
|
|
267
286
|
}
|
|
287
|
+
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
268
288
|
agents.insert(
|
|
269
289
|
id.to_string(),
|
|
270
|
-
running_agent_state(agent, id, provider,
|
|
290
|
+
running_agent_state(agent, id, provider, workspace, &spawned_at, &team_id, pane_pid)?,
|
|
271
291
|
);
|
|
272
292
|
}
|
|
273
293
|
if let Some(obj) = state.as_object_mut() {
|
|
@@ -277,21 +297,108 @@ fn persist_spawn_agent_state(
|
|
|
277
297
|
obj.insert("agents".to_string(), serde_json::Value::Object(agents));
|
|
278
298
|
state = serde_json::Value::Object(obj);
|
|
279
299
|
}
|
|
280
|
-
|
|
300
|
+
save_launched_team_state_for_key(workspace, &state, Some(&team_id))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn pane_pids_by_started_agent(
|
|
304
|
+
transport: &dyn Transport,
|
|
305
|
+
started: &[StartedAgent],
|
|
306
|
+
) -> BTreeMap<String, u32> {
|
|
307
|
+
let panes = transport.list_targets().unwrap_or_default();
|
|
308
|
+
started
|
|
309
|
+
.iter()
|
|
310
|
+
.filter_map(|agent| {
|
|
311
|
+
panes
|
|
312
|
+
.iter()
|
|
313
|
+
.find(|pane| pane.pane_id.as_str() == agent.target)
|
|
314
|
+
.and_then(|pane| pane.pane_pid)
|
|
315
|
+
.map(|pid| (agent.agent_id.as_str().to_string(), pid))
|
|
316
|
+
})
|
|
317
|
+
.collect()
|
|
281
318
|
}
|
|
282
319
|
|
|
283
320
|
fn save_launched_team_state(workspace: &Path, launched: &serde_json::Value) -> Result<(), LifecycleError> {
|
|
321
|
+
save_launched_team_state_for_key(workspace, launched, None)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn save_launched_team_state_for_key(
|
|
325
|
+
workspace: &Path,
|
|
326
|
+
launched: &serde_json::Value,
|
|
327
|
+
team_key: Option<&str>,
|
|
328
|
+
) -> Result<(), LifecycleError> {
|
|
284
329
|
let existing = load_runtime_state(workspace).unwrap_or_else(|_| serde_json::json!({}));
|
|
285
|
-
let launched_key =
|
|
330
|
+
let launched_key = team_key
|
|
331
|
+
.filter(|key| !key.is_empty())
|
|
332
|
+
.map(str::to_string)
|
|
333
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(launched));
|
|
286
334
|
let mut launched = launched.clone();
|
|
335
|
+
if let Some(obj) = launched.as_object_mut() {
|
|
336
|
+
obj.insert(
|
|
337
|
+
"active_team_key".to_string(),
|
|
338
|
+
serde_json::Value::String(launched_key.clone()),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
287
341
|
promote_launched_binding_from_team_entry(&mut launched, &launched_key);
|
|
288
342
|
drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
|
|
289
|
-
let merged =
|
|
343
|
+
let merged = if team_key.is_some() {
|
|
344
|
+
merge_workspace_team_state_with_key(&existing, &launched, &launched_key)
|
|
345
|
+
} else {
|
|
346
|
+
crate::state::projection::merge_workspace_team_state(&existing, &launched)
|
|
347
|
+
};
|
|
290
348
|
let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
|
|
291
349
|
drop_unbound_top_level_owner(&mut projected);
|
|
292
350
|
save_runtime_state(workspace, &projected).map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
293
351
|
}
|
|
294
352
|
|
|
353
|
+
fn merge_workspace_team_state_with_key(
|
|
354
|
+
existing: &serde_json::Value,
|
|
355
|
+
launched: &serde_json::Value,
|
|
356
|
+
launched_key: &str,
|
|
357
|
+
) -> serde_json::Value {
|
|
358
|
+
let mut launched_obj = launched.as_object().cloned().unwrap_or_default();
|
|
359
|
+
let mut teams = launched
|
|
360
|
+
.get("teams")
|
|
361
|
+
.and_then(serde_json::Value::as_object)
|
|
362
|
+
.cloned()
|
|
363
|
+
.unwrap_or_default();
|
|
364
|
+
let launched_entry = crate::state::projection::compact_team_state(launched);
|
|
365
|
+
if !existing
|
|
366
|
+
.get("session_name")
|
|
367
|
+
.and_then(serde_json::Value::as_str)
|
|
368
|
+
.is_some_and(|session| !session.is_empty())
|
|
369
|
+
{
|
|
370
|
+
teams.insert(launched_key.to_string(), launched_entry);
|
|
371
|
+
launched_obj.insert("teams".to_string(), serde_json::Value::Object(teams));
|
|
372
|
+
return serde_json::Value::Object(launched_obj);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let existing_key = explicit_active_team_key(existing)
|
|
376
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(existing));
|
|
377
|
+
if existing_key == launched_key {
|
|
378
|
+
let mut teams = existing
|
|
379
|
+
.get("teams")
|
|
380
|
+
.and_then(serde_json::Value::as_object)
|
|
381
|
+
.cloned()
|
|
382
|
+
.unwrap_or_default();
|
|
383
|
+
teams.insert(launched_key.to_string(), launched_entry);
|
|
384
|
+
launched_obj.insert("teams".to_string(), serde_json::Value::Object(teams));
|
|
385
|
+
return serde_json::Value::Object(launched_obj);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let mut merged = existing.as_object().cloned().unwrap_or_default();
|
|
389
|
+
let mut teams = merged
|
|
390
|
+
.get("teams")
|
|
391
|
+
.and_then(serde_json::Value::as_object)
|
|
392
|
+
.cloned()
|
|
393
|
+
.unwrap_or_default();
|
|
394
|
+
teams
|
|
395
|
+
.entry(existing_key)
|
|
396
|
+
.or_insert_with(|| crate::state::projection::compact_team_state(existing));
|
|
397
|
+
teams.insert(launched_key.to_string(), launched_entry);
|
|
398
|
+
merged.insert("teams".to_string(), serde_json::Value::Object(teams));
|
|
399
|
+
serde_json::Value::Object(merged)
|
|
400
|
+
}
|
|
401
|
+
|
|
295
402
|
fn promote_launched_binding_from_team_entry(launched: &mut serde_json::Value, launched_key: &str) {
|
|
296
403
|
let entry = launched
|
|
297
404
|
.get("teams")
|
|
@@ -448,6 +555,7 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
|
|
|
448
555
|
"status": "attached",
|
|
449
556
|
"provider": provider,
|
|
450
557
|
"pane_id": "__team_agent_unbound__",
|
|
558
|
+
"pane": "__team_agent_unbound__",
|
|
451
559
|
"leader_session_uuid": uuid.as_str(),
|
|
452
560
|
"owner_epoch": owner_epoch,
|
|
453
561
|
"discovery": "quick_start",
|
|
@@ -482,6 +590,7 @@ fn running_agent_state(
|
|
|
482
590
|
workspace: &Path,
|
|
483
591
|
spawned_at: &str,
|
|
484
592
|
team_id: &str,
|
|
593
|
+
pane_pid: Option<u32>,
|
|
485
594
|
) -> Result<serde_json::Value, LifecycleError> {
|
|
486
595
|
let model = agent.get("model").and_then(Value::as_str);
|
|
487
596
|
let auth_mode = agent
|
|
@@ -523,6 +632,9 @@ fn running_agent_state(
|
|
|
523
632
|
serde_json::json!(workspace.to_string_lossy().to_string()),
|
|
524
633
|
);
|
|
525
634
|
state.insert("spawned_at".to_string(), serde_json::json!(spawned_at));
|
|
635
|
+
if let Some(pane_pid) = pane_pid {
|
|
636
|
+
state.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
637
|
+
}
|
|
526
638
|
Ok(serde_json::Value::Object(state))
|
|
527
639
|
}
|
|
528
640
|
|
|
@@ -769,13 +881,6 @@ fn runtime_active_team_key_for_spawn(
|
|
|
769
881
|
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name))
|
|
770
882
|
}
|
|
771
883
|
|
|
772
|
-
fn process_team_id_for_spawn(workspace: &Path, spec: &Value) -> Option<String> {
|
|
773
|
-
load_runtime_state(workspace)
|
|
774
|
-
.ok()
|
|
775
|
-
.and_then(|state| explicit_active_team_key(&state))
|
|
776
|
-
.or_else(|| spec_team_id(spec))
|
|
777
|
-
}
|
|
778
|
-
|
|
779
884
|
fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
|
|
780
885
|
state
|
|
781
886
|
.get("active_team_key")
|
|
@@ -824,6 +929,43 @@ fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
|
824
929
|
}
|
|
825
930
|
}
|
|
826
931
|
|
|
932
|
+
fn quick_start_requested_team_key<'a>(team_id: Option<&'a str>, name: Option<&'a str>) -> Option<&'a str> {
|
|
933
|
+
team_id.or(name).filter(|team| !team.is_empty())
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
|
|
937
|
+
explicit_active_team_key(state).as_deref() == Some(team)
|
|
938
|
+
|| state
|
|
939
|
+
.get("teams")
|
|
940
|
+
.and_then(serde_json::Value::as_object)
|
|
941
|
+
.is_some_and(|teams| {
|
|
942
|
+
teams.contains_key(team)
|
|
943
|
+
|| teams
|
|
944
|
+
.values()
|
|
945
|
+
.any(|entry| json_team_identity_matches(entry, team))
|
|
946
|
+
})
|
|
947
|
+
|| crate::state::projection::team_state_key(state) == team
|
|
948
|
+
|| json_team_identity_matches(state, team)
|
|
949
|
+
|| state
|
|
950
|
+
.get("session_name")
|
|
951
|
+
.and_then(serde_json::Value::as_str)
|
|
952
|
+
.is_some_and(|session| {
|
|
953
|
+
session == team || session.strip_prefix("team-") == Some(team)
|
|
954
|
+
})
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
|
|
958
|
+
state
|
|
959
|
+
.get("team")
|
|
960
|
+
.and_then(|value| value.get("id").or_else(|| value.get("name")))
|
|
961
|
+
.and_then(serde_json::Value::as_str)
|
|
962
|
+
.is_some_and(|value| value == team)
|
|
963
|
+
|| state
|
|
964
|
+
.get("name")
|
|
965
|
+
.and_then(serde_json::Value::as_str)
|
|
966
|
+
.is_some_and(|value| value == team)
|
|
967
|
+
}
|
|
968
|
+
|
|
827
969
|
/// `quick_start(agents_dir, name, yes, fresh, team_id)`(`diagnose/quick_start.py:18`)。
|
|
828
970
|
/// 面向用户的零配置入口:编译 team_dir → `launch` → autobind leader receiver → 起
|
|
829
971
|
/// coordinator → `wait_ready` 轮询就绪。归入 lifecycle module(不与 diagnose 混)。
|
|
@@ -834,14 +976,29 @@ pub fn quick_start(
|
|
|
834
976
|
fresh: bool,
|
|
835
977
|
team_id: Option<&str>,
|
|
836
978
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
837
|
-
|
|
979
|
+
let workspace = team_workspace(agents_dir);
|
|
980
|
+
quick_start_in_workspace(&workspace, agents_dir, name, yes, fresh, team_id)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
pub fn quick_start_in_workspace(
|
|
984
|
+
workspace: &Path,
|
|
985
|
+
agents_dir: &Path,
|
|
986
|
+
name: Option<&str>,
|
|
987
|
+
yes: bool,
|
|
988
|
+
fresh: bool,
|
|
989
|
+
team_id: Option<&str>,
|
|
990
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
991
|
+
let workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
992
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
993
|
+
quick_start_with_transport_in_workspace(
|
|
994
|
+
&workspace,
|
|
838
995
|
agents_dir,
|
|
839
996
|
name,
|
|
840
997
|
yes,
|
|
841
998
|
fresh,
|
|
842
999
|
team_id,
|
|
843
|
-
// CP-1: per-team socket bound to the run workspace
|
|
844
|
-
&crate::tmux_backend::TmuxBackend::for_workspace(&
|
|
1000
|
+
// CP-1: per-team socket bound to the selected run workspace.
|
|
1001
|
+
&crate::tmux_backend::TmuxBackend::for_workspace(&workspace),
|
|
845
1002
|
)
|
|
846
1003
|
}
|
|
847
1004
|
|
|
@@ -854,6 +1011,19 @@ pub fn quick_start_with_transport(
|
|
|
854
1011
|
fresh: bool,
|
|
855
1012
|
team_id: Option<&str>,
|
|
856
1013
|
transport: &dyn Transport,
|
|
1014
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
1015
|
+
let workspace = team_workspace(agents_dir);
|
|
1016
|
+
quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, fresh, team_id, transport)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
pub fn quick_start_with_transport_in_workspace(
|
|
1020
|
+
workspace: &Path,
|
|
1021
|
+
agents_dir: &Path,
|
|
1022
|
+
name: Option<&str>,
|
|
1023
|
+
yes: bool,
|
|
1024
|
+
fresh: bool,
|
|
1025
|
+
team_id: Option<&str>,
|
|
1026
|
+
transport: &dyn Transport,
|
|
857
1027
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
858
1028
|
if !agents_dir.exists() {
|
|
859
1029
|
return Err(LifecycleError::Compile(format!(
|
|
@@ -861,34 +1031,43 @@ pub fn quick_start_with_transport(
|
|
|
861
1031
|
agents_dir.display()
|
|
862
1032
|
)));
|
|
863
1033
|
}
|
|
864
|
-
let workspace =
|
|
1034
|
+
let workspace = workspace.to_path_buf();
|
|
1035
|
+
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
1036
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1037
|
+
let requested_team = quick_start_requested_team_key(team_id, name)
|
|
1038
|
+
.map(str::to_string)
|
|
1039
|
+
.or_else(|| spec_team_id(&spec));
|
|
1040
|
+
let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
|
|
865
1041
|
if !fresh {
|
|
866
1042
|
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
867
1043
|
if state_path.exists() {
|
|
868
1044
|
let state = crate::state::persist::load_runtime_state(&workspace)
|
|
869
1045
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1046
|
+
if requested_team
|
|
1047
|
+
.as_deref()
|
|
1048
|
+
.is_none_or(|team| runtime_state_has_quick_start_team(&state, team))
|
|
1049
|
+
{
|
|
1050
|
+
return Ok(QuickStartReport::ExistingRuntime {
|
|
1051
|
+
team: requested_team.clone(),
|
|
1052
|
+
session_name: state
|
|
1053
|
+
.get("session_name")
|
|
1054
|
+
.and_then(serde_json::Value::as_str)
|
|
1055
|
+
.filter(|s| !s.is_empty())
|
|
1056
|
+
.map(SessionName::new),
|
|
1057
|
+
state_path: Some(state_path),
|
|
1058
|
+
next_actions: vec![
|
|
1059
|
+
"run restart to resume the existing team or pass --fresh to replace it".to_string(),
|
|
1060
|
+
],
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
882
1063
|
}
|
|
883
1064
|
}
|
|
884
|
-
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
885
|
-
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
886
1065
|
// CR-040/042: repeated quick-start from one template with distinct --team-id/--name
|
|
887
1066
|
// must NOT collide on the template-derived tmux session. Override the compiled
|
|
888
1067
|
// spec's runtime.session_name with one derived from the REQUESTED team identity
|
|
889
1068
|
// so launch_with_transport (which reads runtime.session_name) spawns into an
|
|
890
1069
|
// isolated session per requested team.
|
|
891
|
-
if let Some(requested) =
|
|
1070
|
+
if let Some(requested) = requested_team.as_deref() {
|
|
892
1071
|
override_spec_session_name(&mut spec, &format!("team-{requested}"));
|
|
893
1072
|
}
|
|
894
1073
|
let spec_path = agents_dir.join("team.spec.yaml");
|
|
@@ -900,11 +1079,13 @@ pub fn quick_start_with_transport(
|
|
|
900
1079
|
let session_name = spec_session_name(&spec);
|
|
901
1080
|
let resolved_spec_path = std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
902
1081
|
let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
903
|
-
|
|
1082
|
+
let state_team_key = explicit_team_key
|
|
1083
|
+
.unwrap_or_else(|| runtime_team_key_for_spec(&resolved_spec_path, &spec, &session_name));
|
|
1084
|
+
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
904
1085
|
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
905
1086
|
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
906
1087
|
// also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
|
|
907
|
-
let launch =
|
|
1088
|
+
let launch = launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
908
1089
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
909
1090
|
let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
910
1091
|
.map(|report| report.ok)
|
|
@@ -1178,7 +1359,8 @@ pub fn add_agent(
|
|
|
1178
1359
|
let team_dir = selected
|
|
1179
1360
|
.spec_workspace
|
|
1180
1361
|
.ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
|
|
1181
|
-
|
|
1362
|
+
add_agent_with_transport_at_paths(
|
|
1363
|
+
&selected.run_workspace,
|
|
1182
1364
|
&team_dir,
|
|
1183
1365
|
agent_id,
|
|
1184
1366
|
role_file_path,
|
|
@@ -1200,11 +1382,31 @@ pub fn add_agent_with_transport(
|
|
|
1200
1382
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
1201
1383
|
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
1202
1384
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1385
|
+
add_agent_with_transport_at_paths(
|
|
1386
|
+
&run_workspace,
|
|
1387
|
+
workspace,
|
|
1388
|
+
agent_id,
|
|
1389
|
+
role_file_path,
|
|
1390
|
+
open_display,
|
|
1391
|
+
team,
|
|
1392
|
+
transport,
|
|
1393
|
+
)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
fn add_agent_with_transport_at_paths(
|
|
1397
|
+
run_workspace: &Path,
|
|
1398
|
+
team_dir: &Path,
|
|
1399
|
+
agent_id: &AgentId,
|
|
1400
|
+
role_file_path: &Path,
|
|
1401
|
+
open_display: bool,
|
|
1402
|
+
team: Option<&str>,
|
|
1403
|
+
transport: &dyn Transport,
|
|
1404
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
1203
1405
|
let owner_state = if team.is_some() {
|
|
1204
|
-
crate::state::projection::select_runtime_state(
|
|
1406
|
+
crate::state::projection::select_runtime_state(run_workspace, team)
|
|
1205
1407
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?
|
|
1206
1408
|
} else {
|
|
1207
|
-
load_runtime_state(
|
|
1409
|
+
load_runtime_state(run_workspace).map_err(|e| LifecycleError::StatePersist(e.to_string()))?
|
|
1208
1410
|
};
|
|
1209
1411
|
ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
|
|
1210
1412
|
if !role_file_path.exists() {
|
|
@@ -1213,7 +1415,6 @@ pub fn add_agent_with_transport(
|
|
|
1213
1415
|
role_file_path.display()
|
|
1214
1416
|
)));
|
|
1215
1417
|
}
|
|
1216
|
-
let team_dir = workspace;
|
|
1217
1418
|
if agent_id_exists_in_team_dir(team_dir, agent_id) {
|
|
1218
1419
|
return Err(LifecycleError::RequirementUnmet(format!(
|
|
1219
1420
|
"agent id already exists: {agent_id}"
|
|
@@ -1228,10 +1429,9 @@ pub fn add_agent_with_transport(
|
|
|
1228
1429
|
})?;
|
|
1229
1430
|
let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
|
|
1230
1431
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1231
|
-
|
|
1232
|
-
upsert_agent_state_from_role(&run_ws, agent_id, &meta, &dynamic_role_file)?;
|
|
1432
|
+
upsert_agent_state_from_role(run_workspace, team, agent_id, &meta, &dynamic_role_file)?;
|
|
1233
1433
|
let started = crate::lifecycle::restart::start_agent_at_paths(
|
|
1234
|
-
|
|
1434
|
+
run_workspace,
|
|
1235
1435
|
team_dir,
|
|
1236
1436
|
agent_id,
|
|
1237
1437
|
false,
|
|
@@ -1260,12 +1460,18 @@ pub fn add_agent_with_transport(
|
|
|
1260
1460
|
|
|
1261
1461
|
fn upsert_agent_state_from_role(
|
|
1262
1462
|
workspace: &Path,
|
|
1463
|
+
team: Option<&str>,
|
|
1263
1464
|
agent_id: &AgentId,
|
|
1264
1465
|
meta: &Value,
|
|
1265
1466
|
dynamic_role_file: &Path,
|
|
1266
1467
|
) -> Result<(), LifecycleError> {
|
|
1267
|
-
let mut state =
|
|
1268
|
-
|
|
1468
|
+
let mut state = if team.is_some() {
|
|
1469
|
+
crate::state::projection::select_runtime_state(workspace, team)
|
|
1470
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?
|
|
1471
|
+
} else {
|
|
1472
|
+
crate::state::persist::load_runtime_state(workspace)
|
|
1473
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?
|
|
1474
|
+
};
|
|
1269
1475
|
if !state.is_object() {
|
|
1270
1476
|
state = serde_json::json!({});
|
|
1271
1477
|
}
|
|
@@ -1310,7 +1516,13 @@ fn upsert_agent_state_from_role(
|
|
|
1310
1516
|
}
|
|
1311
1517
|
}
|
|
1312
1518
|
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
1313
|
-
|
|
1519
|
+
if team.is_some() {
|
|
1520
|
+
let team_key = explicit_active_team_key(&state)
|
|
1521
|
+
.or_else(|| team.filter(|key| !key.is_empty()).map(str::to_string));
|
|
1522
|
+
save_launched_team_state_for_key(workspace, &state, team_key.as_deref())
|
|
1523
|
+
} else {
|
|
1524
|
+
save_runtime_state(workspace, &state).map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
1525
|
+
}
|
|
1314
1526
|
}
|
|
1315
1527
|
|
|
1316
1528
|
fn materialize_added_role_file(
|
|
@@ -1863,6 +2075,7 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
1863
2075
|
"status": "attached",
|
|
1864
2076
|
"provider": provider,
|
|
1865
2077
|
"pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
2078
|
+
"pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
1866
2079
|
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
1867
2080
|
"owner_epoch": owner_epoch,
|
|
1868
2081
|
"discovery": "quick_start",
|
|
@@ -160,6 +160,71 @@ pub(super) fn agent_rollout_path(agent: &serde_json::Value) -> Option<RolloutPat
|
|
|
160
160
|
.map(RolloutPath::new)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
pub(crate) fn refresh_missing_provider_sessions(
|
|
164
|
+
state: &mut serde_json::Value,
|
|
165
|
+
) -> Result<bool, LifecycleError> {
|
|
166
|
+
let Some(agents) = state.get_mut("agents").and_then(serde_json::Value::as_object_mut) else {
|
|
167
|
+
return Ok(false);
|
|
168
|
+
};
|
|
169
|
+
let mut changed = false;
|
|
170
|
+
for (agent_id, agent) in agents {
|
|
171
|
+
let Some(agent_obj) = agent.as_object_mut() else {
|
|
172
|
+
continue;
|
|
173
|
+
};
|
|
174
|
+
if agent_obj
|
|
175
|
+
.get("session_id")
|
|
176
|
+
.and_then(serde_json::Value::as_str)
|
|
177
|
+
.is_some_and(|session| !session.is_empty())
|
|
178
|
+
{
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let Some(spawn_cwd) = agent_obj
|
|
182
|
+
.get("spawn_cwd")
|
|
183
|
+
.and_then(serde_json::Value::as_str)
|
|
184
|
+
.filter(|cwd| !cwd.is_empty())
|
|
185
|
+
else {
|
|
186
|
+
continue;
|
|
187
|
+
};
|
|
188
|
+
let provider = agent_provider(&serde_json::Value::Object(agent_obj.clone()));
|
|
189
|
+
let adapter = crate::provider::get_adapter(provider);
|
|
190
|
+
let captured = adapter
|
|
191
|
+
.capture_session_id(agent_id, Path::new(spawn_cwd), 0)
|
|
192
|
+
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
193
|
+
let Some(captured) = captured else {
|
|
194
|
+
continue;
|
|
195
|
+
};
|
|
196
|
+
if let Some(session_id) = captured.session_id {
|
|
197
|
+
agent_obj.insert(
|
|
198
|
+
"session_id".to_string(),
|
|
199
|
+
serde_json::json!(session_id.as_str()),
|
|
200
|
+
);
|
|
201
|
+
changed = true;
|
|
202
|
+
}
|
|
203
|
+
if let Some(rollout_path) = captured.rollout_path {
|
|
204
|
+
agent_obj.insert(
|
|
205
|
+
"rollout_path".to_string(),
|
|
206
|
+
serde_json::json!(rollout_path.as_path().to_string_lossy()),
|
|
207
|
+
);
|
|
208
|
+
changed = true;
|
|
209
|
+
}
|
|
210
|
+
agent_obj.insert(
|
|
211
|
+
"captured_at".to_string(),
|
|
212
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
213
|
+
);
|
|
214
|
+
agent_obj.insert(
|
|
215
|
+
"captured_via".to_string(),
|
|
216
|
+
serde_json::to_value(captured.captured_via)
|
|
217
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
218
|
+
);
|
|
219
|
+
agent_obj.insert(
|
|
220
|
+
"attribution_confidence".to_string(),
|
|
221
|
+
serde_json::to_value(captured.attribution_confidence)
|
|
222
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
Ok(changed)
|
|
226
|
+
}
|
|
227
|
+
|
|
163
228
|
/// Tools list off an agent's runtime state entry (`tools: [...]`). Restart paths
|
|
164
229
|
/// don't have the full spec object, only the runtime state — so they read tools from
|
|
165
230
|
/// the state row, falling back to an empty list. Contract C requires the worker
|