1use crate::settings::WorkflowSize;
11
12pub fn workflow_trigger(text: &str) -> Option<&'static str> {
17 let lowered = text.to_lowercase();
18 let words = lowered
19 .split(|character: char| !(character.is_alphanumeric() || character == '_'))
20 .filter(|word| !word.is_empty())
21 .collect::<Vec<_>>();
22 for (phrase, phrase_words) in [
23 ("use a workflow", &["use", "a", "workflow"][..]),
24 ("run a workflow", &["run", "a", "workflow"][..]),
25 ("as a workflow", &["as", "a", "workflow"][..]),
26 ("dynamic workflow", &["dynamic", "workflow"][..]),
27 ] {
28 if words
29 .windows(phrase_words.len())
30 .any(|window| window == phrase_words)
31 {
32 return Some(phrase);
33 }
34 }
35 words.contains(&"ultracode").then_some("ultracode")
36}
37
38pub(crate) const FAN_OUT_EXAMPLE: &str = r#"export const meta = {
40 name: 'audit-tools',
41 description: 'Audit every tool file for missing path checks',
42 phases: [{ title: 'Discover' }, { title: 'Audit' }, { title: 'Report' }],
43}
44
45phase('Discover')
46const found = await agent('List every .rs file under crates/kiss-coding/src/tools. Return the paths.', {
47 schema: {
48 type: 'object',
49 required: ['files'],
50 properties: { files: { type: 'array', items: { type: 'string' } } },
51 },
52})
53log(`auditing ${found.files.length} files`)
54
55phase('Audit')
56const findings = await pipeline(
57 found.files,
58 file => agent(`Audit ${file} for paths used without validation. Report each one.`, { label: file }),
59 finding => agent(`Try to refute this finding. Reply "confirmed" or "refuted" and why.\n\n${finding}`),
60)
61
62phase('Report')
63const kept = findings.filter(Boolean)
64return await agent(`Merge these audits into one ranked list, most severe first.\n\n${kept.join('\n\n')}`)
65"#;
66
67pub(crate) const PERSPECTIVES_EXAMPLE: &str = r#"export const meta = {
69 name: 'three-angles',
70 description: 'Draft a plan from three angles, then choose between them',
71 phases: [{ title: 'Draft' }, { title: 'Choose' }],
72}
73
74phase('Draft')
75const drafts = await parallel([
76 () => agent(`Plan this work for correctness above all.\n\n${args}`, { label: 'correctness' }),
77 () => agent(`Plan this work for the smallest change.\n\n${args}`, { label: 'smallest' }),
78 () => agent(`Plan this work for long-term maintenance.\n\n${args}`, { label: 'maintenance' }),
79])
80
81phase('Choose')
82const usable = drafts.filter(Boolean)
83if (usable.length === 0) {
84 return 'every draft failed'
85}
86return await agent(`Weigh these plans against each other and recommend one.\n\n${usable.join('\n\n---\n\n')}`)
87"#;
88
89pub fn authoring_prompt(size: WorkflowSize, max_agents: u32, max_fanout: usize) -> String {
91 let size_advice = match size.target_agents() {
92 Some(target) => format!(
93 "Aim for fewer than {target} agents unless the task clearly needs more. \
94 This is guidance, not a limit."
95 ),
96 None => {
97 "Size the workflow to the task because no agent-count guideline is set.".to_string()
98 }
99 };
100
101 format!(
102 "# Writing a dynamic workflow\n\
103 \n\
104 The user asked for this task to run as a dynamic workflow. Call the `run_workflow` tool \
105 with a script that orchestrates child agents, instead of working through the task turn by \
106 turn yourself.\n\
107 \n\
108 Write the script once and let it run. Its intermediate results stay in script variables \
109 rather than in your context, which is what lets one run coordinate far more agents than a \
110 conversation can. {size_advice}\n\
111 \n\
112 ## The language\n\
113 \n\
114 A script is a small, fixed subset of JavaScript. It is not JavaScript: anything outside \
115 this list is an error.\n\
116 \n\
117 Statements: `const`, `let`, assignment, `if` / `else`, `for (const item of list)`, \
118 `while`, `break`, `continue`, `return`, and an expression on its own. Top-level `await` \
119 is allowed.\n\
120 \n\
121 Expressions: numbers, strings, back-quoted template strings with `${{...}}`, `true`, \
122 `false`, `null`, array and object literals, `.field`, `[index]`, calls, arrow functions, \
123 `await`, `!`, unary `-`, `+ - * / %`, `=== !==`, `< <= > >=`, `&& || ??`, and `a ? b : c`.\n\
124 \n\
125 Not available, with what to use instead:\n\
126 - counted `for` loops, `++`, `--`: use `for (const item of list)` or `pipeline(...)`\n\
127 - `==`, `!=`: use `===` and `!==`\n\
128 - `function`, `class`, `var`: use `const` and arrow functions\n\
129 - `try` / `catch` / `throw`: an agent that fails returns null, so test for null instead\n\
130 - `import`, `require`: a script loads no modules and touches no files\n\
131 - `typeof`, `instanceof`: use `Array.isArray(value)` or `value === null`\n\
132 - `...` spread, `?.`, destructuring, computed object keys\n\
133 - `Date.now()`, `Math.random()`, `new Date()`: a script must be repeatable so a stopped \
134 run can resume. Pass a timestamp or a seed in through `args`.\n\
135 \n\
136 ## What a script can call\n\
137 \n\
138 - `agent(prompt, options?)` starts one child agent and waits for its answer.\n\
139 - `parallel(tasks)` runs an array of zero-argument functions at once and returns their \
140 results in input order.\n\
141 - `pipeline(items, ...stages)` sends every item through each stage in turn, with items \
142 processed at the same time and results in input order.\n\
143 - `phase(title)` names the group the agents after it belong to, for the progress view.\n\
144 - `log(message)` shows one line above the phases in the progress view.\n\
145 - `args` is the input the workflow was invoked with. `cwd` is the working directory.\n\
146 - `JSON.stringify`, `JSON.parse`, `Object.keys`, `Object.values`, `Object.entries`, \
147 `Array.isArray`, `Math.min`, `Math.max`, `Math.floor`, `Math.ceil`, `Math.abs`, \
148 `Math.round`, `Number`, `String`, `Boolean`, `parseInt`, `parseFloat`, `isNaN`.\n\
149 - On arrays: `length`, `map`, `filter`, `find`, `some`, `every`, `slice`, `join`, `push`, \
150 `includes`, `indexOf`, `concat`, `flat`, `reverse`, `sort`.\n\
151 - On strings: `length`, `split`, `trim`, `toLowerCase`, `toUpperCase`, `includes`, \
152 `indexOf`, `startsWith`, `endsWith`, `slice`, `replace`, `replaceAll`, `padStart`, \
153 `padEnd`.\n\
154 \n\
155 `agent()` options, all optional: `label` for the progress view, `phase` to override the \
156 current phase, `model` such as `sonnet` or `haiku`, `effort` such as `low` or `high`, \
157 `schema` for a JSON Schema the answer must match, `timeoutMs`, and `retries`.\n\
158 \n\
159 ## Two rules that decide whether a script works\n\
160 \n\
161 First, `agent()` returns **null** when that agent is stopped or fails. `parallel` and \
162 `pipeline` keep those nulls in place so the results line up with the input. Always drop \
163 them before using the results, with `.filter(Boolean)`, and never read a field off an \
164 agent result without knowing it is not null.\n\
165 \n\
166 Second, `agent()` returns **text** unless you pass a `schema`. Pass a schema whenever the \
167 script needs to read fields or iterate a list out of the answer. Without one you get a \
168 string and `.files` on it is an error.\n\
169 \n\
170 ## Limits\n\
171 \n\
172 Up to {max_agents} agents in one run, up to {max_fanout} items in a single `parallel` or \
173 `pipeline` call, and up to 16 agents running at once. A script that exceeds one of these \
174 fails rather than quietly doing less.\n\
175 \n\
176 ## Write a good workflow, not just a big one\n\
177 \n\
178 The value of a workflow is the pattern, not the agent count. Prefer shapes that produce a \
179 more trustworthy answer than one pass would: have independent agents check or try to \
180 refute each other's findings, draft from several angles and weigh them, or repeat a \
181 check-and-fix round until it stops making progress. Give each agent one bounded task and \
182 enough context to do it without guessing.\n\
183 \n\
184 Agents share one working directory. Two agents told to edit the same file will conflict, \
185 so give parallel writers separate files, or fan out for reading and keep the writing in \
186 one place.\n\
187 \n\
188 ## Example: fan out over files, then verify\n\
189 \n\
190 {FAN_OUT_EXAMPLE}\n\
191 ## Example: several angles, then choose\n\
192 \n\
193 {PERSPECTIVES_EXAMPLE}\n\
194 Give `run_workflow` a short kebab-case `name`, a one-sentence `description`, and the \
195 `script`. If the script does not parse, the error names the line and a supported \
196 alternative: fix it and call the tool again.\n"
197 )
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn both_examples_parse_with_the_real_parser() {
206 for (name, source) in [
210 ("fan out", FAN_OUT_EXAMPLE),
211 ("perspectives", PERSPECTIVES_EXAMPLE),
212 ] {
213 let script = kiss_workflow::Script::parse(source)
214 .unwrap_or_else(|error| panic!("the {name} example must parse: {error}"));
215 assert!(!script.meta().name.is_empty());
216 assert!(!script.meta().description.is_empty());
217 }
218 }
219
220 #[test]
221 fn the_fan_out_example_declares_the_phases_it_uses() {
222 let script = kiss_workflow::Script::parse(FAN_OUT_EXAMPLE).unwrap();
223 assert_eq!(script.declared_phases(), ["Discover", "Audit", "Report"]);
224 assert_eq!(script.estimated_agents(), None);
227 }
228
229 #[test]
230 fn the_perspectives_example_has_a_known_agent_count() {
231 let script = kiss_workflow::Script::parse(PERSPECTIVES_EXAMPLE).unwrap();
232 assert_eq!(script.estimated_agents(), Some(4));
234 }
235
236 #[test]
237 fn the_size_guideline_reaches_the_instructions() {
238 let small = authoring_prompt(WorkflowSize::Small, 1000, 4096);
239 assert!(small.contains("fewer than 5 agents"));
240
241 let unrestricted = authoring_prompt(WorkflowSize::Unrestricted, 1000, 4096);
242 assert!(unrestricted.contains("no agent-count guideline"));
243 }
244
245 #[test]
246 fn the_instructions_state_the_two_rules_that_break_scripts() {
247 let text = authoring_prompt(WorkflowSize::Medium, 1000, 4096);
248 assert!(text.contains("filter(Boolean)"));
249 assert!(text.contains("schema"));
250 assert!(text.contains("Date.now()"));
251 assert!(text.contains("${...}"));
254 }
255}