refactor(prompt): extract agent/AI prompts and protocol config into dedicated modules
This commit is contained in:
13
src/agent.rs
13
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, String> {
|
||||
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<Self, String> {
|
||||
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();
|
||||
|
||||
@@ -59,6 +59,10 @@ pub struct AgentConfig {
|
||||
pub task: String,
|
||||
#[serde(default = "default_cwd")]
|
||||
pub cwd: String,
|
||||
#[serde(default)]
|
||||
pub host: Option<String>,
|
||||
#[serde(default)]
|
||||
pub protocol: Option<String>,
|
||||
}
|
||||
|
||||
fn default_cwd() -> String {
|
||||
@@ -72,34 +76,22 @@ pub struct LoadedConfig {
|
||||
pub interval: Option<u64>,
|
||||
}
|
||||
|
||||
/// Built-in presets.
|
||||
/// Built-in presets (embedded from src/presets/*.json).
|
||||
pub fn preset(name: &str) -> Option<Vec<AgentConfig>> {
|
||||
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<AgentConfig> = 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<AtomicBool>) ->
|
||||
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<config::AgentConfig> {
|
||||
};
|
||||
|
||||
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));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod claude;
|
||||
pub mod config;
|
||||
pub mod prompt;
|
||||
pub mod judge;
|
||||
pub mod executor;
|
||||
pub mod ai;
|
||||
|
||||
14
src/presets/daily.json
Normal file
14
src/presets/daily.json
Normal file
@@ -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."
|
||||
}
|
||||
]
|
||||
10
src/presets/improve.json
Normal file
10
src/presets/improve.json
Normal file
@@ -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."
|
||||
}
|
||||
]
|
||||
14
src/presets/report.json
Normal file
14
src/presets/report.json
Normal file
@@ -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."
|
||||
}
|
||||
]
|
||||
10
src/presets/review.json
Normal file
10
src/presets/review.json
Normal file
@@ -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."
|
||||
}
|
||||
]
|
||||
10
src/presets/security.json
Normal file
10
src/presets/security.json
Normal file
@@ -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'."
|
||||
}
|
||||
]
|
||||
59
src/prompt.rs
Normal file
59
src/prompt.rs
Normal file
@@ -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(),
|
||||
}
|
||||
}
|
||||
3
src/prompts/agent_system.json
Normal file
3
src/prompts/agent_system.json
Normal file
@@ -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"
|
||||
}
|
||||
3
src/prompts/ai_identity.json
Normal file
3
src/prompts/ai_identity.json
Normal file
@@ -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一言だけ挨拶してください。"
|
||||
}
|
||||
3
src/prompts/integrate.json
Normal file
3
src/prompts/integrate.json
Normal file
@@ -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は書かないでください。"
|
||||
}
|
||||
6
src/prompts/protocols.json
Normal file
6
src/prompts/protocols.json
Normal file
@@ -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."
|
||||
}
|
||||
16
src/tui.rs
16
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<AgentConfig>) -> 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"))
|
||||
|
||||
Reference in New Issue
Block a user