1use super::artifacts::Artifacts;
11use super::children::{ChildKind, Children};
12use super::events::{Event, kinds};
13use super::timers::Timers;
14use crate::config::v2::{RunUntil, Settings};
15use crate::context::memory::Memory;
16use crate::context::{Contexts, skills, tokens};
17use crate::engine::{RunState, RunStatus, Workflow};
18use crate::governor::Governor;
19use crate::mcp::client::McpClient;
20use crate::obs::log::Logger;
21use crate::registry::Registry;
22use crate::state::{Durable, InboxEvent, Kind, now_ms};
23use crate::subagent::protocol::AgentMsg;
24use crate::supervisor::reap::Reaped;
25use crate::supervisor::tree::NodeId;
26use serde_json::{Value, json};
27use std::collections::{BTreeMap, VecDeque};
28use std::sync::Arc;
29use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
30use std::time::{Duration, Instant};
31
32pub const TICK: Duration = Duration::from_millis(200);
34pub const ABANDON_GRACE: Duration = Duration::from_secs(3);
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum Target {
40 Child(NodeId, u64),
42 Step(String, String),
44}
45
46#[derive(Debug, Clone)]
48pub struct PendingTool {
49 pub target: Target,
50 pub name: String,
51 pub kind: PendingKind,
52 pub started_ms: u64,
53}
54
55#[derive(Debug, Clone)]
56pub enum PendingKind {
57 Timer { id: String },
59 Subagent { handle: String },
61 Think { child: NodeId },
63 Run { run: String, deadline_ms: u64 },
65 Await { condition: String, deadline_ms: u64 },
67 Human {
72 task: String,
73 question: String,
74 deadline_ms: u64,
75 standalone: bool,
78 auto_fired: bool,
80 },
81}
82
83#[derive(Debug, Clone)]
86pub struct TurnJob {
87 pub ctx: String,
88 pub event: Option<String>,
90 pub principal: Option<String>,
91 pub message: Option<crate::context::Msg>,
94 pub skills: Vec<String>,
96 pub text: String,
98 pub preflight_done: bool,
100 pub knowledge_done: bool,
102 pub knowledge: Option<String>,
104}
105
106impl TurnJob {
107 pub fn new(
108 ctx: String,
109 event: Option<String>,
110 principal: Option<String>,
111 message: Option<crate::context::Msg>,
112 skills: Vec<String>,
113 text: String,
114 ) -> TurnJob {
115 TurnJob {
116 ctx,
117 event,
118 principal,
119 message,
120 skills,
121 text,
122 preflight_done: false,
123 knowledge_done: false,
124 knowledge: None,
125 }
126 }
127}
128
129#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
131pub struct SubagentRecord {
132 pub handle: String,
133 pub instruction: String,
134 pub mode: String,
135 pub status: String,
136 #[serde(default)]
137 pub attempt: u32,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub result: Option<Value>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub error: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub requested_by: Option<Value>,
144 #[serde(default)]
145 pub tokens: u64,
146 #[serde(default)]
147 pub created: u64,
148 #[serde(default)]
149 pub updated: u64,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub payload: Option<Value>,
153 #[serde(skip)]
154 pub node: Option<NodeId>,
155 #[serde(skip)]
156 pub dirty: bool,
157}
158
159#[derive(Debug, Clone)]
161pub struct Instruction {
162 pub text: String,
163 pub source: &'static str,
164 pub uri: Option<String>,
165 pub server: Option<String>,
166 pub version: u64,
167}
168
169#[derive(Debug, Default, Clone)]
171pub struct Counters {
172 pub turns: u64,
173 pub tool_calls: u64,
174 pub runs_started: u64,
175 pub runs_finished: u64,
176 pub inbox_processed: u64,
177 pub tokens_in: u64,
178 pub tokens_out: u64,
179}
180
181pub struct Runtime {
182 pub(crate) settings: Settings,
183 pub(crate) settings_doc: Value,
185 pub(crate) args: Vec<String>,
187 pub(crate) env: Vec<(String, String)>,
188 pub(crate) pinned: BTreeMap<String, Workflow>,
190 pub(crate) recent_signals: BTreeMap<String, Value>,
192 pub(crate) log: Logger,
193 pub(crate) instance: String,
194 pub(crate) run_id: String,
195 pub(crate) durable: Durable,
196 pub(crate) mcp: BTreeMap<String, Arc<McpClient>>,
197 pub(crate) mcp_specs: BTreeMap<String, crate::config::McpServerSpec>,
198 pub(crate) registry: Registry,
199 pub(crate) contexts: Contexts,
200 pub(crate) memory: Memory,
201 pub(crate) artifacts: Artifacts,
202 pub(crate) skills: skills::Catalogue,
203 pub(crate) governor: Governor,
204 pub(crate) workflows: BTreeMap<String, Workflow>,
205 pub(crate) runs: BTreeMap<String, RunState>,
206 pub(crate) children: Children,
207 pub(crate) timers: Timers,
208 pub(crate) events_rx: Receiver<Event>,
209 pub(crate) events_tx: Sender<Event>,
210 pub(crate) child_rx: Receiver<(NodeId, AgentMsg)>,
211 pub(crate) reap_rx: Receiver<Reaped>,
212 pub(crate) pending: Vec<PendingTool>,
213 pub(crate) turn_queue: VecDeque<TurnJob>,
214 pub(crate) staged_turns: BTreeMap<u64, TurnJob>,
216 pub(crate) inbox_queue: VecDeque<InboxEvent>,
217 pub(crate) subagents: BTreeMap<String, SubagentRecord>,
218 pub(crate) instruction: Instruction,
219 pub(crate) job_shape: bool,
220 pub(crate) exit: Option<i32>,
221 pub(crate) draining: bool,
222 pub(crate) paused: bool,
225 pub(crate) drain_started: Option<Instant>,
226 pub(crate) drain_reason: String,
227 pub(crate) idle_since: Option<Instant>,
228 pub(crate) intel_uri: String,
229 pub(crate) intel_token: Option<String>,
230 pub(crate) intel_headers: Vec<(String, String)>,
233 pub(crate) intel_bearer: Option<std::sync::Arc<dyn Fn() -> Option<String> + Send + Sync>>,
238 pub(crate) model: String,
239 pub(crate) trace_id: Option<String>,
240 pub(crate) started: Instant,
241 pub(crate) seq: u64,
242 pub(crate) counters: Counters,
243 pub(crate) job_runs: Vec<String>,
245 pub(crate) executing: BTreeMap<String, Instant>,
247 pub(crate) last_manifest_flush: Instant,
248 pub(crate) goal_judge_at: Option<u64>,
251 #[cfg(feature = "a2a")]
253 pub(crate) tasks: BTreeMap<String, crate::a2a::Task>,
254 #[cfg(feature = "a2a")]
256 pub(crate) event_to_task: BTreeMap<String, String>,
257 #[cfg(feature = "a2a")]
259 #[cfg(feature = "a2a")]
261 pub(crate) a2a_feed: Option<std::sync::Arc<super::a2a_server::SharedFeed>>,
262 #[cfg(feature = "a2a")]
264 pub(crate) a2a_pairing: Option<std::sync::Arc<super::a2a_server::PairingState>>,
265 #[cfg(feature = "a2a")]
269 pub(crate) reserved_task_id: Option<String>,
270 #[cfg(feature = "a2a")]
272 pub(crate) a2a_sink: Option<std::sync::Arc<crate::a2a::ports::StreamSink>>,
273 #[cfg(feature = "a2a")]
275 pub(crate) a2a_listener: Option<crate::a2a::serve::Listener>,
276 pub(crate) activity: BTreeMap<u64, super::activity::Activity>,
278 pub(crate) last_root_reply: Option<String>,
281 #[cfg(feature = "a2a")]
283 pub(crate) feed_marks: BTreeMap<String, u64>,
284 #[cfg(feature = "a2a")]
286 pub(crate) feed_last: Instant,
287 #[cfg(feature = "a2a")]
290 pub(crate) webhook_callbacks: super::webhooks::SharedCallbacks,
291 #[cfg(feature = "a2a")]
293 pub(crate) webhook_sync: std::collections::HashMap<
294 String,
295 std::sync::mpsc::SyncSender<super::webhooks::WebhookReply>,
296 >,
297}
298
299impl Runtime {
300 pub(crate) fn next_id(&mut self, prefix: &str) -> String {
302 self.seq += 1;
303 format!("{prefix}-{}", self.seq)
304 }
305
306 pub fn run_loop(&mut self) -> i32 {
310 self.log.info("proc.ready", json!({"instance": self.instance, "job_shape": self.job_shape, "workflows": self.workflows.len(), "runs": self.runs.len(), "inbox_pending": self.inbox_queue.len()}));
311 loop {
312 crate::obs::health::tick();
313 while let Ok((node, msg)) = self.child_rx.try_recv() {
315 self.on_child_frame(node, msg);
316 }
317 let _ = crate::signals::take_child_exit();
319 crate::supervisor::reaper::reap_and_dispatch();
320 while let Ok(r) = self.reap_rx.try_recv() {
321 self.on_reaped(r);
322 }
323 while let Ok(ev) = self.events_rx.try_recv() {
325 self.on_event(ev);
326 }
327 let now = now_ms();
329 for t in self.timers.fire(&self.durable, now) {
330 self.on_timer(t);
331 }
332 self.process_inbox();
334 self.poll_starts();
336 self.poll_waits();
337 self.schedule_runs();
338 self.dispatch_turns();
340 self.poll_pending();
342 self.poll_mcp_notifications();
343 for (node, health) in self.children.tick() {
345 self.on_unhealthy_child(node, health);
346 }
347 self.checkpoint(false);
349 crate::obs::metrics::set_inbox_pending(self.inbox_queue.len() as u64);
350 crate::obs::metrics::set_context_tokens(self.contexts.max_est_tokens());
351 #[cfg(feature = "a2a")]
355 self.feed_tick();
356 self.check_signals();
358 if let Some(code) = self.lifecycle_step() {
359 self.shutdown(code);
360 return code;
361 }
362 crate::signals::drain_wakeup();
366 let wait = self.next_wake().min(TICK);
367 match self.events_rx.recv_timeout(wait) {
368 Ok(ev) => self.on_event(ev),
369 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => {}
370 }
371 }
372 }
373
374 fn on_event(&mut self, ev: Event) {
375 match ev {
376 Event::Child(node, msg) => self.on_child_frame(node, msg),
377 Event::Reaped(r) => self.on_reaped(r),
378 Event::StepDone {
379 run,
380 step,
381 output,
382 is_error,
383 error,
384 tokens,
385 } => self.on_step_done(&run, &step, output, is_error, error, tokens),
386 Event::ToolDone {
387 node,
388 req,
389 result,
390 is_error,
391 } => self.on_tool_done(node, req, result, is_error),
392 Event::KnowledgeDone { job, block } => self.on_knowledge_done(job, block),
393 Event::TimerFired { id, owner, payload } => self.on_timer(crate::state::TimerRecord {
394 id,
395 deadline_ms: now_ms(),
396 owner,
397 payload,
398 }),
399 Event::Inbox(ev) => self.inbox_queue.push_back(ev),
400 #[cfg(feature = "a2a")]
401 Event::A2a(req) => self.on_a2a_request(*req),
402 #[cfg(feature = "a2a")]
403 Event::Webhook(req) => self.on_webhook_request(*req),
404 Event::Background { id, result } if id == "goal.judge" => self.on_goal_judge(&result),
405 Event::Background { id, result } if id.starts_with("human.judge:") => {
406 let ask = id.trim_start_matches("human.judge:").to_string();
407 self.on_human_judge(&ask, &result);
408 }
409 Event::Background { .. } | Event::Tick => {}
410 }
411 }
412
413 pub(crate) fn accept_event(
417 &mut self,
418 kind: &str,
419 principal: Option<String>,
420 payload: Value,
421 ) -> Result<String, String> {
422 let ev = InboxEvent::new(kind, principal, payload);
423 self.durable
424 .inbox_put(&ev)
425 .map_err(|e| format!("inbox: {e}"))?;
426 let id = ev.id.clone();
427 self.log
428 .info("inbox.accepted", json!({"inbox_event": id, "kind": kind}));
429 self.inbox_queue.push_back(ev);
430 Ok(id)
431 }
432
433 fn process_inbox(&mut self) {
434 let mut batch = std::mem::take(&mut self.inbox_queue);
443 while let Some(ev) = batch.pop_front() {
444 if self.draining {
445 batch.push_front(ev);
447 break;
448 }
449 self.counters.inbox_processed += 1;
450 match ev.kind.as_str() {
451 kinds::START_FIRED | kinds::WORKFLOW_RUN => {
452 let done = self.on_start_event(&ev);
453 if done {
454 self.inbox_done(&ev.id);
455 }
456 }
457 kinds::A2A_MESSAGE => {
458 self.on_a2a_message_event(&ev);
460 }
461 kinds::SIGNAL => {
462 let name = ev.payload["name"].as_str().unwrap_or("").to_string();
463 let payload = ev.payload.get("payload").cloned().unwrap_or(Value::Null);
464 let target = ev
465 .payload
466 .get("run")
467 .and_then(Value::as_str)
468 .map(str::to_string);
469 let from = ev
470 .payload
471 .get("from")
472 .and_then(Value::as_str)
473 .map(str::to_string);
474 let delivered =
475 self.deliver_signal(&name, payload, target.as_deref(), from.as_deref());
476 self.log.info(
477 "signal.received",
478 json!({"inbox_event": ev.id, "name": name, "delivered": delivered}),
479 );
480 self.inbox_done(&ev.id);
481 }
482 other => {
483 self.log.warn(
484 "inbox.unknown_kind",
485 json!({"inbox_event": ev.id, "kind": other}),
486 );
487 self.inbox_done(&ev.id);
488 }
489 }
490 }
491 batch.append(&mut self.inbox_queue);
494 self.inbox_queue = batch;
495 }
496
497 pub(crate) fn inbox_done(&mut self, id: &str) {
498 if let Err(e) = self.durable.inbox_done(id) {
499 self.log.warn(
500 "inbox.done.fail",
501 json!({"inbox_event": id, "err": e.to_string()}),
502 );
503 }
504 }
505
506 fn on_a2a_message_event(&mut self, ev: &InboxEvent) {
509 let ctx = ev.payload["context_id"]
510 .as_str()
511 .unwrap_or("default")
512 .to_string();
513 let text = ev.payload["text"]
514 .as_str()
515 .map(str::to_string)
516 .unwrap_or_else(|| ev.payload["parts"].to_string());
517 let principal = ev.principal.clone();
518 #[cfg(feature = "a2a")]
520 if let Some(task_id) = ev.payload["task"].as_str() {
521 self.event_to_task
522 .insert(ev.id.clone(), task_id.to_string());
523 }
524 let skills = self.skills.references(&text);
525 self.turn_queue.push_back(TurnJob::new(
526 ctx,
527 Some(ev.id.clone()),
528 principal.clone(),
529 Some(crate::context::Msg::user(text.clone(), principal)),
530 skills,
531 text,
532 ));
533 }
534
535 fn on_child_frame(&mut self, node: NodeId, msg: AgentMsg) {
538 if !self.children.on_frame(node, &msg) {
539 return; }
541 match msg {
542 AgentMsg::Ready
543 | AgentMsg::Pong { .. }
544 | AgentMsg::Gate { .. }
545 | AgentMsg::GateClosed { .. } => {}
546 AgentMsg::Event { event, fields } => self.on_child_progress(node, &event, &fields),
549 AgentMsg::Usage(u) => {
550 self.counters.tokens_in += u.input_tokens;
551 self.counters.tokens_out += u.output_tokens;
552 crate::obs::metrics::record_tokens(u.input_tokens, u.output_tokens);
553 if let Some(ChildKind::Subagent { .. }) = self.children.get(node).map(|c| &c.kind) {
556 self.governor.charge(u, &[]);
557 }
558 }
559 AgentMsg::IntelHealth { all_down, .. } => {
560 if crate::signals::set_intel_all_down(all_down) {
561 self.log.warn("intel.health", json!({"all_down": all_down}));
562 }
563 }
564 AgentMsg::ToolRequest { id, name, args } => self.on_tool_request(node, id, &name, args),
565 AgentMsg::BudgetRequest { id, estimate } => self.on_budget_request(node, id, estimate),
566 AgentMsg::TurnDone { turn } => self.on_turn_done(node, *turn),
567 AgentMsg::Turn { outcome } => self.on_subagent_turn(node, outcome),
568 AgentMsg::Result { outcome } => self.on_subagent_result(node, Ok(outcome)),
569 AgentMsg::Failed { error } => {
570 let kind = self.children.get(node).map(|c| c.kind.clone());
571 match kind {
572 Some(ChildKind::Subagent { .. }) => self.on_subagent_result(node, Err(error)),
573 Some(_) => self.on_turn_failed(node, error),
574 None => {}
575 }
576 }
577 }
578 }
579
580 fn on_reaped(&mut self, r: Reaped) {
581 let Some((node, child)) = self.children.on_reaped(&r) else {
582 return;
583 };
584 self.activity_end(node);
585 self.log.info("child.exit", json!({"node": node.0, "pid": r.pid, "kind": super::children::kind_label(&child.kind), "outcome": format!("{:?}", r.outcome)}));
586 match child.kind {
588 ChildKind::StepTurn {
597 ref run,
598 ref step,
599 reservation,
600 } => {
601 let node_owned = node.0.to_string();
602 let orphaned = self
603 .runs
604 .get(run)
605 .and_then(|st| st.step(step))
606 .is_some_and(|s| {
607 s.status == crate::engine::StepStatus::Running
608 && s.worker.as_deref() == Some(node_owned.as_str())
609 });
610 if orphaned {
611 if let Some(res) = reservation {
615 self.governor.release(res);
616 }
617 self.log.warn(
618 "turn.failed",
619 json!({"node": node.0, "kind": super::children::kind_label(&child.kind), "err": "worker exited without a result"}),
620 );
621 self.on_step_turn_done(
622 run,
623 step,
624 crate::subagent::protocol::TurnResult {
625 status: "failed".into(),
626 error: Some(format!(
627 "worker exited without a result ({:?})",
628 r.outcome
629 )),
630 ..Default::default()
631 },
632 );
633 }
634 }
635 ChildKind::RootTurn { .. } | ChildKind::Think { .. } => {
644 if self.pending_turn_exists(node) {
645 self.on_turn_failed(
646 node,
647 format!("worker exited without a result ({:?})", r.outcome),
648 );
649 }
650 }
651 ChildKind::Subagent { ref handle } => {
652 if self
653 .subagents
654 .get(handle)
655 .is_some_and(|s| !is_terminal_status(&s.status))
656 {
657 self.on_subagent_result(
658 node,
659 Err(format!(
660 "subagent exited without a result ({:?})",
661 r.outcome
662 )),
663 );
664 }
665 }
666 }
667 let waiting: Vec<PendingTool> = self
669 .pending
670 .iter()
671 .filter(|p| matches!(&p.kind, PendingKind::Think { child } if *child == node))
672 .cloned()
673 .collect();
674 for p in waiting {
675 self.pending.retain(|q| q.target != p.target);
676 self.reply(
677 &p.target,
678 Value::String("think worker exited without a result".into()),
679 true,
680 );
681 }
682 }
683
684 fn on_unhealthy_child(&mut self, node: NodeId, health: crate::supervisor::liveness::Health) {
685 self.log.warn(
686 "child.unhealthy",
687 json!({"node": node.0, "health": format!("{health:?}")}),
688 );
689 self.children.cancel(node, &format!("{health:?}"));
690 let started = self
692 .children
693 .get(node)
694 .map(|c| c.started)
695 .unwrap_or_else(Instant::now);
696 if started.elapsed() > Duration::from_secs(1) {
697 self.children.kill(node);
698 }
699 }
700
701 fn check_signals(&mut self) {
704 if crate::signals::draining() && !self.draining {
705 self.begin_drain("signal");
706 }
707 if crate::signals::reload_requested() {
708 crate::signals::clear_reload();
709 self.on_reload_requested();
710 }
711 }
712
713 pub(crate) fn begin_drain(&mut self, reason: &str) {
714 if self.draining {
715 return;
716 }
717 self.draining = true;
718 self.drain_started = Some(Instant::now());
719 self.drain_reason = reason.to_string();
720 crate::signals::set_lame_duck(true);
721 self.log.info("drain.start", json!({"reason": reason, "children": self.children.len(), "runs": self.runs.values().filter(|r| !r.status.is_terminal()).count()}));
722 crate::obs::metrics::record_drain("started");
723 #[cfg(feature = "a2a")]
725 self.feed_push(
726 "lifecycle",
727 super::a2a_server::FeedVis::All,
728 json!({"draining": true, "reason": reason}),
729 );
730 self.children.begin_drain(reason);
731 }
732
733 fn lifecycle_step(&mut self) -> Option<i32> {
735 if let Some(code) = self.exit {
736 if !self.draining {
739 self.begin_drain("exit");
740 }
741 if self.children.is_empty() {
742 return Some(code);
743 }
744 }
745 if self.draining {
746 let timeout = self.settings.lifecycle.drain_timeout();
747 let started = self.drain_started.unwrap_or_else(Instant::now);
748 let force = crate::signals::force() || started.elapsed() >= timeout;
749 let done = self.children.drive_drain(force);
750 if done || started.elapsed() >= timeout + ABANDON_GRACE {
751 if !done {
752 self.log
753 .warn("drain.abandon", json!({"children": self.children.len()}));
754 self.children.abandon();
755 }
756 crate::obs::metrics::record_drain("completed");
757 self.checkpoint(true);
758 self.log
759 .info("drain.done", json!({"reason": self.drain_reason}));
760 return Some(self.exit.unwrap_or(crate::exit::SUCCESS));
761 }
762 return None;
763 }
764 let run_until = self.settings.lifecycle.run_until;
766 let job_now = self.job_shape && !self.workflows.values().any(|w| w.is_long_lived());
773 let idle_policy = match run_until {
774 RunUntil::Idle => true,
775 RunUntil::Drained => false,
776 RunUntil::Auto => job_now,
777 };
778 if !idle_policy {
779 return None;
780 }
781 let busy = self.paused || !self.children.is_empty()
783 || !self.turn_queue.is_empty()
784 || !self.staged_turns.is_empty()
785 || !self.inbox_queue.is_empty()
786 || !self.pending.is_empty()
787 || !self.executing.is_empty()
788 || self.runs.values().any(|r| !r.status.is_terminal())
789 || !self.timers.is_empty();
790 if busy {
791 self.idle_since = None;
792 return None;
793 }
794 let since = *self.idle_since.get_or_insert_with(Instant::now);
795 if since.elapsed() >= self.settings.lifecycle.idle_grace() || job_now {
796 let code = self.job_exit_code();
797 self.log.info(
798 "lifecycle.idle_exit",
799 json!({"code": code, "job_shape": self.job_shape}),
800 );
801 self.checkpoint(true);
802 return Some(code);
803 }
804 None
805 }
806
807 fn job_exit_code(&self) -> i32 {
810 let mut code = crate::exit::SUCCESS;
811 for id in &self.job_runs {
812 if let Some(r) = self.runs.get(id) {
813 let c = run_exit_code(r);
814 if c != crate::exit::SUCCESS {
815 code = c;
816 }
817 }
818 }
819 if self.job_runs.is_empty() && self.job_shape {
820 return crate::exit::SUCCESS;
822 }
823 crate::exit::apply_budget_remap(
824 code,
825 self.settings
826 .lifecycle
827 .exit_code_map
828 .get(&code.to_string())
829 .copied(),
830 )
831 }
832
833 fn shutdown(&mut self, code: i32) {
834 self.children.abandon();
835 let _ = self.durable.flush(true);
836 self.log.info("proc.exit", json!({"code": code, "uptime_ms": self.started.elapsed().as_millis() as u64, "turns": self.counters.turns, "tool_calls": self.counters.tool_calls, "runs": self.counters.runs_finished, "tokens_in": self.counters.tokens_in, "tokens_out": self.counters.tokens_out}));
837 }
838
839 pub fn job_output(&self) -> Option<Value> {
841 self.job_runs
842 .iter()
843 .rev()
844 .filter_map(|id| self.runs.get(id))
845 .find_map(|r| r.output.clone())
846 .or_else(|| self.last_root_reply.clone().map(Value::String))
849 }
850
851 pub(crate) fn checkpoint(&mut self, force: bool) {
856 let mut failed: Option<String> = None;
857 for run in self.runs.values_mut() {
858 if run.dirty {
859 crate::state::kill_point("step.before_done");
860 match self.durable.put(
861 Kind::Run,
862 &run.id,
863 serde_json::to_value(&*run).unwrap_or(Value::Null),
864 Some(run.workflow_hash.clone()),
865 ) {
866 Ok(_) => run.dirty = false,
867 Err(e) => failed = Some(format!("run {}: {e}", run.id)),
868 }
869 }
870 }
871 if let Err(e) = self.contexts.checkpoint(&self.durable) {
872 failed = Some(format!("context: {e}"));
873 }
874 for s in self.subagents.values_mut() {
875 if s.dirty {
876 match self.durable.put(
877 Kind::Subagent,
878 &s.handle,
879 serde_json::to_value(&*s).unwrap_or(Value::Null),
880 None,
881 ) {
882 Ok(_) => s.dirty = false,
883 Err(e) => failed = Some(format!("subagent {}: {e}", s.handle)),
884 }
885 }
886 }
887 let budget = self.governor.to_value();
889 self.durable.manifest_update(|m| {
890 m.budget = budget;
891 });
892 match self.durable.flush(force) {
893 Ok(_) => {}
894 Err(e) => failed = Some(format!("manifest: {e}")),
895 }
896 if let Some(e) = failed {
897 self.log.error("store.checkpoint.fail", json!({"err": e}));
898 if !self.durable.is_degraded() {
899 self.exit = Some(crate::exit::GENERIC);
901 }
902 }
903 }
904
905 pub(crate) fn status_value(&self) -> Value {
909 json!({
910 "instance": self.instance,
911 "run_id": self.run_id,
912 "uptime_ms": self.started.elapsed().as_millis() as u64,
913 "job_shape": self.job_shape,
914 "draining": self.draining,
915 "paused": self.paused,
916 "store": {"kind": self.durable.store_kind(), "degraded": self.durable.is_degraded(), "generation": self.durable.manifest().generation},
917 "workflows": self.workflows.values().map(|w| json!({"name": w.name, "hash": w.hash, "armed": w.armed, "starts": w.start_steps().iter().map(|s| s.kind.clone()).collect::<Vec<_>>()})).collect::<Vec<_>>(),
918 "runs": self.runs.values().map(RunState::summary).collect::<Vec<_>>(),
919 "conversations": self.contexts.status(),
920 "subagents": self.subagents.values().map(|s| json!({"handle": s.handle, "mode": s.mode, "status": s.status, "tokens": s.tokens})).collect::<Vec<_>>(),
921 "children": self.children.status(),
922 "timers": self.timers.status(),
923 "inbox_pending": self.inbox_queue.len(),
924 "budget": self.governor.status(now_ms()),
925 "tools": self.registry.len(),
926 "skills": self.skills.names(),
927 "counters": {"turns": self.counters.turns, "tool_calls": self.counters.tool_calls, "runs_started": self.counters.runs_started, "runs_finished": self.counters.runs_finished, "tokens_in": self.counters.tokens_in, "tokens_out": self.counters.tokens_out},
928 "instruction": {"source": self.instruction.source, "uri": self.instruction.uri, "version": self.instruction.version, "bytes": self.instruction.text.len()},
929 "model": self.model,
930 "activity": self.activity_value(),
931 })
932 }
933
934 fn next_wake(&self) -> Duration {
939 let now = now_ms();
940 let mut soonest = now + 200;
941 if let Some(t) = self.timers.next_deadline() {
942 soonest = soonest.min(t);
943 }
944 for st in self.durable.manifest().starts.values() {
945 for k in ["next_ms", "debounce_until"] {
946 if let Some(n) = st[k].as_u64() {
947 soonest = soonest.min(n);
948 }
949 }
950 }
951 for run in self.runs.values() {
952 if run.status.is_terminal() {
953 continue;
954 }
955 for step in run.steps.values() {
956 if let Some(w) = &step.wait
957 && let Some(d) = w["deadline_ms"].as_u64()
958 {
959 soonest = soonest.min(d);
960 }
961 }
962 }
963 if !self.pending.is_empty() || !self.turn_queue.is_empty() {
964 soonest = soonest.min(now + 50);
965 }
966 Duration::from_millis(soonest.saturating_sub(now).max(5))
967 }
968
969 pub(crate) fn model_window(&self) -> u64 {
972 self.settings
973 .context
974 .model_window
975 .unwrap_or_else(|| tokens::window_for_model(&self.model))
976 }
977}
978
979pub(crate) fn is_terminal_status(s: &str) -> bool {
980 matches!(
981 s,
982 "completed" | "failed" | "cancelled" | "refused" | "killed" | "crashed"
983 )
984}
985
986pub fn run_exit_code(r: &RunState) -> i32 {
988 match r.status {
989 RunStatus::Completed => crate::exit::SUCCESS,
990 RunStatus::Refused => crate::exit::REFUSED,
991 RunStatus::Stalled => crate::exit::PARTIAL,
992 RunStatus::Failed => {
993 let e = r.error.as_deref().unwrap_or("");
994 if e.contains("exhausted") || e.contains("budget") {
995 crate::exit::BUDGET
996 } else if e.contains("deadline") {
997 crate::exit::DEADLINE
998 } else if e.contains("intel") {
999 crate::exit::INTEL_UNAVAILABLE
1000 } else {
1001 crate::exit::GENERIC
1002 }
1003 }
1004 RunStatus::Cancelled => crate::exit::GENERIC,
1005 _ => crate::exit::PARTIAL,
1006 }
1007}