mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Cache Claude effort-flag probe results
This commit is contained in:
1 parent
97bf7de45c
commit
42fded2180
4 files changed
+177
-6
No files matched your search
@@ -2,12 +2,39 @@ import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/executio
|
||||
import { runAdapterExecutionTargetProcess } from "@paperclipai/adapter-utils/execution-target";
|
||||
import path from "node:path";
|
||||
|
||||
const effortFlagSupportCache = new Map<string, Promise<boolean | null>>();
|
||||
|
||||
export function claudeCommandLooksLike(command: string, expected = "claude"): boolean {
|
||||
const base = path.basename(command).toLowerCase();
|
||||
return base === expected || base === `${expected}.cmd` || base === `${expected}.exe`;
|
||||
}
|
||||
|
||||
export async function claudeCommandSupportsEffortFlag(input: {
|
||||
function cacheKeyForTarget(command: string, target: AdapterExecutionTarget | null | undefined): string {
|
||||
if (!target) return `local::${command}`;
|
||||
if (target.kind === "local") {
|
||||
return `local:${target.environmentId ?? ""}:${target.leaseId ?? ""}:${command}`;
|
||||
}
|
||||
if (target.transport === "sandbox") {
|
||||
return [
|
||||
"sandbox",
|
||||
target.providerKey ?? "",
|
||||
target.environmentId ?? "",
|
||||
target.leaseId ?? "",
|
||||
command,
|
||||
].join(":");
|
||||
}
|
||||
return [
|
||||
"ssh",
|
||||
target.environmentId ?? "",
|
||||
target.leaseId ?? "",
|
||||
target.spec.host,
|
||||
target.spec.port ?? "",
|
||||
target.spec.username ?? "",
|
||||
command,
|
||||
].join(":");
|
||||
}
|
||||
|
||||
async function probeClaudeCommandSupportsEffortFlag(input: {
|
||||
runId: string;
|
||||
command: string;
|
||||
target: AdapterExecutionTarget | null | undefined;
|
||||
@@ -16,8 +43,6 @@ export async function claudeCommandSupportsEffortFlag(input: {
|
||||
timeoutSec: number;
|
||||
graceSec: number;
|
||||
}): Promise<boolean | null> {
|
||||
if (!claudeCommandLooksLike(input.command, "claude")) return null;
|
||||
|
||||
const help = await runAdapterExecutionTargetProcess(
|
||||
input.runId,
|
||||
input.target,
|
||||
@@ -38,3 +63,30 @@ export async function claudeCommandSupportsEffortFlag(input: {
|
||||
if ((help.exitCode ?? 0) === 0) return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function claudeCommandSupportsEffortFlag(input: {
|
||||
runId: string;
|
||||
command: string;
|
||||
target: AdapterExecutionTarget | null | undefined;
|
||||
cwd: string;
|
||||
env: Record<string, string>;
|
||||
timeoutSec: number;
|
||||
graceSec: number;
|
||||
}): Promise<boolean | null> {
|
||||
if (!claudeCommandLooksLike(input.command, "claude")) return null;
|
||||
|
||||
const key = cacheKeyForTarget(input.command, input.target);
|
||||
const cached = effortFlagSupportCache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const probe = probeClaudeCommandSupportsEffortFlag(input).catch((error) => {
|
||||
effortFlagSupportCache.delete(key);
|
||||
throw error;
|
||||
});
|
||||
effortFlagSupportCache.set(key, probe);
|
||||
return probe;
|
||||
}
|
||||
|
||||
export function resetClaudeCliCapabilitiesCacheForTests() {
|
||||
effortFlagSupportCache.clear();
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export { claudeSessionCwdMatchesExecutionTarget, execute, runClaudeLogin } from
|
||||
export { listClaudeSkills, syncClaudeSkills } from "./skills.js";
|
||||
export { listClaudeModels, refreshClaudeModels, resetClaudeModelsCacheForTests } from "./models.js";
|
||||
export { testEnvironment } from "./test.js";
|
||||
export { resetClaudeCliCapabilitiesCacheForTests } from "./cli-capabilities.js";
|
||||
export {
|
||||
parseClaudeStreamJson,
|
||||
describeClaudeFailure,
|
||||
|
||||
@@ -3,13 +3,14 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { runChildProcess } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { testEnvironment } from "@paperclipai/adapter-claude-local/server";
|
||||
import { resetClaudeCliCapabilitiesCacheForTests, testEnvironment } from "@paperclipai/adapter-claude-local/server";
|
||||
|
||||
const ORIGINAL_ANTHROPIC = process.env.ANTHROPIC_API_KEY;
|
||||
const ORIGINAL_BEDROCK = process.env.CLAUDE_CODE_USE_BEDROCK;
|
||||
const ORIGINAL_BEDROCK_URL = process.env.ANTHROPIC_BEDROCK_BASE_URL;
|
||||
|
||||
afterEach(() => {
|
||||
resetClaudeCliCapabilitiesCacheForTests();
|
||||
if (ORIGINAL_ANTHROPIC === undefined) {
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { runChildProcess } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { claudeSessionCwdMatchesExecutionTarget, execute } from "@paperclipai/adapter-claude-local/server";
|
||||
import {
|
||||
claudeSessionCwdMatchesExecutionTarget,
|
||||
execute,
|
||||
resetClaudeCliCapabilitiesCacheForTests,
|
||||
} from "@paperclipai/adapter-claude-local/server";
|
||||
|
||||
async function writeFailingClaudeCommand(
|
||||
commandPath: string,
|
||||
@@ -112,6 +116,45 @@ console.log(JSON.stringify({ type: "result", session_id: "33333333-3333-4333-833
|
||||
await fs.chmod(commandPath, 0o755);
|
||||
}
|
||||
|
||||
async function writeHelpWithEffortClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
if (argv.includes("--help")) {
|
||||
const helpCountPath = process.env.PAPERCLIP_TEST_HELP_COUNT_PATH;
|
||||
if (helpCountPath) {
|
||||
const current = fs.existsSync(helpCountPath) ? Number(fs.readFileSync(helpCountPath, "utf8")) || 0 : 0;
|
||||
fs.writeFileSync(helpCountPath, String(current + 1), "utf8");
|
||||
}
|
||||
process.stdout.write("Usage: claude [options]\\n --print\\n --effort <level>\\n --model <id>\\n");
|
||||
process.exit(0);
|
||||
}
|
||||
const addDirIndex = argv.indexOf("--add-dir");
|
||||
const addDir = addDirIndex >= 0 ? argv[addDirIndex + 1] : null;
|
||||
const instructionsIndex = argv.indexOf("--append-system-prompt-file");
|
||||
const instructionsFilePath = instructionsIndex >= 0 ? argv[instructionsIndex + 1] : null;
|
||||
const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH;
|
||||
const payload = {
|
||||
argv,
|
||||
prompt: fs.readFileSync(0, "utf8"),
|
||||
addDir,
|
||||
instructionsFilePath,
|
||||
instructionsContents: instructionsFilePath ? fs.readFileSync(instructionsFilePath, "utf8") : null,
|
||||
skillEntries: addDir ? fs.readdirSync(path.join(addDir, ".claude", "skills")).sort() : [],
|
||||
};
|
||||
if (capturePath) {
|
||||
fs.writeFileSync(capturePath, JSON.stringify(payload), "utf8");
|
||||
}
|
||||
console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "44444444-4444-4444-8444-444444444444", model: "claude-sonnet" }));
|
||||
console.log(JSON.stringify({ type: "assistant", session_id: "44444444-4444-4444-8444-444444444444", message: { content: [{ type: "text", text: "hello" }] } }));
|
||||
console.log(JSON.stringify({ type: "result", session_id: "44444444-4444-4444-8444-444444444444", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } }));
|
||||
`;
|
||||
await fs.writeFile(commandPath, script, "utf8");
|
||||
await fs.chmod(commandPath, 0o755);
|
||||
}
|
||||
|
||||
type CapturePayload = {
|
||||
argv: string[];
|
||||
prompt: string;
|
||||
@@ -128,6 +171,10 @@ type CapturePayload = {
|
||||
appendedSystemPromptFileContents?: string | null;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
resetClaudeCliCapabilitiesCacheForTests();
|
||||
});
|
||||
|
||||
async function writePoisonedMessageIdClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
const fs = require("node:fs");
|
||||
@@ -826,6 +873,76 @@ describe("claude execute", () => {
|
||||
}
|
||||
}, 10_000);
|
||||
|
||||
it("passes through --effort and reuses the sandbox capability probe when the installed Claude CLI advertises it", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-claude-execute-sandbox-effort-supported-"));
|
||||
const { workspace, commandPath, capturePath, restore } = await setupExecuteEnv(root, {
|
||||
commandWriter: writeHelpWithEffortClaudeCommand,
|
||||
});
|
||||
const helpCountPath = path.join(root, "help-count.txt");
|
||||
const remoteWorkspace = path.join(root, "sandbox-workspace");
|
||||
await fs.mkdir(remoteWorkspace, { recursive: true });
|
||||
|
||||
const baseInput = {
|
||||
agent: {
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
name: "Claude Coder",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
},
|
||||
runtime: {
|
||||
sessionId: null,
|
||||
sessionParams: null,
|
||||
sessionDisplayId: null,
|
||||
taskKey: null,
|
||||
},
|
||||
config: {
|
||||
command: commandPath,
|
||||
cwd: workspace,
|
||||
effort: "low",
|
||||
env: {
|
||||
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
|
||||
PAPERCLIP_TEST_HELP_COUNT_PATH: helpCountPath,
|
||||
},
|
||||
promptTemplate: "Keep the requested effort when supported.",
|
||||
},
|
||||
context: {},
|
||||
executionTarget: {
|
||||
kind: "remote" as const,
|
||||
transport: "sandbox" as const,
|
||||
providerKey: "daytona",
|
||||
environmentId: "env-1",
|
||||
leaseId: "lease-1",
|
||||
remoteCwd: remoteWorkspace,
|
||||
timeoutMs: 30_000,
|
||||
runner: createLocalSandboxRunner(),
|
||||
},
|
||||
authToken: "run-jwt-token",
|
||||
onLog: async () => {},
|
||||
};
|
||||
|
||||
try {
|
||||
const first = await execute({
|
||||
runId: "run-sandbox-effort-supported-1",
|
||||
...baseInput,
|
||||
});
|
||||
const second = await execute({
|
||||
runId: "run-sandbox-effort-supported-2",
|
||||
...baseInput,
|
||||
});
|
||||
|
||||
expect(first.exitCode).toBe(0);
|
||||
expect(second.exitCode).toBe(0);
|
||||
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
||||
expect(capture.argv).toContain("--effort");
|
||||
expect(capture.argv).toContain("low");
|
||||
expect(await fs.readFile(helpCountPath, "utf8")).toBe("1");
|
||||
} finally {
|
||||
restore();
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 10_000);
|
||||
|
||||
it("allows remote session resumes when saved cwd is the host workspace", () => {
|
||||
expect(claudeSessionCwdMatchesExecutionTarget({
|
||||
runtimeSessionCwd: "/host/workspace",
|
||||
|
||||
Reference in new issue
Block a user