Skip to main content

agent_runtime_execution/
lib.rs

1//! Execution lifecycle coordinator between Outer Shell and Runtime Kernel.
2
3use std::{
4    collections::BTreeMap,
5    sync::{
6        Arc,
7        atomic::{AtomicBool, Ordering},
8    },
9    time::{Duration, Instant},
10};
11
12use async_trait::async_trait;
13use runtime_api_contract::{
14    CreateExecutionRequest, CreateExecutionResponse, EventPage, EventPayload, ExecutionFailure,
15    ExecutionOptions, ExecutionOutcome, ExecutionState, ExecutionView,
16    ModelGenerationOptions as ApiModelGenerationOptions, ModelToolChoice as ApiModelToolChoice,
17    RuntimeInput, SubmitInputRequest,
18};
19use runtime_kernel_api::{
20    AgentDefinitionResolver, ContextLimits, KernelEvent, KernelEventSink, KernelFailure,
21    KernelLimits, KernelSpec, RuntimeKernel,
22};
23use runtime_link_api::{
24    ExecutionJournal, JournalDelegation, JournalError, JournalReservation, LinkError,
25    NewJournalExecution, RuntimeLink,
26};
27use runtime_ports::{
28    CommitDisposition, ExecutionDelegationLease, ExecutionSessionFactory, InteractionRequest,
29    InteractionResponse, InteractionSession, ModelGenerationOptions, ModelToolChoice,
30    OperationContext, PortFailure, PortFailureKind, ResolvedExecutionContext,
31    RuntimeInstanceResolver, SessionScope, SubagentOutcome, SubagentRequest, SubagentSession,
32    TraceSession,
33};
34use runtime_types::{ExecutionId, OperationId, RequestAuthority};
35use sha2::{Digest, Sha256};
36use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore};
37use tokio_util::{sync::CancellationToken, task::TaskTracker};
38
39#[derive(Clone)]
40pub struct ExecutionCoordinator {
41    journal: Arc<dyn ExecutionJournal>,
42    instance_resolver: Arc<dyn RuntimeInstanceResolver>,
43    session_factory: Arc<dyn ExecutionSessionFactory>,
44    definition_resolver: Arc<dyn AgentDefinitionResolver>,
45    kernel: Arc<dyn RuntimeKernel>,
46    context_limits: ContextLimits,
47    capacity: Arc<Semaphore>,
48    cancellations: Arc<Mutex<BTreeMap<ExecutionId, CancellationToken>>>,
49    authorities: Arc<Mutex<BTreeMap<ExecutionId, RequestAuthority>>>,
50    accepting: Arc<AtomicBool>,
51    tasks: TaskTracker,
52    recovery_tasks: TaskTracker,
53    recovery_stop: CancellationToken,
54    recovery_started: Arc<AtomicBool>,
55    worker_id: Arc<str>,
56}
57
58impl ExecutionCoordinator {
59    pub fn new(
60        journal: Arc<dyn ExecutionJournal>,
61        instance_resolver: Arc<dyn RuntimeInstanceResolver>,
62        session_factory: Arc<dyn ExecutionSessionFactory>,
63        definition_resolver: Arc<dyn AgentDefinitionResolver>,
64        kernel: Arc<dyn RuntimeKernel>,
65        max_concurrent_executions: usize,
66    ) -> Result<Self, LinkError> {
67        Self::new_with_context_limits(
68            journal,
69            instance_resolver,
70            session_factory,
71            definition_resolver,
72            kernel,
73            max_concurrent_executions,
74            ContextLimits::default(),
75        )
76    }
77
78    #[allow(clippy::too_many_arguments)]
79    pub fn new_with_context_limits(
80        journal: Arc<dyn ExecutionJournal>,
81        instance_resolver: Arc<dyn RuntimeInstanceResolver>,
82        session_factory: Arc<dyn ExecutionSessionFactory>,
83        definition_resolver: Arc<dyn AgentDefinitionResolver>,
84        kernel: Arc<dyn RuntimeKernel>,
85        max_concurrent_executions: usize,
86        context_limits: ContextLimits,
87    ) -> Result<Self, LinkError> {
88        if max_concurrent_executions == 0 {
89            return Err(LinkError::Invalid(
90                "max_concurrent_executions must be positive".into(),
91            ));
92        }
93        Ok(Self {
94            journal,
95            instance_resolver,
96            session_factory,
97            definition_resolver,
98            kernel,
99            context_limits,
100            capacity: Arc::new(Semaphore::new(max_concurrent_executions)),
101            cancellations: Arc::new(Mutex::new(BTreeMap::new())),
102            authorities: Arc::new(Mutex::new(BTreeMap::new())),
103            accepting: Arc::new(AtomicBool::new(true)),
104            tasks: TaskTracker::new(),
105            recovery_tasks: TaskTracker::new(),
106            recovery_stop: CancellationToken::new(),
107            recovery_started: Arc::new(AtomicBool::new(false)),
108            worker_id: format!("runtime-worker-{}", ExecutionId::random().as_str()).into(),
109        })
110    }
111
112    /// Claim and recover work left by a crashed host. Queued executions with a
113    /// durable delegation are resumed; executions interrupted after starting
114    /// are safely finalized without replaying unknown side effects.
115    pub async fn recover(&self, limit: usize) -> Result<usize, LinkError> {
116        let now = chrono::Utc::now().timestamp_millis();
117        let records = self
118            .journal
119            .recoverable(now, limit)
120            .await
121            .map_err(map_journal)?;
122        let mut scheduled = 0;
123        for record in records {
124            let Ok(permit) = self.capacity.clone().try_acquire_owned() else {
125                break;
126            };
127            scheduled += 1;
128            if record.view.state == ExecutionState::Queued && record.delegation.is_some() {
129                self.tasks
130                    .spawn(self.clone().run(record.view.id.clone(), permit));
131            } else {
132                self.tasks.spawn(
133                    self.clone()
134                        .recover_interrupted(record.view.id.clone(), permit),
135                );
136            }
137        }
138        Ok(scheduled)
139    }
140
141    /// Keep scanning after startup so a row whose prior worker claim had not
142    /// yet expired at boot is recovered once the short lease elapses.
143    pub fn start_recovery_loop(
144        &self,
145        interval: Duration,
146        batch_size: usize,
147    ) -> Result<(), LinkError> {
148        if interval.is_zero() || batch_size == 0 || batch_size > 10_000 {
149            return Err(LinkError::Invalid(
150                "recovery interval and batch size must be bounded and positive".into(),
151            ));
152        }
153        if self
154            .recovery_started
155            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
156            .is_err()
157        {
158            return Ok(());
159        }
160        let coordinator = self.clone();
161        let stop = self.recovery_stop.clone();
162        self.recovery_tasks.spawn(async move {
163            let mut ticker = tokio::time::interval(interval);
164            ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
165            // Wiring performs one synchronous startup scan before admission;
166            // the loop begins with the first real interval.
167            ticker.tick().await;
168            loop {
169                tokio::select! {
170                    _ = stop.cancelled() => return,
171                    _ = ticker.tick() => {
172                        if !coordinator.accepting.load(Ordering::SeqCst) {
173                            return;
174                        }
175                        if let Err(error) = coordinator.recover(batch_size).await {
176                            tracing::warn!(%error, "periodic execution recovery scan failed");
177                        }
178                    }
179                }
180            }
181        });
182        Ok(())
183    }
184
185    /// Stop admission and close the task scope so shutdown can wait for every
186    /// execution spawned by this coordinator.
187    pub fn begin_shutdown(&self) {
188        self.accepting.store(false, Ordering::SeqCst);
189        self.recovery_stop.cancel();
190        self.recovery_tasks.close();
191        self.tasks.close();
192    }
193
194    /// Drain active executions within `grace`. On expiry, propagate
195    /// cancellation to every registered execution and wait for cleanup.
196    pub async fn shutdown(&self, grace: Duration) -> bool {
197        self.begin_shutdown();
198        if tokio::time::timeout(grace, async {
199            tokio::join!(self.recovery_tasks.wait(), self.tasks.wait());
200        })
201        .await
202        .is_ok()
203        {
204            return true;
205        }
206        for cancellation in self.cancellations.lock().await.values() {
207            cancellation.cancel();
208        }
209        tokio::time::timeout(Duration::from_secs(5), async {
210            tokio::join!(self.recovery_tasks.wait(), self.tasks.wait());
211        })
212        .await
213        .is_ok()
214    }
215
216    pub fn active_executions(&self) -> usize {
217        self.tasks.len()
218    }
219
220    async fn run(self, execution_id: ExecutionId, permit: OwnedSemaphorePermit) {
221        let _permit = permit;
222        let now = chrono::Utc::now().timestamp_millis();
223        let expires = now.saturating_add(30_000);
224        if self
225            .journal
226            .claim(&execution_id, &self.worker_id, now, expires)
227            .await
228            .is_err()
229        {
230            return;
231        }
232        if let Err(error) = self.run_with_claim_heartbeat(&execution_id).await {
233            tracing::error!(execution_id=%execution_id, code=%error.code, message=%error.message, "execution failed");
234            if error.code == "WORKER_CLAIM_LOST" {
235                // This worker no longer owns terminal decisions. Its execution
236                // cancellation is local; the next claimant resolves the
237                // durable state after the short lease expires.
238                self.cancellations.lock().await.remove(&execution_id);
239                self.authorities.lock().await.remove(&execution_id);
240                return;
241            }
242            if error.commit == CommitDisposition::Unknown {
243                let payload = EventPayload::Warning {
244                    code: "COMMIT_DISPOSITION_UNKNOWN".into(),
245                    message: format!(
246                        "{}: {}; recovery must resolve the committed operation before terminal state",
247                        error.code, error.message
248                    ),
249                };
250                let state = self
251                    .journal
252                    .get(&execution_id)
253                    .await
254                    .ok()
255                    .map(|record| record.view.state);
256                let preserved = match state {
257                    Some(ExecutionState::Finalizing) => self
258                        .journal
259                        .append_event(&execution_id, payload)
260                        .await
261                        .is_ok(),
262                    Some(ExecutionState::Running) => self
263                        .journal
264                        .transition_with_event(
265                            &execution_id,
266                            &[ExecutionState::Running],
267                            ExecutionState::Finalizing,
268                            None,
269                            None,
270                            payload,
271                        )
272                        .await
273                        .is_ok(),
274                    _ => false,
275                };
276                if preserved {
277                    self.cancellations.lock().await.remove(&execution_id);
278                    self.authorities.lock().await.remove(&execution_id);
279                    return;
280                }
281            }
282            if error.code == "CANCELED"
283                && self
284                    .journal
285                    .get(&execution_id)
286                    .await
287                    .is_ok_and(|record| record.view.state == ExecutionState::Finalizing)
288            {
289                self.cancellations.lock().await.remove(&execution_id);
290                self.authorities.lock().await.remove(&execution_id);
291                return;
292            }
293            let _ = self
294                .finalize_failure(
295                    &execution_id,
296                    ExecutionFailure {
297                        code: error.code,
298                        message: error.message,
299                        retryable: error.retryable,
300                    },
301                )
302                .await;
303        }
304        self.cleanup_delegation(&execution_id).await;
305        self.cancellations.lock().await.remove(&execution_id);
306        self.authorities.lock().await.remove(&execution_id);
307    }
308
309    async fn run_with_claim_heartbeat(
310        &self,
311        execution_id: &ExecutionId,
312    ) -> Result<(), KernelFailure> {
313        let mut execution = Box::pin(self.run_inner(execution_id));
314        let mut heartbeat = tokio::time::interval(Duration::from_secs(10));
315        // `interval` ticks immediately once; the initial claim was committed
316        // by `run`, so only subsequent ticks renew it.
317        heartbeat.tick().await;
318        loop {
319            tokio::select! {
320                result = &mut execution => return result,
321                _ = heartbeat.tick() => {
322                    let now = chrono::Utc::now().timestamp_millis();
323                    if self
324                        .journal
325                        .claim(
326                            execution_id,
327                            &self.worker_id,
328                            now,
329                            now.saturating_add(30_000),
330                        )
331                        .await
332                        .is_err()
333                    {
334                        if let Some(cancellation) = self.cancellations.lock().await.get(execution_id) {
335                            cancellation.cancel();
336                        }
337                        // Give cancellation-aware ports a bounded opportunity
338                        // to stop commands/streams before this worker yields
339                        // ownership. Durable cleanup remains the next
340                        // claimant's responsibility.
341                        let _ = tokio::time::timeout(Duration::from_secs(5), &mut execution).await;
342                        return Err(KernelFailure {
343                            code: "WORKER_CLAIM_LOST".into(),
344                            message: "worker could not renew its durable execution claim".into(),
345                            retryable: true,
346                            commit: CommitDisposition::Unknown,
347                        });
348                    }
349                }
350            }
351        }
352    }
353
354    async fn recover_interrupted(self, execution_id: ExecutionId, permit: OwnedSemaphorePermit) {
355        let _permit = permit;
356        let now = chrono::Utc::now().timestamp_millis();
357        if self
358            .journal
359            .claim(
360                &execution_id,
361                &self.worker_id,
362                now,
363                now.saturating_add(60_000),
364            )
365            .await
366            .is_err()
367        {
368            return;
369        }
370        let Ok(mut record) = self.journal.get(&execution_id).await else {
371            return;
372        };
373        if record.view.state.is_terminal() {
374            self.cleanup_delegation(&execution_id).await;
375            return;
376        }
377        if matches!(
378            record.view.state,
379            ExecutionState::Running | ExecutionState::WaitingForInput
380        ) {
381            let Ok(mutation) = self
382                .journal
383                .transition_with_event(
384                    &execution_id,
385                    &[ExecutionState::Running, ExecutionState::WaitingForInput],
386                    ExecutionState::Finalizing,
387                    None,
388                    None,
389                    EventPayload::Warning {
390                        code: "EXECUTION_INTERRUPTED".into(),
391                        message: "the prior worker stopped; recovery will not replay uncheckpointed side effects".into(),
392                    },
393                )
394                .await
395            else {
396                return;
397            };
398            record = mutation.execution;
399        }
400        if record.view.state == ExecutionState::Queued {
401            let _ = self
402                .journal
403                .transition_with_event(
404                    &execution_id,
405                    &[ExecutionState::Queued],
406                    ExecutionState::Failed,
407                    None,
408                    Some(ExecutionFailure {
409                        code: "DELEGATION_NOT_DURABLE".into(),
410                        message: "queued execution lost caller authority before delegation attach"
411                            .into(),
412                        retryable: true,
413                    }),
414                    EventPayload::ExecutionFailed {
415                        code: "DELEGATION_NOT_DURABLE".into(),
416                        message: "queued execution cannot be recovered without a delegation".into(),
417                    },
418                )
419                .await;
420            return;
421        }
422        if let Some(stored) = record
423            .delegation
424            .clone()
425            .filter(|delegation| !delegation.cleanup_complete)
426        {
427            let delegation = delegation_from_journal(stored);
428            if self
429                .revoke_for_finalization(&execution_id, &record.caller, &delegation)
430                .await
431                .is_err()
432            {
433                return;
434            }
435        }
436        let _ = self
437            .journal
438            .transition_with_event(
439                &execution_id,
440                &[ExecutionState::Finalizing],
441                ExecutionState::Failed,
442                None,
443                Some(ExecutionFailure {
444                    code: "EXECUTION_INTERRUPTED".into(),
445                    message: "execution was interrupted before a durable resumable checkpoint"
446                        .into(),
447                    retryable: true,
448                }),
449                EventPayload::ExecutionFailed {
450                    code: "EXECUTION_INTERRUPTED".into(),
451                    message: "execution was safely finalized after worker recovery".into(),
452                },
453            )
454            .await;
455    }
456
457    async fn run_inner(&self, execution_id: &ExecutionId) -> Result<(), KernelFailure> {
458        let record = self
459            .journal
460            .get(execution_id)
461            .await
462            .map_err(journal_kernel_error)?;
463        let cancellation = CancellationToken::new();
464        self.cancellations
465            .lock()
466            .await
467            .insert(execution_id.clone(), cancellation.clone());
468        let remaining_ms = record
469            .view
470            .created_at_ms
471            .saturating_add((record.request.options.deadline_seconds * 1000) as i64)
472            .saturating_sub(chrono::Utc::now().timestamp_millis())
473            .max(1) as u64;
474        let operation = OperationContext {
475            id: OperationId::new(format!("{execution_id}:run"))
476                .map_err(|error| KernelFailure::new("OPERATION_ID_INVALID", error.to_string()))?,
477            execution_id: execution_id.clone(),
478            deadline: Instant::now() + Duration::from_millis(remaining_ms),
479            cancellation,
480        };
481        let resolved = self
482            .instance_resolver
483            .resolve(
484                &operation,
485                &record.caller,
486                &record.request.runtime_instance_id,
487            )
488            .await
489            .map_err(port_kernel_error)?;
490        let resolved = apply_request_routing(resolved, &record.request);
491        let definition = self
492            .definition_resolver
493            .resolve(&operation, &resolved)
494            .await?;
495        let scope = SessionScope {
496            execution_id: execution_id.clone(),
497            conversation_id: record.request.conversation_id.clone(),
498            resolved: resolved.clone(),
499        };
500        let mut delegation = match record.delegation {
501            Some(delegation) if !delegation.cleanup_complete => delegation_from_journal(delegation),
502            Some(_) => {
503                return Err(KernelFailure::new(
504                    "DELEGATION_ALREADY_CLEANED",
505                    "execution delegation was already revoked",
506                ));
507            }
508            None => {
509                let authority = self
510                    .authorities
511                    .lock()
512                    .await
513                    .get(execution_id)
514                    .cloned()
515                    .ok_or_else(|| KernelFailure::new(
516                        "EXECUTION_AUTHORITY_MISSING",
517                        "queued execution has no durable delegation and caller authority is unavailable",
518                    ))?;
519                let delegation = self
520                    .session_factory
521                    .establish_delegation(&operation, &authority, &scope)
522                    .await
523                    .map_err(port_kernel_error)?;
524                self.journal
525                    .attach_delegation(execution_id, journal_delegation(&delegation))
526                    .await
527                    .map_err(journal_kernel_error)?;
528                delegation
529            }
530        };
531        let now_seconds = chrono::Utc::now().timestamp().max(0) as u64;
532        if delegation.expires_at_seconds
533            <= now_seconds
534                .saturating_add(operation.remaining().map_err(port_kernel_error)?.as_secs())
535                .saturating_add(30)
536        {
537            delegation = self
538                .session_factory
539                .renew_delegation(&operation, &record.caller, &delegation)
540                .await
541                .map_err(port_kernel_error)?;
542            self.journal
543                .attach_delegation(execution_id, journal_delegation(&delegation))
544                .await
545                .map_err(journal_kernel_error)?;
546        }
547        self.journal
548            .transition(
549                execution_id,
550                &[ExecutionState::Queued],
551                ExecutionState::Running,
552            )
553            .await
554            .map_err(journal_kernel_error)?;
555        self.journal
556            .append_event(execution_id, EventPayload::ExecutionStarted)
557            .await
558            .map_err(journal_kernel_error)?;
559        let mut sessions = self
560            .session_factory
561            .create(&operation, &record.caller, &delegation, &scope)
562            .await
563            .map_err(port_kernel_error)?;
564        sessions.interaction = Arc::new(JournaledInteractionSession {
565            execution_id: execution_id.clone(),
566            journal: self.journal.clone(),
567            inner: sessions.interaction.clone(),
568        });
569        sessions.subagent = Arc::new(ChildExecutionSubagentSession {
570            coordinator: self.clone(),
571            authority: self.authorities.lock().await.get(execution_id).cloned(),
572            parent_execution_id: execution_id.clone(),
573            runtime_instance_id: record.request.runtime_instance_id.clone(),
574            workspace_id: record.request.workspace_id.clone(),
575            model: record.request.model.clone(),
576            generation: record.request.generation.clone(),
577        });
578        let prompt = record
579            .request
580            .input
581            .user_text()
582            .ok_or_else(|| {
583                KernelFailure::new(
584                    "INITIAL_INPUT_INVALID",
585                    "execution must start with at least one user message",
586                )
587            })?
588            .to_string();
589        let request_messages = request_messages(&record.request.input)?;
590        let sink: Arc<dyn KernelEventSink> = Arc::new(JournalKernelEventSink {
591            execution_id: execution_id.clone(),
592            journal: self.journal.clone(),
593            trace: sessions.trace.clone(),
594            operation: operation.clone(),
595        });
596        let outcome = self
597            .kernel
598            .execute(
599                operation,
600                KernelSpec {
601                    execution_id: execution_id.clone(),
602                    conversation_id: record.request.conversation_id.clone(),
603                    user_prompt: prompt,
604                    request_messages,
605                    model: resolved.model,
606                    generation: model_generation_options(&record.request.generation),
607                    definition,
608                    granted_capabilities: record.caller.capabilities.iter().cloned().collect(),
609                    limits: self.kernel_limits(&record.request.options, &record.request.generation),
610                },
611                sessions,
612                sink,
613            )
614            .await?;
615        let public = ExecutionOutcome {
616            answer: outcome.answer.clone(),
617            model_turns: outcome.model_turns,
618            tool_calls: outcome.tool_calls,
619            input_tokens: outcome.usage.input_tokens,
620            output_tokens: outcome.usage.output_tokens,
621        };
622        self.journal
623            .transition(
624                execution_id,
625                &[ExecutionState::Running],
626                ExecutionState::Finalizing,
627            )
628            .await
629            .map_err(journal_kernel_error)?;
630        self.revoke_for_finalization(execution_id, &record.caller, &delegation)
631            .await?;
632        self.journal
633            .transition_with_event(
634                execution_id,
635                &[ExecutionState::Finalizing],
636                ExecutionState::Completed,
637                Some(public),
638                None,
639                EventPayload::ExecutionCompleted {
640                    answer: outcome.answer,
641                },
642            )
643            .await
644            .map_err(journal_kernel_error)?;
645        Ok(())
646    }
647
648    async fn revoke_for_finalization(
649        &self,
650        execution_id: &ExecutionId,
651        caller: &runtime_types::CallerScope,
652        delegation: &ExecutionDelegationLease,
653    ) -> Result<(), KernelFailure> {
654        let operation = OperationContext {
655            id: OperationId::new(format!("{execution_id}:cleanup"))
656                .unwrap_or_else(|_| OperationId::random()),
657            execution_id: execution_id.clone(),
658            deadline: Instant::now() + Duration::from_secs(5),
659            cancellation: CancellationToken::new(),
660        };
661        self.session_factory
662            .revoke_delegation(&operation, caller, delegation)
663            .await
664            .map_err(|error| {
665                let mut failure = port_kernel_error(error);
666                failure.commit = CommitDisposition::Unknown;
667                failure
668            })?;
669        self.journal
670            .complete_delegation_cleanup(execution_id, &delegation.lease_ref)
671            .await
672            .map_err(journal_kernel_error)?;
673        Ok(())
674    }
675
676    async fn cleanup_delegation(&self, execution_id: &ExecutionId) {
677        let Ok(record) = self.journal.get(execution_id).await else {
678            return;
679        };
680        let Some(stored) = record
681            .delegation
682            .filter(|delegation| !delegation.cleanup_complete)
683        else {
684            return;
685        };
686        if !record.view.state.is_terminal() {
687            return;
688        }
689        let delegation = delegation_from_journal(stored);
690        let operation = OperationContext {
691            id: OperationId::new(format!("{execution_id}:cleanup"))
692                .unwrap_or_else(|_| OperationId::random()),
693            execution_id: execution_id.clone(),
694            deadline: Instant::now() + Duration::from_secs(5),
695            cancellation: CancellationToken::new(),
696        };
697        match self
698            .session_factory
699            .revoke_delegation(&operation, &record.caller, &delegation)
700            .await
701        {
702            Ok(()) => {
703                let _ = self
704                    .journal
705                    .complete_delegation_cleanup(execution_id, &delegation.lease_ref)
706                    .await;
707            }
708            Err(error) => tracing::warn!(
709                execution_id = %execution_id,
710                code = %error.code,
711                "delegation cleanup deferred to recovery"
712            ),
713        }
714    }
715
716    async fn finalize_failure(
717        &self,
718        execution_id: &ExecutionId,
719        failure: ExecutionFailure,
720    ) -> Result<ExecutionView, LinkError> {
721        let mut record = self.journal.get(execution_id).await.map_err(map_journal)?;
722        if record.view.state.is_terminal() {
723            return Ok(record.view);
724        }
725        if record.view.state != ExecutionState::Finalizing {
726            record = self
727                .journal
728                .transition(
729                    execution_id,
730                    &[
731                        ExecutionState::Queued,
732                        ExecutionState::Running,
733                        ExecutionState::WaitingForInput,
734                    ],
735                    ExecutionState::Finalizing,
736                )
737                .await
738                .map_err(map_journal)?;
739        }
740        if let Some(stored) = record
741            .delegation
742            .clone()
743            .filter(|delegation| !delegation.cleanup_complete)
744        {
745            self.revoke_for_finalization(
746                execution_id,
747                &record.caller,
748                &delegation_from_journal(stored),
749            )
750            .await
751            .map_err(|error| {
752                LinkError::Unavailable(format!("{}: {}", error.code, error.message))
753            })?;
754        }
755        let event = EventPayload::ExecutionFailed {
756            code: failure.code.clone(),
757            message: failure.message.clone(),
758        };
759        self.journal
760            .transition_with_event(
761                execution_id,
762                &[ExecutionState::Finalizing],
763                ExecutionState::Failed,
764                None,
765                Some(failure),
766                event,
767            )
768            .await
769            .map(|mutation| mutation.execution.view)
770            .map_err(map_journal)
771    }
772
773    async fn fail_queued(
774        &self,
775        execution_id: &ExecutionId,
776        code: &str,
777        message: &str,
778        retryable: bool,
779    ) -> Result<ExecutionView, LinkError> {
780        self.finalize_failure(
781            execution_id,
782            ExecutionFailure {
783                code: code.into(),
784                message: message.into(),
785                retryable,
786            },
787        )
788        .await
789    }
790
791    async fn prepare_delegation(
792        &self,
793        record: &runtime_link_api::JournalExecution,
794        authority: &RequestAuthority,
795    ) -> Result<runtime_link_api::JournalExecution, LinkError> {
796        if record.delegation.is_some() {
797            return Ok(record.clone());
798        }
799        let remaining_ms = record
800            .view
801            .created_at_ms
802            .saturating_add((record.request.options.deadline_seconds as i64) * 1000)
803            .saturating_sub(chrono::Utc::now().timestamp_millis())
804            .max(1) as u64;
805        let operation = OperationContext {
806            id: OperationId::new(format!("{}:delegate", record.view.id))
807                .map_err(|error| LinkError::Internal(error.to_string()))?,
808            execution_id: record.view.id.clone(),
809            deadline: Instant::now() + Duration::from_millis(remaining_ms),
810            cancellation: CancellationToken::new(),
811        };
812        let resolved = self
813            .instance_resolver
814            .resolve(
815                &operation,
816                &record.caller,
817                &record.request.runtime_instance_id,
818            )
819            .await
820            .map_err(map_port)?;
821        let resolved = apply_request_routing(resolved, &record.request);
822        let scope = SessionScope {
823            execution_id: record.view.id.clone(),
824            conversation_id: record.request.conversation_id.clone(),
825            resolved,
826        };
827        let delegation = self
828            .session_factory
829            .establish_delegation(&operation, authority, &scope)
830            .await
831            .map_err(map_port)?;
832        self.journal
833            .attach_delegation(&record.view.id, journal_delegation(&delegation))
834            .await
835            .map_err(map_journal)
836    }
837}
838
839#[async_trait]
840impl RuntimeLink for ExecutionCoordinator {
841    async fn create_execution(
842        &self,
843        authority: &RequestAuthority,
844        idempotency_key: &str,
845        request: CreateExecutionRequest,
846    ) -> Result<CreateExecutionResponse, LinkError> {
847        authority
848            .caller
849            .validate()
850            .map_err(|error| LinkError::Invalid(error.to_string()))?;
851        validate_create(idempotency_key, &request)?;
852        if matches!(
853            &request.input,
854            RuntimeInput::Messages { messages }
855                if messages.len() >= self.context_limits.max_messages
856        ) {
857            return Err(LinkError::Invalid(format!(
858                "request messages must contain fewer than {} items",
859                self.context_limits.max_messages
860            )));
861        }
862        if !self.accepting.load(Ordering::SeqCst) {
863            return Err(LinkError::Unavailable("runtime is shutting down".into()));
864        }
865        let now = chrono::Utc::now().timestamp_millis();
866        let view = ExecutionView {
867            id: ExecutionId::random(),
868            runtime_instance_id: request.runtime_instance_id.clone(),
869            conversation_id: request.conversation_id.clone(),
870            workspace_id: request.workspace_id.clone(),
871            model: request.model.clone(),
872            metadata: request.metadata.clone(),
873            state: ExecutionState::Queued,
874            outcome: None,
875            failure: None,
876            created_at_ms: now,
877            updated_at_ms: now,
878        };
879        match self
880            .journal
881            .reserve(NewJournalExecution {
882                idempotency_key: idempotency_key.into(),
883                caller: authority.caller.clone(),
884                request,
885                view,
886            })
887            .await
888            .map_err(map_journal)?
889        {
890            JournalReservation::Existing(record) => {
891                ensure_owner(&record.caller, authority)?;
892                let record = if record.view.state == ExecutionState::Queued {
893                    let prepared = self.prepare_delegation(&record, authority).await?;
894                    if let Ok(permit) = self.capacity.clone().try_acquire_owned() {
895                        self.authorities
896                            .lock()
897                            .await
898                            .insert(prepared.view.id.clone(), authority.clone());
899                        self.tasks
900                            .spawn(self.clone().run(prepared.view.id.clone(), permit));
901                    }
902                    prepared
903                } else {
904                    record
905                };
906                Ok(CreateExecutionResponse {
907                    execution: record.view,
908                    replayed: true,
909                })
910            }
911            JournalReservation::Created(record) => {
912                self.journal
913                    .append_event(&record.view.id, EventPayload::ExecutionQueued)
914                    .await
915                    .map_err(map_journal)?;
916                let record = self.prepare_delegation(&record, authority).await?;
917                let execution = if !self.accepting.load(Ordering::SeqCst) {
918                    let failed = self
919                        .fail_queued(
920                            &record.view.id,
921                            "RUNTIME_SHUTTING_DOWN",
922                            "runtime stopped accepting work before dispatch",
923                            true,
924                        )
925                        .await?;
926                    self.cleanup_delegation(&record.view.id).await;
927                    failed
928                } else if let Ok(permit) = self.capacity.clone().try_acquire_owned() {
929                    self.authorities
930                        .lock()
931                        .await
932                        .insert(record.view.id.clone(), authority.clone());
933                    let execution = record.view.clone();
934                    self.tasks
935                        .spawn(self.clone().run(execution.id.clone(), permit));
936                    execution
937                } else {
938                    let failed = self
939                        .fail_queued(
940                            &record.view.id,
941                            "CAPACITY_EXCEEDED",
942                            "runtime execution capacity is exhausted",
943                            true,
944                        )
945                        .await?;
946                    self.cleanup_delegation(&record.view.id).await;
947                    failed
948                };
949                Ok(CreateExecutionResponse {
950                    execution,
951                    replayed: false,
952                })
953            }
954        }
955    }
956
957    async fn execution(
958        &self,
959        authority: &RequestAuthority,
960        id: &ExecutionId,
961    ) -> Result<ExecutionView, LinkError> {
962        let record = self.journal.get(id).await.map_err(map_journal)?;
963        ensure_owner(&record.caller, authority)?;
964        Ok(record.view)
965    }
966
967    async fn events(
968        &self,
969        authority: &RequestAuthority,
970        id: &ExecutionId,
971        after: Option<u64>,
972        limit: usize,
973    ) -> Result<EventPage, LinkError> {
974        if limit == 0 || limit > 1000 {
975            return Err(LinkError::Invalid(
976                "event limit must be within 1..=1000".into(),
977            ));
978        }
979        ensure_owner(
980            &self.journal.get(id).await.map_err(map_journal)?.caller,
981            authority,
982        )?;
983        let items = self
984            .journal
985            .events(id, after, limit)
986            .await
987            .map_err(map_journal)?;
988        let next_after = items.last().map(|event| event.sequence);
989        Ok(EventPage { items, next_after })
990    }
991
992    async fn submit_input(
993        &self,
994        authority: &RequestAuthority,
995        id: &ExecutionId,
996        request: SubmitInputRequest,
997    ) -> Result<ExecutionView, LinkError> {
998        let record = self.journal.get(id).await.map_err(map_journal)?;
999        ensure_owner(&record.caller, authority)?;
1000        if record.view.state != ExecutionState::WaitingForInput {
1001            return Err(LinkError::Conflict(
1002                "execution is not waiting for input".into(),
1003            ));
1004        }
1005        let (request_id, text) = match request.input {
1006            RuntimeInput::ElicitationResponse { request_id, text } => (request_id, text),
1007            RuntimeInput::ToolApproval {
1008                request_id,
1009                approved,
1010            } => (
1011                request_id,
1012                if approved {
1013                    "approved".into()
1014                } else {
1015                    "denied".into()
1016                },
1017            ),
1018            RuntimeInput::UserMessage { .. } | RuntimeInput::Messages { .. } => {
1019                return Err(LinkError::Invalid(
1020                    "waiting input requires tool_approval or elicitation_response".into(),
1021                ));
1022            }
1023        };
1024        if request_id.is_empty() || request_id.len() > 256 || text.len() > 1024 * 1024 {
1025            return Err(LinkError::Invalid(
1026                "interaction request ID must be 1..=256 bytes and response at most 1 MiB".into(),
1027            ));
1028        }
1029        let operation = OperationContext {
1030            id: request.operation_id.clone(),
1031            execution_id: id.clone(),
1032            deadline: Instant::now() + Duration::from_secs(30),
1033            cancellation: CancellationToken::new(),
1034        };
1035        let mutation = self
1036            .journal
1037            .commit_interaction_input(id, &request.operation_id, &request_id, &text)
1038            .await
1039            .map_err(map_journal)?;
1040        if let Err(error) = self
1041            .session_factory
1042            .submit_input(
1043                &operation,
1044                &record.caller,
1045                id,
1046                &request.operation_id,
1047                &request_id,
1048                InteractionResponse { text },
1049            )
1050            .await
1051        {
1052            // The durable receipt remains replayable. A retry with the same
1053            // operation/body can wake a waiter that appeared after this race.
1054            return Err(map_port(error));
1055        }
1056        Ok(mutation.execution.view)
1057    }
1058
1059    async fn cancel(
1060        &self,
1061        authority: &RequestAuthority,
1062        id: &ExecutionId,
1063    ) -> Result<ExecutionView, LinkError> {
1064        let record = self.journal.get(id).await.map_err(map_journal)?;
1065        ensure_owner(&record.caller, authority)?;
1066        if record.view.state.is_terminal() {
1067            return Ok(record.view);
1068        }
1069        if let Some(token) = self.cancellations.lock().await.get(id) {
1070            token.cancel();
1071        }
1072        let record = if record.view.state == ExecutionState::Finalizing {
1073            record
1074        } else {
1075            self.journal
1076                .transition_with_event(
1077                    id,
1078                    &[
1079                        ExecutionState::Queued,
1080                        ExecutionState::Running,
1081                        ExecutionState::WaitingForInput,
1082                    ],
1083                    ExecutionState::Finalizing,
1084                    None,
1085                    None,
1086                    EventPayload::Warning {
1087                        code: "CANCELLATION_REQUESTED".into(),
1088                        message: "execution cancellation entered cleanup".into(),
1089                    },
1090                )
1091                .await
1092                .map_err(map_journal)?
1093                .execution
1094        };
1095        if let Some(stored) = record
1096            .delegation
1097            .clone()
1098            .filter(|delegation| !delegation.cleanup_complete)
1099        {
1100            self.revoke_for_finalization(id, &record.caller, &delegation_from_journal(stored))
1101                .await
1102                .map_err(|error| {
1103                    LinkError::Unavailable(format!("{}: {}", error.code, error.message))
1104                })?;
1105        }
1106        let updated = self
1107            .journal
1108            .transition_with_event(
1109                id,
1110                &[ExecutionState::Finalizing],
1111                ExecutionState::Canceled,
1112                None,
1113                None,
1114                EventPayload::ExecutionCanceled,
1115            )
1116            .await
1117            .map_err(map_journal)?;
1118        Ok(updated.execution.view)
1119    }
1120}
1121
1122fn journal_delegation(delegation: &ExecutionDelegationLease) -> JournalDelegation {
1123    JournalDelegation {
1124        lease_ref: delegation.lease_ref.clone(),
1125        expires_at_seconds: delegation.expires_at_seconds,
1126        revision: delegation.revision,
1127        cleanup_complete: false,
1128    }
1129}
1130
1131fn delegation_from_journal(delegation: JournalDelegation) -> ExecutionDelegationLease {
1132    ExecutionDelegationLease {
1133        lease_ref: delegation.lease_ref,
1134        expires_at_seconds: delegation.expires_at_seconds,
1135        revision: delegation.revision,
1136    }
1137}
1138
1139struct JournalKernelEventSink {
1140    execution_id: ExecutionId,
1141    journal: Arc<dyn ExecutionJournal>,
1142    trace: Arc<dyn TraceSession>,
1143    operation: OperationContext,
1144}
1145#[async_trait]
1146impl KernelEventSink for JournalKernelEventSink {
1147    async fn emit(&self, event: KernelEvent) -> Result<(), KernelFailure> {
1148        let payload = match event {
1149            // Link commits Running + ExecutionStarted before acquiring Kernel
1150            // resources. Kernel's local Started signal must not duplicate it.
1151            KernelEvent::Started => return Ok(()),
1152            KernelEvent::ModelStarted {
1153                turn,
1154                invocation_id,
1155            } => EventPayload::ModelStarted {
1156                turn,
1157                invocation_id,
1158            },
1159            KernelEvent::ModelCompleted {
1160                turn,
1161                invocation_id,
1162                finish,
1163                usage,
1164            } => EventPayload::ModelCompleted {
1165                turn,
1166                invocation_id,
1167                finish_reason: match finish {
1168                    runtime_ports::ModelFinish::Stop => "stop",
1169                    runtime_ports::ModelFinish::ToolCalls => "tool_calls",
1170                    runtime_ports::ModelFinish::Length => "length",
1171                    runtime_ports::ModelFinish::ContentFilter => "content_filter",
1172                    runtime_ports::ModelFinish::Other => "other",
1173                }
1174                .into(),
1175                input_tokens: usage.input_tokens,
1176                output_tokens: usage.output_tokens,
1177            },
1178            KernelEvent::ToolStarted { call_id, name } => {
1179                EventPayload::ToolStarted { call_id, name }
1180            }
1181            KernelEvent::ToolCompleted {
1182                call_id,
1183                name,
1184                failed,
1185            } => EventPayload::ToolCompleted {
1186                call_id,
1187                name,
1188                failed,
1189            },
1190            KernelEvent::Warning { code, message } => EventPayload::Warning { code, message },
1191        };
1192        let event_type = match &payload {
1193            EventPayload::ModelStarted { .. } => "runtime.model.started",
1194            EventPayload::ModelCompleted { .. } => "runtime.model.completed",
1195            EventPayload::ToolStarted { .. } => "runtime.tool.started",
1196            EventPayload::ToolCompleted { .. } => "runtime.tool.completed",
1197            EventPayload::Warning { .. } => "runtime.warning",
1198            _ => "runtime.event",
1199        };
1200        let trace_payload = serde_json::to_value(&payload)
1201            .map_err(|error| KernelFailure::new("EVENT_SERIALIZATION_FAILED", error.to_string()))?;
1202        self.journal
1203            .append_event(&self.execution_id, payload)
1204            .await
1205            .map_err(journal_kernel_error)?;
1206        // Trace is an observation projection. A trace outage must not roll back
1207        // an event that has already become authoritative in the journal.
1208        if let Err(error) = self
1209            .trace
1210            .append(&self.operation, event_type, trace_payload)
1211            .await
1212        {
1213            tracing::warn!(
1214                execution_id = %self.execution_id,
1215                code = %error.code,
1216                "trace projection failed after journal commit"
1217            );
1218        }
1219        Ok(())
1220    }
1221}
1222
1223struct JournaledInteractionSession {
1224    execution_id: ExecutionId,
1225    journal: Arc<dyn ExecutionJournal>,
1226    inner: Arc<dyn InteractionSession>,
1227}
1228
1229struct ChildExecutionSubagentSession {
1230    coordinator: ExecutionCoordinator,
1231    authority: Option<RequestAuthority>,
1232    parent_execution_id: ExecutionId,
1233    runtime_instance_id: runtime_types::RuntimeInstanceId,
1234    workspace_id: Option<runtime_types::WorkspaceId>,
1235    model: Option<String>,
1236    generation: ApiModelGenerationOptions,
1237}
1238
1239#[async_trait]
1240impl SubagentSession for ChildExecutionSubagentSession {
1241    async fn execute(
1242        &self,
1243        operation: &OperationContext,
1244        request: SubagentRequest,
1245    ) -> Result<SubagentOutcome, PortFailure> {
1246        let remaining = operation.remaining()?;
1247        let authority = self.authority.as_ref().ok_or_else(|| {
1248            PortFailure::new(
1249                PortFailureKind::Unavailable,
1250                "SUBAGENT_AUTHORITY_UNAVAILABLE",
1251                "recovered execution cannot widen its delegation with a new child execution",
1252            )
1253        })?;
1254        if request.prompt.trim().is_empty() || !(1..=256).contains(&request.max_model_turns) {
1255            return Err(PortFailure::new(
1256                PortFailureKind::Invalid,
1257                "SUBAGENT_REQUEST_INVALID",
1258                "subagent prompt and max_model_turns within 1..=256 are required",
1259            ));
1260        }
1261        let mut digest = Sha256::new();
1262        digest.update(self.parent_execution_id.as_str());
1263        digest.update(b":");
1264        digest.update(request.request_id.as_bytes());
1265        let idempotency_key = format!("subagent:{:x}", digest.finalize());
1266        let deadline_seconds = remaining.as_secs().saturating_add(1).clamp(1, 3600);
1267        let child = self
1268            .coordinator
1269            .create_execution(
1270                authority,
1271                &idempotency_key,
1272                CreateExecutionRequest {
1273                    runtime_instance_id: self.runtime_instance_id.clone(),
1274                    conversation_id: None,
1275                    input: RuntimeInput::UserMessage {
1276                        text: request.prompt,
1277                    },
1278                    workspace_id: self.workspace_id.clone(),
1279                    model: self.model.clone(),
1280                    instructions: None,
1281                    metadata: BTreeMap::new(),
1282                    generation: self.generation.clone(),
1283                    options: ExecutionOptions {
1284                        deadline_seconds,
1285                        max_model_turns: request.max_model_turns,
1286                        max_tool_calls: request.max_model_turns.saturating_mul(8).clamp(1, 2048),
1287                    },
1288                },
1289            )
1290            .await
1291            .map_err(link_port_error)?
1292            .execution;
1293        loop {
1294            if operation.cancellation.is_cancelled() {
1295                let _ = self.coordinator.cancel(authority, &child.id).await;
1296                return Err(PortFailure::canceled());
1297            }
1298            operation.remaining()?;
1299            let view = self
1300                .coordinator
1301                .execution(authority, &child.id)
1302                .await
1303                .map_err(link_port_error)?;
1304            match view.state {
1305                ExecutionState::Completed => {
1306                    return Ok(SubagentOutcome {
1307                        answer: view
1308                            .outcome
1309                            .map(|outcome| outcome.answer)
1310                            .unwrap_or_default(),
1311                    });
1312                }
1313                ExecutionState::Failed => {
1314                    let failure = view.failure.unwrap_or(ExecutionFailure {
1315                        code: "SUBAGENT_FAILED".into(),
1316                        message: "subagent failed without a failure payload".into(),
1317                        retryable: false,
1318                    });
1319                    let mut error = PortFailure::new(
1320                        PortFailureKind::Unavailable,
1321                        failure.code,
1322                        failure.message,
1323                    );
1324                    error.retryable = failure.retryable;
1325                    return Err(error);
1326                }
1327                ExecutionState::Canceled => return Err(PortFailure::canceled()),
1328                ExecutionState::WaitingForInput => {
1329                    let _ = self.coordinator.cancel(authority, &child.id).await;
1330                    return Err(PortFailure::new(
1331                        PortFailureKind::Conflict,
1332                        "SUBAGENT_INTERACTION_UNSUPPORTED",
1333                        "a nested execution cannot request direct user input",
1334                    ));
1335                }
1336                ExecutionState::Queued | ExecutionState::Running | ExecutionState::Finalizing => {}
1337            }
1338            tokio::select! {
1339                _ = operation.cancellation.cancelled() => {
1340                    let _ = self.coordinator.cancel(authority, &child.id).await;
1341                    return Err(PortFailure::canceled());
1342                }
1343                _ = tokio::time::sleep(Duration::from_millis(25)) => {}
1344            }
1345        }
1346    }
1347}
1348#[async_trait]
1349impl InteractionSession for JournaledInteractionSession {
1350    async fn prepare(
1351        &self,
1352        _: &OperationContext,
1353        _: InteractionRequest,
1354    ) -> Result<(), PortFailure> {
1355        Err(PortFailure::new(
1356            PortFailureKind::Internal,
1357            "INTERACTION_WRAPPER_PROTOCOL",
1358            "journaled interaction must use request()",
1359        ))
1360    }
1361
1362    async fn wait(
1363        &self,
1364        _: &OperationContext,
1365        _: &str,
1366    ) -> Result<InteractionResponse, PortFailure> {
1367        Err(PortFailure::new(
1368            PortFailureKind::Internal,
1369            "INTERACTION_WRAPPER_PROTOCOL",
1370            "journaled interaction must use request()",
1371        ))
1372    }
1373
1374    async fn request(
1375        &self,
1376        operation: &OperationContext,
1377        request: InteractionRequest,
1378    ) -> Result<InteractionResponse, PortFailure> {
1379        self.inner.prepare(operation, request.clone()).await?;
1380        self.journal
1381            .begin_interaction(&self.execution_id, &request.request_id, &request.prompt)
1382            .await
1383            .map_err(journal_port_error)?;
1384        let result = self.inner.wait(operation, &request.request_id).await;
1385        match result {
1386            Ok(response) => {
1387                self.journal
1388                    .complete_interaction(&self.execution_id, &request.request_id)
1389                    .await
1390                    .map_err(journal_port_error)?;
1391                Ok(response)
1392            }
1393            Err(error) => Err(error),
1394        }
1395    }
1396}
1397
1398fn validate_create(key: &str, request: &CreateExecutionRequest) -> Result<(), LinkError> {
1399    if key.is_empty() || key.len() > 256 {
1400        return Err(LinkError::Invalid(
1401            "Idempotency-Key must contain 1..=256 bytes".into(),
1402        ));
1403    }
1404    request
1405        .options
1406        .validate()
1407        .map_err(|message| LinkError::Invalid(message.into()))?;
1408    request
1409        .generation
1410        .validate()
1411        .map_err(|message| LinkError::Invalid(message.into()))?;
1412    if request
1413        .model
1414        .as_ref()
1415        .is_some_and(|model| model.trim().is_empty())
1416    {
1417        return Err(LinkError::Invalid("model must not be empty".into()));
1418    }
1419    if request
1420        .instructions
1421        .as_ref()
1422        .is_some_and(|instructions| instructions.len() > 256 * 1024)
1423    {
1424        return Err(LinkError::Invalid(
1425            "instructions must not exceed 256 KiB".into(),
1426        ));
1427    }
1428    let user_metadata_count = request
1429        .metadata
1430        .keys()
1431        .filter(|key| key.as_str() != "requestId")
1432        .count();
1433    if user_metadata_count > 16
1434        || request
1435            .metadata
1436            .iter()
1437            .any(|(key, value)| key.is_empty() || key.len() > 64 || value.len() > 512)
1438    {
1439        return Err(LinkError::Invalid(
1440            "metadata supports at most 16 entries with 1..=64 byte keys and values up to 512 bytes"
1441                .into(),
1442        ));
1443    }
1444    match &request.input {
1445        RuntimeInput::UserMessage { text }
1446            if !text.trim().is_empty() && text.len() <= 1024 * 1024 =>
1447        {
1448            Ok(())
1449        }
1450        RuntimeInput::Messages { messages }
1451            if !messages.is_empty()
1452                && messages.len() <= 256
1453                && messages.iter().any(|message| {
1454                    message.role == runtime_api_contract::RuntimeMessageRole::User
1455                        && !message.text.trim().is_empty()
1456                })
1457                && messages
1458                    .iter()
1459                    .all(|message| !message.text.trim().is_empty() && message.text.len() <= 1024 * 1024) =>
1460        {
1461            Ok(())
1462        }
1463        _ => Err(LinkError::Invalid(
1464            "initial input must contain 1..=256 non-empty text messages of at most 1 MiB each and at least one user role".into(),
1465        )),
1466    }
1467}
1468
1469fn request_messages(
1470    input: &RuntimeInput,
1471) -> Result<Vec<runtime_ports::ConversationMessage>, KernelFailure> {
1472    use runtime_api_contract::RuntimeMessageRole;
1473    use runtime_ports::ModelRole;
1474
1475    match input {
1476        RuntimeInput::UserMessage { text } => Ok(vec![runtime_ports::ConversationMessage {
1477            role: ModelRole::User,
1478            text: text.clone(),
1479        }]),
1480        RuntimeInput::Messages { messages } => Ok(messages
1481            .iter()
1482            .map(|message| runtime_ports::ConversationMessage {
1483                role: match message.role {
1484                    RuntimeMessageRole::System | RuntimeMessageRole::Developer => ModelRole::System,
1485                    RuntimeMessageRole::User => ModelRole::User,
1486                    RuntimeMessageRole::Assistant => ModelRole::Assistant,
1487                },
1488                text: message.text.clone(),
1489            })
1490            .collect()),
1491        RuntimeInput::ToolApproval { .. } | RuntimeInput::ElicitationResponse { .. } => {
1492            Err(KernelFailure::new(
1493                "INITIAL_INPUT_INVALID",
1494                "execution must start with messages",
1495            ))
1496        }
1497    }
1498}
1499
1500/// Apply task-scoped routing only after the immutable Runtime binding has been
1501/// resolved. Authorization is intentionally not decided here: the Agent Infra
1502/// delegation is established from the resulting scope and remains the source
1503/// of truth for model and workspace access.
1504fn apply_request_routing(
1505    mut resolved: ResolvedExecutionContext,
1506    request: &CreateExecutionRequest,
1507) -> ResolvedExecutionContext {
1508    if let Some(workspace_id) = &request.workspace_id {
1509        resolved.workspace_id = workspace_id.clone();
1510    }
1511    if let Some(model) = &request.model {
1512        resolved.model = model.clone();
1513    }
1514    if let Some(instructions) = &request.instructions {
1515        resolved
1516            .metadata
1517            .entry("instructions".into())
1518            .and_modify(|current| {
1519                current.push_str("\n\n");
1520                current.push_str(instructions);
1521            })
1522            .or_insert_with(|| instructions.clone());
1523    }
1524    for (key, value) in &request.metadata {
1525        resolved
1526            .metadata
1527            .insert(format!("task.{key}"), value.clone());
1528    }
1529    resolved
1530}
1531
1532fn ensure_owner(
1533    caller: &runtime_types::CallerScope,
1534    authority: &RequestAuthority,
1535) -> Result<(), LinkError> {
1536    if caller.subject == authority.caller.subject
1537        && caller.tenant_id == authority.caller.tenant_id
1538        && caller.project_id == authority.caller.project_id
1539    {
1540        Ok(())
1541    } else {
1542        Err(LinkError::Forbidden)
1543    }
1544}
1545
1546impl ExecutionCoordinator {
1547    fn kernel_limits(
1548        &self,
1549        options: &ExecutionOptions,
1550        generation: &ApiModelGenerationOptions,
1551    ) -> KernelLimits {
1552        let mut context = self.context_limits.clone();
1553        if let Some(max_output_tokens) = generation.max_output_tokens {
1554            context.reserved_output_tokens = max_output_tokens;
1555        }
1556        KernelLimits {
1557            max_model_turns: options.max_model_turns,
1558            max_tool_calls: options.max_tool_calls,
1559            context,
1560            max_tool_output_bytes: 256 * 1024,
1561        }
1562    }
1563}
1564
1565fn model_generation_options(options: &ApiModelGenerationOptions) -> ModelGenerationOptions {
1566    ModelGenerationOptions {
1567        max_output_tokens: options.max_output_tokens,
1568        temperature: options.temperature,
1569        stop_sequences: options.stop_sequences.clone(),
1570        response_format: options.response_format.clone(),
1571        tool_choice: match &options.tool_choice {
1572            ApiModelToolChoice::Auto => ModelToolChoice::Auto,
1573            ApiModelToolChoice::None => ModelToolChoice::None,
1574        },
1575    }
1576}
1577fn map_journal(error: JournalError) -> LinkError {
1578    match error {
1579        JournalError::NotFound => LinkError::NotFound,
1580        JournalError::Conflict => LinkError::Conflict("journal compare-and-set failed".into()),
1581        JournalError::Unavailable(message) => LinkError::Unavailable(message),
1582    }
1583}
1584fn map_port(error: PortFailure) -> LinkError {
1585    LinkError::Unavailable(format!("{}: {}", error.code, error.message))
1586}
1587fn link_port_error(error: LinkError) -> PortFailure {
1588    match error {
1589        LinkError::Invalid(message) => {
1590            PortFailure::new(PortFailureKind::Invalid, "SUBAGENT_INVALID", message)
1591        }
1592        LinkError::NotFound => PortFailure::new(
1593            PortFailureKind::NotFound,
1594            "SUBAGENT_NOT_FOUND",
1595            "nested execution was not found",
1596        ),
1597        LinkError::Forbidden => PortFailure::new(
1598            PortFailureKind::Forbidden,
1599            "SUBAGENT_FORBIDDEN",
1600            "nested execution is forbidden",
1601        ),
1602        LinkError::Conflict(message) => {
1603            PortFailure::new(PortFailureKind::Conflict, "SUBAGENT_CONFLICT", message)
1604        }
1605        LinkError::Overloaded => PortFailure::new(
1606            PortFailureKind::Unavailable,
1607            "SUBAGENT_OVERLOADED",
1608            "nested execution capacity is exhausted",
1609        ),
1610        LinkError::Unavailable(message) => PortFailure::new(
1611            PortFailureKind::Unavailable,
1612            "SUBAGENT_UNAVAILABLE",
1613            message,
1614        ),
1615        LinkError::Internal(message) => {
1616            PortFailure::new(PortFailureKind::Internal, "SUBAGENT_INTERNAL", message)
1617        }
1618    }
1619}
1620fn journal_kernel_error(error: JournalError) -> KernelFailure {
1621    KernelFailure::new("JOURNAL_FAILURE", error.to_string())
1622}
1623fn port_kernel_error(error: PortFailure) -> KernelFailure {
1624    KernelFailure {
1625        code: error.code,
1626        message: error.message,
1627        retryable: error.retryable,
1628        commit: error.commit,
1629    }
1630}
1631fn journal_port_error(error: JournalError) -> PortFailure {
1632    PortFailure::new(
1633        runtime_ports::PortFailureKind::Unavailable,
1634        "JOURNAL_FAILURE",
1635        error.to_string(),
1636    )
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641    use std::{collections::BTreeMap, sync::Arc};
1642
1643    use agent_runtime_code_agent::definition as code_agent_definition;
1644    use agent_runtime_testkit::{ScriptedModelSession, sessions};
1645    use runtime_adapter_execution_journal::MemoryExecutionJournal;
1646    use runtime_agent_definition_loader::DefinitionCatalogResolver;
1647    use runtime_api_contract::{
1648        CreateExecutionRequest, ExecutionOptions, ExecutionState, RuntimeInput,
1649    };
1650    use runtime_kernel::AgentKernel;
1651    use runtime_link_api::RuntimeLink;
1652    use runtime_ports::{
1653        CommitDisposition, ExecutionSessions, InteractionResponse, ModelContent, ModelFinish,
1654        ModelResponse, PortFailureKind, ResolvedExecutionContext, TokenUsage,
1655    };
1656    use runtime_types::{
1657        CallerScope, CredentialHandle, DelegationLeaseRef, RuntimeInstanceId, WorkspaceId,
1658    };
1659    use serde_json::json;
1660
1661    use super::*;
1662
1663    struct FixtureResolver;
1664    #[async_trait]
1665    impl RuntimeInstanceResolver for FixtureResolver {
1666        async fn resolve(
1667            &self,
1668            _: &OperationContext,
1669            _: &CallerScope,
1670            id: &RuntimeInstanceId,
1671        ) -> Result<ResolvedExecutionContext, PortFailure> {
1672            Ok(ResolvedExecutionContext {
1673                runtime_instance_id: id.clone(),
1674                agent_id: "agt_test".into(),
1675                runtime_type: "code".into(),
1676                model: "fixture".into(),
1677                workspace_id: WorkspaceId::new("workspace-1").unwrap(),
1678                definition_id: "runtime-code-agent".into(),
1679                definition_version: code_agent_definition()
1680                    .expect("valid Code Agent definition")
1681                    .manifest
1682                    .version
1683                    .clone(),
1684                definition_digest: None,
1685                metadata: BTreeMap::new(),
1686            })
1687        }
1688    }
1689
1690    fn code_agent_resolver() -> Arc<dyn AgentDefinitionResolver> {
1691        let definition = code_agent_definition().expect("valid Code Agent definition");
1692        Arc::new(
1693            DefinitionCatalogResolver::new([definition])
1694                .expect("single Code Agent definition catalog"),
1695        )
1696    }
1697
1698    struct FixtureFactory {
1699        model: Arc<dyn runtime_ports::ModelSession>,
1700    }
1701
1702    struct HangingModel;
1703    #[async_trait]
1704    impl runtime_ports::ModelSession for HangingModel {
1705        async fn invoke(
1706            &self,
1707            operation: &OperationContext,
1708            _: runtime_ports::ModelRequest,
1709        ) -> Result<ModelResponse, PortFailure> {
1710            operation.cancellation.cancelled().await;
1711            Err(PortFailure::canceled())
1712        }
1713    }
1714
1715    struct CommitUnknownModel;
1716    #[async_trait]
1717    impl runtime_ports::ModelSession for CommitUnknownModel {
1718        async fn invoke(
1719            &self,
1720            _: &OperationContext,
1721            _: runtime_ports::ModelRequest,
1722        ) -> Result<ModelResponse, PortFailure> {
1723            let mut failure = PortFailure::new(
1724                PortFailureKind::Unavailable,
1725                "MODEL_TRANSPORT_UNKNOWN",
1726                "model transport failed after dispatch",
1727            );
1728            failure.commit = CommitDisposition::Unknown;
1729            Err(failure)
1730        }
1731    }
1732    #[async_trait]
1733    impl ExecutionSessionFactory for FixtureFactory {
1734        async fn establish_delegation(
1735            &self,
1736            _: &OperationContext,
1737            _: &RequestAuthority,
1738            _: &SessionScope,
1739        ) -> Result<ExecutionDelegationLease, PortFailure> {
1740            Ok(ExecutionDelegationLease {
1741                lease_ref: DelegationLeaseRef::new("edl_fixture").unwrap(),
1742                expires_at_seconds: u64::MAX,
1743                revision: 1,
1744            })
1745        }
1746
1747        async fn create(
1748            &self,
1749            _: &OperationContext,
1750            _: &CallerScope,
1751            _: &ExecutionDelegationLease,
1752            _: &SessionScope,
1753        ) -> Result<ExecutionSessions, PortFailure> {
1754            Ok(sessions(self.model.clone()))
1755        }
1756        async fn submit_input(
1757            &self,
1758            _: &OperationContext,
1759            _: &CallerScope,
1760            _: &ExecutionId,
1761            _: &OperationId,
1762            _: &str,
1763            _: InteractionResponse,
1764        ) -> Result<(), PortFailure> {
1765            Err(PortFailure::new(
1766                PortFailureKind::Conflict,
1767                "NO_INTERACTION",
1768                "fixture has no pending interaction",
1769            ))
1770        }
1771
1772        async fn renew_delegation(
1773            &self,
1774            _: &OperationContext,
1775            _: &CallerScope,
1776            delegation: &ExecutionDelegationLease,
1777        ) -> Result<ExecutionDelegationLease, PortFailure> {
1778            Ok(delegation.clone())
1779        }
1780
1781        async fn revoke_delegation(
1782            &self,
1783            _: &OperationContext,
1784            _: &CallerScope,
1785            _: &ExecutionDelegationLease,
1786        ) -> Result<(), PortFailure> {
1787            Ok(())
1788        }
1789    }
1790
1791    #[tokio::test]
1792    async fn create_runs_code_distribution_to_durable_terminal_state() {
1793        let model = Arc::new(ScriptedModelSession::new(vec![ModelResponse {
1794            output: vec![ModelContent::ToolUse {
1795                id: "call-1".into(),
1796                name: "runtime.complete".into(),
1797                arguments: json!({"answer":"complete"}),
1798            }],
1799            finish: ModelFinish::ToolCalls,
1800            usage: TokenUsage::default(),
1801        }]));
1802        let coordinator = ExecutionCoordinator::new(
1803            Arc::new(MemoryExecutionJournal::new(100)),
1804            Arc::new(FixtureResolver),
1805            Arc::new(FixtureFactory { model }),
1806            code_agent_resolver(),
1807            Arc::new(AgentKernel),
1808            2,
1809        )
1810        .unwrap();
1811        let authority = RequestAuthority {
1812            caller: CallerScope {
1813                subject: "user-1".into(),
1814                tenant_id: "tenant-1".into(),
1815                project_id: "project-1".into(),
1816                capabilities: Vec::new(),
1817            },
1818            credential: CredentialHandle::new("test-token").unwrap(),
1819        };
1820        let created = coordinator
1821            .create_execution(
1822                &authority,
1823                "test-key",
1824                CreateExecutionRequest {
1825                    runtime_instance_id: RuntimeInstanceId::new("runtime-1").unwrap(),
1826                    conversation_id: None,
1827                    input: RuntimeInput::UserMessage {
1828                        text: "do it".into(),
1829                    },
1830                    workspace_id: None,
1831                    model: None,
1832                    instructions: None,
1833                    metadata: BTreeMap::new(),
1834                    generation: ApiModelGenerationOptions::default(),
1835                    options: ExecutionOptions::default(),
1836                },
1837            )
1838            .await
1839            .unwrap();
1840        let mut view = created.execution;
1841        for _ in 0..100 {
1842            view = coordinator.execution(&authority, &view.id).await.unwrap();
1843            if view.state.is_terminal() {
1844                break;
1845            }
1846            tokio::time::sleep(Duration::from_millis(10)).await;
1847        }
1848        assert_eq!(view.state, ExecutionState::Completed, "{view:?}");
1849        assert_eq!(view.outcome.unwrap().answer, "complete");
1850        let events = coordinator
1851            .events(&authority, &view.id, None, 100)
1852            .await
1853            .unwrap();
1854        assert!(matches!(
1855            events.items.first().unwrap().payload,
1856            EventPayload::ExecutionQueued
1857        ));
1858        assert!(matches!(
1859            events.items.last().unwrap().payload,
1860            EventPayload::ExecutionCompleted { .. }
1861        ));
1862    }
1863
1864    #[tokio::test]
1865    async fn recovery_resumes_queued_execution_from_durable_delegation_without_caller_token() {
1866        let model = Arc::new(ScriptedModelSession::new(vec![ModelResponse {
1867            output: vec![ModelContent::ToolUse {
1868                id: "call-recovered".into(),
1869                name: "runtime.complete".into(),
1870                arguments: json!({"answer":"recovered"}),
1871            }],
1872            finish: ModelFinish::ToolCalls,
1873            usage: TokenUsage::default(),
1874        }]));
1875        let journal = Arc::new(MemoryExecutionJournal::new(100));
1876        let request = CreateExecutionRequest {
1877            runtime_instance_id: RuntimeInstanceId::new("runtime-1").unwrap(),
1878            conversation_id: None,
1879            input: RuntimeInput::UserMessage {
1880                text: "resume".into(),
1881            },
1882            workspace_id: None,
1883            model: None,
1884            instructions: None,
1885            metadata: BTreeMap::new(),
1886            generation: ApiModelGenerationOptions::default(),
1887            options: ExecutionOptions::default(),
1888        };
1889        let now = chrono::Utc::now().timestamp_millis();
1890        let id = ExecutionId::random();
1891        journal
1892            .reserve(NewJournalExecution {
1893                idempotency_key: "recovery-key".into(),
1894                caller: CallerScope {
1895                    subject: "user-1".into(),
1896                    tenant_id: "tenant-1".into(),
1897                    project_id: "project-1".into(),
1898                    capabilities: Vec::new(),
1899                },
1900                request: request.clone(),
1901                view: runtime_api_contract::ExecutionView {
1902                    id: id.clone(),
1903                    runtime_instance_id: request.runtime_instance_id.clone(),
1904                    conversation_id: None,
1905                    workspace_id: request.workspace_id.clone(),
1906                    model: request.model.clone(),
1907                    metadata: request.metadata.clone(),
1908                    state: ExecutionState::Queued,
1909                    outcome: None,
1910                    failure: None,
1911                    created_at_ms: now,
1912                    updated_at_ms: now,
1913                },
1914            })
1915            .await
1916            .unwrap();
1917        journal
1918            .attach_delegation(
1919                &id,
1920                JournalDelegation {
1921                    lease_ref: DelegationLeaseRef::new("edl_recovered").unwrap(),
1922                    expires_at_seconds: u64::MAX,
1923                    revision: 1,
1924                    cleanup_complete: false,
1925                },
1926            )
1927            .await
1928            .unwrap();
1929        let coordinator = ExecutionCoordinator::new(
1930            journal.clone(),
1931            Arc::new(FixtureResolver),
1932            Arc::new(FixtureFactory { model }),
1933            code_agent_resolver(),
1934            Arc::new(AgentKernel),
1935            2,
1936        )
1937        .unwrap();
1938        assert_eq!(coordinator.recover(10).await.unwrap(), 1);
1939        for _ in 0..100 {
1940            let record = journal.get(&id).await.unwrap();
1941            if record.view.state.is_terminal() {
1942                assert_eq!(record.view.state, ExecutionState::Completed);
1943                assert_eq!(record.view.outcome.unwrap().answer, "recovered");
1944                assert!(record.delegation.unwrap().cleanup_complete);
1945                return;
1946            }
1947            tokio::time::sleep(Duration::from_millis(10)).await;
1948        }
1949        panic!("recovered execution did not reach terminal state");
1950    }
1951
1952    #[tokio::test]
1953    async fn periodic_recovery_picks_up_claim_that_expires_after_startup_scan() {
1954        let model = Arc::new(ScriptedModelSession::new(vec![ModelResponse {
1955            output: vec![ModelContent::ToolUse {
1956                id: "call-delayed-recovery".into(),
1957                name: "runtime.complete".into(),
1958                arguments: json!({"answer":"delayed recovery"}),
1959            }],
1960            finish: ModelFinish::ToolCalls,
1961            usage: TokenUsage::default(),
1962        }]));
1963        let journal = Arc::new(MemoryExecutionJournal::new(100));
1964        let request = request("resume after claim expiry");
1965        let now = chrono::Utc::now().timestamp_millis();
1966        let id = ExecutionId::random();
1967        journal
1968            .reserve(NewJournalExecution {
1969                idempotency_key: "delayed-recovery-key".into(),
1970                caller: authority().caller,
1971                request: request.clone(),
1972                view: ExecutionView {
1973                    id: id.clone(),
1974                    runtime_instance_id: request.runtime_instance_id.clone(),
1975                    conversation_id: None,
1976                    workspace_id: request.workspace_id.clone(),
1977                    model: request.model.clone(),
1978                    metadata: request.metadata.clone(),
1979                    state: ExecutionState::Queued,
1980                    outcome: None,
1981                    failure: None,
1982                    created_at_ms: now,
1983                    updated_at_ms: now,
1984                },
1985            })
1986            .await
1987            .unwrap();
1988        journal
1989            .attach_delegation(
1990                &id,
1991                JournalDelegation {
1992                    lease_ref: DelegationLeaseRef::new("edl_delayed_recovery").unwrap(),
1993                    expires_at_seconds: u64::MAX,
1994                    revision: 1,
1995                    cleanup_complete: false,
1996                },
1997            )
1998            .await
1999            .unwrap();
2000        journal
2001            .claim(&id, "dead-worker", now, now + 50)
2002            .await
2003            .unwrap();
2004        let coordinator = ExecutionCoordinator::new(
2005            journal.clone(),
2006            Arc::new(FixtureResolver),
2007            Arc::new(FixtureFactory { model }),
2008            code_agent_resolver(),
2009            Arc::new(AgentKernel),
2010            1,
2011        )
2012        .unwrap();
2013        assert_eq!(coordinator.recover(10).await.unwrap(), 0);
2014        coordinator
2015            .start_recovery_loop(Duration::from_millis(10), 10)
2016            .unwrap();
2017        for _ in 0..100 {
2018            let record = journal.get(&id).await.unwrap();
2019            if record.view.state == ExecutionState::Completed {
2020                assert_eq!(record.view.outcome.unwrap().answer, "delayed recovery");
2021                assert!(coordinator.shutdown(Duration::from_secs(1)).await);
2022                return;
2023            }
2024            tokio::time::sleep(Duration::from_millis(10)).await;
2025        }
2026        coordinator.begin_shutdown();
2027        panic!("periodic recovery did not pick up the expired claim");
2028    }
2029
2030    #[tokio::test]
2031    async fn shutdown_cancels_registered_execution_tasks_after_grace() {
2032        let journal = Arc::new(MemoryExecutionJournal::new(100));
2033        let coordinator = ExecutionCoordinator::new(
2034            journal.clone(),
2035            Arc::new(FixtureResolver),
2036            Arc::new(FixtureFactory {
2037                model: Arc::new(HangingModel),
2038            }),
2039            code_agent_resolver(),
2040            Arc::new(AgentKernel),
2041            1,
2042        )
2043        .unwrap();
2044        let authority = authority();
2045        let created = coordinator
2046            .create_execution(&authority, "shutdown", request("wait"))
2047            .await
2048            .unwrap()
2049            .execution;
2050        for _ in 0..100 {
2051            if coordinator
2052                .execution(&authority, &created.id)
2053                .await
2054                .unwrap()
2055                .state
2056                == ExecutionState::Running
2057            {
2058                break;
2059            }
2060            tokio::time::sleep(Duration::from_millis(5)).await;
2061        }
2062        assert!(coordinator.shutdown(Duration::from_millis(1)).await);
2063        assert_eq!(coordinator.active_executions(), 0);
2064        let terminal = coordinator
2065            .execution(&authority, &created.id)
2066            .await
2067            .unwrap();
2068        assert_eq!(terminal.state, ExecutionState::Failed);
2069        assert_eq!(terminal.failure.unwrap().code, "CANCELED");
2070        assert!(
2071            journal
2072                .get(&created.id)
2073                .await
2074                .unwrap()
2075                .delegation
2076                .unwrap()
2077                .cleanup_complete,
2078            "Failed must imply durable delegation cleanup"
2079        );
2080    }
2081
2082    #[tokio::test]
2083    async fn commit_unknown_stays_finalizing_for_recovery() {
2084        let coordinator = ExecutionCoordinator::new(
2085            Arc::new(MemoryExecutionJournal::new(100)),
2086            Arc::new(FixtureResolver),
2087            Arc::new(FixtureFactory {
2088                model: Arc::new(CommitUnknownModel),
2089            }),
2090            code_agent_resolver(),
2091            Arc::new(AgentKernel),
2092            1,
2093        )
2094        .unwrap();
2095        let authority = authority();
2096        let execution = coordinator
2097            .create_execution(&authority, "commit-unknown", request("invoke"))
2098            .await
2099            .unwrap()
2100            .execution;
2101        for _ in 0..100 {
2102            let view = coordinator
2103                .execution(&authority, &execution.id)
2104                .await
2105                .unwrap();
2106            if view.state == ExecutionState::Finalizing && coordinator.active_executions() == 0 {
2107                break;
2108            }
2109            tokio::time::sleep(Duration::from_millis(5)).await;
2110        }
2111        let view = coordinator
2112            .execution(&authority, &execution.id)
2113            .await
2114            .unwrap();
2115        assert_eq!(view.state, ExecutionState::Finalizing);
2116        assert!(view.failure.is_none());
2117        let events = coordinator
2118            .events(&authority, &execution.id, None, 100)
2119            .await
2120            .unwrap();
2121        assert!(matches!(
2122            events.items.last().unwrap().payload,
2123            EventPayload::Warning { ref code, .. } if code == "COMMIT_DISPOSITION_UNKNOWN"
2124        ));
2125    }
2126
2127    fn authority() -> RequestAuthority {
2128        RequestAuthority {
2129            caller: CallerScope {
2130                subject: "user-1".into(),
2131                tenant_id: "tenant-1".into(),
2132                project_id: "project-1".into(),
2133                capabilities: Vec::new(),
2134            },
2135            credential: CredentialHandle::new("test-token").unwrap(),
2136        }
2137    }
2138
2139    fn request(prompt: &str) -> CreateExecutionRequest {
2140        CreateExecutionRequest {
2141            runtime_instance_id: RuntimeInstanceId::new("runtime-1").unwrap(),
2142            conversation_id: None,
2143            input: RuntimeInput::UserMessage {
2144                text: prompt.into(),
2145            },
2146            workspace_id: None,
2147            model: None,
2148            instructions: None,
2149            metadata: BTreeMap::new(),
2150            generation: ApiModelGenerationOptions::default(),
2151            options: ExecutionOptions::default(),
2152        }
2153    }
2154
2155    #[test]
2156    fn task_routing_overrides_workspace_model_and_augments_instructions() {
2157        let mut execution = request("inspect");
2158        execution.workspace_id = Some(WorkspaceId::new("workspace-2").unwrap());
2159        execution.model = Some("model-2".into());
2160        execution.instructions = Some("request instruction".into());
2161        execution
2162            .metadata
2163            .insert("traceId".into(), "trace-1".into());
2164        let mut metadata = BTreeMap::new();
2165        metadata.insert("instructions".into(), "binding instruction".into());
2166        let resolved = apply_request_routing(
2167            ResolvedExecutionContext {
2168                runtime_instance_id: RuntimeInstanceId::new("runtime-1").unwrap(),
2169                agent_id: "agt_test".into(),
2170                runtime_type: "test".into(),
2171                model: "model-1".into(),
2172                workspace_id: WorkspaceId::new("workspace-1").unwrap(),
2173                definition_id: "definition-1".into(),
2174                definition_version: "1".into(),
2175                definition_digest: None,
2176                metadata,
2177            },
2178            &execution,
2179        );
2180        assert_eq!(resolved.workspace_id.as_str(), "workspace-2");
2181        assert_eq!(resolved.model, "model-2");
2182        assert_eq!(
2183            resolved.metadata["instructions"],
2184            "binding instruction\n\nrequest instruction"
2185        );
2186        assert_eq!(resolved.metadata["task.traceId"], "trace-1");
2187    }
2188}