From 54eb61a4f3928eda5bbec459784a964a9646136a Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 24 Mar 2026 18:32:45 +0900 Subject: [PATCH] refactor(prompt): extract agent/AI prompts and protocol config into dedicated modules --- src/agent.rs | 13 ++++---- src/config.rs | 44 +++++++++++--------------- src/headless.rs | 20 +++++------- src/lib.rs | 1 + src/presets/daily.json | 14 +++++++++ src/presets/improve.json | 10 ++++++ src/presets/report.json | 14 +++++++++ src/presets/review.json | 10 ++++++ src/presets/security.json | 10 ++++++ src/prompt.rs | 59 +++++++++++++++++++++++++++++++++++ src/prompts/agent_system.json | 3 ++ src/prompts/ai_identity.json | 3 ++ src/prompts/integrate.json | 3 ++ src/prompts/protocols.json | 6 ++++ src/tui.rs | 16 +++------- 15 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 src/presets/daily.json create mode 100644 src/presets/improve.json create mode 100644 src/presets/report.json create mode 100644 src/presets/review.json create mode 100644 src/presets/security.json create mode 100644 src/prompt.rs create mode 100644 src/prompts/agent_system.json create mode 100644 src/prompts/ai_identity.json create mode 100644 src/prompts/integrate.json create mode 100644 src/prompts/protocols.json diff --git a/src/agent.rs b/src/agent.rs index 69d7604..c630a1b 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -65,6 +65,10 @@ pub struct Agent { impl Agent { pub fn spawn(id: usize, name: &str, task: &str, cwd: &str) -> Result { + Self::spawn_with_config(id, name, task, cwd, None, None) + } + + pub fn spawn_with_config(id: usize, name: &str, task: &str, cwd: &str, host: Option<&str>, protocol: Option<&str>) -> Result { let cwd_path = std::path::Path::new(cwd); if !cwd_path.is_dir() { return Err(format!("directory not found: {cwd}")); @@ -73,14 +77,9 @@ impl Agent { let (child, mut stdin, stdout) = claude::spawn_claude(Some(cwd))?; let pid = child.id(); - // Inject self-awareness + user/bot context let user_ctx = load_user_context(); - let identity = format!( - "[system]\n\ - You are agent '{name}' running inside aishell.\n\ - {user_ctx}\ - If you find issues or have follow-up work, propose: @agent-name task -c dir\n" - ); + let protocol_ctx = crate::prompt::protocol_context(protocol, host); + let identity = crate::prompt::agent_system(name, &user_ctx, &protocol_ctx); let git_ctx = git_context(cwd) .map(|ctx| format!("\n[git context]\n{ctx}")) .unwrap_or_default(); diff --git a/src/config.rs b/src/config.rs index 986efb7..abcb45d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -59,6 +59,10 @@ pub struct AgentConfig { pub task: String, #[serde(default = "default_cwd")] pub cwd: String, + #[serde(default)] + pub host: Option, + #[serde(default)] + pub protocol: Option, } fn default_cwd() -> String { @@ -72,34 +76,22 @@ pub struct LoadedConfig { pub interval: Option, } -/// Built-in presets. +/// Built-in presets (embedded from src/presets/*.json). pub fn preset(name: &str) -> Option> { + let json = match name { + "daily" => include_str!("presets/daily.json"), + "review" => include_str!("presets/review.json"), + "improve" => include_str!("presets/improve.json"), + "security" => include_str!("presets/security.json"), + "report" => include_str!("presets/report.json"), + _ => return None, + }; + let mut agents: Vec = serde_json::from_str(json).ok()?; let cwd = default_cwd(); - match name { - "daily" => Some(vec![ - AgentConfig { name: "health".into(), task: "Run cargo test and cargo build --release. One line: pass or fail, warning count.".into(), cwd: cwd.clone() }, - AgentConfig { name: "quality".into(), task: "Find one unwrap() that could panic and one function over 50 lines. Give file:line only.".into(), cwd: cwd.clone() }, - AgentConfig { name: "idea".into(), task: "Read docs/architecture.md and git log -5. Suggest one practical improvement. 3 sentences max.".into(), cwd }, - ]), - "review" => Some(vec![ - AgentConfig { name: "diff-review".into(), task: "Review the git diff. Report problems or say 'no issues'.".into(), cwd: cwd.clone() }, - AgentConfig { name: "commit-msg".into(), task: "Suggest a commit message for the current changes. Conventional commits format.".into(), cwd }, - ]), - "improve" => Some(vec![ - AgentConfig { name: "bug-hunt".into(), task: "Find one concrete bug in the codebase. Give file:line and a fix.".into(), cwd: cwd.clone() }, - AgentConfig { name: "simplify".into(), task: "Find one function that can be removed or simplified. Be specific.".into(), cwd }, - ]), - "security" => Some(vec![ - AgentConfig { name: "secrets".into(), task: "Scan src/ for hardcoded secrets, API keys, tokens, passwords, personal emails, private IPs. Also check for hardcoded absolute paths like /Users/ or /home/. Report file:line for each finding, or 'clean' if none.".into(), cwd: cwd.clone() }, - AgentConfig { name: "safe-publish".into(), task: "Check if this repo is safe to push publicly. Look for: .env files, credentials in config/, personal info in docs/, sensitive data in git history (check git log --all --oneline for suspicious commit messages). Report issues or 'safe'.".into(), cwd }, - ]), - "report" => Some(vec![ - AgentConfig { name: "agent-view".into(), task: "You are an agent inside aishell. Reflect: What context did you receive? What was missing? What would make your job easier? 3 concrete points from your perspective.".into(), cwd: cwd.clone() }, - AgentConfig { name: "user-view".into(), task: "You are a developer using aishell daily. Run aishell help and aishell context. What are the 3 biggest friction points in the daily workflow?".into(), cwd: cwd.clone() }, - AgentConfig { name: "system-view".into(), task: "Read src/headless.rs and src/tui.rs. From the system's perspective: what is the most fragile part? What would break first under heavy use? One concrete issue.".into(), cwd }, - ]), - _ => None, + for a in &mut agents { + if a.cwd == "." { a.cwd = cwd.clone(); } } + Some(agents) } // ── File loading ─────────────────────────────────────────── @@ -136,7 +128,7 @@ fn load_file_full(path: &Path) -> LoadedConfig { } if let (Some(name), Some(task), Some(cwd)) = (multi.name, multi.task, multi.cwd) { return LoadedConfig { - agents: vec![AgentConfig { name, task, cwd }], + agents: vec![AgentConfig { name, task, cwd, host: None, protocol: None }], interval: multi.interval, }; } diff --git a/src/headless.rs b/src/headless.rs index 62a42f6..d565c98 100644 --- a/src/headless.rs +++ b/src/headless.rs @@ -27,7 +27,7 @@ pub fn run(config_or_task: &str, cwd_override: Option<&str>, name_override: Opti .map(|p| p.display().to_string()) .unwrap_or_else(|_| ".".to_string())); let name = name_override.unwrap_or("task").to_string(); - (vec![config::AgentConfig { name, task: config_or_task.to_string(), cwd }], None) + (vec![config::AgentConfig { name, task: config_or_task.to_string(), cwd, host: None, protocol: None }], None) }; if configs.is_empty() { @@ -359,7 +359,7 @@ fn spawn_and_wait(configs: &[config::AgentConfig], running: &Arc) -> let mut next_id = 1; for cfg in configs { let cwd = expand_tilde(&cfg.cwd); - match Agent::spawn(next_id, &cfg.name, &cfg.task, &cwd) { + match Agent::spawn_with_config(next_id, &cfg.name, &cfg.task, &cwd, cfg.host.as_deref(), cfg.protocol.as_deref()) { Ok(agent) => { eprintln!(" started: {} ({})", cfg.name, cwd); agents.push(agent); @@ -426,7 +426,7 @@ fn extract_agent_configs(text: &str) -> Vec { }; if name.is_empty() || task.is_empty() { return None; } - Some(config::AgentConfig { name, task, cwd }) + Some(config::AgentConfig { name, task, cwd, host: None, protocol: None }) }) .collect() } @@ -637,16 +637,8 @@ fn integrate_results(agents: &[Agent], prev_decision: &str, cycle: usize) -> Res format!("前回のサイクルでの判断:\n{}\n\n---\n\n", prev_decision) }; - claude.send(&format!( - "{prev_context}以下はcycle {cycle}のエージェント実行結果です。\n\n```json\n{payload}\n```\n\n\ - 結果を統合し回答してください:\n\ - 1. 全体の結論(1行)\n\ - 2. 各エージェントの重要な発見(箇条書き)\n\ - 3. 次に取るべきアクション\n\n\ - 次のサイクルでエージェントを実行する場合は、以下の形式で指示してください:\n\ - @agent-name タスク内容 -c 作業ディレクトリ\n\ - 不要なら@agentは書かないでください。" - )); + let msg = crate::prompt::integrate(&prev_context, cycle, &payload); + claude.send(&msg); let mut output = String::new(); let start = std::time::Instant::now(); @@ -755,6 +747,8 @@ pub fn commit() { name: "commit-msg".into(), task: "Suggest a commit message for the current git changes. Output ONLY the message, nothing else. Conventional commits format. One line.".into(), cwd, + host: None, + protocol: None, }]; let running = Arc::new(AtomicBool::new(true)); diff --git a/src/lib.rs b/src/lib.rs index f0a429e..40da075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod claude; pub mod config; +pub mod prompt; pub mod judge; pub mod executor; pub mod ai; diff --git a/src/presets/daily.json b/src/presets/daily.json new file mode 100644 index 0000000..ac0560f --- /dev/null +++ b/src/presets/daily.json @@ -0,0 +1,14 @@ +[ + { + "name": "health", + "task": "Run cargo test and cargo build --release. One line: pass or fail, warning count." + }, + { + "name": "quality", + "task": "Find one unwrap() that could panic and one function over 50 lines. Give file:line only." + }, + { + "name": "idea", + "task": "Read docs/architecture.md and git log -5. Suggest one practical improvement. 3 sentences max." + } +] diff --git a/src/presets/improve.json b/src/presets/improve.json new file mode 100644 index 0000000..724ac6f --- /dev/null +++ b/src/presets/improve.json @@ -0,0 +1,10 @@ +[ + { + "name": "bug-hunt", + "task": "Find one concrete bug in the codebase. Give file:line and a fix." + }, + { + "name": "simplify", + "task": "Find one function that can be removed or simplified. Be specific." + } +] diff --git a/src/presets/report.json b/src/presets/report.json new file mode 100644 index 0000000..ace7043 --- /dev/null +++ b/src/presets/report.json @@ -0,0 +1,14 @@ +[ + { + "name": "agent-view", + "task": "You are an agent inside aishell. Reflect: What context did you receive? What was missing? What would make your job easier? 3 concrete points from your perspective." + }, + { + "name": "user-view", + "task": "You are a developer using aishell daily. Run aishell help and aishell context. What are the 3 biggest friction points in the daily workflow?" + }, + { + "name": "system-view", + "task": "Read src/headless.rs and src/tui.rs. From the system's perspective: what is the most fragile part? What would break first under heavy use? One concrete issue." + } +] diff --git a/src/presets/review.json b/src/presets/review.json new file mode 100644 index 0000000..f0e54ec --- /dev/null +++ b/src/presets/review.json @@ -0,0 +1,10 @@ +[ + { + "name": "diff-review", + "task": "Review the git diff. Report problems or say 'no issues'." + }, + { + "name": "commit-msg", + "task": "Suggest a commit message for the current changes. Conventional commits format." + } +] diff --git a/src/presets/security.json b/src/presets/security.json new file mode 100644 index 0000000..4dd9ae1 --- /dev/null +++ b/src/presets/security.json @@ -0,0 +1,10 @@ +[ + { + "name": "secrets", + "task": "Scan src/ for hardcoded secrets, API keys, tokens, passwords, personal emails, private IPs. Also check for hardcoded absolute paths like /Users/ or /home/. Report file:line for each finding, or 'clean' if none." + }, + { + "name": "safe-publish", + "task": "Check if this repo is safe to push publicly. Look for: .env files, credentials in config/, personal info in docs/, sensitive data in git history (check git log --all --oneline for suspicious commit messages). Report issues or 'safe'." + } +] diff --git a/src/prompt.rs b/src/prompt.rs new file mode 100644 index 0000000..539d324 --- /dev/null +++ b/src/prompt.rs @@ -0,0 +1,59 @@ +use serde_json::Value; + +const AI_IDENTITY: &str = include_str!("prompts/ai_identity.json"); +const AGENT_SYSTEM: &str = include_str!("prompts/agent_system.json"); +const INTEGRATE: &str = include_str!("prompts/integrate.json"); +const PROTOCOLS: &str = include_str!("prompts/protocols.json"); + +fn load(json: &str) -> Value { + serde_json::from_str(json).unwrap_or_default() +} + +/// Main AI identity prompt for TUI. +pub fn ai_identity() -> String { + load(AI_IDENTITY)["system"].as_str().unwrap_or("").to_string() +} + +/// Agent system prompt. Variables: {name}, {user_ctx}, {protocol_ctx} +pub fn agent_system(name: &str, user_ctx: &str, protocol_ctx: &str) -> String { + load(AGENT_SYSTEM)["system"].as_str().unwrap_or("") + .replace("{name}", name) + .replace("{user_ctx}", user_ctx) + .replace("{protocol_ctx}", protocol_ctx) +} + +/// Integration prompt. Variables: {prev_context}, {cycle}, {payload} +pub fn integrate(prev_context: &str, cycle: usize, payload: &str) -> String { + load(INTEGRATE)["system"].as_str().unwrap_or("") + .replace("{prev_context}", prev_context) + .replace("{cycle}", &cycle.to_string()) + .replace("{payload}", payload) +} + +/// Protocol context string for a given protocol + host. +pub fn protocol_context(protocol: Option<&str>, host: Option<&str>) -> String { + let protocols = load(PROTOCOLS); + match (protocol, host) { + (Some(p), Some(h)) => { + if let Some(tmpl) = protocols[p].as_str() { + format!("{}\n", tmpl.replace("{host}", h)) + } else { + format!("[protocol: {p}]\n[host: {h}]\n") + } + } + (Some(p), None) => { + if let Some(tmpl) = protocols[p].as_str() { + // Remove [host: ...] / [did: ...] / etc lines when no host + let lines: Vec<&str> = tmpl.lines() + .filter(|l| !l.contains("{host}")) + .collect(); + if lines.is_empty() { String::new() } + else { format!("{}\n", lines.join("\n")) } + } else { + format!("[protocol: {p}]\n") + } + } + (None, Some(h)) => format!("[host: {h}]\nExecute commands via: ssh {h} \"command\"\n"), + (None, None) => String::new(), + } +} diff --git a/src/prompts/agent_system.json b/src/prompts/agent_system.json new file mode 100644 index 0000000..fd18c11 --- /dev/null +++ b/src/prompts/agent_system.json @@ -0,0 +1,3 @@ +{ + "system": "[system]\nYou are agent '{name}' running inside aishell.\n{user_ctx}{protocol_ctx}If you find issues or have follow-up work, propose: @agent-name task -c dir" +} diff --git a/src/prompts/ai_identity.json b/src/prompts/ai_identity.json new file mode 100644 index 0000000..f03b6aa --- /dev/null +++ b/src/prompts/ai_identity.json @@ -0,0 +1,3 @@ +{ + "system": "You are the main AI in aishell, a multi-agent development tool.\nYour personality comes from aigpt MCP (core.md).\nYou oversee agents, integrate their results, and guide the user.\nYou can spawn agents with: @name task -c dir\n\nReport any usability issues you notice about aishell itself.\n\n一言だけ挨拶してください。" +} diff --git a/src/prompts/integrate.json b/src/prompts/integrate.json new file mode 100644 index 0000000..66d132f --- /dev/null +++ b/src/prompts/integrate.json @@ -0,0 +1,3 @@ +{ + "system": "{prev_context}以下はcycle {cycle}のエージェント実行結果です。\n\n```json\n{payload}\n```\n\n結果を統合し回答してください:\n1. 全体の結論(1行)\n2. 各エージェントの重要な発見(箇条書き)\n3. 次に取るべきアクション\n\n次のサイクルでエージェントを実行する場合は、以下の形式で指示してください:\n@agent-name タスク内容 -c 作業ディレクトリ\n不要なら@agentは書かないでください。" +} diff --git a/src/prompts/protocols.json b/src/prompts/protocols.json new file mode 100644 index 0000000..82b4f63 --- /dev/null +++ b/src/prompts/protocols.json @@ -0,0 +1,6 @@ +{ + "ssh": "[protocol: ssh]\n[host: {host}]\nExecute commands via: ssh {host} \"command\"", + "https": "[protocol: https]\n[endpoint: {host}]\nAccess via HTTP/HTTPS API calls.", + "at": "[protocol: at]\n[did: {host}]\nAccess via AT Protocol. Use ailog or atproto API.", + "git": "[protocol: git]\n[remote: {host}]\nAccess via git commands." +} diff --git a/src/tui.rs b/src/tui.rs index 3157009..1fb1f09 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -59,15 +59,6 @@ pub struct App { } /// Protocol + self-awareness. Personality comes from aigpt MCP. -const AI_IDENTITY: &str = "\ -You are the main AI in aishell, a multi-agent development tool. -Your personality comes from aigpt MCP (core.md). -You oversee agents, integrate their results, and guide the user. -You can spawn agents with: @name task -c dir - -Report any usability issues you notice about aishell itself. - -一言だけ挨拶してください。"; impl App { fn new(configs: Vec) -> Self { @@ -105,14 +96,15 @@ impl App { .unwrap_or_else(|_| ".".to_string()); let git_ctx = crate::agent::git_context(&cwd).unwrap_or_default(); let identity_ctx = load_identity_context(); - let msg = format!("{AI_IDENTITY}\n\n{identity_ctx}[project]\n{git_ctx}"); + let ai_id = crate::prompt::ai_identity(); + let msg = format!("{ai_id}\n\n{identity_ctx}[project]\n{git_ctx}"); claude.send(&msg); } let mut errors = Vec::new(); for cfg in configs { let cwd = expand_tilde(&cfg.cwd); - match Agent::spawn(app.next_id, &cfg.name, &cfg.task, &cwd) { + match Agent::spawn_with_config(app.next_id, &cfg.name, &cfg.task, &cwd, cfg.host.as_deref(), cfg.protocol.as_deref()) { Ok(agent) => { app.agents.push(agent); app.next_id += 1; @@ -729,7 +721,7 @@ fn load_identity_context() -> String { let user_chat_dir = format!("{base}/{user_did}/ai.syui.log.chat"); for dir in [&chat_dir, &user_chat_dir] { - if let Ok(mut entries) = std::fs::read_dir(dir) { + if let Ok(entries) = std::fs::read_dir(dir) { let mut files: Vec<_> = entries .flatten() .filter(|e| e.path().extension().is_some_and(|x| x == "json"))