Postgres만으로 내구성 있는 워크플로를 구현하는 SQL 전용 최소 라이브러리 Absurd를 소개합니다. 큐와 상태 저장을 활용해 에이전트 루프, 체크포인트, 이벤트 대기/슬립 등을 간단히 구성하는 방법을 설명합니다.
작성일: 2025년 11월 03일
아마도 우리가 어딘가에서 에이전트를 만들고 있다는 건 놀랍지 않을 겁니다. 모두가 그렇게 하죠. 하지만 좋은 에이전트를 만드는 일은 내구성 실행(durable execution)과 관련된 과거의 난제를 다시 소환합니다.
완전히 놀랍지 않게도, 요즘 많은 사람들이 내구성 실행 시스템을 만들고 있습니다. 그러나 그중 상당수는 믿을 수 없을 만큼 복잡하고, 또 다른 서드파티 서비스에 가입하도록 요구합니다. 저는 가능하면 불필요한 복잡성을 피하려 하기 때문에, Postgres만으로 어디까지 갈 수 있는지 확인해 보고 싶었습니다. 이를 위해 저는 Absurd1라는, Postgres 위에서 내구성 워크플로를 가능하게 하는 SQL 전용의 아주 작은 라이브러리와 매우 얇은 SDK를 작성했습니다 — 확장(extension)도 필요 없습니다.
내구성 실행(또는 내구성 워크플로)은 상태를 잃거나 작업을 중복하지 않으면서 충돌, 재시작, 네트워크 장애를 버텨 내는 장수형 함수들을 실행하는 방법입니다. 내구성 실행은 최근 실행 상태를 기억하는 상태 저장소와 큐 시스템의 결합으로 이해할 수 있습니다.
Postgres는 SELECT ... FOR UPDATE SKIP LOCKED 덕분에 큐로 훌륭하게 동작하므로 (예: pgmq) 큐로 사용할 수 있습니다. 그리고 데이터베이스이기 때문에 상태 저장에도 사용할 수 있죠.
상태는 중요합니다. 내구성 실행에서는 로직을 메모리에서 직접 돌리는 대신, 작업을 더 작은 조각(스텝 함수)으로 분해하고 매 스텝과 결정을 기록합니다. 프로세스가 중단되면(실패하든, 의도적으로 일시 중단되든, 머신이 죽든), 엔진은 그 이벤트들을 리플레이하여 정확한 상태를 복원하고 아무 일도 없었던 것처럼 멈춘 지점부터 이어서 실행할 수 있습니다.
Absurd의 핵심은 하나의 .sql 파일(absurd.sql)이며, 원하는 데이터베이스에 적용하면 됩니다. 이 SQL 파일의 목표는 SDK의 복잡함을 데이터베이스로 옮기는 것입니다. 그 위에 SDK가 여러분이 사용하는 언어의 인체공학을 살려 저수준 연산을 추상화해 편리함을 제공합니다.
시스템은 매우 단순합니다. 하나의 작업(task)이 특정 큐(queue)로 디스패치되고, 거기서 워커(worker)가 집어 올려 처리합니다. 작업은 스텝(step)으로 세분화되며, 워커가 순서대로 실행합니다. 작업은 일시 중단되거나 실패할 수 있으며, 그렇게 되면 다시 실행(run)됩니다. 각 스텝의 결과는 데이터베이스에 체크포인트(checkpoint)로 저장됩니다. 중복 작업을 피하기 위해 체크포인트는 Postgres의 상태 저장소에서 자동으로 다시 불러옵니다.
추가로, 작업은 잠들기(sleep)나 이벤트 대기(suspend for events)를 할 수 있으며, 이벤트가 발생할 때까지 기다립니다. 이벤트는 캐시되므로 레이스 프리입니다.
에이전트와 워크플로는 어떤 관계일까요? 일반적으로 워크플로는 사람이 사전에 정의한 DAG입니다. 반면 AI 에이전트는 진행하면서 스스로 모험을 정합니다. 즉, 대부분 단일 스텝으로 이루어진 워크플로처럼 동작하며, 상태가 변하는 동안 반복을 돌다가 완료 조건을 만족하면 종료합니다. Absurd는 반복 스텝에 대해 자동으로 스텝 카운트를 증가시켜 이를 가능하게 합니다:
jsabsurd.registerTask({name: "my-agent"}, async (params, ctx) => { let messages = [{role: "user", content: params.prompt}]; let step = 0; while (step++ < 20) { const { newMessages, finishReason } = await ctx.step("iteration", async () => { return await singleStep(messages); }); messages.push(...newMessages); if (finishReason !== "tool-calls") { break; } } });
이는 my-agent라는 하나의 작업을 정의하며, 스텝은 단 하나입니다. 반환값은 변경된 상태이지만, 현재 상태는 인자로 전달됩니다. 스텝 함수가 실행될 때마다 먼저 체크포인트 저장소에서 데이터가 조회됩니다. 첫 번째 체크포인트는 iteration, 두 번째는 iteration#2, 세 번째는 iteration#3처럼 이어집니다. 각 상태에는 전체 메시지 히스토리가 아니라 해당 스텝에서 새로 생성한 메시지만 저장합니다.
어떤 스텝이 실패하면 작업이 실패하고 재시도됩니다. 그리고 체크포인트 저장 덕분에 5번째 스텝에서 크래시가 나면, 처음 4개 스텝은 저장소에서 자동으로 불러옵니다. 스텝 자체는 재시도되지 않고, 작업 단위로만 재시도됩니다.
어떻게 시작할까요? 큐에 넣기만 하면 됩니다:
jsawait absurd.spawn("my-agent", { prompt: "What's the weather like in Boston?" }, { maxAttempts: 3, });
그리고 참고 삼아, 위에서 사용한 singleStep 함수의 예시 구현은 다음과 같습니다.
단일 스텝 함수
jsasync function singleStep(messages) { const result = await generateText({ model: anthropic("claude-haiku-4-5"), system: "You are a helpful agent", messages, tools: { getWeather: { /* 여기 도구 정의 */ } }, }); const newMessages = (await result.response).messages; const finishReason = await result.finishReason; if (finishReason === "tool-calls") { const toolResults = []; for (const toolCall of result.toolCalls) { /* 여기에서 툴 호출 처리 */ if (toolCall.toolName === "getWeather") { const toolOutput = await getWeather(toolCall.input); toolResults.push({ toolName: toolCall.toolName, toolCallId: toolCall.toolCallId, type: "tool-result", output: {type: "text", value: toolOutput}, }); } } newMessages.push({ role: "tool", content: toolResults }); } return { newMessages, finishReason }; }
Temporal 같은 솔루션들처럼, 원하면 양보(yield)할 수도 있습니다. 예를 들어 7일 뒤에 문제로 돌아오고 싶다면 이렇게 하면 됩니다:
jsawait ctx.sleep(60 * 60 * 24 * 7); // 7일 동안 슬립
혹은 이벤트를 기다리고 싶다면:
jsconst eventName = `email-confirmation-${userId}`; try { const payload = await ctx.waitForEvent(eventName, {timeout: 60 * 5}); // 이벤트 페이로드 처리 } catch (e) { if (e instanceof TimeoutError) { // 타임아웃 처리 } else { throw e; } }
다른 곳에서 다음처럼 이벤트를 발생시킬 수 있습니다:
jsconst eventName = `email-confirmation-${userId}`; await absurd.emitEvent(eventName, { confirmedAt: new Date().toISOString() });
정말로, 이게 전부입니다. 별다른 게 없습니다. 큐와 상태 저장소 — 필요한 건 그것뿐입니다. 컴파일러 플러그인도, 별도의 서비스도, 전체 런타임 통합도 없습니다. 그냥 Postgres뿐이죠. 이것이 다른 솔루션들을 깎아내리려는 건 아닙니다. 그 솔루션들은 훌륭합니다. 다만 모든 문제가 그 정도의 복잡도로 스케일해야 하는 것은 아니며, 훨씬 덜 복잡한 것으로도 꽤 멀리 갈 수 있습니다. 특히 다른 사람들이 셀프 호스팅할 수 있는 소프트웨어를 만들고 싶다면 더욱 매력적일 수 있습니다.
이 글의 태그: ai, announcements