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;
79pub(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#[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
131pub(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 #[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 #[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#[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 SessionError::TreeRejected(inner) => *inner,
281 other => Self::Session(other),
282 }
283 }
284}
285
286impl EngineError {
287 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 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 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
515const 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;
525pub(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#[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, ¤t_models)?;
770 let current_manifest = prepare_runtime_manifest(¤t_models)?;
771 let snapshot = build_runtime_snapshot(¤t_models, &agents, &agent_presets)?;
772 let published_runtime = Arc::new(PublishedRuntime {
773 result: RuntimeSnapshotResult { snapshot },
774 models: Arc::clone(¤t_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 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 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(¶ms.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 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 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 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 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 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 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 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 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 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 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}