이 글에서는 Staged Changes 기준으로, 사용자가 입력한 한 줄 프롬프트가 Inngest 이벤트로 들어가면 E2B 샌드박스 안에서 Agent Kit 에이전트가 터미널·파일 쓰기·읽기 도구로 실제 Next.js 화면을 구현하고, 결과 URL·파일·요약을 반환하는 구성을 정리한다.



배경
이전에는 "프롬프트 → 코드 스니펫 한 번 생성 → 샌드박스 URL 반환" 수준이었다. 사용자 문장 하나로 실제로 동작하는 페이지까지 만들려면, 샌드박스 안에서 패키지 설치·파일 생성·수정·읽기를 반복할 수 있어야 한다. 그래서 Inngest 함수 안에 코드 에이전트를 두고, 에이전트가 도구(terminal, createOrUpdateFile, readFile) 를 쓰다가 작업이 끝나면 <task_summary> 로 "끝났다"고 알리게 했다. 이렇게 하면 토큰 제한이나 횟수로 끊는 게 아니라, 에이전트가 스스로 완료를 선언할 때까지 반복할 수 있다.
전체 흐름
| 단계 | 내용 |
|---|---|
| 1 | 사용자 프롬프트가 이벤트 test/hello.world의 event.data.value로 전달됨 |
| 2 | Inngest 함수가 E2B 템플릿으로 샌드박스 생성 후 sandboxId 확보 |
| 3 | 코드 에이전트가 PROMPT + 도구 3개로 최대 15턴 반복 실행 |
| 4 | 에이전트가 터미널로 npm install, 파일 쓰기/읽기로 app/page.tsx 등 구현. dev 서버는 이미 떠 있으므로 Hot Reload로 반영 |
| 5 | 에이전트가 마지막 응답에 <task_summary>...</task_summary> 를 넣으면 라우터가 반복 종료 |
| 6 | 샌드박스 URL 조회 후, 해당 URL을 한 번 fetch 해서 Next.js 온디맨드 컴파일 트리거(warm) |
| 7 | { url, title: 'Fragment', files, summary } 반환 |
스크립트와 UI
- package.json:
inngest:dev(Inngest Dev Server 실행),e2b:build(E2B 템플릿 빌드) 스크립트 추가. 로컬에서 Inngest·E2B를 쓰기 위함. - compile_page.sh:
npx next dev→npx next dev --turbopack. 샌드박스 안 dev 서버를 Turbopack으로 띄워 빌드·HMR 속도를 높이려고. - page.tsx: 버튼 문구 "Invoke Background Job" → "전송", 버튼에
className="mt-4"추가. 입력과 버튼 사이 간격을 두고, 문구를 단순하게 바꾸려고.
시스템 프롬프트 (src/prompt.ts)
PROMPT 상수로 두고 createAgent의 system에 넣었다. 요지는 다음과 같다.
- 역할: Next.js 15.3.6 샌드박스 안 시니어 엔지니어. 도구는 터미널·createOrUpdateFiles·readFiles만 사용.
- 환경: 메인 파일
app/page.tsx, Shadcn·Tailwind 이미 설치. 스타일은 Tailwind만, 경로는 import 시@/, 파일 시스템 접근 시 실제 경로, createOrUpdateFiles 시 상대 경로만. - 실행 금지:
npm run dev/build/start,next dev/build/start등. 이미 3000에서 Hot Reload 동작 중이라 서버 재시작을 막으려고. - 작업 규칙: 완성도 있는 구현, 새 패키지는 터미널로 설치 후 import, Shadcn은 실제 API에 맞게.
cn유틸은 샌드박스에 없으므로 사용하지 말고 조건부 클래스는 템플릿 리터럴 등으로. - 완료 신호: 작업이 끝나면 응답에
<task_summary>요약 한 줄</task_summary>를 딱 한 번만 출력. 이걸로 네트워크가 반복을 멈추고 summary를 저장한다.
Inngest 함수
샌드박스는 한 번 만들고 ID만 넘긴다
에이전트가 도구를 쓸 때마다 같은 샌드박스를 써야 한다. 맨 앞 step에서 Sandbox.create로 하나만 만들고, 반환된 sandboxId만 이후 step·도구 handler에 넘긴다. 각 handler 안에서는 getSandbox(sandboxId)로 재연결한다. Inngest step은 실행 컨텍스트가 달라질 수 있어서, 샌드박스 인스턴스를 들고 다니지 않고 ID로 다시 붙는 쪽이 안전하다.
const sandboxId = await step.run('get-sandbox-id', async () => {
const sandbox = await Sandbox.create('jinhoforyt/vibe-nextjs-test-2');
return sandbox.sandboxId;
});
도구 호출은 step.run으로 감싼다
터미널·파일 쓰기·읽기는 전부 step.run('이름', async () => { ... }) 안에 넣었다. 한 번에 한 단위씩 Inngest에 기록되고, 실패 시 그 step만 재시도할 수 있게 하려고. 에이전트가 여러 번 도구를 써도 각 호출이 개별 step으로 남아 디버깅·재실행이 쉽다.
완료는 에이전트가 알려 주게 한다
에이전트는 대화형이라 return 한 번으로 끝나지 않는다. 그래서 완료 신호를 응답 텍스트 안에 넣기로 했다. lifecycle.onResponse에서 마지막 assistant 메시지를 파싱해 <task_summary> 가 있으면 network.state.data.summary에 넣고, router는 "summary가 있나?"만 보면 된다. 있으면 더 이상 에이전트를 호출하지 않고 네트워크를 끝낸다.
lifecycle: {
onResponse: async ({ result, network }) => {
const text = lastAssistantTextMessageContent(result);
if (text?.includes('<task_summary>')) {
network.state.data.summary = text;
}
return result;
},
},
router: async ({ network }) => {
if (network.state.data.summary) return;
return codeAgent;
},
warm-sandbox-page
Next.js는 첫 요청 시 해당 페이지를 온디맨드로 컴파일한다. 사용자가 반환된 URL을 열 때 그때 컴파일되면 체감 로딩이 길어지므로, 함수 마지막에 백엔드에서 URL을 한 번 fetch하는 단계를 넣었다. 미리 한 번 호출해 두면 사용자가 링크를 열 때 이미 컴파일된 상태에 가깝게 맞출 수 있다.
유틸
getSandbox(sandboxId) 는 "이미 띄워 둔 샌드박스에 다시 붙기"용이다. 생성은 맨 앞 step에서 한 번만 하고, 그 다음부터는 도구 handler·URL 조회·warm 단계 어디서든 sandboxId만 알면 같은 환경을 쓰게 하려고 분리해 뒀다.
lastAssistantTextMessageContent(result) 는 "에이전트가 마지막에 뭐라고 했는지 텍스트만 뽑기"용이다. Agent Kit 결과가 메시지 배열이라, 그중 마지막 assistant 메시지의 content를 문자열로 합쳐 주는 걸 한 군데에서 처리한다. 그래야 lifecycle에서 <task_summary> 포함 여부를 같은 방식으로 판단할 수 있다.
E2B 템플릿 보강
Shadcn 컴포넌트 대부분이 import { cn } from '@/lib/utils' 를 쓰는데, create-next-app·shadcn init만으로는 src/lib/utils.ts 나 lib/utils.ts 가 없을 수 있다. 그래서 cn 함수를 담은 utils.ts를 직접 두고, Dockerfile에서 src/lib 와 lib 둘 다에 복사해 넣었다. @/ 가 ./ 를 가리키는지 ./src/ 를 가리키는지 프로젝트마다 다를 수 있어서, 둘 다 넣어 두면 어느 쪽이든 @/lib/utils 가 resolve 되게 하려는 것이다. tailwind-merge·clsx도 Shadcn이 cn 안에서 쓰므로 Dockerfile에서 명시 설치한다.
// sandbox-templates/nextjs/utils.ts
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
정리
- 사용자 프롬프트 한 줄이 이벤트로 들어오면, E2B 샌드박스가 생성되고, Agent Kit 코드 에이전트가 PROMPT와 terminal / createOrUpdateFile / readFile 도구로 샌드박스 안에서 반복 작업한다.
- router는
network.state.data.summary가 있으면(에이전트가<task_summary>를 출력한 경우) 반복을 멈추고, lifecycle.onResponse 가 마지막 assistant 메시지에서 요약을 추출해 저장한다. - warm-sandbox-page 로 샌드박스 URL을 한 번 호출해 온디맨드 컴파일을 트리거한 뒤 url, title, files, summary 를 반환한다.
- E2B 템플릿에는 cn 유틸과 tailwind-merge·clsx 가 포함되고, compile_page.sh 는 Turbopack으로 dev 서버를 띄운다.
이 구성을 기준으로 "사용자 문장 → 샌드박스에서 실제 구현 → URL·파일·요약 반환"까지 한 번에 이어지는 흐름을 확장할 수 있다.