use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentInfo {
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInfo {
pub name: String,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolCatalog {
#[serde(default)]
pub agents: Vec<AgentInfo>,
#[serde(default)]
pub tools: Vec<ToolInfo>,
#[serde(default)]
pub models: Vec<String>,
}
const SCHEMA_CHEATSHEET: &str = r#"A workflow is a JSON object:
{
"id": "kebab-id", "name": "Human Name", "start": "<entry stage id>",
"goal": "the overall objective (optional; re-anchored into every step to prevent drift)",
"max_iterations": 100,
"stages": [ { "id": "s1", "name": "Stage 1", "step": <STEP> }, ... ],
"edges": [ { "from": "s1", "to": "s2", "conditions": [<COND>...], "label": "on success" }, ... ]
}
STEP is one of (tagged by "type"):
- {"type":"pattern","pattern":"<KIND>","task":"...","agents":[<AGENT_SPEC>...],"config":{}}
KIND: swarm_parallel | swarm_sequential | swarm_debate | pipeline | supervisor | delegator | map_reduce | vote | fleet | adversarial_review | tournament
adversarial_review: fresh reviewer checks prior work; agents[0]=reviewer, config={"criteria":["..."],"review_key":"stage.<id>.answer"}. Branch on the typed bool stage.<id>.review_passed.
tournament: rank candidates by pairwise judging; last agent (or config.judge_index) is the judge, the rest compete.
- {"type":"proposal","proposal":{"id":"p1","source":"builder","actions":[<ACTION>...],"context":{}}}
- {"type":"approval","prompt":"shown to the human","fields":[{"name":"decision","label":"Decision","field_type":"text","options":[],"required":true}],"output_key":"approval"}
- {"type":"sub_workflow","workflow":{<a nested workflow>}}
- {"type":"loop_until","body":<STEP>,"until":[<COND>...],"max_iterations":5} (repeat body until conditions hold or cap; body may not be approval)
- {"type":"for_each","items_from":"<state key with a JSON array>","body":<STEP>,"max_concurrent":4} ({{item}}/{{index}} substituted into the body)
AGENT_SPEC: {"name":"researcher","system_prompt":"...","tools":["search"],"max_turns":10,"metadata":{}}
ACTION: {"id":"a1","type":"tool_call","tool":"<tool name>","parameters":{},"preconditions":[],"expected_effects":{},"state_dependencies":[],"idempotent":false,"max_retries":3,"failure_behavior":"abort","metadata":{}}
COND: {"key":"stage.<id>.succeeded","operator":"eq","value":true} (operators: eq, neq, exists, not_exists, gt, lt, gte, lte, contains)
RULES:
- Every edge `from`/`to` and the `start` must reference an existing stage id.
- Every reachable path must terminate (a stage with no outgoing edge ends the run).
- Approval stages MUST set a non-empty "output_key" and may NOT be used as a compensation handler.
- After an approval gate, branch on the human's answer with edge conditions like {"key":"approval.decision","operator":"eq","value":"approve"}.
- Prefer reusing the listed agents/tools; do not invent tool names that aren't listed unless the goal clearly needs a new one.
"#;
pub fn build_prompt(req: &crate::BuildRequest, prior_issues: &[String]) -> String {
let mut p = String::new();
p.push_str(
"You design deterministic multi-stage agent workflows for the Common Agent Runtime. \
Output EXACTLY ONE JSON object for the workflow described below — no prose, no markdown fences.\n\n",
);
p.push_str("# Goal\n");
p.push_str(req.goal.trim());
p.push_str("\n\n");
if !req.catalog.agents.is_empty() {
p.push_str("# Available agents (reuse by id)\n");
for a in &req.catalog.agents {
p.push_str(&format!("- {} ({}): {}\n", a.id, a.name, a.description));
}
p.push('\n');
}
if !req.catalog.tools.is_empty() {
p.push_str("# Available tools\n");
for t in &req.catalog.tools {
p.push_str(&format!("- {}: {}\n", t.name, t.description));
}
p.push('\n');
}
if !req.catalog.models.is_empty() {
p.push_str("# Available models\n");
p.push_str(&req.catalog.models.join(", "));
p.push_str("\n\n");
}
if let Some(existing) = &req.existing {
p.push_str(
"# Existing workflow to UPDATE\nApply the goal as targeted edits. \
Preserve existing stage ids where the stage is kept.\n",
);
if let Ok(json) = serde_json::to_string_pretty(existing) {
p.push_str(&json);
p.push_str("\n\n");
}
}
if let Some(fb) = &req.feedback {
if !fb.trim().is_empty() {
p.push_str("# Revision requested by the user\n");
p.push_str(fb.trim());
p.push_str("\n\n");
}
}
if !prior_issues.is_empty() {
p.push_str(
"# Your previous attempt FAILED validation. Fix exactly these issues and return the corrected workflow:\n",
);
for issue in prior_issues {
p.push_str(&format!("- {issue}\n"));
}
p.push('\n');
}
p.push_str("# Workflow schema\n");
p.push_str(SCHEMA_CHEATSHEET);
p.push_str("\nReturn ONLY the workflow JSON object now.");
p
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BuildRequest;
fn req() -> BuildRequest {
BuildRequest {
goal: "research a stock then have a human approve a summary".into(),
catalog: ToolCatalog {
agents: vec![AgentInfo {
id: "agent_researcher".into(),
name: "Researcher".into(),
description: "Finds information".into(),
}],
tools: vec![ToolInfo {
name: "web_search".into(),
description: "search the web".into(),
}],
models: vec!["qwen3".into()],
},
existing: None,
feedback: None,
max_attempts: 3,
}
}
#[test]
fn prompt_includes_goal_catalog_and_schema() {
let p = build_prompt(&req(), &[]);
assert!(p.contains("research a stock"));
assert!(p.contains("agent_researcher"));
assert!(p.contains("web_search"));
assert!(p.contains("qwen3"));
assert!(p.contains("\"type\":\"approval\""));
assert!(p.contains("output_key"));
}
#[test]
fn prior_issues_are_fed_back_for_repair() {
let p = build_prompt(&req(), &["s2: edge to 'ghost' references unknown stage".into()]);
assert!(p.contains("FAILED validation"));
assert!(p.contains("ghost"));
}
#[test]
fn feedback_and_existing_render_update_path() {
let mut r = req();
r.feedback = Some("add a verification stage before approval".into());
let p = build_prompt(&r, &[]);
assert!(p.contains("Revision requested"));
assert!(p.contains("verification stage"));
}
}