use super::engines::EngineManager;
use super::impersonation::{build_impersonation_request, swap_role_message};
use super::request::{PromptContext, build_request};
use super::restart_queue::RestartQueue;
use super::save_queue::SaveQueue;
use super::title::{TitleOrigin, TitleResult, salvage_title_source};
use super::*;
use crate::app::events::RagProgress;
use crate::entities::chat::FeedView;
use crate::entities::message::{Message, MessageRole};
use crate::features::profiles::ProfileEdit;
use crate::shared::api::{ChatChunk, Embedder, EngineBackend};
use crate::app::supervisor::MockSupervisor;
use crate::shared::api::mock::{MockBackend, MockEmbedder};
use crate::shared::paths::Paths;
fn test_embedder() -> Arc<dyn Embedder> {
Arc::new(MockEmbedder::new(16))
}
fn no_auto_cfg() -> AppConfig {
let mut cfg = AppConfig::default();
cfg.interface.auto_title = crate::shared::config::AutoTitleMode::Off;
cfg.default_sampling.max_tokens = Some(2048);
cfg
}
pub(super) struct CapturingBackend {
pub(super) last: std::sync::Mutex<Option<crate::shared::api::ChatRequest>>,
}
impl CapturingBackend {
pub(super) fn new() -> Arc<Self> {
Arc::new(Self {
last: std::sync::Mutex::new(None),
})
}
pub(super) fn last_request(&self) -> crate::shared::api::ChatRequest {
self.last.lock().unwrap().clone().expect("a request")
}
}
#[async_trait::async_trait]
impl EngineBackend for CapturingBackend {
async fn chat_stream(
&self,
req: crate::shared::api::ChatRequest,
_cancel: tokio_util::sync::CancellationToken,
) -> anyhow::Result<crate::shared::api::contract::ChatStream> {
*self.last.lock().unwrap() = Some(req);
let s = async_stream::stream! {
yield ChatChunk::Text("ок".to_string());
yield ChatChunk::Finished(crate::shared::api::FinishReason::Stop);
};
Ok(Box::pin(s))
}
}
fn spawn_orch_sup(
config: AppConfig,
) -> (
tempfile::TempDir,
Arc<MockSupervisor>,
UnboundedSender<AppCommand>,
UnboundedReceiver<AppEvent>,
tokio::task::JoinHandle<()>,
) {
let dir = tempfile::tempdir().unwrap();
let sup = Arc::new(MockSupervisor::with_backend(None));
let storage = Arc::new(Storage::open(Paths::with_root(dir.path())).unwrap());
let (cmd_tx, cmd_rx) = unbounded_channel();
let (evt_tx, evt_rx) = unbounded_channel();
let handle = tokio::spawn(run(OrchestratorDeps {
cmd_rx,
evt_tx,
storage,
config,
supervisor: sup.clone(),
default_language: crate::shared::i18n::Lang::default(),
extra_tools: Vec::new(),
}));
(dir, sup, cmd_tx, evt_rx, handle)
}
fn spawn_orch(
backend: Option<Arc<dyn EngineBackend>>,
) -> (
tempfile::TempDir,
UnboundedSender<AppCommand>,
UnboundedReceiver<AppEvent>,
tokio::task::JoinHandle<()>,
) {
spawn_orch_cfg(backend, AppConfig::default())
}
fn spawn_orch_cfg(
backend: Option<Arc<dyn EngineBackend>>,
config: AppConfig,
) -> (
tempfile::TempDir,
UnboundedSender<AppCommand>,
UnboundedReceiver<AppEvent>,
tokio::task::JoinHandle<()>,
) {
let dir = tempfile::tempdir().unwrap();
let (cmd_tx, evt_rx, handle) = spawn_orch_at(dir.path(), backend, config);
(dir, cmd_tx, evt_rx, handle)
}
fn spawn_orch_tools(
backend: Option<Arc<dyn EngineBackend>>,
config: AppConfig,
extra_tools: Vec<Arc<dyn crate::features::tools::Tool>>,
) -> (
tempfile::TempDir,
UnboundedSender<AppCommand>,
UnboundedReceiver<AppEvent>,
tokio::task::JoinHandle<()>,
) {
let dir = tempfile::tempdir().unwrap();
let (cmd_tx, evt_rx, handle) = spawn_orch_at_tools(dir.path(), backend, config, extra_tools);
(dir, cmd_tx, evt_rx, handle)
}
fn spawn_orch_at(
root: &std::path::Path,
backend: Option<Arc<dyn EngineBackend>>,
config: AppConfig,
) -> (
UnboundedSender<AppCommand>,
UnboundedReceiver<AppEvent>,
tokio::task::JoinHandle<()>,
) {
spawn_orch_at_tools(root, backend, config, Vec::new())
}
fn spawn_orch_at_tools(
root: &std::path::Path,
backend: Option<Arc<dyn EngineBackend>>,
config: AppConfig,
extra_tools: Vec<Arc<dyn crate::features::tools::Tool>>,
) -> (
UnboundedSender<AppCommand>,
UnboundedReceiver<AppEvent>,
tokio::task::JoinHandle<()>,
) {
let storage = Arc::new(Storage::open(Paths::with_root(root)).unwrap());
let (cmd_tx, cmd_rx) = unbounded_channel();
let (evt_tx, evt_rx) = unbounded_channel();
let deps = OrchestratorDeps {
cmd_rx,
evt_tx,
storage,
config,
supervisor: Arc::new(MockSupervisor::with_backend(backend)),
default_language: crate::shared::i18n::Lang::default(),
extra_tools,
};
let handle = tokio::spawn(run(deps));
(cmd_tx, evt_rx, handle)
}
async fn wait_for<F: Fn(&AppEvent) -> bool>(
rx: &mut UnboundedReceiver<AppEvent>,
pred: F,
) -> Option<AppEvent> {
while let Some(ev) = rx.recv().await {
if pred(&ev) {
return Some(ev);
}
}
None
}
fn bare_orch() -> (tempfile::TempDir, Orchestrator) {
let (dir, orch, _rx) = bare_orch_rx();
(dir, orch)
}
fn bare_orch_rx() -> (tempfile::TempDir, Orchestrator, UnboundedReceiver<AppEvent>) {
let dir = tempfile::tempdir().unwrap();
let storage = Arc::new(Storage::open_in_memory(Paths::with_root(dir.path())).unwrap());
let (evt_tx, evt_rx) = unbounded_channel();
let (done_tx, _done_rx) = unbounded_channel();
let (status_tx, _status_rx) = unbounded_channel();
let (title_tx, _title_rx) = unbounded_channel();
let (imp_status_tx, _imp_status_rx) = unbounded_channel();
let (embed_status_tx, _embed_status_rx) = unbounded_channel();
let (imp_done_tx, _imp_done_rx) = unbounded_channel();
let config = AppConfig {
default_sampling: SamplingConfig {
temperature: Some(0.1),
..Default::default()
},
..Default::default()
};
let registry = Arc::new(build_registry(&config, storage.json().sandbox_dir(), None));
let mut engines = EngineManager::new(
Arc::new(MockSupervisor::with_backend(None)),
status_tx,
imp_status_tx,
embed_status_tx,
);
engines.server_status = ServerStatus::Ready;
engines.embedder = test_embedder();
let orch = Orchestrator {
evt_tx,
engines,
mcp: McpManager::new(unbounded_channel().0),
confirm: None,
inflight: None,
background_runs: Vec::new(),
bg_run_tx: unbounded_channel().0,
background_slots: Arc::new(super::generation::BackgroundSlots::new(
crate::shared::config::DEFAULT_SUBAGENT_BACKGROUND_MAX,
)),
pending_landings: Vec::new(),
session_budget_memo: None,
imp_cancel: None,
imp_gen: None,
imp_done_tx,
budget_tx: unbounded_channel().0,
context: Default::default(),
images_withheld_noted: Default::default(),
model_tx: unbounded_channel().0,
model: Default::default(),
slots_tx: unbounded_channel().0,
slots: Default::default(),
tts_cancel: None,
tts_gen: None,
tts_playback: None,
tts_done_tx: unbounded_channel().0,
storage,
config,
registry,
title_tx,
compact_tx: unbounded_channel().0,
profiles: Vec::new(),
chats: Vec::new(),
active_id: None,
gen_state: GenState::Idle,
done_tx,
rag_cancel: None,
attach_tx: unbounded_channel().0,
image_tx: unbounded_channel().0,
open_tx: unbounded_channel().0,
staged_images: Default::default(),
bg: std::collections::HashMap::new(),
bg_done_tx: unbounded_channel().0,
consolidate_counts: std::collections::HashMap::new(),
self_consolidate_counts: std::collections::HashMap::new(),
saves: SaveQueue::default(),
restarts: RestartQueue::default(),
default_language: crate::shared::i18n::Lang::default(),
extra_tools: Vec::new(),
};
(dir, orch, evt_rx)
}
fn bare_with_chat(
messages: Vec<Message>,
) -> (
tempfile::TempDir,
Orchestrator,
UnboundedReceiver<AppEvent>,
Uuid,
) {
let (d, mut orch, rx) = bare_orch_rx();
let profile = Profile::new("P", "sys");
let mut chat = Chat::from_profile(&profile, "Новый чат");
for m in messages {
chat.push_message(m);
}
let chat_id = chat.id;
orch.profiles.push(profile);
orch.chats.push(chat);
(d, orch, rx, chat_id)
}
fn carrier_with_run(title: &str, texts: &[&str]) -> (Message, Uuid) {
let run = crate::entities::subagent::SubagentRun::fixture(title, texts);
let run_id = run.id;
let mut carrier = Message::assistant("делегировал");
carrier.tool_calls = vec![run.on_record()];
(carrier, run_id)
}
async fn enable_all_tools(
cmd_tx: &UnboundedSender<AppCommand>,
evt_rx: &mut UnboundedReceiver<AppEvent>,
) -> Uuid {
use crate::features::tools::all_tool_ids;
let pl = wait_for(evt_rx, |e| matches!(e, AppEvent::ProfileList(_)))
.await
.unwrap();
let pid = match pl {
AppEvent::ProfileList(v) => v[0].id,
_ => unreachable!(),
};
cmd_tx
.send(AppCommand::UpdateProfile {
id: pid,
edit: Box::new(ProfileEdit {
enabled_tools: Some(all_tool_ids()),
..Default::default()
}),
})
.unwrap();
pid
}
fn live_backend() -> Option<Arc<dyn EngineBackend>> {
let client = crate::shared::api::live_client("MINDFORK_ENGINE_URL", "MINDFORK_ENGINE_KEY")?;
Some(crate::shared::api::retry::RetryBackend::wrap(Arc::new(
client,
)))
}
fn live_embedder() -> Option<Arc<dyn Embedder>> {
let client = crate::shared::api::live_client("MINDFORK_EMBED_URL", "MINDFORK_EMBED_KEY")?;
Some(Arc::new(client) as Arc<dyn Embedder>)
}
type OrchHandle = (
tempfile::TempDir,
UnboundedSender<AppCommand>,
UnboundedReceiver<AppEvent>,
tokio::task::JoinHandle<()>,
);
fn spawn_orch_live() -> Option<OrchHandle> {
spawn_orch_live_cfg(AppConfig::default())
}
fn spawn_orch_live_cfg(config: AppConfig) -> Option<OrchHandle> {
spawn_orch_live_with(config, true)
}
fn spawn_orch_live_no_embed(config: AppConfig) -> Option<OrchHandle> {
spawn_orch_live_with(config, false)
}
fn spawn_orch_live_with(config: AppConfig, with_embedder: bool) -> Option<OrchHandle> {
spawn_orch_live_paths(config, with_embedder, None)
}
fn spawn_orch_live_sandbox(config: AppConfig, sandbox: std::path::PathBuf) -> Option<OrchHandle> {
spawn_orch_live_paths(config, true, Some(sandbox))
}
fn spawn_orch_live_paths(
config: AppConfig,
with_embedder: bool,
sandbox: Option<std::path::PathBuf>,
) -> Option<OrchHandle> {
let backend = live_backend()?;
let supervisor: Arc<dyn crate::app::supervisor::ServerSupervisor> = if with_embedder {
Arc::new(MockSupervisor::with_backend_and_embedder(
Some(backend),
live_embedder(),
))
} else {
Arc::new(MockSupervisor::with_backend_no_embedder(Some(backend)))
};
let dir = tempfile::tempdir().unwrap();
let mut paths = Paths::with_root(dir.path());
if let Some(sandbox) = sandbox {
paths = paths.with_sandbox_dir(sandbox);
}
let storage = Arc::new(Storage::open(paths).unwrap());
let (cmd_tx, cmd_rx) = unbounded_channel();
let (evt_tx, evt_rx) = unbounded_channel();
let deps = OrchestratorDeps {
cmd_rx,
evt_tx,
storage,
config,
supervisor,
default_language: crate::shared::i18n::Lang::default(),
extra_tools: Vec::new(),
};
let handle = tokio::spawn(run(deps));
Some((dir, cmd_tx, evt_rx, handle))
}
async fn drain_until_finished<F: Fn(&AppEvent) -> bool>(
rx: &mut UnboundedReceiver<AppEvent>,
pred: F,
) -> bool {
let mut saw = false;
while let Some(ev) = rx.recv().await {
if pred(&ev) {
saw = true;
}
if matches!(ev, AppEvent::Finished { .. }) {
break;
}
}
saw
}
#[cfg(test)]
async fn run_turn_live(
cmd_tx: &UnboundedSender<AppCommand>,
evt_rx: &mut UnboundedReceiver<AppEvent>,
text: &str,
) -> (String, Vec<String>) {
cmd_tx.send(AppCommand::SendMessage(text.into())).unwrap();
let mut out = String::new();
let mut tools = Vec::new();
while let Some(ev) = evt_rx.recv().await {
match &ev {
AppEvent::Chunk { text, .. } => out.push_str(text),
AppEvent::ToolCall { name, .. } => tools.push(name.clone()),
AppEvent::Finished { .. } => break,
_ => {}
}
}
(out, tools)
}
async fn run_turn_capture(
cmd_tx: &UnboundedSender<AppCommand>,
evt_rx: &mut UnboundedReceiver<AppEvent>,
text: &str,
) -> (String, Vec<(String, String)>) {
let (out, calls) = run_turn_capture_args(cmd_tx, evt_rx, text).await;
(out, calls.into_iter().map(|(n, _, r)| (n, r)).collect())
}
async fn run_turn_capture_args(
cmd_tx: &UnboundedSender<AppCommand>,
evt_rx: &mut UnboundedReceiver<AppEvent>,
text: &str,
) -> (String, Vec<(String, String, String)>) {
cmd_tx.send(AppCommand::SendMessage(text.into())).unwrap();
let mut out = String::new();
let mut calls = Vec::new();
while let Some(ev) = evt_rx.recv().await {
match &ev {
AppEvent::Chunk { text, .. } => out.push_str(text),
AppEvent::ToolCall {
name,
arguments,
result,
..
} => calls.push((name.clone(), arguments.clone(), result.clone())),
AppEvent::Finished { .. } => break,
_ => {}
}
}
(out, calls)
}
fn orch_with_active_profile() -> (tempfile::TempDir, Orchestrator, Uuid) {
let (dir, mut orch) = bare_orch();
let profile = Profile::new("P", "sys");
let pid = profile.id;
let chat = Chat::from_profile(&profile, "t");
let chat_id = chat.id;
orch.profiles.push(profile);
orch.chats.push(chat);
orch.active_id = Some(chat_id);
(dir, orch, pid)
}
fn orch_ready_for_reflection() -> (tempfile::TempDir, Orchestrator, Uuid) {
use crate::features::tools::self_model::GET_SELF_MODEL_ID;
let (dir, mut orch) = bare_orch();
orch.config.self_model.auto_reflect_every = 1;
let mut profile = Profile::new("P", "sys");
profile.enabled_tools = vec![GET_SELF_MODEL_ID.into()];
let mut chat = Chat::from_profile(&profile, "t");
chat.push_message(Message::user("привет"));
chat.push_message(Message::assistant("здравствуй"));
let chat_id = chat.id;
orch.profiles.push(profile);
orch.chats.push(chat);
orch.active_id = Some(chat_id);
(dir, orch, chat_id)
}
fn orch_ready_for_self_consolidation() -> (tempfile::TempDir, Orchestrator, Uuid) {
use crate::entities::note::Note;
use crate::features::tools::notes::SELF_NOTE_TAG;
use crate::features::tools::self_model::GET_SELF_MODEL_ID;
let (dir, mut orch) = bare_orch();
orch.config.self_model.auto_consolidate_every = 1;
let mut profile = Profile::new("P", "sys");
profile.enabled_tools = vec![
GET_SELF_MODEL_ID.into(),
"note_merge".into(),
"update_self_model".into(),
];
let pid = profile.id;
let mut chat = Chat::from_profile(&profile, "t");
chat.push_message(Message::user("привет"));
chat.push_message(Message::assistant("здравствуй"));
let chat_id = chat.id;
for text in ["я ценю краткость", "пользователь любит лаконичность"]
{
let note = Note::new(pid, text, vec![SELF_NOTE_TAG.to_string()]);
orch.storage.db().note_insert(¬e).unwrap();
}
orch.profiles.push(profile);
orch.chats.push(chat);
orch.active_id = Some(chat_id);
(dir, orch, chat_id)
}
mod attachments;
mod background;
mod background_dialogue;
mod chats;
mod compaction;
mod concurrent;
mod confirm;
mod demo;
mod dialogue;
mod files;
mod generation;
mod images;
mod impersonation;
mod live;
mod llm_history;
mod mcp;
mod model_name;
mod parallel;
mod profiles;
mod project;
mod rag;
mod reflection;
mod request;
mod search;
mod self_consolidation;
mod self_model;
mod settings;
mod silent;
mod subagent;
mod tasks;
mod title;
mod tts;
struct SampledTool {
id: &'static str,
sample: Option<crate::shared::api::contract::Prefill>,
}
#[async_trait::async_trait]
impl crate::features::tools::Tool for SampledTool {
fn id(&self) -> crate::entities::profile::ToolId {
self.id.into()
}
fn description(&self, _loc: &crate::shared::i18n::Locale) -> String {
"sampled".into()
}
fn parameters(&self, _loc: &crate::shared::i18n::Locale) -> serde_json::Value {
serde_json::json!({"type": "object", "properties": {}})
}
async fn invoke(
&self,
_ctx: &crate::features::tools::ToolContext,
_args: serde_json::Value,
) -> anyhow::Result<crate::features::tools::ToolOutcome> {
Ok(crate::features::tools::ToolOutcome::text("sampled").with_prefill(self.sample))
}
fn group(&self) -> crate::features::tools::meta::ToolGroup {
crate::features::tools::meta::ToolGroup::Files
}
fn ui_label(&self) -> &'static str {
"sampled"
}
}