Files
stock-listing-app/server/routes/api/claude-agent/turn.post.ts
T
exe.dev user e859245867 Add minimal stock listing app: actions, Postgres schema, stocks page, Claude Agent SDK harness
- Add stocks table (server/db) with list/get/add/update/delete actions, scoped
  per user, backing both the chat agent and the /stocks UI via
  useActionQuery/useActionMutation
- Add mobile-first /stocks page (shadcn table/cards, watchlist, search) and
  wire it into the sidebar, navigation state, and view-screen context
- Install the platform's Claude Agent SDK harness (server/plugins,
  server/routes/api/claude-agent) so the app agent runs on the platform's own
  Claude subscription instead of the Builder.io gateway; fix the vendored
  turn.post.ts route to resolve the session and wrap it in
  runWithRequestContext, since custom /api/ routes aren't auto-wrapped in
  request context the way /_agent-native/actions/* are
- Add shadcn table/badge/dialog/select components (Tabler icons, not lucide)
2026-07-29 12:18:14 +00:00

100 lines
3.7 KiB
TypeScript

// 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/<runId>/events (SSE transcript)
// POST /_agent-native/runs/<runId>/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`,
};
},
);
});