1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AgentInfo {
10 pub id: String,
11 #[serde(default)]
12 pub name: String,
13 #[serde(default)]
14 pub description: String,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ToolInfo {
20 pub name: String,
21 #[serde(default)]
22 pub description: String,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
27pub struct ToolCatalog {
28 #[serde(default)]
29 pub agents: Vec<AgentInfo>,
30 #[serde(default)]
31 pub tools: Vec<ToolInfo>,
32 #[serde(default)]
33 pub models: Vec<String>,
34}
35
36const SCHEMA_CHEATSHEET: &str = r#"A workflow is a JSON object:
40{
41 "id": "kebab-id", "name": "Human Name", "start": "<entry stage id>",
42 "goal": "the overall objective (optional; re-anchored into every step to prevent drift)",
43 "max_iterations": 100,
44 "stages": [ { "id": "s1", "name": "Stage 1", "step": <STEP> }, ... ],
45 "edges": [ { "from": "s1", "to": "s2", "conditions": [<COND>...], "label": "on success" }, ... ]
46}
47
48STEP is one of (tagged by "type"):
49- {"type":"pattern","pattern":"<KIND>","task":"...","agents":[<AGENT_SPEC>...],"config":{}}
50 KIND: swarm_parallel | swarm_sequential | swarm_debate | pipeline | supervisor | delegator | map_reduce | vote | fleet | adversarial_review | tournament
51 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.
52 tournament: rank candidates by pairwise judging; last agent (or config.judge_index) is the judge, the rest compete.
53- {"type":"proposal","proposal":{"id":"p1","source":"builder","actions":[<ACTION>...],"context":{}}}
54- {"type":"approval","prompt":"shown to the human","fields":[{"name":"decision","label":"Decision","field_type":"text","options":[],"required":true}],"output_key":"approval"}
55- {"type":"sub_workflow","workflow":{<a nested workflow>}}
56- {"type":"loop_until","body":<STEP>,"until":[<COND>...],"max_iterations":5} (repeat body until conditions hold or cap; body may not be approval)
57- {"type":"for_each","items_from":"<state key with a JSON array>","body":<STEP>,"max_concurrent":4} ({{item}}/{{index}} substituted into the body)
58
59AGENT_SPEC: {"name":"researcher","system_prompt":"...","tools":["search"],"max_turns":10,"metadata":{}}
60ACTION: {"id":"a1","type":"tool_call","tool":"<tool name>","parameters":{},"preconditions":[],"expected_effects":{},"state_dependencies":[],"idempotent":false,"max_retries":3,"failure_behavior":"abort","metadata":{}}
61COND: {"key":"stage.<id>.succeeded","operator":"eq","value":true} (operators: eq, neq, exists, not_exists, gt, lt, gte, lte, contains)
62
63RULES:
64- Every edge `from`/`to` and the `start` must reference an existing stage id.
65- Every reachable path must terminate (a stage with no outgoing edge ends the run).
66- Approval stages MUST set a non-empty "output_key" and may NOT be used as a compensation handler.
67- After an approval gate, branch on the human's answer with edge conditions like {"key":"approval.decision","operator":"eq","value":"approve"}.
68- Prefer reusing the listed agents/tools; do not invent tool names that aren't listed unless the goal clearly needs a new one.
69"#;
70
71pub fn build_prompt(req: &crate::BuildRequest, prior_issues: &[String]) -> String {
73 let mut p = String::new();
74
75 p.push_str(
76 "You design deterministic multi-stage agent workflows for the Common Agent Runtime. \
77 Output EXACTLY ONE JSON object for the workflow described below — no prose, no markdown fences.\n\n",
78 );
79
80 p.push_str("# Goal\n");
81 p.push_str(req.goal.trim());
82 p.push_str("\n\n");
83
84 if !req.catalog.agents.is_empty() {
86 p.push_str("# Available agents (reuse by id)\n");
87 for a in &req.catalog.agents {
88 p.push_str(&format!("- {} ({}): {}\n", a.id, a.name, a.description));
89 }
90 p.push('\n');
91 }
92 if !req.catalog.tools.is_empty() {
93 p.push_str("# Available tools\n");
94 for t in &req.catalog.tools {
95 p.push_str(&format!("- {}: {}\n", t.name, t.description));
96 }
97 p.push('\n');
98 }
99 if !req.catalog.models.is_empty() {
100 p.push_str("# Available models\n");
101 p.push_str(&req.catalog.models.join(", "));
102 p.push_str("\n\n");
103 }
104
105 if let Some(existing) = &req.existing {
107 p.push_str(
108 "# Existing workflow to UPDATE\nApply the goal as targeted edits. \
109 Preserve existing stage ids where the stage is kept.\n",
110 );
111 if let Ok(json) = serde_json::to_string_pretty(existing) {
112 p.push_str(&json);
113 p.push_str("\n\n");
114 }
115 }
116
117 if let Some(fb) = &req.feedback {
119 if !fb.trim().is_empty() {
120 p.push_str("# Revision requested by the user\n");
121 p.push_str(fb.trim());
122 p.push_str("\n\n");
123 }
124 }
125
126 if !prior_issues.is_empty() {
128 p.push_str(
129 "# Your previous attempt FAILED validation. Fix exactly these issues and return the corrected workflow:\n",
130 );
131 for issue in prior_issues {
132 p.push_str(&format!("- {issue}\n"));
133 }
134 p.push('\n');
135 }
136
137 p.push_str("# Workflow schema\n");
138 p.push_str(SCHEMA_CHEATSHEET);
139 p.push_str("\nReturn ONLY the workflow JSON object now.");
140 p
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::BuildRequest;
147
148 fn req() -> BuildRequest {
149 BuildRequest {
150 goal: "research a stock then have a human approve a summary".into(),
151 catalog: ToolCatalog {
152 agents: vec![AgentInfo {
153 id: "agent_researcher".into(),
154 name: "Researcher".into(),
155 description: "Finds information".into(),
156 }],
157 tools: vec![ToolInfo {
158 name: "web_search".into(),
159 description: "search the web".into(),
160 }],
161 models: vec!["qwen3".into()],
162 },
163 existing: None,
164 feedback: None,
165 max_attempts: 3,
166 }
167 }
168
169 #[test]
170 fn prompt_includes_goal_catalog_and_schema() {
171 let p = build_prompt(&req(), &[]);
172 assert!(p.contains("research a stock"));
173 assert!(p.contains("agent_researcher"));
174 assert!(p.contains("web_search"));
175 assert!(p.contains("qwen3"));
176 assert!(p.contains("\"type\":\"approval\""));
177 assert!(p.contains("output_key"));
178 }
179
180 #[test]
181 fn prior_issues_are_fed_back_for_repair() {
182 let p = build_prompt(&req(), &["s2: edge to 'ghost' references unknown stage".into()]);
183 assert!(p.contains("FAILED validation"));
184 assert!(p.contains("ghost"));
185 }
186
187 #[test]
188 fn feedback_and_existing_render_update_path() {
189 let mut r = req();
190 r.feedback = Some("add a verification stage before approval".into());
191 let p = build_prompt(&r, &[]);
192 assert!(p.contains("Revision requested"));
193 assert!(p.contains("verification stage"));
194 }
195}