theway-daemon 0.1.11

theway daemon — the single agent-runtime kernel (bin `thewayd`): harness assembly, local/sandbox tool policy, triggers/cron/session/DAG runtime, skills, MCP/LSP wiring, serving the gRPC/HTTP/MCP transports from theway-transport. Terminal UI lives in the theway-tui crate.
Documentation
//! Session-turn execution kernel used by the daemon host.
//!
//! This module owns the "what work should the agent run" boundary: prompt futures, abort, model
//! capability checks, and queued-turn value types. Client rendering and interaction remain
//! outside this boundary.

use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use crate::agent_session::{AgentSession, RetrySettings};
use crate::orchestration::SessionRuntime;
use theway_core::{AgentHarness, AgentRunError};
use theway_llm_provider::{ImageContent, InputModality};

/// In-flight model turn, polled by the serialized host event loop.
///
/// Running this as a local future (not `tokio::spawn`) sidesteps the `Send` bound:
/// `AgentSession::prompt` briefly holds a `parking_lot` guard across an `.await`, so its future is
/// `!Send`.
pub type TurnFut = Pin<Box<dyn Future<Output = Result<Option<String>, AgentRunError>>>>;

#[derive(Default)]
pub struct TurnState {
    pub fut: Option<TurnFut>,
    pub aborted: bool,
    /// Prefix for the error line if the turn fails (e.g. `triggered turn: `).
    pub prefix: &'static str,
}

pub async fn poll_turn(fut: &mut Option<TurnFut>) -> Result<Option<String>, AgentRunError> {
    // Only created by `select!` when `fut.is_some()`, so the unwrap is sound.
    fut.as_mut().expect("turn future present").await
}

pub enum QueuedTurn {
    UserPrompt {
        display: String,
        prompt: String,
        images: Vec<ImageContent>,
    },
    AgentPrompt {
        display: String,
        prompt: String,
        error_context: &'static str,
    },
    PromptTemplate {
        display: String,
        name: String,
        vars: serde_json::Map<String, serde_json::Value>,
    },
    Compaction {
        display: String,
        custom: Option<String>,
    },
}

impl QueuedTurn {
    pub fn display(&self) -> &str {
        match self {
            Self::UserPrompt { display, .. }
            | Self::AgentPrompt { display, .. }
            | Self::PromptTemplate { display, .. }
            | Self::Compaction { display, .. } => display,
        }
    }
}

#[derive(Clone)]
pub struct ReplKernel {
    harness: Arc<AgentHarness>,
    trigger_executor: Arc<crate::trigger_engine::execution::TriggerExecutor>,
    retry: RetrySettings,
    extension_host: Option<Arc<crate::ts_extensions::SessionPluginHost>>,
}

impl ReplKernel {
    pub fn new(
        harness: Arc<AgentHarness>,
        trigger_executor: Arc<crate::trigger_engine::execution::TriggerExecutor>,
        retry: RetrySettings,
    ) -> Self {
        Self {
            harness,
            trigger_executor,
            retry,
            extension_host: None,
        }
    }

    pub fn set_extension_host(
        &mut self,
        extension_host: Option<Arc<crate::ts_extensions::SessionPluginHost>>,
    ) {
        self.extension_host = extension_host;
    }

    pub fn extension_host(&self) -> Option<&Arc<crate::ts_extensions::SessionPluginHost>> {
        self.extension_host.as_ref()
    }

    pub fn trigger_executor(&self) -> &Arc<crate::trigger_engine::execution::TriggerExecutor> {
        &self.trigger_executor
    }

    pub fn harness(&self) -> &Arc<AgentHarness> {
        &self.harness
    }

    /// Swap in a complete session runtime.
    ///
    /// Retry settings are process configuration and remain unchanged. Harness and trigger
    /// executor are session-scoped and must always move together.
    #[allow(dead_code)] // used by integration tests and future runtime replacement paths
    pub fn replace_runtime(&mut self, runtime: SessionRuntime) {
        self.harness = runtime.harness;
        self.trigger_executor = runtime.trigger_executor;
        self.extension_host = runtime.extension_host;
    }

    pub fn abort(&self) {
        self.harness.abort();
    }

    pub fn is_streaming(&self) -> bool {
        self.harness.agent().is_streaming()
    }

    pub fn current_model_accepts_images(&self) -> bool {
        let state = self.harness.agent().state();
        state
            .model
            .as_ref()
            .map(|model| model.input.contains(&InputModality::Image))
            .unwrap_or(false)
    }

    pub fn prompt_turn(&self, prompt: String) -> TurnFut {
        let harness = self.harness.clone();
        Box::pin(async move { harness.prompt(prompt).await.map(|_| None) })
    }

    pub fn user_prompt_turn(
        &self,
        prompt_text: String,
        loaded_images: Vec<ImageContent>,
    ) -> TurnFut {
        let harness = self.harness.clone();
        let retry = self.retry.clone();
        let has_images = !loaded_images.is_empty();
        Box::pin(async move {
            if has_images {
                harness
                    .prompt_with_images(prompt_text, loaded_images)
                    .await
                    .map(|_| None)
            } else {
                AgentSession::new(harness, retry)
                    .prompt(prompt_text)
                    .await
                    .map(|_| None)
            }
        })
    }

    pub fn template_turn(
        &self,
        name: String,
        vars: serde_json::Map<String, serde_json::Value>,
    ) -> TurnFut {
        let harness = self.harness.clone();
        Box::pin(async move {
            harness
                .prompt_from_template(&name, vars)
                .await
                .map(|_| None)
        })
    }

    pub fn compaction_turn(&self, custom: Option<String>) -> TurnFut {
        let harness = self.harness.clone();
        Box::pin(async move {
            harness.force_compact(custom).await.map(|ran| {
                Some(if ran {
                    "compaction ran".to_string()
                } else {
                    "nothing to compact".to_string()
                })
            })
        })
    }

    pub fn continue_turn(&self) -> TurnFut {
        let harness = self.harness.clone();
        Box::pin(async move { harness.continue_().await.map(|_| None) })
    }
}

#[cfg(test)]
// Test files live in `tests/turn/kernel/` (mirror of src), pulled in by
// path so they keep unit-test semantics (private access). See docs/rust-test-files.md.
tests_bridge_macro::tests_bridge!("turn/kernel");

#[cfg(test)]
mod kernel_extra_tests {
    //! Extra turn-kernel tests live in `tests/turn/kernel/extra/` so the
    //! primary `tests/turn/kernel/mod.rs` bridge stays untouched.
    tests_bridge_macro::tests_bridge!("turn/kernel/extra");
}