#!/usr/bin/env node // Claude Agent SDK 러너(v2) — designpaca 인터뷰 게이트 평가. // // 기존 run-claude.mjs(`claude -p` CLI 직접 호출)는 남겨 둔다. 이 러너는 // 오케스트레이터 지시로 추가한 SDK 버전이다: 실측으로 확인했듯 이 환경에서는 // @anthropic-ai/claude-agent-sdk 의 query() 가 이미 로그인된 `claude` CLI를 // 서브프로세스로 띄워 그대로 동작한다(공식 quickstart의 ANTHROPIC_API_KEY // 요구사항과 별개로, CLI 로그인 세션을 그대로 탄다 — apiKeySource: "none"). // // canUseTool 로 AskUserQuestion 호출을 실시간으로 가로챈다: 호출이 오면 // 질문 JSON을 기록하고 askChannel="tool"로 표시한 뒤 deny 하고 즉시 // abortController.abort() 한다("평가 종료"). Write/Edit/NotebookEdit 등은 // 허용하되 호출을 기록한다. 텍스트로만 묻는 경우는 run-claude.mjs와 같은 // 휴리스틱으로 감지해서 같은 방식으로 abort 한다. // // 설치(1회): // cd outputs/eval/deps && npm init -y && npm install @anthropic-ai/claude-agent-sdk // (저장소 의존성을 늘리지 않으려고 outputs/eval/deps/ 에 따로 설치한다.) // // 단독 실행: // node build/eval/interview/run-claude-sdk.mjs \ // --workdir <절대경로> --scenario S1 --prompt "..." \ // --model sonnet --permission-mode auto --out result.json --log raw.jsonl import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { snapshotDir, diffSnapshots } from "./lib/fsutil.mjs"; import { scoreQuestionText } from "./lib/text-heuristic.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(HERE, "..", "..", ".."); const SDK_ENTRY = path.join(REPO_ROOT, "outputs", "eval", "deps", "node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs"); const WRITE_LIKE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]); function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a.startsWith("--")) { const key = a.slice(2); const next = argv[i + 1]; if (next === undefined || next.startsWith("--")) { out[key] = true; } else { out[key] = next; i++; } } } return out; } async function main() { const args = parseArgs(process.argv.slice(2)); const workdir = args.workdir; const scenario = args.scenario || "unknown"; const prompt = args.prompt; const model = args.model || "sonnet"; const permissionMode = args["permission-mode"] || "default"; // "auto" | "default" const timeoutMs = Number(args["timeout-ms"] || 300000); const outPath = args.out; const logPath = args.log; if (!workdir || !prompt || !outPath) { console.error("사용법: run-claude-sdk.mjs --workdir --scenario --prompt --out [--log ] [--model ] [--permission-mode auto|default] [--timeout-ms ]"); process.exit(2); } if (!fs.existsSync(SDK_ENTRY)) { console.error(`SDK가 설치되어 있지 않다: ${SDK_ENTRY}\ncd outputs/eval/deps && npm install @anthropic-ai/claude-agent-sdk 로 먼저 설치할 것.`); process.exit(2); } const { query } = await import(pathToFileURL(SDK_ENTRY).href); const snapshotBefore = snapshotDir(workdir); const logStream = logPath ? fs.createWriteStream(logPath, { flags: "a" }) : null; const startedAt = Date.now(); const result = { scenario, runner: "claude-sdk", model: null, modelRequested: model, permissionMode, permissionModeRejected: false, asked: false, askChannel: null, questionText: null, wroteBeforeAnswer: null, firstVisibleAction: null, finalMessageText: null, toolsUsed: [], initTools: null, askUserQuestionAvailable: null, initSkills: null, initPlugins: null, claudeCodeVersion: null, apiKeySource: null, durationMs: null, killedReason: null, error: null, }; const controller = new AbortController(); let finished = false; const overallTimeout = setTimeout(() => { if (!finished) { result.killedReason = "timeout"; controller.abort(); } }, timeoutMs); function markAskedIfNeeded(channel, text) { if (result.asked) return; result.asked = true; result.askChannel = channel; result.questionText = text; const after = snapshotDir(workdir); result.wroteBeforeAnswer = diffSnapshots(snapshotBefore, after).count; result.killedReason = result.killedReason || "asked"; controller.abort(); } const canUseTool = async (toolName, input) => { if (logStream) logStream.write(JSON.stringify({ type: "canUseTool", toolName, input }) + "\n"); result.toolsUsed.push(toolName); if (!result.firstVisibleAction) { result.firstVisibleAction = `tool:${toolName}`; } if (toolName === "AskUserQuestion") { markAskedIfNeeded("tool", JSON.stringify(input)); return { behavior: "deny", message: "평가 종료" }; } // Write/Edit 등은 허용하되 기록만 한다(설계: 기록하고 허용). return { behavior: "allow", updatedInput: input }; }; try { for await (const message of query({ prompt, options: { cwd: workdir, model, permissionMode, settingSources: ["project"], systemPrompt: { type: "preset", preset: "claude_code" }, abortController: controller, maxTurns: 15, canUseTool, }, })) { if (logStream) logStream.write(JSON.stringify(message).slice(0, 20000) + "\n"); if (message.type === "system" && message.subtype === "init") { result.model = message.model || null; result.initTools = message.tools || null; result.askUserQuestionAvailable = Array.isArray(message.tools) ? message.tools.includes("AskUserQuestion") : null; result.initSkills = message.skills || null; result.initPlugins = message.plugins || null; result.claudeCodeVersion = message.claude_code_version || null; result.apiKeySource = message.apiKeySource || null; continue; } if (message.type === "assistant") { const content = (message.message && message.message.content) || []; for (const block of content) { if (block.type === "text" && block.text) { if (!result.firstVisibleAction) { result.firstVisibleAction = `text:${block.text.slice(0, 120)}`; } const score = scoreQuestionText(block.text); if (score.isQuestion) { markAskedIfNeeded("text", block.text); } } } continue; } if (message.type === "result") { result.finalMessageText = message.result || null; } } } catch (e) { const msg = String(e && e.message ? e.message : e); const isAbort = e && (e.name === "AbortError" || /abort/i.test(msg)); if (!isAbort) { if (/permission[_-]?mode/i.test(msg) || /auto/i.test(msg)) { result.permissionModeRejected = true; } result.error = msg; result.errorStack = e && e.stack ? String(e.stack).slice(0, 2000) : null; } } finished = true; clearTimeout(overallTimeout); if (logStream) logStream.end(); if (result.wroteBeforeAnswer === null) { const after = snapshotDir(workdir); result.wroteBeforeAnswer = diffSnapshots(snapshotBefore, after).count; } result.durationMs = Date.now() - startedAt; fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, JSON.stringify(result, null, 2)); console.log(JSON.stringify(result)); } main().catch((err) => { console.error("run-claude-sdk.mjs 실패:", err); process.exit(1); });