mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 01:31:39 +00:00
* agents: auto-activate strict-agentic for GPT-5 and emit blocked-exit liveness
Closes two hard blockers on the GPT-5.4 parity completion gate:
1) Criterion 1 (no stalls after planning) is universal, but the pre-existing
strict-agentic execution contract was opt-in only. Out-of-the-box GPT-5
openai / openai-codex users who never set
`agents.defaults.embeddedPi.executionContract` still got only 1
planning-only retry and then fell through to the normal completion path
with the plan-only text, i.e. they still stalled.
Introduce `resolveEffectiveExecutionContract(...)` in
src/agents/execution-contract.ts. Behavior:
- supported provider/model (openai or openai-codex + gpt-5-family) AND
explicit "strict-agentic" or unspecified → "strict-agentic"
- supported provider/model AND explicit "default" → "default" (opt-out)
- unsupported provider/model → "default" regardless of explicit value
`isStrictAgenticExecutionContractActive` now delegates to the effective
resolver so the 2-retry + blocked-state treatment applies by default to
every GPT-5 openai/codex run. Explicit opt-out still works for users who
intentionally want the pre-parity-program behavior.
2) Criterion 4 (replay/liveness failures are explicit, not silent
disappearance) is violated by the strict-agentic blocked exit itself.
Every other terminal return path in src/agents/pi-embedded-runner/run.ts
sets `replayInvalid` + `livenessState` via `setTerminalLifecycleMeta`,
but the strict-agentic exit at run.ts:1615 falls through without them.
Add explicit `livenessState: "abandoned"` + `replayInvalid` (via the
shared `resolveReplayInvalidForAttempt` helper) to that exit, plus a
`setTerminalLifecycleMeta` call so downstream observers (lifecycle log,
ACP bridge, telemetry) see the same explicit terminal state they see on
every other exit branch.
Regressions added:
- `auto-enables update_plan for unconfigured GPT-5 openai runs`
- `respects explicit default contract opt-out on GPT-5 runs`
- `does not auto-enable update_plan for non-openai providers even when unconfigured`
- `emits explicit replayInvalid + abandoned liveness state at the strict-agentic blocked exit`
- `auto-activates strict-agentic for unconfigured GPT-5 openai runs and surfaces the blocked state`
- `respects explicit default contract opt-out on GPT-5 openai runs`
Local validation:
- pnpm test src/agents/openclaw-tools.update-plan.test.ts src/agents/pi-embedded-runner/run.incomplete-turn.test.ts src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts src/agents/system-prompt.test.ts src/agents/openclaw-tools.sessions.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.test.ts
122/122 passing.
Refs #64227
* agents: address loop-6 review comments on strict-agentic contract
Triages all three loop-6 review comments on PR #64679:
1. Copilot: 'The strict-agentic blocked exit returns an error payload
(isError: true) but sets livenessState to "abandoned". Elsewhere in
the runner/lifecycle flow, error terminal states are treated as
"blocked".' Verified: every other hardcoded error terminal branch in
run.ts (role ordering at 1152, image size at 1206, schema error at
1244, compaction timeout at 1128, aborted-with-no-payloads at 606)
uses livenessState: "blocked". Match that convention at the
strict-agentic blocked exit at 1634. Updated the 'emits explicit
replayInvalid + abandoned liveness state' regression test to assert
the new "blocked" value and renamed the assertion commentary.
2. Copilot: 'The JSDoc for resolveEffectiveExecutionContract says
explicit "strict-agentic" in config always resolves to
"strict-agentic", but the implementation collapses to "default"
whenever the provider/mode is unsupported.' Rewrite the JSDoc to
explicitly document the unsupported-provider collapse as the lead
case (strict-agentic is a GPT-5-family openai/openai-codex-only
runtime contract) before listing the supported-lane behavior matrix.
No code change; this is a docstring-only clarification.
3. Greptile P2: 'Non-preferred Anthropic model constant. CLAUDE.md says
to prefer sonnet-4.6 for Anthropic test constants.' Swap
claude-opus-4-6 → claude-sonnet-4-6 in the two update_plan gating
fixtures that assert non-openai providers don't auto-enable the
planning tool. Behavior unchanged; model constant now matches repo
testing guidance.
Local validation:
- pnpm test src/agents/openclaw-tools.update-plan.test.ts src/agents/pi-embedded-runner/run.incomplete-turn.test.ts
29/29 passing.
Refs #64227
* test: rename strict-agentic blocked-exit liveness regression to match blocked state
Addresses loop-7 Copilot finding on PR #64679: loop 6 changed the
assertion to livenessState === 'blocked' to match the rest of the
hard-error terminal branches in run.ts, but the test title still said
'abandoned liveness state', which made failures and test output
misleading. Rename the test title to match the asserted value. No
code change beyond the it(...) title.
Validation: pnpm test src/agents/pi-embedded-runner/run.incomplete-turn.test.ts
(19/19 pass).
Refs #64227
* agents: widen strict-agentic auto-activation to handle prefixed and variant GPT-5 model ids
* Align strict-agentic retry matching
* runtime: harden strict-agentic model matching
---------
Co-authored-by: Eva <eva@100yen.org>
122 lines
5.1 KiB
TypeScript
122 lines
5.1 KiB
TypeScript
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
|
import { resolveAgentExecutionContract, resolveSessionAgentIds } from "./agent-scope.js";
|
|
|
|
/**
|
|
* Strip any leading `provider/` or `provider:` prefix from a model id so the
|
|
* bare-name regex matching below works against `openai/gpt-5.4` and
|
|
* `openai:gpt-5.4` the same way it does against `gpt-5.4`. Returns the bare
|
|
* model id lowercased for comparison.
|
|
*
|
|
* Without this, auto-activation silently failed on prefixed model ids — a
|
|
* user who configured `model: "openai/gpt-5.4"` in their agent config would
|
|
* get the pre-PR-H looser default behavior because the regex only matched
|
|
* bare names. The adversarial review in #64227 flagged this as a quality
|
|
* gap on completion-gate criterion 1.
|
|
*/
|
|
function stripProviderPrefix(modelId: string): string {
|
|
const normalizedModelId = modelId.trim();
|
|
const match = /^([^/:]+)[/:](.+)$/.exec(normalizedModelId);
|
|
return (match?.[2] ?? normalizedModelId).toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Regex that matches the full set of GPT-5 variants the strict-agentic
|
|
* contract should auto-activate for. Intentionally permissive: every
|
|
* model id in the gpt-5 family should opt in by default, not just the
|
|
* canonical `gpt-5.4`.
|
|
*
|
|
* Covers:
|
|
* - `gpt-5`, `gpt-5o`, `gpt-5o-mini` (no separator after `5`)
|
|
* - `gpt-5.4`, `gpt-5.4-alt`, `gpt-5.0` (dot separator)
|
|
* - `gpt-5-preview`, `gpt-5-turbo`, `gpt-5-2025-03` (dash separator)
|
|
*
|
|
* Does NOT cover `gpt-4.5`, `gpt-6`, or any non-gpt-5 family member.
|
|
*/
|
|
const STRICT_AGENTIC_MODEL_ID_PATTERN = /^gpt-5(?:[.o-]|$)/i;
|
|
|
|
/**
|
|
* Supported provider + model combinations where strict-agentic is the intended
|
|
* runtime contract. Kept as a narrow helper so both the execution-contract
|
|
* resolver and the `update_plan` auto-enable gate converge on the same
|
|
* definition of "GPT-5-family openai/openai-codex run".
|
|
*/
|
|
export function isStrictAgenticSupportedProviderModel(params: {
|
|
provider?: string | null;
|
|
modelId?: string | null;
|
|
}): boolean {
|
|
const provider = normalizeLowercaseStringOrEmpty(params.provider ?? "");
|
|
if (provider !== "openai" && provider !== "openai-codex") {
|
|
return false;
|
|
}
|
|
const modelId = typeof params.modelId === "string" ? params.modelId : "";
|
|
const bareModelId = stripProviderPrefix(modelId);
|
|
return STRICT_AGENTIC_MODEL_ID_PATTERN.test(bareModelId);
|
|
}
|
|
|
|
/**
|
|
* Returns the effective execution contract for an embedded Pi run.
|
|
*
|
|
* strict-agentic is a GPT-5-family openai/openai-codex-only runtime contract,
|
|
* so an unsupported provider/model pair always collapses to `"default"`
|
|
* regardless of what the caller passed or what config says — the contract
|
|
* is inert off-provider. Within the supported lane, the behavior matrix is:
|
|
*
|
|
* - Supported provider/model + explicit `"strict-agentic"` in config
|
|
* (defaults or per-agent override) ⇒ `"strict-agentic"`.
|
|
* - Supported provider/model + explicit `"default"` in config ⇒ `"default"`
|
|
* (opt-out honored).
|
|
* - Supported provider/model + unspecified ⇒ `"strict-agentic"` so the
|
|
* no-stall completion-gate criterion applies to out-of-the-box GPT-5 runs
|
|
* without requiring every user to set the flag.
|
|
* - Unsupported provider/model (anything that is not openai or openai-codex
|
|
* with a gpt-5-family model id) ⇒ `"default"`, even when the config
|
|
* explicitly sets `"strict-agentic"`. The retry guard and blocked-exit
|
|
* helpers all check this lane again, so an explicit `"strict-agentic"`
|
|
* on an unsupported lane is a no-op rather than a hard failure.
|
|
*
|
|
* This means explicit opt-out still works, but the gate criterion
|
|
* "GPT-5.4 no longer stalls after planning" now covers unconfigured
|
|
* installations, not only users who opted in manually.
|
|
*/
|
|
export function resolveEffectiveExecutionContract(params: {
|
|
config?: OpenClawConfig;
|
|
sessionKey?: string;
|
|
agentId?: string | null;
|
|
provider?: string | null;
|
|
modelId?: string | null;
|
|
}): "default" | "strict-agentic" {
|
|
const { sessionAgentId } = resolveSessionAgentIds({
|
|
sessionKey: params.sessionKey,
|
|
config: params.config,
|
|
agentId: params.agentId ?? undefined,
|
|
});
|
|
const explicit = resolveAgentExecutionContract(params.config, sessionAgentId);
|
|
// strict-agentic is a GPT-5-family openai/openai-codex runtime contract
|
|
// regardless of whether it was set explicitly or auto-activated. On an
|
|
// unsupported provider/model pair the contract is inert either way, so
|
|
// the effective value collapses to "default".
|
|
const supported = isStrictAgenticSupportedProviderModel({
|
|
provider: params.provider,
|
|
modelId: params.modelId,
|
|
});
|
|
if (!supported) {
|
|
return "default";
|
|
}
|
|
if (explicit === "default") {
|
|
return "default";
|
|
}
|
|
// Explicit strict-agentic OR unspecified-but-supported → strict-agentic.
|
|
return "strict-agentic";
|
|
}
|
|
|
|
export function isStrictAgenticExecutionContractActive(params: {
|
|
config?: OpenClawConfig;
|
|
sessionKey?: string;
|
|
agentId?: string | null;
|
|
provider?: string | null;
|
|
modelId?: string | null;
|
|
}): boolean {
|
|
return resolveEffectiveExecutionContract(params) === "strict-agentic";
|
|
}
|