@team-agent/installer 0.3.1 → 0.3.2
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 +38 -7
- package/crates/team-agent/src/cli/emit.rs +7 -6
- package/crates/team-agent/src/cli/mod.rs +623 -21
- package/crates/team-agent/src/cli/status_port.rs +170 -44
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/health.rs +9 -0
- package/crates/team-agent/src/lifecycle/launch.rs +271 -58
- package/crates/team-agent/src/lifecycle/restart/common.rs +65 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +57 -15
- package/crates/team-agent/src/lifecycle/restart/remove.rs +5 -1
- package/crates/team-agent/src/lifecycle/restart.rs +20 -0
- package/crates/team-agent/src/messaging/delivery.rs +397 -36
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/messaging/results.rs +200 -47
- package/crates/team-agent/src/provider/adapter.rs +95 -10
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/state/persist.rs +113 -1
- package/crates/team-agent/src/state/projection.rs +127 -3
- package/crates/team-agent/src/tmux_backend.rs +66 -6
- package/package.json +4 -4
|
@@ -14,7 +14,7 @@ use std::path::Path;
|
|
|
14
14
|
use serde_json::{json, Map, Value};
|
|
15
15
|
|
|
16
16
|
use super::StateError;
|
|
17
|
-
use crate::state::persist::{load_runtime_state,
|
|
17
|
+
use crate::state::persist::{load_runtime_state, save_runtime_state_with_deleted_agents};
|
|
18
18
|
|
|
19
19
|
/// `team_state_key`(`state.py:93`):从 team_dir(.name)/spec_path(.parent.name)派生 team key,
|
|
20
20
|
/// 跳过 `.team`/`runtime`;兜底 `session_name` 或 `"current"`。
|
|
@@ -43,6 +43,122 @@ pub fn team_state_key(state: &Value) -> String {
|
|
|
43
43
|
.map_or_else(|| "current".to_string(), str::to_string)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
47
|
+
pub enum OwnerTeamResolution {
|
|
48
|
+
Canonical(String),
|
|
49
|
+
LegacyAlias { requested: String, canonical: String },
|
|
50
|
+
Unresolved { requested: String },
|
|
51
|
+
Ambiguous { requested: String, matches: Vec<String> },
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl OwnerTeamResolution {
|
|
55
|
+
pub fn canonical_key(&self) -> Option<&str> {
|
|
56
|
+
match self {
|
|
57
|
+
OwnerTeamResolution::Canonical(key)
|
|
58
|
+
| OwnerTeamResolution::LegacyAlias { canonical: key, .. } => Some(key),
|
|
59
|
+
OwnerTeamResolution::Unresolved { .. } | OwnerTeamResolution::Ambiguous { .. } => None,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub fn resolve_owner_team_id(state: &Value, owner_team_id: &str) -> OwnerTeamResolution {
|
|
65
|
+
let requested = owner_team_id.trim();
|
|
66
|
+
if requested.is_empty() {
|
|
67
|
+
return OwnerTeamResolution::Unresolved { requested: owner_team_id.to_string() };
|
|
68
|
+
}
|
|
69
|
+
let teams = state.get("teams").and_then(Value::as_object);
|
|
70
|
+
if teams.is_some_and(|teams| teams.contains_key(requested)) {
|
|
71
|
+
if has_top_level_runtime_content(state) {
|
|
72
|
+
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if teams.is_none_or(Map::is_empty) {
|
|
76
|
+
let active = state.get("active_team_key").and_then(Value::as_str).unwrap_or("");
|
|
77
|
+
let derived = team_state_key(state);
|
|
78
|
+
if active == requested || derived == requested {
|
|
79
|
+
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
80
|
+
}
|
|
81
|
+
if !active.is_empty() {
|
|
82
|
+
return OwnerTeamResolution::LegacyAlias {
|
|
83
|
+
requested: requested.to_string(),
|
|
84
|
+
canonical: active.to_string(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if derived != "current" {
|
|
88
|
+
return OwnerTeamResolution::LegacyAlias {
|
|
89
|
+
requested: requested.to_string(),
|
|
90
|
+
canonical: derived,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
94
|
+
}
|
|
95
|
+
let Some(teams) = teams else {
|
|
96
|
+
return OwnerTeamResolution::Unresolved { requested: requested.to_string() };
|
|
97
|
+
};
|
|
98
|
+
let mut matches = Vec::new();
|
|
99
|
+
for (key, entry) in teams {
|
|
100
|
+
if legacy_owner_team_aliases(entry).any(|alias| alias == requested) {
|
|
101
|
+
matches.push(key.clone());
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
matches.sort();
|
|
105
|
+
matches.dedup();
|
|
106
|
+
match matches.len() {
|
|
107
|
+
0 => OwnerTeamResolution::Unresolved { requested: requested.to_string() },
|
|
108
|
+
1 => OwnerTeamResolution::LegacyAlias {
|
|
109
|
+
requested: requested.to_string(),
|
|
110
|
+
canonical: matches.remove(0),
|
|
111
|
+
},
|
|
112
|
+
_ => OwnerTeamResolution::Ambiguous { requested: requested.to_string(), matches },
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn has_top_level_runtime_content(state: &Value) -> bool {
|
|
117
|
+
[
|
|
118
|
+
"session_name",
|
|
119
|
+
"team_dir",
|
|
120
|
+
"spec_path",
|
|
121
|
+
"workspace",
|
|
122
|
+
"agents",
|
|
123
|
+
"tasks",
|
|
124
|
+
"leader_receiver",
|
|
125
|
+
"team_owner",
|
|
126
|
+
]
|
|
127
|
+
.into_iter()
|
|
128
|
+
.any(|key| state.get(key).is_some_and(super::json_truthy))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn legacy_owner_team_aliases(entry: &Value) -> impl Iterator<Item = String> + '_ {
|
|
132
|
+
let scalar_paths = [
|
|
133
|
+
"/team/name",
|
|
134
|
+
"/team/id",
|
|
135
|
+
"/name",
|
|
136
|
+
"/team_name",
|
|
137
|
+
"/team_id",
|
|
138
|
+
"/spec_name",
|
|
139
|
+
"/legacy_owner_team_id",
|
|
140
|
+
"/legacy_team_id",
|
|
141
|
+
"/legacy_team_name",
|
|
142
|
+
"/legacy_alias",
|
|
143
|
+
];
|
|
144
|
+
let list_paths = ["/legacy_aliases", "/legacy_team_aliases", "/legacy_owner_team_ids", "/aliases"];
|
|
145
|
+
let scalars = scalar_paths
|
|
146
|
+
.into_iter()
|
|
147
|
+
.filter_map(|path| entry.pointer(path).and_then(Value::as_str));
|
|
148
|
+
let lists = list_paths.into_iter().flat_map(|path| {
|
|
149
|
+
entry
|
|
150
|
+
.pointer(path)
|
|
151
|
+
.and_then(Value::as_array)
|
|
152
|
+
.into_iter()
|
|
153
|
+
.flatten()
|
|
154
|
+
.filter_map(Value::as_str)
|
|
155
|
+
});
|
|
156
|
+
scalars
|
|
157
|
+
.chain(lists)
|
|
158
|
+
.filter(|alias| !alias.is_empty())
|
|
159
|
+
.map(str::to_string)
|
|
160
|
+
}
|
|
161
|
+
|
|
46
162
|
/// `compact_team_state`(`state.py:105`):剔除 `teams`(team entry 不嵌套全量 teams),保序。
|
|
47
163
|
pub fn compact_team_state(state: &Value) -> Value {
|
|
48
164
|
match state.as_object() {
|
|
@@ -339,6 +455,14 @@ pub fn resolve_team_scoped_state(
|
|
|
339
455
|
/// 纯 `save_runtime_state`(字节等价);多 team 时把本 team 落到 `teams[target_key]=compact(...)`,顶层
|
|
340
456
|
/// 视图按 golden 的 `existing_primary_key` 逻辑择 incoming/existing。§10:无 unwrap/panic。
|
|
341
457
|
pub fn save_team_scoped_state(workspace: &Path, team_state: &Value) -> Result<(), StateError> {
|
|
458
|
+
save_team_scoped_state_with_deleted_agents(workspace, team_state, &[])
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
pub(crate) fn save_team_scoped_state_with_deleted_agents(
|
|
462
|
+
workspace: &Path,
|
|
463
|
+
team_state: &Value,
|
|
464
|
+
deleted_agent_ids: &[&str],
|
|
465
|
+
) -> Result<(), StateError> {
|
|
342
466
|
let target_key = team_state_key(team_state);
|
|
343
467
|
let existing = load_runtime_state(workspace)?;
|
|
344
468
|
// existing_primary_key = team_state_key(existing) if existing.get("session_name") else None
|
|
@@ -367,7 +491,7 @@ pub fn save_team_scoped_state(workspace: &Path, team_state: &Value) -> Result<()
|
|
|
367
491
|
// not existing_teams and existing_primary_key == target_key → 纯 save(剔 teams)。
|
|
368
492
|
if existing_teams.is_empty() && existing_primary_key.as_deref() == Some(target_key.as_str()) {
|
|
369
493
|
let merged = compact_team_state(team_state);
|
|
370
|
-
return
|
|
494
|
+
return save_runtime_state_with_deleted_agents(workspace, &merged, deleted_agent_ids);
|
|
371
495
|
}
|
|
372
496
|
// teams = deepcopy(incoming_teams or existing_teams)
|
|
373
497
|
let mut teams = match incoming_teams {
|
|
@@ -387,7 +511,7 @@ pub fn save_team_scoped_state(workspace: &Path, team_state: &Value) -> Result<()
|
|
|
387
511
|
if merged.get("teams").and_then(Value::as_object).is_some_and(Map::is_empty) {
|
|
388
512
|
merged.remove("teams");
|
|
389
513
|
}
|
|
390
|
-
|
|
514
|
+
save_runtime_state_with_deleted_agents(workspace, &Value::Object(merged), deleted_agent_ids)
|
|
391
515
|
}
|
|
392
516
|
|
|
393
517
|
// ---- helpers ----
|
|
@@ -494,6 +494,14 @@ fn submit_verification_for_key(key: Key) -> SubmitVerification {
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
fn capture_has_pasted_content_prompt(text: &str) -> bool {
|
|
498
|
+
let lower = text.to_ascii_lowercase();
|
|
499
|
+
lower.contains("pasted content") || lower.contains("pasted text")
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const PASTED_CONTENT_APPEAR_POLLS: u32 = 5;
|
|
503
|
+
const PASTED_CONTENT_SUBMIT_ATTEMPTS: u32 = 3;
|
|
504
|
+
|
|
497
505
|
fn shell_command(argv: &[String], cwd: &Path, env: &BTreeMap<String, String>) -> String {
|
|
498
506
|
let mut parts = Vec::new();
|
|
499
507
|
parts.push("cd".to_string());
|
|
@@ -579,7 +587,40 @@ impl Transport for TmuxBackend {
|
|
|
579
587
|
self.run_inject_stage(&argv, stage)?;
|
|
580
588
|
}
|
|
581
589
|
}
|
|
590
|
+
let mut saw_pasted_prompt = false;
|
|
591
|
+
for _ in 0..PASTED_CONTENT_APPEAR_POLLS {
|
|
592
|
+
let captured = self.capture(target, CaptureRange::Tail(80))?;
|
|
593
|
+
if capture_has_pasted_content_prompt(&captured.text) {
|
|
594
|
+
saw_pasted_prompt = true;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
598
|
+
}
|
|
582
599
|
let submit_argv = tmux_send_keys_argv(&pane, &[submit]);
|
|
600
|
+
if saw_pasted_prompt {
|
|
601
|
+
let mut attempts = 0;
|
|
602
|
+
let mut cleared = false;
|
|
603
|
+
for _ in 0..PASTED_CONTENT_SUBMIT_ATTEMPTS {
|
|
604
|
+
attempts += 1;
|
|
605
|
+
self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
|
|
606
|
+
let captured = self.capture(target, CaptureRange::Tail(80))?;
|
|
607
|
+
if !capture_has_pasted_content_prompt(&captured.text) {
|
|
608
|
+
cleared = true;
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return Ok(InjectReport {
|
|
613
|
+
stage_reached: InjectStage::Submit,
|
|
614
|
+
inject_verification: InjectVerification::CaptureContainsNewPastedContentPrompt,
|
|
615
|
+
submit_verification: if cleared {
|
|
616
|
+
SubmitVerification::PastedContentPromptAbsentAfterSubmit
|
|
617
|
+
} else {
|
|
618
|
+
submit_verification_for_key(submit)
|
|
619
|
+
},
|
|
620
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
621
|
+
attempts,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
583
624
|
self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
|
|
584
625
|
}
|
|
585
626
|
}
|
|
@@ -674,7 +715,10 @@ impl Transport for TmuxBackend {
|
|
|
674
715
|
}
|
|
675
716
|
let mut panes = Vec::new();
|
|
676
717
|
for line in output.stdout.lines().filter(|line| !line.is_empty()) {
|
|
677
|
-
if let Some(pane) = parse_pane_info_line(line) {
|
|
718
|
+
if let Some(mut pane) = parse_pane_info_line(line) {
|
|
719
|
+
if pane.pane_pid.is_none() {
|
|
720
|
+
pane.pane_pid = query_pane_pid(self, &pane.pane_id)?;
|
|
721
|
+
}
|
|
678
722
|
panes.push(pane);
|
|
679
723
|
}
|
|
680
724
|
}
|
|
@@ -737,22 +781,22 @@ impl Transport for TmuxBackend {
|
|
|
737
781
|
}
|
|
738
782
|
|
|
739
783
|
fn kill_session(&self, session: &SessionName) -> Result<(), TransportError> {
|
|
740
|
-
let argv =
|
|
784
|
+
let argv = self.tmux_argv(&[
|
|
741
785
|
"tmux".to_string(),
|
|
742
786
|
"kill-session".to_string(),
|
|
743
787
|
"-t".to_string(),
|
|
744
788
|
session.as_str().to_string(),
|
|
745
|
-
];
|
|
789
|
+
]);
|
|
746
790
|
self.run_ok(&argv)
|
|
747
791
|
}
|
|
748
792
|
|
|
749
793
|
fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
|
|
750
|
-
let argv =
|
|
794
|
+
let argv = self.tmux_argv(&[
|
|
751
795
|
"tmux".to_string(),
|
|
752
796
|
"kill-window".to_string(),
|
|
753
797
|
"-t".to_string(),
|
|
754
798
|
target_name(target),
|
|
755
|
-
];
|
|
799
|
+
]);
|
|
756
800
|
self.run_ok(&argv)
|
|
757
801
|
}
|
|
758
802
|
|
|
@@ -771,6 +815,22 @@ impl Transport for TmuxBackend {
|
|
|
771
815
|
}
|
|
772
816
|
}
|
|
773
817
|
|
|
818
|
+
fn query_pane_pid(backend: &TmuxBackend, pane: &PaneId) -> Result<Option<u32>, TransportError> {
|
|
819
|
+
let argv = backend.tmux_argv(&[
|
|
820
|
+
"tmux".to_string(),
|
|
821
|
+
"display-message".to_string(),
|
|
822
|
+
"-p".to_string(),
|
|
823
|
+
"-t".to_string(),
|
|
824
|
+
pane.as_str().to_string(),
|
|
825
|
+
"#{pane_pid}".to_string(),
|
|
826
|
+
]);
|
|
827
|
+
let output = backend.runner.run(&argv)?;
|
|
828
|
+
if !output.success {
|
|
829
|
+
return Ok(None);
|
|
830
|
+
}
|
|
831
|
+
Ok(parse_optional_u32(output.stdout.trim()))
|
|
832
|
+
}
|
|
833
|
+
|
|
774
834
|
fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
|
|
775
835
|
let fields = line.split('\t').collect::<Vec<_>>();
|
|
776
836
|
if fields.len() < 11 {
|
|
@@ -786,7 +846,7 @@ fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
|
|
|
786
846
|
current_command: non_empty(fields[6]).map(str::to_string),
|
|
787
847
|
active: fields[7] == "1",
|
|
788
848
|
current_path: non_empty(fields[8]).map(PathBuf::from),
|
|
789
|
-
pane_pid:
|
|
849
|
+
pane_pid: fields.get(11).and_then(|raw| parse_optional_u32(raw)),
|
|
790
850
|
leader_env: BTreeMap::new(),
|
|
791
851
|
})
|
|
792
852
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.3.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.3.2",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.2",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.2"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|