@team-agent/installer 0.3.14 → 0.3.15

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 (36) 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/mod.rs +60 -10
  6. package/crates/team-agent/src/cli/send.rs +252 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
  9. package/crates/team-agent/src/cli/types.rs +30 -1
  10. package/crates/team-agent/src/compiler/tests.rs +2 -2
  11. package/crates/team-agent/src/compiler.rs +1 -1
  12. package/crates/team-agent/src/fake_worker.rs +32 -145
  13. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  14. package/crates/team-agent/src/lifecycle/launch.rs +560 -69
  15. package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
  16. package/crates/team-agent/src/lifecycle/restart/common.rs +38 -2
  17. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +72 -1
  18. package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
  19. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +668 -8
  20. package/crates/team-agent/src/lifecycle/types.rs +5 -0
  21. package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
  22. package/crates/team-agent/src/mcp_server/wire.rs +153 -3
  23. package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
  24. package/crates/team-agent/src/messaging/mod.rs +6 -3
  25. package/crates/team-agent/src/messaging/results.rs +240 -158
  26. package/crates/team-agent/src/messaging/send.rs +3 -2
  27. package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
  28. package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
  29. package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
  30. package/crates/team-agent/src/os_probe.rs +11 -0
  31. package/crates/team-agent/src/state/persist.rs +6 -0
  32. package/crates/team-agent/src/tmux_backend.rs +85 -0
  33. package/crates/team-agent/src/transport/test_support.rs +46 -1
  34. package/crates/team-agent/src/transport.rs +27 -0
  35. package/package.json +4 -4
  36. 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();
@@ -851,6 +865,7 @@ fn mark_restart_targets_stopped_after_teardown(
851
865
  fn mark_agent_respawned(
852
866
  state: &mut serde_json::Value,
853
867
  agent_id: &AgentId,
868
+ restart_mode: StartMode,
854
869
  spawn: &SpawnedAgentWindow,
855
870
  transport: &dyn crate::transport::Transport,
856
871
  safety: &DangerousApproval,
@@ -867,6 +882,10 @@ fn mark_agent_respawned(
867
882
  )));
868
883
  };
869
884
  agent.insert("status".to_string(), serde_json::json!("running"));
885
+ agent.insert(
886
+ "window".to_string(),
887
+ serde_json::json!(spawn.spawn.window.as_str()),
888
+ );
870
889
  agent.insert(
871
890
  "pane_id".to_string(),
872
891
  serde_json::json!(spawn.spawn.pane_id.as_str()),
@@ -882,8 +901,60 @@ fn mark_agent_respawned(
882
901
  if let Some(pane_pid) = pane_pid {
883
902
  agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
884
903
  }
904
+ if matches!(
905
+ restart_mode,
906
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
907
+ ) {
908
+ if let Some(session_id) = spawn.plan.expected_session_id.as_ref() {
909
+ agent.insert(
910
+ "session_id".to_string(),
911
+ serde_json::json!(session_id.as_str()),
912
+ );
913
+ }
914
+ }
885
915
  crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
886
916
  persist_effective_approval_policy_for_restart(agent, safety);
917
+ if let Some(placement) = spawn.layout_placement.as_ref() {
918
+ agent.insert(
919
+ "layout_window".to_string(),
920
+ serde_json::json!(placement.layout_window.as_str()),
921
+ );
922
+ agent.insert(
923
+ "layout_index".to_string(),
924
+ serde_json::json!(placement.layout_index),
925
+ );
926
+ agent.insert("pane_index".to_string(), serde_json::json!(placement.pane_index));
927
+ agent.insert(
928
+ "display".to_string(),
929
+ serde_json::json!({
930
+ "backend": "adaptive",
931
+ "status": "opened",
932
+ "window": placement.layout_window.as_str(),
933
+ "workspace_window": null,
934
+ "pane_id": spawn.spawn.pane_id.as_str(),
935
+ "pane_title": agent_id.as_str(),
936
+ "target": spawn.spawn.pane_id.as_str(),
937
+ "target_worker_session": spawn.spawn.session.as_str(),
938
+ "linked_session": null,
939
+ "leader_session": spawn.spawn.session.as_str(),
940
+ "display_session": null,
941
+ "fallback": null,
942
+ }),
943
+ );
944
+ }
945
+ if matches!(
946
+ restart_mode,
947
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
948
+ ) && spawn.plan.expected_session_id.is_some()
949
+ {
950
+ agent.insert("rollout_path".to_string(), serde_json::Value::Null);
951
+ agent.insert("captured_at".to_string(), serde_json::Value::Null);
952
+ agent.insert("captured_via".to_string(), serde_json::Value::Null);
953
+ agent.insert(
954
+ "attribution_confidence".to_string(),
955
+ serde_json::Value::Null,
956
+ );
957
+ }
887
958
  agent.remove("startup_prompts");
888
959
  agent.remove("startup_prompt_status");
889
960
  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
  }