fix(runner): restore provider plan negotiation

This commit is contained in:
Dotta 2026-09-03 06:16:44 -05:00
parent da949fe78f
commit db1a18d5ab
2 changed files with 75 additions and 3 deletions

View File

@ -92,6 +92,10 @@ function createTransportBackedNativeSessionBackend(
): NativeSessionBackend {
const driverIdentity = transportDriverIdentity(input);
const isCodex = input.provider.kind === "codex";
const supportsCollaborativePlanning =
isCodex ||
input.provider.kind === "opencode" ||
input.provider.kind === "acpx";
if (
input.provider.kind === "codex"
&& input.provider.approvalPolicy !== undefined
@ -113,14 +117,18 @@ function createTransportBackedNativeSessionBackend(
baseInstructions: nativeSystemInstructions(input),
includeSkillInstructions: isCodex && "runtimeContext" in input,
requestedCollaborationMode:
isCodex && "executionMode" in input ? input.executionMode : "default",
supportsCollaborativePlanning && "executionMode" in input
? input.executionMode
: "default",
taskEnvelope: createCodexTaskEnvelope({
objective: input.completionContract.contract.objective,
contractRevision: input.completionContract.contract.revision,
criteria: input.completionContract.contract.criteria,
constraints: [
"Work only inside the supplied working directory.",
...(isCodex && "executionMode" in input && input.executionMode === "plan"
...(supportsCollaborativePlanning &&
"executionMode" in input &&
input.executionMode === "plan"
? [
"Use native plan collaboration mode and do not modify workspace files.",
"Treat the supplied Paperclip planning context as the canonical pinned base revision.",
@ -143,7 +151,9 @@ function createTransportBackedNativeSessionBackend(
capabilities: isCodex
? {}
: { steering: false, goals: false, threadLineage: false },
collaborationModes: isCodex ? ["default", "plan"] : ["default"],
collaborationModes: supportsCollaborativePlanning
? ["default", "plan"]
: ["default"],
requireProviderSessionIdentity: options.transportFactory !== undefined,
}));
}

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import type { NativeExecutionInput } from "../contracts/native-execution.js";
import { FakeCodexTransport } from "../drivers/codex/codex-app-server-driver.test-support.js";
import { createNativeSessionBackend } from "../index.js";
import { createCodexNativeSessionBackend } from "./codex-native-backend.js";
@ -126,6 +127,23 @@ function opencodeExecution(): NativeExecutionInput {
};
}
function planningExecution(input: NativeExecutionInput): NativeExecutionInput {
return {
...input,
schema: "paperclip.native-execution-input.v2",
task: { ...input.task, workMode: "planning" },
executionMode: "plan",
planningContext: {
documentId: null,
baseRevisionId: null,
baseRevisionNumber: 0,
markdown: "",
sha256: "empty-plan",
reviewContext: {},
},
};
}
function managedExecution(
kind: "claude_managed" | "aws_agentcore",
): NativeExecutionInput {
@ -251,10 +269,53 @@ describe("native backend factory", () => {
resume: true,
interruption: true,
dynamicTools: true,
collaborationModes: ["default", "plan"],
},
});
});
it.each([
["OpenCode", opencodeExecution()],
["ACPX Codex", acpxExecution("codex")],
["ACPX Claude", acpxExecution("claude")],
])(
"opens %s planning runs through the runner-managed plan contract",
async (_label, input) => {
const transport = new FakeCodexTransport();
const backend = createNativeSessionBackend(planningExecution(input), {
codexTransportFactory: () => transport,
});
await expect(backend.descriptor()).resolves.toMatchObject({
capabilities: { collaborationModes: ["default", "plan"] },
});
const session = await backend.openSession({
identity: {
runId: "run",
sessionId: "session",
companyId: "company",
issueId: "issue",
agentId: "agent",
},
workingDirectory: process.cwd(),
});
await expect(
session.startTurn({
message: { role: "user", text: "Author a plan." },
requestedCollaborationMode: "plan",
}),
).resolves.toMatchObject({ effectiveCollaborationMode: "plan" });
expect(
transport.calls.find((call) => call.method === "thread/start")?.params,
).toMatchObject({ permissions: "paperclip-runner-workspace-read-only" });
expect(
transport.calls.find((call) => call.method === "turn/start")?.params,
).toMatchObject({ collaborationMode: { mode: "plan" } });
await session.close({ reason: "test complete" });
},
);
it.each([
[
"claude_managed" as const,
@ -337,6 +398,7 @@ describe("native backend factory", () => {
resume: true,
interruption: true,
dynamicTools: true,
collaborationModes: ["default", "plan"],
},
});
},