diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" index ad4754b..36e5c6d 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" @@ -56,7 +56,7 @@ ### 3.5 记忆检索与 notes 串联仍未完成 -检索排序解释性、notes 生命周期和 notes/retrieval 串联第一轮已经具备,但 structured notes 与 compaction、context builder 的贯通仍不完整。后续需要继续验证 note-aware retrieval 是否应接入更多上下文构建路径,避免新能力只停留在 manager 层报告。 +检索排序解释性、notes 生命周期和 notes/retrieval 串联第一轮已经具备;note-aware retrieval 已接入 context builder,使 active notes 扩展出的召回结果能够进入角色上下文包。当前剩余缺口主要在 structured notes 与 compaction、章节收束及更高层 runtime 编排的贯通,后续需要继续验证 notes 如何参与长期记忆沉淀,而不是只参与单次上下文构建。 ### 3.6 下一层安全输出仍不能扩大权限边界 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" index 125ead3..47b69c8 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" @@ -91,7 +91,8 @@ - `NoNameMemoryManager` 已保证 note 状态变化后同步 retrieval store - 章节收束时 resolved 自动归档的既有行为已保留 - 与 retrieval 的第一轮串联已完成:active notes 可作为检索上下文桥,archived notes 不参与扩展召回 -- 与 compaction、context builder 的更深层串联尚未完成,后续继续推进 +- 与 context builder 的串联已完成第一轮:角色上下文构建会消费 note-aware retrieval,并在 `source_stats` 中暴露 `noteContext` +- 与 compaction、章节收束和更高层 runtime 编排的更深层串联尚未完成,后续继续推进 ### 任务 6.5:优化角色上下文差异度 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/03-\345\215\217\344\275\234\344\273\273\345\212\241\346\270\205\345\215\225.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/03-\345\215\217\344\275\234\344\273\273\345\212\241\346\270\205\345\215\225.md" index 9cdce92..ee3785e 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/03-\345\215\217\344\275\234\344\273\273\345\212\241\346\270\205\345\215\225.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/03-\345\215\217\344\275\234\344\273\273\345\212\241\346\270\205\345\215\225.md" @@ -119,6 +119,8 @@ - 第一轮已完成 - manager 层已提供 note-aware retrieval 报告,包含召回结果、note contexts 与 explanations - 已有测试证明 active notes 会扩大相关 episodic 召回,archived notes 不会扩大召回 +- 第二轮已完成:context builder 已消费 note-aware retrieval,让 active notes 扩展出的召回结果进入角色上下文包 +- `source_stats` 已加入 `noteContext` 统计,便于后续调试上下文来源 - 当前没有修改前端、protocol 或 runtime 主链 ## 5. 当前不再适用的旧任务分法 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" index 7468dbd..ab998b2 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" @@ -276,6 +276,7 @@ - `src-tauri/src/noname_memory_retrieval.rs` - `src-tauri/src/noname_note_store.rs` - `src-tauri/src/noname_memory_manager.rs` + - `src-tauri/src/noname_context_builder.rs` - 交接目标: - notes 与 retrieval 之间形成清晰接点 - 至少有一组测试证明 notes 会影响召回结果 @@ -292,10 +293,13 @@ - active note 会提取 `related_entities / title / summary` 作为补充查询桥,帮助召回相关 episodic / semantic / working memory - archived notes 不参与扩展召回,避免历史 notes 被误激活 - note-aware retrieval 会返回 `note_contexts` 与 explanations,便于调试 notes 如何影响召回 +- 第二轮已完成:`NoNameContextBuilder` 已改用 note-aware retrieval,让 active notes 扩展出的召回结果进入角色上下文包 +- `source_stats` 已新增 `noteContext` 统计,用于观察本次上下文是否被 active notes 扩展 - 未修改前端、protocol 或 runtime 主链 -- 已补充测试证明 active notes 会影响召回,archived notes 不会影响召回 +- 已补充测试证明 active notes 会影响 manager 层召回与 context builder 上下文结果,archived notes 不会影响召回 - 已验证: - `cargo test noname_memory_manager -- --nocapture` + - `cargo test noname_context_builder -- --nocapture` ### 任务 B5:调试台信息结构优化 diff --git a/src-tauri/src/noname_context_builder.rs b/src-tauri/src/noname_context_builder.rs index 4edf240..c541c02 100644 --- a/src-tauri/src/noname_context_builder.rs +++ b/src-tauri/src/noname_context_builder.rs @@ -14,7 +14,7 @@ pub fn build_context_packet( memory_manager: &NoNameMemoryManager, input: &NoNameContextBuildInput, ) -> NoNameContextPacket { - let retrieved = memory_manager.retrieve(&NoNameMemoryQuery { + let query = NoNameMemoryQuery { role: input.role, search_term: input.player_intent.clone(), actor: None, @@ -23,7 +23,10 @@ pub fn build_context_packet( keyword: input.player_intent.clone(), token_budget: input.token_budget, per_section_limit: input.per_section_limit, - }); + }; + let retrieval_report = memory_manager.retrieve_with_active_note_context(&query); + let note_context_count = retrieval_report.note_contexts.len(); + let retrieved = retrieval_report.memories; let mut hard_facts = retrieved .semantic @@ -125,6 +128,31 @@ pub fn build_context_packet( None }; + let mut source_stats = vec![ + NoNameContextSourceStat { + source: "semantic".to_string(), + count: retrieved.semantic.len(), + }, + NoNameContextSourceStat { + source: "working".to_string(), + count: retrieved.working.len(), + }, + NoNameContextSourceStat { + source: "episodic".to_string(), + count: retrieved.episodic.len(), + }, + NoNameContextSourceStat { + source: "narrative".to_string(), + count: retrieved.narrative.len(), + }, + ]; + if note_context_count > 0 { + source_stats.push(NoNameContextSourceStat { + source: "noteContext".to_string(), + count: note_context_count, + }); + } + NoNameContextPacket { role: input.role, hard_facts, @@ -136,24 +164,7 @@ pub fn build_context_packet( referenced_entities, compressed_summary, token_budget_used: used, - source_stats: vec![ - NoNameContextSourceStat { - source: "semantic".to_string(), - count: retrieved.semantic.len(), - }, - NoNameContextSourceStat { - source: "working".to_string(), - count: retrieved.working.len(), - }, - NoNameContextSourceStat { - source: "episodic".to_string(), - count: retrieved.episodic.len(), - }, - NoNameContextSourceStat { - source: "narrative".to_string(), - count: retrieved.narrative.len(), - }, - ], + source_stats, } } @@ -660,7 +671,10 @@ mod tests { use super::*; use crate::memory_layers::{ChapterSummary, MemoryEntry, MemoryLayers, WorldFact}; use crate::noname_memory_manager::NoNameMemoryManager; - use crate::noname_memory_types::NoNameWorkingMemoryItem; + use crate::noname_memory_types::{ + NoNameEpisodicMemoryItem, NoNameMemoryImportance, NoNameNarrativeMemoryItem, + NoNameNarrativeNoteType, NoNameNarrativeStatus, NoNameWorkingMemoryItem, + }; use crate::noname_types::NoNameRole; #[test] @@ -726,6 +740,111 @@ mod tests { assert!(!packet.source_stats.is_empty()); } + #[test] + fn context_builder_uses_active_notes_to_expand_retrieved_memory() { + let store = EntityStore::new(); + let mut manager = NoNameMemoryManager::new(); + manager.push_episodic_memory(NoNameEpisodicMemoryItem { + memory_id: "event-qinghe".to_string(), + event_type: "dialogue".to_string(), + timestamp: 4, + chapter_index: 1, + location_id: None, + actors: vec!["Elder Qinghe".to_string()], + summary: "Elder Qinghe hesitated before naming the saboteur.".to_string(), + detail_ref: None, + importance: NoNameMemoryImportance::High, + }); + manager.upsert_narrative_memory(NoNameNarrativeMemoryItem { + note_id: "note-ward".to_string(), + chapter_index: 1, + arc_id: None, + note_type: NoNameNarrativeNoteType::UnresolvedThread, + title: "Broken Ward".to_string(), + summary: "The saboteur clue points back to Elder Qinghe.".to_string(), + status: NoNameNarrativeStatus::Active, + related_entities: vec!["Elder Qinghe".to_string()], + updated_at: 5, + }); + + let packet = build_context_packet( + &store, + &manager, + &NoNameContextBuildInput { + role: NoNameRole::Director, + world_id: "w1".to_string(), + run_id: "r1".to_string(), + scene_id: "s1".to_string(), + character_ids: vec![], + map_node_id: None, + player_intent: Some("ward".to_string()), + recent_context_lines: vec![], + token_budget: 200, + per_section_limit: 4, + }, + ); + + assert!(packet + .episodic_memory + .iter() + .any(|item| item.contains("Elder Qinghe hesitated"))); + assert!(packet + .source_stats + .iter() + .any(|item| item.source == "noteContext" && item.count == 1)); + } + + #[test] + fn context_builder_ignores_archived_notes_when_expanding_retrieval() { + let store = EntityStore::new(); + let mut manager = NoNameMemoryManager::new(); + manager.push_episodic_memory(NoNameEpisodicMemoryItem { + memory_id: "event-qinghe".to_string(), + event_type: "dialogue".to_string(), + timestamp: 4, + chapter_index: 1, + location_id: None, + actors: vec!["Elder Qinghe".to_string()], + summary: "Elder Qinghe mentioned a saboteur.".to_string(), + detail_ref: None, + importance: NoNameMemoryImportance::High, + }); + manager.upsert_narrative_memory(NoNameNarrativeMemoryItem { + note_id: "note-ward".to_string(), + chapter_index: 1, + arc_id: None, + note_type: NoNameNarrativeNoteType::UnresolvedThread, + title: "Broken Ward".to_string(), + summary: "The saboteur clue points back to Elder Qinghe.".to_string(), + status: NoNameNarrativeStatus::Archived, + related_entities: vec!["Elder Qinghe".to_string()], + updated_at: 5, + }); + + let packet = build_context_packet( + &store, + &manager, + &NoNameContextBuildInput { + role: NoNameRole::Director, + world_id: "w1".to_string(), + run_id: "r1".to_string(), + scene_id: "s1".to_string(), + character_ids: vec![], + map_node_id: None, + player_intent: Some("ward".to_string()), + recent_context_lines: vec![], + token_budget: 200, + per_section_limit: 4, + }, + ); + + assert!(packet.episodic_memory.is_empty()); + assert!(!packet + .source_stats + .iter() + .any(|item| item.source == "noteContext")); + } + #[test] fn context_builder_can_emit_compressed_summary_under_tight_budget() { let store = EntityStore::new();