Skip to content

[NyxID Chat] Conversation 级类型化结构化档案附件(typed structured context attachment) #3525

Open

Description

@eanz17

背景

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,基于代码现状修订)

  1. 注入席位已存在: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。
  2. 固化先例已存在:agent_profile 的整条链路(NyxIdChatConversationCreateCommand.agent_profile = 3 → create resolver NyxIdChatConversationCreateCommandTargetResolver(NyxIdChatLifecycleFacade.cs:175)只读 read model 解析 → AgentProfileBoundEvent 固化 + 幂等/拒绝语义)即为附件固化的模板;失败码族 AdmissionUnavailable 亦复用。
  3. 预算与降级先例已存在:selected skill body 的 65536-byte ceiling(AgentProfileValidationLimits.RequiredMaxSelectedSkillBytes)超限走「整条丢弃 + typed diagnostic,不截断」(AgentTurnToolCatalogMaterializer.cs:1258),附件沿用同款语义。
  4. verified read 已存在:IContentArtifactQueryPort.GetRevisionContentAsync 已做 ACL 复核 + SHA-256 校验 + redacted/expired/tombstoned fail closed(ProjectionContentArtifactQueryPort.cs),turn 注入直接消费,不新增读取通道。
  5. 对原提案的修正:
    • 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 runtime conversation_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 或第二注入通道。

验收标准

  1. 固化链路测试:绑定 / 幂等 / 冲突拒绝 / 无附件三态,对齐 AgentProfileBoundEvent 测试形态;
  2. 创建准入拒绝矩阵:不存在 / 无权 / kind 非法 / 超上限 / pinned revision 不可用 → ADMISSION_UNAVAILABLE,actor 未创建;
  3. turn 注入端到端:附件正文出现在 system prompt 的 conversation 层并带 identity 头;append 新 revision 后 FOLLOW_CURRENT 下一轮反映新内容、PINNED_REVISION 保持不变;
  4. 降级矩阵:redact / tombstone / 超限 / readmodel 不可用 → 占位 + diagnostic,turn 正常完成;
  5. 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。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions