Merge PR #8226: fix(advisor): accept silent Gemini reviews (@roboomp)
This commit is contained in:
@@ -44,6 +44,9 @@
|
||||
|
||||
- Removed the `resolveAgentModelSource` model-resolver export, whose only use was being fed to `resolveExplicitModelRole`. Replaced by `resolveAgentModelSelection`, which returns the expanded `patterns` and the pre-expansion `role` together so a spawn path cannot derive one without the other ([#7910](https://github.com/can1357/oh-my-pi/pull/7910) by [@enieuwy](https://github.com/enieuwy)).
|
||||
- A run is now attributed to the model that actually produced its output, not whichever model the session was last pointed at. A retry fallback that errored on its first request — an exhausted quota, a hard provider error — was credited with the whole run in the Agent Hub row and the settled task result, even when the previous model did every turn. Sessions expose the serving model directly, holding the last model that produced output while a candidate is armed but unproven, and transcript-derived history stops at the newest turn that produced output.
|
||||
### Fixed
|
||||
|
||||
- Fixed Gemini advisors treating a valid silent review as an empty-response failure, repeatedly retrying the turn and eventually dropping the advisor backlog. ([#8223](https://github.com/can1357/oh-my-pi/issues/8223))
|
||||
|
||||
## [17.2.12] - 2026-08-08
|
||||
|
||||
|
||||
@@ -785,14 +785,22 @@ export class SessionAdvisors {
|
||||
mcpResources: this.#advisorMcpResources,
|
||||
});
|
||||
const baseAdvisorStreamFn = this.#advisorStreamFn ?? streamSimple;
|
||||
const advisorStreamFn: StreamFn = (requestModel, context, options) =>
|
||||
baseAdvisorStreamFn(
|
||||
requestModel,
|
||||
context,
|
||||
requestModel.api === "openai-codex-responses"
|
||||
? { ...options, codexSseMaxAttempts: ADVISOR_CODEX_SSE_MAX_ATTEMPTS }
|
||||
: options,
|
||||
);
|
||||
const advisorStreamFn: StreamFn = (requestModel, context, options) => {
|
||||
if (requestModel.api === "openai-codex-responses") {
|
||||
return baseAdvisorStreamFn(requestModel, context, {
|
||||
...options,
|
||||
codexSseMaxAttempts: ADVISOR_CODEX_SSE_MAX_ATTEMPTS,
|
||||
});
|
||||
}
|
||||
if (
|
||||
requestModel.api === "google-generative-ai" ||
|
||||
requestModel.api === "google-gemini-cli" ||
|
||||
requestModel.api === "google-vertex"
|
||||
) {
|
||||
return baseAdvisorStreamFn(requestModel, context, { ...options, acceptEmptyResponse: true });
|
||||
}
|
||||
return baseAdvisorStreamFn(requestModel, context, options);
|
||||
};
|
||||
const advisorAgent = new Agent({
|
||||
initialState: {
|
||||
systemPrompt,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import * as path from "node:path";
|
||||
import { Agent, type StreamFn } from "@oh-my-pi/pi-agent-core";
|
||||
import { type FetchImpl, streamSimple } from "@oh-my-pi/pi-ai";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import { TempDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
test("keeps Gemini 3.6 advisor context and accepts a silent review", async () => {
|
||||
const temp = TempDir.createSync("@issue-8223-");
|
||||
const auth = await AuthStorage.create(path.join(temp.path(), "auth.db"));
|
||||
auth.setRuntimeApiKey("google", "test-key");
|
||||
const registry = new ModelRegistry(auth);
|
||||
const model = getBundledModel("google", "gemini-3.6-flash");
|
||||
if (!model) throw new Error("missing bundled model");
|
||||
const bodies: unknown[] = [];
|
||||
const fetchMock: FetchImpl = async (_input, init) => {
|
||||
bodies.push(JSON.parse(String(init?.body)));
|
||||
const chunk = {
|
||||
candidates: [
|
||||
{
|
||||
content: { role: "model", parts: [{ thought: true, text: "Analyzing only" }] },
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 10,
|
||||
candidatesTokenCount: 5,
|
||||
thoughtsTokenCount: 5,
|
||||
totalTokenCount: 15,
|
||||
},
|
||||
};
|
||||
return new Response(`data: ${JSON.stringify(chunk)}\n\n`, {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
};
|
||||
const advisorStreamFn: StreamFn = (requestModel, context, options) =>
|
||||
streamSimple(requestModel, context, { ...options, fetch: fetchMock });
|
||||
const agent = new Agent({ initialState: { model, systemPrompt: ["Primary"], tools: [] } });
|
||||
const session = new AgentSession({
|
||||
agent,
|
||||
sessionManager: SessionManager.create(temp.path(), temp.path()),
|
||||
settings: Settings.isolated({ "compaction.enabled": false }),
|
||||
modelRegistry: registry,
|
||||
advisorTools: [],
|
||||
advisorStreamFn,
|
||||
});
|
||||
try {
|
||||
session.settings.setModelRole("advisor", "google/gemini-3.6-flash");
|
||||
expect(session.setAdvisorEnabled(true)).toBe(true);
|
||||
const advisor = session.getAdvisorAgent();
|
||||
if (!advisor) throw new Error("advisor did not start");
|
||||
await advisor.prompt("### Session update [in progress — more steps follow]\nImplement an order book.");
|
||||
expect(advisor.state.error).toBeUndefined();
|
||||
expect(bodies).toHaveLength(1);
|
||||
expect(bodies[0]).toMatchObject({
|
||||
systemInstruction: {
|
||||
parts: [{ text: expect.stringContaining("You bring a different angle") }],
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: "advise",
|
||||
description: expect.stringContaining("Send one concrete"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
await session.dispose();
|
||||
auth.close();
|
||||
await temp.remove();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user