use swarm_engine_core::agent::{
ActionCandidate, ContextTarget, ManagerInstruction, ResolvedContext, WorkerCtx,
WorkerDecisionRequest,
};
use swarm_engine_core::types::WorkerId;
use crate::json_prompt::action_selection_template;
#[derive(Debug, Clone, Default)]
pub struct PromptBuilder;
impl PromptBuilder {
pub fn new() -> Self {
Self
}
pub fn build(&self, context: &ResolvedContext) -> String {
let content = self.build_content_sections(context);
action_selection_template().build(&content)
}
fn build_content_sections(&self, context: &ResolvedContext) -> String {
let mut sections = Vec::new();
sections.push(self.format_task(context));
if let Some(ref instruction) = context.manager_instruction {
sections.push(self.format_manager_instruction(instruction));
}
if let Some(ref output) = context.self_last_output {
sections.push(format!("## Last Result\n{}", output));
}
let worker_id = match &context.target {
ContextTarget::Worker(id) => Some(*id),
ContextTarget::Manager(_) => None,
};
if !context.visible_workers.is_empty() {
if let Some(wid) = worker_id {
if let Some(my_ctx) = context.visible_workers.iter().find(|w| w.id == wid) {
sections.push(self.format_your_status(my_ctx));
}
}
let others: Vec<_> = context
.visible_workers
.iter()
.filter(|w| worker_id != Some(w.id))
.collect();
if !others.is_empty() {
sections.push(self.format_team_status(&others, &context.escalations));
}
}
sections.push(self.format_candidates(&context.candidates));
sections.push("Required fields: tool, target, args, confidence".to_string());
sections.join("\n\n")
}
pub fn to_request(&self, context: &ResolvedContext) -> WorkerDecisionRequest {
let worker_id = match context.target {
ContextTarget::Worker(id) => id,
ContextTarget::Manager(id) => WorkerId(id.0), };
let prompt = self.build(context);
WorkerDecisionRequest {
worker_id,
query: prompt,
context: context.clone(),
lora: None,
}
}
fn format_task(&self, context: &ResolvedContext) -> String {
let mut lines = Vec::new();
lines.push("## Task".to_string());
if let Some(ref task) = context.global.task_description {
lines.push(task.clone());
} else {
lines.push("Continue current work".to_string());
}
lines.push(format!(
"Progress: {:.1}% | Tick: {}/{}",
context.global.progress * 100.0,
context.global.tick,
context.global.max_ticks,
));
if let Some(ref hint) = context.global.hint {
lines.push(format!("Hint: {}", hint));
}
lines.join("\n")
}
fn format_manager_instruction(&self, instruction: &ManagerInstruction) -> String {
let mut lines = Vec::new();
lines.push("## Manager's Instruction".to_string());
if let Some(ref text) = instruction.instruction {
lines.push(text.clone());
}
if let Some(ref action) = instruction.suggested_action {
if let Some(ref target) = instruction.suggested_target {
lines.push(format!("Suggested: {} -> {}", action, target));
} else {
lines.push(format!("Suggested: {}", action));
}
}
if let Some(ref hint) = instruction.exploration_hint {
lines.push(format!("Exploration: {}", hint));
}
lines.join("\n")
}
fn format_your_status(&self, ctx: &WorkerCtx) -> String {
let mut lines = Vec::new();
lines.push("## Your Status".to_string());
lines.push(format!("Worker {} (you)", ctx.id.0));
let status = if ctx.has_escalation {
"ESCALATED"
} else {
"active"
};
lines.push(format!(" Status: {}", status));
if ctx.consecutive_failures > 0 {
lines.push(format!(
" Consecutive Failures: {} (consider different approach)",
ctx.consecutive_failures
));
}
if let Some(ref action) = ctx.last_action {
let result = ctx
.last_success
.map_or("unknown", |s| if s { "SUCCESS" } else { "FAILED" });
lines.push(format!(" Last Action: {} -> {}", action, result));
}
if let Some(output) = ctx.metadata.get("last_output") {
if let Some(output_str) = output.as_str() {
let truncated = if output_str.len() > 500 {
format!("{}...(truncated)", &output_str[..500])
} else {
output_str.to_string()
};
lines.push(format!(" Last Result: {}", truncated));
}
}
lines.push(format!(" Actions Taken: {}", ctx.history_len));
lines.join("\n")
}
fn format_team_status(
&self,
others: &[&WorkerCtx],
escalations: &[(WorkerId, swarm_engine_core::state::Escalation)],
) -> String {
let mut lines = Vec::new();
lines.push("## Team Status".to_string());
for ctx in others {
lines.push(self.format_worker_brief(ctx));
}
if !escalations.is_empty() {
lines.push(String::new());
lines.push("** Escalations (need attention) **".to_string());
for (wid, esc) in escalations {
lines.push(format!(" Worker {}: {:?}", wid.0, esc.reason));
}
}
lines.join("\n")
}
fn format_worker_brief(&self, ctx: &WorkerCtx) -> String {
let status = if ctx.has_escalation { "ESC" } else { "ok" };
let last = ctx.last_action.as_deref().unwrap_or("idle");
let result = ctx.last_success.map_or("", |s| if s { "+" } else { "-" });
format!(
"Worker {}: [{}] last={}{} failures={}",
ctx.id.0, status, last, result, ctx.consecutive_failures
)
}
fn format_candidates(&self, candidates: &[ActionCandidate]) -> String {
let mut lines = Vec::new();
lines.push("## Available Actions".to_string());
if candidates.is_empty() {
lines.push("No actions available".to_string());
return lines.join("\n");
}
for c in candidates {
let params_str = if c.params.is_empty() {
String::new()
} else {
let params: Vec<String> = c
.params
.iter()
.map(|p| {
let req = if p.required { " (required)" } else { "" };
format!(" - {}: {}{}", p.name, p.description, req)
})
.collect();
format!("\n params:\n{}", params.join("\n"))
};
let example_str = c
.example
.as_ref()
.map_or(String::new(), |ex| format!("\n example: {}", ex));
lines.push(format!(
"- {}: {}{}{}",
c.name, c.description, params_str, example_str
));
}
lines.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
use swarm_engine_core::agent::{ActionParam, GlobalContext};
fn create_test_candidates() -> Vec<ActionCandidate> {
vec![
ActionCandidate {
name: "Read".to_string(),
description: "Read a file".to_string(),
params: vec![ActionParam {
name: "path".to_string(),
description: "File path to read".to_string(),
required: true,
}],
example: None,
},
ActionCandidate {
name: "Grep".to_string(),
description: "Search for pattern".to_string(),
params: vec![ActionParam {
name: "pattern".to_string(),
description: "Search pattern".to_string(),
required: true,
}],
example: None,
},
]
}
fn create_minimal_context() -> ResolvedContext {
let global = GlobalContext::new(10)
.with_max_ticks(100)
.with_progress(0.5)
.with_task("Find the bug in authentication module");
ResolvedContext::new(global, ContextTarget::Worker(WorkerId(0)))
.with_self_last_output(Some("Found 3 files matching pattern".to_string()))
.with_candidates(create_test_candidates())
}
fn create_detailed_context() -> ResolvedContext {
let global = GlobalContext::new(10)
.with_max_ticks(100)
.with_progress(0.5)
.with_task("Find the bug in authentication module");
let worker0 = WorkerCtx::new(WorkerId(0))
.with_last_action("read:src/auth.rs", true)
.with_history_len(5);
let worker1 = WorkerCtx::new(WorkerId(1))
.with_last_action("grep:error", false)
.with_failures(2)
.with_escalation(true);
ResolvedContext::new(global, ContextTarget::Worker(WorkerId(0)))
.with_workers(vec![worker0, worker1])
.with_candidates(create_test_candidates())
}
#[test]
fn test_build_minimal_prompt() {
let builder = PromptBuilder::new();
let context = create_minimal_context();
let prompt = builder.build(&context);
assert!(prompt.contains("## Task"));
assert!(prompt.contains("Find the bug"));
assert!(prompt.contains("Progress: 50.0%"));
assert!(prompt.contains("## Last Result"));
assert!(prompt.contains("Found 3 files matching pattern"));
assert!(!prompt.contains("## Your Status"));
assert!(!prompt.contains("## Team Status"));
assert!(prompt.contains("## Available Actions"));
assert!(prompt.contains("- Read: Read a file"));
}
#[test]
fn test_build_detailed_prompt() {
let builder = PromptBuilder::new();
let context = create_detailed_context();
let prompt = builder.build(&context);
assert!(prompt.contains("## Task"));
assert!(prompt.contains("## Your Status"));
assert!(prompt.contains("Worker 0 (you)"));
assert!(prompt.contains("## Team Status"));
assert!(prompt.contains("Worker 1"));
assert!(prompt.contains("ESC")); }
#[test]
fn test_to_request() {
let builder = PromptBuilder::new();
let context = create_minimal_context();
let request = builder.to_request(&context);
assert_eq!(request.worker_id, WorkerId(0));
assert!(!request.query.is_empty());
assert_eq!(request.context.candidates.len(), 2);
}
#[test]
fn test_format_candidates_empty() {
let builder = PromptBuilder::new();
let result = builder.format_candidates(&[]);
assert!(result.contains("No actions available"));
}
}