1use std::sync::{Arc, Mutex};
2
3use async_trait::async_trait;
4use microclaw_core::run_protocol::{
5 AgentProfile, RunRequest, RunStatus, RuntimeCapabilities, RuntimeControl, RuntimeError,
6 RuntimeErrorCode, WorkerCommand, WorkerDescriptor, WorkerFrame, WorkerHealth,
7 WorkerHealthStatus, WORKER_PROTOCOL_VERSION,
8};
9
10use crate::internal::runtime::{
11 ControlRequest, ExecutionContext, ExecutionResult, RunExecutor, RunHandle, Runtime, Worker,
12};
13
14#[async_trait]
16pub trait WorkerConnection: Send {
17 async fn send(&mut self, command: WorkerCommand) -> Result<(), RuntimeError>;
18 async fn receive(&mut self) -> Result<Option<WorkerFrame>, RuntimeError>;
19}
20
21#[async_trait]
23pub trait WorkerTransport: Send + Sync + 'static {
24 async fn connect(&self) -> Result<Box<dyn WorkerConnection>, RuntimeError>;
25}
26
27struct RemoteExecutor {
28 transport: Arc<dyn WorkerTransport>,
29 capabilities: RuntimeCapabilities,
30}
31
32#[async_trait]
33impl RunExecutor for RemoteExecutor {
34 async fn execute(
35 &self,
36 profile: AgentProfile,
37 mut request: RunRequest,
38 mut context: ExecutionContext,
39 ) -> Result<ExecutionResult, RuntimeError> {
40 let run_id = context.run_id().clone();
41 request.run_id = Some(run_id.clone());
42 let mut connection = self.transport.connect().await?;
43 connection
44 .send(WorkerCommand::Submit {
45 protocol_version: WORKER_PROTOCOL_VERSION,
46 profile,
47 request: Box::new(request),
48 })
49 .await?;
50
51 let mut controls_open = true;
52 let mut pending_control: Option<ControlRequest> = None;
53 let mut last_remote_sequence = None;
54 let mut reconnect_attempts = 0_u8;
55 loop {
56 tokio::select! {
57 frame = connection.receive() => {
58 let frame = match frame {
59 Ok(Some(frame)) => {
60 reconnect_attempts = 0;
61 frame
62 }
63 Ok(None) | Err(_) if reconnect_attempts < 3 => {
64 reconnect_attempts += 1;
65 connection = self.transport.connect().await?;
66 connection.send(WorkerCommand::ResumeEvents {
67 protocol_version: WORKER_PROTOCOL_VERSION,
68 run_id: run_id.clone(),
69 after_sequence: last_remote_sequence,
70 }).await?;
71 if let Some(control) = pending_control.as_ref() {
72 connection.send(WorkerCommand::Control {
73 protocol_version: WORKER_PROTOCOL_VERSION,
74 control: control.control().clone(),
75 }).await?;
76 }
77 continue;
78 }
79 Ok(None) => return Err(unavailable("remote Worker disconnected before returning a result")),
80 Err(error) => return Err(error),
81 };
82 frame.validate_protocol()?;
83 match frame {
84 WorkerFrame::Accepted { run_id: accepted, .. } => {
85 ensure_run_id(&run_id, &accepted)?;
86 }
87 WorkerFrame::Event { envelope, .. } => {
88 if envelope.run_id != run_id.as_str() {
89 return Err(protocol_error("remote Worker returned an event for a different run"));
90 }
91 if last_remote_sequence.is_some_and(|sequence| envelope.sequence <= sequence) {
92 continue;
93 }
94 last_remote_sequence = Some(envelope.sequence);
95 if !matches!(
96 envelope.event,
97 microclaw_core::runtime_event::RuntimeEvent::FinalResponse { .. }
98 | microclaw_core::runtime_event::RuntimeEvent::Cancelled { .. }
99 ) {
100 context.emit(envelope.event)?;
101 }
102 }
103 WorkerFrame::ControlAcknowledged { run_id: acknowledged, .. } => {
104 ensure_run_id(&run_id, &acknowledged)?;
105 if let Some(control) = pending_control.take() {
106 control.accept();
107 }
108 }
109 WorkerFrame::Result { result, .. } => {
110 ensure_run_id(&run_id, &result.run_id)?;
111 if let Some(control) = pending_control.take() {
112 control.reject(unavailable("run completed before control acknowledgement"));
113 }
114 return if result.status == RunStatus::Completed {
115 Ok(ExecutionResult {
116 session_id: result.session_id,
117 final_text: result.final_text,
118 metadata: result.metadata,
119 })
120 } else {
121 Err(result.error.unwrap_or(RuntimeError {
122 code: RuntimeErrorCode::Internal,
123 message: format!("remote Worker ended with status {:?}", result.status),
124 retryable: false,
125 }))
126 };
127 }
128 WorkerFrame::Error { run_id: frame_run_id, error, .. } => {
129 if let Some(frame_run_id) = frame_run_id.as_ref() {
130 ensure_run_id(&run_id, frame_run_id)?;
131 }
132 if let Some(control) = pending_control.take() {
133 control.reject(error);
134 } else {
135 return Err(error);
136 }
137 }
138 WorkerFrame::Descriptor { .. } | WorkerFrame::Health { .. } => {}
139 }
140 }
141 control = context.next_control_request(), if controls_open && pending_control.is_none() => {
142 match control {
143 Some(control) => {
144 let command = control.control().clone();
145 ensure_control_run_id(&run_id, &command)?;
146 connection.send(WorkerCommand::Control {
147 protocol_version: WORKER_PROTOCOL_VERSION,
148 control: command,
149 }).await?;
150 pending_control = Some(control);
151 }
152 None => controls_open = false,
153 }
154 }
155 }
156 }
157 }
158
159 fn capabilities(&self) -> RuntimeCapabilities {
160 self.capabilities.clone()
161 }
162}
163
164#[derive(Clone)]
166pub struct RemoteWorker {
167 descriptor: WorkerDescriptor,
168 runtime: Runtime,
169 transport: Arc<dyn WorkerTransport>,
170 health: Arc<Mutex<WorkerHealth>>,
171}
172
173impl RemoteWorker {
174 pub async fn connect(transport: impl WorkerTransport) -> Result<Self, RuntimeError> {
175 let transport: Arc<dyn WorkerTransport> = Arc::new(transport);
176 let mut connection = transport.connect().await?;
177 connection
178 .send(WorkerCommand::Describe {
179 protocol_version: WORKER_PROTOCOL_VERSION,
180 })
181 .await?;
182 let frame = connection
183 .receive()
184 .await?
185 .ok_or_else(|| unavailable("remote Worker disconnected during discovery"))?;
186 frame.validate_protocol()?;
187 let WorkerFrame::Descriptor { descriptor, .. } = frame else {
188 return Err(protocol_error(
189 "remote Worker did not return a descriptor during discovery",
190 ));
191 };
192 let health = WorkerHealth {
193 worker_id: descriptor.id.clone(),
194 status: WorkerHealthStatus::Ready,
195 active_runs: 0,
196 queued_runs: 0,
197 observed_at: String::new(),
198 };
199 let runtime = Runtime::builder()
200 .shared_executor(Arc::new(RemoteExecutor {
201 transport: transport.clone(),
202 capabilities: descriptor.capabilities.clone(),
203 }))
204 .max_concurrent_runs(descriptor.max_concurrent_runs.max(1))
205 .build()
206 .map_err(|error| RuntimeError {
207 code: RuntimeErrorCode::Configuration,
208 message: error.to_string(),
209 retryable: false,
210 })?;
211 Ok(Self {
212 descriptor,
213 runtime,
214 transport,
215 health: Arc::new(Mutex::new(health)),
216 })
217 }
218
219 pub async fn refresh_health(&self) -> Result<WorkerHealth, RuntimeError> {
220 let mut connection = self.transport.connect().await?;
221 connection
222 .send(WorkerCommand::Health {
223 protocol_version: WORKER_PROTOCOL_VERSION,
224 })
225 .await?;
226 let frame = connection
227 .receive()
228 .await?
229 .ok_or_else(|| unavailable("remote Worker disconnected during health check"))?;
230 frame.validate_protocol()?;
231 let WorkerFrame::Health { health, .. } = frame else {
232 return Err(protocol_error(
233 "remote Worker did not return health during health check",
234 ));
235 };
236 if health.worker_id != self.descriptor.id {
237 return Err(protocol_error("remote Worker health identity changed"));
238 }
239 *self
240 .health
241 .lock()
242 .unwrap_or_else(|error| error.into_inner()) = health.clone();
243 Ok(health)
244 }
245}
246
247#[async_trait]
248impl Worker for RemoteWorker {
249 fn descriptor(&self) -> WorkerDescriptor {
250 self.descriptor.clone()
251 }
252
253 fn health(&self) -> WorkerHealth {
254 self.health
255 .lock()
256 .unwrap_or_else(|error| error.into_inner())
257 .clone()
258 }
259
260 async fn submit(
261 &self,
262 profile: AgentProfile,
263 request: RunRequest,
264 ) -> Result<RunHandle, RuntimeError> {
265 if matches!(
266 self.health().status,
267 WorkerHealthStatus::Draining | WorkerHealthStatus::Unavailable
268 ) {
269 return Err(unavailable("remote Worker is not accepting new runs"));
270 }
271 Ok(self.runtime.agent(profile).run(request))
272 }
273}
274
275fn ensure_run_id(
276 expected: µclaw_core::run_protocol::RunId,
277 actual: µclaw_core::run_protocol::RunId,
278) -> Result<(), RuntimeError> {
279 if expected == actual {
280 Ok(())
281 } else {
282 Err(protocol_error(
283 "remote Worker returned a frame for a different run",
284 ))
285 }
286}
287
288fn ensure_control_run_id(
289 expected: µclaw_core::run_protocol::RunId,
290 control: &RuntimeControl,
291) -> Result<(), RuntimeError> {
292 let actual = match control {
293 RuntimeControl::Cancel { run_id }
294 | RuntimeControl::Steer { run_id, .. }
295 | RuntimeControl::ResolveApproval { run_id, .. } => run_id,
296 };
297 ensure_run_id(expected, actual)
298}
299
300fn protocol_error(message: impl Into<String>) -> RuntimeError {
301 RuntimeError {
302 code: RuntimeErrorCode::InvalidRequest,
303 message: message.into(),
304 retryable: false,
305 }
306}
307
308fn unavailable(message: impl Into<String>) -> RuntimeError {
309 RuntimeError {
310 code: RuntimeErrorCode::Unavailable,
311 message: message.into(),
312 retryable: true,
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use std::collections::{BTreeMap, VecDeque};
319
320 use microclaw_core::run_protocol::{RunId, RunResult, SessionId, WorkerId};
321 use microclaw_core::runtime_event::{RuntimeEvent, RuntimeEventEnvelope};
322
323 use super::*;
324
325 #[derive(Clone)]
326 struct ScriptedTransport {
327 connections: Arc<Mutex<VecDeque<VecDeque<WorkerFrame>>>>,
328 sent: Arc<Mutex<Vec<WorkerCommand>>>,
329 }
330
331 struct ScriptedConnection {
332 frames: VecDeque<WorkerFrame>,
333 sent: Arc<Mutex<Vec<WorkerCommand>>>,
334 }
335
336 #[async_trait]
337 impl WorkerTransport for ScriptedTransport {
338 async fn connect(&self) -> Result<Box<dyn WorkerConnection>, RuntimeError> {
339 let frames = self
340 .connections
341 .lock()
342 .unwrap()
343 .pop_front()
344 .ok_or_else(|| unavailable("missing scripted connection"))?;
345 Ok(Box::new(ScriptedConnection {
346 frames,
347 sent: self.sent.clone(),
348 }))
349 }
350 }
351
352 #[async_trait]
353 impl WorkerConnection for ScriptedConnection {
354 async fn send(&mut self, command: WorkerCommand) -> Result<(), RuntimeError> {
355 self.sent.lock().unwrap().push(command);
356 Ok(())
357 }
358
359 async fn receive(&mut self) -> Result<Option<WorkerFrame>, RuntimeError> {
360 Ok(self.frames.pop_front())
361 }
362 }
363
364 fn descriptor() -> WorkerDescriptor {
365 WorkerDescriptor {
366 id: WorkerId::new("remote-1"),
367 name: "Remote one".into(),
368 capabilities: RuntimeCapabilities {
369 streaming: true,
370 remote_workers: true,
371 ..RuntimeCapabilities::default()
372 },
373 max_concurrent_runs: 2,
374 labels: BTreeMap::new(),
375 }
376 }
377
378 #[tokio::test]
379 async fn remote_worker_discovers_streams_and_returns_result() {
380 let run_id = RunId::new("remote-run");
381 let transport = ScriptedTransport {
382 connections: Arc::new(Mutex::new(VecDeque::from([
383 VecDeque::from([WorkerFrame::Descriptor {
384 protocol_version: WORKER_PROTOCOL_VERSION,
385 descriptor: descriptor(),
386 }]),
387 VecDeque::from([
388 WorkerFrame::Accepted {
389 protocol_version: WORKER_PROTOCOL_VERSION,
390 run_id: run_id.clone(),
391 },
392 WorkerFrame::Event {
393 protocol_version: WORKER_PROTOCOL_VERSION,
394 envelope: RuntimeEventEnvelope::new(
395 run_id.to_string(),
396 0,
397 RuntimeEvent::TextDelta {
398 delta: "remote".into(),
399 },
400 ),
401 },
402 WorkerFrame::Result {
403 protocol_version: WORKER_PROTOCOL_VERSION,
404 result: RunResult {
405 run_id: run_id.clone(),
406 session_id: SessionId::new("remote-session"),
407 status: RunStatus::Completed,
408 final_text: "done".into(),
409 error: None,
410 metadata: BTreeMap::new(),
411 },
412 },
413 ]),
414 ]))),
415 sent: Arc::new(Mutex::new(Vec::new())),
416 };
417 let worker = RemoteWorker::connect(transport).await.unwrap();
418 assert_eq!(worker.descriptor().id, WorkerId::new("remote-1"));
419 let mut request = RunRequest::new("work remotely");
420 request.run_id = Some(run_id);
421 let mut run = worker
422 .submit(AgentProfile::default(), request)
423 .await
424 .unwrap();
425 let event = run.next_event().await.unwrap();
426 assert!(matches!(event.event, RuntimeEvent::TextDelta { .. }));
427 assert!(matches!(
428 run.next_event().await.unwrap().event,
429 RuntimeEvent::FinalResponse { .. }
430 ));
431 assert_eq!(run.result().await.unwrap().final_text, "done");
432 }
433
434 #[tokio::test]
435 async fn remote_worker_resumes_without_replaying_delivered_events() {
436 let run_id = RunId::new("resumable-run");
437 let sent = Arc::new(Mutex::new(Vec::new()));
438 let result = WorkerFrame::Result {
439 protocol_version: WORKER_PROTOCOL_VERSION,
440 result: RunResult {
441 run_id: run_id.clone(),
442 session_id: SessionId::new("resumed-session"),
443 status: RunStatus::Completed,
444 final_text: "resumed".into(),
445 error: None,
446 metadata: BTreeMap::new(),
447 },
448 };
449 let event = |sequence, delta: &str| WorkerFrame::Event {
450 protocol_version: WORKER_PROTOCOL_VERSION,
451 envelope: RuntimeEventEnvelope::new(
452 run_id.to_string(),
453 sequence,
454 RuntimeEvent::TextDelta {
455 delta: delta.into(),
456 },
457 ),
458 };
459 let transport = ScriptedTransport {
460 connections: Arc::new(Mutex::new(VecDeque::from([
461 VecDeque::from([WorkerFrame::Descriptor {
462 protocol_version: WORKER_PROTOCOL_VERSION,
463 descriptor: descriptor(),
464 }]),
465 VecDeque::from([
466 WorkerFrame::Accepted {
467 protocol_version: WORKER_PROTOCOL_VERSION,
468 run_id: run_id.clone(),
469 },
470 event(0, "first"),
471 ]),
472 VecDeque::from([event(0, "duplicate"), event(1, "second"), result]),
473 ]))),
474 sent: sent.clone(),
475 };
476 let worker = RemoteWorker::connect(transport).await.unwrap();
477 let mut request = RunRequest::new("resume remotely");
478 request.run_id = Some(run_id.clone());
479 let mut run = worker
480 .submit(AgentProfile::default(), request)
481 .await
482 .unwrap();
483
484 let mut deltas = Vec::new();
485 while let Some(envelope) = run.next_event().await {
486 if let RuntimeEvent::TextDelta { delta } = envelope.event {
487 deltas.push(delta);
488 }
489 }
490 assert_eq!(deltas, ["first", "second"]);
491 assert_eq!(run.result().await.unwrap().final_text, "resumed");
492 assert!(sent.lock().unwrap().iter().any(|command| matches!(
493 command,
494 WorkerCommand::ResumeEvents {
495 run_id: resumed,
496 after_sequence: Some(0),
497 ..
498 } if resumed == &run_id
499 )));
500 }
501}