mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
Merge reviewed Claude setup and prepared session corrections
Co-Authored-By: Paperclip <noreply@paperclip.ing> * codex/claude-runner-default: fix(ui): save the configuration admitted during agent setup fix(runner): preserve prepared turns for v7 native input
This commit is contained in:
commit
bd74bf5059
5 files changed
+92
-28
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { createCodexTaskEnvelope } from "../contracts/codex.js";
|
||||
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7 } from "../contracts/native-execution.js";
|
||||
import type { NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import type { PersistedHarnessSession } from "../contracts/harness-driver.js";
|
||||
import type {
|
||||
@@ -115,7 +115,8 @@ function createTransportBackedNativeSessionBackend(
|
||||
}
|
||||
const driverIdentity = transportDriverIdentity(input);
|
||||
const isCodex = input.provider.kind === "codex";
|
||||
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
|
||||
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7;
|
||||
const supportsCollaborativePlanning =
|
||||
isCodex ||
|
||||
input.provider.kind === "opencode" ||
|
||||
|
||||
@@ -4,13 +4,13 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, nativeRuntimePromptDigest, canonicalNativeRuntimeContextDigest, composeNativeSystemInstructions } from "../contracts/runtime-context.js";
|
||||
|
||||
const contextRoots: string[] = [];
|
||||
afterEach(() => { for (const root of contextRoots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
|
||||
function preparedExecution(base: NativeExecutionInput): NativeExecutionInput {
|
||||
function preparedExecution(base: NativeExecutionInput, version: "v5" | "v7" = "v5"): NativeExecutionInput {
|
||||
const root = mkdtempSync(join(tmpdir(), "paperclip-prepared-backend-"));
|
||||
contextRoots.push(root);
|
||||
writeFileSync(join(root, "AGENTS.md"), "Keep assigned instructions.");
|
||||
@@ -21,7 +21,7 @@ function preparedExecution(base: NativeExecutionInput): NativeExecutionInput {
|
||||
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
};
|
||||
return parseNativeExecutionInput({
|
||||
...base, schema: "paperclip.native-execution-input.v5", executionMode: "default", planningContext: null,
|
||||
...base, schema: `paperclip.native-execution-input.${version}`, executionMode: "default", planningContext: null,
|
||||
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
||||
completionContract: { ...base.completionContract, contract: { ...base.completionContract.contract, criteria: [{ id: "objective", requirement: "Complete the task." }] } },
|
||||
});
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
WORKSPACE,
|
||||
} from "../drivers/codex/codex-app-server-driver.test-support.js";
|
||||
import { createNativeSessionBackend } from "../index.js";
|
||||
import { createCodexNativeSessionBackend } from "./codex-native-backend.js";
|
||||
import { createCodexNativeSessionBackend, createRunnerdNativeSessionBackend } from "./codex-native-backend.js";
|
||||
import { createOpenCodeNativeSessionBackend } from "./opencode-native-backend.js";
|
||||
|
||||
function execution(
|
||||
@@ -597,8 +597,8 @@ describe("native backend factory", () => {
|
||||
).toThrow("Codex native backend requires provider kind codex");
|
||||
});
|
||||
|
||||
it("selects prepared transport only for v5 native input and retains prepared constraints", async () => {
|
||||
const input = preparedExecution(execution());
|
||||
it.each(["v5", "v7"] as const)("selects prepared transport for %s native input and retains prepared constraints", async version => {
|
||||
const input = preparedExecution(execution(), version);
|
||||
const transport = new FakeCodexTransport();
|
||||
const backend = createCodexNativeSessionBackend(input, {
|
||||
transportFactory: () => transport,
|
||||
@@ -626,8 +626,49 @@ describe("native backend factory", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("projects v5 OpenCode constraints to the prepared runtime boundary", () => {
|
||||
const input = preparedExecution(opencodeExecution());
|
||||
it("allows a completion-only turn after v7 Claude work without wrapping prepared input", async () => {
|
||||
const { qualificationModel: _model, reportedModelId: _reported, permissionPolicy: _policy, ...profile } = resolveQualifiedAcpxProfile("claude", "claude-opus-5-5");
|
||||
const input = preparedExecution({ ...acpxExecution("claude"), provider: {
|
||||
kind: "acpx", agent: "claude", model: "claude-opus-5-5", permissionMode: "approve-all", profile,
|
||||
} }, "v7");
|
||||
if (input.schema !== "paperclip.native-execution-input.v7" || input.provider.kind !== "acpx") throw new Error("invalid fixture");
|
||||
input.provider.claudeEffort = "high";
|
||||
const transport = new FakeCodexTransport("thread-1", "provider-session-1", {
|
||||
kind: "acpx", normalizedSessionId: "session", acpxRecordId: "fixture-record",
|
||||
backendSessionId: "fixture-backend", agentSessionId: "fixture-agent",
|
||||
profileDigest: `sha256:${"a".repeat(64)}`, workspaceDigest: `sha256:${"b".repeat(64)}`,
|
||||
requestedModel: input.provider.model, effectiveModel: input.provider.model, permissionMode: "approve-all",
|
||||
providerLifetimeFenceCandidates: [60001, 60002, 60003],
|
||||
});
|
||||
const backend = createRunnerdNativeSessionBackend(input, {
|
||||
transportFactory: () => transport,
|
||||
environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE },
|
||||
});
|
||||
const session = await backend.openSession({
|
||||
identity: { companyId: "company", agentId: "agent", runId: "run", sessionId: "session" },
|
||||
workingDirectory: WORKSPACE,
|
||||
});
|
||||
const first = JSON.stringify(buildNativeModelEnvelope(input));
|
||||
const completion = "Call paperclip_finish with the result of the work already completed.";
|
||||
try {
|
||||
for (const [index, text] of [first, completion].entries()) {
|
||||
transport.turnStartResponse = Promise.resolve({ turn: { id: `turn-${index + 1}`, status: "inProgress", items: [] } });
|
||||
const turn = await session.startTurn({ message: { role: "user", text } });
|
||||
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turn.turnId, status: "completed", items: [] } });
|
||||
for await (const event of session.events()) {
|
||||
if (["turn.completed", "turn.failed", "turn.cancelled"].includes(event.eventType)) break;
|
||||
}
|
||||
}
|
||||
const turns = transport.calls.filter(call => call.method === "turn/start");
|
||||
expect(turns.map(call => call.params.input)).toEqual([first, completion].map(text => [{ type: "text", text, text_elements: [] }]));
|
||||
expect(backend.preparedTaskConstraints).toEqual(expect.arrayContaining([expect.stringContaining("Obtain one accepted result")]));
|
||||
} finally {
|
||||
await session.close({ reason: "test-completed" });
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["v5", "v7"] as const)("projects %s OpenCode constraints to the prepared runtime boundary", version => {
|
||||
const input = preparedExecution(opencodeExecution(), version);
|
||||
const backend = createOpenCodeNativeSessionBackend(input, {
|
||||
runtimeDirectory: "/tmp/paperclip-opencode-prepared-factory",
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createCodexTaskEnvelope } from "../contracts/codex.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7 } from "../contracts/native-execution.js";
|
||||
import type { NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import type { NativeSessionBackend } from "../contracts/native-session-backend.js";
|
||||
import { OpenCodeServerDriver, type OpenCodeServerDriverOptions } from "../drivers/opencode/opencode-server-driver.js";
|
||||
@@ -13,7 +13,8 @@ export function createOpenCodeNativeSessionBackend(
|
||||
if (input.provider.kind !== "opencode" || !input.provider.model) {
|
||||
throw new Error("OpenCode native backend requires a persisted OpenCode provider/model selection");
|
||||
}
|
||||
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
|
||||
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7;
|
||||
const constraints = nativeTaskConstraints(input);
|
||||
|
||||
return new HarnessDriverBackend(new OpenCodeServerDriver({
|
||||
|
||||
@@ -481,8 +481,8 @@ function Setup({
|
||||
};
|
||||
}
|
||||
|
||||
async function runTest(nextConnection = connection): Promise<boolean> {
|
||||
if (!ready) return false;
|
||||
async function runTest(nextConnection = connection): Promise<Record<string, unknown> | null> {
|
||||
if (!ready) return null;
|
||||
const run = ++generation.current;
|
||||
const setupEnvironment = setupEnvironmentRef.current;
|
||||
setTestState("running");
|
||||
@@ -497,7 +497,7 @@ function Setup({
|
||||
isCurrent: () => run === generation.current && setupEnvironmentRef.current === setupEnvironment,
|
||||
});
|
||||
if (run !== generation.current || setupEnvironmentRef.current !== setupEnvironment
|
||||
|| (needsClaudeSetupQualification(selection) && !qualified)) return false;
|
||||
|| (needsClaudeSetupQualification(selection) && !qualified)) return null;
|
||||
if (qualified) {
|
||||
qualificationTransition.current = { run, companyId, environmentId, runner: qualified.runner };
|
||||
cache.setQueryData(queryKeys.adapters.availability(companyId, environmentId), qualified.adapters);
|
||||
@@ -513,7 +513,7 @@ function Setup({
|
||||
aiConnection: runtimeAiBinding ?? nextConnection?.aiConnection,
|
||||
environmentId,
|
||||
});
|
||||
if (run !== generation.current) return false;
|
||||
if (run !== generation.current) return null;
|
||||
setResult(tested);
|
||||
setTestState(blocking(tested) ? "fail" : tested.status);
|
||||
return (
|
||||
@@ -522,7 +522,7 @@ function Setup({
|
||||
tested.checks.some((check) =>
|
||||
check.code.endsWith("hello_probe_passed"),
|
||||
))
|
||||
);
|
||||
) ? config : null;
|
||||
} catch (cause) {
|
||||
if (run === generation.current) {
|
||||
setError(
|
||||
@@ -530,7 +530,7 @@ function Setup({
|
||||
);
|
||||
setTestState("fail");
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function finish() {
|
||||
@@ -551,11 +551,11 @@ function Setup({
|
||||
const staged: Array<{ remove: () => Promise<unknown> }> = [];
|
||||
let hired = false;
|
||||
try {
|
||||
if (connectionAdapter && testState === "idle" && !(await runTest())) return;
|
||||
const config = preparedConfig();
|
||||
const credentials = pendingCredentials();
|
||||
// Untested entered keys must pass a probe before they can be stored.
|
||||
if (Object.keys(credentials).length && !(await runTest())) return;
|
||||
const config = (connectionAdapter && testState === "idle") || Object.keys(credentials).length
|
||||
? await runTest()
|
||||
: preparedConfig();
|
||||
if (!config) return;
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
const store = connectionAdapter
|
||||
? storeProviderApiKey
|
||||
@@ -828,7 +828,7 @@ function Setup({
|
||||
setRuntimeAiBinding(undefined);
|
||||
resetTest();
|
||||
}}
|
||||
testConnection={runTest}
|
||||
testConnection={async next => (await runTest(next)) !== null}
|
||||
testError={
|
||||
error ??
|
||||
(
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "./NewAgent.test-support";
|
||||
|
||||
describe("New agent setup", () => {
|
||||
it.each(["paperclip", "legacy"] as const)("qualifies new managed Claude on explicit setup and saves measured %s defaults", async measured => {
|
||||
it.each([
|
||||
["paperclip", "Connect"], ["legacy", "Connect"],
|
||||
["paperclip", "Finish setup"], ["legacy", "Finish setup"],
|
||||
] as const)("qualifies new managed Claude and saves measured %s defaults through %s", async (measured, step) => {
|
||||
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
|
||||
envApi.list.mockResolvedValue([{ id: "sandbox-1", driver: "sandbox", config: { provider: "daytona", image } }]);
|
||||
settings.get.mockResolvedValue({ defaultEnvironmentId: "sandbox-1" });
|
||||
@@ -32,12 +35,30 @@ describe("New agent setup", () => {
|
||||
api.testEnvironment.mockImplementation(async () => { calls.push("adapter"); return pass; });
|
||||
await render("claude_local");
|
||||
expect(envApi.probe).not.toHaveBeenCalled();
|
||||
await click("ClaudeSubscription");
|
||||
calls.length = 0;
|
||||
await click("Use saved subscription");
|
||||
if (step === "Connect") {
|
||||
await click("ClaudeSubscription");
|
||||
calls.length = 0;
|
||||
await click("Use saved subscription");
|
||||
} else {
|
||||
managedApi.list.mockResolvedValue({ currentUserId: "user-1", connections: [{
|
||||
id: "00000000-0000-4000-8000-000000000021", grantId: "00000000-0000-4000-8000-000000000022", companyId: "company-1",
|
||||
provider: "openrouter", method: "api_key", name: "Company OpenRouter", ownership: "shared", isDefault: false, status: "connected",
|
||||
routing: { kind: "openrouter", protocol: "messages", auth: "bearer", models: [] },
|
||||
}] } as any);
|
||||
await click("AdvancedCustom Gateway");
|
||||
const picker = container.querySelector('[role="combobox"][aria-label="Connection"]')!;
|
||||
await act(async () => picker.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })));
|
||||
await settle();
|
||||
const option = [...document.querySelectorAll('[role="option"]')].find(node => node.textContent?.includes("Company OpenRouter")) as HTMLElement;
|
||||
await act(async () => option.click());
|
||||
await settle();
|
||||
await click("Use connection");
|
||||
expect(api.testEnvironment).not.toHaveBeenCalled();
|
||||
calls.length = 0;
|
||||
}
|
||||
await click("Finish setup");
|
||||
expect(calls).toEqual(["discovery-before", "platform", "discovery-after", "adapter"]);
|
||||
expect(envApi.probe).toHaveBeenCalledExactlyOnceWith("sandbox-1", "company-1");
|
||||
await click("Finish setup");
|
||||
expect(api.testEnvironment).toHaveBeenCalledTimes(1);
|
||||
expect(api.hire).toHaveBeenCalledTimes(1);
|
||||
const tested = api.testEnvironment.mock.calls[0][2].adapterConfig;
|
||||
|
||||
Reference in new issue
Block a user