@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.
- 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 +60 -10
- 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/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +560 -69
- 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 +72 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +668 -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(
|
|
@@ -1279,7 +1575,11 @@ fn running_agent_state(
|
|
|
1279
1575
|
.get("profile")
|
|
1280
1576
|
.map(yaml_value_to_json)
|
|
1281
1577
|
.unwrap_or(serde_json::Value::Null);
|
|
1282
|
-
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);
|
|
1283
1583
|
let mcp_config = crate::provider::get_adapter(provider)
|
|
1284
1584
|
.mcp_config(auth_mode)
|
|
1285
1585
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
@@ -1325,6 +1625,25 @@ fn running_agent_state(
|
|
|
1325
1625
|
);
|
|
1326
1626
|
if let Some(started_agent) = started_agent {
|
|
1327
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
|
+
}
|
|
1328
1647
|
}
|
|
1329
1648
|
state.insert(
|
|
1330
1649
|
"spawn_cwd".to_string(),
|
|
@@ -2016,6 +2335,17 @@ fn transport_has_session(transport: &dyn Transport, session_name: &SessionName)
|
|
|
2016
2335
|
}
|
|
2017
2336
|
}
|
|
2018
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
|
+
|
|
2019
2349
|
fn parse_provider(raw: &str) -> Option<Provider> {
|
|
2020
2350
|
match raw {
|
|
2021
2351
|
"claude" => Some(Provider::Claude),
|
|
@@ -2242,6 +2572,7 @@ pub fn quick_start_in_workspace(
|
|
|
2242
2572
|
team_id: Option<&str>,
|
|
2243
2573
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2244
2574
|
let workspace = explicit_quick_start_workspace(workspace);
|
|
2575
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
2245
2576
|
quick_start_with_transport_in_workspace(
|
|
2246
2577
|
&workspace,
|
|
2247
2578
|
agents_dir,
|
|
@@ -2249,11 +2580,55 @@ pub fn quick_start_in_workspace(
|
|
|
2249
2580
|
yes,
|
|
2250
2581
|
fresh,
|
|
2251
2582
|
team_id,
|
|
2252
|
-
|
|
2253
|
-
|
|
2583
|
+
&transport,
|
|
2584
|
+
)
|
|
2585
|
+
}
|
|
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,
|
|
2254
2607
|
)
|
|
2255
2608
|
}
|
|
2256
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
|
+
|
|
2257
2632
|
fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
|
|
2258
2633
|
std::fs::canonicalize(workspace).unwrap_or_else(|_| {
|
|
2259
2634
|
if workspace.is_absolute() {
|
|
@@ -2290,6 +2665,21 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2290
2665
|
fresh: bool,
|
|
2291
2666
|
team_id: Option<&str>,
|
|
2292
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,
|
|
2293
2683
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2294
2684
|
// B-7 / 036b N38 三行 fail-fast — TEAM_AGENT_LEADER_PANE_ID 主动路径在 quick-start
|
|
2295
2685
|
// 入口验活;死/缺(Dead)的 pane 必须明确报错,不可 silent bind 到 spawner /
|
|
@@ -2308,6 +2698,9 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2308
2698
|
let workspace = workspace.to_path_buf();
|
|
2309
2699
|
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
2310
2700
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
2701
|
+
if !open_display {
|
|
2702
|
+
override_spec_display_backend(&mut spec, "none");
|
|
2703
|
+
}
|
|
2311
2704
|
let requested_team = quick_start_requested_team_key(team_id, name)
|
|
2312
2705
|
.map(str::to_string)
|
|
2313
2706
|
.or_else(|| spec_team_id(&spec));
|
|
@@ -2343,7 +2736,13 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2343
2736
|
.as_ref()
|
|
2344
2737
|
.map(|session| {
|
|
2345
2738
|
let windows = quick_start_attach_window_names(&state);
|
|
2346
|
-
|
|
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
|
+
}),
|
|
2347
2746
|
&workspace,
|
|
2348
2747
|
session,
|
|
2349
2748
|
windows.iter().map(String::as_str),
|
|
@@ -2394,7 +2793,8 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2394
2793
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
2395
2794
|
let resolved_spec_path =
|
|
2396
2795
|
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
2397
|
-
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);
|
|
2398
2798
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
2399
2799
|
annotate_persisted_team_depth(
|
|
2400
2800
|
&workspace,
|
|
@@ -2434,13 +2834,12 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2434
2834
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
2435
2835
|
// bare Ready.
|
|
2436
2836
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
2437
|
-
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(),
|
|
2438
2840
|
&workspace,
|
|
2439
2841
|
&session_name,
|
|
2440
|
-
|
|
2441
|
-
.started
|
|
2442
|
-
.iter()
|
|
2443
|
-
.map(|started| started.agent_id.as_str()),
|
|
2842
|
+
attach_windows.iter().map(String::as_str),
|
|
2444
2843
|
);
|
|
2445
2844
|
let mut next_actions = vec![format!(
|
|
2446
2845
|
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
@@ -2464,6 +2863,77 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2464
2863
|
})
|
|
2465
2864
|
}
|
|
2466
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
|
+
|
|
2467
2937
|
fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
|
|
2468
2938
|
let mut windows = state
|
|
2469
2939
|
.get("agents")
|
|
@@ -2870,7 +3340,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
2870
3340
|
role_file_path.display()
|
|
2871
3341
|
)));
|
|
2872
3342
|
}
|
|
2873
|
-
if
|
|
3343
|
+
if runtime_agent_exists(&owner_state, agent_id) {
|
|
2874
3344
|
return Err(LifecycleError::RequirementUnmet(format!(
|
|
2875
3345
|
"agent id already exists: {agent_id}"
|
|
2876
3346
|
)));
|
|
@@ -3027,29 +3497,74 @@ fn inject_agent_into_spec(
|
|
|
3027
3497
|
};
|
|
3028
3498
|
// agents 列表追加。
|
|
3029
3499
|
match pairs.iter_mut().find(|(k, _)| k == "agents") {
|
|
3030
|
-
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
|
+
}
|
|
3031
3508
|
_ => return Err(LifecycleError::Compile("spec.agents missing or not a list".to_string())),
|
|
3032
3509
|
}
|
|
3033
3510
|
// routing.rules 追加 route-<id>(与 compile_team 同形)。
|
|
3034
3511
|
if let Some((_, Value::Map(routing))) = pairs.iter_mut().find(|(k, _)| k == "routing") {
|
|
3035
3512
|
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
|
-
|
|
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
|
+
}
|
|
3048
3530
|
}
|
|
3049
3531
|
}
|
|
3050
3532
|
Ok(())
|
|
3051
3533
|
}
|
|
3052
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
|
+
|
|
3053
3568
|
/// `fork_agent(workspace, source_agent_id, as_agent_id, ...)`(`lifecycle/operations.py:284`)。
|
|
3054
3569
|
/// native session fork(provider 须 supports_session_fork ∧ auth_mode!=compatible_api);
|
|
3055
3570
|
/// 失败回滚,每条失败臂 `adapter.cleanup_mcp`。
|
|
@@ -3751,15 +4266,7 @@ fn initial_runtime_state(
|
|
|
3751
4266
|
}
|
|
3752
4267
|
agents.insert(id.to_string(), value);
|
|
3753
4268
|
}
|
|
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;
|
|
4269
|
+
let display_backend = spec_display_backend(spec);
|
|
3763
4270
|
let mut state = serde_json::Map::new();
|
|
3764
4271
|
state.insert(
|
|
3765
4272
|
"spec_path".to_string(),
|
|
@@ -3927,6 +4434,14 @@ pub(crate) fn write_spec_atomic(spec_path: &Path, spec: &Value) -> Result<(), Li
|
|
|
3927
4434
|
}
|
|
3928
4435
|
|
|
3929
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) {
|
|
3930
4445
|
let Value::Map(root) = spec else { return };
|
|
3931
4446
|
let runtime_slot = root
|
|
3932
4447
|
.iter_mut()
|
|
@@ -3934,28 +4449,19 @@ pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
3934
4449
|
.map(|(_, v)| v);
|
|
3935
4450
|
match runtime_slot {
|
|
3936
4451
|
Some(Value::Map(runtime)) => {
|
|
3937
|
-
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k ==
|
|
3938
|
-
*existing = Value::Str(
|
|
4452
|
+
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == key) {
|
|
4453
|
+
*existing = Value::Str(value.to_string());
|
|
3939
4454
|
} else {
|
|
3940
|
-
runtime.push((
|
|
3941
|
-
"session_name".to_string(),
|
|
3942
|
-
Value::Str(session_name.to_string()),
|
|
3943
|
-
));
|
|
4455
|
+
runtime.push((key.to_string(), Value::Str(value.to_string())));
|
|
3944
4456
|
}
|
|
3945
4457
|
}
|
|
3946
4458
|
Some(other) => {
|
|
3947
|
-
*other = Value::Map(vec![(
|
|
3948
|
-
"session_name".to_string(),
|
|
3949
|
-
Value::Str(session_name.to_string()),
|
|
3950
|
-
)]);
|
|
4459
|
+
*other = Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]);
|
|
3951
4460
|
}
|
|
3952
4461
|
None => {
|
|
3953
4462
|
root.push((
|
|
3954
4463
|
"runtime".to_string(),
|
|
3955
|
-
Value::Map(vec![(
|
|
3956
|
-
"session_name".to_string(),
|
|
3957
|
-
Value::Str(session_name.to_string()),
|
|
3958
|
-
)]),
|
|
4464
|
+
Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]),
|
|
3959
4465
|
));
|
|
3960
4466
|
}
|
|
3961
4467
|
}
|
|
@@ -4104,20 +4610,5 @@ fn team_workspace(team_dir: &Path) -> PathBuf {
|
|
|
4104
4610
|
})
|
|
4105
4611
|
}
|
|
4106
4612
|
|
|
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
4613
|
mod plan;
|
|
4123
4614
|
pub use plan::{handle_report_result, start_plan};
|