diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ba9612f61..a8dbd9d4727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ Docs: https://docs.openclaw.ai - Security/dotenv: block workspace `.env` overrides for Matrix, Mattermost, IRC, and Synology endpoint settings so cloned workspaces cannot redirect bundled connector traffic through local endpoint config. (#70240) Thanks @drobison00. - Telegram: require the same `/models` authorization for group model-picker callbacks, so unauthorized participants can no longer browse or change the session model through inline buttons. (#70235) Thanks @drobison00. - Agents/Pi: keep the filtered tool-name allowlist active for embedded OpenAI/OpenAI Codex GPT-5 runs and compaction sessions, so bundled and client tools still execute after the Pi `0.68.1` session-tool allowlist change instead of stopping at plan-only replies with no tool call. (#70281) Thanks @jalehman. +- Agents/Pi: honor explicit `strict-agentic` execution contracts for incomplete-turn retry guards across providers, so manually opted-in local or compatible models get the same retry behavior without relying on OpenAI model inference. (#66750) Thanks @ziomancer. ## 2026.4.21 diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts index 550ff995505..65db43b2d6d 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts @@ -360,7 +360,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { expect(result.payloads?.[0]?.text).toContain("Please try again"); }); - it("does not retry reasoning-only turns for non-openai assistant metadata", async () => { + it("does not retry reasoning-only turns for non-strict-agentic providers", async () => { mockedClassifyFailoverReason.mockReturnValue(null); mockedRunEmbeddedAttempt.mockResolvedValueOnce( makeAttemptResult({ @@ -386,8 +386,8 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { const result = await runEmbeddedPiAgent({ ...overflowBaseRunParams, - provider: "openai", - model: "gpt-5.4", + provider: "anthropic", + model: "sonnet-4.6", runId: "run-reasoning-only-provider-mismatch", }); diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index acfa1cf9674..a8ba9794062 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -1709,6 +1709,7 @@ export async function runEmbeddedPiAgent( const nextPlanningOnlyRetryInstruction = resolvePlanningOnlyRetryInstruction({ provider, modelId, + executionContract, prompt: params.prompt, aborted, timedOut, @@ -1717,6 +1718,7 @@ export async function runEmbeddedPiAgent( const nextReasoningOnlyRetryInstruction = resolveReasoningOnlyRetryInstruction({ provider: activeErrorContext.provider, modelId: activeErrorContext.model, + executionContract, aborted, timedOut, attempt, @@ -1724,6 +1726,7 @@ export async function runEmbeddedPiAgent( const nextEmptyResponseRetryInstruction = resolveEmptyResponseRetryInstruction({ provider: activeErrorContext.provider, modelId: activeErrorContext.model, + executionContract, payloadCount, aborted, timedOut, diff --git a/src/agents/pi-embedded-runner/run/incomplete-turn.ts b/src/agents/pi-embedded-runner/run/incomplete-turn.ts index da543c92f2b..9e0ad431117 100644 --- a/src/agents/pi-embedded-runner/run/incomplete-turn.ts +++ b/src/agents/pi-embedded-runner/run/incomplete-turn.ts @@ -307,6 +307,7 @@ function shouldSkipPlanningOnlyRetry(params: { export function resolveReasoningOnlyRetryInstruction(params: { provider?: string; modelId?: string; + executionContract?: string; aborted: boolean; timedOut: boolean; attempt: IncompleteTurnAttempt; @@ -319,6 +320,7 @@ export function resolveReasoningOnlyRetryInstruction(params: { !shouldApplyPlanningOnlyRetryGuard({ provider: params.provider, modelId: params.modelId, + executionContract: params.executionContract, }) ) { return null; @@ -341,6 +343,7 @@ export function resolveReasoningOnlyRetryInstruction(params: { export function resolveEmptyResponseRetryInstruction(params: { provider?: string; modelId?: string; + executionContract?: string; payloadCount: number; aborted: boolean; timedOut: boolean; @@ -354,6 +357,7 @@ export function resolveEmptyResponseRetryInstruction(params: { !shouldApplyPlanningOnlyRetryGuard({ provider: params.provider, modelId: params.modelId, + executionContract: params.executionContract, }) ) { return null; @@ -374,7 +378,11 @@ export function resolveEmptyResponseRetryInstruction(params: { function shouldApplyPlanningOnlyRetryGuard(params: { provider?: string; modelId?: string; + executionContract?: string; }): boolean { + if (params.executionContract === "strict-agentic") { + return true; + } return isStrictAgenticSupportedProviderModel({ provider: params.provider, modelId: params.modelId, @@ -531,6 +539,7 @@ export function resolvePlanningOnlyRetryLimit( export function resolvePlanningOnlyRetryInstruction(params: { provider?: string; modelId?: string; + executionContract?: string; prompt?: string; aborted: boolean; timedOut: boolean; @@ -547,6 +556,7 @@ export function resolvePlanningOnlyRetryInstruction(params: { !shouldApplyPlanningOnlyRetryGuard({ provider: params.provider, modelId: params.modelId, + executionContract: params.executionContract, }) || (typeof params.prompt === "string" && !isLikelyActionableUserPrompt(params.prompt)) || params.aborted ||