1use std::collections::BTreeMap;
9use std::sync::Arc;
10use std::time::Duration;
11
12use futures_util::StreamExt;
13use orchestral_core::agent_protocol::{
14 reference::{
15 AgentContinuityState, AgentRunReducer, AgentRunStatus, ApplyOutcome,
16 ReconciliationProofVerifier, RecoveryReplayPolicy, SequencedApply,
17 },
18 spi::{
19 AgentJournalStore, AgentJournalStoreError, AgentProvider, AgentProviderStream,
20 AgentRecoveryRequest, AgentRunCatalogEntry, AgentRunRegistration, AgentStartError,
21 InMemoryAgentJournalStore, StoredAgentRun,
22 },
23 wire::{
24 AgentCommand, AgentCommandEnvelope, AgentEvent, AgentEventAuthority, AgentEventDraft,
25 AgentEventId, AgentExecutionRef, AgentJournalRecord, AgentProtocolError,
26 AgentProtocolErrorCode, AgentProviderStreamItem, AgentRunEnvelope, AgentRunState,
27 AgentRunView, AgentStartRequest, AgentTelemetryEnvelope, CommandAck, CommandId, Content,
28 Digest, ProviderBindingRef, ReconciliationProof, ReconciliationProofRef, RunId,
29 },
30};
31use thiserror::Error;
32use tokio::sync::{broadcast, Mutex, Notify, RwLock};
33use tokio::time::timeout;
34
35const RECOVERY_PREFIX_ITEM_TIMEOUT: Duration = Duration::from_secs(2);
36
37#[derive(Debug, Clone)]
39#[non_exhaustive]
40pub enum AgentControlEvent {
41 Durable(Arc<AgentJournalRecord>),
42 Telemetry(AgentTelemetryEnvelope),
43}
44
45#[derive(Debug, Error)]
48#[non_exhaustive]
49pub enum AgentControlError {
50 #[error(transparent)]
51 Protocol(#[from] AgentProtocolError),
52 #[error(transparent)]
53 Start(#[from] AgentStartError),
54 #[error(transparent)]
55 Journal(#[from] AgentJournalStoreError),
56 #[error("Agent Run not found: {0}")]
57 RunNotFound(RunId),
58 #[error("Agent Run continuity is unknown: {0}")]
59 ContinuityUnknown(RunId),
60 #[error("Agent recovery evidence did not match the committed Run prefix: {0}")]
61 RecoveryMismatch(RunId),
62}
63
64struct RunEntry {
65 request: AgentStartRequest,
66 execution: AgentExecutionRef,
67 reducer: AgentRunReducer,
68 journal: Vec<AgentJournalRecord>,
69}
70
71struct RunSlot {
72 entry: Mutex<RunEntry>,
73 recovery_gate: Mutex<()>,
74 events: broadcast::Sender<AgentControlEvent>,
75 changed: Notify,
76}
77
78impl RunSlot {
79 fn publish(&self, event: AgentControlEvent) {
80 let _ = self.events.send(event);
81 self.changed.notify_waiters();
82 }
83}
84
85pub struct AgentController {
91 provider: Arc<dyn AgentProvider>,
92 binding_ref: ProviderBindingRef,
93 descriptor: orchestral_core::agent_protocol::wire::AgentDescriptorEnvelope,
94 journal_store: Arc<dyn AgentJournalStore>,
95 runs: RwLock<BTreeMap<RunId, Arc<RunSlot>>>,
96 start_gate: Mutex<()>,
97 event_buffer: usize,
98}
99
100impl AgentController {
101 pub fn new(
102 provider: Arc<dyn AgentProvider>,
103 binding_ref: ProviderBindingRef,
104 ) -> Result<Self, AgentProtocolError> {
105 Self::with_event_buffer(provider, binding_ref, 256)
106 }
107
108 pub fn with_event_buffer(
109 provider: Arc<dyn AgentProvider>,
110 binding_ref: ProviderBindingRef,
111 event_buffer: usize,
112 ) -> Result<Self, AgentProtocolError> {
113 Self::with_journal_store_and_event_buffer(
114 provider,
115 binding_ref,
116 Arc::new(InMemoryAgentJournalStore::default()),
117 event_buffer,
118 )
119 }
120
121 pub fn with_journal_store(
122 provider: Arc<dyn AgentProvider>,
123 binding_ref: ProviderBindingRef,
124 journal_store: Arc<dyn AgentJournalStore>,
125 ) -> Result<Self, AgentProtocolError> {
126 Self::with_journal_store_and_event_buffer(provider, binding_ref, journal_store, 256)
127 }
128
129 pub fn with_journal_store_and_event_buffer(
130 provider: Arc<dyn AgentProvider>,
131 binding_ref: ProviderBindingRef,
132 journal_store: Arc<dyn AgentJournalStore>,
133 event_buffer: usize,
134 ) -> Result<Self, AgentProtocolError> {
135 if binding_ref.is_empty() || event_buffer == 0 {
136 return Err(AgentProtocolError::new(
137 AgentProtocolErrorCode::InvalidSpec,
138 "Agent controller requires a Provider binding and a non-zero event buffer",
139 ));
140 }
141 let descriptor = provider.describe();
142 descriptor.validate_integrity()?;
143 Ok(Self {
144 provider,
145 binding_ref,
146 descriptor,
147 journal_store,
148 runs: RwLock::new(BTreeMap::new()),
149 start_gate: Mutex::new(()),
150 event_buffer,
151 })
152 }
153
154 pub fn descriptor(&self) -> &orchestral_core::agent_protocol::wire::AgentDescriptorEnvelope {
155 &self.descriptor
156 }
157
158 pub async fn start(
161 self: &Arc<Self>,
162 run: AgentRunEnvelope,
163 ) -> Result<AgentExecutionRef, AgentControlError> {
164 let request = AgentStartRequest::new(run, self.binding_ref.clone(), &self.descriptor)?;
165 let run_id = request.run.spec.run_id.clone();
166 let _start_guard = self.start_gate.lock().await;
167
168 if let Some(slot) = self.runs.read().await.get(&run_id).cloned() {
169 let entry = slot.entry.lock().await;
170 if entry.request != request {
171 return Err(AgentProtocolError::new(
172 AgentProtocolErrorCode::RunIdConflict,
173 "run_id already belongs to another immutable start request",
174 )
175 .into());
176 }
177 return Ok(entry.execution.clone());
178 }
179
180 if let Some(stored) = self.journal_store.load_run(&run_id).await? {
181 if stored.registration.request != request {
182 return Err(AgentProtocolError::new(
183 AgentProtocolErrorCode::RunIdConflict,
184 "run_id already belongs to another durable start request",
185 )
186 .into());
187 }
188 let slot = self.slot_from_stored(stored)?;
189 let execution = slot.entry.lock().await.execution.clone();
190 self.runs.write().await.insert(run_id.clone(), slot.clone());
191 let view = slot.entry.lock().await.reducer.view();
192 if !view.state.is_terminal() && view.state.status() != AgentRunStatus::Unknown {
193 self.mark_continuity_lost(
194 &run_id,
195 &slot,
196 "Host controller restarted without a continuously attached Provider stream"
197 .to_owned(),
198 )
199 .await?;
200 }
201 return Ok(execution);
202 }
203
204 let observed_descriptor = self.provider.describe();
205 if observed_descriptor != self.descriptor {
206 return Err(AgentProtocolError::new(
207 AgentProtocolErrorCode::RunIdConflict,
208 "Provider descriptor changed after controller binding",
209 )
210 .into());
211 }
212 let start = self.provider.start(request.clone()).await?;
213 start.execution.validate_for(&request, &self.descriptor)?;
214 let compatibility = self
215 .descriptor
216 .descriptor
217 .check_run_compatibility(&request.run)
218 .map_err(|rejection| {
219 AgentProtocolError::new(AgentProtocolErrorCode::Unsupported, rejection.to_string())
220 .with_details(rejection.details)
221 })?;
222 start
223 .admission
224 .validate_against(&request.run, &compatibility)?;
225
226 let mut reducer = AgentRunReducer::new(
227 start.execution.clone(),
228 &request,
229 &self.descriptor,
230 start.admission.clone(),
231 )?;
232 let mut journal = Vec::with_capacity(start.admission.skipped_optional_bindings.len() + 4);
233 push_sequenced(
234 &mut journal,
235 reducer.apply_host_draft(AgentEventDraft {
236 event_id: AgentEventId::new(format!("host-run-accepted-{}", run_id.as_str())),
237 run_id: run_id.clone(),
238 causation_id: None,
239 source_fingerprint: None,
240 payload: AgentEvent::RunAccepted {
241 session_id: request.run.spec.session_id.clone(),
242 spec_digest: request.run.spec_digest.clone(),
243 },
244 })?,
245 );
246 for skip in &start.admission.skipped_optional_bindings {
247 push_sequenced(
248 &mut journal,
249 reducer.apply_host_draft(AgentEventDraft {
250 event_id: AgentEventId::new(format!(
251 "host-resource-skip-{}-{}",
252 run_id.as_str(),
253 skip.binding_id.as_str()
254 )),
255 run_id: run_id.clone(),
256 causation_id: None,
257 source_fingerprint: None,
258 payload: AgentEvent::ResourceBindingSkipped { skip: skip.clone() },
259 })?,
260 );
261 }
262
263 let (events, _) = broadcast::channel(self.event_buffer);
264 self.journal_store
265 .create_run(StoredAgentRun {
266 registration: AgentRunRegistration {
267 request: request.clone(),
268 execution: start.execution.clone(),
269 admission: start.admission.clone(),
270 },
271 records: journal.clone(),
272 })
273 .await?;
274 let slot = Arc::new(RunSlot {
275 entry: Mutex::new(RunEntry {
276 request,
277 execution: start.execution.clone(),
278 reducer,
279 journal: journal.clone(),
280 }),
281 recovery_gate: Mutex::new(()),
282 events,
283 changed: Notify::new(),
284 });
285 self.runs.write().await.insert(run_id.clone(), slot.clone());
286 for record in journal {
287 slot.publish(AgentControlEvent::Durable(Arc::new(record)));
288 }
289
290 let controller = Arc::clone(self);
291 tokio::spawn(async move {
292 controller.drive_stream(run_id, slot, start.stream).await;
293 });
294 Ok(start.execution)
295 }
296
297 pub async fn inspect(&self, run_id: &RunId) -> Result<AgentRunView, AgentControlError> {
299 let slot = self.run_slot(run_id).await?;
300 let view = slot.entry.lock().await.reducer.view();
301 Ok(view)
302 }
303
304 pub async fn initial_input(&self, run_id: &RunId) -> Result<Vec<Content>, AgentControlError> {
311 let slot = self.run_slot(run_id).await?;
312 let input = slot.entry.lock().await.request.run.spec.input.clone();
313 Ok(input)
314 }
315
316 pub async fn run_extensions(
318 &self,
319 run_id: &RunId,
320 ) -> Result<orchestral_core::agent_protocol::wire::Extensions, AgentControlError> {
321 let slot = self.run_slot(run_id).await?;
322 let extensions = slot.entry.lock().await.request.run.spec.extensions.clone();
323 Ok(extensions)
324 }
325
326 pub async fn catalog_runs(&self) -> Result<Vec<AgentRunCatalogEntry>, AgentControlError> {
329 Ok(self.journal_store.catalog_runs().await?)
330 }
331
332 pub async fn can_control_run(&self, run_id: &RunId) -> Result<bool, AgentControlError> {
340 if let Some(slot) = self.runs.read().await.get(run_id).cloned() {
341 let entry = slot.entry.lock().await;
342 return Ok(self.registration_matches_current(&entry.request, &entry.execution));
343 }
344 let Some(stored) = self.journal_store.load_run(run_id).await? else {
345 return Ok(false);
346 };
347 stored.validate_shape()?;
348 Ok(self.registration_matches_current(
349 &stored.registration.request,
350 &stored.registration.execution,
351 ))
352 }
353
354 pub async fn has_run(&self, run_id: &RunId) -> Result<bool, AgentControlError> {
355 if self.runs.read().await.contains_key(run_id) {
356 return Ok(true);
357 }
358 Ok(self.journal_store.load_run(run_id).await?.is_some())
359 }
360
361 pub async fn events(
363 &self,
364 run_id: &RunId,
365 after_run_seq: u64,
366 ) -> Result<Vec<AgentJournalRecord>, AgentControlError> {
367 self.run_slot(run_id).await?;
368 Ok(self.journal_store.records(run_id, after_run_seq).await?)
369 }
370
371 pub async fn subscribe(
374 &self,
375 run_id: &RunId,
376 ) -> Result<broadcast::Receiver<AgentControlEvent>, AgentControlError> {
377 Ok(self.run_slot(run_id).await?.events.subscribe())
378 }
379
380 pub async fn command(
383 &self,
384 command: AgentCommandEnvelope,
385 ) -> Result<CommandAck, AgentControlError> {
386 command.verify_digest()?;
387 if orchestral_core::agent_protocol::wire::QueuedInputOperation::from_command(&command)?
388 .is_some()
389 && !orchestral_core::agent_protocol::wire::QueuedInputOperation::is_supported(
390 &self.descriptor.descriptor,
391 )
392 {
393 return Err(AgentProtocolError::new(
394 AgentProtocolErrorCode::Unsupported,
395 "this Agent does not support queued input; use immediate Steer explicitly",
396 )
397 .into());
398 }
399 let slot = self.run_slot(&command.run_id).await?;
400 let mut entry = slot.entry.lock().await;
401 if entry
402 .reducer
403 .recorded_command(&command.command_id)
404 .is_some_and(|recorded| recorded != &command)
405 {
406 return Err(AgentProtocolError::new(
407 AgentProtocolErrorCode::DuplicateConflict,
408 "command_id was already used with a different immutable command",
409 )
410 .into());
411 }
412 if let Ok(ack) = entry.reducer.command_ack(&command.command_id, true) {
413 return Ok(ack);
414 }
415
416 let mut next_reducer = entry.reducer.clone();
417 let received = next_reducer.apply_host_draft(AgentEventDraft {
418 event_id: AgentEventId::new(format!(
419 "host-command-received-{}-{}",
420 command.run_id.as_str(),
421 command.command_id.as_str()
422 )),
423 run_id: command.run_id.clone(),
424 causation_id: Some(command.command_id.clone()),
425 source_fingerprint: None,
426 payload: AgentEvent::CommandReceived {
427 command: command.clone(),
428 },
429 })?;
430 let duplicate = matches!(received.outcome, ApplyOutcome::ExactDuplicate);
431 self.commit_sequenced(&slot, &mut entry, next_reducer, received)
432 .await?;
433
434 let disposition = self
435 .provider
436 .command(&entry.execution, command.clone())
437 .await?;
438 disposition.validate_for(&command)?;
439 let mut next_reducer = entry.reducer.clone();
440 let disposition_event = next_reducer.apply_provider_draft(disposition.to_event_draft()?)?;
441 self.commit_sequenced(&slot, &mut entry, next_reducer, disposition_event)
442 .await?;
443 Ok(entry.reducer.command_ack(&command.command_id, duplicate)?)
444 }
445
446 pub async fn cancel(
450 &self,
451 run_id: &RunId,
452 reason: impl Into<String>,
453 ) -> Result<CommandAck, AgentControlError> {
454 let command = AgentCommandEnvelope::new(
455 CommandId::new(format!("host-cancel-{}", uuid::Uuid::new_v4())),
456 run_id.clone(),
457 None,
458 AgentCommand::Cancel {
459 reason: reason.into(),
460 },
461 )?;
462 self.command(command).await
463 }
464
465 pub async fn recorded_command(
468 &self,
469 run_id: &RunId,
470 command_id: &CommandId,
471 ) -> Result<Option<AgentCommandEnvelope>, AgentControlError> {
472 if let Some(slot) = self.runs.read().await.get(run_id).cloned() {
473 return Ok(slot
474 .entry
475 .lock()
476 .await
477 .reducer
478 .recorded_command(command_id)
479 .cloned());
480 }
481 let Some(stored) = self.journal_store.load_run(run_id).await? else {
482 return Ok(None);
483 };
484 stored.validate_shape()?;
485 Ok(stored
486 .records
487 .into_iter()
488 .find_map(|record| match record.event.payload {
489 AgentEvent::CommandReceived { command } if &command.command_id == command_id => {
490 Some(command)
491 }
492 _ => None,
493 }))
494 }
495
496 pub async fn command_ack(
497 &self,
498 run_id: &RunId,
499 command_id: &CommandId,
500 ) -> Result<CommandAck, AgentControlError> {
501 let slot = self.run_slot(run_id).await?;
502 let entry = slot.entry.lock().await;
503 let ack = entry.reducer.command_ack(command_id, true)?;
504 Ok(ack)
505 }
506
507 pub async fn recover(
515 self: &Arc<Self>,
516 run_id: &RunId,
517 ) -> Result<AgentRunView, AgentControlError> {
518 let slot = self.run_slot(run_id).await?;
519 let _recovery_guard = slot.recovery_gate.lock().await;
520 let (
521 start_request,
522 execution,
523 expected_provider_prefix,
524 committed_provider_prefix,
525 last_confirmed_seq,
526 loss_event_digest,
527 ) = {
528 let entry = slot.entry.lock().await;
529 let AgentRunState::Unknown {
530 last_confirmed_seq, ..
531 } = entry.reducer.state()
532 else {
533 return Err(AgentProtocolError::new(
534 AgentProtocolErrorCode::InvalidTransition,
535 "recover requires a Run whose continuity is Unknown",
536 )
537 .into());
538 };
539 let loss_record = entry.journal.last().ok_or_else(|| {
540 AgentProtocolError::new(
541 AgentProtocolErrorCode::InvalidTransition,
542 "Unknown Run has no continuity-loss journal record",
543 )
544 })?;
545 if !matches!(
546 (&loss_record.authority, &loss_record.event.payload),
547 (
548 AgentEventAuthority::Host { .. },
549 AgentEvent::ContinuityLost { .. }
550 )
551 ) {
552 return Err(AgentProtocolError::new(
553 AgentProtocolErrorCode::InvalidTransition,
554 "automatic recovery is allowed only after a Host-recorded stream loss",
555 )
556 .into());
557 }
558 let pending_request_ids = entry
559 .reducer
560 .view()
561 .pending_requests
562 .iter()
563 .map(|request| request.request_id.clone())
564 .collect();
565 let provider_records = entry
566 .journal
567 .iter()
568 .filter(|record| {
569 record.event.run_seq <= last_confirmed_seq
570 && matches!(record.authority, AgentEventAuthority::Provider)
571 })
572 .collect::<Vec<_>>();
573 let prefix = provider_records
574 .iter()
575 .map(|record| {
576 (
577 record.event.event_id.clone(),
578 record.draft_digest.clone(),
579 record
580 .event
581 .payload
582 .recovery_replay_policy(&pending_request_ids)
583 == RecoveryReplayPolicy::ProviderEvidenceRequired,
584 )
585 })
586 .collect::<Vec<_>>();
587 let committed_provider_prefix = provider_records
588 .iter()
589 .map(|record| AgentEventDraft {
590 event_id: record.event.event_id.clone(),
591 run_id: record.event.run_id.clone(),
592 causation_id: record.event.causation_id.clone(),
593 source_fingerprint: record.event.source_fingerprint.clone(),
594 payload: record.event.payload.clone(),
595 })
596 .collect::<Vec<_>>();
597 (
598 entry.request.clone(),
599 entry.execution.clone(),
600 prefix,
601 committed_provider_prefix,
602 last_confirmed_seq,
603 loss_record.event.event_digest.clone(),
604 )
605 };
606
607 let recovery =
608 AgentRecoveryRequest::new(start_request, execution.clone(), &self.descriptor)?
609 .with_committed_provider_prefix(committed_provider_prefix)?;
610 let recovered = self.provider.recover(recovery).await?;
611 let (mut stream, confirmation) = recovered.into_parts();
612 let mut matched_digests = Vec::with_capacity(expected_provider_prefix.len());
613 let mut optional_stream_replays = Vec::new();
614 for (expected_event_id, expected_draft_digest, requires_stream_replay) in
615 &expected_provider_prefix
616 {
617 if !requires_stream_replay {
618 matched_digests.push(expected_draft_digest.clone());
619 optional_stream_replays
620 .push((expected_event_id.clone(), expected_draft_digest.clone()));
621 continue;
622 }
623 loop {
624 let recovered_item = timeout(RECOVERY_PREFIX_ITEM_TIMEOUT, stream.next())
625 .await
626 .map_err(|_| AgentControlError::RecoveryMismatch(run_id.clone()))?;
627 match recovered_item {
628 Some(Ok(AgentProviderStreamItem::Telemetry(telemetry))) => {
629 telemetry.validate_integrity()?;
630 if telemetry.run_id != *run_id {
631 return Err(AgentControlError::RecoveryMismatch(run_id.clone()));
632 }
633 slot.publish(AgentControlEvent::Telemetry(telemetry));
634 }
635 Some(Ok(AgentProviderStreamItem::Event(draft))) => {
636 let draft_digest = draft.computed_digest()?;
637 if draft.event_id == *expected_event_id
638 && draft_digest == *expected_draft_digest
639 {
640 optional_stream_replays.clear();
645 matched_digests.push(expected_draft_digest.clone());
646 break;
647 }
648 if let Some(position) = optional_stream_replays.iter().position(
649 |(optional_event_id, optional_digest)| {
650 draft.event_id == *optional_event_id
651 && draft_digest == *optional_digest
652 },
653 ) {
654 optional_stream_replays.drain(..=position);
655 continue;
656 }
657 return Err(AgentControlError::RecoveryMismatch(run_id.clone()));
658 }
659 Some(Ok(_)) | Some(Err(_)) | None => {
660 return Err(AgentControlError::RecoveryMismatch(run_id.clone()));
661 }
662 }
663 }
664 }
665
666 let mut recovered_fingerprint = format!("{run_id}:{last_confirmed_seq}");
667 for digest in &matched_digests {
668 recovered_fingerprint.push(':');
669 recovered_fingerprint.push_str(digest.as_str());
670 }
671 let proof = ReconciliationProof::new(
672 ReconciliationProofRef::new(format!(
673 "host-recovery/{}/{}",
674 run_id.as_str(),
675 last_confirmed_seq
676 )),
677 last_confirmed_seq,
678 loss_event_digest,
679 Digest::sha256(recovered_fingerprint),
680 )?;
681 let verifier = ExactReconciliationVerifier {
682 execution: execution.clone(),
683 proof: proof.clone(),
684 };
685 {
686 let mut entry = slot.entry.lock().await;
687 let mut next_reducer = entry.reducer.clone();
688 let restored = next_reducer.apply_verified_reconciliation(
689 AgentEventDraft {
690 event_id: AgentEventId::new(format!(
691 "host-continuity-restored-{}-{}",
692 run_id.as_str(),
693 last_confirmed_seq
694 )),
695 run_id: run_id.clone(),
696 causation_id: None,
697 source_fingerprint: None,
698 payload: AgentEvent::ContinuityRestored {
699 proof,
700 reason: "Provider recovery prefix matched the Host journal".to_owned(),
701 },
702 },
703 &verifier,
704 )?;
705 self.commit_sequenced(&slot, &mut entry, next_reducer, restored)
706 .await?;
707 }
708
709 if let Err(error) = confirmation.await {
710 self.mark_continuity_lost(
711 run_id,
712 &slot,
713 format!("Provider could not resume after reconciliation: {error}"),
714 )
715 .await?;
716 return Err(error.into());
717 }
718
719 let controller = Arc::clone(self);
720 let recovered_run_id = run_id.clone();
721 let recovered_slot = slot.clone();
722 tokio::spawn(async move {
723 controller
724 .drive_stream(recovered_run_id, recovered_slot, stream)
725 .await;
726 });
727 self.inspect(run_id).await
728 }
729
730 pub async fn wait_for_terminal(
733 &self,
734 run_id: &RunId,
735 ) -> Result<AgentRunView, AgentControlError> {
736 let slot = self.run_slot(run_id).await?;
737 loop {
738 let changed = slot.changed.notified();
739 let view = slot.entry.lock().await.reducer.view();
740 if view.state.is_terminal() {
741 return Ok(view);
742 }
743 if view.state.status() == AgentRunStatus::Unknown {
744 return Err(AgentControlError::ContinuityUnknown(run_id.clone()));
745 }
746 changed.await;
747 }
748 }
749
750 async fn run_slot(&self, run_id: &RunId) -> Result<Arc<RunSlot>, AgentControlError> {
751 if let Some(slot) = self.runs.read().await.get(run_id).cloned() {
752 return Ok(slot);
753 }
754 let _start_guard = self.start_gate.lock().await;
755 if let Some(slot) = self.runs.read().await.get(run_id).cloned() {
756 return Ok(slot);
757 }
758 let stored = self
759 .journal_store
760 .load_run(run_id)
761 .await?
762 .ok_or_else(|| AgentControlError::RunNotFound(run_id.clone()))?;
763 let slot = self.slot_from_stored(stored)?;
764 self.runs.write().await.insert(run_id.clone(), slot.clone());
765 let view = slot.entry.lock().await.reducer.view();
766 if !view.state.is_terminal() && view.state.status() != AgentRunStatus::Unknown {
767 self.mark_continuity_lost(
768 run_id,
769 &slot,
770 "Host controller restarted without a continuously attached Provider stream"
771 .to_owned(),
772 )
773 .await?;
774 }
775 Ok(slot)
776 }
777
778 fn slot_from_stored(&self, stored: StoredAgentRun) -> Result<Arc<RunSlot>, AgentControlError> {
779 stored.validate_shape()?;
780 let registration = &stored.registration;
781 registration
782 .execution
783 .validate_for(®istration.request, &self.descriptor)?;
784 let compatibility = self
785 .descriptor
786 .descriptor
787 .check_run_compatibility(®istration.request.run)
788 .map_err(|rejection| {
789 AgentProtocolError::new(AgentProtocolErrorCode::Unsupported, rejection.to_string())
790 .with_details(rejection.details)
791 })?;
792 registration
793 .admission
794 .validate_against(®istration.request.run, &compatibility)?;
795 let mut reducer = AgentRunReducer::new(
796 registration.execution.clone(),
797 ®istration.request,
798 &self.descriptor,
799 registration.admission.clone(),
800 )?;
801 for record in &stored.records {
802 reducer.replay_journal_record(record.clone())?;
803 }
804 let (events, _) = broadcast::channel(self.event_buffer);
805 Ok(Arc::new(RunSlot {
806 entry: Mutex::new(RunEntry {
807 request: registration.request.clone(),
808 execution: registration.execution.clone(),
809 reducer,
810 journal: stored.records,
811 }),
812 recovery_gate: Mutex::new(()),
813 events,
814 changed: Notify::new(),
815 }))
816 }
817
818 fn registration_matches_current(
819 &self,
820 request: &AgentStartRequest,
821 execution: &AgentExecutionRef,
822 ) -> bool {
823 request.provider_binding == self.binding_ref
824 && request.expected_descriptor_digest == self.descriptor.descriptor_digest
825 && execution.binding_ref == self.binding_ref
826 && execution.descriptor_digest == self.descriptor.descriptor_digest
827 }
828
829 async fn drive_stream(
830 self: Arc<Self>,
831 run_id: RunId,
832 slot: Arc<RunSlot>,
833 mut stream: AgentProviderStream,
834 ) {
835 let mut end_reason = "Provider stream ended before an authoritative terminal".to_owned();
836 while let Some(item) = stream.next().await {
837 match item {
838 Ok(AgentProviderStreamItem::Event(draft)) => {
839 let mut entry = slot.entry.lock().await;
840 let mut next_reducer = entry.reducer.clone();
841 match next_reducer.apply_provider_draft(*draft) {
842 Ok(sequenced) => {
843 if let Err(error) = self
844 .commit_sequenced(&slot, &mut entry, next_reducer, sequenced)
845 .await
846 {
847 end_reason =
848 format!("Provider event could not be journaled: {error}");
849 break;
850 }
851 if entry.reducer.state().is_terminal()
852 || entry.reducer.state().status() == AgentRunStatus::Unknown
853 {
854 return;
855 }
856 }
857 Err(error) => {
858 end_reason = format!("Provider event was rejected: {error}");
859 break;
860 }
861 }
862 }
863 Ok(AgentProviderStreamItem::Telemetry(telemetry)) => {
864 if let Err(error) = telemetry.validate_integrity() {
865 end_reason = format!("Provider telemetry was invalid: {error}");
866 break;
867 }
868 if telemetry.run_id != run_id {
869 end_reason = "Provider telemetry crossed a Run boundary".to_owned();
870 break;
871 }
872 slot.publish(AgentControlEvent::Telemetry(telemetry));
873 }
874 Ok(_) => {
875 end_reason = "Provider emitted an unsupported stream item".to_owned();
876 break;
877 }
878 Err(error) => {
879 end_reason = format!("Provider stream failed: {error}");
880 break;
881 }
882 }
883 }
884 if let Err(error) = self.mark_continuity_lost(&run_id, &slot, end_reason).await {
885 tracing::error!(run_id = %run_id, error = %error, "failed to journal continuity loss");
886 }
887 }
888
889 async fn commit_sequenced(
890 &self,
891 slot: &RunSlot,
892 entry: &mut RunEntry,
893 next_reducer: AgentRunReducer,
894 sequenced: SequencedApply,
895 ) -> Result<(), AgentControlError> {
896 if matches!(sequenced.outcome, ApplyOutcome::ExactDuplicate) {
897 return Ok(());
898 }
899 let expected_previous = entry.reducer.view().last_run_seq.unwrap_or(0);
900 self.journal_store
901 .append_record(
902 &entry.execution.run_id,
903 expected_previous,
904 sequenced.record.clone(),
905 )
906 .await?;
907 entry.reducer = next_reducer;
908 entry.journal.push(sequenced.record.clone());
909 slot.publish(AgentControlEvent::Durable(Arc::new(sequenced.record)));
910 Ok(())
911 }
912
913 async fn mark_continuity_lost(
914 &self,
915 run_id: &RunId,
916 slot: &Arc<RunSlot>,
917 reason: String,
918 ) -> Result<(), AgentControlError> {
919 let mut entry = slot.entry.lock().await;
920 let view = entry.reducer.view();
921 if view.state.is_terminal() || view.state.status() == AgentRunStatus::Unknown {
922 return Ok(());
923 }
924 let last_confirmed_seq = view.last_run_seq.unwrap_or(0);
925 let draft = AgentEventDraft {
926 event_id: AgentEventId::new(format!(
927 "host-continuity-lost-{}-{}",
928 run_id.as_str(),
929 last_confirmed_seq + 1
930 )),
931 run_id: run_id.clone(),
932 causation_id: None,
933 source_fingerprint: None,
934 payload: AgentEvent::ContinuityLost {
935 last_confirmed_seq,
936 reason,
937 },
938 };
939 let mut next_reducer = entry.reducer.clone();
940 let sequenced = next_reducer.apply_host_draft(draft)?;
941 self.commit_sequenced(slot, &mut entry, next_reducer, sequenced)
942 .await?;
943 Ok(())
944 }
945}
946
947fn push_sequenced(journal: &mut Vec<AgentJournalRecord>, sequenced: SequencedApply) {
948 if !matches!(sequenced.outcome, ApplyOutcome::ExactDuplicate) {
949 journal.push(sequenced.record);
950 }
951}
952
953struct ExactReconciliationVerifier {
954 execution: AgentExecutionRef,
955 proof: ReconciliationProof,
956}
957
958impl ReconciliationProofVerifier for ExactReconciliationVerifier {
959 fn verify(
960 &self,
961 execution: &AgentExecutionRef,
962 continuity: &AgentContinuityState,
963 proof: &ReconciliationProof,
964 ) -> Result<(), AgentProtocolError> {
965 if execution != &self.execution || proof != &self.proof {
966 return Err(AgentProtocolError::new(
967 AgentProtocolErrorCode::InvalidDigest,
968 "reconciliation proof is not the controller-verified evidence",
969 ));
970 }
971 let AgentContinuityState::Unknown {
972 last_confirmed_seq,
973 loss_event_digest,
974 ..
975 } = continuity
976 else {
977 return Err(AgentProtocolError::new(
978 AgentProtocolErrorCode::InvalidTransition,
979 "reconciliation requires Unknown continuity",
980 ));
981 };
982 if *last_confirmed_seq != proof.last_confirmed_seq
983 || *loss_event_digest != proof.loss_event_digest
984 {
985 return Err(AgentProtocolError::new(
986 AgentProtocolErrorCode::InvalidDigest,
987 "reconciliation proof is not bound to the current continuity loss",
988 ));
989 }
990 proof.verify_integrity()
991 }
992}