// Vendored by builder-app alongside claude-agent-sdk-harness.ts. See that // file's header for why the harness is vendored rather than installed. // // Starts one Claude Agent SDK turn for a thread and returns its runId. The turn // itself streams over the framework's existing run routes — there is no second // streaming mechanism here: // // POST /api/claude-agent/turn { threadId, prompt } -> { runId, sessionId } // GET /_agent-native/runs//events (SSE transcript) // POST /_agent-native/runs//abort (cancel) // // A streaming route is one of the documented exceptions to actions-first; the // start call is kept here with it so the two stay in one place. import { randomUUID } from "node:crypto"; import { ensureAgentHarnessSessionTables, getLatestAgentHarnessSessionForThread, resolveAgentHarness, startAgentHarnessRun, } from "@agent-native/core/agent/harness"; import { getSession } from "@agent-native/core/server"; import { runWithRequestContext } from "@agent-native/core/server/request-context"; import { defineEventHandler, readBody, setResponseStatus } from "h3"; /** Where the agent works. Defaults to the app's own directory. */ const WORKSPACE_DIR = process.env.CLAUDE_AGENT_WORKSPACE_DIR || process.cwd(); // Custom `/api/*` routes are not wrapped in the framework's per-request // AsyncLocalStorage context the way `/_agent-native/actions/*` are — only the // action dispatcher resolves the session and calls `runWithRequestContext` // itself. Do the same resolution here so `ownerEmail`/`orgId` are correct // for both a real signed-in session and the AUTH_DISABLED dev fallback. export default defineEventHandler(async (event) => { const session = await getSession(event); const ownerEmail = session?.email; if (!ownerEmail) { setResponseStatus(event, 401); return { error: "Sign in to run the agent." }; } return runWithRequestContext( { userEmail: ownerEmail, orgId: session?.orgId ?? undefined }, async () => { const body = (await readBody(event)) as { threadId?: unknown; prompt?: unknown; } | null; const threadId = typeof body?.threadId === "string" ? body.threadId : ""; const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : ""; if (!threadId || !prompt) { setResponseStatus(event, 400); return { error: "threadId and prompt are required." }; } await ensureAgentHarnessSessionTables(); // Resume this thread's previous SDK session instead of replaying history. // Scoped to this harness by name: a thread that also ran another harness // would otherwise hand us that harness's resumeState, which this one // cannot interpret. A thread with no prior session starts fresh. const previous = await getLatestAgentHarnessSessionForThread( threadId, "claude-agent-sdk", ); const resumeState = previous?.resumeState; const adapter = resolveAgentHarness("claude-agent-sdk", { cwd: WORKSPACE_DIR, // Reads run; edits and shell prompt. Loosen per app only deliberately. permissionMode: "allow-reads", }); const runId = randomUUID(); startAgentHarnessRun({ runId, threadId, adapter, input: { prompt }, createSession: { sessionId: previous?.id, resumeState, cwd: WORKSPACE_DIR, permissionMode: "allow-reads", }, ownerEmail, orgId: session?.orgId ?? null, }); return { runId, sessionId: previous?.id ?? null, events: `/_agent-native/runs/${runId}/events`, }; }, ); });