use std::sync::{Arc, OnceLock};
use crate::hook_executors::daemon_executors;
use crate::hooks;
use crate::orchestration::DaemonServices;
use crate::runtime_storage::{RuntimeStorage, SessionRepository};
use crate::trigger_engine::notification_hook::DynNotificationHook;
use crate::{agent_specs, tools, triggers};
use anyhow::{Context, Result};
use theway_contract::session::SessionStore;
use theway_core::multiagent::goal;
use theway_core::multiagent::graph::engine::DagEngine;
use theway_core::multiagent::jobs::JobTranscriptStore;
use theway_core::{AgentHarness, AgentHarnessOptions, ThinkingLevel};
use theway_transport::feed::FeedUpdate;
use theway_transport::inbox;
mod activation_build;
pub(crate) use activation_build::load_persisted_dag_runs;
#[derive(Clone)]
pub struct SessionProjectResources {
pub memory_block: String,
pub skills: Vec<theway_core::Skill>,
pub templates: Vec<theway_core::PromptTemplate>,
pub memory_dir: std::path::PathBuf,
pub reload_skills_fn: theway_core::ReloadSkillsFn,
pub load_local_sources: bool,
}
impl SessionProjectResources {
pub async fn load(
paths: &crate::DaemonPaths,
cli_builtin_skills: &[String],
config_builtin_skills: &[String],
load_local_sources: bool,
) -> Result<Self> {
let memory_dir = paths.base.join("memory");
let memory_block = crate::tools::memory::load_memory_block(&memory_dir).await;
let loaded_skills = if load_local_sources {
crate::skills::load_all(paths).await
} else {
crate::skills::LoadedSkills {
skills: Vec::new(),
diagnostics: Vec::new(),
}
};
let loaded_templates = if load_local_sources {
crate::templates::load_all(paths).await
} else {
crate::templates::LoadedTemplates {
templates: Vec::new(),
diagnostics: Vec::new(),
}
};
let resolved_builtins =
crate::builtin_skills::resolve_builtins(cli_builtin_skills, config_builtin_skills)?;
let mut skills = crate::builtin_skills::merge_with_user_project(
resolved_builtins.skills.clone(),
&loaded_skills.skills,
);
let state = if load_local_sources {
crate::skill_overrides::load(&paths.base).await
} else {
crate::skill_overrides::SkillOverrides::default()
};
crate::skill_overrides::apply(&state, &mut skills);
let reload_skills_fn: theway_core::ReloadSkillsFn = {
let paths = paths.clone();
let builtins = resolved_builtins.skills.clone();
std::sync::Arc::new(move || {
let paths = paths.clone();
let builtins = builtins.clone();
Box::pin(async move {
let loaded = if load_local_sources {
crate::skills::load_all(&paths).await
} else {
crate::skills::LoadedSkills {
skills: Vec::new(),
diagnostics: Vec::new(),
}
};
let mut merged =
crate::builtin_skills::merge_with_user_project(builtins, &loaded.skills);
let state = if load_local_sources {
crate::skill_overrides::load(&paths.base).await
} else {
crate::skill_overrides::SkillOverrides::default()
};
crate::skill_overrides::apply(&state, &mut merged);
theway_core::LoadSkillsOutput {
skills: merged,
diagnostics: loaded.diagnostics,
}
})
})
};
Ok(Self {
memory_block,
skills,
templates: loaded_templates.templates,
memory_dir,
reload_skills_fn,
load_local_sources,
})
}
}
#[derive(Clone, Default)]
pub struct SessionMcpResources {
pub tools: Vec<Arc<dyn theway_core::AgentTool>>,
pub notification_hooks: Arc<parking_lot::Mutex<Vec<Arc<triggers::McpNotificationHook>>>>,
pub inject_summary_servers: std::collections::HashSet<String>,
pub inject_and_run_servers: std::collections::HashSet<String>,
pub server_count: usize,
pub server_names: Vec<String>,
pub tool_names: Vec<String>,
pub notification_hook_count: usize,
}
impl SessionMcpResources {
pub fn from_loaded(loaded: crate::mcp_loader::LoadedMcp) -> Self {
for diagnostic in &loaded.diagnostics {
tracing::warn!(target: "mcp", "{diagnostic}");
}
let tool_names = loaded
.tools
.iter()
.map(|tool| tool.definition().name.clone())
.collect::<Vec<_>>();
let notification_hook_count = loaded.notification_hooks.len();
Self {
tools: loaded.tools,
notification_hooks: Arc::new(parking_lot::Mutex::new(loaded.notification_hooks)),
inject_summary_servers: loaded.inject_summary_servers,
inject_and_run_servers: loaded.inject_and_run_servers,
server_count: loaded.client_count,
server_names: loaded.server_names,
tool_names,
notification_hook_count,
}
}
}
#[derive(Clone)]
pub struct SessionExtensionResources {
pub compact_algorithms:
std::sync::Arc<theway_core::agent::compaction::algorithm::CompactAlgorithmRegistry>,
pub legacy_compaction_host: Option<std::sync::Arc<crate::ts_extensions::LegacyCompactionHost>>,
pub runtime_extension_packages:
std::sync::Arc<parking_lot::RwLock<crate::ts_extensions::PackageCatalog>>,
pub runtime_extension_engine: Option<std::sync::Arc<crate::ts_extensions::QuickJsEnginePool>>,
}
impl SessionExtensionResources {
pub fn new(
cwd: &std::path::Path,
base: &std::path::Path,
executor: std::sync::Arc<dyn theway_core::executor::ToolExecutor>,
load_local_sources: bool,
) -> Self {
let ts_extensions = if load_local_sources {
crate::ts_extensions::ExtensionRegistry::discover(cwd, base)
} else {
crate::ts_extensions::ExtensionRegistry::new()
};
for error in &ts_extensions.errors {
tracing::warn!(target: "extensions", "{error}");
}
let legacy_compaction_host = std::sync::Arc::new(
crate::ts_extensions::LegacyCompactionHost::new(&ts_extensions),
);
let compact_algorithms = legacy_compaction_host.registry();
let runtime_extension_packages = std::sync::Arc::new(parking_lot::RwLock::new(
ts_extensions.package_catalog().clone(),
));
let runtime_extension_engine = load_local_sources.then(|| {
let broker_services =
crate::ts_extensions::ExtensionBrokerServices::new(base, executor);
for package in runtime_extension_packages.read().effective_packages() {
for permission in package.granted_permissions() {
if let theway_contract::extension::ExtensionPermission::SecretsRead(name) =
permission
&& let Ok(value) = std::env::var(name)
{
broker_services.set_secret(name, value);
}
}
}
std::sync::Arc::new(
crate::ts_extensions::QuickJsEnginePool::with_broker_services(
std::thread::available_parallelism()
.map(usize::from)
.unwrap_or(1)
.min(4),
crate::ts_extensions::QuickJsEngineLimits::default(),
broker_services,
),
)
});
Self {
compact_algorithms,
legacy_compaction_host: Some(legacy_compaction_host),
runtime_extension_packages,
runtime_extension_engine,
}
}
}
#[derive(Clone)]
pub struct SessionHookResources {
loaded: Arc<hooks::LoadedHooks>,
}
impl SessionHookResources {
pub async fn load(paths: &crate::DaemonPaths, read_local_files: bool) -> Self {
let loaded = hooks::load_with(
paths,
"",
None::<&theway_llm_provider::Model>,
None::<ThinkingLevel>,
daemon_executors(),
read_local_files,
)
.await;
for diag in &loaded.diagnostics {
tracing::warn!(target: "hooks", "hooks loader: {diag}");
}
Self {
loaded: Arc::new(loaded),
}
}
pub fn loaded_hooks(
&self,
session_id: impl Into<String>,
model: Option<&theway_llm_provider::Model>,
thinking_level: Option<ThinkingLevel>,
) -> hooks::LoadedHooks {
hooks::LoadedHooks {
runner: Arc::new(
self.loaded
.runner
.for_session(session_id, model, thinking_level),
),
diagnostics: self.loaded.diagnostics.clone(),
}
}
}
#[derive(Clone)]
pub struct SessionExecutionContext {
pub session_id: String,
pub cwd: std::path::PathBuf,
pub transcript_store: Arc<dyn JobTranscriptStore>,
pub repo: Arc<dyn SessionRepository>,
pub storage: Arc<dyn RuntimeStorage>,
pub paths: crate::DaemonPaths,
pub executor: Arc<dyn theway_core::executor::ToolExecutor>,
pub model: theway_llm_provider::Model,
pub thinking: theway_core::ThinkingLevel,
pub resources: SessionProjectResources,
pub mcp: SessionMcpResources,
pub hooks: SessionHookResources,
pub extension_resources: SessionExtensionResources,
}
impl SessionExecutionContext {
pub fn new(
session_id: impl Into<String>,
cwd: std::path::PathBuf,
repo: Arc<dyn SessionRepository>,
storage: Arc<dyn RuntimeStorage>,
paths: crate::DaemonPaths,
executor: Arc<dyn theway_core::executor::ToolExecutor>,
model: theway_llm_provider::Model,
thinking: theway_core::ThinkingLevel,
resources: SessionProjectResources,
mcp: SessionMcpResources,
hooks: SessionHookResources,
) -> Self {
let session_id = session_id.into();
let cwd = cwd.canonicalize().unwrap_or(cwd);
let transcript_store = storage.job_transcript_store(&cwd);
let paths = paths.with_work_dir(cwd.clone());
let extension_resources = SessionExtensionResources::new(
&cwd,
&paths.base,
executor.clone(),
resources.load_local_sources,
);
Self {
session_id,
cwd,
transcript_store,
repo,
storage,
paths,
executor,
model,
thinking,
resources,
mcp,
hooks,
extension_resources,
}
}
#[allow(dead_code)] pub async fn build_for_work_dir(
session_id: impl Into<String>,
requested_work_dir: std::path::PathBuf,
repo: Arc<dyn SessionRepository>,
storage: Arc<dyn RuntimeStorage>,
base_paths: crate::DaemonPaths,
model: theway_llm_provider::Model,
thinking: theway_core::ThinkingLevel,
cli_builtin_skills: &[String],
config_builtin_skills: &[String],
load_local_sources: bool,
) -> Result<Self> {
let cwd = requested_work_dir
.canonicalize()
.with_context(|| format!("canonicalize work dir {}", requested_work_dir.display()))?;
let paths = base_paths.with_work_dir(cwd.clone());
let executor = crate::executor::executor_for_cwd(cwd.clone());
let loaded_mcp = if load_local_sources {
crate::mcp_loader::load_all(&paths).await
} else {
crate::mcp_loader::LoadedMcp::empty()
};
let resources = SessionProjectResources::load(
&paths,
cli_builtin_skills,
config_builtin_skills,
load_local_sources,
)
.await?;
let hooks = SessionHookResources::load(&paths, load_local_sources).await;
Ok(SessionExecutionContext::new(
session_id,
cwd,
repo,
storage,
base_paths,
executor,
model,
thinking,
resources,
SessionMcpResources::from_loaded(loaded_mcp),
hooks,
))
}
}
pub struct SessionRuntimeBuilder {
#[allow(dead_code)] pub thinking: ThinkingLevel,
pub stream_fn: theway_core::StreamFn,
pub dag_engine: Arc<DagEngine>,
pub subagent_registry: theway_core::multiagent::jobs::SubagentJobRegistry,
pub services: DaemonServices,
pub before_tool_call: Option<theway_core::BeforeToolCallHook>,
pub control_plane_hook: Option<theway_core::OnControlPlanePromptHook>,
pub after_tool_call: Option<theway_core::AfterToolCallHook>,
pub feed_tx: tokio::sync::mpsc::UnboundedSender<FeedUpdate>,
pub main_run_tx: tokio::sync::mpsc::UnboundedSender<String>,
pub debug: bool,
}
pub struct SessionRuntime {
pub session_id: String,
pub cwd: std::path::PathBuf,
pub harness: Arc<AgentHarness>,
pub trigger_executor: Arc<crate::trigger_engine::execution::TriggerExecutor>,
pub tool_names: Vec<String>,
pub hooks_active: bool,
pub extension_host: Option<Arc<crate::ts_extensions::SessionPluginHost>>,
}
#[cfg(test)]
impl SessionRuntime {
pub(crate) fn for_test(session_id: impl Into<String>, harness: Arc<AgentHarness>) -> Self {
let session_id = session_id.into();
let trigger_executor = Arc::new(crate::trigger_engine::execution::TriggerExecutor::new(
harness.agent_arc(),
harness.session().clone(),
crate::trigger_engine::runtime::TriggerRuntimeConfig::default(),
None,
None,
None,
None,
None,
None,
));
Self {
session_id: session_id.clone(),
cwd: std::env::temp_dir().join("theway-test").join(session_id),
harness,
trigger_executor,
tool_names: Vec::new(),
hooks_active: false,
extension_host: None,
}
}
}
impl SessionRuntimeBuilder {
pub async fn build(&self, ctx: &SessionExecutionContext, id: &str) -> Result<SessionRuntime> {
let store = ctx
.repo
.resume(Some(id))
.await
.with_context(|| format!("open session {id}"))?;
self.build_opened(ctx, store, true).await
}
pub async fn build_opened(
&self,
ctx: &SessionExecutionContext,
store: Arc<dyn SessionStore>,
rehydrate: bool,
) -> Result<SessionRuntime> {
let (ctx, session_id, store) = self.opened_context(ctx, store).await?;
let restored = load_persisted_dag_runs(&ctx, &session_id).await?;
let skill_harness_cell = self.install_execution_context(ctx.clone(), restored);
self.assemble_opened(ctx, store, session_id, rehydrate, skill_harness_cell)
.await
}
async fn opened_context(
&self,
ctx: &SessionExecutionContext,
store: Arc<dyn SessionStore>,
) -> Result<(Arc<SessionExecutionContext>, String, Arc<dyn SessionStore>)> {
let meta = store.get_metadata_json().await?;
let session_id = meta
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_string();
let mut owned_ctx = ctx.clone();
owned_ctx.session_id = session_id.clone();
Ok((Arc::new(owned_ctx), session_id, store))
}
async fn assemble_opened(
&self,
ctx: Arc<SessionExecutionContext>,
store: Arc<dyn SessionStore>,
session_id: String,
rehydrate: bool,
skill_harness_cell: crate::tools::skill::SkillHarnessCell,
) -> Result<SessionRuntime> {
let extension_state_store = Arc::clone(&store);
let session = theway_core::Session::from_store(store);
let mut tools = tools::session_tool_set_for_cwd(
&ctx.resources.memory_dir,
&ctx.paths.base,
&self.dag_engine,
&self.subagent_registry,
&ctx.model,
Some(&self.stream_fn),
&skill_harness_cell,
&session_id,
ctx.executor.clone(),
&self.services,
ctx.cwd.clone(),
);
tools.extend(ctx.mcp.tools.iter().cloned());
let goal_harness_cell: Arc<OnceLock<Arc<AgentHarness>>> = Arc::new(OnceLock::new());
let mut opts = AgentHarnessOptions::new(ctx.model.clone(), session);
opts.observer = self.subagent_registry.observer();
opts.observation_context = theway_core::ObservationContext {
session_id: Some(session_id.clone()),
..theway_core::ObservationContext::default()
};
opts.runtime_extension_cwd = ctx.cwd.to_string_lossy().into_owned();
let session_credentials = self.services.session_execution.clone();
let session_id_for_credentials = session_id.clone();
let session_credential_resolver: theway_core::GetApiKey = Arc::new(move |provider_id| {
session_credentials
.get_credential(&session_id_for_credentials, provider_id)
.map(|secret| String::from_utf8_lossy(secret.as_bytes()).into_owned())
});
let mut runtime_extension_host = None;
if let Some(engine) = &ctx.extension_resources.runtime_extension_engine {
let base_tools = tools.clone();
let extensions = Arc::new(
crate::ts_extensions::SessionPluginHost::load_with_state_and_legacy(
ctx.extension_resources.runtime_extension_packages.read().clone(),
engine.as_ref().clone(),
session_id.clone(),
&ctx.cwd,
crate::ts_extensions::RuntimeExtensionHostConfig::default(),
Arc::new(
theway_core::agent::runtime_extensions::PersistentSessionExtensionStatePort::new(
extension_state_store,
),
),
ctx.extension_resources.legacy_compaction_host.clone(),
Some(ctx.extension_resources.runtime_extension_packages.clone()),
)
.await,
);
for diagnostic in extensions
.diagnostics()
.into_iter()
.filter(|diagnostic| diagnostic.session_id.is_some())
{
tracing::warn!(
target: "extensions",
extension_id = diagnostic.extension_id,
"{}",
diagnostic.message
);
}
tools = extensions.merge_registered_tools(tools);
let session_credential_resolver = session_credential_resolver.clone();
let credential_host = Arc::clone(&extensions);
opts.get_api_key = Some(Arc::new(move |provider_id| {
if let Some(secret) = session_credential_resolver(provider_id) {
return Some(secret);
}
credential_host.provider_api_key(provider_id)
}));
opts.runtime_extension_model_context = extensions.model_context_projection();
opts.runtime_extensions = extensions.clone();
runtime_extension_host = Some((extensions, base_tools));
} else {
opts.get_api_key = Some(session_credential_resolver);
}
let tool_names = tools
.iter()
.map(|tool| tool.definition().name.clone())
.collect::<Vec<_>>();
let system_prompt = crate::system_prompt::compose_system_prompt(
&ctx.cwd,
&ctx.resources.memory_block,
&tool_names,
);
opts.system_prompt = system_prompt;
opts.thinking_level = ctx.thinking;
opts.tools = tools;
opts.skills = ctx.resources.skills.clone();
opts.prompt_templates = ctx.resources.templates.clone();
opts.compact_algorithms = ctx.extension_resources.compact_algorithms.clone();
opts.stream_fn = Some(self.stream_fn.clone());
opts.reload_skills_fn = Some(ctx.resources.reload_skills_fn.clone());
opts.on_turn_end = Some(goal::stop_hook(
goal_harness_cell.clone(),
self.dag_engine.clone(),
agent_specs::launch_resolver(),
self.subagent_registry.clone(),
Some(self.stream_fn.clone()),
));
opts.turn_continuation_cap = Some(goal::MAX_CONTINUATIONS);
opts.before_tool_call = self.before_tool_call.clone();
opts.on_control_plane_prompt = self.control_plane_hook.clone();
opts.after_tool_call = self.after_tool_call.clone();
let harness = std::sync::Arc::new(AgentHarness::new(opts));
if let Some((extensions, base_tools)) = &runtime_extension_host {
let agent = harness.agent_arc();
let agent = Arc::downgrade(&agent);
extensions.configure_reload_tool_publisher(
base_tools.clone(),
Arc::new(move |tools| {
if let Some(agent) = agent.upgrade() {
agent.state().tools = tools;
}
}),
);
}
let before_trigger_action = triggers::cron_action_hook(
self.services.cron.clone(),
triggers::direct_inject_action_hook(
ctx.mcp.inject_summary_servers.clone(),
ctx.mcp.inject_and_run_servers.clone(),
triggers::before_trigger_action_hook(self.services.dynamic_triggers.clone()),
),
);
let trigger_executor =
std::sync::Arc::new(crate::trigger_engine::execution::TriggerExecutor::new(
harness.agent_arc(),
harness.session().clone(),
crate::trigger_engine::runtime::TriggerRuntimeConfig::default(),
None,
None,
Some(before_trigger_action),
Some(self.stream_fn.clone()),
self.before_tool_call.clone(),
self.after_tool_call.clone(),
));
let mcp_notification_hooks = std::mem::take(&mut *ctx.mcp.notification_hooks.lock());
register_notification_hooks(
&trigger_executor,
&mcp_notification_hooks,
&ctx.cwd,
&self.services.cron,
&self.services.dynamic_triggers,
);
let _ = skill_harness_cell.set(harness.clone());
let _ = goal_harness_cell.set(harness.clone());
let _agent_broadcast = crate::turn::listener::spawn_agent_broadcast_listener(
harness.agent().subscribe_broadcast(),
self.feed_tx.clone(),
);
let _harness_broadcast = crate::turn::listener::spawn_harness_broadcast_listener(
harness.subscribe_session_broadcast(),
self.feed_tx.clone(),
self.debug,
);
let _ = trigger_executor.subscribe(crate::turn::listener::trigger_listener(
self.feed_tx.clone(),
self.debug,
));
let _ = trigger_executor.subscribe(triggers::fire_once_trigger_listener(
self.services.dynamic_triggers.clone(),
));
let _ = trigger_executor.subscribe(triggers::cron_trigger_listener(
self.services.cron.clone(),
inbox::default_inbox_path(),
));
let (hook_model, hook_thinking) = {
let state = harness.agent().state();
(state.model.clone(), state.thinking_level)
};
let loaded_hooks =
ctx.hooks
.loaded_hooks(session_id.clone(), hook_model.as_ref(), hook_thinking);
let _ = harness.agent().subscribe(loaded_hooks.runner.listener());
let _ = harness.subscribe_harness(loaded_hooks.runner.harness_listener());
let main_run_tx = self.main_run_tx.clone();
let _ = trigger_executor.subscribe(std::sync::Arc::new(
move |ev: crate::trigger_engine::event::TriggerEvent| {
if let crate::trigger_engine::event::TriggerEvent::TriggerRequestsMainRun {
trace_id,
} = ev
{
let _ = main_run_tx.send(trace_id);
}
},
));
if rehydrate {
harness
.rehydrate_from_session()
.await
.with_context(|| format!("rehydrate session {session_id}"))?;
}
harness.start_runtime_extensions().await;
Ok(SessionRuntime {
session_id,
cwd: ctx.cwd.clone(),
harness,
trigger_executor,
tool_names,
hooks_active: !loaded_hooks.runner.is_empty(),
extension_host: runtime_extension_host.map(|(host, _)| host),
})
}
}
trait NotificationHookSink {
fn register(&self, hook: DynNotificationHook);
}
impl NotificationHookSink for std::sync::Arc<crate::trigger_engine::execution::TriggerExecutor> {
fn register(&self, hook: DynNotificationHook) {
self.register_notification_hook(hook);
}
}
fn register_notification_hooks(
sink: &(impl NotificationHookSink + ?Sized),
mcp_notification_hooks: &[Arc<triggers::McpNotificationHook>],
cwd: &std::path::Path,
cron_registry: &triggers::cron::CronRegistry,
dynamic_trigger_registry: &triggers::dynamic::DynamicTriggerRegistry,
) {
for hook in mcp_notification_hooks {
sink.register(hook.clone());
}
sink.register(Arc::new(triggers::CronNotificationHook::new(
cron_registry.clone(),
)));
sink.register(Arc::new(triggers::DynamicTriggerCheckHook::new_for_cwd(
dynamic_trigger_registry.clone(),
cwd,
)));
}
#[cfg(test)]
tests_bridge_macro::tests_bridge!("orchestration/session");