Pool¶
Guía: Pool. El calentamiento de plazas para un agente
(PoolConfig.warmup) usa agent_pool_warmup y WarmupStep, documentados
en Subclientes: imagen y arranque rápido;
cuándo compensa: ¿Qué uso?.
SandboxPool(config: PoolConfig, *, backend: PoolBackend | None = None, region: str | None = None, session: boto3.session.Session | None = None, control_plane: ControlPlane | None = None, transport: TransportSettings | None = None, monotonic: Callable[[], float] = time.monotonic, now: Callable[[], datetime] = utc_now, sleep: Callable[[float], None] | None = None, random: Callable[[], float] = random.random)
¶
Pool de sandboxes suspendidos. Arrancar con start() (o with),
tomar con take(), cerrar con close().
sleep es la espera del backoff del relleno; por defecto es
interrumpible por close(). monotonic, now y random son
inyectables para los tests.
session: boto3.session.Session | None
property
¶
La sesión boto3 del pool: con ella firman sus plazas las URLs y
las transferencias enrutadas de transfer=S3Staging(...).
start() -> Self
¶
Resuelve la imagen, recupera el backend, barre y arranca el hilo de relleno. Idempotente.
close(*, drain: bool = True) -> None
¶
Para el relleno y, con drain, termina todas las plazas ready.
drain=False sólo con un backend persistente: deja las plazas
aparcadas para que otro pool las recupere. Idempotente.
take(*, wait: float = 0.0, ready_timeout: float = DEFAULT_READY_TIMEOUT_SECONDS, request_timeout: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, reconnect_timeout: float = DEFAULT_RECONNECT_TIMEOUT_SECONDS, secrets: Mapping[str, str | SecretRef] | None = None, secret_cache: SecretCache | None = None, gateways: Mapping[str, SecretGateway] | None = None) -> Sandbox
¶
La plaza ready más vieja con al menos min_remaining_seconds de
vida, reanudada y abierta con su token; sin plaza (tras esperar hasta
wait s) o con una plaza muerta, un create() normal.
Coste y activación¶
Activa: secrets={"ENV": "nombre" | SecretRef} enlaza secretos al
sandbox que sale del pool (sólo en take(): las plazas calientes
nunca los llevan, ni en su lanzamiento ni en su SlotRecord).
Recursos y llamadas AWS: secretsmanager:GetSecretValue una vez por
secreto y TTL de SecretCache, antes de reclamar la plaza.
Coste aproximado: $0,05 por 10 000 llamadas (us-east-1, 2026-09-30).
IAM: secretsmanager:GetSecretValue en las credenciales del llamante.
Cómo apagarla: secrets=None y secret_cache=None (por defecto).
Ejemplo:
sbx = pool.take(secrets={"OPENAI_API_KEY": "openai"})
sbx.commands.run("python agent.py")
gateways={"nombre": SecretGateway(...)} abre la pasarela de
secretos (ADR-023) en el sandbox que sale del pool, igual que
Sandbox.create(gateways=): las plazas calientes nunca la llevan.
Recursos y llamadas AWS: secretsmanager:GetSecretValue una vez por
secreto inyectado y TTL de SecretCache (y uno por secreto en
cada sbx.gateways.refresh()); ningún recurso nuevo.
Coste aproximado: el mismo $0,05 por 10 000 llamadas de arriba.
IAM: secretsmanager:GetSecretValue en las credenciales del llamante.
Cómo apagarla: gateways=None (por defecto); sin él, take() no
hace ni un ConfigureSandbox. Exige una imagen 0.6.0 o
posterior: con una anterior, take() termina la plaza tomada y
lanza UnimplementedError.
Ejemplo:
sbx = pool.take(gateways={"anthropic": SecretGateway(
upstream="https://api.anthropic.com",
headers={"x-api-key": "anthropic"},
allow=[("POST", "/v1/messages")],
)})
AsyncSandboxPool(config: PoolConfig, *, backend: PoolBackend | None = None, region: str | None = None, session: boto3.session.Session | None = None, control_plane: ControlPlane | None = None, transport: TransportSettings | None = None, monotonic: Callable[[], float] = time.monotonic, now: Callable[[], datetime] = utc_now, sleep: AsyncSleeper | None = None, random: Callable[[], float] = random.random)
¶
Pool de sandboxes suspendidos para asyncio: await start() (o
async with), await take(), await close(); stats() es síncrono.
session: boto3.session.Session | None
property
¶
La sesión boto3 del pool: con ella firman sus plazas las URLs y
las transferencias enrutadas de transfer=S3Staging(...).
take(*, wait: float = 0.0, ready_timeout: float = DEFAULT_READY_TIMEOUT_SECONDS, request_timeout: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, reconnect_timeout: float = DEFAULT_RECONNECT_TIMEOUT_SECONDS, secrets: Mapping[str, str | SecretRef] | None = None, secret_cache: SecretCache | None = None, gateways: Mapping[str, SecretGateway] | None = None) -> AsyncSandbox
async
¶
Misma semántica que SandboxPool.take, secrets= y gateways=
incluidos.
Coste y activación¶
Activa: secrets= enlaza secretos al sandbox que sale del pool.
Recursos y llamadas AWS: secretsmanager:GetSecretValue una vez por
secreto y TTL de SecretCache, antes de reclamar la plaza.
Coste aproximado: $0,05 por 10 000 llamadas (us-east-1, 2026-09-30).
IAM: secretsmanager:GetSecretValue en las credenciales del llamante.
Cómo apagarla: secrets=None y secret_cache=None (por defecto).
Ejemplo:
sbx = await pool.take(secrets={"OPENAI_API_KEY": "openai"})
gateways= abre la pasarela de secretos (ADR-023) en el sandbox
tomado; mismo coste, IAM y forma de apagarla (gateways=None, por
defecto) que en SandboxPool.take.
PoolConfig(size: int, template: str | None = None, template_version: str | None = None, timeout: int = MAX_DURATION_SECONDS, idle: IdlePolicy = DEFAULT_IDLE_POLICY, envs: Mapping[str, str] | None = None, metadata: Mapping[str, str] | None = None, cpu_time_limit: int | None = None, execution_role_arn: str | None = None, ingress: Sequence[str] | None = None, egress: Sequence[str] | None = None, logging: LoggingOption = 'disabled', min_remaining_seconds: int = DEFAULT_MIN_REMAINING_SECONDS, fill_concurrency: int = DEFAULT_FILL_CONCURRENCY, sweep_interval_seconds: float = DEFAULT_SWEEP_INTERVAL_SECONDS, ready_timeout: float = DEFAULT_READY_TIMEOUT_SECONDS, index: DynamoDbIndex | None = None, warmup: Sequence[WarmupStep] = (), allow_internet_access: bool = True, network: NetworkInput = None)
dataclass
¶
Configuración de lanzamiento, inmutable y común a todas las plazas.
Cada campo se corresponde con el kwarg homónimo de Sandbox.create():
envs, metadata, cpu_time_limit, la política de idle, la vida máxima
y los conectores son por pool, nunca por toma. timeout es 28 800 por
defecto (aparcar todo lo que AWS permite) y min_remaining_seconds la
vida mínima con la que se entrega una plaza: por debajo se recicla.
idle es obligatorio y con auto_resume=True: sin política de idle el
comportamiento de un VM suspendido por la API no está documentado y con
ella una plaza olvidada se termina sola en timeout. No hay
access_token (uno por plaza) ni allowed_ports (get_host(port) acuña
por puerto tras la toma).
index=DynamoDbIndex(...) (None por defecto) escribe la fila del
índice de metadatos de cada plaza al lanzarla, con la metadata del
pool: así Sandbox.list(metadata=..., states=["SUSPENDED"], index=...)
encuentra también las plazas aparcadas. Una escritura fallida con
on_write_failure='terminate' descarta la plaza como cualquier otro
calentamiento fallido.
warmup (vacío por defecto) son pasos de calentamiento que cada plaza
corre tras asentarse y antes de su pause(): uno de primer plano espera
su salida (un exit distinto de 0 o su plazo es un calentamiento fallido:
la plaza se termina y el relleno aplica el backoff); uno con
background=True se arranca y se suelta. Un reciclado relanza la plaza
y vuelve a correrlos. agent_pool_warmup() da los del agente de IA;
para los turnos de una misma conversación basta con pausar su VM.
allow_internet_access y network son la política de egress de todas
las plazas, como en Sandbox.create(); un pool de agentes debe pasar
allow_internet_access=False. Las pasarelas siguen aplicándose sólo en
take().
Coste y activación¶
Activa: PoolConfig(index=DynamoDbIndex("tabla")) escribe una fila por
plaza lanzada (relleno, reciclado y caídas a create()).
Recursos y llamadas AWS: dynamodb:PutItem una vez por plaza lanzada; sin
index= ningún cliente dynamodb.
Coste aproximado: ~1 WRU por plaza ≈ $0,000000625 (DynamoDB on-demand,
us-east-1, consultado 2026-09-30) + $0,25/GB-mes almacenado.
IAM: dynamodb:PutItem sobre la tabla (política RayitoIndexWriter de
infra/metadata-index.yaml) en las credenciales del proceso del pool.
Cómo apagarla: index=None (por defecto).
Ejemplo:
idx = DynamoDbIndex("rayito-sandboxes")
pool = SandboxPool(PoolConfig(size=2, metadata={"pool": "a"}, index=idx))
PoolStats(size: int, ready: int, warming: int, takes: int, hits: int, misses: int, launched: int, recycled: int, lost: int, failed: int, slots: tuple[PoolSlotInfo, ...] = ())
dataclass
¶
Contadores acumulados desde start() y las plazas actuales.
takes == hits + misses; launched cuenta cada run-microvm aceptado
(calentamientos y fallbacks), recycled los reciclados del sweeper,
lost las plazas perdidas (reconciliación, toma o recuperación) y
failed los calentamientos y tomas que lanzaron una excepción.
PoolSlotInfo(sandbox_id: str, state: SlotState, started_at: datetime, expires_at: datetime, parked_at: datetime | None)
dataclass
¶
Una plaza vista desde stats(): nunca lleva el token.
PoolBackend
¶
Bases: Protocol
Registros de plaza indexados por sandbox_id.
InMemoryPoolBackend()
¶
Un dict: los registros (y sus secretos) mueren con el proceso.
JsonFilePoolBackend(path: str | os.PathLike[str])
¶
{"schema": "rayito.pool/1", "slots": [...]} en path.
Cada escritura vuelca el fichero completo a un temporal de nombre
aleatorio en el mismo directorio, creado en exclusiva y con el modo
fijado sobre el descriptor (en Windows el modo no tiene efecto), y lo
promueve con os.replace, así un corte a mitad deja el fichero anterior
intacto y nadie puede precrear el temporal para quedarse con los tokens.
La lectura se niega a seguir un enlace en la propia ruta del estado. Un
fichero ausente carga (); otro schema es InvalidArgumentException.
El mismo esquema lo lee y escribe el SDK TypeScript.