AWS Lambda 지속성

AWS Lambda 지속성 (AWS Lambda Durability)

AWSLambdaDurability는 에이전트를 AWS Lambda durable functions 위에서 재개 가능하게 만들어요. 모든 모델 요청, 함수 도구 호출, MCP 호출, 다이내믹 툴셋 해석이 durable 스텝으로 체크포인트되어, 타임아웃·실패·재시도된 호출이 이미 지불한 작업을 반복하는 대신 마지막 완료 스텝부터 계속돼요.

Lambda는 durable 작업 로그를 유지해요. 실행이 재개되면 핸들러가 처음부터 다시 실행되고, 완료된 스텝은 실행하는 대신 저장된 결과를 반환해요. 체크포인트가 없으면 재개된 실행이 모든 모델 요청과 도구 호출을 반복할 거예요.

출처: 문서

본문

설치 (Installation)

pip install "pydantic-ai-harness[aws-lambda]"
uv add "pydantic-ai-harness[aws-lambda]"

AWS Durable Execution SDK는 Python 3.11 이상이 필요해요. 아래 빠른 시작은 Bedrock 제공자 모델을 쓰고, 그건 pydantic-ai-slim[bedrock]의 Bedrock SDK가 필요해요.

pip install "pydantic-ai-harness[aws-lambda]" "pydantic-ai-slim[bedrock]"
uv add "pydantic-ai-harness[aws-lambda]" "pydantic-ai-slim[bedrock]"

빠른 시작 (Quick start)

에이전트를 만들 때 capability를 붙이고, durable_agent_handler로 async 핸들러 본문을 적응시키세요.

from typing import Any

from aws_durable_execution_sdk_python import DurableContext, durable_execution
from pydantic_ai import Agent

from pydantic_ai_harness.aws_lambda import AWSLambdaDurability, durable_agent_handler

agent = Agent(
    'bedrock:us.amazon.nova-pro-v1:0',
    name='support',
    capabilities=[AWSLambdaDurability()],
)


@agent.tool_plain
def get_weather(city: str) -> str:
    return f'It is sunny in {city}.'


@durable_execution
@durable_agent_handler
async def handler(event: dict[str, Any], context: DurableContext) -> str:
    result = await agent.run(str(event['prompt']))
    return result.output

capability를 실행마다가 아니라 생성 시 붙이세요. 실행별 붙이기도 작동하지만(어느 쪽이든 래핑과 toolset 바인딩은 됨), 한 번 붙이면 래핑과 배포된 스텝 형태가 호출 전반에서 안정적이고 다른 지속성 통합과도 맞아요.

진행 중 실행은 시작한 버전에 고정되므로, durable 구성으로 배포하고 발행된 버전을 호출하세요.

aws lambda create-function \
  --function-name support-agent \
  --runtime python3.13 \
  --handler handler.handler \
  --role <ROLE_ARN> \
  --zip-file fileb://support-agent.zip \
  --timeout 300 --memory-size 1024 \
  --durable-config '{"ExecutionTimeout":3600,"RetentionPeriodInDays":7}'

aws lambda publish-version --function-name support-agent

실행은 durable 핸들러 브리지 안에서만 durable해요

AWSLambdaDurability를 붙인다고 그 자체로 실행이 durable해지진 않아요. durable_agent_handlerrun_durable을 통해서 들어간 실행만 체크포인트돼요. agent.run_sync(...)을 부르거나, 자신의 asyncio.run(...)에서 에이전트를 await하면 경고 없이 완전히 작동하지만 non-durable 실행이 만들어져요.

요구 사항 (Requirements)

에이전트는 name(또는 AWSLambdaDurability(name=...))이 필요하고, 모든 리프 toolset은 고유한 id가 필요해요. 둘 다 모든 스텝 이름의 일부라, 에이전트를 만들 때 둘 다 검사돼요. 이름 없는 에이전트는 Agent(...)에서 UserError를 올리고, id가 없거나 다른 toolset과 공유하는 toolset도 마찬가지예요. 에이전트에 직접 등록된 도구는 id<agent>로 렌더링되는 toolset에 살아서, @agent.tool_plain def get_weather{name}__function_toolset__<agent>.call_tool:get_weather로 체크포인트돼요.

sync 핸들러와 async 에이전트가 연결되는 방식

Lambda의 durable API는 동기예요. context.step(...)이 블로킹하고, 모든 스텝은 Lambda가 호출한 스레드에서 만들어져야 해요. 에이전트 실행은 async죠. durable_agent_handlerrun_durable을 써서 둘을 잇는데, async 핸들러 본문을 백그라운드 이벤트 루프에 호스팅하고 Lambda 핸들러 스레드에서 그 스텝을 서비스해서 모든 스텝이 하나의 연속 시퀀스로 만들어져요. 스텝 본문은 작업을 에이전트 루프에 넘기고 끝날 때까지 블로킹해, 핸들러 스레드가 기다리는 동안 루프가 자유롭게 유지돼요.

async 본문은 두 에이전트 실행을 await하거나, asyncio.gather를 쓰거나, 실행 사이에 async 후처리를 수행할 수 있고, 그 모든 구간이 같은 브리지와 스텝 시퀀스를 공유해요. 동기 핸들러는 각 async 구간마다 run_durable을 따로 불러야 하고, 동시 호출은 거부돼요.

알아둘 결과 세 가지:

  • @durable_execution은 가장 바깥 데코레이터여야 해요. 그 래퍼가 Lambda가 호출하는 것이니까요. 순서를 뒤집으면 핸들러 정의 시 UserError가 올라와요.
  • run_durable은 호출 스레드를 블로킹해, 실행 중인 이벤트 루프 안에서 부를 수 없어요. 동기 핸들러에서 직접 부르세요. 핸들러 꼭대기가 아닌 어딘가에서 브리지에 들어가야 할 때도 여전히 쓸 수 있어요.
  • durable 스텝은 중첩될 수 없어요. 다른 durable 에이전트 실행을 시작하는 도구는 교착 대신 설명적인 오류와 함께 거부돼요.

루프는 따뜻한 실행 환경의 호출들 사이에 재사용돼서, 제공자의 캐시된 HTTP 클라이언트 같은 루프 결합 자원이 그 사이 유효하게 유지돼요. 중단이나 오류로 버려진 실행은 핸들러가 반환하기 전에 취소되고, run_durablecancel_timeout초(기본 5)를 기다려 풀리게 해요. 정리가 정말 느린 워크로드에는 그걸 올려요. 타임아웃이 만료되면 버려진 정리는 은퇴한 루프에서 은퇴 루프의 유예 기간만큼 계속 실행되고 다음 따뜻한 호출과 겹칠 수 있어요. 에이전트 실행과 핸들러 사이에 변경 가능한 모듈 전역 상태를 공유하지 마세요. 저장소에 쓰기, 공유 잠금 해제, 메트릭 발행 같은 정리로부터의 외부 부작용도 이후 호출 중에 떨어질 수 있어요. 루프 결합 자원은 격리돼요. 다음 호출은 새 루프를 받으므로 버려진 정리가 그 호출의 제공자 HTTP 클라이언트 같은 자원을 건드릴 수 없어요.

루프 재사용에 한 가지 더 결과가 있어요. asyncio.create_task()로 도구에서 백그라운드 작업을 떼어내거나 executor 작업을 await 없이 남기지 마세요. 떼어진 작업은 체크포인트되지 않고 durable 실행의 보장을 받지 못하며, 시작한 호출보다 오래 살 수 있어요.

무엇이 체크포인트되나 (What gets checkpointed)

스텝 이름은 에이전트의 name과 각 toolset의 id로 만들어져요.

스텝 이름 작업
{name}__model.request 모델 요청 세그먼트 하나
{name}__model.request_stream 스트리밍 모델 요청 세그먼트 하나
{name}__model.compact_messages 모델 메시지 컴팩션 작업 하나
{name}__model.cancel_suspended_response 중단된 응답 해체
{name}__capability__{capability_id}.{operation} 다른 capability가 만든 작업 하나
{name}__function_toolset__{id}.validate_args 함수 도구 호출 인수 검증
{name}__function_toolset__{id}.call_tool:{tool} 함수 도구 호출
{name}__mcp_server__{id}.get_tools MCP 서버 도구 나열
{name}__mcp_server__{id}.get_instructions MCP 서버 지시문
{name}__mcp_server__{id}.call_tool MCP 도구 호출
{name}__dynamic_toolset__{id}.get_tools 다이내믹 toolset 해석
{name}__dynamic_toolset__{id}.validate_args 다이내믹 toolset 호출 인수 검증
{name}__dynamic_toolset__{id}.call_tool:{tool} 다이내믹 toolset의 도구 호출
{name}__event_stream_handler event_stream_handler에 전달된 이벤트 하나

에이전트의 기본 모델을 쓰지 않는 모델 작업은 스텝 이름에 모델 id를 기록해요(예: {name}__model.request.{model_id}). 그래서 재개된 실행이 각 체크포인트를 기록된 모델에 매핑해요. 기본 모델은 평범한, 접미사 없는 이름을 유지해요.

제약 (Constraints)

  • 스텝은 적어도 한 번이고, 기본적으로 재시도돼요. 스텝은 실행 후 체크포인트되므로, 도구의 부작용과 체크포인트 사이의 중단은 실행이 재개될 때 도구를 다시 실행해요. 게다가 SDK 기본 재시도 정책은 지수 백오프(5s~60s)로 6회 시도이고, 모든 모델 요청과 도구 호출에 적용돼요. 도구 부작용을 멱등으로 유지하세요. AT_MOST_ONCE_PER_RETRY만으로는 도구를 한 번 실행하게 할 수 없어요. 시도 안에서 중단 후 재실행을 막을 뿐이고, 재시도 정책은 본문을 실행하는 더 많은 시도를 여전히 시작해요. 반복해서는 안 되는 도구는 둘 다 설정하세요.

    from aws_durable_execution_sdk_python.config import StepSemantics
    from aws_durable_execution_sdk_python.retries import RetryPresets
    
    metadata={'aws_lambda': {'step_semantics': StepSemantics.AT_MOST_ONCE_PER_RETRY,
                             'retry_strategy': RetryPresets.none()}}
    
  • 재시도가 겹쳐 쌓여요. Pydantic AI와 프로바이더 클라이언트는 자체 재시도 로직이 있어요. 스텝 재시도 정책과 함께 켜두면 시도가 배가되고 Retry-After를 잘못 다뤄요. 한쪽을 꺼두세요.

  • 도구 호출은 한 번에 하나씩 실행돼요. 스텝의 정체성은 스텝에 도달한 순서에서 오므로, 동시에 예약된 도구 호출이 실행 재개 시 서로의 체크포인트를 가로챌 수 있어요. durable 핸들러 안에서는 실행이 순차 도구 실행으로 전환돼요. 밖에서는 에이전트가 구성된 병렬성을 유지해요.

  • 실행 형태를 바꾸면 진행 중 실행이 깨져요. 재개된 실행은 도달한 작업 순서로 체크포인트를 일치시키므로, 스텝 수나 순서를 바꾸는 것은 옛 코드로 시작된 실행을 깨뜨려요. 도구·MCP 서버 추가/제거, metadata={'aws_lambda': False} 옵트아웃 뒤집기, event_stream_handler 추가(모델 스텝을 model.request_stream으로 바꾸고 핸들러 스텝 추가), 스텝 이름 접미사를 바꾸는 모델 변경 등요. 에이전트나 toolset id 이름을 바꾸면 기록된 이름도 바뀌어요. 새 발행 버전으로 배포하고 진행 중 실행은 옛 버전에서 소진시키세요.

  • 스텝 결과는 SDK 직렬화기를 살아남아야 해요. 결과는 Lambda SDK 직렬화기로 체크포인트돼요. 도구 결과는 먼저 Pydantic으로 인코딩되어 ToolReturn, BinaryContent 같은 구조화 반환은 왕복하지만, Pydantic이 직렬화할 수 없는 값은 안 돼요.

  • 이벤트는 실행 중인 durable 실행을 떠날 수 없어요. run_streamiter는 핸들러 안에서 작업하고 정상적으로 체크포인트되지만, durable 실행은 완료 시 단일 값을 반환하므로 실행 중 호출자에게 토큰을 스트리밍할 채널이 없어요. event_stream_handler는 작동해요. 모델 이벤트는 모델 스텝 안에서 실시간 처리되고 각 에이전트 수준 이벤트는 자체 스텝에서 체크포인트돼요.

  • ctx.enqueue()는 durable 스텝 안에서는 쓸 수 없어요. 체크포인트된 도구든 event_stream_handler(모델 이벤트는 모델 스텝 안, 에이전트 이벤트는 자체 스텝)든, 재개된 실행이 기록된 스텝 출력을 서비스하고 enqueue된 메시지를 버리기 때문이에요. 핸들러 수준 코드에서 enqueue하세요.

  • 예산. durable 실행은 3,000개 작업과 누적 체크포인트 상태 100MB를 허용해요. 한 턴은 모델 스텝 하나 + 도구 호출당 스텝 하나를 쓰므로 작업 예산은 넉넉하지만, 큰 도구 결과가 상태 예산을 소비해요. blob 대신 참조(S3 키 같은)를 반환하세요.

도구별 구성 (Per-tool configuration)

aws_lambda 키 아래의 도구 메타데이터가 그 도구의 스텝을 구성해요. StepConfig 필드 retry_strategy, step_semantics, serdes를 받아요.

from aws_durable_execution_sdk_python.config import StepSemantics
from pydantic_ai.toolsets import FunctionToolset

toolset = FunctionToolset(id='billing')


@toolset.tool_plain(metadata={'aws_lambda': {'step_semantics': StepSemantics.AT_MOST_ONCE_PER_RETRY}})
def charge_card(amount: int) -> str:
    return f'charged {amount}'

metadata={'aws_lambda': False}는 도구를 체크포인트에서 완전히 빼서, 매 시도마다 인라인으로 실행돼요. 결과가 체크포인트를 아깝지 않은 싸고 부작용 없는 도구에 쓰세요. MCP 도구는 옵트아웃할 수 없어요. 실행이 재개될 때 다시 실행되면 안 되는 I/O를 수행하니까요.

AWSLambdaDurability(step_config=...)는 모든 스텝의 기본 구성을 설정해요. 도구별 메타데이터가 키 단위로 덮어써서, step_semantics만 설정한 도구는 기본 retry_strategy를 유지해요.

다른 capability와의 조합

AWSLambdaDurability는 스스로를 가장 안쪽으로 정렬해서, 모델 요청에 대한 다른 capability의 기여가 이미 durable 스텝 안에서 적용돼요. 다른 capability와 함께 평소대로 붙이세요.

API 참조 (API reference)

AWSLambdaDurability

Bases: BaseDurabilityCapability[AgentDepsT]

에이전트의 I/O를 AWS Lambda durable 스텝으로 체크포인트하는 capability.

capabilities=[AWSLambdaDurability()]로 붙이고 async 핸들러를 durable_agent_handler로 데코레이션해요. 모든 모델 요청, 함수 도구 호출, MCP 호출, 다이내믹 toolset 해석이 DurableContext.step(...)으로 감싸져요. 완료된 스텝은 실행 재개 시 체크포인트에서 제공되어, 끝낸 작업이 반복되지 않고 토큰이 다시 쓰이지 않아요.

스텝은 실행 후 체크포인트되므로, 도구의 부작용과 체크포인트 사이의 중단은 실행 재개 시 도구를 재실행해요. 도구 부작용을 멱등으로 유지하거나, 견딜 수 없는 도구는 step_semanticsAT_MOST_ONCE_PER_RETRY로 설정하세요.

durable 핸들러 밖에서는 capability가 투명하고 실행은 평범한 에이전트 실행이에요.

스텝 결과는 Lambda SDK 직렬화기로 체크포인트되므로, 체크포인트된 도구의 반환값은 그 왕복을 견뎌야 해요. 제어 흐름 신호(ModelRetry, ApprovalRequired, CallDeferred, ToolFailed)는 예외가 아니라 값으로 경계를 건너므로, 승인과 지연 도구 흐름이 durable 실행 안에서 작동해요.

메서드
  • __init__def __init__(*, models=None, event_stream_handler=None, name=None, step_config=None) -> None. AWSLambdaDurability capability를 만들. 에이전트의 모델, 이름, toolset은 capability가 바인딩될 때 발견돼요.
    • models: ID로 키 지정된 선택적 추가 모델, agent.run(model='<id>')로 런타임 모델 전환용. ID는 스텝 이름에 접혀 재개된 실행이 각 체크포인트를 기록된 모델에 매핑해요. Default: None
    • event_stream_handler: 선택적 이벤트 스트림 핸들러. 모델 이벤트는 모델-요청 스텝 안에서 실시간 처리되고, 각 에이전트 수준 이벤트는 자체 체크포인트 스텝에서 처리돼요. Default: None
    • name: 모든 스텝 이름의 접두사로 쓰이는 고유 에이전트 이름. capability 바인딩 시 에이전트의 name 기본값. Default: None
    • step_config: retry_strategy, step_semantics, serdes 매핑으로서 모든 스텝에 적용되는 기본 StepConfig 필드. 도구별 metadata={'aws_lambda': {...}}가 그 도구에 대해 키 단위로 덮어써요. Default: None

durable_agent_handler

def durable_agent_handler(
    func: Callable[[Any, DurableStepContext], Coroutine[Any, Any, T]],
    /,
) -> Callable[[Any, DurableStepContext], T]
def durable_agent_handler(
    func: None = None,
    /,
    *,
    cancel_timeout: float = DEFAULT_CANCEL_TIMEOUT_SECONDS,
) -> Callable[[Callable[[Any, DurableStepContext], Coroutine[Any, Any, T]]], Callable[[Any, DurableStepContext], T]]

async 핸들러 본문을 AWS durable 실행 데코레이터에 맞게 적응시켜요.

@durable_execution은 가장 바깥이어야 해요. 그 동기 래퍼가 Lambda가 호출하는 것이니까요.

  • func: async durable 핸들러 본문. Default: None
  • cancel_timeout: 버려진 핸들러 본문이 풀리길 기다리는 초. run_durable 참고. Default: DEFAULT_CANCEL_TIMEOUT_SECONDS

run_durable

def run_durable(
    agent_run: Callable[[], Coroutine[Any, Any, T]],
    *,
    context: DurableStepContext,
    cancel_timeout: float = DEFAULT_CANCEL_TIMEOUT_SECONDS,
) -> T

동기 Lambda durable 핸들러에서 async 에이전트 호출을 실행. agent_run()을 백그라운드 이벤트 루프에 호스팅하고 durable 스텝을 호출(핸들러) 스레드에서 서비스해, 모든 context.step(...)이 Lambda가 호출한 스레드에서 하나의 연속 시퀀스로 만들어져요. agent_run()이 반환하는 것을 반환해요.

  • agent_run: 실행할 coroutine을 반환하는 callable, 예: lambda: agent.run(prompt). 핸들러 호출마다(각 재생 포함) 한 번 불려요.
  • context: durable 핸들러가 호출된 DurableContext.
  • cancel_timeout: 반환 전에 중단이나 오류로 버려진 실행이 풀리길 기다리는 초. 정리가 정말 느린 워크로드에는 올려요. 만료되면 백그라운드 이벤트 루프가 은퇴해 다음 호출이 새 루프를 만들고 정리가 제공자의 캐시된 HTTP 클라이언트 같은 루프 결합 자원을 건드릴 수 없어요. 정리는 은퇴 루프의 유예 기간 동안 그 호출 중 실행될 수 있어 모듈 전역 상태나 외부 시스템에 여전히 영향을 줄 수 있어요. Default: DEFAULT_CANCEL_TIMEOUT_SECONDS

AgentLoopGone

Bases: BaseException

durable 스텝이 진행 중일 때 에이전트 루프가 멈춰서 그 결과가 결코 도착할 수 없음.

의도적으로 Exception이 아니라 BaseException이에요. consume()은 평범한 스텝 실패를 에이전트 실행으로 다시 라우팅해 처리하게 하는데, 여기서는 정확히 작동할 수 없는 것이죠. 그걸 받을 루프가 사라진 루프니까요. SDK 자체 제어 흐름처럼, 이것은 핸들러를 떠나야 해요.

더 알아보기 (Learn more)