@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +92 -11
- package/crates/team-agent/src/cli/leader.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +255 -37
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/status.rs +5 -1
- package/crates/team-agent/src/cli/status_port.rs +24 -0
- package/crates/team-agent/src/cli/tests/base.rs +21 -3
- package/crates/team-agent/src/cli/tests/divergence.rs +6 -6
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +285 -2
- package/crates/team-agent/src/cli/tests/status_send.rs +25 -0
- package/crates/team-agent/src/cli/types.rs +32 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/fake_worker.rs +32 -145
- package/crates/team-agent/src/leader/start.rs +272 -8
- package/crates/team-agent/src/leader/tests/basics.rs +1 -0
- package/crates/team-agent/src/leader/tests/identity.rs +121 -25
- package/crates/team-agent/src/leader/types.rs +9 -0
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +631 -70
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +38 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +79 -4
- package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +731 -8
- package/crates/team-agent/src/lifecycle/types.rs +5 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +153 -3
- package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
- package/crates/team-agent/src/messaging/mod.rs +6 -3
- package/crates/team-agent/src/messaging/results.rs +240 -158
- package/crates/team-agent/src/messaging/send.rs +3 -2
- package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
- package/crates/team-agent/src/os_probe.rs +11 -0
- package/crates/team-agent/src/state/persist.rs +6 -0
- package/crates/team-agent/src/tmux_backend.rs +85 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +27 -0
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +277 -0
|
@@ -123,6 +123,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
123
123
|
session_name,
|
|
124
124
|
started,
|
|
125
125
|
dry_run,
|
|
126
|
+
tmux_endpoint: transport.tmux_endpoint(),
|
|
126
127
|
routes,
|
|
127
128
|
permissions,
|
|
128
129
|
safety,
|
|
@@ -157,6 +158,22 @@ fn spawn_agents(
|
|
|
157
158
|
spec.get("runtime").and_then(|v| v.get("fast")),
|
|
158
159
|
Some(Value::Bool(true))
|
|
159
160
|
);
|
|
161
|
+
let display_backend = spec_display_backend(spec);
|
|
162
|
+
let active_agent_ids = spec_agent_values(spec)
|
|
163
|
+
.into_iter()
|
|
164
|
+
.filter_map(|agent| {
|
|
165
|
+
if agent_is_paused(agent) {
|
|
166
|
+
None
|
|
167
|
+
} else {
|
|
168
|
+
agent.get("id").and_then(Value::as_str).map(AgentId::new)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
.collect::<Vec<_>>();
|
|
172
|
+
let layout_plan = if display_backend == DisplayBackend::Adaptive {
|
|
173
|
+
adaptive_layout_plan(&active_agent_ids, ADAPTIVE_LAYOUT_MAX_PER_WINDOW)
|
|
174
|
+
} else {
|
|
175
|
+
Vec::new()
|
|
176
|
+
};
|
|
160
177
|
let mut started = Vec::new();
|
|
161
178
|
for agent in spec_agent_values(spec) {
|
|
162
179
|
let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
|
|
@@ -284,7 +301,6 @@ fn spawn_agents(
|
|
|
284
301
|
)?;
|
|
285
302
|
}
|
|
286
303
|
fill_spawn_placeholders_full(&mut plan.argv, workspace, agent_id_raw, Some(&mcp_team_id));
|
|
287
|
-
let window = WindowName::new(agent_id_raw);
|
|
288
304
|
let mut env =
|
|
289
305
|
inherited_env_with_team_overrides(workspace, agent_id_raw, Some(&mcp_team_id));
|
|
290
306
|
apply_profile_launch_env(&mut env, &profile_launch);
|
|
@@ -349,7 +365,46 @@ fn spawn_agents(
|
|
|
349
365
|
}),
|
|
350
366
|
);
|
|
351
367
|
}
|
|
352
|
-
let
|
|
368
|
+
let placement = layout_plan
|
|
369
|
+
.iter()
|
|
370
|
+
.find(|placement| placement.agent_id == agent_id)
|
|
371
|
+
.cloned();
|
|
372
|
+
let window = placement
|
|
373
|
+
.as_ref()
|
|
374
|
+
.map(|placement| placement.layout_window.clone())
|
|
375
|
+
.unwrap_or_else(|| WindowName::new(agent_id_raw));
|
|
376
|
+
let spawn = if let Some(placement) = placement.as_ref() {
|
|
377
|
+
if placement.starts_window {
|
|
378
|
+
if started.is_empty() {
|
|
379
|
+
transport.spawn_first_with_env_unset(
|
|
380
|
+
session_name,
|
|
381
|
+
&window,
|
|
382
|
+
&plan.argv,
|
|
383
|
+
workspace,
|
|
384
|
+
&env,
|
|
385
|
+
&env_unset,
|
|
386
|
+
)
|
|
387
|
+
} else {
|
|
388
|
+
transport.spawn_into_with_env_unset(
|
|
389
|
+
session_name,
|
|
390
|
+
&window,
|
|
391
|
+
&plan.argv,
|
|
392
|
+
workspace,
|
|
393
|
+
&env,
|
|
394
|
+
&env_unset,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
transport.spawn_split_with_env_unset(
|
|
399
|
+
session_name,
|
|
400
|
+
&window,
|
|
401
|
+
&plan.argv,
|
|
402
|
+
workspace,
|
|
403
|
+
&env,
|
|
404
|
+
&env_unset,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
} else if started.is_empty() {
|
|
353
408
|
transport.spawn_first_with_env_unset(
|
|
354
409
|
session_name,
|
|
355
410
|
&window,
|
|
@@ -383,6 +438,25 @@ fn spawn_agents(
|
|
|
383
438
|
if matches!(transport.liveness(&spawn.pane_id), Ok(PaneLiveness::Dead)) {
|
|
384
439
|
continue;
|
|
385
440
|
}
|
|
441
|
+
let display = if placement.is_some() {
|
|
442
|
+
WorkerDisplay::Adaptive {
|
|
443
|
+
status: DisplayStatus::Opened,
|
|
444
|
+
window: Some(spawn.window.clone()),
|
|
445
|
+
workspace_window: None,
|
|
446
|
+
pane_id: Some(spawn.pane_id.clone()),
|
|
447
|
+
pane_title: Some(agent_id_raw.to_string()),
|
|
448
|
+
target: Some(spawn.pane_id.as_str().to_string()),
|
|
449
|
+
target_worker_session: Some(session_name.as_str().to_string()),
|
|
450
|
+
linked_session: None,
|
|
451
|
+
leader_session: Some(session_name.clone()),
|
|
452
|
+
display_session: None,
|
|
453
|
+
fallback: None,
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
WorkerDisplay::Blocked {
|
|
457
|
+
reason: AdaptiveBlockReason::NotImplementedThisPlatform,
|
|
458
|
+
}
|
|
459
|
+
};
|
|
386
460
|
started.push(StartedAgent {
|
|
387
461
|
agent_id,
|
|
388
462
|
start_mode: StartMode::Fresh,
|
|
@@ -396,14 +470,232 @@ fn spawn_agents(
|
|
|
396
470
|
.clone()
|
|
397
471
|
.or_else(|| profile_launch.claude_projects_root.clone()),
|
|
398
472
|
managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
473
|
+
layout_window: placement
|
|
474
|
+
.as_ref()
|
|
475
|
+
.map(|placement| placement.layout_window.clone()),
|
|
476
|
+
layout_index: placement.as_ref().map(|placement| placement.layout_index),
|
|
477
|
+
pane_index: placement.as_ref().map(|placement| placement.pane_index),
|
|
478
|
+
display,
|
|
402
479
|
});
|
|
403
480
|
}
|
|
404
481
|
Ok(started)
|
|
405
482
|
}
|
|
406
483
|
|
|
484
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
485
|
+
pub(crate) struct LayoutPlacement {
|
|
486
|
+
pub agent_id: AgentId,
|
|
487
|
+
pub layout_window: WindowName,
|
|
488
|
+
pub layout_index: usize,
|
|
489
|
+
pub pane_index: usize,
|
|
490
|
+
pub starts_window: bool,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
pub(crate) fn adaptive_layout_plan(
|
|
494
|
+
agent_ids: &[AgentId],
|
|
495
|
+
max_per_window: usize,
|
|
496
|
+
) -> Vec<LayoutPlacement> {
|
|
497
|
+
let max_per_window = max_per_window.max(1);
|
|
498
|
+
agent_ids
|
|
499
|
+
.iter()
|
|
500
|
+
.enumerate()
|
|
501
|
+
.map(|(idx, agent_id)| {
|
|
502
|
+
let layout_index = idx / max_per_window;
|
|
503
|
+
let pane_index = idx % max_per_window;
|
|
504
|
+
LayoutPlacement {
|
|
505
|
+
agent_id: agent_id.clone(),
|
|
506
|
+
layout_window: WindowName::new(format!("team-w{}", layout_index + 1)),
|
|
507
|
+
layout_index,
|
|
508
|
+
pane_index,
|
|
509
|
+
starts_window: pane_index == 0,
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
.collect()
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
pub(crate) const ADAPTIVE_LAYOUT_MAX_PER_WINDOW: usize = 3;
|
|
516
|
+
|
|
517
|
+
pub(crate) fn state_uses_adaptive_layout(state: &serde_json::Value) -> bool {
|
|
518
|
+
state
|
|
519
|
+
.get("display_backend")
|
|
520
|
+
.and_then(serde_json::Value::as_str)
|
|
521
|
+
.is_some_and(|backend| backend == "adaptive")
|
|
522
|
+
|| state
|
|
523
|
+
.get("runtime")
|
|
524
|
+
.and_then(|runtime| runtime.get("display_backend"))
|
|
525
|
+
.and_then(serde_json::Value::as_str)
|
|
526
|
+
.is_some_and(|backend| backend == "adaptive")
|
|
527
|
+
|| state
|
|
528
|
+
.get("agents")
|
|
529
|
+
.and_then(serde_json::Value::as_object)
|
|
530
|
+
.is_some_and(|agents| {
|
|
531
|
+
agents.values().any(|agent| {
|
|
532
|
+
agent
|
|
533
|
+
.get("layout_window")
|
|
534
|
+
.and_then(serde_json::Value::as_str)
|
|
535
|
+
.is_some_and(|window| !window.is_empty())
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
pub(crate) fn adaptive_placement_for_agent(
|
|
541
|
+
state: &serde_json::Value,
|
|
542
|
+
transport: &dyn Transport,
|
|
543
|
+
session_name: &SessionName,
|
|
544
|
+
agent_id: &AgentId,
|
|
545
|
+
) -> Option<LayoutPlacement> {
|
|
546
|
+
if !state_uses_adaptive_layout(state) {
|
|
547
|
+
return None;
|
|
548
|
+
}
|
|
549
|
+
let live_panes: BTreeSet<String> = transport
|
|
550
|
+
.list_targets()
|
|
551
|
+
.unwrap_or_default()
|
|
552
|
+
.into_iter()
|
|
553
|
+
.map(|pane| pane.pane_id.as_str().to_string())
|
|
554
|
+
.collect();
|
|
555
|
+
let live_windows: BTreeSet<String> = transport
|
|
556
|
+
.list_windows(session_name)
|
|
557
|
+
.unwrap_or_default()
|
|
558
|
+
.into_iter()
|
|
559
|
+
.map(|window| window.as_str().to_string())
|
|
560
|
+
.collect();
|
|
561
|
+
let mut windows: BTreeMap<usize, (String, usize)> = BTreeMap::new();
|
|
562
|
+
if let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) {
|
|
563
|
+
for (id, agent) in agents {
|
|
564
|
+
if id == agent_id.as_str() {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
let Some(window) = agent
|
|
568
|
+
.get("layout_window")
|
|
569
|
+
.or_else(|| agent.get("window"))
|
|
570
|
+
.and_then(serde_json::Value::as_str)
|
|
571
|
+
.filter(|window| !window.is_empty())
|
|
572
|
+
else {
|
|
573
|
+
continue;
|
|
574
|
+
};
|
|
575
|
+
let pane_live = agent
|
|
576
|
+
.get("pane_id")
|
|
577
|
+
.and_then(serde_json::Value::as_str)
|
|
578
|
+
.is_some_and(|pane| live_panes.contains(pane));
|
|
579
|
+
if !pane_live && (!live_panes.is_empty() || !live_windows.contains(window)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
let layout_index = agent
|
|
583
|
+
.get("layout_index")
|
|
584
|
+
.and_then(serde_json::Value::as_u64)
|
|
585
|
+
.map(|idx| idx as usize)
|
|
586
|
+
.or_else(|| parse_team_layout_index(window))
|
|
587
|
+
.unwrap_or(windows.len());
|
|
588
|
+
let entry = windows
|
|
589
|
+
.entry(layout_index)
|
|
590
|
+
.or_insert_with(|| (window.to_string(), 0));
|
|
591
|
+
entry.1 = entry.1.saturating_add(1);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if let Some((&layout_index, (window, count))) = windows.iter().next_back() {
|
|
595
|
+
if *count < ADAPTIVE_LAYOUT_MAX_PER_WINDOW {
|
|
596
|
+
return Some(LayoutPlacement {
|
|
597
|
+
agent_id: agent_id.clone(),
|
|
598
|
+
layout_window: WindowName::new(window.clone()),
|
|
599
|
+
layout_index,
|
|
600
|
+
pane_index: *count,
|
|
601
|
+
starts_window: false,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
let next_index = windows.keys().next_back().map(|idx| idx + 1).unwrap_or(0);
|
|
606
|
+
let base = format!("team-w{}", next_index + 1);
|
|
607
|
+
Some(LayoutPlacement {
|
|
608
|
+
agent_id: agent_id.clone(),
|
|
609
|
+
layout_window: unique_layout_window_name(&base, &live_windows),
|
|
610
|
+
layout_index: next_index,
|
|
611
|
+
pane_index: 0,
|
|
612
|
+
starts_window: true,
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
pub(crate) fn adaptive_existing_placement_for_agent(
|
|
617
|
+
state: &serde_json::Value,
|
|
618
|
+
transport: &dyn Transport,
|
|
619
|
+
session_name: &SessionName,
|
|
620
|
+
agent_id: &AgentId,
|
|
621
|
+
) -> Option<LayoutPlacement> {
|
|
622
|
+
if !state_uses_adaptive_layout(state) {
|
|
623
|
+
return None;
|
|
624
|
+
}
|
|
625
|
+
let agent = state.get("agents")?.get(agent_id.as_str())?;
|
|
626
|
+
let window = agent
|
|
627
|
+
.get("layout_window")
|
|
628
|
+
.or_else(|| agent.get("window"))
|
|
629
|
+
.and_then(serde_json::Value::as_str)
|
|
630
|
+
.filter(|window| !window.is_empty())?;
|
|
631
|
+
let layout_index = agent
|
|
632
|
+
.get("layout_index")
|
|
633
|
+
.and_then(serde_json::Value::as_u64)
|
|
634
|
+
.map(|idx| idx as usize)
|
|
635
|
+
.or_else(|| parse_team_layout_index(window))
|
|
636
|
+
.unwrap_or(0);
|
|
637
|
+
let desired_pane_index = agent
|
|
638
|
+
.get("pane_index")
|
|
639
|
+
.and_then(serde_json::Value::as_u64)
|
|
640
|
+
.map(|idx| idx as usize)
|
|
641
|
+
.unwrap_or(0);
|
|
642
|
+
let live_windows: BTreeSet<String> = transport
|
|
643
|
+
.list_windows(session_name)
|
|
644
|
+
.unwrap_or_default()
|
|
645
|
+
.into_iter()
|
|
646
|
+
.map(|window| window.as_str().to_string())
|
|
647
|
+
.collect();
|
|
648
|
+
if !live_windows.contains(window) {
|
|
649
|
+
return Some(LayoutPlacement {
|
|
650
|
+
agent_id: agent_id.clone(),
|
|
651
|
+
layout_window: WindowName::new(window),
|
|
652
|
+
layout_index,
|
|
653
|
+
pane_index: desired_pane_index,
|
|
654
|
+
starts_window: desired_pane_index == 0,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
let existing_panes = transport
|
|
658
|
+
.list_targets()
|
|
659
|
+
.unwrap_or_default()
|
|
660
|
+
.into_iter()
|
|
661
|
+
.filter(|pane| {
|
|
662
|
+
pane.window_name.as_ref().is_some_and(|name| name.as_str() == window)
|
|
663
|
+
&& agent
|
|
664
|
+
.get("pane_id")
|
|
665
|
+
.and_then(serde_json::Value::as_str)
|
|
666
|
+
.is_none_or(|agent_pane| agent_pane != pane.pane_id.as_str())
|
|
667
|
+
})
|
|
668
|
+
.count();
|
|
669
|
+
Some(LayoutPlacement {
|
|
670
|
+
agent_id: agent_id.clone(),
|
|
671
|
+
layout_window: WindowName::new(window),
|
|
672
|
+
layout_index,
|
|
673
|
+
pane_index: existing_panes,
|
|
674
|
+
starts_window: false,
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
fn parse_team_layout_index(window: &str) -> Option<usize> {
|
|
679
|
+
window
|
|
680
|
+
.strip_prefix("team-w")
|
|
681
|
+
.and_then(|rest| rest.split('-').next())
|
|
682
|
+
.and_then(|raw| raw.parse::<usize>().ok())
|
|
683
|
+
.and_then(|idx| idx.checked_sub(1))
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
fn unique_layout_window_name(base: &str, live_windows: &BTreeSet<String>) -> WindowName {
|
|
687
|
+
if !live_windows.contains(base) {
|
|
688
|
+
return WindowName::new(base);
|
|
689
|
+
}
|
|
690
|
+
for suffix in 2.. {
|
|
691
|
+
let candidate = format!("{base}-{suffix}");
|
|
692
|
+
if !live_windows.contains(&candidate) {
|
|
693
|
+
return WindowName::new(candidate);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
unreachable!("unbounded suffix search always returns")
|
|
697
|
+
}
|
|
698
|
+
|
|
407
699
|
fn persist_spawn_agent_state(
|
|
408
700
|
workspace: &Path,
|
|
409
701
|
spec_path: &Path,
|
|
@@ -464,7 +756,12 @@ fn persist_spawn_agent_state(
|
|
|
464
756
|
agents.insert(id.to_string(), serde_json::Value::Object(paused));
|
|
465
757
|
continue;
|
|
466
758
|
}
|
|
467
|
-
let
|
|
759
|
+
let started_agent = started.iter().find(|agent| agent.agent_id.as_str() == id);
|
|
760
|
+
let window = started_agent
|
|
761
|
+
.and_then(|started| started.layout_window.as_ref())
|
|
762
|
+
.map(WindowName::as_str)
|
|
763
|
+
.or_else(|| agent.get("window").and_then(Value::as_str))
|
|
764
|
+
.unwrap_or(id);
|
|
468
765
|
if !live_started_agents.contains(id)
|
|
469
766
|
|| (!live_windows.is_empty() && !live_windows.contains(window))
|
|
470
767
|
{
|
|
@@ -483,7 +780,6 @@ fn persist_spawn_agent_state(
|
|
|
483
780
|
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
484
781
|
let spawned_at = spawn_timestamp_for_agent(spawn_index);
|
|
485
782
|
spawn_index = spawn_index.saturating_add(1);
|
|
486
|
-
let started_agent = started.iter().find(|agent| agent.agent_id.as_str() == id);
|
|
487
783
|
agents.insert(
|
|
488
784
|
id.to_string(),
|
|
489
785
|
running_agent_state(
|
|
@@ -562,6 +858,7 @@ fn save_launched_team_state_for_key(
|
|
|
562
858
|
);
|
|
563
859
|
}
|
|
564
860
|
promote_launched_binding_from_team_entry(&mut launched, &launched_key);
|
|
861
|
+
preserve_existing_leader_topology(&existing, &launched_key, &mut launched);
|
|
565
862
|
drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
|
|
566
863
|
drop_bare_worker_seeded_owner(&mut launched, &launched_key);
|
|
567
864
|
let merged = if team_key.is_some() {
|
|
@@ -575,6 +872,28 @@ fn save_launched_team_state_for_key(
|
|
|
575
872
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
576
873
|
}
|
|
577
874
|
|
|
875
|
+
fn preserve_existing_leader_topology(
|
|
876
|
+
existing: &serde_json::Value,
|
|
877
|
+
launched_key: &str,
|
|
878
|
+
launched: &mut serde_json::Value,
|
|
879
|
+
) {
|
|
880
|
+
let Some(obj) = launched.as_object_mut() else {
|
|
881
|
+
return;
|
|
882
|
+
};
|
|
883
|
+
let existing_team = existing
|
|
884
|
+
.get("teams")
|
|
885
|
+
.and_then(serde_json::Value::as_object)
|
|
886
|
+
.and_then(|teams| teams.get(launched_key))
|
|
887
|
+
.unwrap_or(existing);
|
|
888
|
+
for key in ["is_external_leader", "leader_client"] {
|
|
889
|
+
if !obj.contains_key(key) {
|
|
890
|
+
if let Some(value) = existing_team.get(key).or_else(|| existing.get(key)) {
|
|
891
|
+
obj.insert(key.to_string(), value.clone());
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
578
897
|
fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
579
898
|
if has_positive_caller_leader_env() {
|
|
580
899
|
return;
|
|
@@ -1279,7 +1598,11 @@ fn running_agent_state(
|
|
|
1279
1598
|
.get("profile")
|
|
1280
1599
|
.map(yaml_value_to_json)
|
|
1281
1600
|
.unwrap_or(serde_json::Value::Null);
|
|
1282
|
-
let window =
|
|
1601
|
+
let window = started_agent
|
|
1602
|
+
.and_then(|started| started.layout_window.as_ref())
|
|
1603
|
+
.map(WindowName::as_str)
|
|
1604
|
+
.or_else(|| agent.get("window").and_then(Value::as_str))
|
|
1605
|
+
.unwrap_or(id);
|
|
1283
1606
|
let mcp_config = crate::provider::get_adapter(provider)
|
|
1284
1607
|
.mcp_config(auth_mode)
|
|
1285
1608
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
@@ -1325,6 +1648,25 @@ fn running_agent_state(
|
|
|
1325
1648
|
);
|
|
1326
1649
|
if let Some(started_agent) = started_agent {
|
|
1327
1650
|
persist_started_agent_plan_state(&mut state, started_agent);
|
|
1651
|
+
if let Some(layout_window) = started_agent.layout_window.as_ref() {
|
|
1652
|
+
state.insert(
|
|
1653
|
+
"layout_window".to_string(),
|
|
1654
|
+
serde_json::json!(layout_window.as_str()),
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
if let Some(layout_index) = started_agent.layout_index {
|
|
1658
|
+
state.insert("layout_index".to_string(), serde_json::json!(layout_index));
|
|
1659
|
+
}
|
|
1660
|
+
if let Some(pane_index) = started_agent.pane_index {
|
|
1661
|
+
state.insert("pane_index".to_string(), serde_json::json!(pane_index));
|
|
1662
|
+
}
|
|
1663
|
+
if !matches!(started_agent.display, WorkerDisplay::Blocked { .. }) {
|
|
1664
|
+
state.insert(
|
|
1665
|
+
"display".to_string(),
|
|
1666
|
+
serde_json::to_value(&started_agent.display)
|
|
1667
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1328
1670
|
}
|
|
1329
1671
|
state.insert(
|
|
1330
1672
|
"spawn_cwd".to_string(),
|
|
@@ -2016,6 +2358,17 @@ fn transport_has_session(transport: &dyn Transport, session_name: &SessionName)
|
|
|
2016
2358
|
}
|
|
2017
2359
|
}
|
|
2018
2360
|
|
|
2361
|
+
fn spec_display_backend(spec: &Value) -> DisplayBackend {
|
|
2362
|
+
let requested = spec
|
|
2363
|
+
.get("runtime")
|
|
2364
|
+
.and_then(|runtime| runtime.get("display_backend"))
|
|
2365
|
+
.and_then(Value::as_str)
|
|
2366
|
+
.and_then(|backend| {
|
|
2367
|
+
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
2368
|
+
});
|
|
2369
|
+
crate::lifecycle::display::resolve_display_backend(requested, None).backend
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2019
2372
|
fn parse_provider(raw: &str) -> Option<Provider> {
|
|
2020
2373
|
match raw {
|
|
2021
2374
|
"claude" => Some(Provider::Claude),
|
|
@@ -2242,6 +2595,7 @@ pub fn quick_start_in_workspace(
|
|
|
2242
2595
|
team_id: Option<&str>,
|
|
2243
2596
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2244
2597
|
let workspace = explicit_quick_start_workspace(workspace);
|
|
2598
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
2245
2599
|
quick_start_with_transport_in_workspace(
|
|
2246
2600
|
&workspace,
|
|
2247
2601
|
agents_dir,
|
|
@@ -2249,11 +2603,55 @@ pub fn quick_start_in_workspace(
|
|
|
2249
2603
|
yes,
|
|
2250
2604
|
fresh,
|
|
2251
2605
|
team_id,
|
|
2252
|
-
|
|
2253
|
-
|
|
2606
|
+
&transport,
|
|
2607
|
+
)
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
pub fn quick_start_in_workspace_with_display(
|
|
2611
|
+
workspace: &Path,
|
|
2612
|
+
agents_dir: &Path,
|
|
2613
|
+
name: Option<&str>,
|
|
2614
|
+
yes: bool,
|
|
2615
|
+
fresh: bool,
|
|
2616
|
+
team_id: Option<&str>,
|
|
2617
|
+
open_display: bool,
|
|
2618
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
2619
|
+
let workspace = explicit_quick_start_workspace(workspace);
|
|
2620
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
2621
|
+
quick_start_with_transport_in_workspace_with_display(
|
|
2622
|
+
&workspace,
|
|
2623
|
+
agents_dir,
|
|
2624
|
+
name,
|
|
2625
|
+
yes,
|
|
2626
|
+
fresh,
|
|
2627
|
+
team_id,
|
|
2628
|
+
&transport,
|
|
2629
|
+
open_display,
|
|
2254
2630
|
)
|
|
2255
2631
|
}
|
|
2256
2632
|
|
|
2633
|
+
pub(crate) fn quick_start_tmux_backend(workspace: &Path) -> crate::tmux_backend::TmuxBackend {
|
|
2634
|
+
if let Some(endpoint) = crate::tmux_backend::socket_name_from_tmux_env() {
|
|
2635
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
|
|
2636
|
+
} else {
|
|
2637
|
+
crate::tmux_backend::TmuxBackend::for_workspace(workspace)
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
pub(crate) fn selected_tmux_socket_source(
|
|
2642
|
+
transport: &dyn Transport,
|
|
2643
|
+
workspace: &Path,
|
|
2644
|
+
) -> Option<&'static str> {
|
|
2645
|
+
let endpoint = transport.tmux_endpoint()?;
|
|
2646
|
+
if crate::tmux_backend::socket_name_from_tmux_env().as_deref() == Some(endpoint.as_str()) {
|
|
2647
|
+
Some("leader_env")
|
|
2648
|
+
} else if endpoint == crate::tmux_backend::socket_name_for_workspace(workspace) {
|
|
2649
|
+
Some("workspace")
|
|
2650
|
+
} else {
|
|
2651
|
+
None
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2257
2655
|
fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
|
|
2258
2656
|
std::fs::canonicalize(workspace).unwrap_or_else(|_| {
|
|
2259
2657
|
if workspace.is_absolute() {
|
|
@@ -2290,6 +2688,21 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2290
2688
|
fresh: bool,
|
|
2291
2689
|
team_id: Option<&str>,
|
|
2292
2690
|
transport: &dyn Transport,
|
|
2691
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
2692
|
+
quick_start_with_transport_in_workspace_with_display(
|
|
2693
|
+
workspace, agents_dir, name, yes, fresh, team_id, transport, true,
|
|
2694
|
+
)
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
pub fn quick_start_with_transport_in_workspace_with_display(
|
|
2698
|
+
workspace: &Path,
|
|
2699
|
+
agents_dir: &Path,
|
|
2700
|
+
name: Option<&str>,
|
|
2701
|
+
yes: bool,
|
|
2702
|
+
fresh: bool,
|
|
2703
|
+
team_id: Option<&str>,
|
|
2704
|
+
transport: &dyn Transport,
|
|
2705
|
+
open_display: bool,
|
|
2293
2706
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2294
2707
|
// B-7 / 036b N38 三行 fail-fast — TEAM_AGENT_LEADER_PANE_ID 主动路径在 quick-start
|
|
2295
2708
|
// 入口验活;死/缺(Dead)的 pane 必须明确报错,不可 silent bind 到 spawner /
|
|
@@ -2308,6 +2721,9 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2308
2721
|
let workspace = workspace.to_path_buf();
|
|
2309
2722
|
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
2310
2723
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
2724
|
+
if !open_display {
|
|
2725
|
+
override_spec_display_backend(&mut spec, "none");
|
|
2726
|
+
}
|
|
2311
2727
|
let requested_team = quick_start_requested_team_key(team_id, name)
|
|
2312
2728
|
.map(str::to_string)
|
|
2313
2729
|
.or_else(|| spec_team_id(&spec));
|
|
@@ -2343,7 +2759,13 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2343
2759
|
.as_ref()
|
|
2344
2760
|
.map(|session| {
|
|
2345
2761
|
let windows = quick_start_attach_window_names(&state);
|
|
2346
|
-
|
|
2762
|
+
attach_commands_for_runtime_windows(
|
|
2763
|
+
state
|
|
2764
|
+
.get("tmux_endpoint")
|
|
2765
|
+
.and_then(serde_json::Value::as_str)
|
|
2766
|
+
.or_else(|| {
|
|
2767
|
+
state.get("tmux_socket").and_then(serde_json::Value::as_str)
|
|
2768
|
+
}),
|
|
2347
2769
|
&workspace,
|
|
2348
2770
|
session,
|
|
2349
2771
|
windows.iter().map(String::as_str),
|
|
@@ -2394,7 +2816,8 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2394
2816
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
2395
2817
|
let resolved_spec_path =
|
|
2396
2818
|
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
2397
|
-
let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
2819
|
+
let mut state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
2820
|
+
annotate_runtime_tmux_endpoint(&mut state, transport, &workspace);
|
|
2398
2821
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
2399
2822
|
annotate_persisted_team_depth(
|
|
2400
2823
|
&workspace,
|
|
@@ -2434,13 +2857,17 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2434
2857
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
2435
2858
|
// bare Ready.
|
|
2436
2859
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
2437
|
-
let
|
|
2860
|
+
let attach_windows = load_runtime_state(&workspace)
|
|
2861
|
+
.ok()
|
|
2862
|
+
.map(|state| {
|
|
2863
|
+
attach_window_names_with_managed_leader(&state, started_attach_window_names(&launch.started))
|
|
2864
|
+
})
|
|
2865
|
+
.unwrap_or_else(|| started_attach_window_names(&launch.started));
|
|
2866
|
+
let attach_commands = attach_commands_for_runtime_windows(
|
|
2867
|
+
launch.tmux_endpoint.as_deref(),
|
|
2438
2868
|
&workspace,
|
|
2439
2869
|
&session_name,
|
|
2440
|
-
|
|
2441
|
-
.started
|
|
2442
|
-
.iter()
|
|
2443
|
-
.map(|started| started.agent_id.as_str()),
|
|
2870
|
+
attach_windows.iter().map(String::as_str),
|
|
2444
2871
|
);
|
|
2445
2872
|
let mut next_actions = vec![format!(
|
|
2446
2873
|
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
@@ -2464,8 +2891,104 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2464
2891
|
})
|
|
2465
2892
|
}
|
|
2466
2893
|
|
|
2894
|
+
fn annotate_runtime_tmux_endpoint(state: &mut serde_json::Value, transport: &dyn Transport, workspace: &Path) {
|
|
2895
|
+
let Some(endpoint) = transport.tmux_endpoint() else {
|
|
2896
|
+
return;
|
|
2897
|
+
};
|
|
2898
|
+
if let Some(obj) = state.as_object_mut() {
|
|
2899
|
+
obj.insert("tmux_endpoint".to_string(), serde_json::json!(endpoint));
|
|
2900
|
+
obj.insert(
|
|
2901
|
+
"tmux_socket".to_string(),
|
|
2902
|
+
obj.get("tmux_endpoint")
|
|
2903
|
+
.cloned()
|
|
2904
|
+
.unwrap_or(serde_json::Value::Null),
|
|
2905
|
+
);
|
|
2906
|
+
if let Some(source) = selected_tmux_socket_source(transport, workspace) {
|
|
2907
|
+
obj.insert("tmux_socket_source".to_string(), serde_json::json!(source));
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
fn attach_commands_for_runtime_windows<'a>(
|
|
2913
|
+
endpoint: Option<&str>,
|
|
2914
|
+
workspace: &Path,
|
|
2915
|
+
session_name: &SessionName,
|
|
2916
|
+
window_names: impl IntoIterator<Item = &'a str>,
|
|
2917
|
+
) -> Vec<String> {
|
|
2918
|
+
let windows = window_names.into_iter().collect::<Vec<_>>();
|
|
2919
|
+
let attach = if let Some(endpoint) = endpoint.filter(|endpoint| !endpoint.is_empty()) {
|
|
2920
|
+
if Path::new(endpoint).is_absolute() {
|
|
2921
|
+
windows
|
|
2922
|
+
.iter()
|
|
2923
|
+
.map(|window_name| {
|
|
2924
|
+
format!(
|
|
2925
|
+
"tmux -S {} attach -t {}:{}",
|
|
2926
|
+
endpoint,
|
|
2927
|
+
session_name.as_str(),
|
|
2928
|
+
window_name
|
|
2929
|
+
)
|
|
2930
|
+
})
|
|
2931
|
+
.collect::<Vec<_>>()
|
|
2932
|
+
} else {
|
|
2933
|
+
crate::tmux_backend::attach_commands_for_windows(
|
|
2934
|
+
workspace,
|
|
2935
|
+
session_name,
|
|
2936
|
+
windows.iter().copied(),
|
|
2937
|
+
)
|
|
2938
|
+
}
|
|
2939
|
+
} else {
|
|
2940
|
+
crate::tmux_backend::attach_commands_for_windows(
|
|
2941
|
+
workspace,
|
|
2942
|
+
session_name,
|
|
2943
|
+
windows.iter().copied(),
|
|
2944
|
+
)
|
|
2945
|
+
};
|
|
2946
|
+
attach
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
fn started_attach_window_names(started: &[StartedAgent]) -> Vec<String> {
|
|
2950
|
+
let mut windows = started
|
|
2951
|
+
.iter()
|
|
2952
|
+
.map(|started| {
|
|
2953
|
+
started
|
|
2954
|
+
.layout_window
|
|
2955
|
+
.as_ref()
|
|
2956
|
+
.map(|window| window.as_str().to_string())
|
|
2957
|
+
.unwrap_or_else(|| started.agent_id.as_str().to_string())
|
|
2958
|
+
})
|
|
2959
|
+
.collect::<Vec<_>>();
|
|
2960
|
+
windows.sort();
|
|
2961
|
+
windows.dedup();
|
|
2962
|
+
windows
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
pub(crate) fn attach_window_names_for_state_agents<'a>(
|
|
2966
|
+
state: &serde_json::Value,
|
|
2967
|
+
agent_ids: impl IntoIterator<Item = &'a str>,
|
|
2968
|
+
) -> Vec<String> {
|
|
2969
|
+
let windows = agent_ids
|
|
2970
|
+
.into_iter()
|
|
2971
|
+
.map(|agent_id| {
|
|
2972
|
+
state
|
|
2973
|
+
.get("agents")
|
|
2974
|
+
.and_then(serde_json::Value::as_object)
|
|
2975
|
+
.and_then(|agents| agents.get(agent_id))
|
|
2976
|
+
.and_then(|agent| {
|
|
2977
|
+
agent
|
|
2978
|
+
.get("layout_window")
|
|
2979
|
+
.or_else(|| agent.get("window"))
|
|
2980
|
+
.and_then(serde_json::Value::as_str)
|
|
2981
|
+
.filter(|window| !window.is_empty())
|
|
2982
|
+
})
|
|
2983
|
+
.unwrap_or(agent_id)
|
|
2984
|
+
.to_string()
|
|
2985
|
+
})
|
|
2986
|
+
.collect::<Vec<_>>();
|
|
2987
|
+
attach_window_names_with_managed_leader(state, windows)
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2467
2990
|
fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
|
|
2468
|
-
let
|
|
2991
|
+
let windows = state
|
|
2469
2992
|
.get("agents")
|
|
2470
2993
|
.and_then(serde_json::Value::as_object)
|
|
2471
2994
|
.map(|agents| {
|
|
@@ -2482,11 +3005,28 @@ fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
|
|
|
2482
3005
|
.collect::<Vec<_>>()
|
|
2483
3006
|
})
|
|
2484
3007
|
.unwrap_or_default();
|
|
3008
|
+
attach_window_names_with_managed_leader(state, windows)
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
fn attach_window_names_with_managed_leader(
|
|
3012
|
+
state: &serde_json::Value,
|
|
3013
|
+
mut windows: Vec<String>,
|
|
3014
|
+
) -> Vec<String> {
|
|
3015
|
+
if state_uses_managed_leader(state) {
|
|
3016
|
+
windows.push("leader".to_string());
|
|
3017
|
+
}
|
|
2485
3018
|
windows.sort();
|
|
2486
3019
|
windows.dedup();
|
|
2487
3020
|
windows
|
|
2488
3021
|
}
|
|
2489
3022
|
|
|
3023
|
+
fn state_uses_managed_leader(state: &serde_json::Value) -> bool {
|
|
3024
|
+
state
|
|
3025
|
+
.get("is_external_leader")
|
|
3026
|
+
.and_then(serde_json::Value::as_bool)
|
|
3027
|
+
.is_some_and(|external| !external)
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2490
3030
|
/// BUG-7 helper: derive a [`QuickStartReadiness`] verdict from the just-written
|
|
2491
3031
|
/// runtime state. Reads `agents[*].status`; any non-`running` agent flips the
|
|
2492
3032
|
/// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
|
|
@@ -2870,7 +3410,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
2870
3410
|
role_file_path.display()
|
|
2871
3411
|
)));
|
|
2872
3412
|
}
|
|
2873
|
-
if
|
|
3413
|
+
if runtime_agent_exists(&owner_state, agent_id) {
|
|
2874
3414
|
return Err(LifecycleError::RequirementUnmet(format!(
|
|
2875
3415
|
"agent id already exists: {agent_id}"
|
|
2876
3416
|
)));
|
|
@@ -3027,29 +3567,74 @@ fn inject_agent_into_spec(
|
|
|
3027
3567
|
};
|
|
3028
3568
|
// agents 列表追加。
|
|
3029
3569
|
match pairs.iter_mut().find(|(k, _)| k == "agents") {
|
|
3030
|
-
Some((_, Value::List(agents))) =>
|
|
3570
|
+
Some((_, Value::List(agents))) => {
|
|
3571
|
+
if !agents
|
|
3572
|
+
.iter()
|
|
3573
|
+
.any(|existing| yaml_agent_id(existing) == Some(agent_id))
|
|
3574
|
+
{
|
|
3575
|
+
agents.push(agent);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3031
3578
|
_ => return Err(LifecycleError::Compile("spec.agents missing or not a list".to_string())),
|
|
3032
3579
|
}
|
|
3033
3580
|
// routing.rules 追加 route-<id>(与 compile_team 同形)。
|
|
3034
3581
|
if let Some((_, Value::Map(routing))) = pairs.iter_mut().find(|(k, _)| k == "routing") {
|
|
3035
3582
|
if let Some((_, Value::List(rules))) = routing.iter_mut().find(|(k, _)| k == "rules") {
|
|
3036
|
-
rules
|
|
3037
|
-
|
|
3038
|
-
(
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3583
|
+
if !rules
|
|
3584
|
+
.iter()
|
|
3585
|
+
.any(|rule| yaml_route_assigns_to(rule) == Some(agent_id))
|
|
3586
|
+
{
|
|
3587
|
+
rules.push(Value::Map(vec![
|
|
3588
|
+
("id".to_string(), Value::Str(format!("route-{agent_id}"))),
|
|
3589
|
+
(
|
|
3590
|
+
"match".to_string(),
|
|
3591
|
+
Value::Map(vec![(
|
|
3592
|
+
"assignee".to_string(),
|
|
3593
|
+
Value::List(vec![Value::Str(agent_id.to_string())]),
|
|
3594
|
+
)]),
|
|
3595
|
+
),
|
|
3596
|
+
("assign_to".to_string(), Value::Str(agent_id.to_string())),
|
|
3597
|
+
("priority".to_string(), Value::Int(10)),
|
|
3598
|
+
]));
|
|
3599
|
+
}
|
|
3048
3600
|
}
|
|
3049
3601
|
}
|
|
3050
3602
|
Ok(())
|
|
3051
3603
|
}
|
|
3052
3604
|
|
|
3605
|
+
fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId) -> bool {
|
|
3606
|
+
state
|
|
3607
|
+
.get("agents")
|
|
3608
|
+
.and_then(serde_json::Value::as_object)
|
|
3609
|
+
.is_some_and(|agents| agents.contains_key(agent_id.as_str()))
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
fn yaml_agent_id(agent: &Value) -> Option<&str> {
|
|
3613
|
+
let Value::Map(pairs) = agent else {
|
|
3614
|
+
return None;
|
|
3615
|
+
};
|
|
3616
|
+
pairs
|
|
3617
|
+
.iter()
|
|
3618
|
+
.find(|(key, _)| key == "id")
|
|
3619
|
+
.and_then(|(_, value)| match value {
|
|
3620
|
+
Value::Str(id) => Some(id.as_str()),
|
|
3621
|
+
_ => None,
|
|
3622
|
+
})
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
fn yaml_route_assigns_to(rule: &Value) -> Option<&str> {
|
|
3626
|
+
let Value::Map(pairs) = rule else {
|
|
3627
|
+
return None;
|
|
3628
|
+
};
|
|
3629
|
+
pairs
|
|
3630
|
+
.iter()
|
|
3631
|
+
.find(|(key, _)| key == "assign_to")
|
|
3632
|
+
.and_then(|(_, value)| match value {
|
|
3633
|
+
Value::Str(id) => Some(id.as_str()),
|
|
3634
|
+
_ => None,
|
|
3635
|
+
})
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3053
3638
|
/// `fork_agent(workspace, source_agent_id, as_agent_id, ...)`(`lifecycle/operations.py:284`)。
|
|
3054
3639
|
/// native session fork(provider 须 supports_session_fork ∧ auth_mode!=compatible_api);
|
|
3055
3640
|
/// 失败回滚,每条失败臂 `adapter.cleanup_mcp`。
|
|
@@ -3751,15 +4336,7 @@ fn initial_runtime_state(
|
|
|
3751
4336
|
}
|
|
3752
4337
|
agents.insert(id.to_string(), value);
|
|
3753
4338
|
}
|
|
3754
|
-
let
|
|
3755
|
-
.get("runtime")
|
|
3756
|
-
.and_then(|runtime| runtime.get("display_backend"))
|
|
3757
|
-
.and_then(Value::as_str)
|
|
3758
|
-
.and_then(|backend| {
|
|
3759
|
-
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
3760
|
-
});
|
|
3761
|
-
let display_backend =
|
|
3762
|
-
crate::lifecycle::display::resolve_display_backend(requested_display, None).backend;
|
|
4339
|
+
let display_backend = spec_display_backend(spec);
|
|
3763
4340
|
let mut state = serde_json::Map::new();
|
|
3764
4341
|
state.insert(
|
|
3765
4342
|
"spec_path".to_string(),
|
|
@@ -3927,6 +4504,14 @@ pub(crate) fn write_spec_atomic(spec_path: &Path, spec: &Value) -> Result<(), Li
|
|
|
3927
4504
|
}
|
|
3928
4505
|
|
|
3929
4506
|
pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
4507
|
+
override_spec_runtime_str(spec, "session_name", session_name);
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
fn override_spec_display_backend(spec: &mut Value, display_backend: &str) {
|
|
4511
|
+
override_spec_runtime_str(spec, "display_backend", display_backend);
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
fn override_spec_runtime_str(spec: &mut Value, key: &str, value: &str) {
|
|
3930
4515
|
let Value::Map(root) = spec else { return };
|
|
3931
4516
|
let runtime_slot = root
|
|
3932
4517
|
.iter_mut()
|
|
@@ -3934,28 +4519,19 @@ pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
3934
4519
|
.map(|(_, v)| v);
|
|
3935
4520
|
match runtime_slot {
|
|
3936
4521
|
Some(Value::Map(runtime)) => {
|
|
3937
|
-
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k ==
|
|
3938
|
-
*existing = Value::Str(
|
|
4522
|
+
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == key) {
|
|
4523
|
+
*existing = Value::Str(value.to_string());
|
|
3939
4524
|
} else {
|
|
3940
|
-
runtime.push((
|
|
3941
|
-
"session_name".to_string(),
|
|
3942
|
-
Value::Str(session_name.to_string()),
|
|
3943
|
-
));
|
|
4525
|
+
runtime.push((key.to_string(), Value::Str(value.to_string())));
|
|
3944
4526
|
}
|
|
3945
4527
|
}
|
|
3946
4528
|
Some(other) => {
|
|
3947
|
-
*other = Value::Map(vec![(
|
|
3948
|
-
"session_name".to_string(),
|
|
3949
|
-
Value::Str(session_name.to_string()),
|
|
3950
|
-
)]);
|
|
4529
|
+
*other = Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]);
|
|
3951
4530
|
}
|
|
3952
4531
|
None => {
|
|
3953
4532
|
root.push((
|
|
3954
4533
|
"runtime".to_string(),
|
|
3955
|
-
Value::Map(vec![(
|
|
3956
|
-
"session_name".to_string(),
|
|
3957
|
-
Value::Str(session_name.to_string()),
|
|
3958
|
-
)]),
|
|
4534
|
+
Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]),
|
|
3959
4535
|
));
|
|
3960
4536
|
}
|
|
3961
4537
|
}
|
|
@@ -4104,20 +4680,5 @@ fn team_workspace(team_dir: &Path) -> PathBuf {
|
|
|
4104
4680
|
})
|
|
4105
4681
|
}
|
|
4106
4682
|
|
|
4107
|
-
fn agent_id_exists_in_team_dir(team_dir: &Path, agent_id: &AgentId) -> bool {
|
|
4108
|
-
let spec_path = team_dir.join("team.spec.yaml");
|
|
4109
|
-
if let Ok(text) = std::fs::read_to_string(&spec_path) {
|
|
4110
|
-
if let Ok(spec) = yaml::loads(&text) {
|
|
4111
|
-
return spec_agents(&spec)
|
|
4112
|
-
.into_iter()
|
|
4113
|
-
.any(|existing| existing.as_str() == agent_id.as_str());
|
|
4114
|
-
}
|
|
4115
|
-
}
|
|
4116
|
-
team_dir
|
|
4117
|
-
.join("agents")
|
|
4118
|
-
.join(format!("{}.md", agent_id.as_str()))
|
|
4119
|
-
.exists()
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
4683
|
mod plan;
|
|
4123
4684
|
pub use plan::{handle_report_result, start_plan};
|