@team-agent/installer 0.3.13 → 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.
- 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/mod.rs +228 -85
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
- package/crates/team-agent/src/cli/types.rs +30 -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 +279 -4
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +692 -92
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
- 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 +90 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +31 -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(
|
|
@@ -1059,23 +1355,13 @@ fn unbound_launched_provider(launched: &serde_json::Value) -> Option<String> {
|
|
|
1059
1355
|
{
|
|
1060
1356
|
return Some(provider);
|
|
1061
1357
|
}
|
|
1062
|
-
let workspace = launched
|
|
1063
|
-
.get("workspace")
|
|
1064
|
-
.and_then(serde_json::Value::as_str)
|
|
1065
|
-
.filter(|workspace| !workspace.is_empty())?;
|
|
1066
1358
|
let pane = launched
|
|
1067
1359
|
.get("team_owner")
|
|
1068
1360
|
.and_then(|owner| owner.get("pane_id"))
|
|
1069
1361
|
.and_then(serde_json::Value::as_str)
|
|
1070
1362
|
.filter(|pane| !pane.is_empty())?;
|
|
1071
1363
|
let target = PaneId::new(pane);
|
|
1072
|
-
|
|
1073
|
-
.list_targets()
|
|
1074
|
-
.ok()?
|
|
1075
|
-
.into_iter()
|
|
1076
|
-
.find(|info| info.pane_id == target)
|
|
1077
|
-
.and_then(|info| crate::leader::attribute_pane_provider(&info))
|
|
1078
|
-
.and_then(provider_wire_string)
|
|
1364
|
+
attributed_provider_for_pane_across_tmux_sockets(&target).and_then(provider_wire_string)
|
|
1079
1365
|
}
|
|
1080
1366
|
|
|
1081
1367
|
fn provider_wire_string(provider: Provider) -> Option<String> {
|
|
@@ -1084,9 +1370,38 @@ fn provider_wire_string(provider: Provider) -> Option<String> {
|
|
|
1084
1370
|
.and_then(|value| value.as_str().map(str::to_string))
|
|
1085
1371
|
}
|
|
1086
1372
|
|
|
1373
|
+
fn attributed_provider_for_pane_across_tmux_sockets(pane: &PaneId) -> Option<Provider> {
|
|
1374
|
+
crate::tmux_backend::tmux_socket_endpoints()
|
|
1375
|
+
.into_iter()
|
|
1376
|
+
.filter_map(|endpoint| {
|
|
1377
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
|
|
1378
|
+
.list_targets()
|
|
1379
|
+
.ok()
|
|
1380
|
+
})
|
|
1381
|
+
.flatten()
|
|
1382
|
+
.find(|info| info.pane_id == *pane)
|
|
1383
|
+
.and_then(|info| crate::leader::attribute_pane_provider(&info))
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
fn caller_provider_for_seed_with_lookup(
|
|
1387
|
+
caller: &crate::state::owner_gate::CallerIdentity,
|
|
1388
|
+
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
1389
|
+
) -> Option<String> {
|
|
1390
|
+
if !caller.provider.is_empty() {
|
|
1391
|
+
if let Some(provider) = parse_provider(&caller.provider).and_then(provider_wire_string) {
|
|
1392
|
+
return Some(provider);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
(!caller.pane_id.is_empty())
|
|
1396
|
+
.then(|| PaneId::new(&caller.pane_id))
|
|
1397
|
+
.and_then(|pane| lookup_pane_provider(&pane))
|
|
1398
|
+
.and_then(provider_wire_string)
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1087
1401
|
#[cfg(test)]
|
|
1088
1402
|
mod e22_unbound_owner_provider_tests {
|
|
1089
1403
|
use super::*;
|
|
1404
|
+
use crate::state::owner_gate::CallerIdentity;
|
|
1090
1405
|
|
|
1091
1406
|
#[test]
|
|
1092
1407
|
fn unbound_owner_preserves_explicit_copilot_provider() {
|
|
@@ -1137,6 +1452,83 @@ mod e22_unbound_owner_provider_tests {
|
|
|
1137
1452
|
"unattributed unbound owner must not silently become codex: {launched}"
|
|
1138
1453
|
);
|
|
1139
1454
|
}
|
|
1455
|
+
|
|
1456
|
+
fn caller(provider: &str, pane_id: &str) -> CallerIdentity {
|
|
1457
|
+
CallerIdentity {
|
|
1458
|
+
pane_id: pane_id.to_string(),
|
|
1459
|
+
provider: provider.to_string(),
|
|
1460
|
+
machine_fingerprint: "machine".to_string(),
|
|
1461
|
+
leader_session_uuid: "leader-uuid".to_string(),
|
|
1462
|
+
leader_session_uuid_source: "derived".to_string(),
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
#[test]
|
|
1467
|
+
fn env_seed_attributes_in_tmux_node_form_copilot_from_caller_pane() {
|
|
1468
|
+
let mut state = serde_json::json!({
|
|
1469
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1470
|
+
"leader": {"provider": "copilot"},
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
assert!(seed_launched_owner_from_caller_with_provider_lookup(
|
|
1474
|
+
&mut state,
|
|
1475
|
+
caller("", "%0"),
|
|
1476
|
+
|pane| (pane.as_str() == "%0").then_some(Provider::Copilot),
|
|
1477
|
+
));
|
|
1478
|
+
|
|
1479
|
+
assert_eq!(
|
|
1480
|
+
state
|
|
1481
|
+
.pointer("/team_owner/provider")
|
|
1482
|
+
.and_then(serde_json::Value::as_str),
|
|
1483
|
+
Some("copilot")
|
|
1484
|
+
);
|
|
1485
|
+
assert_eq!(
|
|
1486
|
+
state
|
|
1487
|
+
.pointer("/leader_receiver/provider")
|
|
1488
|
+
.and_then(serde_json::Value::as_str),
|
|
1489
|
+
Some("copilot")
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
#[test]
|
|
1494
|
+
fn env_seed_unknown_caller_pane_does_not_default_codex() {
|
|
1495
|
+
let mut state = serde_json::json!({
|
|
1496
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1497
|
+
"leader": {"provider": "copilot"},
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
assert!(seed_launched_owner_from_caller_with_provider_lookup(
|
|
1501
|
+
&mut state,
|
|
1502
|
+
caller("", "%0"),
|
|
1503
|
+
|_| None,
|
|
1504
|
+
));
|
|
1505
|
+
assert_eq!(
|
|
1506
|
+
state
|
|
1507
|
+
.pointer("/team_owner/pane_id")
|
|
1508
|
+
.and_then(serde_json::Value::as_str),
|
|
1509
|
+
Some("%0")
|
|
1510
|
+
);
|
|
1511
|
+
assert_eq!(
|
|
1512
|
+
state
|
|
1513
|
+
.pointer("/leader_receiver/pane_id")
|
|
1514
|
+
.and_then(serde_json::Value::as_str),
|
|
1515
|
+
Some("%0")
|
|
1516
|
+
);
|
|
1517
|
+
assert!(
|
|
1518
|
+
state
|
|
1519
|
+
.pointer("/leader_receiver/provider")
|
|
1520
|
+
.and_then(serde_json::Value::as_str)
|
|
1521
|
+
!= Some("codex"),
|
|
1522
|
+
"unknown caller pane must not silently seed a codex receiver: {state}"
|
|
1523
|
+
);
|
|
1524
|
+
assert!(
|
|
1525
|
+
state
|
|
1526
|
+
.pointer("/team_owner/provider")
|
|
1527
|
+
.and_then(serde_json::Value::as_str)
|
|
1528
|
+
!= Some("codex"),
|
|
1529
|
+
"unknown caller pane must not silently become codex: {state}"
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1140
1532
|
}
|
|
1141
1533
|
|
|
1142
1534
|
fn owner_pane_belongs_to_other_team(
|
|
@@ -1183,7 +1575,11 @@ fn running_agent_state(
|
|
|
1183
1575
|
.get("profile")
|
|
1184
1576
|
.map(yaml_value_to_json)
|
|
1185
1577
|
.unwrap_or(serde_json::Value::Null);
|
|
1186
|
-
let window =
|
|
1578
|
+
let window = started_agent
|
|
1579
|
+
.and_then(|started| started.layout_window.as_ref())
|
|
1580
|
+
.map(WindowName::as_str)
|
|
1581
|
+
.or_else(|| agent.get("window").and_then(Value::as_str))
|
|
1582
|
+
.unwrap_or(id);
|
|
1187
1583
|
let mcp_config = crate::provider::get_adapter(provider)
|
|
1188
1584
|
.mcp_config(auth_mode)
|
|
1189
1585
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
@@ -1229,6 +1625,25 @@ fn running_agent_state(
|
|
|
1229
1625
|
);
|
|
1230
1626
|
if let Some(started_agent) = started_agent {
|
|
1231
1627
|
persist_started_agent_plan_state(&mut state, started_agent);
|
|
1628
|
+
if let Some(layout_window) = started_agent.layout_window.as_ref() {
|
|
1629
|
+
state.insert(
|
|
1630
|
+
"layout_window".to_string(),
|
|
1631
|
+
serde_json::json!(layout_window.as_str()),
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if let Some(layout_index) = started_agent.layout_index {
|
|
1635
|
+
state.insert("layout_index".to_string(), serde_json::json!(layout_index));
|
|
1636
|
+
}
|
|
1637
|
+
if let Some(pane_index) = started_agent.pane_index {
|
|
1638
|
+
state.insert("pane_index".to_string(), serde_json::json!(pane_index));
|
|
1639
|
+
}
|
|
1640
|
+
if !matches!(started_agent.display, WorkerDisplay::Blocked { .. }) {
|
|
1641
|
+
state.insert(
|
|
1642
|
+
"display".to_string(),
|
|
1643
|
+
serde_json::to_value(&started_agent.display)
|
|
1644
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1232
1647
|
}
|
|
1233
1648
|
state.insert(
|
|
1234
1649
|
"spawn_cwd".to_string(),
|
|
@@ -1920,6 +2335,17 @@ fn transport_has_session(transport: &dyn Transport, session_name: &SessionName)
|
|
|
1920
2335
|
}
|
|
1921
2336
|
}
|
|
1922
2337
|
|
|
2338
|
+
fn spec_display_backend(spec: &Value) -> DisplayBackend {
|
|
2339
|
+
let requested = spec
|
|
2340
|
+
.get("runtime")
|
|
2341
|
+
.and_then(|runtime| runtime.get("display_backend"))
|
|
2342
|
+
.and_then(Value::as_str)
|
|
2343
|
+
.and_then(|backend| {
|
|
2344
|
+
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
2345
|
+
});
|
|
2346
|
+
crate::lifecycle::display::resolve_display_backend(requested, None).backend
|
|
2347
|
+
}
|
|
2348
|
+
|
|
1923
2349
|
fn parse_provider(raw: &str) -> Option<Provider> {
|
|
1924
2350
|
match raw {
|
|
1925
2351
|
"claude" => Some(Provider::Claude),
|
|
@@ -2146,6 +2572,7 @@ pub fn quick_start_in_workspace(
|
|
|
2146
2572
|
team_id: Option<&str>,
|
|
2147
2573
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2148
2574
|
let workspace = explicit_quick_start_workspace(workspace);
|
|
2575
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
2149
2576
|
quick_start_with_transport_in_workspace(
|
|
2150
2577
|
&workspace,
|
|
2151
2578
|
agents_dir,
|
|
@@ -2153,11 +2580,55 @@ pub fn quick_start_in_workspace(
|
|
|
2153
2580
|
yes,
|
|
2154
2581
|
fresh,
|
|
2155
2582
|
team_id,
|
|
2156
|
-
|
|
2157
|
-
&crate::tmux_backend::TmuxBackend::for_workspace(&workspace),
|
|
2583
|
+
&transport,
|
|
2158
2584
|
)
|
|
2159
2585
|
}
|
|
2160
2586
|
|
|
2587
|
+
pub fn quick_start_in_workspace_with_display(
|
|
2588
|
+
workspace: &Path,
|
|
2589
|
+
agents_dir: &Path,
|
|
2590
|
+
name: Option<&str>,
|
|
2591
|
+
yes: bool,
|
|
2592
|
+
fresh: bool,
|
|
2593
|
+
team_id: Option<&str>,
|
|
2594
|
+
open_display: bool,
|
|
2595
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
2596
|
+
let workspace = explicit_quick_start_workspace(workspace);
|
|
2597
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
2598
|
+
quick_start_with_transport_in_workspace_with_display(
|
|
2599
|
+
&workspace,
|
|
2600
|
+
agents_dir,
|
|
2601
|
+
name,
|
|
2602
|
+
yes,
|
|
2603
|
+
fresh,
|
|
2604
|
+
team_id,
|
|
2605
|
+
&transport,
|
|
2606
|
+
open_display,
|
|
2607
|
+
)
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
pub(crate) fn quick_start_tmux_backend(workspace: &Path) -> crate::tmux_backend::TmuxBackend {
|
|
2611
|
+
if let Some(endpoint) = crate::tmux_backend::socket_name_from_tmux_env() {
|
|
2612
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
|
|
2613
|
+
} else {
|
|
2614
|
+
crate::tmux_backend::TmuxBackend::for_workspace(workspace)
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
pub(crate) fn selected_tmux_socket_source(
|
|
2619
|
+
transport: &dyn Transport,
|
|
2620
|
+
workspace: &Path,
|
|
2621
|
+
) -> Option<&'static str> {
|
|
2622
|
+
let endpoint = transport.tmux_endpoint()?;
|
|
2623
|
+
if crate::tmux_backend::socket_name_from_tmux_env().as_deref() == Some(endpoint.as_str()) {
|
|
2624
|
+
Some("leader_env")
|
|
2625
|
+
} else if endpoint == crate::tmux_backend::socket_name_for_workspace(workspace) {
|
|
2626
|
+
Some("workspace")
|
|
2627
|
+
} else {
|
|
2628
|
+
None
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2161
2632
|
fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
|
|
2162
2633
|
std::fs::canonicalize(workspace).unwrap_or_else(|_| {
|
|
2163
2634
|
if workspace.is_absolute() {
|
|
@@ -2194,6 +2665,21 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2194
2665
|
fresh: bool,
|
|
2195
2666
|
team_id: Option<&str>,
|
|
2196
2667
|
transport: &dyn Transport,
|
|
2668
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
2669
|
+
quick_start_with_transport_in_workspace_with_display(
|
|
2670
|
+
workspace, agents_dir, name, yes, fresh, team_id, transport, true,
|
|
2671
|
+
)
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
pub fn quick_start_with_transport_in_workspace_with_display(
|
|
2675
|
+
workspace: &Path,
|
|
2676
|
+
agents_dir: &Path,
|
|
2677
|
+
name: Option<&str>,
|
|
2678
|
+
yes: bool,
|
|
2679
|
+
fresh: bool,
|
|
2680
|
+
team_id: Option<&str>,
|
|
2681
|
+
transport: &dyn Transport,
|
|
2682
|
+
open_display: bool,
|
|
2197
2683
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2198
2684
|
// B-7 / 036b N38 三行 fail-fast — TEAM_AGENT_LEADER_PANE_ID 主动路径在 quick-start
|
|
2199
2685
|
// 入口验活;死/缺(Dead)的 pane 必须明确报错,不可 silent bind 到 spawner /
|
|
@@ -2212,6 +2698,9 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2212
2698
|
let workspace = workspace.to_path_buf();
|
|
2213
2699
|
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
2214
2700
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
2701
|
+
if !open_display {
|
|
2702
|
+
override_spec_display_backend(&mut spec, "none");
|
|
2703
|
+
}
|
|
2215
2704
|
let requested_team = quick_start_requested_team_key(team_id, name)
|
|
2216
2705
|
.map(str::to_string)
|
|
2217
2706
|
.or_else(|| spec_team_id(&spec));
|
|
@@ -2247,7 +2736,13 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2247
2736
|
.as_ref()
|
|
2248
2737
|
.map(|session| {
|
|
2249
2738
|
let windows = quick_start_attach_window_names(&state);
|
|
2250
|
-
|
|
2739
|
+
attach_commands_for_runtime_windows(
|
|
2740
|
+
state
|
|
2741
|
+
.get("tmux_endpoint")
|
|
2742
|
+
.and_then(serde_json::Value::as_str)
|
|
2743
|
+
.or_else(|| {
|
|
2744
|
+
state.get("tmux_socket").and_then(serde_json::Value::as_str)
|
|
2745
|
+
}),
|
|
2251
2746
|
&workspace,
|
|
2252
2747
|
session,
|
|
2253
2748
|
windows.iter().map(String::as_str),
|
|
@@ -2298,7 +2793,8 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2298
2793
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
2299
2794
|
let resolved_spec_path =
|
|
2300
2795
|
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
2301
|
-
let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
2796
|
+
let mut state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
2797
|
+
annotate_runtime_tmux_endpoint(&mut state, transport, &workspace);
|
|
2302
2798
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
2303
2799
|
annotate_persisted_team_depth(
|
|
2304
2800
|
&workspace,
|
|
@@ -2338,13 +2834,12 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2338
2834
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
2339
2835
|
// bare Ready.
|
|
2340
2836
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
2341
|
-
let
|
|
2837
|
+
let attach_windows = started_attach_window_names(&launch.started);
|
|
2838
|
+
let attach_commands = attach_commands_for_runtime_windows(
|
|
2839
|
+
launch.tmux_endpoint.as_deref(),
|
|
2342
2840
|
&workspace,
|
|
2343
2841
|
&session_name,
|
|
2344
|
-
|
|
2345
|
-
.started
|
|
2346
|
-
.iter()
|
|
2347
|
-
.map(|started| started.agent_id.as_str()),
|
|
2842
|
+
attach_windows.iter().map(String::as_str),
|
|
2348
2843
|
);
|
|
2349
2844
|
let mut next_actions = vec![format!(
|
|
2350
2845
|
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
@@ -2368,6 +2863,77 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2368
2863
|
})
|
|
2369
2864
|
}
|
|
2370
2865
|
|
|
2866
|
+
fn annotate_runtime_tmux_endpoint(state: &mut serde_json::Value, transport: &dyn Transport, workspace: &Path) {
|
|
2867
|
+
let Some(endpoint) = transport.tmux_endpoint() else {
|
|
2868
|
+
return;
|
|
2869
|
+
};
|
|
2870
|
+
if let Some(obj) = state.as_object_mut() {
|
|
2871
|
+
obj.insert("tmux_endpoint".to_string(), serde_json::json!(endpoint));
|
|
2872
|
+
obj.insert(
|
|
2873
|
+
"tmux_socket".to_string(),
|
|
2874
|
+
obj.get("tmux_endpoint")
|
|
2875
|
+
.cloned()
|
|
2876
|
+
.unwrap_or(serde_json::Value::Null),
|
|
2877
|
+
);
|
|
2878
|
+
if let Some(source) = selected_tmux_socket_source(transport, workspace) {
|
|
2879
|
+
obj.insert("tmux_socket_source".to_string(), serde_json::json!(source));
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
fn attach_commands_for_runtime_windows<'a>(
|
|
2885
|
+
endpoint: Option<&str>,
|
|
2886
|
+
workspace: &Path,
|
|
2887
|
+
session_name: &SessionName,
|
|
2888
|
+
window_names: impl IntoIterator<Item = &'a str>,
|
|
2889
|
+
) -> Vec<String> {
|
|
2890
|
+
let windows = window_names.into_iter().collect::<Vec<_>>();
|
|
2891
|
+
let attach = if let Some(endpoint) = endpoint.filter(|endpoint| !endpoint.is_empty()) {
|
|
2892
|
+
if Path::new(endpoint).is_absolute() {
|
|
2893
|
+
windows
|
|
2894
|
+
.iter()
|
|
2895
|
+
.map(|window_name| {
|
|
2896
|
+
format!(
|
|
2897
|
+
"tmux -S {} attach -t {}:{}",
|
|
2898
|
+
endpoint,
|
|
2899
|
+
session_name.as_str(),
|
|
2900
|
+
window_name
|
|
2901
|
+
)
|
|
2902
|
+
})
|
|
2903
|
+
.collect::<Vec<_>>()
|
|
2904
|
+
} else {
|
|
2905
|
+
crate::tmux_backend::attach_commands_for_windows(
|
|
2906
|
+
workspace,
|
|
2907
|
+
session_name,
|
|
2908
|
+
windows.iter().copied(),
|
|
2909
|
+
)
|
|
2910
|
+
}
|
|
2911
|
+
} else {
|
|
2912
|
+
crate::tmux_backend::attach_commands_for_windows(
|
|
2913
|
+
workspace,
|
|
2914
|
+
session_name,
|
|
2915
|
+
windows.iter().copied(),
|
|
2916
|
+
)
|
|
2917
|
+
};
|
|
2918
|
+
attach
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
fn started_attach_window_names(started: &[StartedAgent]) -> Vec<String> {
|
|
2922
|
+
let mut windows = started
|
|
2923
|
+
.iter()
|
|
2924
|
+
.map(|started| {
|
|
2925
|
+
started
|
|
2926
|
+
.layout_window
|
|
2927
|
+
.as_ref()
|
|
2928
|
+
.map(|window| window.as_str().to_string())
|
|
2929
|
+
.unwrap_or_else(|| started.agent_id.as_str().to_string())
|
|
2930
|
+
})
|
|
2931
|
+
.collect::<Vec<_>>();
|
|
2932
|
+
windows.sort();
|
|
2933
|
+
windows.dedup();
|
|
2934
|
+
windows
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2371
2937
|
fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
|
|
2372
2938
|
let mut windows = state
|
|
2373
2939
|
.get("agents")
|
|
@@ -2774,7 +3340,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
2774
3340
|
role_file_path.display()
|
|
2775
3341
|
)));
|
|
2776
3342
|
}
|
|
2777
|
-
if
|
|
3343
|
+
if runtime_agent_exists(&owner_state, agent_id) {
|
|
2778
3344
|
return Err(LifecycleError::RequirementUnmet(format!(
|
|
2779
3345
|
"agent id already exists: {agent_id}"
|
|
2780
3346
|
)));
|
|
@@ -2931,29 +3497,74 @@ fn inject_agent_into_spec(
|
|
|
2931
3497
|
};
|
|
2932
3498
|
// agents 列表追加。
|
|
2933
3499
|
match pairs.iter_mut().find(|(k, _)| k == "agents") {
|
|
2934
|
-
Some((_, Value::List(agents))) =>
|
|
3500
|
+
Some((_, Value::List(agents))) => {
|
|
3501
|
+
if !agents
|
|
3502
|
+
.iter()
|
|
3503
|
+
.any(|existing| yaml_agent_id(existing) == Some(agent_id))
|
|
3504
|
+
{
|
|
3505
|
+
agents.push(agent);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
2935
3508
|
_ => return Err(LifecycleError::Compile("spec.agents missing or not a list".to_string())),
|
|
2936
3509
|
}
|
|
2937
3510
|
// routing.rules 追加 route-<id>(与 compile_team 同形)。
|
|
2938
3511
|
if let Some((_, Value::Map(routing))) = pairs.iter_mut().find(|(k, _)| k == "routing") {
|
|
2939
3512
|
if let Some((_, Value::List(rules))) = routing.iter_mut().find(|(k, _)| k == "rules") {
|
|
2940
|
-
rules
|
|
2941
|
-
|
|
2942
|
-
(
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3513
|
+
if !rules
|
|
3514
|
+
.iter()
|
|
3515
|
+
.any(|rule| yaml_route_assigns_to(rule) == Some(agent_id))
|
|
3516
|
+
{
|
|
3517
|
+
rules.push(Value::Map(vec![
|
|
3518
|
+
("id".to_string(), Value::Str(format!("route-{agent_id}"))),
|
|
3519
|
+
(
|
|
3520
|
+
"match".to_string(),
|
|
3521
|
+
Value::Map(vec![(
|
|
3522
|
+
"assignee".to_string(),
|
|
3523
|
+
Value::List(vec![Value::Str(agent_id.to_string())]),
|
|
3524
|
+
)]),
|
|
3525
|
+
),
|
|
3526
|
+
("assign_to".to_string(), Value::Str(agent_id.to_string())),
|
|
3527
|
+
("priority".to_string(), Value::Int(10)),
|
|
3528
|
+
]));
|
|
3529
|
+
}
|
|
2952
3530
|
}
|
|
2953
3531
|
}
|
|
2954
3532
|
Ok(())
|
|
2955
3533
|
}
|
|
2956
3534
|
|
|
3535
|
+
fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId) -> bool {
|
|
3536
|
+
state
|
|
3537
|
+
.get("agents")
|
|
3538
|
+
.and_then(serde_json::Value::as_object)
|
|
3539
|
+
.is_some_and(|agents| agents.contains_key(agent_id.as_str()))
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
fn yaml_agent_id(agent: &Value) -> Option<&str> {
|
|
3543
|
+
let Value::Map(pairs) = agent else {
|
|
3544
|
+
return None;
|
|
3545
|
+
};
|
|
3546
|
+
pairs
|
|
3547
|
+
.iter()
|
|
3548
|
+
.find(|(key, _)| key == "id")
|
|
3549
|
+
.and_then(|(_, value)| match value {
|
|
3550
|
+
Value::Str(id) => Some(id.as_str()),
|
|
3551
|
+
_ => None,
|
|
3552
|
+
})
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
fn yaml_route_assigns_to(rule: &Value) -> Option<&str> {
|
|
3556
|
+
let Value::Map(pairs) = rule else {
|
|
3557
|
+
return None;
|
|
3558
|
+
};
|
|
3559
|
+
pairs
|
|
3560
|
+
.iter()
|
|
3561
|
+
.find(|(key, _)| key == "assign_to")
|
|
3562
|
+
.and_then(|(_, value)| match value {
|
|
3563
|
+
Value::Str(id) => Some(id.as_str()),
|
|
3564
|
+
_ => None,
|
|
3565
|
+
})
|
|
3566
|
+
}
|
|
3567
|
+
|
|
2957
3568
|
/// `fork_agent(workspace, source_agent_id, as_agent_id, ...)`(`lifecycle/operations.py:284`)。
|
|
2958
3569
|
/// native session fork(provider 须 supports_session_fork ∧ auth_mode!=compatible_api);
|
|
2959
3570
|
/// 失败回滚,每条失败臂 `adapter.cleanup_mcp`。
|
|
@@ -3655,15 +4266,7 @@ fn initial_runtime_state(
|
|
|
3655
4266
|
}
|
|
3656
4267
|
agents.insert(id.to_string(), value);
|
|
3657
4268
|
}
|
|
3658
|
-
let
|
|
3659
|
-
.get("runtime")
|
|
3660
|
-
.and_then(|runtime| runtime.get("display_backend"))
|
|
3661
|
-
.and_then(Value::as_str)
|
|
3662
|
-
.and_then(|backend| {
|
|
3663
|
-
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
3664
|
-
});
|
|
3665
|
-
let display_backend =
|
|
3666
|
-
crate::lifecycle::display::resolve_display_backend(requested_display, None).backend;
|
|
4269
|
+
let display_backend = spec_display_backend(spec);
|
|
3667
4270
|
let mut state = serde_json::Map::new();
|
|
3668
4271
|
state.insert(
|
|
3669
4272
|
"spec_path".to_string(),
|
|
@@ -3711,19 +4314,26 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
3711
4314
|
) else {
|
|
3712
4315
|
return false;
|
|
3713
4316
|
};
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
4317
|
+
seed_launched_owner_from_caller_with_provider_lookup(
|
|
4318
|
+
state,
|
|
4319
|
+
caller,
|
|
4320
|
+
attributed_provider_for_pane_across_tmux_sockets,
|
|
4321
|
+
)
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
fn seed_launched_owner_from_caller_with_provider_lookup(
|
|
4325
|
+
state: &mut serde_json::Value,
|
|
4326
|
+
caller: crate::state::owner_gate::CallerIdentity,
|
|
4327
|
+
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
4328
|
+
) -> bool {
|
|
4329
|
+
if caller.pane_id.is_empty() {
|
|
3721
4330
|
return false;
|
|
3722
4331
|
}
|
|
4332
|
+
let provider = caller_provider_for_seed_with_lookup(&caller, lookup_pane_provider);
|
|
4333
|
+
let pane_id = caller.pane_id;
|
|
3723
4334
|
let owner_epoch = 1u64;
|
|
3724
|
-
let owner = serde_json::json!({
|
|
4335
|
+
let mut owner = serde_json::json!({
|
|
3725
4336
|
"pane_id": pane_id,
|
|
3726
|
-
"provider": provider.clone(),
|
|
3727
4337
|
"machine_fingerprint": caller.machine_fingerprint,
|
|
3728
4338
|
"leader_session_uuid": caller.leader_session_uuid,
|
|
3729
4339
|
"owner_epoch": owner_epoch,
|
|
@@ -3733,17 +4343,23 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
3733
4343
|
.or_else(|_| std::env::var("USERNAME"))
|
|
3734
4344
|
.unwrap_or_default(),
|
|
3735
4345
|
});
|
|
3736
|
-
let receiver = serde_json::json!({
|
|
4346
|
+
let mut receiver = serde_json::json!({
|
|
3737
4347
|
"mode": "direct_tmux",
|
|
3738
4348
|
"status": "attached",
|
|
3739
|
-
"provider": provider,
|
|
3740
4349
|
"pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
3741
4350
|
"pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
3742
4351
|
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
3743
4352
|
"owner_epoch": owner_epoch,
|
|
3744
4353
|
"discovery": "quick_start",
|
|
3745
4354
|
});
|
|
3746
|
-
let
|
|
4355
|
+
if let Some(provider) = provider.as_ref() {
|
|
4356
|
+
if let Some(owner) = owner.as_object_mut() {
|
|
4357
|
+
owner.insert("provider".to_string(), serde_json::json!(provider));
|
|
4358
|
+
}
|
|
4359
|
+
if let Some(receiver) = receiver.as_object_mut() {
|
|
4360
|
+
receiver.insert("provider".to_string(), serde_json::json!(provider));
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
3747
4363
|
if let (Some(receiver), Some(socket)) = (
|
|
3748
4364
|
receiver.as_object_mut(),
|
|
3749
4365
|
crate::tmux_backend::socket_name_from_tmux_env(),
|
|
@@ -3818,6 +4434,14 @@ pub(crate) fn write_spec_atomic(spec_path: &Path, spec: &Value) -> Result<(), Li
|
|
|
3818
4434
|
}
|
|
3819
4435
|
|
|
3820
4436
|
pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
4437
|
+
override_spec_runtime_str(spec, "session_name", session_name);
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
fn override_spec_display_backend(spec: &mut Value, display_backend: &str) {
|
|
4441
|
+
override_spec_runtime_str(spec, "display_backend", display_backend);
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
fn override_spec_runtime_str(spec: &mut Value, key: &str, value: &str) {
|
|
3821
4445
|
let Value::Map(root) = spec else { return };
|
|
3822
4446
|
let runtime_slot = root
|
|
3823
4447
|
.iter_mut()
|
|
@@ -3825,28 +4449,19 @@ pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
3825
4449
|
.map(|(_, v)| v);
|
|
3826
4450
|
match runtime_slot {
|
|
3827
4451
|
Some(Value::Map(runtime)) => {
|
|
3828
|
-
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k ==
|
|
3829
|
-
*existing = Value::Str(
|
|
4452
|
+
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == key) {
|
|
4453
|
+
*existing = Value::Str(value.to_string());
|
|
3830
4454
|
} else {
|
|
3831
|
-
runtime.push((
|
|
3832
|
-
"session_name".to_string(),
|
|
3833
|
-
Value::Str(session_name.to_string()),
|
|
3834
|
-
));
|
|
4455
|
+
runtime.push((key.to_string(), Value::Str(value.to_string())));
|
|
3835
4456
|
}
|
|
3836
4457
|
}
|
|
3837
4458
|
Some(other) => {
|
|
3838
|
-
*other = Value::Map(vec![(
|
|
3839
|
-
"session_name".to_string(),
|
|
3840
|
-
Value::Str(session_name.to_string()),
|
|
3841
|
-
)]);
|
|
4459
|
+
*other = Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]);
|
|
3842
4460
|
}
|
|
3843
4461
|
None => {
|
|
3844
4462
|
root.push((
|
|
3845
4463
|
"runtime".to_string(),
|
|
3846
|
-
Value::Map(vec![(
|
|
3847
|
-
"session_name".to_string(),
|
|
3848
|
-
Value::Str(session_name.to_string()),
|
|
3849
|
-
)]),
|
|
4464
|
+
Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]),
|
|
3850
4465
|
));
|
|
3851
4466
|
}
|
|
3852
4467
|
}
|
|
@@ -3995,20 +4610,5 @@ fn team_workspace(team_dir: &Path) -> PathBuf {
|
|
|
3995
4610
|
})
|
|
3996
4611
|
}
|
|
3997
4612
|
|
|
3998
|
-
fn agent_id_exists_in_team_dir(team_dir: &Path, agent_id: &AgentId) -> bool {
|
|
3999
|
-
let spec_path = team_dir.join("team.spec.yaml");
|
|
4000
|
-
if let Ok(text) = std::fs::read_to_string(&spec_path) {
|
|
4001
|
-
if let Ok(spec) = yaml::loads(&text) {
|
|
4002
|
-
return spec_agents(&spec)
|
|
4003
|
-
.into_iter()
|
|
4004
|
-
.any(|existing| existing.as_str() == agent_id.as_str());
|
|
4005
|
-
}
|
|
4006
|
-
}
|
|
4007
|
-
team_dir
|
|
4008
|
-
.join("agents")
|
|
4009
|
-
.join(format!("{}.md", agent_id.as_str()))
|
|
4010
|
-
.exists()
|
|
4011
|
-
}
|
|
4012
|
-
|
|
4013
4613
|
mod plan;
|
|
4014
4614
|
pub use plan::{handle_report_result, start_plan};
|