@team-agent/installer 0.5.102 → 0.5.103
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/emit.rs +2 -1
- package/crates/team-agent/src/cli/models.rs +92 -226
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +37 -161
- package/crates/team-agent/src/messaging/results.rs +11 -42
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +650 -0
- package/package.json +4 -4
|
@@ -15,14 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
use std::collections::BTreeSet;
|
|
17
17
|
use std::fs::OpenOptions;
|
|
18
|
-
use std::io::
|
|
18
|
+
use std::io::Write;
|
|
19
19
|
#[cfg(test)]
|
|
20
20
|
use std::cell::RefCell;
|
|
21
21
|
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
|
22
22
|
use std::path::{Path, PathBuf};
|
|
23
|
-
use std::process::
|
|
24
|
-
use std::
|
|
25
|
-
use std::time::{Duration, Instant};
|
|
23
|
+
use std::process::Command;
|
|
24
|
+
use std::time::Duration;
|
|
26
25
|
|
|
27
26
|
use sha2::{Digest, Sha256};
|
|
28
27
|
|
|
@@ -132,62 +131,9 @@ pub(crate) fn new_pi_session_id() -> SessionId {
|
|
|
132
131
|
/// errors: 非 UTF-8、header/row 异常、重复或空 catalog 时返回 ProviderError
|
|
133
132
|
/// ---
|
|
134
133
|
pub(crate) fn parse_pi_list_models_table(bytes: &[u8]) -> Result<Vec<String>, ProviderError> {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let mut lines = text.lines();
|
|
139
|
-
let header = lines
|
|
140
|
-
.next()
|
|
141
|
-
.ok_or_else(|| ProviderError::Command("Pi model catalog is empty".to_string()))?;
|
|
142
|
-
let header_columns = header.split_whitespace().collect::<Vec<_>>();
|
|
143
|
-
if header_columns.get(0) != Some(&"provider") || header_columns.get(1) != Some(&"model") {
|
|
144
|
-
return Err(ProviderError::Command(
|
|
145
|
-
"Pi model catalog has an unexpected header".to_string(),
|
|
146
|
-
));
|
|
147
|
-
}
|
|
148
|
-
let mut seen = BTreeSet::new();
|
|
149
|
-
let mut models = Vec::new();
|
|
150
|
-
for (index, line) in lines.enumerate() {
|
|
151
|
-
if line.trim().is_empty() {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
let malformed = || {
|
|
155
|
-
ProviderError::Command(format!(
|
|
156
|
-
"Pi model catalog row {} is malformed",
|
|
157
|
-
index + 2
|
|
158
|
-
))
|
|
159
|
-
};
|
|
160
|
-
let (provider, model) = line
|
|
161
|
-
.trim()
|
|
162
|
-
.split_once(char::is_whitespace)
|
|
163
|
-
.ok_or_else(malformed)?;
|
|
164
|
-
let mut model = model.trim();
|
|
165
|
-
// Pi prints the raw model id, which may contain spaces. Only the
|
|
166
|
-
// capability columns following it are single whitespace-free values.
|
|
167
|
-
for _ in 2..header_columns.len() {
|
|
168
|
-
model = model
|
|
169
|
-
.rsplit_once(char::is_whitespace)
|
|
170
|
-
.ok_or_else(malformed)?
|
|
171
|
-
.0
|
|
172
|
-
.trim_end();
|
|
173
|
-
}
|
|
174
|
-
if model.is_empty() {
|
|
175
|
-
return Err(malformed());
|
|
176
|
-
}
|
|
177
|
-
let exact = format!("{provider}/{model}");
|
|
178
|
-
if !seen.insert(exact.clone()) {
|
|
179
|
-
return Err(ProviderError::Command(format!(
|
|
180
|
-
"Pi model catalog contains duplicate exact id {exact:?}"
|
|
181
|
-
)));
|
|
182
|
-
}
|
|
183
|
-
models.push(exact);
|
|
184
|
-
}
|
|
185
|
-
if models.is_empty() {
|
|
186
|
-
return Err(ProviderError::Command(
|
|
187
|
-
"Pi model catalog contains no models".to_string(),
|
|
188
|
-
));
|
|
189
|
-
}
|
|
190
|
-
Ok(models)
|
|
134
|
+
crate::provider::model_catalog::parse_pi_catalog(bytes)
|
|
135
|
+
.map(|records| records.into_iter().map(|record| record.id).collect())
|
|
136
|
+
.map_err(|error| ProviderError::Command(error.pi_legacy_message()))
|
|
191
137
|
}
|
|
192
138
|
|
|
193
139
|
/// ---
|
|
@@ -658,43 +604,6 @@ fn observe_pi_catalog_test(f: impl FnOnce(&mut PiCatalogTestContext)) {
|
|
|
658
604
|
});
|
|
659
605
|
}
|
|
660
606
|
|
|
661
|
-
fn spawn_catalog_reader(
|
|
662
|
-
stdout: std::process::ChildStdout,
|
|
663
|
-
max_bytes: u64,
|
|
664
|
-
) -> mpsc::Receiver<(std::io::Result<usize>, Vec<u8>)> {
|
|
665
|
-
let (sender, receiver) = mpsc::channel();
|
|
666
|
-
std::thread::spawn(move || {
|
|
667
|
-
let mut bytes = Vec::new();
|
|
668
|
-
let result = stdout.take(max_bytes + 1).read_to_end(&mut bytes);
|
|
669
|
-
let _ = sender.send((result, bytes));
|
|
670
|
-
});
|
|
671
|
-
receiver
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
fn finish_catalog_reader(
|
|
675
|
-
receiver: mpsc::Receiver<(std::io::Result<usize>, Vec<u8>)>,
|
|
676
|
-
status: std::process::ExitStatus,
|
|
677
|
-
timeout: Duration,
|
|
678
|
-
max_bytes: u64,
|
|
679
|
-
) -> Result<Vec<u8>, String> {
|
|
680
|
-
let (result, bytes) = match receiver.recv_timeout(timeout) {
|
|
681
|
-
Ok(value) => value,
|
|
682
|
-
Err(_) => {
|
|
683
|
-
#[cfg(test)]
|
|
684
|
-
observe_pi_catalog_test(|context| context.observation.reader_timeout = true);
|
|
685
|
-
return Err("Pi model catalog command timed out".to_string());
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
if !status.success() {
|
|
689
|
-
return Err("Pi model catalog command failed".into());
|
|
690
|
-
}
|
|
691
|
-
result.map_err(|_| "Pi model catalog could not be read".to_string())?;
|
|
692
|
-
if bytes.len() as u64 > max_bytes {
|
|
693
|
-
return Err("Pi model catalog exceeds the bounded output limit".into());
|
|
694
|
-
}
|
|
695
|
-
Ok(bytes)
|
|
696
|
-
}
|
|
697
|
-
|
|
698
607
|
#[cfg(test)]
|
|
699
608
|
pub(crate) fn run_pi_catalog_after_parent_exit_for_test(
|
|
700
609
|
stdout: std::process::ChildStdout,
|
|
@@ -708,86 +617,53 @@ pub(crate) fn run_pi_catalog_after_parent_exit_for_test(
|
|
|
708
617
|
context.observation.parent_exit_success = Some(status.success());
|
|
709
618
|
context.observation.parent_exit_code = status.code();
|
|
710
619
|
});
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
status,
|
|
714
|
-
timeout,
|
|
715
|
-
max_bytes,
|
|
620
|
+
crate::provider::model_catalog::finish_stdout_after_exit(
|
|
621
|
+
stdout, status, timeout, max_bytes, &mut PiCatalogObserver,
|
|
716
622
|
)
|
|
623
|
+
.map_err(|error| error.pi_legacy_message())
|
|
717
624
|
}
|
|
718
625
|
|
|
719
|
-
///
|
|
720
|
-
/// does not resolve adapters or scan wrapper chains; callers use it only for
|
|
721
|
-
/// rejecting an unqualified role model before lifecycle mutation.
|
|
626
|
+
/// Shared bounded native-command runner; launch preflight keeps its exact model gate.
|
|
722
627
|
pub(crate) fn run_pi_catalog(
|
|
723
628
|
executable: &Path,
|
|
724
629
|
timeout: Duration,
|
|
725
630
|
max_bytes: u64,
|
|
726
631
|
) -> Result<Vec<u8>, String> {
|
|
727
|
-
let mut
|
|
728
|
-
command.arg("--list-models");
|
|
632
|
+
let mut extra_env = Vec::new();
|
|
729
633
|
#[cfg(test)]
|
|
730
634
|
observe_pi_catalog_test(|context| {
|
|
731
635
|
context.observation.spawn_count += 1;
|
|
732
636
|
context.observation.argv.push("--list-models".to_string());
|
|
733
|
-
|
|
637
|
+
extra_env.push(("TEAM_AGENT_MODELS_TIMEOUT_RECEIPT".into(), context.receipt.as_os_str().into()));
|
|
734
638
|
if let Some(mode) = context.helper_mode.as_deref() {
|
|
735
|
-
|
|
639
|
+
extra_env.push(("TEAM_AGENT_MODELS_TIMEOUT_MODE".into(), mode.into()));
|
|
736
640
|
}
|
|
737
641
|
});
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
"run_pi_catalog spawn failed kind={:?} raw_os_error={:?}",
|
|
748
|
-
error.kind(),
|
|
749
|
-
error.raw_os_error()
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
let _ = error;
|
|
753
|
-
"Pi executable is unavailable on PATH".to_string()
|
|
754
|
-
})?;
|
|
642
|
+
crate::provider::model_catalog::run_command(
|
|
643
|
+
executable, &["--list-models"], None, None, &[], &extra_env, timeout, max_bytes,
|
|
644
|
+
&mut PiCatalogObserver,
|
|
645
|
+
)
|
|
646
|
+
.map_err(|error| error.pi_legacy_message())
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
struct PiCatalogObserver;
|
|
650
|
+
impl crate::provider::model_catalog::CatalogObserver for PiCatalogObserver {
|
|
755
651
|
#[cfg(test)]
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
});
|
|
772
|
-
return finish_catalog_reader(
|
|
773
|
-
receiver,
|
|
774
|
-
status,
|
|
775
|
-
timeout.saturating_sub(start.elapsed()),
|
|
776
|
-
max_bytes,
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
Ok(None) if start.elapsed() < timeout => std::thread::sleep(Duration::from_millis(20)),
|
|
780
|
-
Ok(None) => {
|
|
781
|
-
let _ = child.kill();
|
|
782
|
-
let _ = child.wait();
|
|
783
|
-
return Err("Pi model catalog command timed out".into());
|
|
784
|
-
}
|
|
785
|
-
Err(_) => {
|
|
786
|
-
let _ = child.kill();
|
|
787
|
-
let _ = child.wait();
|
|
788
|
-
return Err("Pi model catalog command could not be observed".into());
|
|
789
|
-
}
|
|
790
|
-
}
|
|
652
|
+
fn spawned(&mut self, pid: u32) {
|
|
653
|
+
observe_pi_catalog_test(|context| context.observation.parent_pid = Some(pid));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#[cfg(test)]
|
|
657
|
+
fn exited(&mut self, status: &std::process::ExitStatus) {
|
|
658
|
+
observe_pi_catalog_test(|context| {
|
|
659
|
+
context.observation.parent_exit_success = Some(status.success());
|
|
660
|
+
context.observation.parent_exit_code = status.code();
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
#[cfg(test)]
|
|
665
|
+
fn output_timeout(&mut self) {
|
|
666
|
+
observe_pi_catalog_test(|context| context.observation.reader_timeout = true);
|
|
791
667
|
}
|
|
792
668
|
}
|
|
793
669
|
|
|
@@ -1046,25 +1046,6 @@ fn leader_result_presentation(task_id: &str) -> super::presentation::Presentatio
|
|
|
1046
1046
|
)
|
|
1047
1047
|
}
|
|
1048
1048
|
|
|
1049
|
-
const LEADER_NOTIFICATION_LIMIT_BYTES: usize = 160;
|
|
1050
|
-
|
|
1051
|
-
fn truncate_utf8(text: &str, max_bytes: usize) -> String {
|
|
1052
|
-
if text.len() <= max_bytes {
|
|
1053
|
-
return text.to_string();
|
|
1054
|
-
}
|
|
1055
|
-
let suffix = "...";
|
|
1056
|
-
let budget = max_bytes.saturating_sub(suffix.len());
|
|
1057
|
-
let mut end = 0;
|
|
1058
|
-
for (index, ch) in text.char_indices() {
|
|
1059
|
-
let next = index + ch.len_utf8();
|
|
1060
|
-
if next > budget {
|
|
1061
|
-
break;
|
|
1062
|
-
}
|
|
1063
|
-
end = next;
|
|
1064
|
-
}
|
|
1065
|
-
format!("{}{}", &text[..end], suffix)
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
1049
|
fn format_report_result_leader_summary(
|
|
1069
1050
|
result_id: &str,
|
|
1070
1051
|
task_id: &str,
|
|
@@ -1075,19 +1056,8 @@ fn format_report_result_leader_summary(
|
|
|
1075
1056
|
let summary = envelope
|
|
1076
1057
|
.get("summary")
|
|
1077
1058
|
.and_then(serde_json::Value::as_str)
|
|
1078
|
-
.unwrap_or("")
|
|
1079
|
-
|
|
1080
|
-
.next()
|
|
1081
|
-
.unwrap_or("")
|
|
1082
|
-
.chars()
|
|
1083
|
-
.filter(|ch| !ch.is_control())
|
|
1084
|
-
.collect::<String>();
|
|
1085
|
-
truncate_utf8(
|
|
1086
|
-
&format!(
|
|
1087
|
-
"Task {task_id} reported {status} from {agent_id}: {summary}; Result id: {result_id}"
|
|
1088
|
-
),
|
|
1089
|
-
LEADER_NOTIFICATION_LIMIT_BYTES,
|
|
1090
|
-
)
|
|
1059
|
+
.unwrap_or("");
|
|
1060
|
+
format!("Task {task_id} reported {status} from {agent_id}: {summary}; Result id: {result_id}")
|
|
1091
1061
|
}
|
|
1092
1062
|
|
|
1093
1063
|
fn auto_finalize_result(
|
|
@@ -1441,18 +1411,17 @@ mod tests {
|
|
|
1441
1411
|
}
|
|
1442
1412
|
|
|
1443
1413
|
#[test]
|
|
1444
|
-
fn
|
|
1445
|
-
let
|
|
1414
|
+
fn leader_result_summary_preserves_long_multiline_text_verbatim() {
|
|
1415
|
+
let source = format!("首行\nbranch: fix/keyboard-receive-and-compose\ncommit: {}", "a1b2c3d4e5f67890".repeat(24));
|
|
1416
|
+
let envelope = serde_json::json!({"summary": source});
|
|
1446
1417
|
let summary = super::format_report_result_leader_summary(
|
|
1447
|
-
"res-1",
|
|
1448
|
-
"task-1",
|
|
1449
|
-
"worker",
|
|
1450
|
-
"success",
|
|
1451
|
-
&envelope,
|
|
1418
|
+
"res-1", "task-1", "worker", "success", &envelope,
|
|
1452
1419
|
);
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
assert!(summary.
|
|
1420
|
+
let source = envelope["summary"].as_str().unwrap();
|
|
1421
|
+
assert_eq!(summary, format!("Task task-1 reported success from worker: {source}; Result id: res-1"));
|
|
1422
|
+
assert!(summary.len() > 160);
|
|
1423
|
+
assert!(summary.contains("\nbranch: fix/keyboard-receive-and-compose\ncommit: "));
|
|
1424
|
+
assert!(summary.contains("a1b2c3d4e5f67890".repeat(24).as_str()));
|
|
1456
1425
|
}
|
|
1457
1426
|
|
|
1458
1427
|
#[test]
|