背景:两条 NyxID 入口路径
仓库当前有两条用户进入 aevatar 的通道:
NyxID → ChatRuntime 路径 :Lark / Telegram bot → NyxID 回调 → AgentBuilder / SkillRunner / ChatRuntime → 通过统一 IAgentToolSource 主干发现工具。当前 AddAevatarPlatform 会打开 EnableSkills 和 EnableOrnnSkills,因此 ChatRuntime 能拿到 SkillsAgentToolSource 暴露的 use_skill 和 OrnnAgentToolSource 暴露的 ornn_search_skills。
NyxID Responses 直连路径 :codex / Cursor / 自家脚本以 NyxID base_url + apikey → NyxID /llm/gateway 或 /proxy/s/aevatar → aevatar POST /v1/responses(PR Scaffold Responses API v1 prototype #625 引入)→ NyxIdLLMProvider。
第二条路径现在没有接入统一 skill 主干,所以用户在 Ornn 配置的 skills 无法被 Responses 路径使用。
这里的目标不是把每个 Ornn skill 展开成一个 function tool。仓库现有主干是:LLM 先看到 ornn_search_skills / use_skill,再由 UseSkillTool 通过 IRemoteSkillFetcher 按需拉取具体 skill body。
当前问题
/v1/responses 只消费 IResponsesToolProvider,没有把已经注册好的 IAgentToolSource skill 工具桥接进来。
当前事实(feature/lark-bot HEAD):
入口:src/Aevatar.Mainnet.Host.Api/Responses/ResponsesEndpoints.cs:33、HandleCreateResponseAsync。
HandleCreateResponseAsync 在构造 LLMRequest 前调用 ResponsesToolClassifier.Classify(...),输入是客户端声明的 function tools + IEnumerable<IResponsesToolProvider>。
唯一注册的 IResponsesToolProvider 是 ResponsesAevatarToolProvider(src/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs:112)。它只提供三类 Responses 本地工具:TodoWrite、Task/task、WebFetch/web_fetch、WebSearch/web_search。
MainnetHostBuilderExtensions 已通过 builder.AddAevatarPlatform(...) 注册 Skills / Ornn 相关的 IAgentToolSource。所以问题不是全局 DI 没有 IRemoteSkillFetcher 或 OrnnAgentToolSource,而是 Responses 路径没有从 IAgentToolSource 取 skill 工具。
caller 身份已可用于工具执行:toolContextMetadata 里有 scope_id、owner_subject、nyxid_access_token;UseSkillTool / OrnnSearchSkillsTool 执行时会从 AgentToolRequestContext 读取 NyxID token。
LLMRequestCallerCredentials 现在只服务于 LLM provider 鉴权,不负责工具发现。
LLMRequest.Tools 当前等于:客户端声明的 function tools,经 substitute 后的本地工具,加上 IResponsesToolProvider.GetAdditiveTools() 返回值。因为现在没有 skill bridge provider,所以不会出现 use_skill / ornn_search_skills。
后果:codex 等以 NyxID apikey 接 aevatar 的用户,无法在 Responses 路径使用自己在 Ornn 上配置的 skills。
设计目标
/v1/responses 复用现有 skill 主干,能把 use_skill 和 ornn_search_skills 注入到发往 LLM 的 tools。
不写第二套 Ornn fetcher/parser,不新增 NyxID 或 Ornn 端点。
ChatRuntime 路径行为不变。
/v1/messages 路径暂不改:它现在显式用 Array.Empty<IResponsesToolProvider>(),避免 Aevatar 工具 shadow Anthropic / Claude Code 客户端自带工具。是否给 Messages 注入 skills 需要单独 issue 讨论。
工具名冲突必须确定性处理,不能向 LLM 发送重复 tool name。
建议方案:新增 Responses skill bridge provider
1. IResponsesToolProvider 改成异步、显式带上下文
当前接口在 src/platform/Aevatar.GAgentService.Application/Responses/ResponsesCompletionApplicationService.cs:
public interface IResponsesToolProvider
{
IReadOnlyList < IAgentTool > GetSubstituteTools ( ) => [ ] ;
IReadOnlyList < IAgentTool > GetAdditiveTools ( ) => [ ] ;
}
建议改为:
public sealed record ResponsesToolProviderContext (
ResponsesToolProviderCallerScope CallerScope ,
IReadOnlyDictionary < string , string > ToolContextMetadata ) ;
public sealed record ResponsesToolProviderCallerScope (
string ScopeId ,
string OwnerSubject ,
string OriginKind ) ;
public interface IResponsesToolProvider
{
ValueTask < IReadOnlyList < IAgentTool > > GetSubstituteToolsAsync (
ResponsesToolProviderContext context ,
CancellationToken ct = default ) =>
ValueTask . FromResult < IReadOnlyList < IAgentTool > > ( [ ] ) ;
ValueTask < IReadOnlyList < IAgentTool > > GetAdditiveToolsAsync (
ResponsesToolProviderContext context ,
CancellationToken ct = default ) =>
ValueTask . FromResult < IReadOnlyList < IAgentTool > > ( [ ] ) ;
}
ResponsesAevatarToolProvider 忽略 context,继续返回现有本地工具即可。
这样做的目的:Responses 工具发现会涉及异步 IAgentToolSource.DiscoverToolsAsync;caller scope / tool metadata 也以强类型显式传入,避免 IHttpContextAccessor、AsyncLocal 或 ambient context。ResponsesToolProviderContext 定义在 Application 层,不能反向依赖 Host 内部的 ResponsesCallerScope;Host 入口负责把已解析 caller scope 映射进去。
2. ResponsesToolClassifier 改为异步,并修复 additive 去重
ResponsesToolClassifier.Classify(...) 改成 ClassifyAsync(...):
先收集 substitute tools。
再收集 additive tools。
处理客户端声明工具时,命中 substitute name 则使用本地 substitute;否则保留 forwarded client tool。
添加 additive tools 时按 tool name 去重。若 effective tools 中已经存在同名工具,保留已有工具并记录日志,避免向 LLM 发送重复 tool name。
这次只改调用点:
/v1/responses 调用 ClassifyAsync(..., toolProviders, context, ct)。
/v1/messages 调用点必须随 ClassifyAsync(...) 签名变更同步修改,但继续传 Array.Empty<IResponsesToolProvider>(),不得注入任何 Aevatar provider。
3. 新增 ResponsesUserSkillsToolProvider : IResponsesToolProvider
建议放在 src/Aevatar.Mainnet.Host.Api/Responses/,职责只有一个:把现有 skill 相关 IAgentToolSource 暴露成 Responses additive tools。
实施约束必须钉死:ResponsesUserSkillsToolProvider 不得枚举所有 IAgentToolSource。它必须显式构造注入 SkillsAgentToolSource 与 OrnnAgentToolSource,只桥接这两个 skill 主干:
SkillsAgentToolSource → use_skill
OrnnAgentToolSource → ornn_search_skills
这样未来如果 DI 里新增 MCPAgentToolSource、LarkAgentToolSource、ServiceInvokeAgentToolSource 或其他 IAgentToolSource,不会被“枚举所有 source”的写法误桥接进 Responses。
调整 AddSkills / AddOrnnSkills 的 DI 注册,让 concrete source 和 IAgentToolSource 枚举指向同一个 singleton,例如:
services . TryAddSingleton < SkillsAgentToolSource > ( ) ;
services . TryAddEnumerable (
ServiceDescriptor . Singleton < IAgentToolSource > ( sp => sp . GetRequiredService < SkillsAgentToolSource > ( ) ) ) ;
services . TryAddSingleton < OrnnAgentToolSource > ( ) ;
services . TryAddEnumerable (
ServiceDescriptor . Singleton < IAgentToolSource > ( sp => sp . GetRequiredService < OrnnAgentToolSource > ( ) ) ) ;
AddSkills / AddOrnnSkills 内部如已有非 Try* 方法注册这些 source 或依赖项,需一并改为 Try* / TryAddEnumerable,避免 ChatRuntime 的 IEnumerable<IAgentToolSource> 解析出重复实例或重复工具。
不要在 Mainnet 手写第二套:
services . AddSingleton < OrnnSkillClient > ( ) ;
services . AddSingleton < IRemoteSkillFetcher , OrnnRemoteSkillFetcher > ( ) ;
这些已经由 AddAevatarPlatform / AddAevatarAIFeatures / AddOrnnSkills 管理。Responses 只需要桥接,不应该再创造第二个 Ornn 配置源或第二个 fetcher 实例。
4. Mainnet DI 只注册桥接 provider
在 MainnetHostBuilderExtensions.cs 里保留现有:
builder . Services . TryAddEnumerable (
ServiceDescriptor . Singleton < IResponsesToolProvider , ResponsesAevatarToolProvider > ( ) ) ;
新增:
builder . Services . TryAddEnumerable (
ServiceDescriptor . Singleton < IResponsesToolProvider , ResponsesUserSkillsToolProvider > ( ) ) ;
OrnnOptions 继续走现有 Aevatar:Ornn:NyxIdSlug 配置,不能在 Responses 路径再 bind 一份。
5. Discovery 成本假设
当前前提:OrnnAgentToolSource.DiscoverToolsAsync 只在本地声明 ornn_search_skills,不触发 Ornn 网络调用;SkillsAgentToolSource.DiscoverToolsAsync 只扫描本地 skill 目录并返回统一 use_skill。因此本 issue 不引入 scope 级 in-memory TTL。
如果实施 PR 中发现 OrnnAgentToolSource.DiscoverToolsAsync 会访问 Ornn 网络,必须在 PR 中重新评估每个 codex/Responses 请求额外 Ornn 往返的成本和缓存策略。
6. ToolContextMetadata 后续强类型化
ToolContextMetadata 目前仍是 IReadOnlyDictionary<string, string>,并承载 scope_id / owner_subject / nyxid_access_token 等稳定语义。按仓库字段命名与 Metadata 决策树,这些属于核心语义,长期应抽进 ResponsesCallerScope / LLMRequestCallerContext / typed sub-record,而不是固化在 bag 里。
这次不一并修改 ToolContextMetadata 的形状,避免扩散到 ChatRuntime、UseSkillTool 和其他消费者;需要后续单独开 issue 做强类型化收敛。
外部 surface 校验
本功能不需要改 NyxID / chrono-ornn。现有公开能力已经够用:
NyxID 代理:/api/v1/proxy/s/{slug}/{path}。
Ornn 搜索:GET /api/v1/skill-search。
Ornn skill JSON:GET /api/v1/skills/:idOrName/json,需要 authenticated caller 和 ornn:skill:read。
OrnnSkillClient 已经通过 NyxID proxy 调这些接口。
验证计划
单测:ResponsesToolClassifier 异步分类,覆盖 substitute、additive、同名 additive 去重。
单测:ResponsesUserSkillsToolProvider 能从 concrete skill sources 返回 use_skill 和 ornn_search_skills,且不返回无关 IAgentToolSource 工具。
Host 组合测试:Mainnet 能解析两个 IResponsesToolProvider;SkillsAgentToolSource / OrnnAgentToolSource concrete 注册与 IAgentToolSource 枚举不产生重复实例或重复工具。
Endpoint / 集成测试:调用 /v1/responses,fake LLM provider 捕获到的 LLMRequest.Tools 包含 use_skill 与 ornn_search_skills。
回归测试:/v1/messages 不注入 IResponsesToolProvider,不会突然出现 Aevatar additive tools。
变更涉及测试时执行:bash tools/ci/test_stability_guards.sh。至少跑相关测试项目;如果改到 shared DI,补跑 host composition / AI tool provider 相关测试。
不在范围内
背景:两条 NyxID 入口路径
仓库当前有两条用户进入 aevatar 的通道:
IAgentToolSource主干发现工具。当前AddAevatarPlatform会打开EnableSkills和EnableOrnnSkills,因此 ChatRuntime 能拿到SkillsAgentToolSource暴露的use_skill和OrnnAgentToolSource暴露的ornn_search_skills。base_url + apikey→ NyxID/llm/gateway或/proxy/s/aevatar→ aevatarPOST /v1/responses(PR Scaffold Responses API v1 prototype #625 引入)→NyxIdLLMProvider。第二条路径现在没有接入统一 skill 主干,所以用户在 Ornn 配置的 skills 无法被 Responses 路径使用。
当前问题
/v1/responses只消费IResponsesToolProvider,没有把已经注册好的IAgentToolSourceskill 工具桥接进来。当前事实(
feature/lark-botHEAD):src/Aevatar.Mainnet.Host.Api/Responses/ResponsesEndpoints.cs:33、HandleCreateResponseAsync。HandleCreateResponseAsync在构造LLMRequest前调用ResponsesToolClassifier.Classify(...),输入是客户端声明的 function tools +IEnumerable<IResponsesToolProvider>。IResponsesToolProvider是ResponsesAevatarToolProvider(src/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs:112)。它只提供三类 Responses 本地工具:TodoWrite、Task/task、WebFetch/web_fetch、WebSearch/web_search。MainnetHostBuilderExtensions已通过builder.AddAevatarPlatform(...)注册 Skills / Ornn 相关的IAgentToolSource。所以问题不是全局 DI 没有IRemoteSkillFetcher或OrnnAgentToolSource,而是 Responses 路径没有从IAgentToolSource取 skill 工具。toolContextMetadata里有scope_id、owner_subject、nyxid_access_token;UseSkillTool/OrnnSearchSkillsTool执行时会从AgentToolRequestContext读取 NyxID token。LLMRequestCallerCredentials现在只服务于 LLM provider 鉴权,不负责工具发现。LLMRequest.Tools当前等于:客户端声明的 function tools,经 substitute 后的本地工具,加上IResponsesToolProvider.GetAdditiveTools()返回值。因为现在没有 skill bridge provider,所以不会出现use_skill/ornn_search_skills。后果:codex 等以 NyxID apikey 接 aevatar 的用户,无法在 Responses 路径使用自己在 Ornn 上配置的 skills。
设计目标
/v1/responses复用现有 skill 主干,能把use_skill和ornn_search_skills注入到发往 LLM 的tools。/v1/messages路径暂不改:它现在显式用Array.Empty<IResponsesToolProvider>(),避免 Aevatar 工具 shadow Anthropic / Claude Code 客户端自带工具。是否给 Messages 注入 skills 需要单独 issue 讨论。建议方案:新增 Responses skill bridge provider
1.
IResponsesToolProvider改成异步、显式带上下文当前接口在
src/platform/Aevatar.GAgentService.Application/Responses/ResponsesCompletionApplicationService.cs:建议改为:
ResponsesAevatarToolProvider忽略 context,继续返回现有本地工具即可。这样做的目的:Responses 工具发现会涉及异步
IAgentToolSource.DiscoverToolsAsync;caller scope / tool metadata 也以强类型显式传入,避免IHttpContextAccessor、AsyncLocal或 ambient context。ResponsesToolProviderContext定义在 Application 层,不能反向依赖 Host 内部的ResponsesCallerScope;Host 入口负责把已解析 caller scope 映射进去。2.
ResponsesToolClassifier改为异步,并修复 additive 去重ResponsesToolClassifier.Classify(...)改成ClassifyAsync(...):这次只改调用点:
/v1/responses调用ClassifyAsync(..., toolProviders, context, ct)。/v1/messages调用点必须随ClassifyAsync(...)签名变更同步修改,但继续传Array.Empty<IResponsesToolProvider>(),不得注入任何 Aevatar provider。3. 新增
ResponsesUserSkillsToolProvider : IResponsesToolProvider建议放在
src/Aevatar.Mainnet.Host.Api/Responses/,职责只有一个:把现有 skill 相关IAgentToolSource暴露成 Responses additive tools。实施约束必须钉死:
ResponsesUserSkillsToolProvider不得枚举所有IAgentToolSource。它必须显式构造注入SkillsAgentToolSource与OrnnAgentToolSource,只桥接这两个 skill 主干:SkillsAgentToolSource→use_skillOrnnAgentToolSource→ornn_search_skills这样未来如果 DI 里新增
MCPAgentToolSource、LarkAgentToolSource、ServiceInvokeAgentToolSource或其他IAgentToolSource,不会被“枚举所有 source”的写法误桥接进 Responses。调整
AddSkills/AddOrnnSkills的 DI 注册,让 concrete source 和IAgentToolSource枚举指向同一个 singleton,例如:AddSkills/AddOrnnSkills内部如已有非Try*方法注册这些 source 或依赖项,需一并改为Try*/TryAddEnumerable,避免 ChatRuntime 的IEnumerable<IAgentToolSource>解析出重复实例或重复工具。不要在 Mainnet 手写第二套:
这些已经由
AddAevatarPlatform/AddAevatarAIFeatures/AddOrnnSkills管理。Responses 只需要桥接,不应该再创造第二个 Ornn 配置源或第二个 fetcher 实例。4. Mainnet DI 只注册桥接 provider
在
MainnetHostBuilderExtensions.cs里保留现有:新增:
OrnnOptions继续走现有Aevatar:Ornn:NyxIdSlug配置,不能在 Responses 路径再 bind 一份。5. Discovery 成本假设
当前前提:
OrnnAgentToolSource.DiscoverToolsAsync只在本地声明ornn_search_skills,不触发 Ornn 网络调用;SkillsAgentToolSource.DiscoverToolsAsync只扫描本地 skill 目录并返回统一use_skill。因此本 issue 不引入 scope 级 in-memory TTL。如果实施 PR 中发现
OrnnAgentToolSource.DiscoverToolsAsync会访问 Ornn 网络,必须在 PR 中重新评估每个 codex/Responses 请求额外 Ornn 往返的成本和缓存策略。6. ToolContextMetadata 后续强类型化
ToolContextMetadata目前仍是IReadOnlyDictionary<string, string>,并承载scope_id/owner_subject/nyxid_access_token等稳定语义。按仓库字段命名与 Metadata 决策树,这些属于核心语义,长期应抽进ResponsesCallerScope/LLMRequestCallerContext/ typed sub-record,而不是固化在 bag 里。这次不一并修改
ToolContextMetadata的形状,避免扩散到ChatRuntime、UseSkillTool和其他消费者;需要后续单独开 issue 做强类型化收敛。外部 surface 校验
本功能不需要改 NyxID / chrono-ornn。现有公开能力已经够用:
/api/v1/proxy/s/{slug}/{path}。GET /api/v1/skill-search。GET /api/v1/skills/:idOrName/json,需要 authenticated caller 和ornn:skill:read。OrnnSkillClient已经通过 NyxID proxy 调这些接口。验证计划
ResponsesToolClassifier异步分类,覆盖 substitute、additive、同名 additive 去重。ResponsesUserSkillsToolProvider能从 concrete skill sources 返回use_skill和ornn_search_skills,且不返回无关IAgentToolSource工具。IResponsesToolProvider;SkillsAgentToolSource/OrnnAgentToolSourceconcrete 注册与IAgentToolSource枚举不产生重复实例或重复工具。/v1/responses,fake LLM provider 捕获到的LLMRequest.Tools包含use_skill与ornn_search_skills。/v1/messages不注入IResponsesToolProvider,不会突然出现 Aevatar additive tools。bash tools/ci/test_stability_guards.sh。至少跑相关测试项目;如果改到 shared DI,补跑 host composition / AI tool provider 相关测试。不在范围内
/v1/messages注入 Aevatar tools。UseSkillTool已有远程 skill body TTL;这次先不新增 discovery 缓存。/daily、/social-media)的 Ornn 化迁移,继续走 refactor: migrate AgentBuilder templates (/daily, /social-media) from hard-code to Ornn skill platform #367。