fix(runner): honor Codex effort selected in composer (#14568)

## Thinking Path

> - Paperclip manages AI agents that work on assigned tasks.
> - The task composer lets a person choose an assignee, model, and
effort for the next run.
> - A Paperclip Runner agent can use Codex as its provider.
> - The composer hid Codex effort for that agent because it checked only
the older Codex adapter.
> - The native Runner input also did not carry an effort choice to
Codex.
> - This pull request carries the chosen effort from the composer to
each Codex turn.
> - People can now select a supported effort and get the effort they
selected.

## Linked Issues or Issue Description

Refs #14322

**What happened?**

The composer showed a model but no effort slider when the assignee used
Paperclip Runner with the Codex provider. A task-level model override
also did not reach the native Runner input.

**Expected behavior**

The composer shows effort choices for a known Codex model. The next
native Codex turn uses the selected model and effort.

**Steps to reproduce**

1. Open a task composer.
2. Select an agent that uses Paperclip Runner with the Codex provider.
3. Select a known Codex model such as `gpt-6-astra`.
4. Open the assignee and model picker. The effort slider is missing
before this change.

## What Changed

- Show known Codex effort levels for Paperclip Runner Codex assignees.
- Save the task effort override in the native run input and send it to
Codex on each turn.
- Apply the task's merged model and effort overrides when the native run
starts.
- Apply a task model override for OpenCode Runner without changing the
agent's provider.
- Add Runner effort tests and desktop and mobile Storybook cases.

## Verification

- `pnpm -r typecheck` passed.
- `pnpm build` passed.
- `pnpm build-storybook` passed.
- `pnpm check:token-gates` passed.
- Focused UI, server, Runner contract, and Codex driver tests passed.
- The full CI test matrix, build, typecheck, and canary dry run passed
on the latest head.

## Risks

- Native Runner inputs add an optional Codex effort field to the current
v5 input. Older inputs keep their previous behavior.
- A known model rejects an effort that its catalog does not support.
Unknown models do not show a slider.

> This fixes an existing composer bug. I checked `ROADMAP.md`; it does
not describe this bug as planned work.

## Model Used

OpenAI Codex, GPT-6. The exact deployment ID and context window are not
exposed in this session. The model used reasoning, code execution, and
repository tools.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: OpenAI GPT-6 Astra <noreply@openai.com>
This commit is contained in:
authored and GitHub committed 2026-09-29 09:41:05 -05:00
1 parent 3f4f8b37ba
commit da887ea3e9
19 files changed
+185 -9

No files matched your search

+2
View File
@@ -115,6 +115,8 @@ pending card.
Use **Composer → Model and effort picker** to review harness-specific model
choices. Codex uses the curated adapter catalog unless the instance declares
`PAPERCLIP_ADAPTER_MODELS`; general OpenAI API models are not Codex choices.
The Paperclip Runner Codex profile shows the same known model effort levels.
Its selected effort is saved with the run and sent to Codex for each turn.
The Storybook visual regression suite uses external PNG baselines instead of
committed screenshots:
@@ -145,6 +145,9 @@ function createTransportBackedNativeSessionBackend(
return new HarnessDriverBackend(
new CodexAppServerDriver({
...(input.provider.model ? { model: input.provider.model } : {}),
...(input.provider.kind === "codex" && "reasoningEffort" in input.provider && input.provider.reasoningEffort
? { reasoningEffort: input.provider.reasoningEffort }
: {}),
// Runnerd owns provider permissions for non-Codex facades. Their
// Codex-compatible surface must never open a second approval channel.
approvalPolicy:
@@ -120,6 +120,21 @@ describe("NativeExecutionInputV1", () => {
schema: "paperclip.native-execution-input.v4",
provider: { kind: "codex", approvalPolicy: "on-request" },
});
const withEffort = parseNativeExecutionInput({
...current,
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
provider: { kind: "codex", model: "gpt-6-astra", approvalPolicy: "on-request", reasoningEffort: "ultra" },
});
expect(withEffort.provider).toMatchObject({ kind: "codex", reasoningEffort: "ultra" });
expect(parseNativeExecutionInput(withEffort)).toEqual(withEffort);
expect(() => parseNativeExecutionInput({
...withEffort,
provider: { kind: "codex", model: "gpt-6-astra", approvalPolicy: "on-request", reasoningEffort: "impossible" },
})).toThrow("reasoningEffort");
expect(() => parseNativeExecutionInput({
...current,
provider: { kind: "codex", model: null, approvalPolicy: "on-request", reasoningEffort: "ultra" },
})).toThrow("input.provider");
expect(() => parseNativeExecutionInput({
...parsed,
schema: "paperclip.native-execution-input.v4",
@@ -140,6 +140,10 @@ export type NativeProviderConfigV4 =
profile: NativeAcpxProfileSnapshot;
};
export type NativeProviderConfigV5 =
| Exclude<NativeProviderConfigV4, { kind: "codex" }>
| (Extract<NativeProviderConfigV4, { kind: "codex" }> & { reasoningEffort?: string });
export interface NativeExecutionInputV1 {
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA_V1;
binding: {
@@ -217,8 +221,9 @@ export interface NativeCompletionSources {
criteria: Array<{ id: string; source: NativeCompletionSource }>;
}
export interface NativeExecutionInputV5 extends Omit<NativeExecutionInputV4, "schema"> {
export interface NativeExecutionInputV5 extends Omit<NativeExecutionInputV4, "schema" | "provider"> {
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA;
provider: NativeProviderConfigV5;
completionSources?: NativeCompletionSources;
}
@@ -447,7 +452,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
: provider.kind === "acpx"
? ["kind", "agent", "model", isV4 ? "permissionMode" : "permissionPolicy", "profile"]
: provider.kind === "codex" && isV4
? ["kind", "model", "approvalPolicy"]
? ["kind", "model", "approvalPolicy", ...(isV5 ? ["reasoningEffort"] : [])]
: provider.kind === "opencode" && isV4
? ["kind", "model", "permissionMode"]
: ["kind", "model"],
@@ -468,7 +473,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
if (provider.kind === "opencode" && (providerModel === null || !providerModel.includes("/"))) {
throw new NativeExecutionInputError("input.provider.model is required for opencode in provider/model form");
}
let parsedProvider: NativeProviderConfig;
let parsedProvider: NativeProviderConfig | NativeProviderConfigV5;
if (provider.kind === "claude_managed") {
if (providerModel === null) {
throw new NativeExecutionInputError("input.provider.model is required for claude_managed");
@@ -640,12 +645,18 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
if (isV4 && provider.approvalPolicy !== "never" && provider.approvalPolicy !== "on-request" && provider.approvalPolicy !== "untrusted") {
throw new NativeExecutionInputError("input.provider.approvalPolicy must be never, on-request, or untrusted");
}
if (isV5 && provider.reasoningEffort !== undefined && !["minimal", "low", "medium", "high", "xhigh", "max", "ultra"].includes(String(provider.reasoningEffort))) {
throw new NativeExecutionInputError("input.provider.reasoningEffort is unsupported");
}
parsedProvider = {
kind: "codex",
model: providerModel,
...(isV4
? { approvalPolicy: provider.approvalPolicy as NativeCodexApprovalPolicy }
: {}),
...(isV5 && provider.reasoningEffort !== undefined
? { reasoningEffort: text(provider.reasoningEffort, "input.provider.reasoningEffort") }
: {}),
};
}
if (!Array.isArray(contract.criteria) || contract.criteria.length === 0) {
@@ -761,6 +772,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
return {
...withPermissions,
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
provider: parsedProvider as NativeProviderConfigV5,
...(input.completionSources !== undefined ? { completionSources: parseCompletionSources(input.completionSources) } : {}),
};
}
@@ -930,6 +930,7 @@ export class CodexAppServerDriver implements HarnessDriver {
goalCapability: this.#goalCapability,
dynamicTools: this.#providerDynamicTools(),
skillInputs: this.#options.skillInputs,
reasoningEffort: this.#options.reasoningEffort,
dynamicToolHandler: this.#options.dynamicToolHandler,
completionFeedback: this.#options.completionFeedback,
});
@@ -1436,6 +1436,18 @@ describe("Codex app-server Codex driver", () => {
});
});
it("sends the selected reasoning effort with the Codex turn", async () => {
const transport = new FakeCodexTransport();
const session = await makeDriver([transport], { model: "gpt-6-astra", reasoningEffort: "ultra" }).openSession({
runId: "run-effort",
normalizedSessionId: "session-effort",
workingDirectory: TEST_WORKING_DIRECTORY,
});
await session.startTurn({ message: { role: "user", text: "Continue" } });
expect(transport.calls.find((call) => call.method === "turn/start")?.params).toMatchObject({ effort: "ultra" });
await session.close({ reason: "test complete" });
});
it("fails closed when the installed app-server does not confirm plan mode", async () => {
const transport = new FakeCodexTransport();
transport.confirmCollaborationMode = false;
@@ -17,6 +17,8 @@ export interface CodexAppServerDriverOptions {
taskEnvelope: CodexTaskEnvelope;
/** Explicit provider model selected by the persisted native execution. */
model?: string;
/** Per-run reasoning effort sent with each Codex turn, including resumed turns. */
reasoningEffort?: string;
approvalPolicy?: "never" | "on-request" | "untrusted";
baseInstructions?: string;
includeSkillInstructions?: boolean;
@@ -221,6 +221,7 @@ export class CodexHarnessSession
try {
response = await this.transport.request("turn/start", {
threadId: this.opened.threadId,
...(this.reasoningEffort ? { effort: this.reasoningEffort } : {}),
cwd: this.opened.context.workingDirectory,
permissions:
text(record(record(this.opened.context.sandbox).permissionProfile).id) || (requestedMode === "plan"
@@ -101,6 +101,7 @@ export class CodexSessionState {
readonly goalReasonCode: string | null;
readonly goalReason: string | null;
readonly skillInputs: NonNullable<CodexAppServerDriverOptions["skillInputs"]>;
readonly reasoningEffort: string | undefined;
readonly dynamicTools: readonly Readonly<Record<string, unknown>>[];
readonly completionFeedback: CodexAppServerDriverOptions["completionFeedback"];
readonly dynamicToolHandler: CodexAppServerDriverOptions["dynamicToolHandler"];
@@ -174,6 +175,7 @@ export class CodexSessionState {
goalReasonCode: string | null;
goalReason: string | null;
skillInputs?: CodexAppServerDriverOptions["skillInputs"];
reasoningEffort?: string;
dynamicTools: readonly Readonly<Record<string, unknown>>[];
completionFeedback?: CodexAppServerDriverOptions["completionFeedback"];
dynamicToolHandler?: CodexAppServerDriverOptions["dynamicToolHandler"];
@@ -198,6 +200,7 @@ export class CodexSessionState {
this.goalReasonCode = input.goalReasonCode;
this.goalReason = input.goalReason;
this.skillInputs = structuredClone(input.skillInputs ?? []);
this.reasoningEffort = input.reasoningEffort;
this.dynamicTools = input.dynamicTools;
this.dynamicToolHandler = input.dynamicToolHandler;
this.completionFeedback = input.completionFeedback;
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
assertAgentCoreProfileRecoveryBinding,
assertManagedProfileRecoveryBinding,
projectPaperclipRunnerTaskConfig,
resolvePaperclipRunnerNativeProviderInput,
} from "./native-runtime/provider-profile.js";
@@ -44,6 +45,45 @@ describe("Paperclip Runner native provider configuration", () => {
).toThrow("codexPermissionMode set to never");
});
it("projects Codex effort only when the selected model supports it", () => {
expect(resolvePaperclipRunnerNativeProviderInput({
backend: "codex_app_server",
adapterConfig: { provider: "codex", model: "gpt-6-astra", modelReasoningEffort: "ultra" },
})).toMatchObject({ provider: "codex", model: "gpt-6-astra", codexReasoningEffort: "ultra" });
expect(() => resolvePaperclipRunnerNativeProviderInput({
backend: "codex_app_server",
adapterConfig: { provider: "codex", model: "gpt-6-luna", modelReasoningEffort: "ultra" },
})).toThrow("reasoning effort is not supported");
});
it("uses only task model and effort overrides without changing the Runner provider", () => {
const base = { provider: "codex", model: "gpt-6-astra", modelReasoningEffort: "ultra", codexPermissionMode: "never" };
const projected = projectPaperclipRunnerTaskConfig("codex_app_server", base, {
provider: "opencode", model: "gpt-6-luna", managedProfileId: "other-profile",
});
expect(projected).toEqual({ provider: "codex", model: "gpt-6-luna", codexPermissionMode: "never" });
expect(resolvePaperclipRunnerNativeProviderInput({
backend: "codex_app_server", adapterConfig: projected,
})).toEqual({ provider: "codex", model: "gpt-6-luna", codexApprovalPolicy: "never" });
expect(projectPaperclipRunnerTaskConfig("codex_app_server", base, { modelReasoningEffort: "high" }))
.toMatchObject({ provider: "codex", model: "gpt-6-astra", modelReasoningEffort: "high" });
});
it("uses an OpenCode task model without changing the Runner provider", () => {
const projected = projectPaperclipRunnerTaskConfig(
"opencode_server",
{ provider: "opencode", model: "openrouter/qwen/qwen3-coder-next", opencodePermissionMode: "deny" },
{ provider: "codex", model: "openrouter/deepseek/deepseek-v4-flash-0731", modelReasoningEffort: "ultra" },
);
expect(projected).toEqual({
provider: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731", opencodePermissionMode: "deny",
});
expect(resolvePaperclipRunnerNativeProviderInput({ backend: "opencode_server", adapterConfig: projected }))
.toEqual({
provider: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731", opencodePermissionMode: "deny",
});
});
it("requires persisted Claude recovery to use the qualified identity and current profile secret", () => {
const stored = {
id: "00000000-0000-4000-8000-000000000001",
+9 -1
View File
@@ -253,6 +253,7 @@ import {
import {
assertAgentCoreProfileRecoveryBinding,
assertManagedProfileRecoveryBinding,
projectPaperclipRunnerTaskConfig,
resolvePaperclipRunnerNativeProviderInput,
} from "./native-runtime/provider-profile.js";
import {
@@ -23771,7 +23772,14 @@ export function heartbeatService(
: null,
...resolvePaperclipRunnerNativeProviderInput({
backend: nativeRuntimeResolution.profile.backend,
adapterConfig: agent.adapterConfig,
adapterConfig: nativeRuntimeResolution.profile.backend === "codex_app_server"
|| nativeRuntimeResolution.profile.backend === "opencode_server"
? projectPaperclipRunnerTaskConfig(
nativeRuntimeResolution.profile.backend,
agent.adapterConfig,
issueAssigneeOverrides?.adapterConfig,
)
: agent.adapterConfig,
managedProfile,
agentCoreProfile,
}),
@@ -18,11 +18,14 @@ describe("LCA-05 explicit native work mode", () => {
taskPrompt: text,
workspace: { id: "50000000-0000-4000-8000-000000000005", cwd: "/workspace", repoUrl: null, repoRef: null, branchName: null },
normalizedSessionId: null,
provider: "codex",
codexReasoningEffort: "high",
planningContext: workMode === "planning" ? { documentId: null, baseRevisionId: null, baseRevisionNumber: 0, markdown: "", sha256: "a".repeat(64), reviewContext: {} } : null,
completionContract: { id: "70000000-0000-4000-8000-000000000007", sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.run-result.v1", contract: { revision: "1", objective: "Deliver the requested work", criteria: [{ id: "output", requirement: "Deliver the requested work" }] } },
runtimeContext: nativeRuntimeContextFixture(),
});
expect(input.task.workMode).toBe(workMode);
expect(input.provider).toMatchObject({ kind: "codex", reasoningEffort: "high" });
expect(input.executionMode).toBe(workMode === "planning" ? "plan" : "default");
expect(input.task.title).toBe(text);
}
@@ -66,6 +66,7 @@ export function buildNativeExecutionInput(input: {
provider?: "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx";
acpxAgent?: NativeAcpxAgent;
codexApprovalPolicy?: NativeCodexApprovalPolicy;
codexReasoningEffort?: string;
opencodePermissionMode?: NativeOpenCodePermissionMode;
acpxPermissionMode?: NativeAcpxPermissionMode;
model?: string | null;
@@ -281,6 +282,7 @@ export function buildNativeExecutionInput(input: {
kind: "codex",
model: input.model ?? null,
approvalPolicy: input.codexApprovalPolicy ?? "never",
...(input.codexReasoningEffort ? { reasoningEffort: input.codexReasoningEffort } : {}),
},
completionContract: {
id: input.completionContract.id,
@@ -5,6 +5,10 @@ import {
resolvePaperclipRunnerPermissionMode,
type PaperclipRunnerProvider,
} from "@paperclipai/adapter-utils";
import {
codexLocalReasoningEffortsForModel,
isCodexLocalKnownModel,
} from "@paperclipai/adapter-codex-local";
import {
AGENTCORE_QUALIFIED_MODEL,
CLAUDE_MANAGED_QUALIFIED_MODEL,
@@ -63,6 +67,7 @@ export type PaperclipRunnerNativeProviderInput =
provider: "codex";
model: string | null;
codexApprovalPolicy: "never" | "on-request" | "untrusted";
codexReasoningEffort?: string;
}
| {
provider: "opencode";
@@ -138,6 +143,29 @@ function optionalString(value: unknown): string | null {
: null;
}
/** A task may change a local Runner model (and Codex effort), but not provider identity. */
export function projectPaperclipRunnerTaskConfig(
backend: "codex_app_server" | "opencode_server",
agentConfig: unknown,
taskOverrides: unknown,
): Record<string, unknown> {
const base = asRecord(agentConfig);
const task = asRecord(taskOverrides);
const config = { ...base };
const model = optionalString(task.model);
const effortKey = backend === "codex_app_server"
? ["modelReasoningEffort", "reasoningEffort", "effort"].find((key) => key in task)
: undefined;
if (backend === "codex_app_server" && ((model !== null && model !== optionalString(base.model)) || effortKey !== undefined)) {
delete config.modelReasoningEffort;
delete config.reasoningEffort;
delete config.effort;
}
if (model !== null) config.model = model;
if (effortKey !== undefined) config.modelReasoningEffort = task[effortKey];
return config;
}
function positiveNumberOrNull(
value: unknown,
code: string,
@@ -650,6 +678,18 @@ export function resolvePaperclipRunnerNativeProviderInput(input: {
},
};
}
const effort = config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort;
const allowedEfforts = ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
if (effort !== undefined && effort !== null && effort !== "" && (
typeof effort !== "string" || !allowedEfforts.includes(effort)
|| (profile.model && isCodexLocalKnownModel(profile.model)
&& !codexLocalReasoningEffortsForModel(profile.model).includes(effort as "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra"))
)) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_codex_effort_invalid",
"Paperclip Runner Codex reasoning effort is not supported for this model.",
);
}
return {
provider: "codex",
model: profile.model,
@@ -657,5 +697,6 @@ export function resolvePaperclipRunnerNativeProviderInput(input: {
"codex",
config.codexPermissionMode,
) as "never" | "on-request" | "untrusted",
...(typeof effort === "string" && effort ? { codexReasoningEffort: effort } : {}),
};
}
@@ -136,7 +136,8 @@ export function ComposerRunSettingsPicker({
const filteredModels = models.filter((item) =>
`${item.label} ${item.id}`.toLowerCase().includes(query.toLowerCase()));
const exactMatch = models.some((item) => item.id.toLowerCase() === query.toLowerCase());
const needsProvider = agent && ["opencode_local", "pi_local", "kimi_local"].includes(agent.adapterType);
const needsProvider = agent && (["opencode_local", "pi_local", "kimi_local"].includes(agent.adapterType)
|| (agent.adapterType === "paperclip_runner" && provider === "opencode"));
const manualValid = query.length > 0 && !/\s/.test(query)
&& (!needsProvider || /^[^/]+\/.+[^/]$/.test(query))
&& (provider !== "openrouter" || /^openrouter\/[^/]+\/.+[^/]$/.test(query));
@@ -12,6 +12,8 @@ const agent = (adapterType: Agent["adapterType"], provider?: string) => ({
describe("composer run settings", () => {
it("shows only effort levels known for the selected harness and model", () => {
expect(composerEfforts(agent("codex_local"), "gpt-6-astra", [])).toContain("ultra");
expect(composerEfforts(agent("paperclip_runner", "codex"), "gpt-6-astra", [])).toContain("ultra");
expect(composerEfforts(agent("paperclip_runner", "opencode"), "gpt-6-astra", [])).toEqual([]);
expect(composerEfforts(agent("codex_local"), "custom-private-model", [])).toEqual([]);
expect(composerEfforts(agent("opencode_local"), "openrouter/x/y", ["openrouter/x/y"])).toEqual([]);
expect(composerEfforts(agent("kimi_local"), "kimi-code/k3", ["kimi-code/k3"])).toEqual(["low", "high", "max"]);
@@ -32,5 +34,8 @@ describe("composer run settings", () => {
expect(readComposerRunSettings({ adapterConfig: { reasoningEffort: "xhigh" } }, "codex_local").effort).toBe("xhigh");
expect(mergeComposerRunSettings(previous, "codex_local", { model: "gpt-6-astra", effort: "ultra", fast: true }, true))
.toEqual({ adapterConfig: { model: "gpt-6-astra", modelReasoningEffort: "ultra", fastMode: true } });
const runner = mergeComposerRunSettings(null, "paperclip_runner", { model: "gpt-6-astra", effort: "ultra", fast: false });
expect(runner).toEqual({ adapterConfig: { model: "gpt-6-astra", modelReasoningEffort: "ultra" } });
expect(readComposerRunSettings(runner, "paperclip_runner").effort).toBe("ultra");
});
});
@@ -35,7 +35,7 @@ export function composerCatalogProvider(agent: Agent | undefined): string | unde
export function composerEfforts(agent: Agent | undefined, model: string, catalogIds: readonly string[]): readonly string[] {
if (!agent || !model) return [];
if (agent.adapterType === "codex_local") {
if (agent.adapterType === "codex_local" || (agent.adapterType === "paperclip_runner" && composerCatalogProvider(agent) === "codex")) {
return isCodexLocalKnownModel(model) ? codexLocalReasoningEffortsForModel(model) : [];
}
if (!catalogIds.includes(model)) return [];
@@ -53,7 +53,7 @@ export function readComposerRunSettings(overrides: IssueAssigneeAdapterOverrides
const config = overrides?.adapterConfig ?? {};
const effortKey = composerEffortKey(adapterType);
const effortValue = effortKey && (config[effortKey]
?? (adapterType === "codex_local" ? config.reasoningEffort ?? config.effort : undefined));
?? (adapterType === "codex_local" || adapterType === "paperclip_runner" ? config.reasoningEffort ?? config.effort : undefined));
return {
model: typeof config.model === "string" ? config.model : null,
effort: typeof effortValue === "string" ? effortValue : null,
@@ -62,7 +62,7 @@ export function readComposerRunSettings(overrides: IssueAssigneeAdapterOverrides
}
function composerEffortKey(adapterType: string | undefined): string | null {
if (adapterType === "codex_local") return "modelReasoningEffort";
if (adapterType === "codex_local" || adapterType === "paperclip_runner") return "modelReasoningEffort";
if (adapterType === "claude_local" || adapterType === "kimi_local") return "effort";
if (adapterType === "pi_local") return "thinking";
if (adapterType === "opencode_local") return "variant";
@@ -90,7 +90,7 @@ export function effortChoices(agent: ComposerAgent, model: string): readonly str
}
export function fastModeAvailable(agent: ComposerAgent, model: string): boolean {
return (agent.adapterType === "codex_local" || agent.id === "runner")
return agent.adapterType === "codex_local"
&& isCodexLocalKnownModel(model)
&& isCodexLocalFastModeSupported(model);
}
@@ -139,6 +139,10 @@ export const RunnerCodexProfile: Story = {
name: "16 · Runner · Codex profile",
args: { agentId: "runner", initialPanel: "models" },
};
export const RunnerCodexEffort: Story = {
name: "16b · Runner · Codex effort",
args: { agentId: "runner", initialPanel: "settings", initialModel: "gpt-6-astra", initialEffort: "high" },
};
export const GrokModelOnly: Story = {
name: "17 · Grok · model only",
args: { agentId: "grok", initialPanel: "settings" },
@@ -250,6 +254,16 @@ export const ProductionComposer: Story = {
},
};
export const ProductionRunnerCodexEffort: Story = {
name: "23a · App Runner picker with Codex effort",
render: () => <ComposerRunSettingsLiveStory agentId="runner" initialPanel="settings" initialModel="gpt-6-astra" initialEffort="high" />,
play: async ({ canvasElement }) => {
const screen = within(canvasElement.ownerDocument.body);
await expect(screen.getByRole("slider", { name: "Effort" })).toBeVisible();
await expect(screen.getByTestId("selected-effort")).toHaveTextContent("High");
},
};
export const ProductionMobileComposer: Story = {
name: "23b · App picker on mobile",
render: () => <ComposerRunSettingsLiveStory initialPanel="settings" mobile compact />,
@@ -262,6 +276,17 @@ export const ProductionMobileComposer: Story = {
},
};
export const ProductionMobileRunnerCodexEffort: Story = {
name: "23c · App Runner Codex effort on mobile",
render: () => <ComposerRunSettingsLiveStory agentId="runner" initialPanel="settings" initialModel="gpt-6-astra" mobile compact />,
globals: { viewport: { value: "mobile1", isRotated: false } },
play: async ({ canvasElement }) => {
const screen = within(canvasElement.ownerDocument.body);
await expect(screen.getByTestId("composer-mobile-dialog")).toBeVisible();
await expect(screen.getByRole("slider", { name: "Effort" })).toBeVisible();
},
};
export const IntermediateWidthPlan: Story = {
name: "24 · Narrow desktop with Plan and open picker",
args: { compact: true, initialMode: "planning", initialPanel: "settings" },