2
0

refactor(prompt): extract agent/AI prompts and protocol config into dedicated modules

This commit is contained in:
2026-03-24 18:32:45 +09:00
parent 4a47542f7a
commit 54eb61a4f3
15 changed files with 168 additions and 58 deletions

View File

@@ -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();

View File

@@ -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,
};
}

View File

@@ -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));

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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(),
}
}

View 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"
}

View 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一言だけ挨拶してください。"
}

View 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は書かないでください。"
}

View 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."
}

View File

@@ -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"))