Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/项目文档/03-规划/01-现状评估.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 下一层安全输出仍不能扩大权限边界

Expand Down
3 changes: 2 additions & 1 deletion docs/项目文档/03-规划/02-后续路线图.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:优化角色上下文差异度

Expand Down
2 changes: 2 additions & 0 deletions docs/项目文档/03-规划/03-协作任务清单.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 当前不再适用的旧任务分法
Expand Down
6 changes: 5 additions & 1 deletion docs/项目文档/03-规划/04-未来两周开发排期.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 会影响召回结果
Expand All @@ -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:调试台信息结构优化

Expand Down
161 changes: 140 additions & 21 deletions src-tauri/src/noname_context_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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();
Expand Down
Loading