mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
fix: enable incomplete-turn recovery for Gemini
This commit is contained in:
committed by
Peter Steinberger
parent
73a6a2a6ab
commit
0ce93c9f1a
@@ -735,6 +735,33 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
|
||||
expect(retryInstruction).toBe(REASONING_ONLY_RETRY_INSTRUCTION);
|
||||
});
|
||||
|
||||
it("detects reasoning-only Gemini turns from signed thinking blocks", () => {
|
||||
const retryInstruction = resolveReasoningOnlyRetryInstruction({
|
||||
provider: "google",
|
||||
modelId: "gemini-2.5-pro",
|
||||
aborted: false,
|
||||
timedOut: false,
|
||||
attempt: makeAttemptResult({
|
||||
assistantTexts: [],
|
||||
lastAssistant: {
|
||||
role: "assistant",
|
||||
stopReason: "end_turn",
|
||||
provider: "google",
|
||||
model: "gemini-2.5-pro",
|
||||
content: [
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "internal reasoning",
|
||||
thinkingSignature: JSON.stringify({ id: "gemini_rs_helper", type: "reasoning" }),
|
||||
},
|
||||
],
|
||||
} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(retryInstruction).toBe(REASONING_ONLY_RETRY_INSTRUCTION);
|
||||
});
|
||||
|
||||
it("treats exact NO_REPLY as a deliberate silent assistant reply", () => {
|
||||
const incompleteTurnText = resolveIncompleteTurnPayloadText({
|
||||
payloadCount: 0,
|
||||
@@ -916,6 +943,28 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
|
||||
expect(DEFAULT_EMPTY_RESPONSE_RETRY_LIMIT).toBe(1);
|
||||
});
|
||||
|
||||
it("detects generic empty Gemini turns without visible text", () => {
|
||||
const retryInstruction = resolveEmptyResponseRetryInstruction({
|
||||
provider: "google-vertex",
|
||||
modelId: "google/gemini-3.1-flash",
|
||||
payloadCount: 0,
|
||||
aborted: false,
|
||||
timedOut: false,
|
||||
attempt: makeAttemptResult({
|
||||
assistantTexts: [],
|
||||
lastAssistant: {
|
||||
role: "assistant",
|
||||
stopReason: "end_turn",
|
||||
provider: "google-vertex",
|
||||
model: "gemini-3.1-flash",
|
||||
content: [{ type: "text", text: "" }],
|
||||
} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(retryInstruction).toBe(EMPTY_RESPONSE_RETRY_INSTRUCTION);
|
||||
});
|
||||
|
||||
it("does not retry generic empty GPT turns after side effects", () => {
|
||||
const retryInstruction = resolveEmptyResponseRetryInstruction({
|
||||
provider: "openai",
|
||||
@@ -985,6 +1034,21 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
|
||||
expect(result.meta.livenessState).toBe("working");
|
||||
});
|
||||
|
||||
it("detects replay-safe planning-only Gemini turns", () => {
|
||||
const retryInstruction = resolvePlanningOnlyRetryInstruction({
|
||||
provider: "google-gemini-cli",
|
||||
modelId: "gemini-3.1-pro",
|
||||
prompt: "Please inspect the code, make the change, and run the checks.",
|
||||
aborted: false,
|
||||
timedOut: false,
|
||||
attempt: makeAttemptResult({
|
||||
assistantTexts: ["I'll inspect the code, make the change, and run the checks."],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(retryInstruction).toContain("Do not restate the plan");
|
||||
});
|
||||
|
||||
it("does not misclassify a direct answer that says 'i'm not going to' as planning-only", () => {
|
||||
const retryInstruction = resolvePlanningOnlyRetryInstruction({
|
||||
provider: "openai-codex",
|
||||
|
||||
@@ -77,6 +77,13 @@ const SINGLE_ACTION_RETRY_SAFE_TOOL_NAMES = new Set([
|
||||
"glob",
|
||||
"ls",
|
||||
]);
|
||||
const GEMINI_INCOMPLETE_TURN_PROVIDER_IDS = new Set([
|
||||
"google",
|
||||
"google-vertex",
|
||||
"google-antigravity",
|
||||
"google-gemini-cli",
|
||||
]);
|
||||
const GEMINI_INCOMPLETE_TURN_MODEL_ID_PATTERN = /^gemini(?:[.-]|$)/i;
|
||||
const DEFAULT_PLANNING_ONLY_RETRY_LIMIT = 1;
|
||||
const STRICT_AGENTIC_PLANNING_ONLY_RETRY_LIMIT = 2;
|
||||
// Allow one immediate continuation plus one follow-up continuation before
|
||||
@@ -394,12 +401,38 @@ function shouldApplyPlanningOnlyRetryGuard(params: {
|
||||
if (params.executionContract === "strict-agentic") {
|
||||
return true;
|
||||
}
|
||||
return isStrictAgenticSupportedProviderModel({
|
||||
return isIncompleteTurnRecoverySupportedProviderModel({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
});
|
||||
}
|
||||
|
||||
function stripProviderPrefix(modelId: string): string {
|
||||
const normalizedModelId = modelId.trim();
|
||||
const match = /^([^/:]+)[/:](.+)$/.exec(normalizedModelId);
|
||||
return (match?.[2] ?? normalizedModelId).toLowerCase();
|
||||
}
|
||||
|
||||
function isIncompleteTurnRecoverySupportedProviderModel(params: {
|
||||
provider?: string;
|
||||
modelId?: string;
|
||||
}): boolean {
|
||||
if (
|
||||
isStrictAgenticSupportedProviderModel({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const provider = normalizeLowercaseStringOrEmpty(params.provider ?? "");
|
||||
if (!GEMINI_INCOMPLETE_TURN_PROVIDER_IDS.has(provider)) {
|
||||
return false;
|
||||
}
|
||||
const modelId = typeof params.modelId === "string" ? params.modelId : "";
|
||||
return GEMINI_INCOMPLETE_TURN_MODEL_ID_PATTERN.test(stripProviderPrefix(modelId));
|
||||
}
|
||||
|
||||
function normalizeAckPrompt(text: string): string {
|
||||
const normalized = text
|
||||
.normalize("NFKC")
|
||||
|
||||
Reference in New Issue
Block a user