1use std::collections::BTreeMap;
7use std::sync::atomic::{AtomicU64, AtomicU8, AtomicUsize, Ordering};
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use microclaw_core::run_protocol::{
12 AgentProfile, RunId, RunRequest, RunResult, RunStatus, RuntimeCapabilities, RuntimeControl,
13 RuntimeError, RuntimeErrorCode, SessionId, WorkerDescriptor, WorkerHealth, WorkerHealthStatus,
14 WorkerId,
15};
16use microclaw_core::runtime_event::{RuntimeEvent, RuntimeEventEnvelope};
17use tokio::sync::{mpsc, oneshot, Notify, Semaphore};
18
19mod remote;
20
21pub use remote::{RemoteWorker, RemoteWorkerOptions, WorkerConnection, WorkerTransport};
22
23struct PendingControl {
24 control: RuntimeControl,
25 receipt: Option<oneshot::Sender<Result<(), RuntimeError>>>,
26}
27
28pub struct ControlRequest {
33 control: RuntimeControl,
34 receipt: Option<oneshot::Sender<Result<(), RuntimeError>>>,
35}
36
37impl ControlRequest {
38 pub fn control(&self) -> &RuntimeControl {
39 &self.control
40 }
41
42 pub fn into_control(self) -> RuntimeControl {
43 let control = self.control.clone();
44 self.accept();
45 control
46 }
47
48 pub fn accept(mut self) {
49 if let Some(receipt) = self.receipt.take() {
50 let _ = receipt.send(Ok(()));
51 }
52 }
53
54 pub fn reject(mut self, error: RuntimeError) {
55 if let Some(receipt) = self.receipt.take() {
56 let _ = receipt.send(Err(error));
57 }
58 }
59}
60
61impl Drop for ControlRequest {
62 fn drop(&mut self) {
63 if let Some(receipt) = self.receipt.take() {
64 let _ = receipt.send(Err(RuntimeError {
65 code: RuntimeErrorCode::Unavailable,
66 message: "executor did not acknowledge the control request".into(),
67 retryable: false,
68 }));
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub struct ExecutionResult {
75 pub session_id: SessionId,
76 pub final_text: String,
77 pub metadata: BTreeMap<String, serde_json::Value>,
78}
79
80impl ExecutionResult {
81 pub fn new(session_id: impl Into<SessionId>, final_text: impl Into<String>) -> Self {
82 Self {
83 session_id: session_id.into(),
84 final_text: final_text.into(),
85 metadata: BTreeMap::new(),
86 }
87 }
88}
89
90pub struct ExecutionContext {
92 run_id: RunId,
93 event_tx: mpsc::UnboundedSender<RuntimeEventEnvelope>,
94 control_rx: mpsc::UnboundedReceiver<PendingControl>,
95 next_sequence: Arc<AtomicU64>,
96}
97
98impl ExecutionContext {
99 pub fn run_id(&self) -> &RunId {
100 &self.run_id
101 }
102
103 pub fn emit(&self, event: RuntimeEvent) -> Result<(), RuntimeError> {
104 let sequence = self.next_sequence.fetch_add(1, Ordering::Relaxed);
105 self.event_tx
106 .send(RuntimeEventEnvelope::new(
107 self.run_id.to_string(),
108 sequence,
109 event,
110 ))
111 .map_err(|_| RuntimeError {
112 code: RuntimeErrorCode::Unavailable,
113 message: "runtime event consumer disconnected".into(),
114 retryable: false,
115 })
116 }
117
118 pub fn try_next_control(&mut self) -> Option<RuntimeControl> {
119 self.try_next_control_request()
120 .map(ControlRequest::into_control)
121 }
122
123 pub async fn next_control(&mut self) -> Option<RuntimeControl> {
124 self.next_control_request()
125 .await
126 .map(ControlRequest::into_control)
127 }
128
129 pub fn try_next_control_request(&mut self) -> Option<ControlRequest> {
130 self.control_rx
131 .try_recv()
132 .ok()
133 .map(|pending| ControlRequest {
134 control: pending.control,
135 receipt: pending.receipt,
136 })
137 }
138
139 pub async fn next_control_request(&mut self) -> Option<ControlRequest> {
140 self.control_rx.recv().await.map(|pending| ControlRequest {
141 control: pending.control,
142 receipt: pending.receipt,
143 })
144 }
145}
146
147#[async_trait]
152pub trait RunExecutor: Send + Sync + 'static {
153 async fn execute(
154 &self,
155 profile: AgentProfile,
156 request: RunRequest,
157 context: ExecutionContext,
158 ) -> Result<ExecutionResult, RuntimeError>;
159
160 fn capabilities(&self) -> RuntimeCapabilities {
161 RuntimeCapabilities::default()
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum RuntimeBuildError {
167 MissingExecutor,
168 InvalidConcurrency,
169}
170
171impl std::fmt::Display for RuntimeBuildError {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 match self {
174 Self::MissingExecutor => f.write_str("a run executor is required"),
175 Self::InvalidConcurrency => f.write_str("max concurrent runs must be greater than 0"),
176 }
177 }
178}
179
180impl std::error::Error for RuntimeBuildError {}
181
182pub struct RuntimeBuilder {
183 executor: Option<Arc<dyn RunExecutor>>,
184 max_concurrent_runs: usize,
185}
186
187impl Default for RuntimeBuilder {
188 fn default() -> Self {
189 Self {
190 executor: None,
191 max_concurrent_runs: 4,
192 }
193 }
194}
195
196impl RuntimeBuilder {
197 pub fn executor(mut self, executor: impl RunExecutor) -> Self {
198 self.executor = Some(Arc::new(executor));
199 self
200 }
201
202 pub fn shared_executor(mut self, executor: Arc<dyn RunExecutor>) -> Self {
203 self.executor = Some(executor);
204 self
205 }
206
207 pub fn max_concurrent_runs(mut self, max: usize) -> Self {
208 self.max_concurrent_runs = max;
209 self
210 }
211
212 pub fn build(self) -> Result<Runtime, RuntimeBuildError> {
213 if self.max_concurrent_runs == 0 {
214 return Err(RuntimeBuildError::InvalidConcurrency);
215 }
216 let executor = self.executor.ok_or(RuntimeBuildError::MissingExecutor)?;
217 Ok(Runtime {
218 inner: Arc::new(RuntimeInner {
219 executor,
220 permits: Arc::new(Semaphore::new(self.max_concurrent_runs)),
221 max_concurrent_runs: self.max_concurrent_runs,
222 active_runs: Arc::new(AtomicUsize::new(0)),
223 queued_runs: AtomicUsize::new(0),
224 idle: Notify::new(),
225 }),
226 })
227 }
228}
229
230struct RuntimeInner {
231 executor: Arc<dyn RunExecutor>,
232 permits: Arc<Semaphore>,
233 max_concurrent_runs: usize,
234 active_runs: Arc<AtomicUsize>,
235 queued_runs: AtomicUsize,
236 idle: Notify,
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct RuntimeStats {
241 pub active_runs: usize,
242 pub queued_runs: usize,
243 pub max_concurrent_runs: usize,
244 pub shutting_down: bool,
245}
246
247#[derive(Clone)]
248pub struct Runtime {
249 inner: Arc<RuntimeInner>,
250}
251
252impl Runtime {
253 pub fn builder() -> RuntimeBuilder {
254 RuntimeBuilder::default()
255 }
256
257 pub fn agent(&self, profile: AgentProfile) -> AgentHandle {
258 AgentHandle {
259 runtime: self.clone(),
260 profile,
261 }
262 }
263
264 pub fn capabilities(&self) -> RuntimeCapabilities {
265 self.inner.executor.capabilities()
266 }
267
268 pub fn max_concurrent_runs(&self) -> usize {
269 self.inner.max_concurrent_runs
270 }
271
272 pub fn active_runs(&self) -> usize {
273 self.inner.active_runs.load(Ordering::Acquire)
274 }
275
276 pub fn queued_runs(&self) -> usize {
277 self.inner.queued_runs.load(Ordering::Acquire)
278 }
279
280 pub fn stats(&self) -> RuntimeStats {
281 RuntimeStats {
282 active_runs: self.active_runs(),
283 queued_runs: self.queued_runs(),
284 max_concurrent_runs: self.max_concurrent_runs(),
285 shutting_down: self.is_shutting_down(),
286 }
287 }
288
289 pub async fn shutdown(&self) {
291 self.inner.permits.close();
292 self.wait_for_idle().await;
293 }
294
295 pub fn is_shutting_down(&self) -> bool {
297 self.inner.permits.is_closed()
298 }
299
300 pub async fn wait_for_idle(&self) {
301 loop {
302 let notified = self.inner.idle.notified();
303 if self.active_runs() == 0 && self.queued_runs() == 0 {
304 return;
305 }
306 notified.await;
307 }
308 }
309}
310
311#[derive(Clone)]
312pub struct AgentHandle {
313 runtime: Runtime,
314 profile: AgentProfile,
315}
316
317impl AgentHandle {
318 pub fn profile(&self) -> &AgentProfile {
319 &self.profile
320 }
321
322 pub fn run(&self, request: RunRequest) -> RunHandle {
323 let run_id = request
324 .run_id
325 .clone()
326 .unwrap_or_else(|| RunId::new(uuid::Uuid::new_v4().to_string()));
327 let (event_tx, event_rx) = mpsc::unbounded_channel();
328 let (control_tx, control_rx) = mpsc::unbounded_channel();
329 let (result_tx, result_rx) = oneshot::channel();
330 let next_sequence = Arc::new(AtomicU64::new(0));
331 let executor = self.runtime.inner.executor.clone();
332 let permits = self.runtime.inner.permits.clone();
333 let active_runs = self.runtime.inner.active_runs.clone();
334 let runtime_inner = self.runtime.inner.clone();
335 let profile = self.profile.clone();
336 let task_run_id = run_id.clone();
337 let fallback_session_id = request
338 .session_id
339 .clone()
340 .unwrap_or_else(|| SessionId::new(task_run_id.to_string()));
341
342 runtime_inner.queued_runs.fetch_add(1, Ordering::AcqRel);
343 tokio::spawn(async move {
344 let permit = match permits.acquire_owned().await {
345 Ok(permit) => permit,
346 Err(_) => {
347 runtime_inner.queued_runs.fetch_sub(1, Ordering::AcqRel);
348 runtime_inner.idle.notify_waiters();
349 let _ = result_tx.send(RunResult {
350 run_id: task_run_id,
351 session_id: fallback_session_id,
352 status: RunStatus::Failed,
353 final_text: String::new(),
354 error: Some(RuntimeError {
355 code: RuntimeErrorCode::Unavailable,
356 message: "runtime is shutting down".into(),
357 retryable: true,
358 }),
359 metadata: BTreeMap::new(),
360 });
361 return;
362 }
363 };
364 runtime_inner.queued_runs.fetch_sub(1, Ordering::AcqRel);
365 let _active_run = ActiveRunGuard::new(active_runs, runtime_inner.clone());
366 let context = ExecutionContext {
367 run_id: task_run_id.clone(),
368 event_tx: event_tx.clone(),
369 control_rx,
370 next_sequence: next_sequence.clone(),
371 };
372 let result = executor.execute(profile, request, context).await;
373 drop(permit);
374
375 let terminal = match result {
376 Ok(output) => {
377 emit_terminal_event(
378 &event_tx,
379 &next_sequence,
380 &task_run_id,
381 RuntimeEvent::FinalResponse {
382 text: output.final_text.clone(),
383 },
384 );
385 RunResult {
386 run_id: task_run_id,
387 session_id: output.session_id,
388 status: RunStatus::Completed,
389 final_text: output.final_text,
390 error: None,
391 metadata: output.metadata,
392 }
393 }
394 Err(error) => {
395 let status = match error.code {
396 RuntimeErrorCode::Cancelled => RunStatus::Cancelled,
397 RuntimeErrorCode::TimedOut => RunStatus::TimedOut,
398 _ => RunStatus::Failed,
399 };
400 if status == RunStatus::Cancelled {
401 emit_terminal_event(
402 &event_tx,
403 &next_sequence,
404 &task_run_id,
405 RuntimeEvent::Cancelled {
406 final_text: error.message.clone(),
407 },
408 );
409 }
410 RunResult {
411 run_id: task_run_id,
412 session_id: fallback_session_id,
413 status,
414 final_text: String::new(),
415 error: Some(error),
416 metadata: BTreeMap::new(),
417 }
418 }
419 };
420 let _ = result_tx.send(terminal);
421 });
422
423 RunHandle {
424 run_id,
425 events: event_rx,
426 control_tx,
427 result_rx,
428 }
429 }
430}
431
432struct ActiveRunGuard {
433 active_runs: Arc<AtomicUsize>,
434 runtime: Arc<RuntimeInner>,
435}
436
437impl ActiveRunGuard {
438 fn new(active_runs: Arc<AtomicUsize>, runtime: Arc<RuntimeInner>) -> Self {
439 active_runs.fetch_add(1, Ordering::AcqRel);
440 Self {
441 active_runs,
442 runtime,
443 }
444 }
445}
446
447impl Drop for ActiveRunGuard {
448 fn drop(&mut self) {
449 self.active_runs.fetch_sub(1, Ordering::AcqRel);
450 self.runtime.idle.notify_waiters();
451 }
452}
453
454fn emit_terminal_event(
455 event_tx: &mpsc::UnboundedSender<RuntimeEventEnvelope>,
456 next_sequence: &AtomicU64,
457 run_id: &RunId,
458 event: RuntimeEvent,
459) {
460 let sequence = next_sequence.fetch_add(1, Ordering::Relaxed);
461 let _ = event_tx.send(RuntimeEventEnvelope::new(
462 run_id.to_string(),
463 sequence,
464 event,
465 ));
466}
467
468pub struct RunHandle {
469 run_id: RunId,
470 events: mpsc::UnboundedReceiver<RuntimeEventEnvelope>,
471 control_tx: mpsc::UnboundedSender<PendingControl>,
472 result_rx: oneshot::Receiver<RunResult>,
473}
474
475impl RunHandle {
476 pub fn id(&self) -> &RunId {
477 &self.run_id
478 }
479
480 pub async fn next_event(&mut self) -> Option<RuntimeEventEnvelope> {
481 self.events.recv().await
482 }
483
484 pub fn try_next_event(&mut self) -> Option<RuntimeEventEnvelope> {
485 self.events.try_recv().ok()
486 }
487
488 pub fn cancel(&self) -> Result<(), RuntimeError> {
489 self.controller().cancel()
490 }
491
492 pub fn controller(&self) -> RunController {
493 RunController {
494 run_id: self.run_id.clone(),
495 control_tx: self.control_tx.clone(),
496 }
497 }
498
499 pub async fn cancel_confirmed(&self) -> Result<(), RuntimeError> {
500 self.controller().cancel_confirmed().await
501 }
502
503 pub fn steer(&self, message: impl Into<String>) -> Result<(), RuntimeError> {
504 self.controller().steer(message)
505 }
506
507 pub async fn steer_confirmed(&self, message: impl Into<String>) -> Result<(), RuntimeError> {
508 self.controller().steer_confirmed(message).await
509 }
510
511 pub fn resolve_approval(
512 &self,
513 approval_id: impl Into<String>,
514 decision: impl Into<String>,
515 ) -> Result<(), RuntimeError> {
516 self.controller().resolve_approval(approval_id, decision)
517 }
518
519 pub async fn resolve_approval_confirmed(
520 &self,
521 approval_id: impl Into<String>,
522 decision: impl Into<String>,
523 ) -> Result<(), RuntimeError> {
524 self.controller()
525 .resolve_approval_confirmed(approval_id, decision)
526 .await
527 }
528
529 pub async fn result(self) -> Result<RunResult, RuntimeError> {
530 self.result_rx.await.map_err(|_| RuntimeError {
531 code: RuntimeErrorCode::Internal,
532 message: "run ended without a terminal result".into(),
533 retryable: false,
534 })
535 }
536}
537
538#[derive(Clone)]
543pub struct RunController {
544 run_id: RunId,
545 control_tx: mpsc::UnboundedSender<PendingControl>,
546}
547
548impl RunController {
549 pub fn id(&self) -> &RunId {
550 &self.run_id
551 }
552
553 pub fn cancel(&self) -> Result<(), RuntimeError> {
554 self.send_control(RuntimeControl::Cancel {
555 run_id: self.run_id.clone(),
556 })
557 }
558
559 pub fn steer(&self, message: impl Into<String>) -> Result<(), RuntimeError> {
560 self.send_control(RuntimeControl::Steer {
561 run_id: self.run_id.clone(),
562 message: message.into(),
563 })
564 }
565
566 pub fn resolve_approval(
567 &self,
568 approval_id: impl Into<String>,
569 decision: impl Into<String>,
570 ) -> Result<(), RuntimeError> {
571 self.send_control(RuntimeControl::ResolveApproval {
572 run_id: self.run_id.clone(),
573 approval_id: approval_id.into(),
574 decision: decision.into(),
575 })
576 }
577
578 fn send_control(&self, control: RuntimeControl) -> Result<(), RuntimeError> {
579 self.control_tx
580 .send(PendingControl {
581 control,
582 receipt: None,
583 })
584 .map_err(|_| RuntimeError {
585 code: RuntimeErrorCode::Unavailable,
586 message: "run is no longer accepting control messages".into(),
587 retryable: false,
588 })
589 }
590
591 pub async fn cancel_confirmed(&self) -> Result<(), RuntimeError> {
592 self.send_control_confirmed(RuntimeControl::Cancel {
593 run_id: self.run_id.clone(),
594 })
595 .await
596 }
597
598 pub async fn steer_confirmed(&self, message: impl Into<String>) -> Result<(), RuntimeError> {
599 self.send_control_confirmed(RuntimeControl::Steer {
600 run_id: self.run_id.clone(),
601 message: message.into(),
602 })
603 .await
604 }
605
606 pub async fn resolve_approval_confirmed(
607 &self,
608 approval_id: impl Into<String>,
609 decision: impl Into<String>,
610 ) -> Result<(), RuntimeError> {
611 self.send_control_confirmed(RuntimeControl::ResolveApproval {
612 run_id: self.run_id.clone(),
613 approval_id: approval_id.into(),
614 decision: decision.into(),
615 })
616 .await
617 }
618
619 async fn send_control_confirmed(&self, control: RuntimeControl) -> Result<(), RuntimeError> {
620 let (receipt_tx, receipt_rx) = oneshot::channel();
621 self.control_tx
622 .send(PendingControl {
623 control,
624 receipt: Some(receipt_tx),
625 })
626 .map_err(|_| RuntimeError {
627 code: RuntimeErrorCode::Unavailable,
628 message: "run is no longer accepting control messages".into(),
629 retryable: false,
630 })?;
631 receipt_rx.await.map_err(|_| RuntimeError {
632 code: RuntimeErrorCode::Unavailable,
633 message: "run ended before acknowledging the control request".into(),
634 retryable: false,
635 })?
636 }
637}
638
639#[async_trait]
640pub trait Worker: Send + Sync {
641 fn descriptor(&self) -> WorkerDescriptor;
642 fn health(&self) -> WorkerHealth;
643 async fn submit(
644 &self,
645 profile: AgentProfile,
646 request: RunRequest,
647 ) -> Result<RunHandle, RuntimeError>;
648}
649
650const WORKER_READY: u8 = 0;
651const WORKER_DRAINING: u8 = 1;
652const WORKER_UNAVAILABLE: u8 = 2;
653
654#[derive(Clone)]
655pub struct LocalWorker {
656 descriptor: WorkerDescriptor,
657 runtime: Runtime,
658 state: Arc<AtomicU8>,
659}
660
661impl LocalWorker {
662 pub fn new(runtime: Runtime, id: impl Into<WorkerId>, name: impl Into<String>) -> Self {
663 let max_concurrent_runs = runtime.max_concurrent_runs();
664 Self {
665 descriptor: WorkerDescriptor {
666 id: id.into(),
667 name: name.into(),
668 capabilities: runtime.capabilities(),
669 max_concurrent_runs,
670 labels: BTreeMap::new(),
671 },
672 runtime,
673 state: Arc::new(AtomicU8::new(WORKER_READY)),
674 }
675 }
676
677 pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
678 self.descriptor.labels.insert(key.into(), value.into());
679 self
680 }
681
682 pub fn begin_drain(&self) {
683 self.state.store(WORKER_DRAINING, Ordering::Release);
684 }
685
686 pub fn mark_unavailable(&self) {
687 self.state.store(WORKER_UNAVAILABLE, Ordering::Release);
688 }
689
690 pub fn resume(&self) {
691 self.state.store(WORKER_READY, Ordering::Release);
692 }
693
694 pub async fn wait_for_idle(&self) {
695 self.runtime.wait_for_idle().await;
696 }
697}
698
699#[async_trait]
700impl Worker for LocalWorker {
701 fn descriptor(&self) -> WorkerDescriptor {
702 self.descriptor.clone()
703 }
704
705 fn health(&self) -> WorkerHealth {
706 let active_runs = self.runtime.active_runs();
707 let state = self.state.load(Ordering::Acquire);
708 WorkerHealth {
709 worker_id: self.descriptor.id.clone(),
710 status: match state {
711 WORKER_DRAINING => WorkerHealthStatus::Draining,
712 WORKER_UNAVAILABLE => WorkerHealthStatus::Unavailable,
713 _ if active_runs >= self.descriptor.max_concurrent_runs => WorkerHealthStatus::Busy,
714 _ => WorkerHealthStatus::Ready,
715 },
716 active_runs,
717 queued_runs: self.runtime.queued_runs(),
718 observed_at: std::time::SystemTime::now()
719 .duration_since(std::time::UNIX_EPOCH)
720 .unwrap_or_default()
721 .as_secs()
722 .to_string(),
723 }
724 }
725
726 async fn submit(
727 &self,
728 profile: AgentProfile,
729 request: RunRequest,
730 ) -> Result<RunHandle, RuntimeError> {
731 if self.state.load(Ordering::Acquire) != WORKER_READY {
732 return Err(RuntimeError {
733 code: RuntimeErrorCode::Unavailable,
734 message: "Worker is not accepting new runs".into(),
735 retryable: true,
736 });
737 }
738 Ok(self.runtime.agent(profile).run(request))
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use std::sync::atomic::{AtomicUsize, Ordering};
745 use std::time::Duration;
746
747 use super::*;
748
749 struct EchoExecutor;
750
751 #[async_trait]
752 impl RunExecutor for EchoExecutor {
753 async fn execute(
754 &self,
755 profile: AgentProfile,
756 request: RunRequest,
757 context: ExecutionContext,
758 ) -> Result<ExecutionResult, RuntimeError> {
759 context.emit(RuntimeEvent::TextDelta {
760 delta: format!("{}: ", profile.name),
761 })?;
762 Ok(ExecutionResult::new(
763 request
764 .session_id
765 .clone()
766 .unwrap_or_else(|| SessionId::new("default")),
767 request.prompt,
768 ))
769 }
770
771 fn capabilities(&self) -> RuntimeCapabilities {
772 RuntimeCapabilities {
773 streaming: true,
774 cancellation: true,
775 steering: true,
776 approvals: true,
777 skills: false,
778 subagents: false,
779 remote_workers: false,
780 }
781 }
782 }
783
784 #[tokio::test]
785 async fn runtime_streams_events_and_returns_a_result() {
786 let runtime = Runtime::builder().executor(EchoExecutor).build().unwrap();
787 let agent = runtime.agent(AgentProfile {
788 name: "coder".into(),
789 ..AgentProfile::default()
790 });
791 let mut request = RunRequest::new("hello");
792 request.session_id = Some(SessionId::new("session-1"));
793 let mut run = agent.run(request);
794 let run_id = run.id().clone();
795
796 let delta = run.next_event().await.unwrap();
797 assert_eq!(delta.run_id, run_id.as_str());
798 assert_eq!(delta.sequence, 0);
799 assert!(matches!(delta.event, RuntimeEvent::TextDelta { .. }));
800 let final_event = run.next_event().await.unwrap();
801 assert_eq!(final_event.sequence, 1);
802 assert!(matches!(
803 final_event.event,
804 RuntimeEvent::FinalResponse { ref text } if text == "hello"
805 ));
806
807 let result = run.result().await.unwrap();
808 assert_eq!(result.status, RunStatus::Completed);
809 assert_eq!(result.session_id, SessionId::new("session-1"));
810 assert_eq!(result.final_text, "hello");
811 }
812
813 struct ControlledExecutor;
814
815 #[async_trait]
816 impl RunExecutor for ControlledExecutor {
817 async fn execute(
818 &self,
819 _profile: AgentProfile,
820 _request: RunRequest,
821 mut context: ExecutionContext,
822 ) -> Result<ExecutionResult, RuntimeError> {
823 match context.next_control().await {
824 Some(RuntimeControl::Cancel { .. }) => Err(RuntimeError {
825 code: RuntimeErrorCode::Cancelled,
826 message: "cancelled by caller".into(),
827 retryable: false,
828 }),
829 Some(RuntimeControl::Steer { message, .. }) => {
830 Ok(ExecutionResult::new("steered", message))
831 }
832 Some(RuntimeControl::ResolveApproval { decision, .. }) => {
833 Ok(ExecutionResult::new("approved", decision))
834 }
835 None => Err(RuntimeError {
836 code: RuntimeErrorCode::Unavailable,
837 message: "control channel closed".into(),
838 retryable: false,
839 }),
840 }
841 }
842 }
843
844 #[tokio::test]
845 async fn run_handle_routes_cancellation() {
846 let runtime = Runtime::builder()
847 .executor(ControlledExecutor)
848 .build()
849 .unwrap();
850 let run = runtime
851 .agent(AgentProfile::default())
852 .run(RunRequest::new("wait"));
853 run.cancel().unwrap();
854 let result = run.result().await.unwrap();
855 assert_eq!(result.status, RunStatus::Cancelled);
856 assert_eq!(result.error.unwrap().code, RuntimeErrorCode::Cancelled);
857 }
858
859 #[tokio::test]
860 async fn run_handle_routes_steering() {
861 let runtime = Runtime::builder()
862 .executor(ControlledExecutor)
863 .build()
864 .unwrap();
865 let run = runtime
866 .agent(AgentProfile::default())
867 .run(RunRequest::new("wait"));
868 run.steer("new direction").unwrap();
869 let result = run.result().await.unwrap();
870 assert_eq!(result.final_text, "new direction");
871 }
872
873 #[tokio::test]
874 async fn confirmed_control_waits_for_executor_acknowledgement() {
875 let runtime = Runtime::builder()
876 .executor(ControlledExecutor)
877 .build()
878 .unwrap();
879 let run = runtime
880 .agent(AgentProfile::default())
881 .run(RunRequest::new("wait"));
882 let controller = run.controller();
883
884 controller.steer_confirmed("accepted update").await.unwrap();
885 let result = run.result().await.unwrap();
886 assert_eq!(result.final_text, "accepted update");
887 }
888
889 struct RejectingExecutor;
890
891 #[async_trait]
892 impl RunExecutor for RejectingExecutor {
893 async fn execute(
894 &self,
895 _profile: AgentProfile,
896 _request: RunRequest,
897 mut context: ExecutionContext,
898 ) -> Result<ExecutionResult, RuntimeError> {
899 let request = context.next_control_request().await.unwrap();
900 request.reject(RuntimeError {
901 code: RuntimeErrorCode::ApprovalDenied,
902 message: "control rejected by policy".into(),
903 retryable: false,
904 });
905 Ok(ExecutionResult::new("rejected", "unchanged"))
906 }
907 }
908
909 #[tokio::test]
910 async fn confirmed_control_surfaces_executor_rejection() {
911 let runtime = Runtime::builder()
912 .executor(RejectingExecutor)
913 .build()
914 .unwrap();
915 let run = runtime
916 .agent(AgentProfile::default())
917 .run(RunRequest::new("wait"));
918 let controller = run.controller();
919
920 let error = controller.cancel_confirmed().await.unwrap_err();
921 assert_eq!(error.code, RuntimeErrorCode::ApprovalDenied);
922 assert_eq!(run.result().await.unwrap().final_text, "unchanged");
923 }
924
925 struct ConcurrentExecutor {
926 active: Arc<AtomicUsize>,
927 peak: Arc<AtomicUsize>,
928 }
929
930 #[async_trait]
931 impl RunExecutor for ConcurrentExecutor {
932 async fn execute(
933 &self,
934 _profile: AgentProfile,
935 request: RunRequest,
936 _context: ExecutionContext,
937 ) -> Result<ExecutionResult, RuntimeError> {
938 let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
939 self.peak.fetch_max(active, Ordering::SeqCst);
940 tokio::time::sleep(Duration::from_millis(20)).await;
941 self.active.fetch_sub(1, Ordering::SeqCst);
942 Ok(ExecutionResult::new("session", request.prompt))
943 }
944 }
945
946 #[tokio::test]
947 async fn runtime_enforces_concurrency_limit() {
948 let active = Arc::new(AtomicUsize::new(0));
949 let peak = Arc::new(AtomicUsize::new(0));
950 let runtime = Runtime::builder()
951 .executor(ConcurrentExecutor {
952 active: active.clone(),
953 peak: peak.clone(),
954 })
955 .max_concurrent_runs(1)
956 .build()
957 .unwrap();
958 let agent = runtime.agent(AgentProfile::default());
959 let first = agent.run(RunRequest::new("one"));
960 let second = agent.run(RunRequest::new("two"));
961 let (a, b) = tokio::join!(first.result(), second.result());
962 assert!(a.is_ok());
963 assert!(b.is_ok());
964 assert_eq!(peak.load(Ordering::SeqCst), 1);
965 }
966
967 #[tokio::test]
968 async fn runtime_shutdown_drains_and_rejects_new_runs() {
969 let runtime = Runtime::builder().executor(EchoExecutor).build().unwrap();
970 runtime.shutdown().await;
971 assert!(runtime.is_shutting_down());
972 assert!(runtime.stats().shutting_down);
973
974 let result = runtime
975 .agent(AgentProfile::default())
976 .run(RunRequest::new("after shutdown"))
977 .result()
978 .await
979 .unwrap();
980 assert_eq!(result.status, RunStatus::Failed);
981 assert_eq!(result.error.unwrap().code, RuntimeErrorCode::Unavailable);
982 }
983
984 #[tokio::test]
985 async fn local_worker_uses_the_same_runtime_contract() {
986 let runtime = Runtime::builder().executor(EchoExecutor).build().unwrap();
987 let worker = LocalWorker::new(runtime, "local", "Local worker");
988 let descriptor = worker.descriptor();
989 assert_eq!(descriptor.id, WorkerId::new("local"));
990 assert_eq!(descriptor.max_concurrent_runs, 4);
991 assert!(descriptor.capabilities.streaming);
992 let run = worker
993 .submit(AgentProfile::default(), RunRequest::new("worker task"))
994 .await
995 .unwrap();
996 assert_eq!(run.result().await.unwrap().final_text, "worker task");
997 }
998
999 #[tokio::test]
1000 async fn local_worker_health_reports_runtime_capacity_and_activity() {
1001 let runtime = Runtime::builder()
1002 .executor(ConcurrentExecutor {
1003 active: Arc::new(AtomicUsize::new(0)),
1004 peak: Arc::new(AtomicUsize::new(0)),
1005 })
1006 .max_concurrent_runs(1)
1007 .build()
1008 .unwrap();
1009 let worker = LocalWorker::new(runtime, "local", "Local worker");
1010 let run = worker
1011 .submit(AgentProfile::default(), RunRequest::new("worker task"))
1012 .await
1013 .unwrap();
1014
1015 tokio::time::timeout(Duration::from_secs(1), async {
1016 loop {
1017 let health = worker.health();
1018 if health.active_runs == 1 {
1019 assert_eq!(health.status, WorkerHealthStatus::Busy);
1020 assert!(!health.observed_at.is_empty());
1021 break;
1022 }
1023 tokio::task::yield_now().await;
1024 }
1025 })
1026 .await
1027 .expect("worker should report its active run");
1028
1029 run.result().await.unwrap();
1030 let health = worker.health();
1031 assert_eq!(health.active_runs, 0);
1032 assert_eq!(health.status, WorkerHealthStatus::Ready);
1033 }
1034
1035 struct RelationshipExecutor;
1036
1037 #[async_trait]
1038 impl RunExecutor for RelationshipExecutor {
1039 async fn execute(
1040 &self,
1041 profile: AgentProfile,
1042 request: RunRequest,
1043 _context: ExecutionContext,
1044 ) -> Result<ExecutionResult, RuntimeError> {
1045 let mut output = ExecutionResult::new("child-session", request.prompt);
1046 output.metadata.insert(
1047 "agent_id".into(),
1048 serde_json::json!(profile.id.map(|id| id.into_inner())),
1049 );
1050 output.metadata.insert(
1051 "parent_run_id".into(),
1052 serde_json::json!(request.parent_run_id.map(|id| id.into_inner())),
1053 );
1054 Ok(output)
1055 }
1056 }
1057
1058 #[tokio::test]
1059 async fn parent_child_identity_survives_local_worker_submission() {
1060 let runtime = Runtime::builder()
1061 .executor(RelationshipExecutor)
1062 .build()
1063 .unwrap();
1064 let worker = LocalWorker::new(runtime, "local", "Local worker");
1065 let profile = AgentProfile {
1066 id: Some(microclaw_core::run_protocol::AgentId::new("reviewer")),
1067 ..AgentProfile::default()
1068 };
1069 let mut request = RunRequest::new("review child task");
1070 request.parent_run_id = Some(RunId::new("parent-1"));
1071
1072 let result = worker
1073 .submit(profile, request)
1074 .await
1075 .unwrap()
1076 .result()
1077 .await
1078 .unwrap();
1079 assert_eq!(result.metadata["agent_id"], "reviewer");
1080 assert_eq!(result.metadata["parent_run_id"], "parent-1");
1081 }
1082
1083 #[tokio::test]
1084 async fn local_worker_drains_without_accepting_new_runs() {
1085 let runtime = Runtime::builder()
1086 .executor(ConcurrentExecutor {
1087 active: Arc::new(AtomicUsize::new(0)),
1088 peak: Arc::new(AtomicUsize::new(0)),
1089 })
1090 .max_concurrent_runs(1)
1091 .build()
1092 .unwrap();
1093 let worker =
1094 LocalWorker::new(runtime, "local", "Local worker").with_label("pool", "foreground");
1095 let run = worker
1096 .submit(AgentProfile::default(), RunRequest::new("existing"))
1097 .await
1098 .unwrap();
1099 worker.begin_drain();
1100
1101 assert_eq!(worker.descriptor().labels["pool"], "foreground");
1102 assert_eq!(worker.health().status, WorkerHealthStatus::Draining);
1103 let rejected = match worker
1104 .submit(AgentProfile::default(), RunRequest::new("new"))
1105 .await
1106 {
1107 Ok(_) => panic!("draining Worker should reject new runs"),
1108 Err(error) => error,
1109 };
1110 assert_eq!(rejected.code, RuntimeErrorCode::Unavailable);
1111
1112 run.result().await.unwrap();
1113 worker.wait_for_idle().await;
1114 assert_eq!(worker.health().active_runs, 0);
1115 worker.mark_unavailable();
1116 assert_eq!(worker.health().status, WorkerHealthStatus::Unavailable);
1117 worker.resume();
1118 assert_eq!(worker.health().status, WorkerHealthStatus::Ready);
1119 }
1120}