use std::sync::Arc;
use anyhow::Result;
use theway_core::PermissionPolicy;
use theway_core::multiagent::jobs::SubagentJobRegistry;
use super::DaemonOptions;
use super::process_state::ProcessRuntime;
use crate::orchestration::session::SessionProjectResources;
use crate::orchestration::{
SessionExecutionContext, SessionHookResources, SessionMcpResources, SessionRuntime,
SessionRuntimeBuilder,
};
use crate::session_activation::SessionActivator;
use crate::startup_config::StartupConfig;
use crate::turn::daemon::RuntimeCapabilities;
use crate::{agent_specs, runtime_capabilities, session_ops};
pub(super) struct SessionAssembly {
pub(super) context: SessionExecutionContext,
pub(super) mcp_provision: Arc<std::sync::RwLock<crate::mcp_loader::McpProvisionState>>,
pub(super) runtime: SessionRuntime,
pub(super) session_factory: session_ops::SessionFactory,
pub(super) capabilities: RuntimeCapabilities,
pub(super) thinking_summary: Option<crate::turn::thinking_summary::ThinkingSummarySettings>,
}
pub(super) async fn assemble_session(
process: &mut ProcessRuntime,
options: &DaemonOptions,
) -> Result<SessionAssembly> {
let session_paths = process.paths.with_work_dir(process.cwd.clone());
let (mcp_resources, mcp_provision) = load_mcp_resources(&session_paths, &process.startup).await;
let (project_resources, hook_resources) =
load_session_resources(&session_paths, options, &process.startup).await?;
let session_context = SessionExecutionContext::new(
process.session_id.clone(),
process.cwd.clone(),
process.repo.clone(),
process.storage.clone(),
process.paths.clone(),
process.executor.clone(),
process.startup.executor_kind,
process.model.clone(),
process.thinking,
project_resources,
mcp_resources,
hook_resources,
);
let dynamic_triggers = &process.services.dynamic_triggers;
dynamic_triggers.set_poll_interval_secs(process.startup.trigger_poll_secs);
let thinking_summary_cfg = process.startup.thinking_summary.clone();
let before_tool_call = PermissionPolicy::default_for_coding_agent().as_before_tool_call();
let (lsp_lang_count, after_tool_call) =
start_lsp_supervisor(&process.startup, &session_context, &process.cwd).await;
let session_runtime_builder = Arc::new(SessionRuntimeBuilder {
thinking: process.thinking,
stream_fn: process.stream_fn.clone(),
dag_engine: process.dag_engine.clone(),
subagent_registry: process.subagent_registry.clone(),
services: process.services.clone(),
before_tool_call: Some(before_tool_call.clone()),
control_plane_hook: process.control_plane_hook.take(),
control_plane_prompt_tx: process.control_plane_prompt_tx.take(),
after_tool_call,
feed_tx: process.feed_tx.clone(),
main_run_tx: process.main_run_tx.clone(),
debug: options.debug,
session_cells: Default::default(),
});
process
.services
.session_activator
.set(Arc::new(
SessionActivator::new(
&session_runtime_builder,
process.storage.clone(),
process.paths.clone(),
process.thinking,
options.builtin_skills.clone(),
process.startup.builtin_skills.clone(),
process.startup.load_local_sources,
)
.with_mcp_provision(mcp_provision.clone()),
))
.map_err(|_| anyhow::anyhow!("session activator already installed"))?;
let initial_runtime = session_runtime_builder
.build_opened(&session_context, process.store.clone(), process.resumed)
.await?;
let tool_names = initial_runtime.tool_names.clone();
let hooks_active = initial_runtime.hooks_active;
let storage = &process.storage;
let dag_persist = storage.spawn_dag_persist_for_sessions(
process.dag_engine.clone(),
process.cwd.clone(),
process.services.session_execution.clone(),
);
process.dag_persist = Some(dag_persist);
let session_factory = session_factory(session_runtime_builder, session_context.clone());
let capabilities = RuntimeCapabilities {
mcp_servers: session_context.mcp.server_count,
mcp_tools: session_context.mcp.tool_names.len(),
mcp_server_names: session_context.mcp.server_names.clone(),
mcp_tool_names: session_context.mcp.tool_names.clone(),
mcp_server_errors: session_context.mcp.server_errors.clone(),
tool_names,
mcp_notification_hooks: session_context.mcp.notification_hook_count,
hook_points: runtime_capabilities::active_hook_registrations(lsp_lang_count, hooks_active),
trigger_features: runtime_capabilities::active_trigger_features(),
};
let thinking_summary = build_thinking_summary(
thinking_summary_cfg,
&process.model,
&process.stream_fn,
&process.subagent_registry,
&process.session_id,
);
Ok(SessionAssembly {
context: session_context,
mcp_provision,
runtime: initial_runtime,
session_factory,
capabilities,
thinking_summary,
})
}
async fn load_mcp_resources(
session_paths: &crate::DaemonPaths,
startup: &StartupConfig,
) -> (
SessionMcpResources,
Arc<std::sync::RwLock<crate::mcp_loader::McpProvisionState>>,
) {
let mcp = if startup.load_local_sources {
crate::mcp_loader::load_all(session_paths).await
} else {
crate::mcp_loader::LoadedMcp::empty()
};
let mcp_provision = Arc::new(std::sync::RwLock::new(
crate::mcp_loader::McpProvisionState::default(),
));
let mut mcp_resources = SessionMcpResources::from_loaded(mcp);
if !startup.load_local_sources {
mcp_resources.provision = Some(mcp_provision.clone());
}
(mcp_resources, mcp_provision)
}
async fn load_session_resources(
session_paths: &crate::DaemonPaths,
options: &DaemonOptions,
startup: &StartupConfig,
) -> Result<(SessionProjectResources, SessionHookResources)> {
let project_resources = SessionProjectResources::load(
session_paths,
&options.builtin_skills,
&startup.builtin_skills,
startup.load_local_sources,
)
.await?;
let hook_resources =
SessionHookResources::load(session_paths, startup.load_local_sources).await;
Ok((project_resources, hook_resources))
}
async fn start_lsp_supervisor(
startup: &StartupConfig,
context: &SessionExecutionContext,
cwd: &std::path::Path,
) -> (usize, Option<theway_core::AfterToolCallHook>) {
let lsp_supervisor = Arc::new(if startup.load_local_sources {
crate::lsp_supervisor::LspSupervisor::load(&context.paths).await
} else {
crate::lsp_supervisor::LspSupervisor::from_config(cwd, Default::default())
});
let lsp_lang_count = lsp_supervisor.language_count();
let after_tool_call = if lsp_supervisor.is_empty() {
None
} else {
Some(crate::lsp_supervisor::as_after_tool_call(
lsp_supervisor.clone(),
))
};
(lsp_lang_count, after_tool_call)
}
fn session_factory(
plan: Arc<SessionRuntimeBuilder>,
startup_ctx: SessionExecutionContext,
) -> session_ops::SessionFactory {
Arc::new(move |id: String| {
let plan = plan.clone();
let startup_ctx = startup_ctx.clone();
Box::pin(async move {
let ctx = plan
.services
.session_execution
.get_context(&id)
.unwrap_or_else(|| Arc::new(startup_ctx.clone()));
plan.build(&ctx, &id).await
})
})
}
fn build_thinking_summary(
cfg: Option<theway_transport::config::ThinkingSummarySettings>,
model: &Option<theway_llm_provider::Model>,
stream_fn: &theway_core::StreamFn,
subagent_registry: &SubagentJobRegistry,
session_id: &str,
) -> Option<crate::turn::thinking_summary::ThinkingSummarySettings> {
cfg.map(|cfg| {
use crate::turn::thinking_summary::{ThinkingSummarizerFn, ThinkingSummarySettings};
let summarizer_model = model.clone();
let summarizer_stream = stream_fn.clone();
let summarizer_registry = subagent_registry.clone();
let summarizer_session = session_id.to_string();
let summarizer_launch = agent_specs::launch_resolver();
let summarizer: ThinkingSummarizerFn = Arc::new(move |text: String| {
let summarizer_launch = summarizer_launch.clone();
let summarizer_model = summarizer_model.clone();
let summarizer_stream = summarizer_stream.clone();
let summarizer_registry = summarizer_registry.clone();
let summarizer_session = summarizer_session.clone();
Box::pin(async move {
let Some(launch) = summarizer_launch("general") else {
return Err("general subagent spec unavailable".to_string());
};
let prompt = format!(
"Summarize the following reasoning transcript into a STRUCTURED markdown summary. Output ONLY the summary:\n## Goal\n- ...\n## Key steps\n- ...\n## Findings\n- ...\n## Decision\n- ...\n\nThinking transcript:\n\n{}",
theway_transport::feed::truncate_chars(&text, 24_000)
);
let Some(summarizer_model) = summarizer_model else {
return Err("no model set for this session; cannot summarize thinking"
.to_string());
};
let result = theway_core::multiagent::runner::run_agent(
theway_core::multiagent::runner::AgentRunOptions {
launch,
tools: Vec::new(),
prompt,
model: summarizer_model,
stream_fn: Some(summarizer_stream),
timeout: None,
thinking: None,
registry: summarizer_registry,
source: "thinking-summary".into(),
run_id: None,
node_id: None,
session_id: Some(summarizer_session),
observation_parent: None,
cancel: tokio_util::sync::CancellationToken::new(),
system_prompt_extra: Some(
"You are a thinking summarizer: compress verbose step-by-step \
reasoning into a concise structured summary. Never run tools. \
Never add commentary beyond the summary."
.to_string(),
),
on_turn_end: None,
},
)
.await;
match result.error {
Some(error) => Err(error),
None => Ok(result.text),
}
})
});
ThinkingSummarySettings {
min_chars: cfg.min_chars,
summarizer,
}
})
}