id: cluster-triage-lark-channel-turn-routing
severity: medium
requires_design: true
来源
本 issue 由 maintainer 手动开,triage codex 深度调研补 evidence。
核心问题
当前代码已经不是“Lark 自然语言完全不走 LLM”:普通文本会进入 LlmReplyRequested,unknown slash 也有 Ornn skill discovery fallback。真正需要进入 refactor loop 的问题是:Lark/Nyx relay 的 turn routing 仍由 ChannelConversationTurnRunner 的 ordered branches、NyxRelayAgentBuilderFlow 白名单 fall-through、ConversationReplyGenerator 工具注入共同隐式表达,缺少一个 typed route decision / policy contract 来统一表达“本地 slash、agent-builder、workflow resume、Ornn skill shortcut、普通 LLM fallback”的边界。
这会让后续新增自然语言、tool、approval、skill discovery 策略继续堆进 runner 中间层,容易破坏分层、命令骨架内聚和 command/query 边界。Fix boundary 必须限制在 aevatar 本仓库内,不能要求 NyxID/chrono-* 新增能力或改 scope。
Evidence
1. Turn runner 同时承担多种业务路由与最终 LLM fallback
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.cs:111
public async Task<ConversationTurnResult> RunInboundAsync(
ChatActivity activity,
ConversationTurnRuntimeContext runtimeContext,
CancellationToken ct)
{
var registration = await ResolveRegistrationAsync(activity, ct);
...
if (await TryHandleWorkflowResumeAsync(inbound, ct) is { } workflowResumeResult)
return workflowResumeResult;
if (await TryHandleSlashCommandAsync(activity, inbound, registration, runtimeContext, ct) is { } slashResult)
return slashResult;
...
if (await TryHandleAgentBuilderAsync(activity, inboundEvent, registration, runtimeContext, typingReactionTask, ct) is { } agentBuilderResult)
return agentBuilderResult;
...
return ConversationTurnResult.LlmReplyRequested(
await BuildLlmReplyRequestAsync(...));
}
违反点:routing policy 由方法内顺序和早返回表达;新增策略时只能继续扩张 runner,而不是组合 typed route decision。
2. Agent-builder route 是独立白名单,靠 false fall-through 表达“交给后续 LLM/skill”
agents/Aevatar.GAgents.Authoring.Lark/NyxRelayAgentBuilderFlow.cs:19
public static bool TryResolve(ChannelInboundEvent evt, out AgentBuilderFlowDecision? decision)
{
...
var command = tokens[0];
if (!IsKnownCommand(command))
return false;
if (!IsPrivateChat(evt.ChatType))
{
decision = AgentBuilderFlowDecision.DirectReply(BuildPrivateChatRestrictionReply(command));
return true;
}
return TryResolveKnownCommand(command, tokens, out decision);
}
违反点:false 同时可能表示“不是 slash”“unknown slash”“应交给 Ornn skill/LLM”,没有强类型决策结果区分,调用方只能通过外部顺序隐式解释。
3. Runner 内部直接执行 AgentBuilderTool 并选择 renderer,route decision 与执行耦合
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.cs:970
var relayDecisionMatched = NyxRelayAgentBuilderFlow.TryResolve(inboundEvent, out decision);
...
if (decision.RequiresToolExecution)
{
using (AgentToolContextScope.Push(...))
{
var tool = ActivatorUtilities.CreateInstance<AgentBuilderTool>(_toolServiceProvider);
var toolResult = await tool.ExecuteAsync(decision.ToolArgumentsJson!, ct);
replyContent = relayDecisionMatched
? NyxRelayAgentBuilderFlow.FormatToolResult(decision, toolResult)
: AgentBuilderCardFlow.FormatToolResult(decision, toolResult);
}
}
违反点:中间层 runner 同时解析 route、构造 tool context、执行 tool、选择渲染器;它不再只是把 typed command/event 投入 actor 主链或把 query 交给 readmodel。
4. Canon 文档确认当前事实是多分支 ordered flow,而不是“没有 LLM 路径”
docs/canon/daily-command-pipeline.md:31
`/daily` slash shortcut 不走本地 agent-builder 创建逻辑;`ChannelConversationTurnRunner` 把它改写为一次 LLM turn,要求模型先调用 `use_skill(skill="chrono-ai-daily", args="<slash args>")`。
docs/canon/daily-command-pipeline.md:165
`/daily` 与其他未知 slash(如 `/goal`)是 Ornn skill shortcut:本路由放行给 LLM reply path,不走 `agent_builder`
违反点:文档事实说明普通/unknown 路径已进入 LLM/skill fallback;所以本 issue 的 actionable 方向应是收敛 route/policy 设计,而不是重复添加一条已存在的 LLM 路径。
违反条款
- 严格分层:
Domain / Application / Infrastructure / Host;API 仅做宿主与组合,不承载业务编排。
- 读写分离:
Command -> Event,Query -> ReadModel;异步完成通过事件通知,不在会话内拼装流程。
- 依赖反转:上层依赖抽象,禁止跨层反向依赖和对具体实现的直接耦合。
- 命令骨架内聚:标准生命周期
Normalize -> Resolve Target -> Build Context -> Build Envelope -> Dispatch -> Receipt -> Observe;业务模块只负责目标解析与载荷/结果映射。
- AI 对话主链必须流式化:实时会话入口必须使用
ChatStreamAsync;ChatAsync 仅可用于明确的非交互式离线场景。
- 本仓库的功能实现禁止依赖外部仓库的新增 / 修改。NyxID、chrono-storage、chrono-ornn 等都是独立产品,aevatar 的需求不是它们的功能路线图。
新原则
Channel inbound 应先归一化为 typed turn route decision,再进入对应 command/event/LLM execution path;false fall-through 不应承载业务语义。
Agent-builder、workflow resume、slash command、Ornn skill shortcut、普通 LLM reply 应共享同一 decision contract,decision 只描述目标语义,不直接执行 tool 或拼装最终流程。
自然语言问答接入必须继续走 ChatStreamAsync 主链;不得新增 ChatAsync 或 webhook 同步 RPC 旁路。
Fix boundary
scope_paths:
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.cs
agents/Aevatar.GAgents.NyxidChat/ConversationReplyGenerator.cs
agents/Aevatar.GAgents.NyxidChat/NyxIdChatEndpoints.Relay.cs
agents/Aevatar.GAgents.NyxidChat/ServiceCollectionExtensions.cs
agents/Aevatar.GAgents.Authoring.Lark/NyxRelayAgentBuilderFlow.cs
agents/Aevatar.GAgents.Authoring.Lark/AgentBuilderCardFlow.cs
agents/Aevatar.GAgents.Authoring.Lark/AgentBuilderTool.cs
agents/Aevatar.GAgents.Channel.Runtime/Conversation/IConversationTurnRunner.cs
agents/Aevatar.GAgents.Channel.Runtime/Conversation/ConversationGAgent.cs
src/Aevatar.ChatRouting.Core/ChatRouteResolver.cs
src/Aevatar.ChatRouting.Core/*
src/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdProxyTool.cs
src/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdCodeExecuteTool.cs
src/Aevatar.AI.ToolProviders.Skills/UseSkillTool.cs
src/Aevatar.AI.ToolProviders.Ornn/*
test/Aevatar.GAgents.ChannelRuntime.Tests/ChannelConversationTurnRunnerTests.cs
test/Aevatar.GAgents.ChannelRuntime.Tests/NyxRelayAgentBuilderFlowTests.cs
test/Aevatar.ChatRouting.Core.Tests/*
docs/canon/daily-command-pipeline.md
Out of scope:
- 不要求 NyxID 增加
im:message:readonly 或任何新 endpoint/scope。
- 不要求 chrono-ornn/chrono-storage 修改 schema 或协议。
- 不把 Lark service 权限配置问题作为本 cluster 的完成条件;若现有 surface 不足,solver 应在 aevatar 内降级或不做相关能力。
Decision questions
- typed turn route decision 应放在
Aevatar.GAgents.Channel.Runtime 还是 Aevatar.GAgents.NyxidChat?如果放 Runtime,必须避免引入 AI/NyxID/Studio 反向依赖。
NyxRelayAgentBuilderFlow.TryResolve 是否应返回 Matched/NotMatched/PassToLlm/Reject/DirectReply/DispatchCommand 这样的强类型结果,替代 bool fall-through?
- unknown slash 的 Ornn skill discovery 是 route policy 的一种 action,还是 LLM prompt builder 的内部策略?
- AgentBuilderTool 执行应继续由 turn runner 调用,还是拆成 command handler / port,使 runner 只发 typed command 并返回 accepted/direct reply?
- 是否复用
src/Aevatar.ChatRouting.Core/ChatRouteResolver.cs 的 policy/readmodel 语义,还是只抽取 channel-turn-local decision contract,避免扩大 scope?
original_authors
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.cs: eanzhao, maintainer-redacted
agents/Aevatar.GAgents.Authoring.Lark/NyxRelayAgentBuilderFlow.cs: eanzhao, maintainer-redacted
agents/Aevatar.GAgents.NyxidChat/ConversationReplyGenerator.cs: eanzhao, maintainer-redacted
agents/Aevatar.GAgents.NyxidChat/NyxIdChatEndpoints.Relay.cs: eanzhao, maintainer-redacted, louis.li
cc
原始作者见上;本自动 triage 遵守 shared hard rules,不写 private maintainer name 或 @-mention。
⟦AI:AUTO-LOOP⟧
来源
本 issue 由 maintainer 手动开,triage codex 深度调研补 evidence。
核心问题
当前代码已经不是“Lark 自然语言完全不走 LLM”:普通文本会进入
LlmReplyRequested,unknown slash 也有 Ornn skill discovery fallback。真正需要进入 refactor loop 的问题是:Lark/Nyx relay 的 turn routing 仍由ChannelConversationTurnRunner的 ordered branches、NyxRelayAgentBuilderFlow白名单 fall-through、ConversationReplyGenerator工具注入共同隐式表达,缺少一个 typed route decision / policy contract 来统一表达“本地 slash、agent-builder、workflow resume、Ornn skill shortcut、普通 LLM fallback”的边界。这会让后续新增自然语言、tool、approval、skill discovery 策略继续堆进 runner 中间层,容易破坏分层、命令骨架内聚和 command/query 边界。Fix boundary 必须限制在 aevatar 本仓库内,不能要求 NyxID/chrono-* 新增能力或改 scope。
Evidence
1. Turn runner 同时承担多种业务路由与最终 LLM fallback
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.cs:111违反点:routing policy 由方法内顺序和早返回表达;新增策略时只能继续扩张 runner,而不是组合 typed route decision。
2. Agent-builder route 是独立白名单,靠
falsefall-through 表达“交给后续 LLM/skill”agents/Aevatar.GAgents.Authoring.Lark/NyxRelayAgentBuilderFlow.cs:19违反点:
false同时可能表示“不是 slash”“unknown slash”“应交给 Ornn skill/LLM”,没有强类型决策结果区分,调用方只能通过外部顺序隐式解释。3. Runner 内部直接执行 AgentBuilderTool 并选择 renderer,route decision 与执行耦合
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.cs:970违反点:中间层 runner 同时解析 route、构造 tool context、执行 tool、选择渲染器;它不再只是把 typed command/event 投入 actor 主链或把 query 交给 readmodel。
4. Canon 文档确认当前事实是多分支 ordered flow,而不是“没有 LLM 路径”
docs/canon/daily-command-pipeline.md:31docs/canon/daily-command-pipeline.md:165违反点:文档事实说明普通/unknown 路径已进入 LLM/skill fallback;所以本 issue 的 actionable 方向应是收敛 route/policy 设计,而不是重复添加一条已存在的 LLM 路径。
违反条款
新原则
Channel inbound 应先归一化为 typed turn route decision,再进入对应 command/event/LLM execution path;
falsefall-through 不应承载业务语义。Agent-builder、workflow resume、slash command、Ornn skill shortcut、普通 LLM reply 应共享同一 decision contract,decision 只描述目标语义,不直接执行 tool 或拼装最终流程。
自然语言问答接入必须继续走
ChatStreamAsync主链;不得新增ChatAsync或 webhook 同步 RPC 旁路。Fix boundary
scope_paths:
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.csagents/Aevatar.GAgents.NyxidChat/ConversationReplyGenerator.csagents/Aevatar.GAgents.NyxidChat/NyxIdChatEndpoints.Relay.csagents/Aevatar.GAgents.NyxidChat/ServiceCollectionExtensions.csagents/Aevatar.GAgents.Authoring.Lark/NyxRelayAgentBuilderFlow.csagents/Aevatar.GAgents.Authoring.Lark/AgentBuilderCardFlow.csagents/Aevatar.GAgents.Authoring.Lark/AgentBuilderTool.csagents/Aevatar.GAgents.Channel.Runtime/Conversation/IConversationTurnRunner.csagents/Aevatar.GAgents.Channel.Runtime/Conversation/ConversationGAgent.cssrc/Aevatar.ChatRouting.Core/ChatRouteResolver.cssrc/Aevatar.ChatRouting.Core/*src/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdProxyTool.cssrc/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdCodeExecuteTool.cssrc/Aevatar.AI.ToolProviders.Skills/UseSkillTool.cssrc/Aevatar.AI.ToolProviders.Ornn/*test/Aevatar.GAgents.ChannelRuntime.Tests/ChannelConversationTurnRunnerTests.cstest/Aevatar.GAgents.ChannelRuntime.Tests/NyxRelayAgentBuilderFlowTests.cstest/Aevatar.ChatRouting.Core.Tests/*docs/canon/daily-command-pipeline.mdOut of scope:
im:message:readonly或任何新 endpoint/scope。Decision questions
Aevatar.GAgents.Channel.Runtime还是Aevatar.GAgents.NyxidChat?如果放 Runtime,必须避免引入 AI/NyxID/Studio 反向依赖。NyxRelayAgentBuilderFlow.TryResolve是否应返回Matched/NotMatched/PassToLlm/Reject/DirectReply/DispatchCommand这样的强类型结果,替代 bool fall-through?src/Aevatar.ChatRouting.Core/ChatRouteResolver.cs的 policy/readmodel 语义,还是只抽取 channel-turn-local decision contract,避免扩大 scope?original_authors
agents/Aevatar.GAgents.NyxidChat/ChannelConversationTurnRunner.cs: eanzhao, maintainer-redactedagents/Aevatar.GAgents.Authoring.Lark/NyxRelayAgentBuilderFlow.cs: eanzhao, maintainer-redactedagents/Aevatar.GAgents.NyxidChat/ConversationReplyGenerator.cs: eanzhao, maintainer-redactedagents/Aevatar.GAgents.NyxidChat/NyxIdChatEndpoints.Relay.cs: eanzhao, maintainer-redacted, louis.licc
原始作者见上;本自动 triage 遵守 shared hard rules,不写 private maintainer name 或 @-mention。
⟦AI:AUTO-LOOP⟧