Repository navigation
Open
Labels
Description
背景
NyxID Chat 的 Agent Profile 已经能把 exact Ornn skill、system instructions 与 UserMemory 指针固化进 Conversation(docs/canon/nyxid-chat-agent-profile-binding.md、docs/canon/conversation-context-and-memory.md)。但对于结构化、长文档、需要 immutable revision 的领域档案(一份已确认的档案、一份周期性生成的报告),现有机制存在能力空档:
- UserMemory 语义是「摘要性用户事实」,50 条上限,不适合承载结构化长文档;
- ContentArtifact 有 immutable revision / verified content read / redact / tombstone,是唯一合适的载体;
- 但 Conversation 的 turn prompt context 目前只能注入 system instructions + UserMemory,无法把某几份 ContentArtifact 以 typed、版本化的方式声明为会话固定上下文并随每轮注入。
当前替代方案(system skill 指令 + UserMemory 指针 + 每轮靠 LLM 自觉调用读取工具)没有版本保证,也无法表达「这份档案是该会话的固定上下文」。
Review 结论(2026-08-25,基于代码现状修订)
- 注入席位已存在:
SystemPromptLayerComposer.Compose(..., ConversationContextPromptLayer? conversation)(src/Aevatar.AI.Core/Prompting/SystemPromptLayerComposer.cs:18)已有 conversation 层,两个调用点目前都传null(agents/Aevatar.GAgents.NyxidChat/NyxIdChatGAgent.cs:145、ConversationReplyGenerator.cs:2402)。本 feature 不新增 prompt 拼接通道,只填充这个既有 typed layer。 - 固化先例已存在:
agent_profile的整条链路(NyxIdChatConversationCreateCommand.agent_profile = 3→ create resolverNyxIdChatConversationCreateCommandTargetResolver(NyxIdChatLifecycleFacade.cs:175)只读 read model 解析 →AgentProfileBoundEvent固化 + 幂等/拒绝语义)即为附件固化的模板;失败码族AdmissionUnavailable亦复用。 - 预算与降级先例已存在:selected skill body 的 65536-byte ceiling(
AgentProfileValidationLimits.RequiredMaxSelectedSkillBytes)超限走「整条丢弃 + typed diagnostic,不截断」(AgentTurnToolCatalogMaterializer.cs:1258),附件沿用同款语义。 - verified read 已存在:
IContentArtifactQueryPort.GetRevisionContentAsync已做 ACL 复核 + SHA-256 校验 + redacted/expired/tombstoned fail closed(ProjectionContentArtifactQueryPort.cs),turn 注入直接消费,不新增读取通道。 - 对原提案的修正:
injectionMode: full|summary|pinned-excerpt收窄为 revision 选择(FOLLOW_CURRENT/PINNED_REVISION)+ 全文注入。summary移出 v1:摘要是派生视图,应由独立管道维护为另一个 artifact 再被附加;注入期做摘要会把不可版本化的派生计算塞进 prompt assembly。pinned-excerpt与 [ContentArtifact] 按 metadata key 的 list/filter 查询与同 scope+kind 唯一置顶语义 #3527 的 pin 语义撞名,且摘录需要锚定 exact revision 的结构化 locator,价值不足以进 v1。expectedSchemaVersion无对应现实(artifact 没有 schema version):改为创建期kind白名单校验。
设计方案
契约(proto,src/Aevatar.AI.Abstractions/ai_messages.proto)
enum ConversationContextAttachmentRevisionMode {
CONVERSATION_CONTEXT_ATTACHMENT_REVISION_MODE_UNSPECIFIED = 0;
CONVERSATION_CONTEXT_ATTACHMENT_REVISION_MODE_FOLLOW_CURRENT = 1; // 每轮读 current revision
CONVERSATION_CONTEXT_ATTACHMENT_REVISION_MODE_PINNED_REVISION = 2; // 固定 exact revision
}
message ConversationContextAttachment {
string artifact_id = 1;
ConversationContextAttachmentRevisionMode revision_mode = 2;
string pinned_revision_id = 3; // 仅 PINNED_REVISION 非空
}
message ConversationContextAttachmentSet {
repeated ConversationContextAttachment attachments = 1; // 上限 4,按声明顺序注入
}落位(与 agent_profile 同位的 sealed 载体各补一个字段):
NyxIdChatConversationCreateCommand.context_attachments = 7;RoleGAgentState(新字段 14)、NyxIdChatConversationGAgentState、Channel runtimeconversation_state.proto对应补位;- 固化事件
ConversationContextAttachmentsBoundEvent:未绑定 + create 携带有效集合 → 提交;deterministic bytes 相同 → 幂等;不同/缺失 → 拒绝不改写;无附件 create 保持字段缺失。只影响之后创建的 Conversation,存量不 hot-upgrade、不 replay/backfill。
创建准入(create resolver,只读 read model)
NyxIdChatConversationCreateCommandTargetResolver 在 profile 解析后逐条校验附件(经 IContentArtifactQueryPort,不激活 actor、不 prime projection):
- artifact 存在且
lifecycle_status = ACTIVE; - caller principal 是该 artifact 的 owner 或 reader;
kind ∈ {TEXT, MARKDOWN, STRUCTURED_DOCUMENT}(OTHER_CONTENT拒绝);PINNED_REVISION时该 revision 存在且availability = AVAILABLE;- 条数 ≤ 4,
artifact_id去重。
任一不满足 → 在创建 Actor 之前 fail closed,复用 NyxIdChatLifecycleCommandStartError.AdmissionUnavailable 码族;不得降级为无附件创建。
Turn 注入
- 每轮 turn 构造 prompt 时(execution 路径,非 query API),按声明顺序读取:
GetAsync(元数据 +current_revision_id)→GetRevisionContentAsync(verified read,requester = conversation scope principal); - 组装进
ConversationContextPromptLayer,provenance 标注 attachment 来源;每段内容带 typed 头:artifact_id、实际注入的revision_id、content_hash前缀、media_type——revision 变化、redact、tombstone 一律以当轮当前事实为准; - 附件只是 ContentArtifact 的读取投影:conversation 不因注入获得任何写权,写入仍走 artifact 自身命令链。
预算与降级(fail closed 为 typed degraded)
- 单附件正文 ceiling:65536 UTF-8 bytes(治理口径对齐
RequiredMaxSelectedSkillBytes,固定常量);层总预算PromptLayerBounds(131072, 32768); - 超限沿 selected-skill 先例:该附件整条不注入(不截断),改注入 typed unavailable 占位(
artifact_id+ 原因枚举),并记入层 diagnostics(PromptLayerCompositionReport); - 同样降级路径:ACL 已撤销、redacted、retention-expired、tombstoned、backing 不可用、readmodel 暂不可用——turn 以降级 prompt 继续(canon:prompt context 缺失可降级,不构成新权威事实);占位与 diagnostic 必须成对出现,禁止静默跳过,也禁止注入部分字节。
边界
- v1 仅全文注入;summary / excerpt 不做(派生视图 = 独立 artifact,自行附加);
- create-only:v1 不提供会话中途增删附件的命令;不对存量 Conversation 生效;
- 与 [ContentArtifact] 按 metadata key 的 list/filter 查询与同 scope+kind 唯一置顶语义 #3527 相互独立:附件按 exact
artifact_id引用,不提供「跟随 scope pin」联动(客户端创建会话前自行解析 pin 再传artifact_id); - 不引入新的可写权威、全局 memory/context framework 或第二注入通道。
验收标准
- 固化链路测试:绑定 / 幂等 / 冲突拒绝 / 无附件三态,对齐
AgentProfileBoundEvent测试形态; - 创建准入拒绝矩阵:不存在 / 无权 / kind 非法 / 超上限 / pinned revision 不可用 →
ADMISSION_UNAVAILABLE,actor 未创建; - turn 注入端到端:附件正文出现在 system prompt 的 conversation 层并带 identity 头;append 新 revision 后
FOLLOW_CURRENT下一轮反映新内容、PINNED_REVISION保持不变; - 降级矩阵:redact / tombstone / 超限 / readmodel 不可用 → 占位 + diagnostic,turn 正常完成;
dotnet build/test aevatar.slnx --nologo+bash tools/ci/architecture_guards.sh通过;同步更新docs/canon/conversation-context-and-memory.md与docs/canon/nyxid-chat-agent-profile-binding.md。
Activity
Fix milestone 45 host startup and layering regressions