Files
IdeA/crates/app-tauri/src/state.rs
Blomios 32398827fb feat(memory): embedders vectoriels réels HTTP + ONNX derrière features (LOT C1)
Remplace les StubEmbedder pour les stratégies localServer/api/localOnnx par de
vrais moteurs, chacun derrière une feature cargo off-by-default — la posture
fondatrice « rien d'imposé, zéro dépendance » (défaut none → rappel naïf) reste
byte-for-byte inchangée.

C1a (feature vector-http, reqwest rustls optional):
- HttpEmbedder couvrant localServer (Ollama/llama.cpp) et api (OpenAI/Voyage…),
  payload OpenAI-compatible /v1/embeddings, ordre restauré par index, bearer
  token lu via env var (jamais en clair), timeout client 30s.
- detect_ollama() pour la détection de l'existant (C3).

C1b (feature vector-onnx, fastembed v5 optional):
- OnnxEmbedder en-process (e5-small, dim 384), init paresseuse + spawn_blocking,
  cache modèle sous <app_data>/embedders/onnx — aucun download au build ni au
  first-run, uniquement à la demande au 1er embed.
- Catalogue RECOMMENDED_ONNX_MODELS + ONNX_CACHE_SUBDIR + onnx_model_is_cached
  exposés (sans feature) pour la config (C2) et la popup (C3).

embedder_from_profile(profile, onnx_cache_dir) dispatche feature-gated ; sans la
feature, retombe sur StubEmbedder (Unsupported) → fallback naïf via
AdaptiveMemoryRecall. Composition root (build_memory_recall) propage le cache dir.

Tests: 10 HTTP + 6 ONNX (dont 2 #[ignore] download réel) + 26 vectoriels, verts
en défaut, --features vector-http et --features vector-onnx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:00:40 +02:00

865 lines
37 KiB
Rust

//! Managed application state — the product of the **composition root**.
//!
//! The composition root ([`build_app_state`]) is the *single* place that
//! constructs concrete adapters (`new ConcreteAdapter`) and injects them as
//! `Arc<dyn Port>` into the use cases (ARCHITECTURE §1.1, §10). The use cases
//! are then exposed through `tauri::State<AppState>` to the command handlers.
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use application::{
CloseProject, CloseTab, CloseTerminal, ConfigureProfiles, CreateAgentFromScratch,
CreateAgentFromTemplate, CreateLayout, CreateProject, CreateTemplate, DeleteAgent,
DeleteLayout, DeleteProfile, DeleteTemplate, DetectAgentDrift, DetectProfiles, FirstRunState,
GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
HealthUseCase, InspectConversation, LaunchAgent, ListAgents, ListLayouts, ListProfiles, ListProjects, ListSkills,
ListTemplates, LiveAgentRegistry, LoadLayout, MoveTabToNewWindow, MutateLayout, OpenProject,
OpenTerminal, OrchestratorService, ReadAgentContext, ReferenceProfiles, RenameLayout,
ResizeTerminal, SaveProfile, SetActiveLayout, SnapshotRunningAgents,
AssignSkillToAgent, CreateSkill, DeleteSkill, UnassignSkillFromAgent, UpdateSkill,
CreateMemory, DeleteMemory, GetMemory, ListMemories, ReadMemoryIndex, RecallMemory,
ResolveMemoryLinks, UpdateMemory,
SyncAgentWithTemplate, TerminalSessions, UpdateAgentContext, UpdateTemplate, WriteToTerminal,
};
use domain::ports::{
AgentContextStore, AgentRuntime, Clock, Embedder, EventBus, FileSystem, GitPort, IdGenerator,
MemoryRecall, MemoryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, SkillStore,
TemplateStore,
};
use domain::{DomainEvent, EmbedderProfile, Project, ProjectId};
use infrastructure::{
embedder_from_profile, AdaptiveMemoryRecall, ClaudeTranscriptInspector, CliAgentRuntime,
FsEmbedderProfileStore, FsMemoryStore, FsOrchestratorWatcher, FsProfileStore, FsProjectStore,
FsSkillStore, FsTemplateStore, Git2Repository, IdeaiContextStore, LocalFileSystem,
LocalProcessSpawner, NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter,
SystemClock, TokioBroadcastEventBus, UuidGenerator, VectorMemoryRecall, ONNX_CACHE_SUBDIR,
};
use crate::pty::PtyBridge;
/// Everything the IPC layer needs at runtime, managed by Tauri.
///
/// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete
/// adapters are owned here and never leak past the composition root as concrete
/// types — downstream code only sees the `Arc<dyn Port>` held inside the use
/// cases.
pub struct AppState {
/// Trivial health use case validating the end-to-end wiring.
pub health: Arc<HealthUseCase>,
/// Create a project (init `.ideai/`, register it).
pub create_project: Arc<CreateProject>,
/// Open a project (load meta + manifest).
pub open_project: Arc<OpenProject>,
/// Close a project (persist state).
pub close_project: Arc<CloseProject>,
/// Close a tab.
pub close_tab: Arc<CloseTab>,
/// List known projects.
pub list_projects: Arc<ListProjects>,
/// Open a terminal (spawn PTY, register session).
pub open_terminal: Arc<OpenTerminal>,
/// Write keystrokes to a terminal.
pub write_terminal: Arc<WriteToTerminal>,
/// Resize a terminal.
pub resize_terminal: Arc<ResizeTerminal>,
/// Close a terminal (kill PTY).
pub close_terminal: Arc<CloseTerminal>,
/// Load a project's persisted layout tree.
pub load_layout: Arc<LoadLayout>,
/// Mutate + persist a project's layout tree.
pub mutate_layout: Arc<MutateLayout>,
/// List all named layouts for a project (#4).
pub list_layouts: Arc<ListLayouts>,
/// Create a new named layout (#4).
pub create_layout: Arc<CreateLayout>,
/// Rename a named layout (#4).
pub rename_layout: Arc<RenameLayout>,
/// Delete a named layout (#4).
pub delete_layout: Arc<DeleteLayout>,
/// Set the active named layout (#4).
pub set_active_layout: Arc<SetActiveLayout>,
/// Freeze `agent_was_running` on every agent leaf before a PTY kill (T5).
pub snapshot_running_agents: Arc<SnapshotRunningAgents>,
/// Detect which candidate profiles' CLIs are installed (first-run).
pub detect_profiles: Arc<DetectProfiles>,
/// List configured profiles.
pub list_profiles: Arc<ListProfiles>,
/// Save (upsert) a profile.
pub save_profile: Arc<SaveProfile>,
/// Delete a profile.
pub delete_profile: Arc<DeleteProfile>,
/// Persist the batch of chosen profiles (closes the first run).
pub configure_profiles: Arc<ConfigureProfiles>,
/// Expose the pre-filled reference catalogue.
pub reference_profiles: Arc<ReferenceProfiles>,
/// Whether the first-run wizard should show + the reference catalogue.
pub first_run_state: Arc<FirstRunState>,
/// The local PTY adapter, kept port-typed so the presentation layer can
/// `subscribe_output` to pump bytes into the [`PtyBridge`] (it owns transport).
pub pty_port: Arc<dyn PtyPort>,
/// Active-terminal registry shared by the terminal use cases.
pub terminal_sessions: Arc<TerminalSessions>,
/// The domain event bus (also handed to the event relay).
pub event_bus: Arc<TokioBroadcastEventBus>,
/// Generic PTY↔Channel bridge registry (consumed by L3).
pub pty_bridge: Arc<PtyBridge>,
// --- Agents (L6) ---
/// Create a project agent from scratch.
pub create_agent: Arc<CreateAgentFromScratch>,
/// List a project's agents.
pub list_agents: Arc<ListAgents>,
/// Read an agent's Markdown context.
pub read_agent_context: Arc<ReadAgentContext>,
/// Overwrite an agent's Markdown context.
pub update_agent_context: Arc<UpdateAgentContext>,
/// Delete an agent from the manifest.
pub delete_agent: Arc<DeleteAgent>,
/// Launch an agent (spawn PTY, apply injection strategy).
pub launch_agent: Arc<LaunchAgent>,
/// Best-effort inspection of a conversation (last topic + token indicator)
/// for the resume popup (T7). Optional/extensible: backed by a `Vec` of
/// [`domain::ports::SessionInspector`]s; an empty/missing match yields empty
/// details, never an error.
pub inspect_conversation: Arc<InspectConversation>,
/// Project registry — used by agent commands to resolve a `Project` from an id.
pub project_store: Arc<dyn ProjectStore>,
// --- Windows (L10) ---
/// Detach a tab into a new OS window (persists the workspace topology).
pub move_tab: Arc<MoveTabToNewWindow>,
// --- Templates & sync (L7) ---
/// Create a template in the global store.
pub create_template: Arc<CreateTemplate>,
/// Update a template's content (bumps version).
pub update_template: Arc<UpdateTemplate>,
/// List all templates in the global store.
pub list_templates: Arc<ListTemplates>,
/// Delete a template from the global store.
pub delete_template: Arc<DeleteTemplate>,
/// Create an agent from a template.
pub create_agent_from_template: Arc<CreateAgentFromTemplate>,
/// Detect which synchronized agents are behind their template.
pub detect_agent_drift: Arc<DetectAgentDrift>,
/// Apply a template update to a synchronized agent.
pub sync_agent_with_template: Arc<SyncAgentWithTemplate>,
// --- Git (L8) ---
/// Report the working-tree status of a repository.
pub git_status: Arc<GitStatus>,
/// Stage a path.
pub git_stage: Arc<GitStage>,
/// Unstage a path.
pub git_unstage: Arc<GitUnstage>,
/// Create a commit.
pub git_commit: Arc<GitCommit>,
/// List branches.
pub git_branches: Arc<GitBranches>,
/// Check out a branch.
pub git_checkout: Arc<GitCheckout>,
/// Return the recent commit log.
pub git_log: Arc<GitLog>,
/// Initialise a repository.
pub git_init: Arc<GitInit>,
/// Return the commit graph for all local branches.
pub git_graph: Arc<GitGraph>,
// --- Skills (L12) ---
/// Create a skill in a scope's store.
pub create_skill: Arc<CreateSkill>,
/// Update a skill's content.
pub update_skill: Arc<UpdateSkill>,
/// List skills in a scope.
pub list_skills: Arc<ListSkills>,
/// Delete a skill from its scope's store.
pub delete_skill: Arc<DeleteSkill>,
/// Assign a skill to an agent (records a `SkillRef`).
pub assign_skill: Arc<AssignSkillToAgent>,
/// Unassign a skill from an agent.
pub unassign_skill: Arc<UnassignSkillFromAgent>,
// --- Memory (LOT A — §14.5.1) ---
/// Create a memory note in the project's store.
pub create_memory: Arc<CreateMemory>,
/// Replace an existing memory note.
pub update_memory: Arc<UpdateMemory>,
/// List the project's memory notes.
pub list_memories: Arc<ListMemories>,
/// Read one memory note by slug.
pub get_memory: Arc<GetMemory>,
/// Delete a memory note.
pub delete_memory: Arc<DeleteMemory>,
/// Read the structured `MEMORY.md` index.
pub read_memory_index: Arc<ReadMemoryIndex>,
/// Resolve a note's outgoing `[[slug]]` links.
pub resolve_memory_links: Arc<ResolveMemoryLinks>,
/// Recall the most relevant memory entries for a query within a budget
/// (LOT B — §14.5.2).
pub recall_memory: Arc<RecallMemory>,
// --- Orchestrator (§14.3) ---
/// Dispatches validated orchestrator requests to the agent/skill use cases.
/// Shared by every per-project filesystem watcher.
pub orchestrator_service: Arc<OrchestratorService>,
/// Live orchestrator request watchers, keyed by project. One watcher per open
/// project tails its `.ideai/requests/` tree; dropping the handle stops it.
/// Guarded by a `Mutex` so the open/close commands can register/unregister
/// watchers concurrently.
pub orchestrator_watchers: Mutex<HashMap<ProjectId, OrchestratorWatchHandle>>,
}
impl AppState {
/// **Composition root.** Builds all adapters and use cases.
///
/// `app_data_dir` is the machine-local IDE data directory (ARCHITECTURE
/// §9.2), resolved by the caller via the Tauri path API and injected here so
/// the stores never touch Tauri themselves (Dependency Inversion).
///
/// This is the only function that constructs concrete adapters; every other
/// layer depends on ports. Adapters added in later lots (PTY, git, remote)
/// are wired in here.
#[must_use]
pub fn build(app_data_dir: PathBuf) -> Self {
// --- Concrete adapters (driven adapters) ---
let event_bus = Arc::new(TokioBroadcastEventBus::new());
let clock = Arc::new(SystemClock::new());
let ids = Arc::new(UuidGenerator::new());
let fs = Arc::new(LocalFileSystem::new());
let store = Arc::new(FsProjectStore::new(
Arc::clone(&fs) as Arc<dyn FileSystem>,
app_data_dir.to_string_lossy().into_owned(),
));
// Port-typed handles for injection.
let fs_port = Arc::clone(&fs) as Arc<dyn FileSystem>;
let store_port = Arc::clone(&store) as Arc<dyn ProjectStore>;
let events_port = Arc::clone(&event_bus) as Arc<dyn EventBus>;
// --- Use cases (ports injected as Arc<dyn Port>) ---
let health = Arc::new(HealthUseCase::new(
Arc::clone(&clock) as Arc<dyn Clock>,
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&events_port),
));
let create_project = Arc::new(CreateProject::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&clock) as Arc<dyn Clock>,
Arc::clone(&events_port),
));
let open_project = Arc::new(OpenProject::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
));
let close_project = Arc::new(CloseProject::new(Arc::clone(&store_port)));
let close_tab = Arc::new(CloseTab::new(Arc::clone(&store_port)));
let list_projects = Arc::new(ListProjects::new(Arc::clone(&store_port)));
// --- PTY adapter + terminal use cases (L3) ---
let pty = Arc::new(PortablePtyAdapter::new());
let pty_port = Arc::clone(&pty) as Arc<dyn PtyPort>;
let terminal_sessions = Arc::new(TerminalSessions::new());
let open_terminal = Arc::new(OpenTerminal::new(
Arc::clone(&pty_port),
Arc::clone(&terminal_sessions),
Arc::clone(&events_port),
));
let write_terminal = Arc::new(WriteToTerminal::new(
Arc::clone(&pty_port),
Arc::clone(&terminal_sessions),
));
let resize_terminal = Arc::new(ResizeTerminal::new(
Arc::clone(&pty_port),
Arc::clone(&terminal_sessions),
));
let close_terminal = Arc::new(CloseTerminal::new(
Arc::clone(&pty_port),
Arc::clone(&terminal_sessions),
));
// --- Layout use cases (L4 + #4) ---
let load_layout = Arc::new(LoadLayout::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
));
let mutate_layout = Arc::new(MutateLayout::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
Arc::clone(&events_port),
));
let list_layouts = Arc::new(ListLayouts::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
));
let create_layout = Arc::new(CreateLayout::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&events_port),
));
let rename_layout = Arc::new(RenameLayout::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
Arc::clone(&events_port),
));
let delete_layout = Arc::new(DeleteLayout::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
Arc::clone(&events_port),
));
let set_active_layout = Arc::new(SetActiveLayout::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
Arc::clone(&events_port),
));
// Close-time snapshot of running agents (T5). Shares the SAME live-session
// registry as the terminal/agent use cases, so its liveness check reflects
// the very PTYs the shutdown hook is about to kill — it must run *before*.
let snapshot_running_agents = Arc::new(SnapshotRunningAgents::new(
Arc::clone(&store_port),
Arc::clone(&fs_port),
Arc::clone(&terminal_sessions) as Arc<dyn LiveAgentRegistry>,
));
// --- Profiles & AI runtime (L5) ---
// One generic, profile-driven runtime adapter (Open/Closed): it holds the
// process spawner used for detection. The profile store persists
// `profiles.json` in the same machine-local app-data dir as the project
// registry.
let spawner = Arc::new(LocalProcessSpawner::new());
let spawner_port = Arc::clone(&spawner) as Arc<dyn ProcessSpawner>;
let runtime = Arc::new(CliAgentRuntime::new(Arc::clone(&spawner_port)));
let runtime_port = Arc::clone(&runtime) as Arc<dyn AgentRuntime>;
let profile_store = Arc::new(FsProfileStore::new(
Arc::clone(&fs_port),
app_data_dir.to_string_lossy().into_owned(),
));
let profile_store_port = Arc::clone(&profile_store) as Arc<dyn ProfileStore>;
let detect_profiles = Arc::new(DetectProfiles::new(Arc::clone(&runtime_port)));
let list_profiles = Arc::new(ListProfiles::new(Arc::clone(&profile_store_port)));
let save_profile = Arc::new(SaveProfile::new(Arc::clone(&profile_store_port)));
let delete_profile = Arc::new(DeleteProfile::new(Arc::clone(&profile_store_port)));
let configure_profiles = Arc::new(ConfigureProfiles::new(Arc::clone(&profile_store_port)));
let reference_profiles = Arc::new(ReferenceProfiles::new());
let first_run_state = Arc::new(FirstRunState::new(Arc::clone(&profile_store_port)));
let pty_bridge = Arc::new(PtyBridge::new());
// --- Agent context store + use cases (L6) ---
let contexts = Arc::new(IdeaiContextStore::new(Arc::clone(&fs_port)));
let contexts_port = Arc::clone(&contexts) as Arc<dyn AgentContextStore>;
// --- Skill store (L12) ---
// Global skills live in the machine-local app-data dir; project skills are
// resolved per call from each project's `.ideai/` (so one store serves all
// open projects). Shared by the skill use cases and the agent launcher
// (assigned-skill injection into the convention file, §14.2).
let skill_store = Arc::new(FsSkillStore::new(
Arc::clone(&fs_port),
app_data_dir.to_string_lossy().into_owned(),
));
let skill_store_port = Arc::clone(&skill_store) as Arc<dyn SkillStore>;
// Memory store + naïve recall (LOT A/B — §14.5.1/§14.5.4). Built here so the
// recall port can be injected into LaunchAgent below (it composes the project
// memory recall into the convention file at activation); the memory use cases
// are wired further down from these same instances.
let memory_store = Arc::new(FsMemoryStore::new(Arc::clone(&fs_port)));
let memory_store_port = Arc::clone(&memory_store) as Arc<dyn MemoryStore>;
// Load the configured embedder profile (mono-profile for now: take the last
// listed, fall back to `none`). A multi-profile selector is a follow-up; the
// `none` default keeps recall strictly naïve and dependency-free.
let embedder_store = FsEmbedderProfileStore::new(
Arc::clone(&fs_port),
app_data_dir.to_string_lossy().into_owned(),
);
// `build` may run inside an ambient async runtime (Tauri's `setup`, or
// `#[tokio::test]`), so blocking the current thread on a future panics.
// Drive the one-shot load on a dedicated thread with its own runtime.
let embedder_profile = std::thread::scope(|s| {
s.spawn(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.ok()
.and_then(|rt| rt.block_on(embedder_store.list()).ok())
.and_then(|mut v| v.pop())
})
.join()
.ok()
.flatten()
})
.unwrap_or_else(EmbedderProfile::none);
let onnx_cache_dir = app_data_dir.join(ONNX_CACHE_SUBDIR);
let memory_recall_port = build_memory_recall(
Arc::clone(&fs_port),
Arc::clone(&memory_store_port),
&embedder_profile,
&onnx_cache_dir,
);
let create_agent = Arc::new(CreateAgentFromScratch::new(
Arc::clone(&contexts_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&events_port),
));
let list_agents = Arc::new(ListAgents::new(Arc::clone(&contexts_port)));
let read_agent_context = Arc::new(ReadAgentContext::new(Arc::clone(&contexts_port)));
let update_agent_context = Arc::new(UpdateAgentContext::new(Arc::clone(&contexts_port)));
let delete_agent = Arc::new(DeleteAgent::new(
Arc::clone(&contexts_port),
Arc::clone(&events_port),
));
// LaunchAgent shares the SAME pty_port and terminal_sessions as the terminal
// use cases — indispensable for the PtyBridge to work correctly.
let launch_agent = Arc::new(LaunchAgent::new(
Arc::clone(&contexts_port),
Arc::clone(&profile_store_port),
Arc::clone(&runtime_port),
Arc::clone(&fs_port),
Arc::clone(&pty_port),
Arc::clone(&skill_store_port),
Arc::clone(&terminal_sessions),
Arc::clone(&events_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&memory_recall_port),
));
// --- Conversation inspection (T7) ---
// Best-effort, optional, extensible: a `Vec` of SessionInspectors routed
// by profile. Adding an inspectable CLI = pushing one more adapter here.
// The Claude inspector reads `<home>/.claude/projects/<cwd>/<id>.jsonl`;
// `$HOME` is resolved from the environment (empty string if unset — the
// inspector then simply finds nothing and yields empty details).
let home_dir = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_default();
let inspectors: Vec<Arc<dyn domain::ports::SessionInspector>> =
vec![Arc::new(ClaudeTranscriptInspector::new(
Arc::clone(&fs_port),
home_dir,
))];
let inspect_conversation = Arc::new(InspectConversation::new(
Arc::clone(&contexts_port),
Arc::clone(&profile_store_port),
inspectors,
));
let project_store = Arc::clone(&store_port);
// --- Template store + use cases (L7) ---
let template_store = Arc::new(FsTemplateStore::new(
Arc::clone(&fs_port),
app_data_dir.to_string_lossy().into_owned(),
));
let template_store_port = Arc::clone(&template_store) as Arc<dyn TemplateStore>;
let create_template = Arc::new(CreateTemplate::new(
Arc::clone(&template_store_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
));
let update_template = Arc::new(UpdateTemplate::new(
Arc::clone(&template_store_port),
Arc::clone(&events_port),
));
let list_templates = Arc::new(ListTemplates::new(Arc::clone(&template_store_port)));
let delete_template = Arc::new(DeleteTemplate::new(Arc::clone(&template_store_port)));
let create_agent_from_template = Arc::new(CreateAgentFromTemplate::new(
Arc::clone(&template_store_port),
Arc::clone(&contexts_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&events_port),
));
let detect_agent_drift = Arc::new(DetectAgentDrift::new(
Arc::clone(&template_store_port),
Arc::clone(&contexts_port),
Arc::clone(&events_port),
));
let sync_agent_with_template = Arc::new(SyncAgentWithTemplate::new(
Arc::clone(&template_store_port),
Arc::clone(&contexts_port),
Arc::clone(&events_port),
));
// --- Git adapter + use cases (L8) ---
let git = Arc::new(Git2Repository::new());
let git_port = Arc::clone(&git) as Arc<dyn GitPort>;
let git_status = Arc::new(GitStatus::new(Arc::clone(&git_port)));
let git_stage = Arc::new(GitStage::new(Arc::clone(&git_port)));
let git_unstage = Arc::new(GitUnstage::new(Arc::clone(&git_port)));
let git_commit = Arc::new(GitCommit::new(Arc::clone(&git_port), Arc::clone(&events_port)));
let git_branches = Arc::new(GitBranches::new(Arc::clone(&git_port)));
let git_checkout = Arc::new(GitCheckout::new(
Arc::clone(&git_port),
Arc::clone(&events_port),
));
let git_log = Arc::new(GitLog::new(Arc::clone(&git_port)));
let git_init = Arc::new(GitInit::new(Arc::clone(&git_port), Arc::clone(&events_port)));
let git_graph = Arc::new(GitGraph::new(Arc::clone(&git_port)));
// --- Skill use cases (L12) ---
// Reuse the skill store (built above for the launcher) and the shared
// agent context store for the agent↔skill assignment.
let create_skill = Arc::new(CreateSkill::new(
Arc::clone(&skill_store_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
));
let update_skill = Arc::new(UpdateSkill::new(Arc::clone(&skill_store_port)));
let list_skills = Arc::new(ListSkills::new(Arc::clone(&skill_store_port)));
let delete_skill = Arc::new(DeleteSkill::new(Arc::clone(&skill_store_port)));
let assign_skill = Arc::new(AssignSkillToAgent::new(
Arc::clone(&contexts_port),
Arc::clone(&events_port),
));
let unassign_skill = Arc::new(UnassignSkillFromAgent::new(
Arc::clone(&contexts_port),
Arc::clone(&events_port),
));
// --- Memory use cases (LOT A — §14.5.1) ---
// `memory_store` / `memory_store_port` / `memory_recall_port` are built
// earlier (above LaunchAgent, which needs the recall port). A single
// FsMemoryStore takes the project root per call (like the skill store), so
// one instance serves every open project. The mutating use cases share the
// event bus to announce Memory{Saved,Deleted}.
let create_memory = Arc::new(CreateMemory::new(
Arc::clone(&memory_store_port),
Arc::clone(&events_port),
));
let update_memory = Arc::new(UpdateMemory::new(
Arc::clone(&memory_store_port),
Arc::clone(&events_port),
));
let list_memories = Arc::new(ListMemories::new(Arc::clone(&memory_store_port)));
let get_memory = Arc::new(GetMemory::new(Arc::clone(&memory_store_port)));
let delete_memory = Arc::new(DeleteMemory::new(
Arc::clone(&memory_store_port),
Arc::clone(&events_port),
));
let read_memory_index = Arc::new(ReadMemoryIndex::new(Arc::clone(&memory_store_port)));
let resolve_memory_links =
Arc::new(ResolveMemoryLinks::new(Arc::clone(&memory_store_port)));
// Naïve recall (LOT B): the same instance injected into LaunchAgent above —
// composes the store, returns index entries in order truncated to the token
// budget. The default, dependency-free MemoryRecall; substitutable by a
// VectorMemoryRecall (LOT C).
let recall_memory = Arc::new(RecallMemory::new(Arc::clone(&memory_recall_port)));
// --- Orchestrator service (§14.3) ---
// Dispatches file-based orchestrator requests through the *same* use cases
// the UI drives (IdeA stays the single source of truth for the agent/skill
// lifecycle). The per-project watcher that feeds it is started lazily when
// a project is opened (see `ensure_orchestrator_watch`).
let orchestrator_service = Arc::new(OrchestratorService::new(
Arc::clone(&create_agent),
Arc::clone(&launch_agent),
Arc::clone(&list_agents),
Arc::clone(&close_terminal),
Arc::clone(&update_agent_context),
Arc::clone(&create_skill),
Arc::clone(&profile_store_port),
Arc::clone(&terminal_sessions),
));
// --- Windows (L10) ---
let move_tab = Arc::new(MoveTabToNewWindow::new(
Arc::clone(&store_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
));
Self {
health,
create_project,
open_project,
close_project,
close_tab,
list_projects,
open_terminal,
write_terminal,
resize_terminal,
close_terminal,
load_layout,
mutate_layout,
list_layouts,
create_layout,
rename_layout,
delete_layout,
set_active_layout,
snapshot_running_agents,
detect_profiles,
list_profiles,
save_profile,
delete_profile,
configure_profiles,
reference_profiles,
first_run_state,
pty_port,
terminal_sessions,
event_bus,
pty_bridge,
create_agent,
list_agents,
read_agent_context,
update_agent_context,
delete_agent,
launch_agent,
inspect_conversation,
project_store,
create_template,
update_template,
list_templates,
delete_template,
create_agent_from_template,
detect_agent_drift,
sync_agent_with_template,
git_status,
git_stage,
git_unstage,
git_commit,
git_branches,
git_checkout,
git_log,
git_init,
git_graph,
create_skill,
update_skill,
list_skills,
delete_skill,
assign_skill,
unassign_skill,
create_memory,
update_memory,
list_memories,
get_memory,
delete_memory,
read_memory_index,
resolve_memory_links,
recall_memory,
orchestrator_service,
orchestrator_watchers: Mutex::new(HashMap::new()),
move_tab,
}
}
/// Starts a [`FsOrchestratorWatcher`] for `project` if one is not already
/// running for it (idempotent). The watcher tails the project's
/// `.ideai/requests/` tree and dispatches each request through the shared
/// [`OrchestratorService`]; processed-request events are republished on the
/// domain event bus so the relay surfaces them to the frontend.
///
/// Called from the `open_project` / `create_project` commands. Must run inside
/// the Tokio runtime (the watcher spawns a background task) — Tauri async
/// commands satisfy this.
pub fn ensure_orchestrator_watch(&self, project: &Project) {
let mut watchers = self
.orchestrator_watchers
.lock()
.expect("orchestrator watcher registry poisoned");
if watchers.contains_key(&project.id) {
return;
}
let bus = Arc::clone(&self.event_bus);
let events: Arc<dyn Fn(DomainEvent) + Send + Sync> =
Arc::new(move |event| bus.publish(event));
let handle = FsOrchestratorWatcher::start(
project.clone(),
Arc::clone(&self.orchestrator_service),
events,
);
watchers.insert(project.id, handle);
}
/// Returns the ids of every currently-open project.
///
/// Derived from the orchestrator watcher registry, which holds exactly one
/// entry per open project (started on open/create, dropped on close). Used by
/// the shutdown hook to snapshot running agents across all open projects
/// before the global PTY kill.
#[must_use]
pub fn open_project_ids(&self) -> Vec<ProjectId> {
self.orchestrator_watchers
.lock()
.map(|w| w.keys().copied().collect())
.unwrap_or_default()
}
/// Stops and removes the orchestrator watcher for `project_id`, if any.
/// Called from `close_project` so a closed project stops consuming requests.
pub fn stop_orchestrator_watch(&self, project_id: &ProjectId) {
if let Some(handle) = self
.orchestrator_watchers
.lock()
.expect("orchestrator watcher registry poisoned")
.remove(project_id)
{
handle.stop();
}
}
}
/// Build the project memory recall port from an embedder profile.
///
/// This is the only place that knows the concrete recall adapters (DIP): callers
/// only ever see `Arc<dyn MemoryRecall>`. With the default `none` profile,
/// `embedder_from_profile` returns `None` and we hand back the plain
/// `NaiveMemoryRecall` — strictly identical, dependency-free behaviour. When an
/// embedder is configured, we wrap naïve + vector recall in an
/// `AdaptiveMemoryRecall` that switches stages live per the profile strategy.
pub(crate) fn build_memory_recall(
fs: Arc<dyn FileSystem>,
store: Arc<dyn MemoryStore>,
profile: &EmbedderProfile,
onnx_cache_dir: &std::path::Path,
) -> Arc<dyn MemoryRecall> {
let naive = Arc::new(NaiveMemoryRecall::new(Arc::clone(&store))) as Arc<dyn MemoryRecall>;
match embedder_from_profile(profile, onnx_cache_dir) {
None => naive,
Some(embedder) => {
let embedder: Arc<dyn Embedder> = Arc::from(embedder);
let vector: Arc<dyn MemoryRecall> = Arc::new(VectorMemoryRecall::new(
embedder,
Arc::clone(&store),
Arc::clone(&fs),
));
Arc::new(AdaptiveMemoryRecall::new(
naive,
vector,
Arc::clone(&store),
profile.strategy,
))
}
}
}
#[cfg(test)]
mod tests {
//! Wiring contract for [`build_memory_recall`] (Pièce 1, §14.5.5).
//!
//! The *behaviour* of `NaiveMemoryRecall` / `VectorMemoryRecall` /
//! `AdaptiveMemoryRecall` is covered exhaustively in
//! `infrastructure/tests/vector_recall.rs`. Here we only pin the **composition
//! root contract**: with the default `EmbedderProfile::none()` profile (the
//! dependency-free default), `build_memory_recall` must hand back a recall whose
//! observable behaviour is *identical* to a bare `NaiveMemoryRecall` over the
//! same store — same index ordering, same budget truncation.
//!
//! `build_memory_recall` is `pub(crate)`, so this lives in `state.rs` (it is not
//! reachable from the `tests/` integration crate). Everything is in-memory and
//! deterministic.
use std::collections::HashMap;
use std::sync::Mutex;
use super::*;
use async_trait::async_trait;
use domain::markdown::MarkdownDoc;
use domain::memory::{Memory, MemoryFrontmatter, MemorySlug, MemoryType};
use domain::ports::{DirEntry, FsError, MemoryQuery, RemotePath};
use domain::project::ProjectPath;
// In-memory FileSystem (same minimal shape as the infrastructure test fixtures).
#[derive(Default)]
struct MemFs {
files: Mutex<HashMap<String, Vec<u8>>>,
}
#[async_trait]
impl FileSystem for MemFs {
async fn read(&self, path: &RemotePath) -> Result<Vec<u8>, FsError> {
self.files
.lock()
.unwrap()
.get(path.as_str())
.cloned()
.ok_or_else(|| FsError::NotFound(path.as_str().to_string()))
}
async fn write(&self, path: &RemotePath, data: &[u8]) -> Result<(), FsError> {
self.files
.lock()
.unwrap()
.insert(path.as_str().to_string(), data.to_vec());
Ok(())
}
async fn exists(&self, path: &RemotePath) -> Result<bool, FsError> {
Ok(self.files.lock().unwrap().contains_key(path.as_str()))
}
async fn create_dir_all(&self, _path: &RemotePath) -> Result<(), FsError> {
Ok(())
}
async fn list(&self, _path: &RemotePath) -> Result<Vec<DirEntry>, FsError> {
Ok(Vec::new())
}
async fn symlink(&self, _src: &RemotePath, _dst: &RemotePath) -> Result<(), FsError> {
Ok(())
}
}
fn root() -> ProjectPath {
ProjectPath::new("/proj").unwrap()
}
fn note(slug: &str, hook: &str) -> Memory {
Memory::new(
MemoryFrontmatter {
name: MemorySlug::new(slug).unwrap(),
description: hook.to_string(),
r#type: MemoryType::Project,
},
MarkdownDoc::new("# body"),
)
.unwrap()
}
fn query(text: &str, budget: usize) -> MemoryQuery {
MemoryQuery {
text: text.to_string(),
token_budget: budget,
}
}
async fn seed_store() -> (Arc<dyn FileSystem>, Arc<dyn MemoryStore>) {
let fs: Arc<dyn FileSystem> = Arc::new(MemFs::default());
let store_concrete = Arc::new(FsMemoryStore::new(Arc::clone(&fs)));
for n in [
note("alpha", "apple orange grape"),
note("beta", "kiwi mango papaya"),
note("gamma", "carrot potato onion"),
] {
store_concrete.save(&root(), &n).await.unwrap();
}
let store: Arc<dyn MemoryStore> = store_concrete;
(fs, store)
}
/// With the default `none` profile, `build_memory_recall` is observationally a
/// plain `NaiveMemoryRecall`: same index ordering and same budget truncation,
/// entry-for-entry, across a representative spread of budgets.
#[tokio::test]
async fn build_memory_recall_none_profile_matches_naive_recall() {
let (fs, store) = seed_store().await;
let wired = build_memory_recall(
Arc::clone(&fs),
Arc::clone(&store),
&EmbedderProfile::none(),
std::path::Path::new("/unused-onnx-cache"),
);
let naive = NaiveMemoryRecall::new(Arc::clone(&store));
// 0 ⇒ empty; mid budgets ⇒ partial truncation; huge ⇒ full index order.
for budget in [0usize, 3, 6, 100, 100_000] {
let q = query("kiwi mango papaya", budget);
let expected = naive.recall(&root(), &q).await.unwrap();
let got = wired.recall(&root(), &q).await.unwrap();
assert_eq!(
got, expected,
"none profile must equal bare NaiveMemoryRecall at budget {budget}"
);
}
}
}