From 5506014e1f06a2457358caba824799650d9e860f Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 8 Oct 2026 14:38:10 -0500 Subject: [PATCH] Preserve Pi thinking and recovery alongside current provider modes --- packages/paperclip-runner/acpx-profiles.json | 6 +++--- packages/paperclip-runner/protocol/manifest.json | 2 +- .../runner-core/src/generated_acpx_profiles.rs | 4 ++-- .../src/backends/codex-acpx-native-backend.ts | 3 ++- .../src/cli/acpx-runtime-sidecar.ts | 6 +++++- .../src/contracts/harness-driver.ts | 1 + .../src/contracts/native-execution.test.ts | 12 ++++++------ .../src/contracts/native-execution.ts | 11 ++++++++++- .../src/drivers/acpx/codex-acpx-driver.ts | 4 ++++ .../src/drivers/acpx/codex-runtime-adapter.ts | 6 +++++- .../src/drivers/acpx/generated-profiles.ts | 6 +++--- .../src/drivers/acpx/profile-compatibility.ts | 2 +- .../src/drivers/acpx/recovery-identity.ts | 16 +++++++++++++++- .../src/drivers/acpx/runtime-host.ts | 6 ++++++ .../src/drivers/acpx/sidecar-protocol.ts | 2 ++ .../src/drivers/codex/codex-driver-values.ts | 3 +++ .../paperclip-runner/src/live/live-session.ts | 1 + .../src/live/runnerd-codex-transport.ts | 4 ++++ .../src/protocol/generated/schema-bundle.ts | 8 -------- .../native-execution-input.test.ts | 2 +- .../native-runtime/native-execution-input.ts | 2 ++ .../native-runtime/native-session-executor.ts | 2 ++ 22 files changed, 79 insertions(+), 30 deletions(-) diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index 79bab3ea9e9..c6b047e110e 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -14,12 +14,12 @@ "requiresProviderPolicy": false }, "pi": { - "agentProfileVersion": 1, + "agentProfileVersion": 13, "agentServerPackage": "pi-acp", "agentServerVersion": "0.0.33", "agentRuntimePackage": "@earendil-works/pi-coding-agent", - "agentRuntimeVersion": "0.84.2", - "commandDigest": "sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f", + "agentRuntimeVersion": "1.0.0", + "commandDigest": "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177", "requiresProviderPolicy": true }, "cursor": { diff --git a/packages/paperclip-runner/protocol/manifest.json b/packages/paperclip-runner/protocol/manifest.json index 2a58ad84068..7f9106e3d0b 100644 --- a/packages/paperclip-runner/protocol/manifest.json +++ b/packages/paperclip-runner/protocol/manifest.json @@ -80,7 +80,7 @@ { "path": "schemas/provider-descriptor.schema.json", "id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json", - "sha256": "b3d42a51be5a632c0404194f036c20e94b03097537f3fe031a908e61ccd29085" + "sha256": "c6618fe65517e01b191c970775a1677ddbd22c80afcffad627920e568dc6ff3e" }, { "path": "schemas/provider-event.schema.json", diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs b/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs index 1da99fcaad7..0830b3c3f67 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs @@ -27,9 +27,9 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option { agent_server_package: "pi-acp", agent_server_version: "0.0.33", agent_runtime_package: Some("@earendil-works/pi-coding-agent"), - agent_runtime_version: Some("0.84.2"), + agent_runtime_version: Some("1.0.0"), command_digest: - "sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f", + "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177", requires_provider_policy: true, }, "cursor" => AcpxReleaseProfile { diff --git a/packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts b/packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts index 88bd0941a41..a77e4100263 100644 --- a/packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts +++ b/packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts @@ -15,7 +15,7 @@ import { export interface CodexAcpxNativeSessionBackendOptions extends Omit< CodexAcpxDriverOptions, - "model" | "permissionMode" | "mode" | "systemInstructions" | "providerPolicy" | "runtimeContext" + "model" | "permissionMode" | "mode" | "piThinkingLevel" | "systemInstructions" | "providerPolicy" | "runtimeContext" > {} export type AcpxNativeSessionBackendOptions = @@ -75,6 +75,7 @@ export function createAcpxNativeSessionBackend( model: input.provider.model, permissionMode: input.provider.permissionMode ?? "approve-reads", mode: input.provider.mode, + piThinkingLevel: input.provider.piThinkingLevel, systemInstructions, runtimeContext: "runtimeContext" in input ? input.runtimeContext : null, providerPolicy: { readOnly: "executionMode" in input && input.executionMode === "plan" }, diff --git a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts index a5af6b9ea0c..a2610cef773 100644 --- a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts +++ b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { resolvePiThinkingLevel } from "../drivers/acpx/pi-thinking.js"; import { parseProviderMode } from "../contracts/provider-mode.js"; import { acpxProfileActivity, type AcpxActivityAdapter, type AcpxToolEvidence } from "../drivers/acpx/profile-activity.js"; import { createHash } from "node:crypto"; @@ -294,6 +295,7 @@ async function dispatch( model: params.model, permissionMode: params.permissionMode, mode: params.mode, + piThinkingLevel: params.piThinkingLevel, providerPolicy: params.providerPolicy, systemInstructions: params.systemInstructions, runtimeContext: params.runtimeContext, @@ -1200,7 +1202,7 @@ function safeOutput(value: unknown): Record { function parseOpenParams( value: Record, ): AcpxSidecarOpenParams { - const fields = new Set(["runtimeDirectory", "normalizedSessionId", "workingDirectory", "agent", "model", "permissionMode", "mode", "permissionModePinned", "providerPolicy", "systemInstructions", "runtimeContext", "tools", "providerSessionKey", "expectedIdentity"]); + const fields = new Set(["runtimeDirectory", "normalizedSessionId", "workingDirectory", "agent", "model", "permissionMode", "mode", "piThinkingLevel", "permissionModePinned", "providerPolicy", "systemInstructions", "runtimeContext", "tools", "providerSessionKey", "expectedIdentity"]); if (Object.keys(value).some(key => !fields.has(key))) throw new Error("ACPX open parameters include an unsupported field"); const agent = requireQualifiedAgent(value.agent); const model = requiredText(value.model, "model"); @@ -1226,6 +1228,7 @@ function parseOpenParams( model, permissionMode: requiredPermissionMode(value.permissionMode), ...(value.mode === undefined ? {} : { mode: parseProviderMode(value.mode) }), + ...(piThinkingLevel ? { piThinkingLevel } : {}), permissionModePinned: value.permissionModePinned === true, ...(value.providerPolicy == null ? {} : { providerPolicy: parseProviderPolicy(value.providerPolicy) }), systemInstructions: boundedText( @@ -1296,6 +1299,7 @@ function parseExpectedIdentity(value: unknown): AcpxExpectedSessionIdentity { ? {} : { permissionMode: requiredPermissionMode(input.permissionMode) }), ...(input.mode === undefined ? {} : { mode: parseProviderMode(input.mode) }), + ...(input.piThinkingLevel === undefined ? {} : { piThinkingLevel: resolvePiThinkingLevel("pi", input.piThinkingLevel) }), providerLifetimeFenceCandidates: requiredFenceCandidates( input.providerLifetimeFenceCandidates, ), diff --git a/packages/paperclip-runner/src/contracts/harness-driver.ts b/packages/paperclip-runner/src/contracts/harness-driver.ts index ee04ce4352b..410496afce5 100644 --- a/packages/paperclip-runner/src/contracts/harness-driver.ts +++ b/packages/paperclip-runner/src/contracts/harness-driver.ts @@ -456,6 +456,7 @@ export interface AcpxSessionIdentity { permissionMode?: "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all"; /** Effective provider mode identifier, bound to the session identity. */ mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; providerLifetimeFenceCandidates: readonly [number, number, number]; } diff --git a/packages/paperclip-runner/src/contracts/native-execution.test.ts b/packages/paperclip-runner/src/contracts/native-execution.test.ts index e6d8eb4fc9d..30cac176fda 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.test.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.test.ts @@ -535,25 +535,25 @@ describe("native task context ownership", () => { } it.each(["off", "low", "high", "max"])("requires exact Pi13 thinking level %s in native input", piThinkingLevel => { - const { qualificationModel, reportedModelId: _reported, permissionPolicy: _permission, modelPolicy: _policy, qualificationStatus: _status, ...profile } = QUALIFIED_ACPX_PROFILES.pi; + const { permissionPolicy: _permission, qualificationStatus: _status, ...profile } = QUALIFIED_ACPX_PROFILES.pi; + const qualificationModel = "openrouter/test-provider-model"; const value = { ...currentInput(), session: { ...currentInput().session, driverKind: "acpx_runtime" }, provider: { kind: "acpx", agent: "pi", model: qualificationModel, permissionMode: "approve-all", piThinkingLevel, profile } }; expect(parseNativeExecutionInput(value).provider).toEqual(value.provider); for (const mode of [undefined, null, "medium", "minimal", "xhigh", "", { toString: () => "low" }]) expect(() => parseNativeExecutionInput({ ...value, provider: { ...value.provider, piThinkingLevel: mode } })).toThrow(/piThinkingLevel/); expect(() => parseNativeExecutionInput({ ...value, provider: { ...value.provider, agent: "codex" } })).toThrow(/piThinkingLevel/); }); - it.each(["agent", "plan", "ask"])("round-trips Cursor mode %s through the closed execution contract", cursorMode => { + it.each(["agent", "plan", "ask"])("round-trips Cursor mode %s through the closed execution contract", mode => { const { qualificationModel: _model, reportedModelId: _reported, permissionPolicy: _permission, modelPolicy: _policy, qualificationStatus: _status, ...profile } = QUALIFIED_ACPX_PROFILES.cursor; const value = { ...currentInput(), session: { ...currentInput().session, driverKind: "acpx_runtime" }, - provider: { kind: "acpx", agent: "cursor", model: "explicit-model", permissionMode: "deny-all", cursorMode, profile } }; + provider: { kind: "acpx", agent: "cursor", model: "explicit-model", permissionMode: "deny-all", mode, profile } }; const result = parseNativeExecutionInput(value); expect(result.provider).toEqual(value.provider); expect(parseNativeExecutionInput(result)).toEqual(result); - for (const mode of [null, "", "PLAN", "auto", true, { toString: () => "plan" }]) { - expect(() => parseNativeExecutionInput({ ...value, provider: { ...value.provider, cursorMode: mode } })).toThrow("cursorMode"); + for (const mode of [null, "", true, { toString: () => "plan" }]) { + expect(() => parseNativeExecutionInput({ ...value, provider: { ...value.provider, mode: mode } })).toThrow("mode"); } - expect(() => parseNativeExecutionInput({ ...value, provider: { ...value.provider, agent: "copilot" } })).toThrow("cursorMode"); }); it.each([ diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index 1e1e1acf6db..bc8d373d3b0 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.ts @@ -130,6 +130,7 @@ export type NativeProviderConfig = model: string; permissionMode?: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; /** Present only in persisted v1-v3 inputs. */ permissionPolicy?: "interactive"; profile: NativeAcpxProfileSnapshot; @@ -145,6 +146,7 @@ export type NativeProviderConfigV4 = model: string; permissionMode: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; profile: NativeAcpxProfileSnapshot; }; @@ -502,7 +504,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput : provider.kind === "aws_agentcore" ? ["kind", "model", "agentCoreProfile", "maxEstimatedSessionCostUsd", "invocationLimits"] : provider.kind === "acpx" - ? ["kind", "agent", "model", isV4 ? "permissionMode" : "permissionPolicy", "profile", ...(isV4 ? ["mode"] : [])] + ? ["kind", "agent", "model", isV4 ? "permissionMode" : "permissionPolicy", "profile", "piThinkingLevel", ...(isV4 ? ["mode"] : [])] : provider.kind === "codex" && isV4 ? ["kind", "model", "approvalPolicy", ...(isV5 ? ["reasoningEffort"] : [])] : provider.kind === "opencode" && isV4 @@ -622,6 +624,12 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput invocationLimits: { maxIterations, maxOutputTokens, timeoutSeconds }, }; } else if (provider.kind === "acpx") { + if (provider.piThinkingLevel !== undefined && (provider.agent !== "pi" || (typeof provider.piThinkingLevel !== "string" || !["off", "low", "high", "max"].includes(provider.piThinkingLevel)))) { + throw new NativeExecutionInputError("input.provider.piThinkingLevel must be off, low, high, or max and is supported only for Pi"); + } + if (provider.agent === "pi" && record(provider.profile, "input.provider.profile").agentProfileVersion === 13 && provider.piThinkingLevel === undefined) { + throw new NativeExecutionInputError("input.provider.piThinkingLevel is required for Pi profile 13"); + } if (provider.mode !== undefined && !isProviderMode(provider.mode)) { throw new NativeExecutionInputError("input.provider.mode must be a bounded nonempty provider mode identifier"); } @@ -673,6 +681,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput ? { permissionMode: provider.permissionMode as NativeAcpxPermissionMode } : { permissionPolicy: "interactive" as const }), ...(provider.mode === undefined ? {} : { mode: provider.mode as string }), + ...(provider.piThinkingLevel === undefined ? {} : { piThinkingLevel: provider.piThinkingLevel as "off" | "low" | "high" | "max" }), profile: { driverKind: "acpx_runtime", protocolVersion: 1, diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts index c43d5b8ad84..1625b6a38d1 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts @@ -146,6 +146,7 @@ export interface CodexAcpxDriverOptions { model: string; permissionMode?: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; providerPolicy?: { readOnly: boolean }; runtimeContext?: OpenAcpxRuntimeHostOptions["runtimeContext"]; systemInstructions?: string; @@ -460,6 +461,7 @@ export class CodexAcpxDriver implements HarnessDriver { model: this.#options.model, permissionMode: this.#options.permissionMode ?? "approve-all", mode: this.#options.mode, + piThinkingLevel: this.#options.piThinkingLevel, providerPolicy: this.#options.providerPolicy, runtimeContext: this.#options.runtimeContext, systemInstructions: this.#options.systemInstructions, @@ -1285,6 +1287,7 @@ class CodexAcpxSession implements HarnessSession { effectiveModel: identity.effectiveModel, permissionMode: identity.permissionMode, ...(identity.mode === undefined ? {} : { mode: identity.mode }), + ...(identity.piThinkingLevel === undefined ? {} : { piThinkingLevel: identity.piThinkingLevel }), providerLifetimeFenceCandidates: identity.providerLifetimeFenceCandidates, }, @@ -2148,6 +2151,7 @@ function validateRecoverySnapshot(snapshot: PersistedHarnessSession): void { identity.permissionMode, )) || (identity.mode !== undefined && !isProviderMode(identity.mode)) || + (identity.piThinkingLevel !== undefined && !["off", "low", "high", "max"].includes(identity.piThinkingLevel)) || !validProviderLifetimeFenceCandidates( identity.providerLifetimeFenceCandidates, ) diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index 580c13002af..e150acd4ba0 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -302,6 +302,10 @@ export async function openQualifiedAcpxRuntime( ); }; const commandLaunches = { count: 0, refreshConsumedCommand: options.refreshConsumedCommand }; + const selectedPiThinkingLevel = resolvePiThinkingLevel(options.profile.agent, options.piThinkingLevel); + const piThinking = selectedPiThinkingLevel + ? createPiThinkingAdmission(selectedPiThinkingLevel, { restoring: options.restoringSession }) + : null; const modeBinding = createAcpxModeBinding(options.profile.agent, options.mode); const cursorInstructions = options.profile.agent === "cursor" ? createCursorInstructionAdmission(options.systemInstructions) @@ -594,7 +598,7 @@ export async function openQualifiedAcpxRuntime( return runtimePort( runtime, handle, - { ...requireIdentity(handle), ...(modeBinding ? { mode: modeBinding.selectedMode } : {}) }, + { ...requireIdentity(handle), ...(modeBinding ? { mode: modeBinding.selectedMode } : {}), ...(selectedPiThinkingLevel ? { piThinkingLevel: selectedPiThinkingLevel } : {}) }, baseStore, children, runtimeCloseTimeoutMs, diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index a7e34cf1411..f11071c139d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts @@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = { "protocolVersion": 1, "acpxVersion": "0.13.1", "agent": "pi", - "agentProfileVersion": 1, + "agentProfileVersion": 13, "agentServerPackage": "pi-acp", "agentServerVersion": "0.0.33", "agentRuntimePackage": "@earendil-works/pi-coding-agent", - "agentRuntimeVersion": "0.84.2", - "commandDigest": "sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f", + "agentRuntimeVersion": "1.0.0", + "commandDigest": "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177", "permissionPolicy": "interactive" }, "cursor": { diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts index 86426040906..4f8b8775667 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts @@ -2,7 +2,7 @@ * Exact current profile/package/digest admission is checked separately. */ export type AcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14; const historicalVersions: Readonly> = { - pi: [1, 2, 3, 4, 5], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5], + pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5], grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5], cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], }; diff --git a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts index 4e2faa18f95..1ab92661813 100644 --- a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts @@ -1,3 +1,4 @@ +import { resolvePiThinkingLevel } from "./pi-thinking.js"; import { parseProviderMode } from "../../contracts/provider-mode.js"; import { createHash } from "node:crypto"; import { realpath, stat } from "node:fs/promises"; @@ -21,6 +22,7 @@ export interface AcpxRecoveryBinding { effectiveModel: string; permissionMode: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; profileSessionKey: string; } @@ -36,6 +38,7 @@ export interface AcpxIdentityRecord { effectiveModel: string; permissionMode: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; providerLifetimeFenceCandidates: readonly [number, number, number]; } @@ -47,10 +50,12 @@ export async function createAcpxRecoveryBinding(input: { requestedModel: string; permissionMode: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; providerPolicy?: { readOnly: boolean; readRoots?: readonly string[]; protectedPaths?: readonly string[] }; }): Promise { validateIdentity(input.normalizedSessionId, "normalized session"); const mode = parseProviderMode(input.mode); + const piThinkingLevel = resolvePiThinkingLevel(input.profile.agent, input.piThinkingLevel); if (input.providerPolicy !== undefined && typeof input.providerPolicy.readOnly !== "boolean") throw new Error("ACPX recovery requires a valid task execution policy"); if (input.requestedModel !== input.profile.qualificationModel) { throw new Error("ACPX recovery requested model does not match its admitted profile"); @@ -95,6 +100,7 @@ export async function createAcpxRecoveryBinding(input: { profileDigest, permissionMode: input.permissionMode, ...(mode ? { mode } : {}), + ...(piThinkingLevel ? { piThinkingLevel } : {}), }), ).replace("sha256:", "paperclip-"); return { @@ -108,6 +114,7 @@ export async function createAcpxRecoveryBinding(input: { effectiveModel: input.requestedModel, permissionMode: input.permissionMode, ...(mode ? { mode } : {}), + ...(piThinkingLevel ? { piThinkingLevel } : {}), profileSessionKey, }; } @@ -129,6 +136,7 @@ export function createAcpxIdentityRecord( effectiveModel: binding.effectiveModel, permissionMode: binding.permissionMode, ...(binding.mode ? { mode: binding.mode } : {}), + ...(binding.piThinkingLevel ? { piThinkingLevel: binding.piThinkingLevel } : {}), providerLifetimeFenceCandidates: Object.freeze([ ...expected.providerLifetimeFenceCandidates, ]) as readonly [number, number, number], @@ -155,6 +163,7 @@ export function acpxProviderSessionIdentity( effectiveModel: record.effectiveModel, permissionMode: record.permissionMode, ...(record.mode ? { mode: record.mode } : {}), + ...(record.piThinkingLevel ? { piThinkingLevel: record.piThinkingLevel } : {}), providerLifetimeFenceCandidates: record.providerLifetimeFenceCandidates, }; verifyExpectedAcpxIdentity(identity, binding, record); @@ -180,7 +189,8 @@ export function verifyExpectedAcpxIdentity( expected.requestedModel !== binding.requestedModel || expected.effectiveModel !== binding.effectiveModel || expected.permissionMode !== binding.permissionMode || - expected.mode !== binding.mode + expected.mode !== binding.mode || + expected.piThinkingLevel !== binding.piThinkingLevel ) { throw new Error( "ACPX recovery identity conflicts with the immutable session configuration", @@ -200,6 +210,7 @@ export function verifyExpectedAcpxIdentity( record.effectiveModel !== binding.effectiveModel || record.permissionMode !== binding.permissionMode || record.mode !== binding.mode || + record.piThinkingLevel !== binding.piThinkingLevel || !sameFenceCandidates( record.providerLifetimeFenceCandidates, expected.providerLifetimeFenceCandidates, @@ -225,6 +236,7 @@ function parsePersistedRecord(value: unknown): AcpxIdentityRecord { "effectiveModel", "permissionMode", "mode", + "piThinkingLevel", "providerLifetimeFenceCandidates", ]); return validatedRecord(record); @@ -252,6 +264,7 @@ function validatedRecord(value: Record): AcpxIdentityRecord { throw new Error("ACPX identity permission mode is invalid"); } if (value.mode !== undefined) parseProviderMode(value.mode); + if (value.piThinkingLevel !== undefined) resolvePiThinkingLevel("pi", value.piThinkingLevel); validateFenceCandidates(value.providerLifetimeFenceCandidates); return value as unknown as AcpxIdentityRecord; } @@ -281,6 +294,7 @@ function validateExpected(expected: AcpxExpectedSessionIdentity): void { throw new Error("Expected ACPX permission mode is invalid"); } if (expected.mode !== undefined) parseProviderMode(expected.mode); + if (expected.piThinkingLevel !== undefined) resolvePiThinkingLevel("pi", expected.piThinkingLevel); validateFenceCandidates(expected.providerLifetimeFenceCandidates); } diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index 35a48501975..10bceac505f 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -81,6 +81,7 @@ const ACPX_ADMISSION_CLEANUP_RESCHEDULE_MS = 1_000; export interface AcpxRuntimePortIdentity { mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; acpxRecordId: string; backendSessionId: string; agentSessionId: string; @@ -160,6 +161,7 @@ export interface AcpxRuntimePortOpenOptions { providerSessionKey: string; permissionMode: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; permissionPolicy: ReturnType; launchEnvironment: Readonly; /** Kernel credential-home quorum inherited by the provider sentinel. */ @@ -234,6 +236,7 @@ export interface OpenAcpxRuntimeHostOptions { model: string; permissionMode: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; systemInstructions?: string; runtimeContext?: NativeRuntimeContextSnapshot | null; environment?: NodeJS.ProcessEnv; @@ -344,6 +347,7 @@ export class AcpxRuntimeHost { requestedModel: options.model, permissionMode: options.permissionMode, mode: resolveAcpxProviderMode(options.agent, options.mode), + piThinkingLevel: resolvePiThinkingLevel(options.agent, options.piThinkingLevel), ...(["cursor", "copilot", "pi"].includes(options.agent) && options.providerPolicy !== undefined ? { providerPolicy: options.providerPolicy } : {}), }), @@ -565,6 +569,7 @@ export class AcpxRuntimeHost { restoringSession: options.expectedIdentity !== undefined, permissionMode: binding.permissionMode, mode: binding.mode, + piThinkingLevel: binding.piThinkingLevel, permissionPolicy: acpxRuntimePermissionPolicy( binding.permissionMode, ), @@ -622,6 +627,7 @@ export class AcpxRuntimeHost { ), dependencies.retainAdmissionCleanup, ); + if (runtimeIdentity.piThinkingLevel !== binding.piThinkingLevel) throw new Error("ACPX runtime Pi thinking level conflicts with session binding"); if (runtimeIdentity.mode !== binding.mode) { throw new Error("ACPX runtime Provider mode does not match the admitted session configuration"); } diff --git a/packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts b/packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts index 6e457d35e2e..d9ce539a9f6 100644 --- a/packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts +++ b/packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts @@ -46,6 +46,7 @@ export interface AcpxSidecarOpenParams { model: string; permissionMode: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; permissionModePinned: boolean; providerPolicy?: { readOnly: boolean }; systemInstructions: string; @@ -67,6 +68,7 @@ export interface AcpxExpectedSessionIdentity { effectiveModel: string; permissionMode?: NativeAcpxPermissionMode; mode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; providerLifetimeFenceCandidates: readonly [number, number, number]; } diff --git a/packages/paperclip-runner/src/drivers/codex/codex-driver-values.ts b/packages/paperclip-runner/src/drivers/codex/codex-driver-values.ts index fe52e695aa0..c6644732cb7 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-driver-values.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-driver-values.ts @@ -1,3 +1,4 @@ +import { resolvePiThinkingLevel } from "../acpx/pi-thinking.js"; import { isProviderMode } from "../../contracts/provider-mode.js"; import type { PersistedHarnessProviderIdentity } from "../../contracts/harness-driver.js"; import type { NativeUserMessage } from "../../contracts/types.js"; @@ -79,6 +80,7 @@ export function parseProviderIdentity( "ACPX provider identity contains an invalid permission mode", ); } + const piThinkingLevel = identity.piThinkingLevel === undefined ? undefined : resolvePiThinkingLevel("pi", identity.piThinkingLevel); const mode = identity.mode; if (mode !== undefined && !isProviderMode(mode)) { throw new Error("ACPX provider identity contains an invalid provider mode"); @@ -109,6 +111,7 @@ export function parseProviderIdentity( effectiveModel: identity.effectiveModel as string, ...(permissionMode === undefined ? {} : { permissionMode }), ...(mode === undefined ? {} : { mode }), + ...(piThinkingLevel === undefined ? {} : { piThinkingLevel }), providerLifetimeFenceCandidates: fenceCandidates as [ number, number, diff --git a/packages/paperclip-runner/src/live/live-session.ts b/packages/paperclip-runner/src/live/live-session.ts index 82cc813c2b3..dc610716c05 100644 --- a/packages/paperclip-runner/src/live/live-session.ts +++ b/packages/paperclip-runner/src/live/live-session.ts @@ -1,5 +1,6 @@ import { normalizeProviderNotice } from "../drivers/provider-notices.js"; import { liveRunResultFeedback } from "./run-result-feedback.js"; +import { resolvePiThinkingLevel } from "../drivers/acpx/pi-thinking.js"; import { createHash, randomUUID } from "node:crypto"; import { resolve } from "node:path"; diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 03790b05d76..f9633e9bd7d 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -1,3 +1,4 @@ +import { admittedPiThinkingLevel, resolvePiThinkingLevel } from "../drivers/acpx/pi-thinking.js"; import { configuredEnvironment } from "../configured-environment.js"; import { resolveAcpxProviderMode } from "../drivers/acpx/provider-mode.js"; import { isAcpxCanonicalInputMethod } from "../drivers/acpx/profile-extensions.js"; @@ -1162,6 +1163,7 @@ export interface CapabilityRunnerdCodexTransportOptions { acpxCandidateProfile?: "pi" | "cursor" | "copilot"; acpxPermissionMode?: NativeAcpxPermissionMode; acpxMode?: string; + piThinkingLevel?: "off" | "low" | "high" | "max"; acpxPermissionModePinned?: boolean; acpxSidecarPath?: string; /** SHA-256 verified by the provider-pack authority before runner startup. */ @@ -3526,6 +3528,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { readonly #bridgedRuntimeInputs = new Map(); constructor(readonly options: CapabilityRunnerdCodexTransportOptions) { + resolvePiThinkingLevel(options.provider === "acpx" ? options.acpxAgent ?? "codex" : "", options.piThinkingLevel); if (options.acpxMode !== undefined && options.provider !== "acpx") { throw new Error("acpxMode requires the ACPX provider"); } @@ -4752,6 +4755,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { instructions: baseInstructions, providerPolicy: { readOnly: params.permissions === "paperclip-runner-workspace-read-only" }, ...(selectedAcpxMode === undefined ? {} : { mode: selectedAcpxMode }), + ...(acpxProfile!.agent === "pi" ? { piThinkingLevel: this.options.piThinkingLevel } : {}), permissionMode: resolveRunnerdAcpxPermissionMode( this.options.acpxPermissionMode, ), diff --git a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts index e1cced30e7d..2cec5d27116 100644 --- a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts +++ b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts @@ -1168,14 +1168,6 @@ export const providerDescriptorSchema = { ], "maxLength": 240 }, - "piThinkingLevel": { - "enum": [ - "off", - "low", - "high", - "max" - ] - }, "turnControls": { "$ref": "#/$defs/turnControls" } diff --git a/server/src/services/native-runtime/native-execution-input.test.ts b/server/src/services/native-runtime/native-execution-input.test.ts index 987693fc283..8abc557e836 100644 --- a/server/src/services/native-runtime/native-execution-input.test.ts +++ b/server/src/services/native-runtime/native-execution-input.test.ts @@ -602,7 +602,7 @@ describe("Cursor mode native execution projection", () => { } it.each([undefined, "agent", "plan", "ask"] as const)("preserves mode %s independently of permissions and task planning", mode => { const result = buildNativeExecutionInput(fixture(mode)); - expect(result.provider).toMatchObject({ kind: "acpx", agent: "cursor", cursorMode: mode ?? "agent", permissionMode: "deny-all" }); + expect(result.provider).toMatchObject({ kind: "acpx", agent: "cursor", mode: mode ?? "agent", permissionMode: "deny-all" }); expect(result.executionMode).toBe("default"); expect(result.task.workMode).toBe("standard"); }); diff --git a/server/src/services/native-runtime/native-execution-input.ts b/server/src/services/native-runtime/native-execution-input.ts index f6b0ca859fd..280acc18360 100644 --- a/server/src/services/native-runtime/native-execution-input.ts +++ b/server/src/services/native-runtime/native-execution-input.ts @@ -114,6 +114,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat throw new Error("native_execution_input_invalid: issue work mode must be standard, planning, or ask"); } const mode = resolvePaperclipRunnerCursorMode(input.provider, input.acpxAgent, input.acpxSessionMode); + const piThinkingLevel = resolvePaperclipRunnerPiThinkingLevel(input.provider, input.acpxAgent, input.piThinkingLevel); const executionMode = input.executionMode ?? (input.issue.workMode === "planning" ? "plan" : "default"); const acpxProfile = input.provider === "acpx" @@ -277,6 +278,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat model: input.model, permissionMode: input.acpxPermissionMode ?? "approve-all", ...(mode === undefined ? {} : { mode }), + ...(piThinkingLevel === undefined ? {} : { piThinkingLevel }), profile: { driverKind: acpxProfile!.driverKind, protocolVersion: acpxProfile!.protocolVersion, diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index d1d33d35445..eb18d552872 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -5624,6 +5624,7 @@ export function providerSessionIdentityFromDurableProviderState(input: { identity.effectiveModel !== expectedModel || identity.permissionMode !== input.execution.provider.permissionMode || !acpxRecoveryModeMatches(input.execution.provider, descriptor.mode, identity.mode) || + !acpxRecoveryPiThinkingMatches(input.execution.provider, descriptor.piThinkingLevel, identity.piThinkingLevel) || !["approve-all", "approve-paperclip", "approve-reads", "deny-all"].includes( String(identity.permissionMode), ) || @@ -13035,6 +13036,7 @@ async function createRunnerdBackendWithinSessionClaim( : resolveAcpxQualification(input.execution.provider, process.env), acpxPermissionMode: input.execution.provider.permissionMode, acpxMode: input.execution.provider.mode, + piThinkingLevel: input.execution.provider.piThinkingLevel, acpxPermissionModePinned: input.execution.schema === "paperclip.native-execution-input.v4" || input.execution.schema === "paperclip.native-execution-input.v5",