Move adapter implementations into shared workspace packages
Extract claude-local and codex-local adapter code from cli/server/ui into packages/adapters/ and packages/adapter-utils/. CLI, server, and UI now import shared adapter logic instead of duplicating it. Removes ~1100 lines of duplicated code across packages. Register new packages in pnpm workspace. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
197
packages/adapters/claude-local/src/server/execute.ts
Normal file
197
packages/adapters/claude-local/src/server/execute.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils";
|
||||
import type { RunProcessResult } from "@paperclip/adapter-utils/server-utils";
|
||||
import {
|
||||
asString,
|
||||
asNumber,
|
||||
asBoolean,
|
||||
asStringArray,
|
||||
parseObject,
|
||||
parseJson,
|
||||
buildPaperclipEnv,
|
||||
redactEnvForLogs,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
ensurePathInEnv,
|
||||
renderTemplate,
|
||||
runChildProcess,
|
||||
} from "@paperclip/adapter-utils/server-utils";
|
||||
import { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js";
|
||||
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
const { runId, agent, runtime, config, context, onLog, onMeta } = ctx;
|
||||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
);
|
||||
const bootstrapTemplate = asString(config.bootstrapPromptTemplate, promptTemplate);
|
||||
const command = asString(config.command, "claude");
|
||||
const model = asString(config.model, "");
|
||||
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
|
||||
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
|
||||
|
||||
const cwd = asString(config.cwd, process.cwd());
|
||||
await ensureAbsoluteDirectory(cwd);
|
||||
const envConfig = parseObject(config.env);
|
||||
const env: Record<string, string> = { ...buildPaperclipEnv(agent) };
|
||||
for (const [k, v] of Object.entries(envConfig)) {
|
||||
if (typeof v === "string") env[k] = v;
|
||||
}
|
||||
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||
|
||||
const timeoutSec = asNumber(config.timeoutSec, 1800);
|
||||
const graceSec = asNumber(config.graceSec, 20);
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
})();
|
||||
|
||||
const sessionId = runtime.sessionId;
|
||||
const template = sessionId ? promptTemplate : bootstrapTemplate;
|
||||
const prompt = renderTemplate(template, {
|
||||
company: { id: agent.companyId },
|
||||
agent,
|
||||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
});
|
||||
|
||||
const buildClaudeArgs = (resumeSessionId: string | null) => {
|
||||
const args = ["--print", prompt, "--output-format", "stream-json", "--verbose"];
|
||||
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
||||
if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
|
||||
if (model) args.push("--model", model);
|
||||
if (maxTurns > 0) args.push("--max-turns", String(maxTurns));
|
||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||
return args;
|
||||
};
|
||||
|
||||
const parseFallbackErrorMessage = (proc: RunProcessResult) => {
|
||||
const stderrLine =
|
||||
proc.stderr
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean) ?? "";
|
||||
|
||||
if ((proc.exitCode ?? 0) === 0) {
|
||||
return "Failed to parse claude JSON output";
|
||||
}
|
||||
|
||||
return stderrLine
|
||||
? `Claude exited with code ${proc.exitCode ?? -1}: ${stderrLine}`
|
||||
: `Claude exited with code ${proc.exitCode ?? -1}`;
|
||||
};
|
||||
|
||||
const runAttempt = async (resumeSessionId: string | null) => {
|
||||
const args = buildClaudeArgs(resumeSessionId);
|
||||
if (onMeta) {
|
||||
await onMeta({
|
||||
adapterType: "claude_local",
|
||||
command,
|
||||
cwd,
|
||||
commandArgs: args.map((value, idx) => (idx === 1 ? `<prompt ${prompt.length} chars>` : value)),
|
||||
env: redactEnvForLogs(env),
|
||||
prompt,
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
const proc = await runChildProcess(runId, command, args, {
|
||||
cwd,
|
||||
env,
|
||||
timeoutSec,
|
||||
graceSec,
|
||||
onLog,
|
||||
});
|
||||
|
||||
const parsedStream = parseClaudeStreamJson(proc.stdout);
|
||||
const parsed = parsedStream.resultJson ?? parseJson(proc.stdout);
|
||||
return { proc, parsedStream, parsed };
|
||||
};
|
||||
|
||||
const toAdapterResult = (
|
||||
attempt: {
|
||||
proc: RunProcessResult;
|
||||
parsedStream: ReturnType<typeof parseClaudeStreamJson>;
|
||||
parsed: Record<string, unknown> | null;
|
||||
},
|
||||
opts: { fallbackSessionId: string | null; clearSessionOnMissingSession?: boolean },
|
||||
): AdapterExecutionResult => {
|
||||
const { proc, parsedStream, parsed } = attempt;
|
||||
if (proc.timedOut) {
|
||||
return {
|
||||
exitCode: proc.exitCode,
|
||||
signal: proc.signal,
|
||||
timedOut: true,
|
||||
errorMessage: `Timed out after ${timeoutSec}s`,
|
||||
clearSession: Boolean(opts.clearSessionOnMissingSession),
|
||||
};
|
||||
}
|
||||
|
||||
if (!parsed) {
|
||||
return {
|
||||
exitCode: proc.exitCode,
|
||||
signal: proc.signal,
|
||||
timedOut: false,
|
||||
errorMessage: parseFallbackErrorMessage(proc),
|
||||
resultJson: {
|
||||
stdout: proc.stdout,
|
||||
stderr: proc.stderr,
|
||||
},
|
||||
clearSession: Boolean(opts.clearSessionOnMissingSession),
|
||||
};
|
||||
}
|
||||
|
||||
const usage =
|
||||
parsedStream.usage ??
|
||||
(() => {
|
||||
const usageObj = parseObject(parsed.usage);
|
||||
return {
|
||||
inputTokens: asNumber(usageObj.input_tokens, 0),
|
||||
cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0),
|
||||
outputTokens: asNumber(usageObj.output_tokens, 0),
|
||||
};
|
||||
})();
|
||||
|
||||
const resolvedSessionId =
|
||||
parsedStream.sessionId ??
|
||||
(asString(parsed.session_id, opts.fallbackSessionId ?? "") || opts.fallbackSessionId);
|
||||
|
||||
return {
|
||||
exitCode: proc.exitCode,
|
||||
signal: proc.signal,
|
||||
timedOut: false,
|
||||
errorMessage:
|
||||
(proc.exitCode ?? 0) === 0
|
||||
? null
|
||||
: describeClaudeFailure(parsed) ?? `Claude exited with code ${proc.exitCode ?? -1}`,
|
||||
usage,
|
||||
sessionId: resolvedSessionId,
|
||||
provider: "anthropic",
|
||||
model: parsedStream.model || asString(parsed.model, model),
|
||||
costUsd: parsedStream.costUsd ?? asNumber(parsed.total_cost_usd, 0),
|
||||
resultJson: parsed,
|
||||
summary: parsedStream.summary || asString(parsed.result, ""),
|
||||
clearSession: Boolean(opts.clearSessionOnMissingSession && !resolvedSessionId),
|
||||
};
|
||||
};
|
||||
|
||||
const initial = await runAttempt(sessionId ?? null);
|
||||
if (
|
||||
sessionId &&
|
||||
!initial.proc.timedOut &&
|
||||
(initial.proc.exitCode ?? 0) !== 0 &&
|
||||
initial.parsed &&
|
||||
isClaudeUnknownSessionError(initial.parsed)
|
||||
) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[paperclip] Claude resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
|
||||
);
|
||||
const retry = await runAttempt(null);
|
||||
return toAdapterResult(retry, { fallbackSessionId: null, clearSessionOnMissingSession: true });
|
||||
}
|
||||
|
||||
return toAdapterResult(initial, { fallbackSessionId: runtime.sessionId });
|
||||
}
|
||||
Reference in New Issue
Block a user