Files
coyote/src/config/app_state.rs
T
Dark-Alex-17 cb025b7fff feat: add background job runner, job__* handlers, and start gates
Detached tokio::process runner with a frozen JobEnvSnapshot (env-derived
bin dirs, vault-interpolated agent envs, COYOTE_TOOL_TIMEOUT resolved at
start), process_group(0) with pgid-guarded SIGTERM/SIGKILL escalation,
capture-only ring-buffer telemetry, and LLM_OUTPUT read after wait().
MCP jobs snapshot a single-entry McpRuntime holding only the validated
server and render through the same free fn as the foreground path.

job__start enforces its gates synchronously before any spawn:
jobs_enabled, the backgroundable whitelist with directionality teaching
errors, the per-request declared-names stash captured in
before_chat_completion, then capacity (lazy supervisor get-or-init in
plain sessions). job__check/list read the shared JobState cell without
consuming; job__collect blocks with the escalation early-out and applies
a tail-biased char-boundary cap plus optional tail_lines; job__cancel
kills the group with a 5s grace.

Job declarations are injected iff jobs are enabled at agent init, the
plain-session function-init sites, and the exit_agent rebuild; job__ is
carved out of enabled_tools filtering and excluded from
concrete_tool_names so REPL toggles cannot grant or revoke it.
2026-08-25 17:36:17 -06:00

98 lines
2.9 KiB
Rust

use super::mcp_factory::{McpFactory, McpServerKey};
use super::rag_cache::RagCache;
use crate::config::AppConfig;
use crate::config::jobs_enabled;
use crate::function::Functions;
use crate::mcp::{McpRegistry, McpServersConfig};
use crate::utils::AbortSignal;
use crate::vault::{GlobalVault, Vault};
use anyhow::Result;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub config: Arc<AppConfig>,
pub vault: GlobalVault,
pub mcp_factory: Arc<McpFactory>,
pub rag_cache: Arc<RagCache>,
pub mcp_config: Option<McpServersConfig>,
pub mcp_log_path: Option<PathBuf>,
pub mcp_registry: Option<Arc<McpRegistry>>,
pub functions: Functions,
}
impl AppState {
#[cfg(test)]
pub fn test_default() -> Self {
Self {
config: Arc::new(AppConfig::default()),
vault: Arc::new(Vault::default()),
mcp_factory: Arc::new(McpFactory::default()),
rag_cache: Arc::new(RagCache::default()),
mcp_config: None,
mcp_log_path: None,
mcp_registry: None,
functions: Functions::default(),
}
}
pub async fn init(
config: Arc<AppConfig>,
log_path: Option<PathBuf>,
start_mcp_servers: bool,
abort_signal: AbortSignal,
) -> Result<Self> {
let vault = Arc::new(Vault::init(&config)?);
let mcp_registry = McpRegistry::init(
log_path,
start_mcp_servers,
config.enabled_mcp_servers.clone(),
abort_signal,
&config,
&vault,
)
.await?;
let mcp_config = mcp_registry.mcp_config().cloned();
let mcp_log_path = mcp_registry.log_path().cloned();
let mcp_factory = Arc::new(McpFactory::default());
if let Some(mcp_servers_config) = &mcp_config {
for (id, handle) in mcp_registry.running_servers() {
if let Some(spec) = mcp_servers_config.mcp_servers.get(id) {
let key = McpServerKey::from_spec(id, spec);
mcp_factory.insert_active(key, handle);
}
}
}
let mut functions = Functions::init(config.visible_tools.as_ref().unwrap_or(&Vec::new()))?;
if !mcp_registry.is_empty() && config.mcp_server_support {
functions.append_mcp_meta_functions(mcp_registry.server_features());
}
if jobs_enabled(None, &config) {
functions.append_job_functions();
}
let mcp_registry = if mcp_registry.is_empty() {
None
} else {
Some(Arc::new(mcp_registry))
};
Ok(Self {
config,
vault,
mcp_factory,
rag_cache: Arc::new(RagCache::default()),
mcp_config,
mcp_log_path,
mcp_registry,
functions,
})
}
}