Skip to content

NyxID Responses 直连无法访问用户 Ornn skills,需共享 skill 主干 #664

@eanzhao

Description

@eanzhao

背景:两条 NyxID 入口路径

仓库当前有两条用户进入 aevatar 的通道:

  1. NyxID → ChatRuntime 路径:Lark / Telegram bot → NyxID 回调 → AgentBuilder / SkillRunner / ChatRuntime → 通过统一 IAgentToolSource 主干发现工具。当前 AddAevatarPlatform 会打开 EnableSkillsEnableOrnnSkills,因此 ChatRuntime 能拿到 SkillsAgentToolSource 暴露的 use_skillOrnnAgentToolSource 暴露的 ornn_search_skills
  2. 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:33HandleCreateResponseAsync
  • HandleCreateResponseAsync 在构造 LLMRequest 前调用 ResponsesToolClassifier.Classify(...),输入是客户端声明的 function tools + IEnumerable<IResponsesToolProvider>
  • 唯一注册的 IResponsesToolProviderResponsesAevatarToolProvidersrc/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs:112)。它只提供三类 Responses 本地工具:TodoWriteTask/taskWebFetch/web_fetchWebSearch/web_search
  • MainnetHostBuilderExtensions 已通过 builder.AddAevatarPlatform(...) 注册 Skills / Ornn 相关的 IAgentToolSource。所以问题不是全局 DI 没有 IRemoteSkillFetcherOrnnAgentToolSource,而是 Responses 路径没有从 IAgentToolSource 取 skill 工具。
  • caller 身份已可用于工具执行:toolContextMetadata 里有 scope_idowner_subjectnyxid_access_tokenUseSkillTool / 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_skillornn_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 也以强类型显式传入,避免 IHttpContextAccessorAsyncLocal 或 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。它必须显式构造注入 SkillsAgentToolSourceOrnnAgentToolSource,只桥接这两个 skill 主干:

  • SkillsAgentToolSourceuse_skill
  • OrnnAgentToolSourceornn_search_skills

这样未来如果 DI 里新增 MCPAgentToolSourceLarkAgentToolSourceServiceInvokeAgentToolSource 或其他 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 的形状,避免扩散到 ChatRuntimeUseSkillTool 和其他消费者;需要后续单独开 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_skillornn_search_skills,且不返回无关 IAgentToolSource 工具。
  • Host 组合测试:Mainnet 能解析两个 IResponsesToolProviderSkillsAgentToolSource / OrnnAgentToolSource concrete 注册与 IAgentToolSource 枚举不产生重复实例或重复工具。
  • Endpoint / 集成测试:调用 /v1/responses,fake LLM provider 捕获到的 LLMRequest.Tools 包含 use_skillornn_search_skills
  • 回归测试:/v1/messages 不注入 IResponsesToolProvider,不会突然出现 Aevatar additive tools。
  • 变更涉及测试时执行:bash tools/ci/test_stability_guards.sh。至少跑相关测试项目;如果改到 shared DI,补跑 host composition / AI tool provider 相关测试。

不在范围内

  • 把每个 Ornn skill 展开为独立 function tool。
  • /v1/messages 注入 Aevatar tools。
  • 新增 Ornn / NyxID API 或修改外部仓库。
  • 引入 scope 级 in-memory TTL。UseSkillTool 已有远程 skill body TTL;这次先不新增 discovery 缓存。
  • AgentBuilder 模板(/daily/social-media)的 Ornn 化迁移,继续走 refactor: migrate AgentBuilder templates (/daily, /social-media) from hard-code to Ornn skill platform #367
  • Responses 路径下 user skill 的 schema 校验、配额、隔离策略,后续单独拆 issue。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions