- bash is now Delegatable so named Agents may run shell commands - remove the legacy anonymous 'general' path entirely: SubAgentManager run_background/run_foreground_batch/run_inline/run_foreground, filter_tools/get_skills_prompt, TaskNotification direct delivery, DelegateContext task-local, background_permits/admission, and the delegate tool's check_task/cancel_task/list_tasks actions (agent_task replaces them); /stop and todo migrate off the task-local bridge - stop writing the legacy background_tasks table (read-only transition); /api/tasks union still renders historical records - ship a built-in general-purpose Agent definition (resources/agents) released to ~/.picobot/agents on first run like built-in skills, with bash/read-only/search/web/calculator/sleep tools; root_delegates now defaults to it so orchestration works out of the box - update AGENTS/README/architecture/config/db-schema docs accordingly
174 lines
5.4 KiB
Rust
174 lines
5.4 KiB
Rust
use std::env;
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
fn main() {
|
|
let out_dir = env::var("OUT_DIR").unwrap();
|
|
build_webui(Path::new(&out_dir));
|
|
|
|
println!("cargo:rerun-if-changed=resources/skills");
|
|
let skills_dir = Path::new("resources/skills");
|
|
let skills_out_dir = Path::new(&out_dir).join("skills");
|
|
fs::create_dir_all(&skills_out_dir).unwrap();
|
|
|
|
println!("cargo:rerun-if-changed=resources/agents");
|
|
let agents_dir = Path::new("resources/agents");
|
|
let agents_out_dir = Path::new(&out_dir).join("agents");
|
|
fs::create_dir_all(&agents_out_dir).unwrap();
|
|
let mut agents = Vec::new();
|
|
if let Ok(entries) = fs::read_dir(agents_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.extension().and_then(|e| e.to_str()) != Some("md") {
|
|
continue;
|
|
}
|
|
let agent_name = path
|
|
.file_stem()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.to_string();
|
|
fs::copy(&path, agents_out_dir.join(format!("{agent_name}.md"))).unwrap();
|
|
agents.push(agent_name);
|
|
}
|
|
}
|
|
agents.sort();
|
|
|
|
let mut skills = Vec::new();
|
|
|
|
if let Ok(entries) = fs::read_dir(skills_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if !path.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let skill_name = path.file_name().unwrap().to_str().unwrap().to_string();
|
|
let compressed_path = skills_out_dir.join(format!("{}.tar.zst", skill_name));
|
|
|
|
let compressed = compress_skill_dir(&path);
|
|
fs::write(&compressed_path, &compressed).unwrap();
|
|
|
|
skills.push(skill_name);
|
|
}
|
|
}
|
|
|
|
let mut code = String::from(
|
|
r#"pub struct EmbeddedSkill {
|
|
pub name: &'static str,
|
|
pub data: &'static [u8],
|
|
}
|
|
|
|
pub static EMBEDDED_SKILLS: &[EmbeddedSkill] = &[
|
|
"#,
|
|
);
|
|
|
|
for name in &skills {
|
|
let file_path = skills_out_dir
|
|
.join(format!("{}.tar.zst", name))
|
|
.to_string_lossy()
|
|
.to_string();
|
|
code.push_str(&format!(
|
|
" EmbeddedSkill {{ name: \"{}\", data: include_bytes!(\"{}\") }},\n",
|
|
name, file_path
|
|
));
|
|
}
|
|
|
|
code.push_str("];\n");
|
|
|
|
let generated_path = Path::new(&out_dir).join("embedded_skills.rs");
|
|
let mut f = fs::File::create(&generated_path).unwrap();
|
|
f.write_all(code.as_bytes()).unwrap();
|
|
|
|
let mut agent_code = String::from(
|
|
r#"pub struct EmbeddedAgent {
|
|
pub name: &'static str,
|
|
pub content: &'static str,
|
|
}
|
|
|
|
pub static EMBEDDED_AGENTS: &[EmbeddedAgent] = &[
|
|
"#,
|
|
);
|
|
for name in &agents {
|
|
let file_path = agents_out_dir
|
|
.join(format!("{name}.md"))
|
|
.to_string_lossy()
|
|
.to_string();
|
|
agent_code.push_str(&format!(
|
|
" EmbeddedAgent {{ name: \"{name}\", content: include_str!(\"{file_path}\") }},\n",
|
|
));
|
|
}
|
|
agent_code.push_str("];\n");
|
|
let agent_path = Path::new(&out_dir).join("embedded_agents.rs");
|
|
let mut f = fs::File::create(&agent_path).unwrap();
|
|
f.write_all(agent_code.as_bytes()).unwrap();
|
|
}
|
|
|
|
fn build_webui(out_dir: &Path) {
|
|
for path in [
|
|
"webui/src",
|
|
"webui/index.html",
|
|
"webui/jsconfig.json",
|
|
"webui/package.json",
|
|
"webui/package-lock.json",
|
|
"webui/svelte.config.js",
|
|
"webui/vite.config.js",
|
|
"webui/public",
|
|
] {
|
|
println!("cargo:rerun-if-changed={path}");
|
|
}
|
|
|
|
let webui_dir = Path::new("webui");
|
|
let lockfile = webui_dir.join("package-lock.json");
|
|
let dependency_stamp = webui_dir
|
|
.join("node_modules")
|
|
.join(".picobot-package-lock.json");
|
|
let lockfile_contents = fs::read(&lockfile).expect("failed to read webui/package-lock.json");
|
|
let dependencies_current = fs::read(&dependency_stamp)
|
|
.is_ok_and(|stamp| stamp == lockfile_contents)
|
|
&& webui_dir.join("node_modules/.bin/vite").is_file();
|
|
|
|
if !dependencies_current {
|
|
run_npm(webui_dir, &["ci", "--no-audit", "--no-fund"], None);
|
|
fs::write(&dependency_stamp, &lockfile_contents)
|
|
.expect("failed to write WebUI dependency stamp");
|
|
}
|
|
|
|
let webui_out_dir = out_dir.join("webui");
|
|
run_npm(webui_dir, &["run", "build"], Some(&webui_out_dir));
|
|
}
|
|
|
|
fn run_npm(webui_dir: &Path, args: &[&str], output_dir: Option<&Path>) {
|
|
let mut command = Command::new("npm");
|
|
command.args(args).current_dir(webui_dir);
|
|
if let Some(output_dir) = output_dir {
|
|
command.env("PICOBOT_WEBUI_OUT_DIR", output_dir);
|
|
}
|
|
|
|
let output = command.output().unwrap_or_else(|error| {
|
|
panic!("failed to start npm for the WebUI build ({error}); install Node.js 20+ and npm")
|
|
});
|
|
if !output.status.success() {
|
|
panic!(
|
|
"WebUI command `npm {}` failed\nstdout:\n{}\nstderr:\n{}",
|
|
args.join(" "),
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
}
|
|
|
|
fn compress_skill_dir(dir: &Path) -> Vec<u8> {
|
|
let mut buf = Vec::new();
|
|
let mut builder = tar::Builder::new(&mut buf);
|
|
builder.follow_symlinks(false);
|
|
builder
|
|
.append_dir_all(dir.file_name().unwrap().to_str().unwrap(), dir)
|
|
.unwrap();
|
|
drop(builder);
|
|
|
|
zstd::encode_all(buf.as_slice(), 3).unwrap()
|
|
}
|