Replies: 3 comments 1 reply
|
补一条对 §2 "Claude Code v2 通过本地 adapter(Messages → Responses 翻译)接入" 这条措辞的调研——晚 2025 / 早 2026 工业现状对我们的判断有几个反馈点。 现状没有大牌网关把 Messages-ingress → Responses-upstream 这条链路做成熟。 最常推荐的 公开最干净的两个完整实现是小项目:
所有公共桥都不用 Anthropic 没有官方多 provider 计划。 唯一官方 OpenAI 集成是反向的—— 对本 RFC 的 4 个实际含义1. §2 "本地 adapter" 措辞低估了工程量。 最接近可复用的轮子是 raine 的 reducer 代码(MIT,活跃)+ LiteLLM 的参数映射表(已公开),两个合起来才覆盖一个完整桥。建议把 v2 那条改成:"v2 通过本地 adapter(Messages → Responses 翻译)接入;工程量参考 2. §6 long-running tool 续传对 Claude Code 路径物理上不可用。 Claude Code 是 stateless 重发整段历史,没有 client-side 的"我还要继续上次的 response"语义可以传到桥。意味着:
建议在 §6 末尾加一句:"Claude Code 路径(v2)由于客户端 stateless 重发 history,无续传语义来源;long-running tool 仅对 Codex / Cursor / Opencode 可用。" 3. v2 备选路径:aevatar 直接吃 4. MCP plugin 是完全不同的接入形态。 一条 honest caveat(值得抄进 v2 范围)
即便协议翻译 100% 对,Claude Code 的 harness(系统 prompt / loop 时序 / tool 使用模式)是为 Claude 模型调出来的。aevatar 当"模型"接进去时 semantic layer 未必匹配。v2 spec 应明确:"v2 Claude Code 支持 = 可用,不承诺 parity。" 调研来源 |
|
resume 时也要用当前 NyxID delegation token 重新校验同 scope / 同主体是否有权恢复。否则只要猜到或泄露了 response id,就可能跨 scope 续会话。这里和 §8 的 scope resolution 是同一个问题:NyxID user、channel sender、bot owner、Aevatar scope、target actor 需要明确绑定规则,不能默认混成同一个身份语义。 |
|
§5 的 default forward 还需要一个正式的 external-tool continuation contract。LLM 选择 forwarded tool 后,Aevatar 不能只把 客户端回 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
产品 RFC: Aevatar 作为多 agent 编排层,承接 Codex/Cursor(Responses API)与 Lark/TG/WeChat channel 客户端
1. 目标用户场景
用户在自己已经在用的 AI agent 客户端里(Codex / Cursor / Opencode 等说 OpenAI Responses API 的)配置一个第三方 base URL + API key,背后接的不是直连 OpenAI/Anthropic,而是 NyxID(前置网关)+ Aevatar(多 agent 编排) 的运行时。
用户感觉上还是在跟"AI 模型"对话,实际上:
第二条入口:用户绑 Lark / Telegram / WeChat 账户,channel 消息走 NyxID channel-relay 进 Aevatar,输出原路回 channel。和 API key 入口走同一套 agent backend。
2. 协议选定:
/v1/responsesonly(v1)理由:
previous_response_id原生支持跨 HTTP 请求会话续传,适合 long-running tool / 多步编排。/v1/messages无等价 resume 语义;强行支持要塞自定义 session id 进 metadata,客户端不一定认。客户端覆盖代价:
v1 首发覆盖 OpenAI-Responses-native 客户端。
3. NyxID 接入模型:和 ornn 一样走通用 proxy plane
aevatar 在 NyxID 上注册为一个普通
DownstreamServicerow,路径/api/v1/proxy/s/aevatar/v1/responses——与 ornn / chrono-ornn 同模式,NyxID 侧 v1 无新 Rust 代码。X-NyxID-Identity-TokenJWT + 可选X-NyxID-Delegation-Token)(早期版本曾写"NyxID 必须新增
InternalServiceProvider类"——已纠正。那是 v2 nice-to-have(让 aevatar 出现在/api/v1/llm/aevatar/*namespace 蹭翻译 pipeline)。v1 通用 proxy 就够。)4. Aevatar 内部编排(愿景,部分已实现)
LLM 协议层的 "function call" / "tool" 是 JSON Schema + 调用结果的形态;Aevatar 内部 tool 可以是 GAgent / workflow / script / 子 agent / RAG / MCP,只要被 LLM 选中后能产出可序列化结果。所有 Aevatar tool 暴露给 LLM 时被适配成 function-call schema,但反过来不成立——LLM 视角的 function call 不能反推它在 Aevatar 里是哪种实体。这是本 pipeline 真正的耦合点。
具体能力:
ScriptDefinitionGAgent/ScriptRuntimeGAgent注册为 agent 内部 tool。5. 客户端 declared tool 处置策略
默认:forward——客户端 declared 的 tool 原样保留在发给内层 LLM 的 list 里;LLM emit
tool_use透传给客户端;客户端执行回tool_result;继续。v1 不做 refuse:任何 tool aevatar 处理不了就让客户端处理,不让 aevatar 的能力缺口卡用户的事。
必须 substitute(同名 tool 在两个世界里语义不同)
TodoWrite/Todo*系列Task/ 客户端 sub-agent dispatch应该 substitute(aevatar 接管能带来 trace 价值)
WebFetchWebSearchAdditive(aevatar 在客户端 declared list 之外加的,
aevatar_前缀避免命名冲突)aevatar_workflowaevatar_scriptScriptDefinitionGAgent注册的 script。aevatar_delegateaevatar_memory_query典型 forward 列表(明说 aevatar 不接管)
Bash/Read/Write/Edit/Glob/Grep/NotebookEdit/Skill(客户端本地~/.claude/skills)/ stdio-class MCP — 全部 client 文件系统 / 进程资源,aevatar 没访问权。Remote/HTTP MCP 是潜在 substitute 候选,v1 不做。实现位置
在 aevatar 的 protocol boundary(
/v1/responsesingress)层做分类:tool_use出口前在 run/session actor emitToolCallEmitted(response_id, call_id, tool_name, schema_hash, arguments, expiry);客户端回tool_result时按call_id幂等对账。完整契约见 §13。否则 forward 链路依赖 HTTP stream / in-process 状态,违反 Refactor ChatRuntime to match Harness boundary (#568) — blocks NyxID-fronted Responses API gateway #608 要解决的问题Trade-off 明说
Substitute 后客户端有些任务可能比 vanilla Codex / Cursor 弱(丢了客户端独有的工具优化)。换来 aevatar 的多会话编排、跨 agent 协作、RAG。把这个 trade-off 写入 product 定位,不假装无损。
6. Long-running tool 续传
previous_response_id是这条 pipeline 的核心机制。完整契约见 §13。要点:response.id是不透明 handle(opaque),不从 GAgent id / checkpoint 派生。aevatar 内部维护映射到对应 run/session actor,持久化 ownership facts + lifecycle + pending forwarded calls。POST /v1/responses/{id}/cancel(OpenAI Responses 标准端点);过期或 cancel 后即便 token 对也拒绝 resume。这是 #608(ChatRuntime refactor)必须先关掉的关键理由之一 —— 现在的 in-process ChatRuntime 不能跨 HTTP 请求续会话。
7. LLM credential brokering
Aevatar 在所有路径上都不持有 LLM credential(per CLAUDE.md "Aevatar 不保存任何 credential")。
NyxIdLLMProvider(已有),通过AgentToolRequestContext拿 access token。NyxID 已有的
inject_delegation_tokenflag +X-NyxID-Delegation-Tokenheader 满足这条。8. 多租户 scope
Aevatar 当前 scope 走 URL
/api/scopes/{scopeId}/*。新 inbound(/v1/responses+ channel webhook)从 NyxID 进来时,scope 要从 NyxID delegation token 反解。新加一个 scope resolution port:"NyxID user_id → aevatar scope"。两条 inbound(Responses API、channel)复用同一套。
9. NyxID 侧改造清单
v1 几乎零 Rust 代码改动——aevatar 走通用 proxy plane,复用 ornn 已有的全部基础设施。
DownstreamServicerowslug=aevatar,base_url=https://aevatar.example.com,forward_identity_mode=jwt,inject_delegation_token=true。aevatar:*permission scopes 加进 role mappingaevatar:run:invoke等 scope 进 NyxID RBAC 配置。handlers/proxy.rs:2142-2233已对通用 proxy 做bytes_stream()透传 + content-length 剥离 + idle-timeout 看门狗。验证proxy_stream_idle_timeout_secs对 long-running 场景配够大。POST /api/v1/delegation/refresh自续;(b) NyxID 加 API key revoke 时 push 通知给 aevatar。待决,可能要小代码。已删:v1 不需要
InternalServiceProvider类 +/api/v1/llm/aevatar/v1/*路由(推 v2 nice-to-have)。部署 gotcha(per #417 根因):end-user API key 默认
allow_all_services=false,必须把 aevatar 的service_id加进 user 的allowed_service_ids,或用allow_all_services=true的 key,否则handlers/proxy.rs:1045-1066直接 403。写进 ops README。10. Aevatar 侧改造清单
/v1/responsesingress(StreamProxy 实现形态)X-NyxID-Identity-Token(不验签)、scalar header fallback、permission gating、outbound SA-mode token cache。复用 ornnnyxidAuth.ts模式。/v1/responses复用 scope resolution。aevatar_workflow/aevatar_script/aevatar_delegate/aevatar_memory_query。TodoWrite(agent-scoped 持久化)、Task(GAgent topology)、WebFetch/WebSearch(出 trace 入 RAG)。previous_response_id续传 + opaque handleresponse.id→ run/session actor 持久化 ownership + lifecycle + pending forwarded calls;resume 每次重走 scope resolution,跨 origin 默认禁止。tool_use在 run/session actor 事件化持久化(call_id, tool_name, schema_hash, arguments, status, expiry);客户端tool_result按call_id幂等对账 + self-message 继续 LLM run。POST /v1/responses/{id}/cancel端点cancelled。11. v1 范围 / 非目标
/v1/responses;Anthropic Messages 不支持。12. 公开问题
aevatar_前缀 hard-coded 避开冲突。是否够?需不需要更动态的命名空间约定?TodoWriteschema 和 aevatar 版本 schema 不一致时?默认 aevatar 版本 schema 覆盖;记录 schema diff 警告。需要 review。response.in_progress等;v1 follow 已定义 event,不发明新类型。InternalServiceProvider谁来实现:跨仓库改动,需要 NyxID 那边对应排期。13. Session Continuation Contract
源自 comment 16877401(opaque response.id + re-auth)与 16877407(forwarded tool 持久化);两条其实是同一 contract 的两个侧面。本节为 §5(forward)/ §6(previous_response_id)/ §8(scope resolution)的共同规范基础。
Contract shape
Resume 流程(每次
previous_response_id请求)response_idlookup → 对应 run/session actoractor.scope_id(不是验证 token 有效,是验证 resolve 出的 scope 匹配)origin_kind与本次请求 ingress 一致;跨 origin 默认拒绝tool_result:matchcall_id→schema_hashcheck → 状态机推进pending → received不变式(v1 必须实现)
POST /v1/responses/{id}/cancel必须实现tool_use出口前必须在 actor 状态里 emitToolCallEmitted事件tool_resultschema 不匹配当时 emit → reject,不静默接受{"error":"tool_call_expired","call_id":...}喂回 LLM graceful 收尾call_id二次tool_result返回已 resolved 结果,不重走 LLMcancelled与 #608 的关系
#608 把 ChatRuntime 抽掉之后,跨 HTTP 请求的 continuation 事实必须 actor-owned + event-sourced。本 contract 是 #608 在协议入口的具象化——forwarded tool 链路如果不实现本 contract,等于 ChatRuntime 影子复活。本 contract 与 #608 priority 等同。
NyxID 侧依赖
aud=channel-relay/reply,单次性)与 delegation token(长期)形态不同;§8 scope resolution port 必须吃两种输入,且 resolve 出的owner_subject语义明确(channel sender ≠ channel bot owner)。14. Aevatar Inbound Auth Contract(per ornn 模式)
NyxID 接入身份契约——aevatar 必须按 ornn 的
nyxidAuth.ts模式实现:X-NyxID-Identity-Token(JWT),base64url-decode middle segment,不验签(信任 NyxID proxy,proxy 已 verify),抽sub/email/name/roles[]/permissions[]X-NyxID-User-Id/X-NyxID-User-Email/X-NyxID-User-Namescalar headers;此时 permissions 空,所有requirePermission都 403Authorization: Bearer <user_nyxid_token>用于 aevatar 反向调 NyxIDas the user;NyxID 默认不转发,需 binding 开forward_access_token=trueas itself用 OAuth2client_credentials+ token 缓存(到expires_in - 60s)。credentials cluster-wide 持久化(不能 node-local,perfeedback_aevatar_secrets_store_node_local)Content-Type: text/event-stream+Cache-Control: no-cache, no-transform+X-Accel-Buffering: no+ 每 ~15s: keepalive\n\n心跳AEVATAR_PUBLIC_ORIGINenv 给 ops。启动时 log warning if 请求带 non-emptyroles[]但permissions[]空(NyxID sideforward_identity_mode=headers而非jwt的 smell)部署约束:信任不验签的前提是 aevatar 只通过 NyxID proxy 入流,直接外部访问被网络层隔离。
15. Aevatar 自验证计划(不依赖 NyxID 完成的递进验证)
Phase A — 完全不依赖 NyxID(最关键,先做)
目标:验证
/v1/responsesingress + §13 contract + tool 处理 + LLM 出口都自洽。localhost:5000X-NyxID-Identity-Token(任意合法 JWT 形状,middle segment base64url 含{"sub":"test_user_1","permissions":["aevatar:run:invoke"]},header / signature 可乱填——aevatar 不验签所以能用)NyxIdLLMProvider→MEAILLMProvider(已有,直连 OpenAI),dev 自己的 OpenAI key必跑用例:
POST /v1/responses基础推理 + SSE 返回tool_use,client 模拟回tool_result,run 继续tool_use→ HTTP 断 → T2 用previous_response_id+tool_result续,验证 actor 从 event store 重建 pending callPOST /v1/responses/{id}/cancel端点tool_call_expired错误喂回 LLMcall_id二次tool_result返回已 resolved 结果tool_result被 rejecttool_resultresolutionPhase A 过 = #608 commit criterion 满足——跨 HTTP 边界事实归 actor、不靠 in-process state。这步过了 #609 核心架构立住。
Phase B — 假 NyxID 前置
目标:验证 identity propagation 契约 + scope resolution port 双输入。
/api/v1/proxy/s/aevatar/*:接 raw → 注入X-NyxID-Identity-Token+X-NyxID-Delegation-Token→ 转发 aevatarOPENAI_BASE_URL=http://stub-nyxid/api/v1/proxy/s/aevatar/v1Phase C — 真 NyxID dev 实例(端到端)
DownstreamService(slugaevatar-staging)allowed_service_ids含 aevatarNyxIdLLMProvider拿 inject 的 token 反向调/api/v1/llm/gateway/v1/*)proxy_stream_idle_timeout_secs够大Phase A 是真正卡 #608 + #609 自洽性的关卡。B 和 C 是工程胶水验证。
相关
All reactions