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>
865 lines
37 KiB
Rust
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}"
|
|
);
|
|
}
|
|
}
|
|
}
|