@team-agent/installer 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/emit.rs +92 -11
  5. package/crates/team-agent/src/cli/leader.rs +2 -0
  6. package/crates/team-agent/src/cli/mod.rs +255 -37
  7. package/crates/team-agent/src/cli/send.rs +252 -0
  8. package/crates/team-agent/src/cli/status.rs +5 -1
  9. package/crates/team-agent/src/cli/status_port.rs +24 -0
  10. package/crates/team-agent/src/cli/tests/base.rs +21 -3
  11. package/crates/team-agent/src/cli/tests/divergence.rs +6 -6
  12. package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
  13. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +285 -2
  14. package/crates/team-agent/src/cli/tests/status_send.rs +25 -0
  15. package/crates/team-agent/src/cli/types.rs +32 -1
  16. package/crates/team-agent/src/compiler/tests.rs +2 -2
  17. package/crates/team-agent/src/compiler.rs +1 -1
  18. package/crates/team-agent/src/fake_worker.rs +32 -145
  19. package/crates/team-agent/src/leader/start.rs +272 -8
  20. package/crates/team-agent/src/leader/tests/basics.rs +1 -0
  21. package/crates/team-agent/src/leader/tests/identity.rs +121 -25
  22. package/crates/team-agent/src/leader/types.rs +9 -0
  23. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  24. package/crates/team-agent/src/lifecycle/launch.rs +631 -70
  25. package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +38 -2
  27. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +79 -4
  28. package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
  29. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +731 -8
  30. package/crates/team-agent/src/lifecycle/types.rs +5 -0
  31. package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
  32. package/crates/team-agent/src/mcp_server/wire.rs +153 -3
  33. package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
  34. package/crates/team-agent/src/messaging/mod.rs +6 -3
  35. package/crates/team-agent/src/messaging/results.rs +240 -158
  36. package/crates/team-agent/src/messaging/send.rs +3 -2
  37. package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
  38. package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
  39. package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
  40. package/crates/team-agent/src/os_probe.rs +11 -0
  41. package/crates/team-agent/src/state/persist.rs +6 -0
  42. package/crates/team-agent/src/tmux_backend.rs +85 -0
  43. package/crates/team-agent/src/transport/test_support.rs +46 -1
  44. package/crates/team-agent/src/transport.rs +27 -0
  45. package/package.json +4 -4
  46. package/skills/team-agent/references/recovery-runbook.md +277 -0
@@ -86,7 +86,14 @@ pub(crate) fn start_agent_at_paths(
86
86
  }
87
87
  let session_name = state_session_name(&state);
88
88
  let window = agent_window(&agent, agent_id);
89
- if !force && window_exists(transport, &session_name, &window) {
89
+ let adaptive_layout =
90
+ open_display && crate::lifecycle::launch::state_uses_adaptive_layout(&state);
91
+ let agent_live = if adaptive_layout {
92
+ agent_pane_live(transport, &agent)
93
+ } else {
94
+ window_exists(transport, &session_name, &window)
95
+ };
96
+ if !force && agent_live {
90
97
  mark_agent_running_noop(&mut state, agent_id, &session_name, &window)?;
91
98
  crate::state::projection::save_team_scoped_state(workspace, &state)
92
99
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
@@ -141,6 +148,28 @@ pub(crate) fn start_agent_at_paths(
141
148
  let into_existing_session =
142
149
  session_live_or_default(transport, &session_name, session_name_present(&state));
143
150
  let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
151
+ let layout_placement = if adaptive_layout {
152
+ crate::lifecycle::launch::adaptive_existing_placement_for_agent(
153
+ &state,
154
+ transport,
155
+ &session_name,
156
+ agent_id,
157
+ )
158
+ .or_else(|| {
159
+ crate::lifecycle::launch::adaptive_placement_for_agent(
160
+ &state,
161
+ transport,
162
+ &session_name,
163
+ agent_id,
164
+ )
165
+ })
166
+ } else {
167
+ None
168
+ };
169
+ let spawn_window = layout_placement
170
+ .as_ref()
171
+ .map(|placement| placement.layout_window.as_str().to_string())
172
+ .unwrap_or_else(|| window.clone());
144
173
  let spawn = spawn_agent_window(
145
174
  workspace,
146
175
  &session_name,
@@ -150,9 +179,10 @@ pub(crate) fn start_agent_at_paths(
150
179
  into_existing_session,
151
180
  transport,
152
181
  Some(&safety),
182
+ layout_placement.as_ref(),
153
183
  None,
154
184
  )?;
155
- mark_agent_started(&mut state, agent_id, &window, &spawn, &safety)?;
185
+ mark_agent_started(&mut state, agent_id, &spawn_window, &spawn, &safety)?;
156
186
  crate::state::projection::save_team_scoped_state(workspace, &state)
157
187
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
158
188
  write_start_agent_start_event(
@@ -162,9 +192,9 @@ pub(crate) fn start_agent_at_paths(
162
192
  provider,
163
193
  start_mode,
164
194
  &session_name,
165
- &window,
195
+ &spawn_window,
166
196
  spawn_session_id,
167
- into_existing_session,
197
+ tmux_start_mode_for_spawn(&spawn, into_existing_session),
168
198
  )?;
169
199
  let coordinator_started = start_coordinator_for_workspace(workspace)?;
170
200
  Ok(StartAgentOutcome::Running {
@@ -180,6 +210,55 @@ pub(crate) fn start_agent_at_paths(
180
210
  })
181
211
  }
182
212
 
213
+ fn agent_pane_live(
214
+ transport: &dyn crate::transport::Transport,
215
+ agent: &serde_json::Value,
216
+ ) -> bool {
217
+ let Some(pane) = agent
218
+ .get("pane_id")
219
+ .and_then(serde_json::Value::as_str)
220
+ .filter(|pane| !pane.is_empty())
221
+ .map(crate::transport::PaneId::new)
222
+ else {
223
+ return false;
224
+ };
225
+ agent_pane_live_by_id(transport, &pane)
226
+ }
227
+
228
+ fn agent_pane_live_by_id(
229
+ transport: &dyn crate::transport::Transport,
230
+ pane: &crate::transport::PaneId,
231
+ ) -> bool {
232
+ match transport.has_pane(&pane) {
233
+ Ok(Some(live)) => live,
234
+ Ok(None) | Err(_) => !matches!(
235
+ transport.liveness(&pane),
236
+ Ok(crate::model::enums::PaneLiveness::Dead)
237
+ ),
238
+ }
239
+ }
240
+
241
+ fn tmux_start_mode_for_spawn(
242
+ spawn: &SpawnedAgentWindow,
243
+ into_existing_session: bool,
244
+ ) -> &'static str {
245
+ if let Some(placement) = spawn.layout_placement.as_ref() {
246
+ if placement.starts_window {
247
+ if into_existing_session {
248
+ "new-window"
249
+ } else {
250
+ "new-session"
251
+ }
252
+ } else {
253
+ "split-window"
254
+ }
255
+ } else if into_existing_session {
256
+ "new-window"
257
+ } else {
258
+ "new-session"
259
+ }
260
+ }
261
+
183
262
  /// `stop_agent(workspace, agent_id, team)`(`lifecycle/operations.py:62`)。
184
263
  /// owner-gate → kill window → **同时关显示** → 写 state。
185
264
  pub fn stop_agent(
@@ -233,16 +312,34 @@ pub(super) fn stop_agent_at_paths(
233
312
  .filter(|s| !s.is_empty())
234
313
  .unwrap_or_else(|| agent_id.as_str())
235
314
  .to_string();
236
- let target_str = format!("{}:{window}", session_name.as_str());
237
- let stopped = window_exists(transport, &session_name, &window);
315
+ let pane_id = state
316
+ .get("agents")
317
+ .and_then(|v| v.get(agent_id.as_str()))
318
+ .and_then(|v| v.get("pane_id"))
319
+ .and_then(|v| v.as_str())
320
+ .filter(|pane| !pane.is_empty())
321
+ .map(crate::transport::PaneId::new);
322
+ let target_str = pane_id
323
+ .as_ref()
324
+ .map(|pane| pane.as_str().to_string())
325
+ .unwrap_or_else(|| format!("{}:{window}", session_name.as_str()));
326
+ let stopped = pane_id
327
+ .as_ref()
328
+ .map(|pane| agent_pane_live_by_id(transport, pane))
329
+ .unwrap_or_else(|| window_exists(transport, &session_name, &window));
238
330
  if stopped {
239
- let target = Target::SessionWindow {
240
- session: session_name.clone(),
241
- window: WindowName::new(&window),
242
- };
243
331
  // golden operations.py:84-86: a non-zero kill-window raises
244
332
  // RuntimeError(f"failed to stop agent {agent_id}: {proc.stderr.strip()}").
245
- if let Err(e) = transport.kill_window(&target) {
333
+ let kill_result = if let Some(pane) = pane_id.as_ref() {
334
+ transport.kill_pane(pane)
335
+ } else {
336
+ let target = Target::SessionWindow {
337
+ session: session_name.clone(),
338
+ window: WindowName::new(&window),
339
+ };
340
+ transport.kill_window(&target)
341
+ };
342
+ if let Err(e) = kill_result {
246
343
  let stderr = match &e {
247
344
  crate::transport::TransportError::Subprocess { stderr, .. } => stderr.trim().to_string(),
248
345
  other => other.to_string(),
@@ -335,6 +432,34 @@ fn mark_agent_started(
335
432
  &spawn.profile_launch,
336
433
  );
337
434
  crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
435
+ if let Some(placement) = spawn.layout_placement.as_ref() {
436
+ agent.insert(
437
+ "layout_window".to_string(),
438
+ serde_json::json!(placement.layout_window.as_str()),
439
+ );
440
+ agent.insert(
441
+ "layout_index".to_string(),
442
+ serde_json::json!(placement.layout_index),
443
+ );
444
+ agent.insert("pane_index".to_string(), serde_json::json!(placement.pane_index));
445
+ agent.insert(
446
+ "display".to_string(),
447
+ serde_json::json!({
448
+ "backend": "adaptive",
449
+ "status": "opened",
450
+ "window": placement.layout_window.as_str(),
451
+ "workspace_window": null,
452
+ "pane_id": spawn.spawn.pane_id.as_str(),
453
+ "pane_title": agent_id.as_str(),
454
+ "target": spawn.spawn.pane_id.as_str(),
455
+ "target_worker_session": spawn.spawn.session.as_str(),
456
+ "linked_session": null,
457
+ "leader_session": spawn.spawn.session.as_str(),
458
+ "display_session": null,
459
+ "fallback": null,
460
+ }),
461
+ );
462
+ }
338
463
  Ok(())
339
464
  }
340
465
 
@@ -458,7 +583,7 @@ fn write_start_agent_start_event(
458
583
  session_name: &SessionName,
459
584
  window: &str,
460
585
  session_id: Option<&SessionId>,
461
- into_existing_session: bool,
586
+ tmux_start_mode: &'static str,
462
587
  ) -> Result<(), LifecycleError> {
463
588
  let auth_mode = agent_auth_mode(agent);
464
589
  let model = agent.get("model").and_then(|v| v.as_str());
@@ -535,11 +660,6 @@ fn write_start_agent_start_event(
535
660
  agent_id.as_str(),
536
661
  team_id,
537
662
  );
538
- let tmux_start_mode = if into_existing_session {
539
- "new-window"
540
- } else {
541
- "new-session"
542
- };
543
663
  crate::event_log::EventLog::new(workspace)
544
664
  .write(
545
665
  "start_agent.agent_start",
@@ -4,6 +4,7 @@ pub(super) struct SpawnedAgentWindow {
4
4
  pub spawn: crate::transport::SpawnResult,
5
5
  pub plan: crate::provider::CommandPlan,
6
6
  pub profile_launch: crate::provider::ProviderProfileLaunch,
7
+ pub layout_placement: Option<crate::lifecycle::launch::LayoutPlacement>,
7
8
  }
8
9
 
9
10
  pub(super) fn spawn_agent_window(
@@ -15,6 +16,7 @@ pub(super) fn spawn_agent_window(
15
16
  into_existing_session: bool,
16
17
  transport: &dyn crate::transport::Transport,
17
18
  safety: Option<&DangerousApproval>,
19
+ layout_placement: Option<&crate::lifecycle::launch::LayoutPlacement>,
18
20
  spawn_cwd_override: Option<&Path>,
19
21
  ) -> Result<SpawnedAgentWindow, LifecycleError> {
20
22
  let provider = agent_provider(agent);
@@ -114,7 +116,9 @@ pub(super) fn spawn_agent_window(
114
116
  agent_id.as_str(),
115
117
  team_id.as_deref(),
116
118
  );
117
- let window = WindowName::new(agent_id.as_str());
119
+ let window = layout_placement
120
+ .map(|placement| placement.layout_window.clone())
121
+ .unwrap_or_else(|| WindowName::new(agent_id.as_str()));
118
122
  let mut env = crate::lifecycle::launch::inherited_env_with_team_overrides(
119
123
  workspace,
120
124
  agent_id.as_str(),
@@ -132,7 +136,38 @@ pub(super) fn spawn_agent_window(
132
136
  })
133
137
  .unwrap_or(workspace);
134
138
  let env_unset: Vec<String> = profile_launch.env_unset.iter().cloned().collect();
135
- let result = if into_existing_session {
139
+ let result = if let Some(placement) = layout_placement {
140
+ if placement.starts_window {
141
+ if into_existing_session {
142
+ transport.spawn_into_with_env_unset(
143
+ session_name,
144
+ &window,
145
+ &plan.argv,
146
+ spawn_cwd,
147
+ &env,
148
+ &env_unset,
149
+ )
150
+ } else {
151
+ transport.spawn_first_with_env_unset(
152
+ session_name,
153
+ &window,
154
+ &plan.argv,
155
+ spawn_cwd,
156
+ &env,
157
+ &env_unset,
158
+ )
159
+ }
160
+ } else {
161
+ transport.spawn_split_with_env_unset(
162
+ session_name,
163
+ &window,
164
+ &plan.argv,
165
+ spawn_cwd,
166
+ &env,
167
+ &env_unset,
168
+ )
169
+ }
170
+ } else if into_existing_session {
136
171
  transport.spawn_into_with_env_unset(
137
172
  session_name,
138
173
  &window,
@@ -162,6 +197,7 @@ pub(super) fn spawn_agent_window(
162
197
  spawn,
163
198
  plan,
164
199
  profile_launch,
200
+ layout_placement: layout_placement.cloned(),
165
201
  })
166
202
  }
167
203
 
@@ -281,6 +281,12 @@ pub fn restart_with_transport_with_session_convergence_deadline(
281
281
  if fatal_resume_failure {
282
282
  continue;
283
283
  }
284
+ let layout_placement = crate::lifecycle::launch::adaptive_existing_placement_for_agent(
285
+ &state,
286
+ transport,
287
+ &session_name,
288
+ &decision.agent_id,
289
+ );
284
290
  let spawn = match spawn_agent_window(
285
291
  &selected.run_workspace,
286
292
  &session_name,
@@ -290,6 +296,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
290
296
  session_live,
291
297
  transport,
292
298
  Some(&safety),
299
+ layout_placement.as_ref(),
293
300
  Some(spec_workspace),
294
301
  ) {
295
302
  Ok(spawn) => spawn,
@@ -312,7 +319,14 @@ pub fn restart_with_transport_with_session_convergence_deadline(
312
319
  };
313
320
  if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
314
321
  .and_then(|_| {
315
- mark_agent_respawned(&mut state, &decision.agent_id, &spawn, transport, &safety)
322
+ mark_agent_respawned(
323
+ &mut state,
324
+ &decision.agent_id,
325
+ decision.restart_mode,
326
+ &spawn,
327
+ transport,
328
+ &safety,
329
+ )
316
330
  })
317
331
  {
318
332
  let error = error.to_string();
@@ -381,13 +395,17 @@ pub fn restart_with_transport_with_session_convergence_deadline(
381
395
  restart_readiness_deadline(readiness_deadline_ms),
382
396
  restart_readiness_poll_interval(),
383
397
  )?;
384
- let attach_commands = crate::tmux_backend::attach_commands_for_windows(
385
- &selected.run_workspace,
386
- &session_name,
398
+ let attach_windows = crate::lifecycle::launch::attach_window_names_for_state_agents(
399
+ &state,
387
400
  successful_agents
388
401
  .iter()
389
402
  .map(|decision| decision.agent_id.as_str()),
390
403
  );
404
+ let attach_commands = crate::tmux_backend::attach_commands_for_windows(
405
+ &selected.run_workspace,
406
+ &session_name,
407
+ attach_windows.iter().map(String::as_str),
408
+ );
391
409
  let mut next_actions = attach_commands.clone();
392
410
  if !failed_agents.is_empty() {
393
411
  next_actions.extend(restart_failure_next_actions(&failed_agents));
@@ -851,6 +869,7 @@ fn mark_restart_targets_stopped_after_teardown(
851
869
  fn mark_agent_respawned(
852
870
  state: &mut serde_json::Value,
853
871
  agent_id: &AgentId,
872
+ restart_mode: StartMode,
854
873
  spawn: &SpawnedAgentWindow,
855
874
  transport: &dyn crate::transport::Transport,
856
875
  safety: &DangerousApproval,
@@ -867,6 +886,10 @@ fn mark_agent_respawned(
867
886
  )));
868
887
  };
869
888
  agent.insert("status".to_string(), serde_json::json!("running"));
889
+ agent.insert(
890
+ "window".to_string(),
891
+ serde_json::json!(spawn.spawn.window.as_str()),
892
+ );
870
893
  agent.insert(
871
894
  "pane_id".to_string(),
872
895
  serde_json::json!(spawn.spawn.pane_id.as_str()),
@@ -882,8 +905,60 @@ fn mark_agent_respawned(
882
905
  if let Some(pane_pid) = pane_pid {
883
906
  agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
884
907
  }
908
+ if matches!(
909
+ restart_mode,
910
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
911
+ ) {
912
+ if let Some(session_id) = spawn.plan.expected_session_id.as_ref() {
913
+ agent.insert(
914
+ "session_id".to_string(),
915
+ serde_json::json!(session_id.as_str()),
916
+ );
917
+ }
918
+ }
885
919
  crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
886
920
  persist_effective_approval_policy_for_restart(agent, safety);
921
+ if let Some(placement) = spawn.layout_placement.as_ref() {
922
+ agent.insert(
923
+ "layout_window".to_string(),
924
+ serde_json::json!(placement.layout_window.as_str()),
925
+ );
926
+ agent.insert(
927
+ "layout_index".to_string(),
928
+ serde_json::json!(placement.layout_index),
929
+ );
930
+ agent.insert("pane_index".to_string(), serde_json::json!(placement.pane_index));
931
+ agent.insert(
932
+ "display".to_string(),
933
+ serde_json::json!({
934
+ "backend": "adaptive",
935
+ "status": "opened",
936
+ "window": placement.layout_window.as_str(),
937
+ "workspace_window": null,
938
+ "pane_id": spawn.spawn.pane_id.as_str(),
939
+ "pane_title": agent_id.as_str(),
940
+ "target": spawn.spawn.pane_id.as_str(),
941
+ "target_worker_session": spawn.spawn.session.as_str(),
942
+ "linked_session": null,
943
+ "leader_session": spawn.spawn.session.as_str(),
944
+ "display_session": null,
945
+ "fallback": null,
946
+ }),
947
+ );
948
+ }
949
+ if matches!(
950
+ restart_mode,
951
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
952
+ ) && spawn.plan.expected_session_id.is_some()
953
+ {
954
+ agent.insert("rollout_path".to_string(), serde_json::Value::Null);
955
+ agent.insert("captured_at".to_string(), serde_json::Value::Null);
956
+ agent.insert("captured_via".to_string(), serde_json::Value::Null);
957
+ agent.insert(
958
+ "attribution_confidence".to_string(),
959
+ serde_json::Value::Null,
960
+ );
961
+ }
887
962
  agent.remove("startup_prompts");
888
963
  agent.remove("startup_prompt_status");
889
964
  Ok(())
@@ -213,14 +213,21 @@ fn plan_condition_rejects_out_of_grammar() {
213
213
 
214
214
  // ───────────────────────────────────────────────────────────────────────
215
215
  // resolve_display_backend — display/backend.py
216
- // 默认 none;非默认非静默(non_default=true 触发 display.backend_resolved)。
216
+ // 默认 adaptive;显式 none 是逃生口。
217
217
  // ───────────────────────────────────────────────────────────────────────
218
218
 
219
219
  #[test]
220
- fn resolve_backend_defaults_to_none_when_none_requested() {
220
+ fn resolve_backend_defaults_to_adaptive_when_none_requested() {
221
221
  let r = resolve_display_backend(None, None);
222
+ assert_eq!(r.backend, DisplayBackend::Adaptive);
223
+ assert!(!r.non_default, "默认 adaptive 不应标记 non_default");
224
+ }
225
+
226
+ #[test]
227
+ fn resolve_backend_explicit_none_is_escape_hatch() {
228
+ let r = resolve_display_backend(Some(DisplayBackend::None), None);
222
229
  assert_eq!(r.backend, DisplayBackend::None);
223
- assert!(!r.non_default, "默认 none 不应标记 non_default");
230
+ assert!(r.non_default, "显式 none 是偏离默认 adaptive 的逃生口");
224
231
  }
225
232
 
226
233
  #[test]
@@ -732,8 +739,10 @@ fn remove_agent_without_from_spec_is_refused_confirm() {
732
739
  Ok(RemoveAgentOutcome::RefusedFromSpecConfirm { agent_id }) => {
733
740
  assert_eq!(agent_id, aid("w1"));
734
741
  }
735
- // 若先撞 owner / 缺 state 门也可接受(门在 confirm 之前)。
736
- Err(LifecycleError::OwnerRefused(_)) | Err(LifecycleError::TeamSelect(_)) => {}
742
+ // 若先撞 owner / 缺 state / runtime-missing 门也可接受(门在 confirm 之前)。
743
+ Err(LifecycleError::OwnerRefused(_))
744
+ | Err(LifecycleError::RequirementUnmet(_))
745
+ | Err(LifecycleError::TeamSelect(_)) => {}
737
746
  other => panic!("from_spec=false 应 RefusedFromSpecConfirm 或更上游门:{other:?}"),
738
747
  }
739
748
  }