问题
Pi 0.86.0 起,自定义 provider 的 streamSimple 输入由 Context 改为规范化后的 TranscriptContext:系统提示词与工具声明被折进 context.messages 的首条 role: "system" 消息,context.systemPrompt 与 context.tools 不再存在。Pi 0.86.0 CHANGELOG 的 Breaking Changes 已明确要求:
Changed inherited pi-ai provider stream inputs from Context to normalized TranscriptContext values. Custom providers must read system prompts and tool declarations from context.messages with getCurrentSystemPrompt() and getCurrentTools().
extensions/ai-providers/antigravity/ 的适配层仍按旧 Context 读取:
provider.ts:normalizeSystemPrompts(context.systemPrompt)、buildTools(context.tools);
google-conversion.ts:transformMessages() 把非 user / 非 toolResult 的消息一律当作 assistant 处理,convertMessages() 的兜底分支把其余角色当作 toolResult,两处都没有 role === "system" 分支。
于是首条 system 消息(content: ""、只有 sections 与 toolsAdded)先被迭代成空内容,再落进 toolResult 分支,被转换成一个没有 name 的 functionResponse 。contents[0] 恰好就是它,因此每一个请求 (含新会话首轮)都会被 Cloud Code Assist 以 400 拒绝。
症状
Error: Cloud Code Assist API error (400): {
"error": {
"code": 400,
"message": "* GenerateContentRequest.contents[0].parts[0].function_response.name: Name cannot be empty.\n",
"status": "INVALID_ARGUMENT"
}
}
换提示词、换 antigravity 模型、新建 session 都一样,因为失败点在 contents[0],与用户输入无关。
版本边界(已验证)
pi-ai
compat.stream() 对自定义 provider 的输入
适配层是否成立
≤ 0.85.1
原样透传 Context(systemPrompt / tools / messages)
✅
≥ 0.86.0
normalizeContext() 折成 { messages }
❌
对比 dist/compat.js 与 dist/api/google-shared.js:
0.85.1 compat.js normalizeContext: 0 命中 google-shared 系统消息处理: 0 命中
0.86.0 compat.js normalizeContext: 3 命中 google-shared 系统消息处理: 2 命中
0.87.0 compat.js normalizeContext: 3 命中 google-shared 系统消息处理: 2 命中
@earendil-works/pi-ai@0.85.1 的根入口也不导出 getCurrentSystemPrompt / getCurrentTools / collapseSystemMessages(0.86+ 才导出),因此修复不能直接假设这些 helper 可用。
最小复现(已验证)
Pi 0.86+ 实际传给 provider 的 context 形状(新会话首轮即可):
const context = {
messages : [
{
role : "system" ,
content : "" ,
sections : { preamble : "..." , tools : "..." , rules : "..." , /* ... */ } ,
toolsAdded : [ /* 工具声明 */ ] ,
timestamp : 0 ,
} ,
{ role : "user" , content : [ { type : "text" , text : "hi" } ] , timestamp : 1 } ,
] ,
} ;
// 注意:没有 context.systemPrompt,也没有 context.tools
convertMessages ( model , context ) [ 0 ] ;
// => { role: "user", parts: [{ functionResponse: { response: { output: "" } } }] }
对照:把同一条 transcript 去掉 system 消息、并把系统文本与工具声明放回 context.systemPrompt / context.tools(即 0.85.1 的形状),同一函数输出正常。
按 Pi 的真实链路(convertToLlm → normalizeContext → buildRequestBody)在一份真实 session 上重放,得到的请求体是:
request.systemInstruction: undefined
request.tools: undefined
request.contents[0]: {"role":"user","parts":[{"functionResponse":{"response":{"output":""}}}]}
而修复后的同一份 transcript(system 消息被正确 replay):
request.systemInstruction: {"role":"user","parts":[{"text":"You are an expert coding assistant ..."}]}
request.tools[0].functionDeclarations.length: 11
request.contents[0]: {"role":"user","parts":[{"text":"..."}]}
影响面
完全不可用 :antigravity 下所有模型、所有请求都 400,没有 provider 侧 workaround。
即使兜掉 400,请求仍是错的 :system prompt 不会发出(systemInstruction 缺失),工具声明一个都不会发出(request.tools 缺失)。即模型既不知道自己的系统提示,也没有任何工具。
打破 feat(ai-providers): add Google Antigravity and Cursor OAuth providers #234 的验收项 :/login google-antigravity 后「普通 Pi 工具回合可跑」目前不成立。
Cursor provider 同源假设 :extensions/ai-providers/cursor/provider.ts 同样读 context.systemPrompt / context.tools(buildSystemPrompt(context.systemPrompt, store, !!context.tools?.length) 等),需一并核对;本票先按 antigravity 的实测结论立据。
为什么现有测试与 CI 没有拦住
测试仍按旧形状构造 context:tests/extensions/ai-providers/antigravity.test.ts 使用 { systemPrompt: "You are helpful.", messages: [{ role: "user", ... }], tools: [...] },从未覆盖 0.86+ 的首条 system 消息形状;全仓也没有任何一处引用 toolsAdded / getCurrentTools / collapseSystemMessages。
开发依赖与 lock 固定在变更之前的版本:package.json devDependencies ^0.85.1,bun.lock 实际锁定 0.85.1。
CI 只验证锁定版本:.github/workflows/ci.yml:89-95 从 node_modules 读取 pi-ai / pi-coding-agent 版本再原样安装,peer 区间不进入任何验证。
也就是说:peerDependencies: ">=0.85.1" 让 Pi 0.86/0.87 成为合法但从未被验证 的安装组合,而该区间内存在硬失败。这与 #328 中已记录的现象同类(09-04 评论里 testikun 指出的 fresh-install 漂移当时表现为 web 子进程退出),只是后果更严重。
Workaround
建议
路线 A(与基线解耦,建议先做) :做版本无关的双形状适配 —— context.messages 中存在 system 消息时按 transcript replay 取系统文本与工具声明,否则回退 context.systemPrompt / context.tools;同时让 convertMessages 显式跳过非 user / assistant / toolResult 的角色,保证任何残留形状都不会再产出匿名 functionResponse。不引入 0.85.1 不存在的新 API,因此可在当前锁定基线上直接补 0.86+ 形状的回归测试。
路线 B :按 #410 的先例把支持基线抬到 ≥0.86,并改用上游 getCurrentSystemPrompt() / getCurrentTools() / collapseSystemMessages(),成套更新 manifest / lock / README / SETUP / CI / package-contract 测试与决策记录。
两条路线都需要在 PR 中显式说明与 #488 的关系:#488 正在改同一个 extensions/ai-providers/antigravity/google-conversion.ts 与同一个测试文件,需 rebase 或先合。
回归测试至少应覆盖:0.86+ 形状下(首条 system 消息 content: "" + sections + toolsAdded)不产生无 name 的 functionResponse、systemInstruction 非空、tools 声明数与 transcript 一致;以及 0.85.1 旧形状不回归。
与其它票的关系
验收
问题
Pi 0.86.0 起,自定义 provider 的
streamSimple输入由Context改为规范化后的TranscriptContext:系统提示词与工具声明被折进context.messages的首条role: "system"消息,context.systemPrompt与context.tools不再存在。Pi 0.86.0 CHANGELOG 的 Breaking Changes 已明确要求:extensions/ai-providers/antigravity/的适配层仍按旧Context读取:provider.ts:normalizeSystemPrompts(context.systemPrompt)、buildTools(context.tools);google-conversion.ts:transformMessages()把非user/ 非toolResult的消息一律当作 assistant 处理,convertMessages()的兜底分支把其余角色当作 toolResult,两处都没有role === "system"分支。于是首条 system 消息(
content: ""、只有sections与toolsAdded)先被迭代成空内容,再落进 toolResult 分支,被转换成一个没有name的functionResponse。contents[0]恰好就是它,因此每一个请求(含新会话首轮)都会被 Cloud Code Assist 以 400 拒绝。症状
换提示词、换 antigravity 模型、新建 session 都一样,因为失败点在
contents[0],与用户输入无关。版本边界(已验证)
compat.stream()对自定义 provider 的输入Context(systemPrompt/tools/messages)normalizeContext()折成{ messages }对比
dist/compat.js与dist/api/google-shared.js:@earendil-works/pi-ai@0.85.1的根入口也不导出getCurrentSystemPrompt/getCurrentTools/collapseSystemMessages(0.86+ 才导出),因此修复不能直接假设这些 helper 可用。最小复现(已验证)
Pi 0.86+ 实际传给 provider 的 context 形状(新会话首轮即可):
对照:把同一条 transcript 去掉 system 消息、并把系统文本与工具声明放回
context.systemPrompt/context.tools(即 0.85.1 的形状),同一函数输出正常。按 Pi 的真实链路(
convertToLlm→normalizeContext→buildRequestBody)在一份真实 session 上重放,得到的请求体是:而修复后的同一份 transcript(system 消息被正确 replay):
影响面
systemInstruction缺失),工具声明一个都不会发出(request.tools缺失)。即模型既不知道自己的系统提示,也没有任何工具。/login google-antigravity后「普通 Pi 工具回合可跑」目前不成立。extensions/ai-providers/cursor/provider.ts同样读context.systemPrompt/context.tools(buildSystemPrompt(context.systemPrompt, store, !!context.tools?.length)等),需一并核对;本票先按 antigravity 的实测结论立据。为什么现有测试与 CI 没有拦住
tests/extensions/ai-providers/antigravity.test.ts使用{ systemPrompt: "You are helpful.", messages: [{ role: "user", ... }], tools: [...] },从未覆盖 0.86+ 的首条 system 消息形状;全仓也没有任何一处引用toolsAdded/getCurrentTools/collapseSystemMessages。package.jsondevDependencies^0.85.1,bun.lock实际锁定0.85.1。.github/workflows/ci.yml:89-95从node_modules读取 pi-ai / pi-coding-agent 版本再原样安装,peer 区间不进入任何验证。也就是说:
peerDependencies: ">=0.85.1"让 Pi 0.86/0.87 成为合法但从未被验证的安装组合,而该区间内存在硬失败。这与 #328 中已记录的现象同类(09-04 评论里 testikun 指出的 fresh-install 漂移当时表现为 web 子进程退出),只是后果更严重。Workaround
建议
路线 A(与基线解耦,建议先做):做版本无关的双形状适配 ——
context.messages中存在 system 消息时按 transcript replay 取系统文本与工具声明,否则回退context.systemPrompt/context.tools;同时让convertMessages显式跳过非user/assistant/toolResult的角色,保证任何残留形状都不会再产出匿名functionResponse。不引入 0.85.1 不存在的新 API,因此可在当前锁定基线上直接补 0.86+ 形状的回归测试。路线 B:按 #410 的先例把支持基线抬到 ≥0.86,并改用上游
getCurrentSystemPrompt()/getCurrentTools()/collapseSystemMessages(),成套更新 manifest / lock / README / SETUP / CI / package-contract 测试与决策记录。两条路线都需要在 PR 中显式说明与 #488 的关系:#488 正在改同一个
extensions/ai-providers/antigravity/google-conversion.ts与同一个测试文件,需 rebase 或先合。回归测试至少应覆盖:0.86+ 形状下(首条 system 消息
content: ""+sections+toolsAdded)不产生无name的functionResponse、systemInstruction非空、tools声明数与 transcript 一致;以及 0.85.1 旧形状不回归。与其它票的关系
验收
function_response.name400systemInstruction非空,且与 transcript 中的系统提示一致request.tools的声明数与 transcript 工具集合一致