1mod prompt;
11mod runner;
12mod store;
13mod tool;
14
15pub use prompt::{authoring_prompt, workflow_trigger};
16pub use store::{SaveLocation, SavedWorkflow, discover, save};
17
18pub use kiss_workflow::{
21 AgentId, AgentOutcome, AgentSnapshot, AgentStatus, PhaseSnapshot, RunSnapshot, RunStatus,
22 Script,
23};
24
25use crate::session_runner::{AgentSession, WorkflowTurnStatus};
26use kiss_agent::DynTool;
27use kiss_workflow::{Limits, Workflow};
28use serde_json::Value;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::sync::{Arc, Mutex, Weak};
31use std::time::Duration;
32use tokio_util::sync::CancellationToken;
33
34pub type RunId = u64;
36
37const MAX_RETAINED_RUNS: usize = 20;
40
41pub struct RunRecord {
43 pub id: RunId,
44 pub name: String,
45 pub description: String,
46 pub source: Arc<str>,
48 workflow: Arc<Workflow>,
49}
50
51impl RunRecord {
52 pub fn snapshot(&self) -> Arc<RunSnapshot> {
53 self.workflow.snapshot()
54 }
55
56 pub fn subscribe(&self) -> tokio::sync::watch::Receiver<u64> {
57 self.workflow.subscribe()
58 }
59
60 pub fn pause(&self) {
61 self.workflow.pause();
62 }
63
64 pub fn resume(&self) {
65 self.workflow.resume();
66 }
67
68 pub fn is_paused(&self) -> bool {
69 self.workflow.is_paused()
70 }
71
72 pub fn stop(&self) {
73 self.workflow.stop();
74 }
75
76 pub fn stop_agent(&self, agent: kiss_workflow::AgentId) {
77 self.workflow.stop_agent(agent);
78 }
79
80 pub fn restart_agent(&self, agent: kiss_workflow::AgentId) {
81 self.workflow.restart_agent(agent);
82 }
83}
84
85pub fn report_text(value: Value) -> String {
87 match value {
88 Value::String(text) => text,
89 Value::Null => "The workflow returned nothing.".to_string(),
90 other => serde_json::to_string_pretty(&other).unwrap_or_else(|_| other.to_string()),
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct RunSummary {
97 pub id: RunId,
98 pub name: String,
99 pub description: String,
100 pub status: RunStatus,
101 pub total_agents: usize,
102 pub finished_agents: usize,
103 pub tokens: u64,
104 pub elapsed: Duration,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum ApprovalDecision {
110 Approve,
111 Cancel,
112}
113
114#[derive(Debug, Clone)]
116pub struct WorkflowPlan {
117 pub name: String,
118 pub description: String,
119 pub phases: Vec<String>,
120 pub estimated_agents: Option<u32>,
122 pub source: Arc<str>,
123}
124
125impl WorkflowPlan {
126 pub fn from_script(script: &Script) -> WorkflowPlan {
127 WorkflowPlan {
128 name: script.meta().name.clone(),
129 description: script.meta().description.clone(),
130 phases: script.declared_phases().to_vec(),
131 estimated_agents: script.estimated_agents(),
132 source: Arc::from(script.source()),
133 }
134 }
135
136 pub fn agent_estimate(&self) -> String {
142 match self.estimated_agents {
143 Some(1) => "1 agent".into(),
144 Some(count) => format!("{count} agents"),
145 None => "an unbounded number of agents".into(),
146 }
147 }
148}
149
150pub type WorkflowApprover = Arc<
153 dyn Fn(WorkflowPlan) -> futures::future::BoxFuture<'static, ApprovalDecision> + Send + Sync,
154>;
155
156pub struct WorkflowRuntime {
158 parent: Weak<AgentSession>,
159 runs: Mutex<Vec<Arc<RunRecord>>>,
160 next_id: AtomicU64,
161}
162
163impl WorkflowRuntime {
164 pub(crate) fn new(parent: Weak<AgentSession>) -> Arc<WorkflowRuntime> {
165 Arc::new(WorkflowRuntime {
166 parent,
167 runs: Mutex::new(Vec::new()),
168 next_id: AtomicU64::new(1),
169 })
170 }
171
172 pub(crate) fn tool(self: &Arc<Self>) -> DynTool {
174 Arc::new(tool::RunWorkflowTool::new(self.clone()))
175 }
176
177 pub(crate) fn emit_outcome(
178 &self,
179 run: Option<RunId>,
180 name: String,
181 status: WorkflowTurnStatus,
182 ) {
183 if let Some(parent) = self.parent.upgrade() {
184 parent.emit_workflow_outcome(run, name, status);
185 }
186 }
187
188 pub async fn approve(&self, plan: WorkflowPlan) -> ApprovalDecision {
194 let Some(parent) = self.parent.upgrade() else {
195 return ApprovalDecision::Cancel;
196 };
197 if !parent.settings().workflows.confirm {
198 return ApprovalDecision::Approve;
199 }
200 match parent.workflow_approver() {
201 Some(approver) => approver(plan).await,
202 None => ApprovalDecision::Approve,
203 }
204 }
205
206 pub fn prepare(
211 &self,
212 plan: &WorkflowPlan,
213 script: Script,
214 args: Value,
215 ) -> anyhow::Result<Arc<RunRecord>> {
216 let parent = self
217 .parent
218 .upgrade()
219 .ok_or_else(|| anyhow::anyhow!("the parent session has closed"))?;
220 let limits = Limits::default();
221 let runner = Arc::new(runner::SessionAgentRunner::new(
222 self.parent.clone(),
223 limits.max_concurrency,
224 ));
225 let cwd = parent
226 .manager
227 .lock()
228 .map(|manager| manager.cwd().display().to_string())
229 .unwrap_or_default();
230
231 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
232 let name = plan.name.clone();
233 let description = plan.description.clone();
234 let source: Arc<str> = Arc::from(script.source());
235 let workflow = Arc::new(Workflow::new(script, args, cwd, runner, limits));
236 let record = Arc::new(RunRecord {
237 id,
238 name,
239 description,
240 source,
241 workflow,
242 });
243
244 let mut updates = record.subscribe();
248 let parent_for_updates = self.parent.clone();
249 let record_for_updates = record.clone();
250 tokio::spawn(async move {
251 while updates.changed().await.is_ok() {
252 let version = *updates.borrow_and_update();
253 let Some(parent) = parent_for_updates.upgrade() else {
254 break;
255 };
256 parent.emit_workflow(id, version);
257 if record_for_updates.snapshot().status.is_finished() {
258 break;
259 }
260 }
261 });
262
263 let mut runs = self
264 .runs
265 .lock()
266 .map_err(|_| anyhow::anyhow!("the workflow list is unavailable"))?;
267 runs.push(record.clone());
268 while runs.len() > MAX_RETAINED_RUNS {
270 let Some(position) = runs
271 .iter()
272 .position(|run| run.snapshot().status.is_finished())
273 else {
274 break;
275 };
276 runs.remove(position);
277 }
278 Ok(record)
279 }
280
281 pub async fn run(
285 &self,
286 record: &Arc<RunRecord>,
287 stop: CancellationToken,
288 ) -> Result<Value, String> {
289 let stopping = record.clone();
290 let guard = tokio::spawn(async move {
291 stop.cancelled().await;
292 stopping.stop();
293 });
294 let outcome = record
295 .workflow
296 .run()
297 .await
298 .map_err(|error| error.to_string());
299 guard.abort();
300 outcome
301 }
302
303 pub fn get(&self, id: RunId) -> Option<Arc<RunRecord>> {
304 self.runs
305 .lock()
306 .ok()?
307 .iter()
308 .find(|run| run.id == id)
309 .cloned()
310 }
311
312 pub fn summaries(&self) -> Vec<RunSummary> {
314 let Ok(runs) = self.runs.lock() else {
315 return Vec::new();
316 };
317 runs.iter()
318 .map(|run| {
319 let snapshot = run.snapshot();
320 RunSummary {
321 id: run.id,
322 name: run.name.clone(),
323 description: run.description.clone(),
324 status: snapshot.status,
325 total_agents: snapshot.total_agents(),
326 finished_agents: snapshot.finished_agents(),
327 tokens: snapshot.tokens,
328 elapsed: snapshot.elapsed,
329 }
330 })
331 .collect()
332 }
333
334 pub fn active(&self) -> Option<Arc<RunRecord>> {
336 let runs = self.runs.lock().ok()?;
337 runs.iter()
338 .rev()
339 .find(|run| !run.snapshot().status.is_finished())
340 .cloned()
341 }
342
343 pub fn latest(&self) -> Option<Arc<RunRecord>> {
345 self.runs.lock().ok()?.last().cloned()
346 }
347
348 pub fn is_empty(&self) -> bool {
349 self.runs.lock().map(|runs| runs.is_empty()).unwrap_or(true)
350 }
351
352 pub(crate) fn stop_all(&self) {
355 let Ok(runs) = self.runs.lock() else {
356 return;
357 };
358 for run in runs.iter() {
359 if !run.snapshot().status.is_finished() {
360 run.stop();
361 }
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn an_agent_estimate_never_guesses_a_number_it_does_not_know() {
372 let plan = |estimated_agents| WorkflowPlan {
373 name: "x".into(),
374 description: "y".into(),
375 phases: Vec::new(),
376 estimated_agents,
377 source: Arc::from(""),
378 };
379 assert_eq!(plan(Some(1)).agent_estimate(), "1 agent");
380 assert_eq!(plan(Some(12)).agent_estimate(), "12 agents");
381 assert_eq!(plan(None).agent_estimate(), "an unbounded number of agents");
382 }
383}