feat(memory): système de mémoire projet model-agnostic (L14, LOT A+B+C)
Base de connaissance persistante par projet, indépendante de tout modèle/CLU
et de git. Cadrage archi en §14.5 (ARCHITECTURE.md), cycle Archi→Dev→Test.
LOT A — étage 1 (.md, source de vérité)
- domaine: entité Memory (+ MemorySlug, MemoryType, MemoryFrontmatter,
MemoryLink, MemoryIndexEntry), liens [[slug]], index MEMORY.md dérivé
- port MemoryStore + MemoryError, adapter FsMemoryStore (.ideai/memory/)
- application: 7 use cases (Create/Update/List/Get/Delete/ReadIndex/
ResolveLinks), From<MemoryError> for AppError
- app-tauri: commandes + DTO, events MemorySaved/MemoryDeleted
- suppression de la variante morte DomainError::MalformedFrontmatter
LOT B — rappel adaptatif (étage 1)
- port MemoryRecall + MemoryQuery, adapter NaiveMemoryRecall (troncature
au budget de tokens, court-circuit budget-0), use case RecallMemory
LOT C — étage 2 vectoriel (structure complète, zéro dépendance lourde)
- port Embedder + EmbedderError, profils déclaratifs EmbedderProfile/
EmbedderStrategy (embedder.json)
- VectorMemoryRecall (cosinus, cache .ideai/memory/.index/ gitignoré)
- AdaptiveMemoryRecall (bascule pure should_use_vector), défaut none
- HashEmbedder (déterministe, tests), StubEmbedder (onnx/server/api)
Tests: 57 binaires verts, build + clippy --workspace sans warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -17,6 +17,8 @@ use application::{
|
||||
UpdateAgentContextInput,
|
||||
AssignSkillToAgentInput, CreateSkillInput, DeleteSkillInput, ListSkillsInput,
|
||||
UnassignSkillFromAgentInput, UpdateSkillInput,
|
||||
CreateMemoryInput, DeleteMemoryInput, GetMemoryInput, ListMemoriesInput, ReadMemoryIndexInput,
|
||||
RecallMemoryInput, ResolveMemoryLinksInput, UpdateMemoryInput,
|
||||
};
|
||||
use domain::ports::PtyHandle;
|
||||
|
||||
@ -37,6 +39,8 @@ use crate::dto::{
|
||||
TemplateListDto, TerminalClosedDto, TerminalSessionDto, UpdateAgentContextRequestDto,
|
||||
UpdateTemplateRequestDto, WriteTerminalRequestDto, parse_skill_id, AssignSkillRequestDto,
|
||||
CreateSkillRequestDto, SkillDto, SkillListDto, UnassignSkillRequestDto, UpdateSkillRequestDto,
|
||||
parse_memory_slug, CreateMemoryRequestDto, MemoryDto, MemoryIndexDto, MemoryLinksDto,
|
||||
MemoryListDto, RecallMemoryRequestDto, UpdateMemoryRequestDto,
|
||||
};
|
||||
use domain::{SkillRef, SkillScope};
|
||||
use crate::pty::{PtyBridge, PtyChunk};
|
||||
@ -1392,3 +1396,197 @@ pub async fn unassign_skill_from_agent(
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Memory (LOT A — §14.5.1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `create_memory` — create a memory note in the project's store.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/empty fields or
|
||||
/// malformed project id, `NOT_FOUND` if the project is unknown, `STORE` on
|
||||
/// failure).
|
||||
#[tauri::command]
|
||||
pub async fn create_memory(
|
||||
request: CreateMemoryRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MemoryDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
state
|
||||
.create_memory
|
||||
.execute(CreateMemoryInput {
|
||||
project_root: project.root,
|
||||
name: request.name,
|
||||
description: request.description,
|
||||
r#type: request.r#type,
|
||||
content: request.content,
|
||||
})
|
||||
.await
|
||||
.map(MemoryDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `update_memory` — replace an existing memory note (re-validates invariants).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/empty fields,
|
||||
/// `NOT_FOUND` if the project is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn update_memory(
|
||||
request: UpdateMemoryRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MemoryDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let slug = parse_memory_slug(&request.slug)?;
|
||||
state
|
||||
.update_memory
|
||||
.execute(UpdateMemoryInput {
|
||||
project_root: project.root,
|
||||
slug,
|
||||
description: request.description,
|
||||
r#type: request.r#type,
|
||||
content: request.content,
|
||||
})
|
||||
.await
|
||||
.map(MemoryDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `list_memories` — list the project's memory notes.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
|
||||
/// project is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn list_memories(
|
||||
project_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MemoryListDto, ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
state
|
||||
.list_memories
|
||||
.execute(ListMemoriesInput {
|
||||
project_root: project.root,
|
||||
})
|
||||
.await
|
||||
.map(MemoryListDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `get_memory` — read one memory note by slug.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/id, `NOT_FOUND` if the
|
||||
/// project or note is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn get_memory(
|
||||
project_id: String,
|
||||
slug: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MemoryDto, ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
let slug = parse_memory_slug(&slug)?;
|
||||
state
|
||||
.get_memory
|
||||
.execute(GetMemoryInput {
|
||||
project_root: project.root,
|
||||
slug,
|
||||
})
|
||||
.await
|
||||
.map(MemoryDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `delete_memory` — remove a memory note (and its index row).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/id, `NOT_FOUND` if the
|
||||
/// project or note is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn delete_memory(
|
||||
project_id: String,
|
||||
slug: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
let slug = parse_memory_slug(&slug)?;
|
||||
state
|
||||
.delete_memory
|
||||
.execute(DeleteMemoryInput {
|
||||
project_root: project.root,
|
||||
slug,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `read_memory_index` — read the structured `MEMORY.md` index.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
|
||||
/// project is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn read_memory_index(
|
||||
project_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MemoryIndexDto, ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
state
|
||||
.read_memory_index
|
||||
.execute(ReadMemoryIndexInput {
|
||||
project_root: project.root,
|
||||
})
|
||||
.await
|
||||
.map(MemoryIndexDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `recall_memory` — recall the most relevant memory entries for a query within
|
||||
/// a token budget (LOT B — §14.5.2).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
|
||||
/// project is unknown, `STORE` on failure). An empty or absent memory and a zero
|
||||
/// budget both yield an empty list, not an error.
|
||||
#[tauri::command]
|
||||
pub async fn recall_memory(
|
||||
request: RecallMemoryRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MemoryIndexDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
state
|
||||
.recall_memory
|
||||
.execute(RecallMemoryInput {
|
||||
project_root: project.root,
|
||||
text: request.text,
|
||||
token_budget: request.token_budget,
|
||||
})
|
||||
.await
|
||||
.map(MemoryIndexDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `resolve_memory_links` — resolve a note's outgoing `[[slug]]` links.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/id, `NOT_FOUND` if the
|
||||
/// project or source note is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn resolve_memory_links(
|
||||
project_id: String,
|
||||
slug: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MemoryLinksDto, ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
let slug = parse_memory_slug(&slug)?;
|
||||
state
|
||||
.resolve_memory_links
|
||||
.execute(ResolveMemoryLinksInput {
|
||||
project_root: project.root,
|
||||
slug,
|
||||
})
|
||||
.await
|
||||
.map(MemoryLinksDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user