Skip to main content

car_builder/
prompt.rs

1//! Prompt construction: turn a goal + catalog (+ optional existing workflow,
2//! revision feedback, and prior validation issues) into the instruction the
3//! model answers with a single workflow JSON object.
4
5use serde::{Deserialize, Serialize};
6
7/// An agent the builder may wire into a workflow stage.
8#[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/// A tool a proposal/agent stage may call.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ToolInfo {
20    pub name: String,
21    #[serde(default)]
22    pub description: String,
23}
24
25/// What the builder knows is available to compose into a workflow.
26#[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
36/// The car-workflow schema, taught to the model so it emits a parseable,
37/// verifiable manifest. Kept compact but complete — these are the only stage
38/// types and the exact field names the runtime accepts.
39const 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
71/// Build the full instruction for one generation attempt.
72pub 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    // Catalog
85    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    // Update path: show the existing definition and ask for targeted edits.
106    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    // Human revision feedback (from an approve/revise loop).
118    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    // Repair feedback: prior attempt's validation errors.
127    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}