@team-agent/installer 0.5.101 → 0.5.102
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/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +49 -65
- package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
- package/crates/team-agent/src/leader/start.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
- package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
- package/crates/team-agent/src/mcp_server/mod.rs +7 -12
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
- package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
- package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
- package/crates/team-agent/src/mcp_server/tests.rs +4 -200
- package/crates/team-agent/src/mcp_server/tools.rs +5 -396
- package/crates/team-agent/src/mcp_server/types.rs +3 -36
- package/crates/team-agent/src/mcp_server/wire.rs +2 -223
- package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/scheduler.rs +2 -205
- package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
- package/crates/team-agent/src/messaging/types.rs +0 -20
- package/crates/team-agent/src/model/spec.rs +14 -18
- package/crates/team-agent/src/state/repository/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +1 -33
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +5 -5
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +24 -116
- package/skills/team-agent/references/team-in-team.md +4 -19
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
use crate::cli::spec::{CommandKind, COMMAND_SPECS};
|
|
2
2
|
use crate::compiler::compile_role_agent;
|
|
3
3
|
use crate::lifecycle::launch::pi_mcp::parse_pi_leader_args;
|
|
4
|
-
use crate::mcp_server::TeamOrchestratorTools;
|
|
5
4
|
use crate::model::enums::{AuthMode, Provider, ProviderEffort};
|
|
6
|
-
use crate::model::ids::
|
|
5
|
+
use crate::model::ids::AgentId;
|
|
7
6
|
use crate::model::yaml::Value;
|
|
8
7
|
use crate::provider::{get_adapter, ProviderCommandContext};
|
|
9
8
|
use crate::transport::test_support::OfflineTransport;
|
|
@@ -44,71 +43,6 @@ fn run_process_isolated(marker: &str, test_name: &str, body: impl FnOnce()) {
|
|
|
44
43
|
);
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
#[cfg(unix)]
|
|
48
|
-
const FILE_H_NATIVE_COMPILE_DEADLINE: std::time::Duration = std::time::Duration::from_secs(15);
|
|
49
|
-
#[cfg(unix)]
|
|
50
|
-
const FILE_H_NATIVE_READINESS_DEADLINE: std::time::Duration = std::time::Duration::from_secs(3);
|
|
51
|
-
|
|
52
|
-
#[cfg(unix)]
|
|
53
|
-
#[derive(Debug)]
|
|
54
|
-
struct FileHCommandTimeout {
|
|
55
|
-
stage: &'static str,
|
|
56
|
-
pid: u32,
|
|
57
|
-
deadline: std::time::Duration,
|
|
58
|
-
elapsed: std::time::Duration,
|
|
59
|
-
reaped_status: std::process::ExitStatus,
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
#[cfg(unix)]
|
|
63
|
-
fn run_file_h_command_with_deadline(
|
|
64
|
-
command: &mut std::process::Command,
|
|
65
|
-
stage: &'static str,
|
|
66
|
-
deadline: std::time::Duration,
|
|
67
|
-
stdout_path: &Path,
|
|
68
|
-
stderr_path: &Path,
|
|
69
|
-
) -> Result<std::process::Output, FileHCommandTimeout> {
|
|
70
|
-
let stdout_file = std::fs::File::create(stdout_path)
|
|
71
|
-
.unwrap_or_else(|error| panic!("{stage}: create stdout receipt failed: {error}"));
|
|
72
|
-
let stderr_file = std::fs::File::create(stderr_path)
|
|
73
|
-
.unwrap_or_else(|error| panic!("{stage}: create stderr receipt failed: {error}"));
|
|
74
|
-
let mut child = command
|
|
75
|
-
.stdout(std::process::Stdio::from(stdout_file))
|
|
76
|
-
.stderr(std::process::Stdio::from(stderr_file))
|
|
77
|
-
.spawn()
|
|
78
|
-
.unwrap_or_else(|error| panic!("{stage}: spawn failed: {error}"));
|
|
79
|
-
let pid = child.id();
|
|
80
|
-
let started = std::time::Instant::now();
|
|
81
|
-
loop {
|
|
82
|
-
if let Some(status) = child
|
|
83
|
-
.try_wait()
|
|
84
|
-
.unwrap_or_else(|error| panic!("{stage}: poll pid {pid} failed: {error}"))
|
|
85
|
-
{
|
|
86
|
-
return Ok(std::process::Output {
|
|
87
|
-
status,
|
|
88
|
-
stdout: std::fs::read(stdout_path).unwrap_or_default(),
|
|
89
|
-
stderr: std::fs::read(stderr_path).unwrap_or_default(),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
let elapsed = started.elapsed();
|
|
93
|
-
if elapsed >= deadline {
|
|
94
|
-
child
|
|
95
|
-
.kill()
|
|
96
|
-
.unwrap_or_else(|error| panic!("{stage}: kill exact pid {pid} failed: {error}"));
|
|
97
|
-
let reaped_status = child
|
|
98
|
-
.wait()
|
|
99
|
-
.unwrap_or_else(|error| panic!("{stage}: reap exact pid {pid} failed: {error}"));
|
|
100
|
-
return Err(FileHCommandTimeout {
|
|
101
|
-
stage,
|
|
102
|
-
pid,
|
|
103
|
-
deadline,
|
|
104
|
-
elapsed,
|
|
105
|
-
reaped_status,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
std::thread::park_timeout(std::time::Duration::from_millis(10));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
46
|
fn dynamic_add_fixture(root: &Path, label: &str, role_doc: &str) -> (PathBuf, PathBuf) {
|
|
113
47
|
let team = root.join(label);
|
|
114
48
|
std::fs::create_dir_all(team.join("agents")).expect("create dynamic team fixture");
|
|
@@ -287,10 +221,13 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
|
|
|
287
221
|
.expect("write provider-default teammate role");
|
|
288
222
|
let defaults = compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace")
|
|
289
223
|
.expect("provider: pi teammate may use Pi model and effort defaults");
|
|
290
|
-
assert_eq!(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
224
|
+
assert_eq!(
|
|
225
|
+
defaults.agent.get("model"),
|
|
226
|
+
Some(&Value::Str("openai-codex/gpt-6-luna".to_string()))
|
|
227
|
+
);
|
|
228
|
+
assert_eq!(
|
|
229
|
+
defaults.agent.get("effort"),
|
|
230
|
+
Some(&Value::Str("max".to_string()))
|
|
294
231
|
);
|
|
295
232
|
std::fs::remove_dir_all(root).expect("remove role fixture");
|
|
296
233
|
|
|
@@ -572,336 +509,6 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
|
|
|
572
509
|
crate::state::persist::load_runtime_state(&dead_team).expect("load dead rollback state");
|
|
573
510
|
assert!(dead_state.pointer("/agents/mate").is_none());
|
|
574
511
|
|
|
575
|
-
#[cfg(unix)]
|
|
576
|
-
{
|
|
577
|
-
let live = dynamic_root.join("live-public-add");
|
|
578
|
-
std::fs::create_dir_all(live.join("agents")).expect("create live agents dir");
|
|
579
|
-
std::fs::create_dir_all(live.join("roles")).expect("create live roles dir");
|
|
580
|
-
std::fs::write(
|
|
581
|
-
live.join("TEAM.md"),
|
|
582
|
-
"---\nname: current\nobjective: Public no-spec add fixture.\nprovider: fake\n---\n\nfixture\n",
|
|
583
|
-
)
|
|
584
|
-
.expect("write live TEAM.md");
|
|
585
|
-
std::fs::write(
|
|
586
|
-
live.join("agents/implementer.md"),
|
|
587
|
-
"---\nname: implementer\nrole: Existing Worker\nprovider: fake\nmodel: fake\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nexisting\n",
|
|
588
|
-
)
|
|
589
|
-
.expect("write live existing role");
|
|
590
|
-
std::fs::write(live.join("roles/mate.md"), fake_role).expect("write live add role");
|
|
591
|
-
let live_socket = "/private/tmp/tmux-501/ta-live-owning";
|
|
592
|
-
let live_session = "team-agent-leader-pi-live-owning";
|
|
593
|
-
crate::state::persist::save_runtime_state(
|
|
594
|
-
&live,
|
|
595
|
-
&json!({
|
|
596
|
-
"active_team_key": "current",
|
|
597
|
-
"tmux_endpoint": worker_socket,
|
|
598
|
-
"tmux_socket": worker_socket,
|
|
599
|
-
"agents": {
|
|
600
|
-
"implementer": {
|
|
601
|
-
"status": "running",
|
|
602
|
-
"provider": "fake",
|
|
603
|
-
"model": "fake",
|
|
604
|
-
"auth_mode": "subscription",
|
|
605
|
-
"window": "implementer"
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
"teams": {
|
|
609
|
-
"current": {
|
|
610
|
-
"status": "alive",
|
|
611
|
-
"team_dir": live,
|
|
612
|
-
"session_name": null,
|
|
613
|
-
"tmux_endpoint": worker_socket,
|
|
614
|
-
"tmux_socket": worker_socket,
|
|
615
|
-
"agents": {
|
|
616
|
-
"implementer": {
|
|
617
|
-
"status": "running",
|
|
618
|
-
"provider": "fake",
|
|
619
|
-
"model": "fake",
|
|
620
|
-
"auth_mode": "subscription",
|
|
621
|
-
"window": "implementer"
|
|
622
|
-
}
|
|
623
|
-
},
|
|
624
|
-
"leader_receiver": {
|
|
625
|
-
"status": "attached",
|
|
626
|
-
"session_name": live_session,
|
|
627
|
-
"tmux_socket": live_socket
|
|
628
|
-
}
|
|
629
|
-
},
|
|
630
|
-
"sibling": {
|
|
631
|
-
"status": "alive",
|
|
632
|
-
"team_dir": live.join("sibling"),
|
|
633
|
-
"session_name": null,
|
|
634
|
-
"agents": {},
|
|
635
|
-
"leader_receiver": {
|
|
636
|
-
"status": "attached",
|
|
637
|
-
"session_name": "team-agent-leader-pi-sibling",
|
|
638
|
-
"tmux_socket": "/private/tmp/tmux-501/ta-live-sibling"
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}),
|
|
643
|
-
)
|
|
644
|
-
.expect("seed live nested receiver");
|
|
645
|
-
super::launch_spawn::seed_healthy_coordinator(&live);
|
|
646
|
-
let runtime_spec = crate::model::paths::runtime_spec_path(&live, "current");
|
|
647
|
-
assert!(
|
|
648
|
-
!runtime_spec.exists(),
|
|
649
|
-
"fixture must enter the TEAM.md fallback before runtime spec exists"
|
|
650
|
-
);
|
|
651
|
-
|
|
652
|
-
let shim_root = live.join("tmux-shim");
|
|
653
|
-
std::fs::create_dir_all(&shim_root).expect("create tmux shim dir");
|
|
654
|
-
let shim_log = shim_root.join("argv.log");
|
|
655
|
-
let shim_source = shim_root.join("tmux.c");
|
|
656
|
-
let shim = shim_root.join("tmux");
|
|
657
|
-
std::fs::write(
|
|
658
|
-
&shim_source,
|
|
659
|
-
r#"#include <stdio.h>
|
|
660
|
-
#include <stdlib.h>
|
|
661
|
-
#include <string.h>
|
|
662
|
-
#include <unistd.h>
|
|
663
|
-
|
|
664
|
-
static int has_arg(int argc, char **argv, const char *needle) {
|
|
665
|
-
for (int i = 1; i < argc; i++) {
|
|
666
|
-
if (strcmp(argv[i], needle) == 0) return 1;
|
|
667
|
-
}
|
|
668
|
-
return 0;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
int main(int argc, char **argv) {
|
|
672
|
-
if (getenv("TEAM_AGENT_PI_TEST_HANG") != NULL) for (;;) pause();
|
|
673
|
-
const char *log_path = getenv("TEAM_AGENT_PI_TMUX_LOG");
|
|
674
|
-
const char *socket = getenv("TEAM_AGENT_PI_OWNING_SOCKET");
|
|
675
|
-
const char *session = getenv("TEAM_AGENT_PI_OWNING_SESSION");
|
|
676
|
-
const char *workspace = getenv("TEAM_AGENT_PI_RUN_WORKSPACE");
|
|
677
|
-
FILE *log = log_path == NULL ? NULL : fopen(log_path, "a");
|
|
678
|
-
if (log == NULL || socket == NULL || session == NULL || workspace == NULL) return 92;
|
|
679
|
-
for (int i = 1; i < argc; i++) fprintf(log, "%s%s", i == 1 ? "" : " ", argv[i]);
|
|
680
|
-
fputc('\n', log);
|
|
681
|
-
fclose(log);
|
|
682
|
-
|
|
683
|
-
int owns_socket = 0;
|
|
684
|
-
for (int i = 1; i + 1 < argc; i++) {
|
|
685
|
-
if (strcmp(argv[i], "-S") == 0 && strcmp(argv[i + 1], socket) == 0) owns_socket = 1;
|
|
686
|
-
}
|
|
687
|
-
if (!owns_socket) return 91;
|
|
688
|
-
if (has_arg(argc, argv, "new-window") || has_arg(argc, argv, "display-message")) {
|
|
689
|
-
puts("%9101");
|
|
690
|
-
} else if (has_arg(argc, argv, "list-panes")) {
|
|
691
|
-
printf("%%9101__TA_FIELD__%s__TA_FIELD__0__TA_FIELD__mate__TA_FIELD__0__TA_FIELD__/dev/ttys099__TA_FIELD__sh__TA_FIELD__1__TA_FIELD__%s__TA_FIELD__1__TA_FIELD__0__TA_FIELD__9101__TA_FIELD__\n", session, workspace);
|
|
692
|
-
}
|
|
693
|
-
return 0;
|
|
694
|
-
}
|
|
695
|
-
"#,
|
|
696
|
-
)
|
|
697
|
-
.expect("write native tmux shim source");
|
|
698
|
-
let compile = run_file_h_command_with_deadline(
|
|
699
|
-
std::process::Command::new("cc")
|
|
700
|
-
.args(["-O0", "-o"])
|
|
701
|
-
.arg(&shim)
|
|
702
|
-
.arg(&shim_source),
|
|
703
|
-
"File-H native fake compile",
|
|
704
|
-
FILE_H_NATIVE_COMPILE_DEADLINE,
|
|
705
|
-
&shim_root.join("compile.stdout"),
|
|
706
|
-
&shim_root.join("compile.stderr"),
|
|
707
|
-
)
|
|
708
|
-
.unwrap_or_else(|timeout| panic!("{timeout:?}"));
|
|
709
|
-
assert!(
|
|
710
|
-
compile.status.success(),
|
|
711
|
-
"native tmux shim compile failed: {}",
|
|
712
|
-
String::from_utf8_lossy(&compile.stderr)
|
|
713
|
-
);
|
|
714
|
-
let tooth_started = std::time::Instant::now();
|
|
715
|
-
let tooth = run_file_h_command_with_deadline(
|
|
716
|
-
std::process::Command::new(&shim).env("TEAM_AGENT_PI_TEST_HANG", "1"),
|
|
717
|
-
"File-H native fake readiness timeout tooth",
|
|
718
|
-
FILE_H_NATIVE_READINESS_DEADLINE,
|
|
719
|
-
&shim_root.join("readiness-tooth.stdout"),
|
|
720
|
-
&shim_root.join("readiness-tooth.stderr"),
|
|
721
|
-
)
|
|
722
|
-
.expect_err("non-exiting native fake must hit the readiness deadline");
|
|
723
|
-
assert_eq!(tooth.stage, "File-H native fake readiness timeout tooth");
|
|
724
|
-
assert_eq!(tooth.deadline, FILE_H_NATIVE_READINESS_DEADLINE);
|
|
725
|
-
assert!(!tooth.reaped_status.success(), "{tooth:?}");
|
|
726
|
-
assert!(
|
|
727
|
-
!crate::platform::process::pid_is_alive(tooth.pid),
|
|
728
|
-
"readiness timeout must leave its exact child dead and reaped: {tooth:?}"
|
|
729
|
-
);
|
|
730
|
-
assert!(
|
|
731
|
-
tooth_started.elapsed()
|
|
732
|
-
<= FILE_H_NATIVE_READINESS_DEADLINE + std::time::Duration::from_secs(1),
|
|
733
|
-
"readiness timeout tooth exceeded its deadline plus reap allowance: {tooth:?}"
|
|
734
|
-
);
|
|
735
|
-
|
|
736
|
-
let preflight = run_file_h_command_with_deadline(
|
|
737
|
-
std::process::Command::new(&shim)
|
|
738
|
-
.args(["-S", worker_socket, "has-session"])
|
|
739
|
-
.env("TEAM_AGENT_PI_TMUX_LOG", shim_root.join("preflight.log"))
|
|
740
|
-
.env("TEAM_AGENT_PI_OWNING_SOCKET", worker_socket)
|
|
741
|
-
.env("TEAM_AGENT_PI_OWNING_SESSION", live_session)
|
|
742
|
-
.env("TEAM_AGENT_PI_RUN_WORKSPACE", &live),
|
|
743
|
-
"File-H native fake has-session preflight",
|
|
744
|
-
FILE_H_NATIVE_READINESS_DEADLINE,
|
|
745
|
-
&shim_root.join("preflight.stdout"),
|
|
746
|
-
&shim_root.join("preflight.stderr"),
|
|
747
|
-
)
|
|
748
|
-
.unwrap_or_else(|timeout| panic!("{timeout:?}"));
|
|
749
|
-
assert!(
|
|
750
|
-
preflight.status.success(),
|
|
751
|
-
"native tmux shim preflight failed: {}",
|
|
752
|
-
String::from_utf8_lossy(&preflight.stderr)
|
|
753
|
-
);
|
|
754
|
-
let previous_path = std::env::var_os("PATH").unwrap_or_default();
|
|
755
|
-
let joined_path = std::env::join_paths(
|
|
756
|
-
std::iter::once(shim_root.clone()).chain(std::env::split_paths(&previous_path)),
|
|
757
|
-
)
|
|
758
|
-
.expect("join native shim PATH");
|
|
759
|
-
let _path = EnvVarGuard::set("PATH", joined_path);
|
|
760
|
-
let _log = EnvVarGuard::set("TEAM_AGENT_PI_TMUX_LOG", &shim_log);
|
|
761
|
-
let _socket = EnvVarGuard::set("TEAM_AGENT_PI_OWNING_SOCKET", worker_socket);
|
|
762
|
-
let _session = EnvVarGuard::set("TEAM_AGENT_PI_OWNING_SESSION", live_session);
|
|
763
|
-
let _workspace = EnvVarGuard::set("TEAM_AGENT_PI_RUN_WORKSPACE", &live);
|
|
764
|
-
|
|
765
|
-
let tools = TeamOrchestratorTools::with_identity(
|
|
766
|
-
&live,
|
|
767
|
-
Some(AgentId::new("leader")),
|
|
768
|
-
Some(TeamKey::new("current")),
|
|
769
|
-
);
|
|
770
|
-
let result = tools
|
|
771
|
-
.add_agent("mate", "roles/mate.md")
|
|
772
|
-
.expect("public MCP add must use the persisted worker endpoint");
|
|
773
|
-
assert_eq!(
|
|
774
|
-
result.fields.get("ok").and_then(serde_json::Value::as_bool),
|
|
775
|
-
Some(true)
|
|
776
|
-
);
|
|
777
|
-
let tmux_argv = std::fs::read_to_string(&shim_log).expect("read tmux shim log");
|
|
778
|
-
assert!(
|
|
779
|
-
tmux_argv.contains(&format!("-S {worker_socket}")),
|
|
780
|
-
"public add must address the persisted worker socket: {tmux_argv}"
|
|
781
|
-
);
|
|
782
|
-
assert!(
|
|
783
|
-
tmux_argv.contains(&format!(
|
|
784
|
-
"new-window -d -P -F #{{pane_id}} -t {live_session} -n mate"
|
|
785
|
-
)),
|
|
786
|
-
"public add must reuse the owner session with new-window: {tmux_argv}"
|
|
787
|
-
);
|
|
788
|
-
assert!(
|
|
789
|
-
tmux_argv.contains(&live.to_string_lossy().to_string()),
|
|
790
|
-
"spawn command must retain the MCP scratch as cwd: {tmux_argv}"
|
|
791
|
-
);
|
|
792
|
-
assert!(
|
|
793
|
-
!tmux_argv.contains("new-session"),
|
|
794
|
-
"public add must not start a same-named session on another server: {tmux_argv}"
|
|
795
|
-
);
|
|
796
|
-
let parent_socket = crate::tmux_backend::socket_name_for_workspace(
|
|
797
|
-
live.parent().expect("live scratch parent"),
|
|
798
|
-
);
|
|
799
|
-
assert!(
|
|
800
|
-
!tmux_argv.contains(&parent_socket),
|
|
801
|
-
"public add must not derive transport from the scratch parent: {tmux_argv}"
|
|
802
|
-
);
|
|
803
|
-
|
|
804
|
-
let added_state = crate::state::persist::load_runtime_state(&live)
|
|
805
|
-
.expect("load public add state for send/status");
|
|
806
|
-
assert_eq!(
|
|
807
|
-
added_state
|
|
808
|
-
.pointer("/teams/current/session_name")
|
|
809
|
-
.and_then(serde_json::Value::as_str),
|
|
810
|
-
Some(live_session),
|
|
811
|
-
"dynamic add must persist the selected receiver session for shared send/status routing"
|
|
812
|
-
);
|
|
813
|
-
assert!(
|
|
814
|
-
added_state
|
|
815
|
-
.pointer("/teams/sibling/session_name")
|
|
816
|
-
.is_some_and(serde_json::Value::is_null),
|
|
817
|
-
"selected-team routing must not borrow or rewrite a sibling session: {added_state}"
|
|
818
|
-
);
|
|
819
|
-
assert_eq!(
|
|
820
|
-
added_state
|
|
821
|
-
.pointer("/teams/sibling/leader_receiver/session_name")
|
|
822
|
-
.and_then(serde_json::Value::as_str),
|
|
823
|
-
Some("team-agent-leader-pi-sibling")
|
|
824
|
-
);
|
|
825
|
-
|
|
826
|
-
let mut old_state = added_state.clone();
|
|
827
|
-
old_state["teams"]["current"]["session_name"] = serde_json::Value::Null;
|
|
828
|
-
crate::state::persist::save_runtime_state(&live, &old_state)
|
|
829
|
-
.expect("seed controlled old null-session view");
|
|
830
|
-
let blocked = tools
|
|
831
|
-
.send_message(
|
|
832
|
-
&crate::messaging::MessageTarget::Single("mate".to_string()),
|
|
833
|
-
"controlled old session routing probe",
|
|
834
|
-
None,
|
|
835
|
-
None,
|
|
836
|
-
None,
|
|
837
|
-
)
|
|
838
|
-
.expect("old public send must return a structured refusal")
|
|
839
|
-
.to_value();
|
|
840
|
-
assert_eq!(blocked.get("ok"), Some(&json!(false)), "{blocked}");
|
|
841
|
-
assert_eq!(
|
|
842
|
-
blocked.get("reason"),
|
|
843
|
-
Some(&json!("tmux_target_missing")),
|
|
844
|
-
"the old null-session view must fail at real send target resolution: {blocked}"
|
|
845
|
-
);
|
|
846
|
-
let old_status = tools
|
|
847
|
-
.get_team_status()
|
|
848
|
-
.expect("old public status must return an honest dead view");
|
|
849
|
-
assert_eq!(
|
|
850
|
-
old_status
|
|
851
|
-
.fields
|
|
852
|
-
.get("agents")
|
|
853
|
-
.and_then(|agents| agents.get("mate"))
|
|
854
|
-
.and_then(|mate| mate.get("stale_reason"))
|
|
855
|
-
.and_then(serde_json::Value::as_str),
|
|
856
|
-
Some("pane_dead"),
|
|
857
|
-
"the old null-session view must fail at real status liveness resolution: {:?}",
|
|
858
|
-
old_status.fields
|
|
859
|
-
);
|
|
860
|
-
crate::state::persist::save_runtime_state(&live, &added_state)
|
|
861
|
-
.expect("restore corrected selected-team session");
|
|
862
|
-
|
|
863
|
-
let sent = tools
|
|
864
|
-
.send_message(
|
|
865
|
-
&crate::messaging::MessageTarget::Single("mate".to_string()),
|
|
866
|
-
"session routing probe",
|
|
867
|
-
None,
|
|
868
|
-
None,
|
|
869
|
-
None,
|
|
870
|
-
)
|
|
871
|
-
.expect("public send must resolve the added mate")
|
|
872
|
-
.to_value();
|
|
873
|
-
assert_eq!(sent.get("status"), Some(&json!("accepted")), "{sent}");
|
|
874
|
-
assert!(
|
|
875
|
-
sent.get("message_id")
|
|
876
|
-
.and_then(serde_json::Value::as_str)
|
|
877
|
-
.is_some_and(|message_id| message_id.starts_with("msg_")),
|
|
878
|
-
"blocked target resolution must not be reported as a successful send: {sent}"
|
|
879
|
-
);
|
|
880
|
-
|
|
881
|
-
let status = tools
|
|
882
|
-
.get_team_status()
|
|
883
|
-
.expect("public status must resolve the selected owner session");
|
|
884
|
-
assert_eq!(
|
|
885
|
-
status
|
|
886
|
-
.fields
|
|
887
|
-
.get("session_name")
|
|
888
|
-
.and_then(serde_json::Value::as_str),
|
|
889
|
-
Some(live_session),
|
|
890
|
-
"status must report the selected receiver session"
|
|
891
|
-
);
|
|
892
|
-
assert_ne!(
|
|
893
|
-
status
|
|
894
|
-
.fields
|
|
895
|
-
.get("agents")
|
|
896
|
-
.and_then(|agents| agents.get("mate"))
|
|
897
|
-
.and_then(|mate| mate.get("stale_reason"))
|
|
898
|
-
.and_then(serde_json::Value::as_str),
|
|
899
|
-
Some("pane_dead"),
|
|
900
|
-
"the live owner-session mate must not be reported pane_dead: {:?}",
|
|
901
|
-
status.fields
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
512
|
let shared_source = include_str!("../launch/pi_mcp.rs");
|
|
906
513
|
let leader_source = include_str!("../../leader/start.rs");
|
|
907
514
|
let teammate_source = include_str!("../launch/spawn.rs");
|
|
@@ -372,7 +372,7 @@ fn pi_leader_wrapper_materializes_from_empty_unicode_wsl_workspace() {
|
|
|
372
372
|
team_id: "current",
|
|
373
373
|
agent_id: "leader",
|
|
374
374
|
workspace: &workspace,
|
|
375
|
-
include_tools: &["
|
|
375
|
+
include_tools: &["send_message", "report_result", "get_team_status"],
|
|
376
376
|
})
|
|
377
377
|
.expect("materialize Pi leader wrapper from an empty Unicode workspace");
|
|
378
378
|
|
|
@@ -437,7 +437,7 @@ fn pi_wrapper_publish_needs_no_final_path_metadata_lookup() {
|
|
|
437
437
|
team_id: "current",
|
|
438
438
|
agent_id: "leader",
|
|
439
439
|
workspace: &workspace,
|
|
440
|
-
include_tools: &["
|
|
440
|
+
include_tools: &["send_message", "report_result", "get_team_status"],
|
|
441
441
|
},
|
|
442
442
|
|temp, destination| {
|
|
443
443
|
std::fs::rename(temp, destination)?;
|
|
@@ -30,6 +30,9 @@ All communication must go through Team Agent MCP tools.
|
|
|
30
30
|
- Teammate: {send_message}(to='<agent_id>', content='...')
|
|
31
31
|
- Broadcast: {send_message}(to='*', content='...')
|
|
32
32
|
- Complete: {report_result}(summary='...') — call exactly once
|
|
33
|
+
- Optional roster/status: {get_team_status} — use only when useful; it is not a communication or completion prerequisite.
|
|
34
|
+
|
|
35
|
+
Only these three Team Agent MCP tools are available. Lifecycle management remains on the separately authorized operator CLI.
|
|
33
36
|
|
|
34
37
|
## Rules:
|
|
35
38
|
|
|
@@ -55,7 +58,7 @@ pub(crate) struct WorkerCommandAgent {
|
|
|
55
58
|
system_prompt_file: Option<String>,
|
|
56
59
|
output_contract_format: Option<String>,
|
|
57
60
|
communication_mode: CommunicationMode,
|
|
58
|
-
///
|
|
61
|
+
/// Missing `dangerously_skip_permissions` defaults to true; an explicit false remains respected.
|
|
59
62
|
dangerously_skip_permissions: bool,
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -107,9 +110,9 @@ impl WorkerCommandAgent {
|
|
|
107
110
|
.get("communication_mode")
|
|
108
111
|
.and_then(crate::model::yaml::Value::as_str),
|
|
109
112
|
)?,
|
|
110
|
-
dangerously_skip_permissions: matches!(
|
|
113
|
+
dangerously_skip_permissions: !matches!(
|
|
111
114
|
agent.get("dangerously_skip_permissions"),
|
|
112
|
-
Some(crate::model::yaml::Value::Bool(
|
|
115
|
+
Some(crate::model::yaml::Value::Bool(false))
|
|
113
116
|
),
|
|
114
117
|
})
|
|
115
118
|
}
|
|
@@ -164,7 +167,7 @@ impl WorkerCommandAgent {
|
|
|
164
167
|
dangerously_skip_permissions: agent
|
|
165
168
|
.get("dangerously_skip_permissions")
|
|
166
169
|
.and_then(serde_json::Value::as_bool)
|
|
167
|
-
.unwrap_or(
|
|
170
|
+
.unwrap_or(true),
|
|
168
171
|
})
|
|
169
172
|
}
|
|
170
173
|
}
|
|
@@ -182,15 +185,20 @@ pub(crate) fn compile_worker_system_prompt(
|
|
|
182
185
|
// C-1 cr verdict / B2 灵魂件 — identity 必须 FIRST(MUST-4 行为层守:空白上下文问
|
|
183
186
|
// "你是谁"必须先答 Team Agent worker 身份)。runtime contract 跟后。
|
|
184
187
|
let send_message = mcp_tool_name(agent.provider, "team_orchestrator", "send_message");
|
|
188
|
+
let get_team_status = if agent.provider == Provider::Pi {
|
|
189
|
+
r#"mcp({tool:"team_orchestrator_get_team_status", args:{}})"#.to_string()
|
|
190
|
+
} else {
|
|
191
|
+
format!("{}()", mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status"))
|
|
192
|
+
};
|
|
185
193
|
let report_result = if agent.provider == Provider::Pi {
|
|
186
194
|
r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#.to_string()
|
|
187
195
|
} else {
|
|
188
196
|
mcp_tool_name(agent.provider, "team_orchestrator", "report_result")
|
|
189
197
|
};
|
|
190
198
|
let runtime_contract = if agent.provider == Provider::Pi {
|
|
191
|
-
pi_runtime_contract_section()
|
|
199
|
+
pi_runtime_contract_section(&get_team_status)
|
|
192
200
|
} else {
|
|
193
|
-
runtime_contract_section(&send_message, &report_result)
|
|
201
|
+
runtime_contract_section(&send_message, &report_result, &get_team_status)
|
|
194
202
|
};
|
|
195
203
|
let communication_contract = if agent.provider == Provider::Pi {
|
|
196
204
|
pi_communication_contract(agent.communication_mode)
|
|
@@ -241,13 +249,18 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
|
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
fn runtime_contract_section(
|
|
252
|
+
fn runtime_contract_section(
|
|
253
|
+
send_message: &str,
|
|
254
|
+
report_result: &str,
|
|
255
|
+
get_team_status: &str,
|
|
256
|
+
) -> String {
|
|
245
257
|
RUNTIME_CONTRACT_SECTION
|
|
246
258
|
.replace("{send_message}", send_message)
|
|
247
259
|
.replace("{report_result}", report_result)
|
|
260
|
+
.replace("{get_team_status}", get_team_status)
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
fn pi_runtime_contract_section() -> String {
|
|
263
|
+
fn pi_runtime_contract_section(get_team_status: &str) -> String {
|
|
251
264
|
RUNTIME_CONTRACT_SECTION
|
|
252
265
|
.replace(
|
|
253
266
|
"{send_message}(to='<agent_id>', content='...')",
|
|
@@ -261,6 +274,7 @@ fn pi_runtime_contract_section() -> String {
|
|
|
261
274
|
"{report_result}(summary='...')",
|
|
262
275
|
r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#,
|
|
263
276
|
)
|
|
277
|
+
.replace("{get_team_status}", get_team_status)
|
|
264
278
|
}
|
|
265
279
|
|
|
266
280
|
fn pi_communication_contract(mode: CommunicationMode) -> String {
|
|
@@ -40,22 +40,6 @@ pub fn is_worker_recipient(to: &MessageTarget) -> bool {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/// `_merge_tasks_by_id` (`tools.py:30-49`): dedupe a task list keyed by `id`,
|
|
44
|
-
/// `prefer` entries winning on duplicates (so an earlier `done` is not regressed).
|
|
45
|
-
pub fn merge_tasks_by_id(prefer: &[Value], fallback: &[Value]) -> Vec<Value> {
|
|
46
|
-
let mut seen = std::collections::BTreeSet::new();
|
|
47
|
-
let mut out = Vec::new();
|
|
48
|
-
for item in prefer.iter().chain(fallback.iter()) {
|
|
49
|
-
let Some(id) = item.get("id").and_then(Value::as_str) else {
|
|
50
|
-
continue;
|
|
51
|
-
};
|
|
52
|
-
if seen.insert(id.to_string()) && item.is_object() {
|
|
53
|
-
out.push(item.clone());
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
out
|
|
57
|
-
}
|
|
58
|
-
|
|
59
43
|
pub(crate) fn tool_error_reason_wire(reason: ToolErrorReason) -> &'static str {
|
|
60
44
|
match reason {
|
|
61
45
|
ToolErrorReason::UnknownTool => "unknown_tool",
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
//!
|
|
2
|
-
//! MCP
|
|
3
|
-
//!
|
|
4
|
-
//! S0 keeps the old placeholder behavior behind stable module boundaries so
|
|
5
|
-
//! follow-up lanes can implement real lifecycle logic without touching tools.rs.
|
|
2
|
+
//! MCP status query facade.
|
|
6
3
|
|
|
7
|
-
mod agent_ops;
|
|
8
4
|
mod state_status;
|
|
9
5
|
|
|
10
|
-
pub(crate) use
|
|
11
|
-
pub(crate) use state_status::{get_team_status, update_state};
|
|
6
|
+
pub(crate) use state_status::get_team_status;
|