Skip to main content

cookie_agent_engine/
runtime.rs

1use std::{
2    collections::{BTreeMap, HashMap, HashSet},
3    path::PathBuf,
4    sync::{
5        Arc, Mutex,
6        atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
7    },
8};
9
10use arc_swap::ArcSwap;
11use cookie_agent_config::LoadedConfiguration;
12use cookie_agent_identity::ModelKey;
13use cookie_agent_models::{ModelManager, manifests::ManifestError};
14use cookie_agent_protocol::{
15    AgentId, ApprovalId, ApprovalInternalDecisionKind, ApprovalRequest, ApprovalRespondErrorCode,
16    ApprovalRespondParams, ApprovalRespondResult, ApprovalStatus, EventPayload as Event,
17    EventSubscriptionMessage, EventsSubscribeResult, InvocationId, OperationFingerprint,
18    PermissionMode, PersistedModelTurn, PersistedToolResult as ToolResult, ProviderConnectParams,
19    ProviderConnectResult, ProviderDisconnectParams, ProviderDisconnectResult, RunCancelResult,
20    RunId, RunRecallSteerResult, RunStartParams, RunStartResult, RunSteerResult,
21    RunToolStdinParams, RunToolStdinResult, RuntimeChangeReason, RuntimeChangedNotification,
22    RuntimeSnapshotResult, SafeCode, SessionId, SessionMeta, SessionRenameParams,
23    SessionRenameResult, SessionRevertResult, ToolCallId, ToolCallPresentation,
24};
25use oven_sdk::{ModelError, ToolDefinition};
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use thiserror::Error;
29use tokio::{
30    sync::{broadcast, mpsc, oneshot},
31    task::JoinHandle,
32};
33use tokio_util::sync::CancellationToken;
34
35use crate::{
36    delegation_events::{DelegationEventError, DelegationEventStore},
37    events::{self, EventLogError},
38    grant_journal::{GrantInvalidationJournal, GrantJournalError},
39    model_history,
40    model_snapshots::prepare_runtime_manifest,
41    permissions::PermissionPipeline,
42    policy::FrozenRunPolicy,
43    runtime_snapshot::{
44        AgentRegistry, PublishedRuntime, RuntimePublication, build_runtime_snapshot,
45    },
46    session::{SessionError, SessionStore},
47    tool_api::ToolPreparationContext,
48};
49
50mod admission;
51mod agent_md;
52#[cfg(test)]
53pub(crate) use agent_md::AGENT_MD_MAX_BYTES;
54mod approval_api;
55mod approval_flow;
56mod approval_projection;
57mod artifact_reads;
58pub(crate) mod artifacts;
59mod blocking_io;
60pub(crate) mod compaction;
61mod delegation;
62mod get_history;
63pub(crate) mod handles;
64mod helpers;
65mod internal_agents;
66mod mailbox;
67pub(crate) mod messaging_api;
68mod model_loop;
69mod output_capture;
70pub(crate) mod plugin_diagnostics;
71mod producer_claims;
72pub(crate) mod producers;
73mod prompt_blocks;
74mod recovery;
75mod residency;
76mod runs;
77mod sessions;
78mod skills;
79/// Test-only hooks; the module body is `#![cfg(test)]`.
80pub(crate) mod test_hooks;
81#[cfg(test)]
82mod tests;
83mod titles;
84pub(crate) mod tool_execution;
85mod tool_prompts;
86mod working_directory;
87
88pub use artifact_reads::ArtifactReadPage;
89pub(crate) use artifact_reads::read_artifact_async;
90pub(crate) use artifacts::ArtifactRouter;
91pub(crate) use delegation::render_subagent_notification;
92
93/// Session id for artifact/capture tests that do not model tree placement.
94#[cfg(test)]
95pub(crate) fn test_session_id() -> cookie_agent_protocol::SessionId {
96    cookie_agent_protocol::SessionId::new_v7()
97}
98use approval_flow::{
99    ApprovalEvaluationTransition, ApprovalRuntimeState, ApprovalTerminal, ApprovalToolInput,
100    ModelApprovalInput, PreparedApprovalInvalidation,
101};
102pub(crate) use approval_flow::{ApprovalOutcome, PendingApproval};
103#[cfg(test)]
104pub(crate) use artifacts::ArtifactStore;
105#[cfg(test)]
106pub(crate) use blocking_io::gate as block_artifact_io_for_test;
107#[cfg_attr(not(test), allow(unused_imports))]
108pub(crate) use compaction::ContextTokenEstimator;
109use compaction::{
110    CompactionDeferredKind, CompactionState, PredictiveCompactionInput,
111    should_run_predictive_compaction,
112};
113use delegation::DelegationRuntimeState;
114pub use get_history::EngineHistoryView;
115use helpers::safe_code;
116pub(crate) use internal_agents::FrozenInternalAgentPolicy;
117#[cfg_attr(not(test), allow(unused_imports))]
118pub(crate) use internal_agents::InternalAgentLimits;
119use internal_agents::{InternalAgentExecution, InternalAgentHistoryInput};
120use mailbox::SessionRuntimeState;
121pub use messaging_api::{AgentMessageHandle, AgentMessageInvocation, AgentRecipientState};
122pub(crate) use output_capture::OutputCapture;
123use output_capture::OutputState;
124pub(crate) use output_capture::finish_page;
125use plugin_diagnostics::{PluginDiagnosticsState, run_plugin_diagnostic_aggregator};
126pub use skills::SkillInvocation;
127use skills::SkillRuntimeState;
128#[cfg(test)]
129pub(crate) use test_hooks::*;
130
131// A terminal result was finalized successfully before cancellation won its commit.
132pub(crate) const CANCELLED_AFTER_COMPLETION: &str = "cancelled_after_completion";
133
134use crate::tool_api::{
135    PreparedExecutorCell, PreparedSerializationKey, PreparedTool, StdinWrite, ToolCall,
136    ToolConcurrency, ToolError, ToolProvider, ToolSpec, TurnAgentContext,
137};
138
139#[derive(Clone)]
140pub struct EngineOptions {
141    pub data_dir: PathBuf,
142    pub cwd: PathBuf,
143    pub config: LoadedConfiguration,
144    pub model_manager: Arc<ModelManager>,
145    pub tools: Vec<Arc<dyn ToolProvider>>,
146}
147
148#[derive(Debug, Error)]
149pub enum EngineError {
150    // Not `transparent`: the manual `From<SessionError>` below unwraps lazy tree
151    // load rejections back to their engine-side type, and a transparent variant
152    // may not carry an explicit `#[source]`.
153    #[error("{0}")]
154    Session(#[source] SessionError),
155    #[error(transparent)]
156    DelegationEvents(#[from] DelegationEventError),
157    #[error(transparent)]
158    Event(#[from] EventLogError),
159    #[error(transparent)]
160    GrantJournal(#[from] GrantJournalError),
161    #[error("tool output storage error: {0}")]
162    ToolOutput(#[from] std::io::Error),
163    #[error("AGENTS.md context read failed at {path}: {source}")]
164    AgentMdIo {
165        path: PathBuf,
166        #[source]
167        source: std::io::Error,
168    },
169    #[error("configuration error: {0}")]
170    Config(#[source] Box<cookie_agent_config::ConfigError>),
171    #[error("agent `{0}` is not eligible in this session origin")]
172    IneligibleAgent(AgentId),
173    #[error(
174        "agent `{agent}` references unknown model `{model}`; refresh models.dev or choose an available model"
175    )]
176    UnknownAgentModel { agent: AgentId, model: ModelKey },
177    #[error(
178        "agent `{agent}` references catalog model `{model}`, but it is unavailable in the compiled runtime"
179    )]
180    UnavailableAgentModel { agent: AgentId, model: ModelKey },
181    #[error("agent `{0}` is disabled")]
182    DisabledAgent(AgentId),
183    #[error("run {0} not found")]
184    MissingRun(RunId),
185    #[error("session {0} is already running")]
186    SessionRunning(SessionId),
187    #[error("client run id conflicts with durable run parameters")]
188    RunIdempotencyConflict,
189    #[error("input was handled by plugin: {0}")]
190    InputHandled(String),
191    #[error("model selection was blocked by plugin: {0}")]
192    ModelSelectionBlocked(String),
193    #[error("session operation was blocked by plugin: {0}")]
194    SessionOperationBlocked(String),
195    #[error("compaction cancelled by plugin: {0}")]
196    CompactionCancelled(String),
197    #[error("tool call is not running or is not interactive")]
198    StdinUnavailable,
199    #[error("approval `{approval_id}` is not pending for session {session_id}")]
200    ApprovalNotPending {
201        session_id: SessionId,
202        approval_id: ApprovalId,
203    },
204    #[error("approval response conflicts with durable approval state")]
205    ApprovalConflict,
206    #[error("approval response was rejected: {0:?}")]
207    ApprovalResponse(Box<ApprovalRespondFailure>),
208    #[error("client rename id conflicts with a durable rename operation")]
209    RenameConflict,
210    #[error("invalid base64 stdin: {0}")]
211    Base64(#[from] base64::DecodeError),
212    #[error("model failure: {0}")]
213    Model(Box<ModelError>),
214    #[error("model history failure: {0}")]
215    ModelHistory(#[from] model_history::HistoryError),
216    /// A tool, skill, or delegation operation was rejected; the message is
217    /// self-contained and safe to surface to the model and the client.
218    #[error("{0}")]
219    ToolFailed(String),
220    #[error("tool prompt composition failed: {0}")]
221    ToolPrompt(String),
222    #[error("session actor for {0} is unavailable")]
223    MissingActor(SessionId),
224    #[error("session {0} is owned by another cookie process")]
225    SessionOwnedByAnotherProcess(SessionId),
226    #[error("session actor stopped before replying")]
227    ActorStopped,
228    #[error("no_runnable_model")]
229    NoRunnableModel,
230    #[error("unknown agent preset `{0}`")]
231    UnknownAgentPreset(String),
232    #[error("provider_store_reload_failed")]
233    ProviderStoreReloadFailed,
234    #[error("runtime_compile_failed")]
235    RuntimeCompileFailed,
236    #[error("invalid runtime agent `{0}`")]
237    InvalidRuntimeAgent(AgentId),
238    #[error("MCP configuration error: {0}")]
239    Mcp(String),
240    #[error("cache strategy configuration error: {0}")]
241    CacheStrategy(String),
242    #[error("session permission error: {0}")]
243    Permission(String),
244    #[error("goal operation rejected: {0}")]
245    Goal(String),
246    #[error("producer operation rejected: {0}")]
247    Producer(String),
248    #[error("messaging: {0}")]
249    Messaging(String),
250    #[error(transparent)]
251    ModelManager(#[from] cookie_agent_models::ModelManagerError),
252    #[error(transparent)]
253    Manifest(ManifestError),
254}
255
256/// Atomic, secret-safe rejection details produced by the serialized approval transaction.
257#[derive(Clone, Debug, Eq, PartialEq)]
258pub struct ApprovalRespondFailure {
259    pub code: ApprovalRespondErrorCode,
260    pub session_id: SessionId,
261    pub approval_id: ApprovalId,
262    pub client_response_id: String,
263    pub current_status: Option<ApprovalStatus>,
264    pub current_revision: Option<u64>,
265    pub current_expires_at: Option<jiff::Timestamp>,
266    pub current_operation_fingerprint: Option<OperationFingerprint>,
267}
268
269impl From<ModelError> for EngineError {
270    fn from(error: ModelError) -> Self {
271        Self::Model(Box::new(error))
272    }
273}
274
275impl From<SessionError> for EngineError {
276    fn from(error: SessionError) -> Self {
277        match error {
278            // A lazy tree load the engine rejected carries its own failure; keep
279            // the original type so callers see what actually went wrong.
280            SessionError::TreeRejected(inner) => *inner,
281            other => Self::Session(other),
282        }
283    }
284}
285
286impl EngineError {
287    /// Explicit user-facing rendering; Debug is not a diagnostic transport.
288    pub fn user_message(&self) -> String {
289        match self {
290            Self::Model(error) => {
291                cookie_agent_protocol::diagnostics::model(&crate::model_policy::summary(error))
292            }
293            _ => cookie_agent_protocol::diagnostics::error_chain(self),
294        }
295    }
296}
297
298#[derive(Debug)]
299struct ActiveRun {
300    session: SessionId,
301    policy: Arc<FrozenRunPolicy>,
302    cancellation: CancellationToken,
303    cancelled_committed: Mutex<bool>,
304    stdin: Mutex<HashMap<ToolCallId, mpsc::Sender<StdinWrite>>>,
305    fallback_index: AtomicU64,
306    auto_compaction_failures: AtomicU8,
307    auto_compaction_diagnostic_emitted: AtomicBool,
308}
309
310struct AttemptTurn {
311    turn: PersistedModelTurn,
312    model_turn_seq: u64,
313    turn_context: Arc<TurnAgentContext>,
314    /// Persisted model call IDs whose tool-call name was normalized away from
315    /// invalid provider output. Dispatch must fail these instead of executing a
316    /// possibly-aliased tool.
317    normalized_tool_calls: HashSet<String>,
318}
319
320#[derive(Clone, Debug, Eq, PartialEq)]
321struct PendingInput {
322    admission_seq: u64,
323    origin: cookie_agent_protocol::EventOrigin,
324    input: String,
325}
326
327struct DelegatedResumeAdmission {
328    accepted: bool,
329    admission_seq: Option<u64>,
330}
331
332struct PendingPromotionState {
333    promoted: bool,
334    pending: Vec<PendingInput>,
335    continue_run: bool,
336}
337
338pub(super) enum UserInputInterception {
339    Accepted {
340        input: String,
341        original_input: Option<String>,
342    },
343    Handled {
344        reason: String,
345    },
346}
347
348struct PreparedToolCall {
349    call: ToolCall,
350    permission_name: Option<String>,
351    presentation: ToolCallPresentation,
352    prepared: Result<PreparedTool, ToolFailure>,
353    interception: Option<ToolInterceptionContext>,
354    intercepted_arguments: Arc<Mutex<Value>>,
355}
356
357impl PreparedToolCall {
358    fn concurrency(&self) -> ToolConcurrency {
359        self.interception
360            .as_ref()
361            .map_or(ToolConcurrency::Exclusive, |context| {
362                context.spec.concurrency
363            })
364    }
365
366    /// Calls sharing a serialization key must execute in model call order:
367    /// chained preparations build on each other's output.
368    fn serialization_key(&self) -> Option<crate::PreparedSerializationKey> {
369        self.prepared
370            .as_ref()
371            .ok()
372            .and_then(|prepared| prepared.serialization_key.clone())
373    }
374}
375
376struct ToolInterceptionContext {
377    provider: Arc<dyn ToolProvider>,
378    spec: ToolSpec,
379    preparation: ToolPreparationContext,
380    permission_name: String,
381    permission_resource: Option<String>,
382}
383
384struct PublishedTool {
385    provider: Arc<dyn ToolProvider>,
386    spec: ToolSpec,
387}
388
389struct PublishedToolSet {
390    definitions: Vec<ToolDefinition>,
391    tools: HashMap<String, PublishedTool>,
392}
393
394#[derive(Clone, Debug)]
395pub(crate) struct ToolFailure {
396    pub(crate) code: ToolCallFailureCode,
397    pub(crate) message: String,
398    pub(crate) partial_output: Option<Box<ToolResult>>,
399}
400
401#[derive(Clone, Copy, Debug)]
402pub(crate) enum ToolCallFailureCode {
403    InvalidUrl,
404    PermissionDenied,
405    RedirectError,
406    Timeout,
407    TransportError,
408    UnsupportedContentType,
409    ExecutionFailed,
410    OperationChanged,
411    PreparedCapabilityLost,
412    UnsupportedPlatform,
413}
414
415impl ToolCallFailureCode {
416    fn safe_code(self) -> SafeCode {
417        safe_code(match self {
418            Self::InvalidUrl => "invalid_url",
419            Self::PermissionDenied => "permission_denied",
420            Self::RedirectError => "redirect_error",
421            Self::Timeout => "timeout",
422            Self::TransportError => "transport_error",
423            Self::UnsupportedContentType => "unsupported_content_type",
424            Self::ExecutionFailed => "execution_failed",
425            Self::OperationChanged => "operation_changed",
426            Self::PreparedCapabilityLost => "prepared_capability_lost",
427            Self::UnsupportedPlatform => "unsupported_platform",
428        })
429    }
430}
431
432impl From<ToolError> for ToolFailure {
433    fn from(error: ToolError) -> Self {
434        Self {
435            code: error.code(),
436            message: error.message(),
437            partial_output: None,
438        }
439    }
440}
441
442enum PendingTool {
443    Prepared {
444        prepared: Box<PreparedToolCall>,
445        permission: crate::permissions::PermissionDecision,
446    },
447    ImmediateFailure(ToolFailure),
448}
449
450#[derive(Clone, Debug, Deserialize, Serialize)]
451#[serde(deny_unknown_fields)]
452struct StoredRuntimeRevisionMapping {
453    protocol_version: cookie_agent_protocol::ProtocolVersion,
454    runtime_revision: cookie_agent_protocol::RuntimeRevision,
455    model_runtime_revision: cookie_agent_protocol::RuntimeRevision,
456}
457
458#[derive(Debug)]
459struct RuntimeRevisionIndex {
460    path: PathBuf,
461    mappings:
462        HashMap<cookie_agent_protocol::RuntimeRevision, cookie_agent_protocol::RuntimeRevision>,
463}
464
465impl RuntimeRevisionIndex {
466    fn open(path: PathBuf) -> Result<Self, EngineError> {
467        let mut mappings = HashMap::new();
468        for record in events::load_jsonl_shared::<StoredRuntimeRevisionMapping>(&path)? {
469            match mappings.insert(
470                record.runtime_revision.clone(),
471                record.model_runtime_revision.clone(),
472            ) {
473                Some(existing) if existing != record.model_runtime_revision => {
474                    return Err(EngineError::RuntimeCompileFailed);
475                }
476                _ => {}
477            }
478        }
479        Ok(Self { path, mappings })
480    }
481
482    fn record(
483        &mut self,
484        runtime_revision: cookie_agent_protocol::RuntimeRevision,
485        model_runtime_revision: cookie_agent_protocol::RuntimeRevision,
486    ) -> Result<(), EngineError> {
487        if let Some(existing) = self.mappings.get(&runtime_revision) {
488            return if existing == &model_runtime_revision {
489                Ok(())
490            } else {
491                Err(EngineError::RuntimeCompileFailed)
492            };
493        }
494        events::append_jsonl(
495            &self.path,
496            &StoredRuntimeRevisionMapping {
497                protocol_version: cookie_agent_protocol::ProtocolVersion::current(),
498                runtime_revision: runtime_revision.clone(),
499                model_runtime_revision: model_runtime_revision.clone(),
500            },
501        )?;
502        self.mappings
503            .insert(runtime_revision, model_runtime_revision);
504        Ok(())
505    }
506
507    fn resolve(
508        &self,
509        runtime_revision: &cookie_agent_protocol::RuntimeRevision,
510    ) -> Option<cookie_agent_protocol::RuntimeRevision> {
511        self.mappings.get(runtime_revision).cloned()
512    }
513}
514
515/// How long shutdown waits, in total, for the cancelled run tasks to write
516/// their terminal events.
517///
518/// A cancelled run only has to unwind its current tool call and append one
519/// event, so this is generous; a task still running when it expires is aborted
520/// so shutdown never depends on a wedged provider or tool.
521const RUN_TASK_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
522
523const SESSION_MAILBOX_CAPACITY: usize = 256;
524const MAX_PENDING_PREPARED_TOOLS: usize = 64;
525/// Semantic revision of the no-model builtin runtime contract.
526/// This is intentionally independent of the protocol and event schema version.
527pub(crate) const UNAVAILABLE_BUILTIN_REVISION: &str = "internal-agent.unavailable.runtime.1";
528
529pub(super) fn event_origin(value: &'static str) -> cookie_agent_protocol::EventOrigin {
530    cookie_agent_protocol::EventOrigin::new(value).expect("static event origin is valid")
531}
532
533enum SessionCommand {
534    Producer(producers::ProducerCommand),
535    Append {
536        run: Option<RunId>,
537        origin: cookie_agent_protocol::EventOrigin,
538        event: Box<Event>,
539        reply: oneshot::Sender<Result<(), EngineError>>,
540    },
541    EnsureToolCallLinked {
542        run: RunId,
543        tool_call_id: ToolCallId,
544        child_session_id: SessionId,
545        reply: oneshot::Sender<Result<(), EngineError>>,
546    },
547    Start {
548        params: RunStartParams,
549        origin: cookie_agent_protocol::EventOrigin,
550        admission: Option<(InvocationId, u64)>,
551        reply: oneshot::Sender<Result<RunStartResult, EngineError>>,
552    },
553    Steer {
554        run: RunId,
555        origin: cookie_agent_protocol::EventOrigin,
556        input: String,
557        original_input: Option<String>,
558        reply: oneshot::Sender<Result<RunSteerResult, EngineError>>,
559    },
560    AdmitDelegatedResume {
561        run: RunId,
562        input: String,
563        reply: oneshot::Sender<Result<DelegatedResumeAdmission, EngineError>>,
564    },
565    RecallDelegatedResume {
566        run: RunId,
567        admission_seq: u64,
568        reply: oneshot::Sender<Result<bool, EngineError>>,
569    },
570    RecallSteer {
571        run: RunId,
572        reply: oneshot::Sender<Result<RunRecallSteerResult, EngineError>>,
573    },
574    CommitPendingPromotion {
575        run: RunId,
576        through_admission_seq: u64,
577        final_text: Option<String>,
578        complete_if_empty: bool,
579        already_promoted: bool,
580        reply: oneshot::Sender<Result<PendingPromotionState, EngineError>>,
581    },
582    Compact {
583        focus: Option<String>,
584        origin: cookie_agent_protocol::EventOrigin,
585        reply: oneshot::Sender<Result<cookie_agent_protocol::SessionCompactResult, EngineError>>,
586    },
587    Revert {
588        through_seq: u64,
589        origin: cookie_agent_protocol::EventOrigin,
590        instructions_override: Option<String>,
591        reply: oneshot::Sender<Result<SessionRevertResult, EngineError>>,
592    },
593    CompactionFinished {
594        reply: oneshot::Sender<Result<(), EngineError>>,
595    },
596    Cancel {
597        run: RunId,
598        reply: oneshot::Sender<Result<RunCancelResult, EngineError>>,
599    },
600    Stdin {
601        params: RunToolStdinParams,
602        reply: oneshot::Sender<Result<RunToolStdinResult, EngineError>>,
603    },
604    Subscribe {
605        cursor: Option<u64>,
606        reply: oneshot::Sender<
607            Result<
608                (
609                    EventsSubscribeResult,
610                    mpsc::Receiver<EventSubscriptionMessage>,
611                ),
612                EngineError,
613            >,
614        >,
615    },
616    Resume {
617        reply: oneshot::Sender<Result<SessionMeta, EngineError>>,
618    },
619    Rename {
620        params: SessionRenameParams,
621        origin: cookie_agent_protocol::EventOrigin,
622        reply: oneshot::Sender<Result<SessionRenameResult, EngineError>>,
623    },
624    ApprovalRespond {
625        params: ApprovalRespondParams,
626        origin: cookie_agent_protocol::EventOrigin,
627        reply: oneshot::Sender<Result<ApprovalRespondResult, EngineError>>,
628    },
629    ApprovalCapabilityInvalid {
630        params: ApprovalRespondParams,
631        invalidation: PreparedApprovalInvalidation,
632        reply: oneshot::Sender<Result<ApprovalRespondResult, EngineError>>,
633    },
634    ApprovalEvaluationComplete {
635        run: RunId,
636        request: ApprovalRequest,
637        executor: PreparedExecutorCell,
638        decision: ApprovalInternalDecisionKind,
639        permission_mode: PermissionMode,
640        cancelled: bool,
641        reply: oneshot::Sender<Result<ApprovalEvaluationTransition, EngineError>>,
642    },
643    ApprovalTerminal {
644        run: RunId,
645        approval_id: ApprovalId,
646        terminal: ApprovalTerminal,
647        reply: oneshot::Sender<Result<bool, EngineError>>,
648    },
649    ToolResult {
650        run: RunId,
651        tool_call_id: ToolCallId,
652        result: Result<ToolResult, ToolFailure>,
653        cancelled: bool,
654        reply: oneshot::Sender<Result<bool, EngineError>>,
655    },
656    ResolveDelegateFailureIfPending {
657        run: RunId,
658        tool_call_id: ToolCallId,
659        result: ToolResult,
660        reply: oneshot::Sender<Result<bool, EngineError>>,
661    },
662    ResolveAbandonedDelegateFailureIfPending {
663        invocation_id: InvocationId,
664        generation: u64,
665        run: RunId,
666        tool_call_id: ToolCallId,
667        result: ToolResult,
668        reply: oneshot::Sender<Result<bool, EngineError>>,
669    },
670    PromotePendingOrComplete {
671        run: RunId,
672        final_text: Option<String>,
673        complete_if_empty: bool,
674        reply: oneshot::Sender<Result<bool, EngineError>>,
675    },
676    PromotePendingInputs {
677        run: RunId,
678        reply: oneshot::Sender<Result<producer_claims::ClaimedPrompt, EngineError>>,
679    },
680    EvictionBarrier {
681        reply: oneshot::Sender<Result<(), EngineError>>,
682    },
683}
684
685impl SessionCommand {
686    fn compaction_deferred_kind(&self) -> Option<CompactionDeferredKind> {
687        match self {
688            Self::Start { .. } => Some(CompactionDeferredKind::Start),
689            Self::PromotePendingInputs { .. } => Some(CompactionDeferredKind::PromotePendingInputs),
690            Self::PromotePendingOrComplete { .. } => {
691                Some(CompactionDeferredKind::PromotePendingOrComplete)
692            }
693            Self::Resume { .. } => Some(CompactionDeferredKind::Resume),
694            _ => None,
695        }
696    }
697
698    fn reject_duplicate_during_compaction(self, session: SessionId) {
699        match self {
700            Self::Start { reply, .. } => {
701                let _ = reply.send(Err(EngineError::SessionRunning(session)));
702            }
703            Self::PromotePendingInputs { reply, .. } => {
704                let _ = reply.send(Err(EngineError::SessionRunning(session)));
705            }
706            Self::PromotePendingOrComplete { reply, .. } => {
707                let _ = reply.send(Ok(false));
708            }
709            Self::Resume { reply } => {
710                let _ = reply.send(Err(EngineError::SessionRunning(session)));
711            }
712            _ => unreachable!("only barrier-sensitive commands are superseded"),
713        }
714    }
715}
716
717const MAX_COMPACTION_DEFERRED_COMMANDS: usize = 4;
718
719pub(crate) struct Inner {
720    config: LoadedConfiguration,
721    pub(crate) config_store: Mutex<crate::config_store::ConfigStore>,
722    pub(crate) artifacts: Arc<ArtifactRouter>,
723    mutation_locks: Mutex<HashMap<PreparedSerializationKey, Arc<tokio::sync::Mutex<()>>>>,
724    pub(crate) store: Arc<SessionStore>,
725    pub(crate) delegation_events: Arc<DelegationEventStore>,
726    pub(crate) grant_journal: Arc<GrantInvalidationJournal>,
727    pub(crate) model_manager: Arc<ModelManager>,
728    published_runtime: ArcSwap<PublishedRuntime>,
729    runtime_mutation: Mutex<()>,
730    runtime_notifications: broadcast::Sender<RuntimeChangedNotification>,
731    engine_events: broadcast::Sender<crate::EngineEvent>,
732    plugin_diagnostics: PluginDiagnosticsState,
733    runtime_revision_index: Mutex<RuntimeRevisionIndex>,
734    tools: Mutex<Vec<Arc<dyn ToolProvider>>>,
735    provider_ids: Mutex<HashSet<&'static str>>,
736    pub(crate) mcp: Arc<crate::McpRegistry>,
737    pub(crate) plugins: Arc<crate::PluginRegistry>,
738    pub(crate) mcp_mutation: tokio::sync::Mutex<()>,
739    permissions: PermissionPipeline,
740    pub(crate) skills: Arc<cookie_agent_config::SkillRegistry>,
741    pub(crate) sessions: SessionRuntimeState,
742    pub(crate) delegation: DelegationRuntimeState,
743    pub(crate) approvals: ApprovalRuntimeState,
744    pub(crate) skills_runtime: SkillRuntimeState,
745    pub(crate) output: OutputState,
746    pub(crate) compaction: CompactionState,
747    runtime: Option<tokio::runtime::Handle>,
748    janitor_task: Mutex<Option<JoinHandle<()>>>,
749    #[cfg(test)]
750    pub(crate) test_hooks: test_hooks::TestHooks,
751}
752
753/// Cloneable in-process engine handle. It contains no transport concerns and
754/// is safe for tool providers to call while their parent call is executing.
755#[derive(Clone)]
756pub struct Engine {
757    pub(crate) inner: Arc<Inner>,
758}
759
760impl Engine {
761    pub fn open(options: EngineOptions) -> Result<Self, EngineError> {
762        let mut provider_ids = HashSet::new();
763        for provider in &options.tools {
764            reserve_provider_id(&mut provider_ids, provider.as_ref())?;
765        }
766        let skills = Arc::new(options.config.skills.clone());
767        let config_store = crate::config_store::ConfigStore::new(&options.config);
768        let current_models = options.model_manager.current();
769        let (agents, agent_presets) = resolve_agent_registries(&options.config, &current_models)?;
770        let current_manifest = prepare_runtime_manifest(&current_models)?;
771        let snapshot = build_runtime_snapshot(&current_models, &agents, &agent_presets)?;
772        let published_runtime = Arc::new(PublishedRuntime {
773            result: RuntimeSnapshotResult { snapshot },
774            models: Arc::clone(&current_models),
775            agents,
776            agent_presets,
777            current_manifest,
778        });
779        let store = SessionStore::open(&options.data_dir, &options.cwd)?;
780        let artifacts = ArtifactRouter::for_store(&store)?;
781        // Every write belongs to the directory of the writing session's root.
782        let placement = Arc::clone(&store);
783        artifacts.install_tree_resolver(Arc::new(move |session| {
784            Some(placement.root_of(session).unwrap_or(session))
785        }));
786        // A sweep reuses what a tree load harvested only while the store
787        // still agrees the harvest is current, so it has to ask the store:
788        // resident tip and durable length, the same pair a fold is verified
789        // against (§3.3, §5.2).
790        let fingerprinting = Arc::clone(&store);
791        artifacts.install_log_fingerprint_probe(Arc::new(move |session| {
792            fingerprinting.log_fingerprint(session)
793        }));
794        let mcp = Arc::new(
795            crate::McpRegistry::new(
796                options.config.mcp_servers.clone(),
797                options.data_dir.join("mcp-oauth.json"),
798            )
799            .map_err(|error| EngineError::ToolFailed(error.to_string()))?,
800        );
801        reserve_provider_id(&mut provider_ids, mcp.as_ref())?;
802        for provider in &options.tools {
803            mcp.reserve_provider(provider.as_ref())
804                .map_err(|error| EngineError::ToolFailed(error.to_string()))?;
805        }
806        let plugins = Arc::new(crate::PluginRegistry::new(
807            options.config.plugins.clone(),
808            Arc::clone(&mcp),
809        ));
810        reserve_provider_id(&mut provider_ids, plugins.as_ref())?;
811        let mut tools = options.tools;
812        tools.push(mcp.clone());
813        tools.push(plugins.clone());
814        let delegation_events = DelegationEventStore::open(Arc::clone(&store))?;
815        let grant_journal = GrantInvalidationJournal::open(
816            store.workdir_dir_path().join("grant-invalidations.jsonl"),
817        )?;
818        let (runtime_notifications, _) = broadcast::channel(64);
819        let (engine_events, _) = broadcast::channel(256);
820        let plugin_diagnostics =
821            Arc::new(plugin_diagnostics::PluginDiagnosticAccumulator::default());
822        let mut runtime_revision_index = RuntimeRevisionIndex::open(
823            store.workdir_dir_path().join("runtime-revisions-v8.jsonl"),
824        )?;
825        runtime_revision_index.record(
826            published_runtime.result.snapshot.runtime_revision.clone(),
827            current_models.runtime_revision().clone(),
828        )?;
829        let engine = Self {
830            inner: Arc::new(Inner {
831                config: options.config,
832                config_store: Mutex::new(config_store),
833                artifacts,
834                mutation_locks: Mutex::new(HashMap::new()),
835                store,
836                delegation_events,
837                grant_journal,
838                model_manager: options.model_manager,
839                published_runtime: ArcSwap::from(published_runtime),
840                runtime_mutation: Mutex::new(()),
841                runtime_notifications,
842                engine_events,
843                plugin_diagnostics: PluginDiagnosticsState {
844                    accumulator: Arc::clone(&plugin_diagnostics),
845                    task: Mutex::new(None),
846                },
847                runtime_revision_index: Mutex::new(runtime_revision_index),
848                tools: Mutex::new(tools),
849                provider_ids: Mutex::new(provider_ids),
850                mcp,
851                plugins,
852                mcp_mutation: tokio::sync::Mutex::new(()),
853                permissions: PermissionPipeline::default(),
854                skills,
855                sessions: SessionRuntimeState::default(),
856                delegation: DelegationRuntimeState::default(),
857                approvals: ApprovalRuntimeState::default(),
858                skills_runtime: SkillRuntimeState::default(),
859                output: OutputState::default(),
860                compaction: CompactionState::default(),
861                runtime: tokio::runtime::Handle::try_current().ok(),
862                janitor_task: Mutex::new(None),
863                #[cfg(test)]
864                test_hooks: test_hooks::TestHooks::default(),
865            }),
866        };
867        let weak = Arc::downgrade(&engine.inner);
868        engine
869            .inner
870            .plugins
871            .set_emit_handler(Arc::new(move |request| {
872                let weak = weak.clone();
873                Box::pin(async move {
874                    let Some(inner) = weak.upgrade() else {
875                        return crate::plugin::PluginEmitOutcome {
876                            bus: cookie_agent_protocol::ExtensionEmitStatus::Dropped,
877                            durable: cookie_agent_protocol::ExtensionEmitStatus::Rejected,
878                            reason: Some("engine is shutting down".into()),
879                        };
880                    };
881                    Engine { inner }.publish_plugin_emit(request).await
882                })
883            }));
884        if let Some(runtime) = &engine.inner.runtime {
885            let weak = Arc::downgrade(&engine.inner);
886            let task = runtime.spawn(async move {
887                run_plugin_diagnostic_aggregator(weak, plugin_diagnostics).await;
888            });
889            *engine
890                .inner
891                .plugin_diagnostics
892                .task
893                .lock()
894                .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(task);
895        }
896        engine.install_tree_load_observer()?;
897        engine.rebuild_approvals();
898        engine.rebuild_delegation_registry(&engine.inner.delegation_events.entries(), false)?;
899        engine.install_producer_runtime();
900        if let Some(runtime) = &engine.inner.runtime {
901            engine.inner.mcp.start_eager(runtime);
902            engine.inner.plugins.start_eager(runtime);
903        }
904        engine.start_subagent_janitor();
905        Ok(engine)
906    }
907
908    #[must_use]
909    pub fn current_runtime(&self) -> Arc<PublishedRuntime> {
910        self.inner.published_runtime.load_full()
911    }
912
913    pub fn runtime_snapshot(&self) -> Result<RuntimeSnapshotResult, EngineError> {
914        self.reconcile_provider_store()?;
915        Ok(self.current_runtime().result.clone())
916    }
917
918    #[must_use]
919    pub fn subscribe_runtime_changes(&self) -> broadcast::Receiver<RuntimeChangedNotification> {
920        self.inner.runtime_notifications.subscribe()
921    }
922
923    #[must_use]
924    pub fn subscribe_engine_events(&self) -> broadcast::Receiver<crate::EngineEvent> {
925        self.inner.engine_events.subscribe()
926    }
927
928    #[cfg(feature = "test-support")]
929    #[doc(hidden)]
930    pub fn publish_engine_event_for_test(&self, event: crate::EngineEvent) {
931        let _ = self.inner.engine_events.send(event);
932    }
933
934    pub fn connect_provider(
935        &self,
936        params: ProviderConnectParams,
937    ) -> Result<ProviderConnectResult, EngineError> {
938        use cookie_agent_models::provider_store::{
939            ClientConnectId, ProviderAuthValues, ProviderStoreMutation,
940        };
941
942        let _mutation = self
943            .inner
944            .runtime_mutation
945            .lock()
946            .unwrap_or_else(|poisoned| poisoned.into_inner());
947        let provider_id = params.provider_id.clone();
948        let auth_values = params
949            .auth_values
950            .field_names()
951            .map(|name| {
952                let field = cookie_agent_protocol::AuthFieldName::new(name.to_owned())
953                    .map_err(|_| EngineError::RuntimeCompileFailed)?;
954                let value = params
955                    .auth_values
956                    .get(&field)
957                    .ok_or(EngineError::RuntimeCompileFailed)?
958                    .to_owned();
959                Ok((field, value))
960            })
961            .collect::<Result<_, EngineError>>()?;
962        let request = cookie_agent_models::ProviderConnectRequest {
963            provider_id,
964            expected_catalog_revision: params.expected_catalog_revision,
965            setup_values: params
966                .setup_values
967                .into_iter()
968                .map(|(id, value)| {
969                    let value = serde_json::from_value(
970                        serde_json::to_value(value)
971                            .map_err(|_| EngineError::RuntimeCompileFailed)?,
972                    )
973                    .map_err(|_| EngineError::RuntimeCompileFailed)?;
974                    Ok((id, value))
975                })
976                .collect::<Result<_, EngineError>>()?,
977            auth_method: params.auth_method,
978            auth_values: ProviderAuthValues::new(auth_values)
979                .map_err(cookie_agent_models::ModelManagerError::from)?,
980            client_connect_id: ClientConnectId::new(params.client_connect_id.as_str())
981                .map_err(cookie_agent_models::ModelManagerError::from)?,
982        };
983        let previous = self.current_runtime();
984        let result = self.inner.model_manager.connect(request, |candidate, _| {
985            self.prepare_publication(
986                candidate,
987                &previous,
988                vec![RuntimeChangeReason::ProviderConnected],
989            )
990            .map_err(|_| cookie_agent_models::ModelManagerError::RuntimeCompileFailed)
991        })?;
992        let runtime = result.publication.map_or_else(
993            || self.current_runtime(),
994            |publication| self.publish(publication),
995        );
996        let durable_connection = match &result.mutation {
997            ProviderStoreMutation::Connect {
998                durable_connection, ..
999            } => {
1000                crate::runtime_snapshot::projection::project_durable_connection(durable_connection)?
1001            }
1002            ProviderStoreMutation::Disconnect { .. } => {
1003                return Err(EngineError::RuntimeCompileFailed);
1004            }
1005        };
1006        Ok(ProviderConnectResult {
1007            durable_connection,
1008            effective_auth_source: crate::runtime_snapshot::projection::effective_auth_source(
1009                result.effective_auth,
1010            )?,
1011            runtime: runtime.result.snapshot.clone(),
1012            replayed: result.replayed,
1013        })
1014    }
1015
1016    pub fn disconnect_provider(
1017        &self,
1018        params: ProviderDisconnectParams,
1019    ) -> Result<ProviderDisconnectResult, EngineError> {
1020        use cookie_agent_models::provider_store::{ClientRequestId, ProviderStoreMutation};
1021
1022        let _mutation = self
1023            .inner
1024            .runtime_mutation
1025            .lock()
1026            .unwrap_or_else(|poisoned| poisoned.into_inner());
1027        let previous = self.current_runtime();
1028        let expected_model_runtime_revision = self
1029            .inner
1030            .runtime_revision_index
1031            .lock()
1032            .unwrap_or_else(|poisoned| poisoned.into_inner())
1033            .resolve(&params.expected_runtime_revision)
1034            .ok_or({
1035                EngineError::ModelManager(cookie_agent_models::ModelManagerError::ProviderStore(
1036                    cookie_agent_models::provider_store::ProviderStoreError::RuntimeRevisionConflict,
1037                ))
1038            })?;
1039        let request = cookie_agent_models::ProviderDisconnectRequest {
1040            provider_id: params.provider_id.clone(),
1041            expected_runtime_revision: expected_model_runtime_revision,
1042            expected_provider_state_revision: params.expected_provider_state_revision,
1043            expected_connection_generation: params
1044                .expected_connection_generation
1045                .map(|value| {
1046                    cookie_agent_models::provider_store::ProviderConnectionGeneration::new(
1047                        value.get(),
1048                    )
1049                })
1050                .transpose()
1051                .map_err(cookie_agent_models::ModelManagerError::from)?,
1052            client_request_id: ClientRequestId::new(params.client_request_id.as_str())
1053                .map_err(cookie_agent_models::ModelManagerError::from)?,
1054        };
1055        let result = self
1056            .inner
1057            .model_manager
1058            .disconnect(request, |candidate, _| {
1059                self.prepare_publication(
1060                    candidate,
1061                    &previous,
1062                    vec![RuntimeChangeReason::ProviderDisconnected],
1063                )
1064                .map_err(|_| cookie_agent_models::ModelManagerError::RuntimeCompileFailed)
1065            })?;
1066        let runtime = result.publication.map_or_else(
1067            || self.current_runtime(),
1068            |publication| self.publish(publication),
1069        );
1070        if !matches!(result.mutation, ProviderStoreMutation::Disconnect { .. }) {
1071            return Err(EngineError::RuntimeCompileFailed);
1072        }
1073        let receipt = result.mutation.durable_receipt();
1074        Ok(ProviderDisconnectResult {
1075            durable_receipt: cookie_agent_protocol::DurableProviderReceipt {
1076                receipt_id: receipt
1077                    .receipt_id
1078                    .to_string()
1079                    .parse()
1080                    .map_err(|_| EngineError::RuntimeCompileFailed)?,
1081                store_revision: receipt.store_revision.clone(),
1082                provider_state_revision: receipt.provider_state_revision.clone(),
1083                committed_at: receipt.committed_at,
1084            },
1085            provider_id: params.provider_id,
1086            disconnected: true,
1087            effective_auth_state: crate::runtime_snapshot::projection::effective_auth_state(
1088                result.effective_auth,
1089            ),
1090            runtime: runtime.result.clone(),
1091            replayed: result.replayed,
1092        })
1093    }
1094
1095    pub fn reconcile_provider_store(&self) -> Result<bool, EngineError> {
1096        let _mutation = self
1097            .inner
1098            .runtime_mutation
1099            .lock()
1100            .unwrap_or_else(|poisoned| poisoned.into_inner());
1101        let previous = self.current_runtime();
1102        let reloaded = self
1103            .inner
1104            .model_manager
1105            .reload_store_if_changed(|candidate| {
1106                self.prepare_publication(
1107                    candidate,
1108                    &previous,
1109                    vec![
1110                        RuntimeChangeReason::ProviderStoreChanged,
1111                        RuntimeChangeReason::ProviderStoreReloaded,
1112                    ],
1113                )
1114                .map_err(|_| cookie_agent_models::ModelManagerError::RuntimeCompileFailed)
1115            })
1116            .map_err(|_| EngineError::ProviderStoreReloadFailed)?;
1117        if let Some((_, publication)) = reloaded {
1118            self.publish(publication);
1119            Ok(true)
1120        } else {
1121            Ok(false)
1122        }
1123    }
1124
1125    /// Atomically recompiles and publishes a newly acquired catalog snapshot.
1126    pub fn refresh_catalog(
1127        &self,
1128        catalog: Arc<cookie_agent_models::catalog::CatalogSnapshot>,
1129    ) -> Result<RuntimeSnapshotResult, EngineError> {
1130        let _mutation = self
1131            .inner
1132            .runtime_mutation
1133            .lock()
1134            .unwrap_or_else(|poisoned| poisoned.into_inner());
1135        let previous = self.current_runtime();
1136        let authored = previous.models.authored().clone();
1137        let reason = match catalog.source {
1138            cookie_agent_models::catalog::CatalogSource::Network => {
1139                RuntimeChangeReason::CatalogRefreshed
1140            }
1141            cookie_agent_models::catalog::CatalogSource::Cache
1142            | cookie_agent_models::catalog::CatalogSource::Bootstrap => {
1143                RuntimeChangeReason::CatalogFallback
1144            }
1145        };
1146        let (_, publication) =
1147            self.inner
1148                .model_manager
1149                .reload_inputs(authored, catalog, |candidate| {
1150                    self.prepare_publication(candidate, &previous, vec![reason])
1151                        .map_err(|_| cookie_agent_models::ModelManagerError::RuntimeCompileFailed)
1152                })?;
1153        Ok(self.publish(publication).result.clone())
1154    }
1155
1156    fn prepare_publication(
1157        &self,
1158        models: &Arc<cookie_agent_models::CompiledModelRuntime>,
1159        previous: &Arc<PublishedRuntime>,
1160        mut reasons: Vec<RuntimeChangeReason>,
1161    ) -> Result<RuntimePublication, EngineError> {
1162        #[cfg(test)]
1163        if self
1164            .inner
1165            .test_hooks
1166            .publication_failure
1167            .swap(false, Ordering::AcqRel)
1168        {
1169            return Err(EngineError::RuntimeCompileFailed);
1170        }
1171        let (agents, agent_presets) = resolve_agent_registries(&self.inner.config, models)?;
1172        let current_manifest = prepare_runtime_manifest(models)?;
1173        let snapshot = build_runtime_snapshot(models, &agents, &agent_presets)?;
1174        self.inner
1175            .runtime_revision_index
1176            .lock()
1177            .unwrap_or_else(|poisoned| poisoned.into_inner())
1178            .record(
1179                snapshot.runtime_revision.clone(),
1180                models.runtime_revision().clone(),
1181            )?;
1182        reasons.sort();
1183        reasons.dedup();
1184        let notification = RuntimeChangedNotification {
1185            previous_revision: Some(previous.result.snapshot.runtime_revision.clone()),
1186            snapshot: snapshot.clone(),
1187            reasons,
1188        };
1189        Ok(RuntimePublication {
1190            runtime: Arc::new(PublishedRuntime {
1191                result: RuntimeSnapshotResult { snapshot },
1192                models: Arc::clone(models),
1193                agents,
1194                agent_presets,
1195                current_manifest,
1196            }),
1197            notification,
1198        })
1199    }
1200
1201    fn publish(&self, publication: RuntimePublication) -> Arc<PublishedRuntime> {
1202        self.inner
1203            .published_runtime
1204            .store(Arc::clone(&publication.runtime));
1205        let _ = self
1206            .inner
1207            .runtime_notifications
1208            .send(publication.notification);
1209        publication.runtime
1210    }
1211
1212    /// Receives the products of a lazy tree load and folds them into the engine
1213    /// singletons (§3.3(d)). Validation runs first so a rejected load cannot
1214    /// leave half-applied state behind, and the artifact router is told the tree
1215    /// is loaded last: until this function returns `Ok` the load is not a
1216    /// completed one, and a sweep must still treat the child logs as unproven.
1217    fn apply_tree_load(
1218        &self,
1219        products: Arc<crate::session::TreeLoadProducts>,
1220    ) -> Result<(), EngineError> {
1221        let extended = self
1222            .inner
1223            .delegation_events
1224            .extend_from_payloads(&products.delegations)?;
1225        if !extended.is_empty() {
1226            // The registry is a fold over the whole event store, and its rebuild
1227            // retains only what it is handed; rebuilding from all entries is now
1228            // cheap because no child log is read to do it (§4.1).
1229            self.rebuild_delegation_registry(&self.inner.delegation_events.entries(), false)?;
1230        }
1231        let invalidated = self.inner.grant_journal.invalidated_ids();
1232        for grant in &products.grants {
1233            if !invalidated.contains(&grant.grant_id) {
1234                self.inner.approvals.store.grant(grant.clone());
1235            }
1236        }
1237        self.reconcile_loaded_tree_producers(products.producer_projections.clone());
1238        // Its child logs are now part of the durable live set for artifact
1239        // collection, and its own directory may be collected: the set the one
1240        // fold harvested replaces any later child-log scan (§3.3(b), §5.2).
1241        self.inner.artifacts.note_tree_live_refs(
1242            products.root,
1243            products.artifact_refs.clone(),
1244            products.child_log_fingerprints.clone(),
1245        );
1246        self.inner.artifacts.note_tree_loaded(products.root);
1247        Ok(())
1248    }
1249
1250    /// Installs the store-side hook and delivers everything a load that already
1251    /// completed was holding back (§3.3, D5).
1252    fn install_tree_load_observer(&self) -> Result<(), EngineError> {
1253        struct Observer(std::sync::Weak<Inner>);
1254
1255        impl crate::session::TreeLoadObserver for Observer {
1256            fn tree_loaded(
1257                &self,
1258                products: Arc<crate::session::TreeLoadProducts>,
1259            ) -> Result<(), EngineError> {
1260                let inner = self.0.upgrade().ok_or(EngineError::ActorStopped)?;
1261                Engine { inner }.apply_tree_load(products)
1262            }
1263        }
1264
1265        self.inner
1266            .store
1267            .set_tree_load_observer(Arc::new(Observer(Arc::downgrade(&self.inner))))?;
1268        Ok(())
1269    }
1270
1271    /// Completes the tree of `id` before it is used (§3.2/§3.3), then delivers
1272    /// any load this process finished while no hook was installed yet (D5).
1273    /// Cheap once the tree is loaded.
1274    pub(crate) fn ensure_tree_loaded(&self, id: SessionId) -> Result<(), EngineError> {
1275        self.inner.store.ensure_tree_for(id)?;
1276        self.inner.store.drain_pending_loads()?;
1277        Ok(())
1278    }
1279
1280    /// Producer reconciliation for children surfaced by a tree load. Runs on the
1281    /// engine runtime when one is available; otherwise the periodic plugin
1282    /// producer scan picks them up, since loaded trees cache their hits (§4.4).
1283    fn reconcile_loaded_tree_producers(
1284        &self,
1285        projections: Vec<(SessionId, crate::goal_projection::GoalProducerProjection)>,
1286    ) {
1287        if projections.is_empty() {
1288            return;
1289        }
1290        let Some(handle) = self
1291            .inner
1292            .runtime
1293            .clone()
1294            .or_else(|| tokio::runtime::Handle::try_current().ok())
1295        else {
1296            return;
1297        };
1298        let weak = Arc::downgrade(&self.inner);
1299        handle.spawn(async move {
1300            for (session, projection) in projections {
1301                let Some(inner) = weak.upgrade() else {
1302                    return;
1303                };
1304                let _ = Engine { inner }
1305                    .request(session, |reply| {
1306                        SessionCommand::Producer(
1307                            crate::runtime::producers::ProducerCommand::Reconcile {
1308                                projection: Some(projection.clone()),
1309                                reply,
1310                            },
1311                        )
1312                    })
1313                    .await;
1314            }
1315        });
1316    }
1317
1318    pub(super) fn mutation_lock(
1319        &self,
1320        key: &PreparedSerializationKey,
1321    ) -> Arc<tokio::sync::Mutex<()>> {
1322        self.inner
1323            .mutation_locks
1324            .lock()
1325            .unwrap_or_else(|poisoned| poisoned.into_inner())
1326            .entry(key.clone())
1327            .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
1328            .clone()
1329    }
1330
1331    /// Registers a tool provider after engine open, allowing providers that
1332    /// require an Engine (notably delegate) to break the construction cycle.
1333    pub fn register_tool_provider(&self, provider: Arc<dyn ToolProvider>) {
1334        self.try_register_tool_provider(provider)
1335            .expect("tool provider collision");
1336    }
1337
1338    pub fn try_register_tool_provider(
1339        &self,
1340        provider: Arc<dyn ToolProvider>,
1341    ) -> Result<(), EngineError> {
1342        let mut provider_ids = self
1343            .inner
1344            .provider_ids
1345            .lock()
1346            .unwrap_or_else(|poisoned| poisoned.into_inner());
1347        reserve_provider_id(&mut provider_ids, provider.as_ref())?;
1348        if let Err(error) = self.inner.mcp.reserve_provider(provider.as_ref()) {
1349            provider_ids.remove(provider.provider_id());
1350            return Err(EngineError::ToolFailed(error.to_string()));
1351        }
1352        self.inner
1353            .tools
1354            .lock()
1355            .unwrap_or_else(|poisoned| poisoned.into_inner())
1356            .push(provider);
1357        Ok(())
1358    }
1359
1360    #[must_use]
1361    pub fn mcp_statuses(&self) -> Vec<crate::McpServerStatus> {
1362        self.inner.mcp.statuses()
1363    }
1364
1365    #[must_use]
1366    pub fn plugin_statuses(&self) -> Vec<crate::PluginStatus> {
1367        self.inner.plugins.statuses()
1368    }
1369
1370    #[cfg(test)]
1371    pub(crate) fn block_plugin_diagnostic_appends_for_test(&self) {
1372        *self
1373            .inner
1374            .test_hooks
1375            .plugin_diagnostic_append_block
1376            .lock()
1377            .unwrap_or_else(|poisoned| poisoned.into_inner()) =
1378            Some(Arc::new(tokio::sync::Notify::new()));
1379    }
1380
1381    #[cfg(test)]
1382    pub(crate) fn block_tool_progress_appends_for_test(&self) -> Arc<tokio::sync::Notify> {
1383        let reached = Arc::new(tokio::sync::Notify::new());
1384        *self
1385            .inner
1386            .test_hooks
1387            .tool_progress_append_block
1388            .lock()
1389            .unwrap_or_else(|poisoned| poisoned.into_inner()) =
1390            Some(Arc::new(ToolProgressAppendBlock {
1391                reached: reached.clone(),
1392                release: tokio::sync::Notify::new(),
1393            }));
1394        reached
1395    }
1396
1397    #[cfg(test)]
1398    pub(crate) fn pending_plugin_diagnostic_keys_for_test(&self) -> usize {
1399        self.inner.plugin_diagnostics.accumulator.key_count()
1400    }
1401
1402    pub async fn ping_plugin(&self, name: &str) -> Result<(), String> {
1403        self.inner.plugins.ping(name).await
1404    }
1405
1406    /// Stops new session mailbox traffic and cancels active work. Existing
1407    /// client clones may keep a session mailbox alive.
1408    pub async fn shutdown(&self) {
1409        self.inner
1410            .delegation
1411            .admission_tasks_closing
1412            .store(true, Ordering::Release);
1413        let janitor = self
1414            .inner
1415            .janitor_task
1416            .lock()
1417            .ok()
1418            .and_then(|mut task| task.take());
1419        if let Some(task) = janitor {
1420            task.abort();
1421            let _ = task.await;
1422        }
1423        let tasks = self
1424            .inner
1425            .delegation
1426            .admission_tasks
1427            .lock()
1428            .map(|mut tasks| tasks.drain(..).collect::<Vec<_>>())
1429            .unwrap_or_default();
1430        for task in &tasks {
1431            task.abort();
1432        }
1433        for task in tasks {
1434            if let Err(error) = task.await
1435                && !error.is_cancelled()
1436            {
1437                eprintln!("admission task stopped during shutdown: {error}");
1438            }
1439        }
1440        let blocking_tasks = self
1441            .inner
1442            .delegation
1443            .admission_blocking_tasks
1444            .lock()
1445            .map(|mut tasks| tasks.drain(..).collect::<Vec<_>>())
1446            .unwrap_or_default();
1447        for task in blocking_tasks {
1448            if let Err(error) = task.await {
1449                eprintln!("admission blocking task stopped during shutdown: {error}");
1450            }
1451        }
1452        let active: Vec<_> = self
1453            .inner
1454            .sessions
1455            .active
1456            .lock()
1457            .unwrap_or_else(|poisoned| poisoned.into_inner())
1458            .values()
1459            .cloned()
1460            .collect();
1461        for run in active {
1462            run.cancellation.cancel();
1463        }
1464        // Joined here, before the plugin and MCP transports go away and well
1465        // before the actors are cleared: a run task may be mid tool call, and
1466        // it needs live transports to unwind and a live store to append its
1467        // `RunCancelled`. Anything still running when the bound expires is
1468        // aborted, so shutdown stays bounded.
1469        self.join_run_tasks(RUN_TASK_SHUTDOWN_TIMEOUT).await;
1470        self.inner.plugins.shutdown().await;
1471        self.inner.mcp.shutdown().await;
1472        self.inner
1473            .plugin_diagnostics
1474            .accumulator
1475            .shutdown
1476            .store(true, Ordering::Release);
1477        self.inner
1478            .plugin_diagnostics
1479            .accumulator
1480            .notify
1481            .notify_one();
1482        let diagnostic_task = self
1483            .inner
1484            .plugin_diagnostics
1485            .task
1486            .lock()
1487            .ok()
1488            .and_then(|mut task| task.take());
1489        if let Some(mut task) = diagnostic_task
1490            && tokio::time::timeout(
1491                plugin_diagnostics::PLUGIN_DIAGNOSTIC_SHUTDOWN_TIMEOUT,
1492                &mut task,
1493            )
1494            .await
1495            .is_err()
1496        {
1497            for plugin in self.inner.plugin_diagnostics.accumulator.offenders() {
1498                self.inner.plugins.note_offender_diagnostic(
1499                    &plugin,
1500                    "plugin diagnostic drain incomplete: shutdown deadline exceeded".into(),
1501                );
1502            }
1503            task.abort();
1504            let _ = task.await;
1505        }
1506        self.inner
1507            .sessions
1508            .actors
1509            .lock()
1510            .unwrap_or_else(|poisoned| poisoned.into_inner())
1511            .clear();
1512        self.inner
1513            .tools
1514            .lock()
1515            .unwrap_or_else(|poisoned| poisoned.into_inner())
1516            .clear();
1517        self.inner.store.release_ownership();
1518    }
1519}
1520
1521fn reserve_provider_id(
1522    provider_ids: &mut HashSet<&'static str>,
1523    provider: &dyn ToolProvider,
1524) -> Result<(), EngineError> {
1525    let provider_id = provider.provider_id();
1526    if !provider_ids.insert(provider_id) {
1527        return Err(EngineError::ToolFailed(format!(
1528            "tool provider ID `{provider_id}` is already registered"
1529        )));
1530    }
1531    Ok(())
1532}
1533
1534type ResolvedAgentRegistries = (Arc<AgentRegistry>, BTreeMap<String, Arc<AgentRegistry>>);
1535
1536fn resolve_agent_registries(
1537    config: &LoadedConfiguration,
1538    models: &Arc<cookie_agent_models::CompiledModelRuntime>,
1539) -> Result<ResolvedAgentRegistries, EngineError> {
1540    let shared = Arc::new(AgentRegistry::resolve(
1541        &config.agent_registry(),
1542        models,
1543        None,
1544    )?);
1545    let presets = config
1546        .agent_preset_registries()
1547        .into_iter()
1548        .map(|(name, authored)| {
1549            AgentRegistry::resolve(&authored, models, Some(name.clone()))
1550                .map(|registry| (name, Arc::new(registry)))
1551        })
1552        .collect::<Result<_, _>>()?;
1553    Ok((shared, presets))
1554}
1555
1556#[cfg(test)]
1557mod context_token_estimator_tests {
1558    use super::{ContextTokenEstimator, should_run_predictive_compaction};
1559
1560    #[test]
1561    fn learns_from_committed_usage_and_projects() {
1562        let mut estimator = ContextTokenEstimator::default();
1563        estimator.record_committed_turn(200, Some(50));
1564
1565        assert_eq!(estimator.tokens_per_byte, 0.25);
1566        assert_eq!(estimator.last_committed_input_tokens, 50);
1567        assert_eq!(estimator.projected_tokens(40), Some(60));
1568        assert_eq!(estimator.estimated_context_tokens(40), Some(10));
1569        assert_eq!(estimator.estimated_context_tokens(41), Some(11));
1570    }
1571
1572    #[test]
1573    fn skips_degenerate_ratio_updates() {
1574        let mut estimator = ContextTokenEstimator {
1575            tokens_per_byte: 0.5,
1576            last_committed_input_tokens: 10,
1577        };
1578        estimator.record_committed_turn(0, Some(20));
1579        assert_eq!(estimator.tokens_per_byte, 0.5);
1580        assert_eq!(estimator.last_committed_input_tokens, 20);
1581
1582        estimator.record_committed_turn(100, None);
1583        assert_eq!(estimator.tokens_per_byte, 0.5);
1584        assert_eq!(estimator.last_committed_input_tokens, 0);
1585
1586        estimator.record_committed_turn(100, Some(0));
1587        assert_eq!(estimator.tokens_per_byte, 0.5);
1588        assert_eq!(estimator.last_committed_input_tokens, 0);
1589    }
1590
1591    #[test]
1592    fn predictive_trigger_crosses_or_stays_below_effective_limit() {
1593        let estimator = ContextTokenEstimator {
1594            tokens_per_byte: 0.5,
1595            last_committed_input_tokens: 60,
1596        };
1597
1598        assert!(estimator.should_compact(20, 70));
1599        assert!(!estimator.should_compact(18, 70));
1600        assert!(!ContextTokenEstimator::default().should_compact(usize::MAX, 1));
1601    }
1602
1603    #[test]
1604    fn predictive_compaction_is_disabled_until_session_persistence() {
1605        let estimator = ContextTokenEstimator {
1606            tokens_per_byte: 1.0,
1607            last_committed_input_tokens: 100,
1608        };
1609
1610        assert!(!should_run_predictive_compaction(estimator, 100, 70, false));
1611        assert!(should_run_predictive_compaction(estimator, 100, 70, true));
1612    }
1613}