mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
Fall back to Codex CLI when ACP rejects session configuration
A codex_local agent configured with a manual model id (e.g. gpt-5.5-pro) failed every ACP-lane run with acpx_session_config_failed: codex-acp validates session/set_config_option values against its internal model list and rejects ids it does not advertise (ACP -32602), even though the Codex CLI lane accepts manual model ids via --model. The existing ACP->CLI fallback only covered thrown startup errors, not structured failed results from the configure_session phase. Auto-selected ACP now falls back to the CLI lane when the ACP server rejects session configuration, preserving the configured model/effort/fast-mode via startup args. Explicit engine=acp remains strict and fails loudly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cc81eefb60
commit
8c46620678
5 files changed
+97
-3
No files matched your search
@@ -5,6 +5,11 @@ export const DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS = "deny";
|
||||
export const DEFAULT_ACP_ENGINE_TIMEOUT_SEC = 0;
|
||||
export const DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS = 0;
|
||||
|
||||
// The ACP server rejected a session/set_config_option value (for example a
|
||||
// manual model id its validation list does not include). Adapters with a
|
||||
// non-ACP lane can treat this as recoverable and retry there.
|
||||
export const ACPX_SESSION_CONFIG_FAILED_ERROR_CODE = "acpx_session_config_failed";
|
||||
|
||||
export const ACPX_ADAPTER_AGENT_IDS = {
|
||||
claude_local: "claude",
|
||||
codex_local: "codex",
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
type AcpRuntimeTurnResult,
|
||||
} from "acpx/runtime";
|
||||
import {
|
||||
ACPX_SESSION_CONFIG_FAILED_ERROR_CODE,
|
||||
DEFAULT_ACP_ENGINE_AGENT,
|
||||
DEFAULT_ACP_ENGINE_MODE,
|
||||
DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS,
|
||||
@@ -1442,7 +1443,7 @@ function classifyError(
|
||||
if (acpCode === "ACP_BACKEND_MISSING") return "acpx_backend_missing";
|
||||
if (acpCode === "ACP_BACKEND_UNAVAILABLE") return "acpx_backend_unavailable";
|
||||
if (phase === "ensure_session") return "acpx_session_init_failed";
|
||||
if (phase === "configure_session") return "acpx_session_config_failed";
|
||||
if (phase === "configure_session") return ACPX_SESSION_CONFIG_FAILED_ERROR_CODE;
|
||||
if (phase === "turn") return "acpx_turn_failed";
|
||||
return null;
|
||||
})();
|
||||
|
||||
@@ -104,5 +104,5 @@ Notes:
|
||||
- Some model/tool combinations reject certain effort levels (for example minimal with web search enabled).
|
||||
- Fast mode is supported on GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`.
|
||||
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
|
||||
- Codex ACP is the preferred auto lane when Node >=22.13.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
|
||||
- Codex ACP is the preferred auto lane when Node >=22.13.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable or the ACP server rejects the session configuration (for example a manual model id its set_config_option validation does not list); explicit engine="acp" fails loudly.
|
||||
`;
|
||||
@@ -158,4 +158,75 @@ describe("codex_local ACP startup fallback", () => {
|
||||
|
||||
expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to Codex CLI when auto-selected ACP rejects session configuration", async () => {
|
||||
executeCodexAcp.mockResolvedValueOnce({
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage:
|
||||
'Agent rejected session/set_config_option for "model"="gpt-5.5-pro": Invalid params (ACP -32602).',
|
||||
errorCode: "acpx_session_config_failed",
|
||||
provider: "acpx",
|
||||
model: "gpt-5.5-pro",
|
||||
resultJson: { phase: "configure_session" },
|
||||
summary: "config rejected",
|
||||
} as never);
|
||||
const ctx = buildContext({ model: "gpt-5.5-pro" });
|
||||
|
||||
const result = await execute(ctx as never);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.summary).toBe("hello");
|
||||
expect(executeCodexAcp).toHaveBeenCalledTimes(1);
|
||||
expect(runAdapterExecutionTargetProcess).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.onLog).toHaveBeenCalledWith(
|
||||
"stderr",
|
||||
expect.stringContaining("Codex ACP rejected session configuration"),
|
||||
);
|
||||
expect(ctx.onLog).toHaveBeenCalledWith(
|
||||
"stderr",
|
||||
expect.stringContaining("gpt-5.5-pro"),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the session-config failure unchanged when engine=acp is explicit", async () => {
|
||||
const failed = {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage: "Agent rejected session/set_config_option",
|
||||
errorCode: "acpx_session_config_failed",
|
||||
provider: "acpx",
|
||||
resultJson: { phase: "configure_session" },
|
||||
summary: "config rejected",
|
||||
};
|
||||
executeCodexAcp.mockResolvedValueOnce(failed as never);
|
||||
const ctx = buildContext({ engine: "acp" });
|
||||
|
||||
const result = await execute(ctx as never);
|
||||
|
||||
expect(result).toBe(failed);
|
||||
expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not fall back for other failed ACP results", async () => {
|
||||
const failed = {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage: "turn failed",
|
||||
errorCode: "acpx_turn_failed",
|
||||
provider: "acpx",
|
||||
resultJson: { phase: "turn" },
|
||||
summary: "turn failed",
|
||||
};
|
||||
executeCodexAcp.mockResolvedValueOnce(failed as never);
|
||||
const ctx = buildContext();
|
||||
|
||||
const result = await execute(ctx as never);
|
||||
|
||||
expect(result).toBe(failed);
|
||||
expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
formatOutputInactivityMonitorErrorMessage,
|
||||
resolveCodexInactivityTimeout,
|
||||
} from "./output-inactivity-monitor.js";
|
||||
import { ACPX_SESSION_CONFIG_FAILED_ERROR_CODE } from "@paperclipai/adapter-utils/acpx-engine/constants";
|
||||
import {
|
||||
createCodexAcpExecutor,
|
||||
formatCodexAcpFallbackMessage,
|
||||
@@ -330,8 +331,9 @@ export async function ensureCodexSkillsInjected(
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
const engineSelection = await resolveCodexExecutionEngineForRun(ctx);
|
||||
if (engineSelection.engine === "acp") {
|
||||
let acpResult: AdapterExecutionResult | null = null;
|
||||
try {
|
||||
return await executeCodexAcp(ctx);
|
||||
acpResult = await executeCodexAcp(ctx);
|
||||
} catch (err) {
|
||||
if (engineSelection.explicit) throw err;
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
@@ -340,6 +342,21 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
formatCodexAcpFallbackMessage(`Codex ACP startup failed: ${reason}`),
|
||||
);
|
||||
}
|
||||
if (acpResult) {
|
||||
// A session/set_config_option rejection (e.g. a manual model id the ACP
|
||||
// server's validation list does not include) is recoverable: the CLI
|
||||
// lane applies model/effort/fast-mode via startup args instead of ACP
|
||||
// session controls. Explicit engine=acp stays strict and fails loudly.
|
||||
const sessionConfigRejected =
|
||||
acpResult.errorCode === ACPX_SESSION_CONFIG_FAILED_ERROR_CODE;
|
||||
if (engineSelection.explicit || !sessionConfigRejected) return acpResult;
|
||||
await ctx.onLog(
|
||||
"stderr",
|
||||
formatCodexAcpFallbackMessage(
|
||||
`Codex ACP rejected session configuration: ${acpResult.errorMessage ?? "unknown error"}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!engineSelection.explicit && engineSelection.fallbackReason) {
|
||||
await ctx.onLog("stderr", formatCodexAcpFallbackMessage(engineSelection.fallbackReason));
|
||||
|
||||
Reference in new issue
Block a user