feat: add agent structure and improve tooling for provider
This commit is contained in:
parent
e2c966d31e
commit
b341fdb368
21 changed files with 2567 additions and 443 deletions
304
src/agent/prompt.rs
Normal file
304
src/agent/prompt.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
use crate::config::IdentityConfig;
|
||||
use crate::identity;
|
||||
use crate::skills::Skill;
|
||||
use crate::tools::Tool;
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
const BOOTSTRAP_MAX_CHARS: usize = 20_000;
|
||||
|
||||
pub struct PromptContext<'a> {
|
||||
pub workspace_dir: &'a Path,
|
||||
pub model_name: &'a str,
|
||||
pub tools: &'a [Box<dyn Tool>],
|
||||
pub skills: &'a [Skill],
|
||||
pub identity_config: Option<&'a IdentityConfig>,
|
||||
pub dispatcher_instructions: &'a str,
|
||||
}
|
||||
|
||||
pub trait PromptSection: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn build(&self, ctx: &PromptContext<'_>) -> Result<String>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SystemPromptBuilder {
|
||||
sections: Vec<Box<dyn PromptSection>>,
|
||||
}
|
||||
|
||||
impl SystemPromptBuilder {
|
||||
pub fn with_defaults() -> Self {
|
||||
Self {
|
||||
sections: vec![
|
||||
Box::new(IdentitySection),
|
||||
Box::new(ToolsSection),
|
||||
Box::new(SafetySection),
|
||||
Box::new(SkillsSection),
|
||||
Box::new(WorkspaceSection),
|
||||
Box::new(DateTimeSection),
|
||||
Box::new(RuntimeSection),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_section(mut self, section: Box<dyn PromptSection>) -> Self {
|
||||
self.sections.push(section);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
|
||||
let mut output = String::new();
|
||||
for section in &self.sections {
|
||||
let part = section.build(ctx)?;
|
||||
if part.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
output.push_str(part.trim_end());
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IdentitySection;
|
||||
pub struct ToolsSection;
|
||||
pub struct SafetySection;
|
||||
pub struct SkillsSection;
|
||||
pub struct WorkspaceSection;
|
||||
pub struct RuntimeSection;
|
||||
pub struct DateTimeSection;
|
||||
|
||||
impl PromptSection for IdentitySection {
|
||||
fn name(&self) -> &str {
|
||||
"identity"
|
||||
}
|
||||
|
||||
fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
|
||||
let mut prompt = String::from("## Project Context\n\n");
|
||||
if let Some(config) = ctx.identity_config {
|
||||
if identity::is_aieos_configured(config) {
|
||||
if let Ok(Some(aieos)) = identity::load_aieos_identity(config, ctx.workspace_dir) {
|
||||
let rendered = identity::aieos_to_system_prompt(&aieos);
|
||||
if !rendered.is_empty() {
|
||||
prompt.push_str(&rendered);
|
||||
return Ok(prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt.push_str(
|
||||
"The following workspace files define your identity, behavior, and context.\n\n",
|
||||
);
|
||||
for file in [
|
||||
"AGENTS.md",
|
||||
"SOUL.md",
|
||||
"TOOLS.md",
|
||||
"IDENTITY.md",
|
||||
"USER.md",
|
||||
"HEARTBEAT.md",
|
||||
"BOOTSTRAP.md",
|
||||
"MEMORY.md",
|
||||
] {
|
||||
inject_workspace_file(&mut prompt, ctx.workspace_dir, file);
|
||||
}
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptSection for ToolsSection {
|
||||
fn name(&self) -> &str {
|
||||
"tools"
|
||||
}
|
||||
|
||||
fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
|
||||
let mut out = String::from("## Tools\n\n");
|
||||
for tool in ctx.tools {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"- **{}**: {}\n Parameters: `{}`",
|
||||
tool.name(),
|
||||
tool.description(),
|
||||
tool.parameters_schema()
|
||||
);
|
||||
}
|
||||
if !ctx.dispatcher_instructions.is_empty() {
|
||||
out.push('\n');
|
||||
out.push_str(ctx.dispatcher_instructions);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptSection for SafetySection {
|
||||
fn name(&self) -> &str {
|
||||
"safety"
|
||||
}
|
||||
|
||||
fn build(&self, _ctx: &PromptContext<'_>) -> Result<String> {
|
||||
Ok("## Safety\n\n- Do not exfiltrate private data.\n- Do not run destructive commands without asking.\n- Do not bypass oversight or approval mechanisms.\n- Prefer `trash` over `rm`.\n- When in doubt, ask before acting externally.".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptSection for SkillsSection {
|
||||
fn name(&self) -> &str {
|
||||
"skills"
|
||||
}
|
||||
|
||||
fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
|
||||
if ctx.skills.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let mut prompt = String::from("## Available Skills\n\n<available_skills>\n");
|
||||
for skill in ctx.skills {
|
||||
let location = skill.location.clone().unwrap_or_else(|| {
|
||||
ctx.workspace_dir
|
||||
.join("skills")
|
||||
.join(&skill.name)
|
||||
.join("SKILL.md")
|
||||
});
|
||||
let _ = writeln!(
|
||||
prompt,
|
||||
" <skill>\n <name>{}</name>\n <description>{}</description>\n <location>{}</location>\n </skill>",
|
||||
skill.name,
|
||||
skill.description,
|
||||
location.display()
|
||||
);
|
||||
}
|
||||
prompt.push_str("</available_skills>");
|
||||
Ok(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptSection for WorkspaceSection {
|
||||
fn name(&self) -> &str {
|
||||
"workspace"
|
||||
}
|
||||
|
||||
fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
|
||||
Ok(format!(
|
||||
"## Workspace\n\nWorking directory: `{}`",
|
||||
ctx.workspace_dir.display()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptSection for RuntimeSection {
|
||||
fn name(&self) -> &str {
|
||||
"runtime"
|
||||
}
|
||||
|
||||
fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
|
||||
let host =
|
||||
hostname::get().map_or_else(|_| "unknown".into(), |h| h.to_string_lossy().to_string());
|
||||
Ok(format!(
|
||||
"## Runtime\n\nHost: {host} | OS: {} | Model: {}",
|
||||
std::env::consts::OS,
|
||||
ctx.model_name
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptSection for DateTimeSection {
|
||||
fn name(&self) -> &str {
|
||||
"datetime"
|
||||
}
|
||||
|
||||
fn build(&self, _ctx: &PromptContext<'_>) -> Result<String> {
|
||||
let now = Local::now();
|
||||
Ok(format!(
|
||||
"## Current Date & Time\n\nTimezone: {}",
|
||||
now.format("%Z")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_workspace_file(prompt: &mut String, workspace_dir: &Path, filename: &str) {
|
||||
let path = workspace_dir.join(filename);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
let trimmed = content.trim();
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
let _ = writeln!(prompt, "### {filename}\n");
|
||||
let truncated = if trimmed.chars().count() > BOOTSTRAP_MAX_CHARS {
|
||||
trimmed
|
||||
.char_indices()
|
||||
.nth(BOOTSTRAP_MAX_CHARS)
|
||||
.map(|(idx, _)| &trimmed[..idx])
|
||||
.unwrap_or(trimmed)
|
||||
} else {
|
||||
trimmed
|
||||
};
|
||||
prompt.push_str(truncated);
|
||||
if truncated.len() < trimmed.len() {
|
||||
let _ = writeln!(
|
||||
prompt,
|
||||
"\n\n[... truncated at {BOOTSTRAP_MAX_CHARS} chars — use `read` for full file]\n"
|
||||
);
|
||||
} else {
|
||||
prompt.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = writeln!(prompt, "### {filename}\n\n[File not found: {filename}]\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tools::traits::Tool;
|
||||
use async_trait::async_trait;
|
||||
|
||||
struct TestTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TestTool {
|
||||
fn name(&self) -> &str {
|
||||
"test_tool"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"tool desc"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({"type": "object"})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_args: serde_json::Value,
|
||||
) -> anyhow::Result<crate::tools::ToolResult> {
|
||||
Ok(crate::tools::ToolResult {
|
||||
success: true,
|
||||
output: "ok".into(),
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_builder_assembles_sections() {
|
||||
let tools: Vec<Box<dyn Tool>> = vec![Box::new(TestTool)];
|
||||
let ctx = PromptContext {
|
||||
workspace_dir: Path::new("/tmp"),
|
||||
model_name: "test-model",
|
||||
tools: &tools,
|
||||
skills: &[],
|
||||
identity_config: None,
|
||||
dispatcher_instructions: "instr",
|
||||
};
|
||||
let prompt = SystemPromptBuilder::with_defaults().build(&ctx).unwrap();
|
||||
assert!(prompt.contains("## Tools"));
|
||||
assert!(prompt.contains("test_tool"));
|
||||
assert!(prompt.contains("instr"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue