SDK de tiempo de ejecución para inquilinos, independiente del framework, para el motor de agentes de Atlas.
Este es un paquete central interno; los agentes no lo instalan directamente. Instale un SDK de framework (
agent-engine-sdk-langgraphoagent-engine-sdk-adk), que depende de él.
Descripción general de la arquitectura
El SDK de Runner proporciona los componentes de integración AER, Tool Pod y Memory Server de la arquitectura de la plataforma. El motor de orquestación es un servicio independiente de Atlas Agent Engine.
Componente | Puerto | Nivel de confianza | Acceso a la red | Acceso a secretos | Propósito |
|---|---|---|---|---|---|
AER (Entorno de ejecución del agente) | 8001 | Intermedio | Solo API de LLM | Solo claves LLM | Plano de ejecución: ejecuta la lógica del agente. |
Herramienta | 8002 | Varía | Según sea necesario | Según sea necesario | Plano de datos: ejecución de herramientas aislada |
Servidor de memoria | 8081 | Intermedio | MongoDB | credenciales de la base de datos | Servicio de memoria para memoria semántica/episódica/taxonómica |
Runner-shared detecta automáticamente el enlace del oyente al inicio: :: (IPv6 pila dual: uvicorn lo muestra como http://[::]:port en formato URL) cuando el kernel tiene IPv6 habilitado con IPV6_V6ONLY=0, de lo contrario 0.0.0.0. No se requiere configuración: las implementaciones de producción en redes con IPv6 primero (espacios de nombres de inquilinos de celda) obtienen :: automáticamente y el desarrollo local de Docker (donde el IPv6 del puente suele estar deshabilitado) recurre a 0.0.0.0. El host resuelto por la sonda se registra al inicio (listener bind resolved: host=...) para que la verificación posterior a la implementación no tenga que inferirlo de la propia línea de inicio de uvicorn.
Configure APP_HOST en el entorno para anular la sonda; esto es necesario en contenedores donde el kernel informa que IPv6 está disponible, pero la red circundante solo enruta IPv4 (por ejemplo, el reenvío de puertos de Docker con host_ip: 127.0.0.1). Las plantillas de composición de la prueba de integración configuran APP_HOST=0.0.0.0 por este motivo.
El oyente utiliza el puerto predeterminado fijo para el RUNNER_MODE activo (8001/8002/8081). RUNNER_MODE es necesario y lo inyecta el entorno de ejecución de la plataforma; el desarrollador local ignora las anulaciones de .env del usuario para este, y los secretos del espacio de trabajo reservan el nombre por completo.
Registro de inicio
La imagen CMD es python -m agent_engine_runner_shared.launcher (TypeScript: node …/launcher.js). El lanzador instala el registro antes de importar AGENT_ENTRYPOINT, por lo que un fallo en el momento de la importación o una excepción de la función de punto de entrada es un registro ERROR (JSON estructurado cuando STRUCTURED_LOGGING=true) en lugar de un rastreo sin procesar que los registros del espacio de trabajo mostrarían como INFO. TenantRuntime todavía llama a setup_logging / setupLogging más tarde; esa segunda instalación es idempotente. Los fallos que ocurren antes de que comience el proceso del lanzador (errores del intérprete/imagen) todavía dependen de la heurística de ruta de lectura de API Gateway documentada en `docs/api-gateway/README.md <../../../../../docs/api-gateway/README.md#agent--operator-logs-s3-read-path>`__.
¿Por qué tres componentes?
Esta separación permite: - Registro de auditoría: OE ve todas las llamadas a herramientas/LLM sin ejecutarlas.- Aplicación de políticas: OE puede bloquear las llamadas antes de su ejecución. - Privilegios mínimos: cada componente solo tiene el acceso que necesita. - Reproducción: OE puede devolver resultados almacenados en caché para una reanudación determinista.
Ciclo de vida de ejecución y dónde están disponibles sus secretos.
Contrato normativo completo: `docs/runner/README.md <../../../../../docs/runner/README.md#execution-lifecycle--secret-availability>`__. Las reglas que un autor de agentes necesita para escribir código correcto:
El código de nivel superior del módulo (todo lo que esté fuera del cuerpo de una función) se ejecuta una vez al inicio del proceso, tanto en AER como en Tool Pod. Puede basarse en la configuración de MCP OAuth y en secretos de inicio que el inquilino puede entregar, incluidas las claves API
MONGODB_URIy LLM. Las sesiones de invocación y las credenciales de solicitud delegadas no están disponibles durante el inicio. Las escrituras de caché de MCP OAuth (~/.agentic/mcp-oauth/<server>.jsonoAGENTIC_MCP_OAUTH_DIRen entornos de ejecución implementados) se serializan en un archivo<server>.json.lockde aviso para que las actualizaciones de tokens concurrentes para el mismo servidor no se sobrescriban entre sí.Laconstrucción de la aplicación de la herramienta intenta ejecutar la función
@app.entrypointdurante el inicio para descubrir cada registroapp.llm(llm_id=...). Las solicitudes del modelo reutilizan el registro preparado. La construcción utiliza una configuración de inicio estática y no debe ejecutar un turno de negocio, una llamada al modelo ni el cuerpo de la herramienta. Si la construcción falla, se emite una advertencia y se restauran los registros anteriores, lo que permite que una solicitud de modelo posterior pueda reintentarlo. La construcción exitosa se almacena en caché incluso sin LLM con nombre. El almacenamiento en caché del gráfico AER y el calentamiento siguen siendo propiedad del marco.``app.llm(llm, llm_id=...)`` debe llamarse mientras el punto de entrada esté en la pila de llamadas, ya sea directamente en su cuerpo o en una función auxiliar a la que llame. Llamarla desde cualquier lugar fuera de eso (nivel superior del módulo, cuerpo de una herramienta, una función auxiliar invocada desde un lugar distinto al punto de entrada) genera
RuntimeErrorinmediatamente, indicando el sitio de llamada que causó el problema.Los cuerpos de herramientas locales (
is_local=True) se ejecutan únicamente en el proceso propio de AER. Tanto las cargas de trabajo de AER como las de Tool reciben secretos entregables al inquilino al iniciarse.Las herramientas remotas (
is_local=False, o herramientas que requieren credenciales delegadas) se ejecutan únicamente en el Tool Pod, bajo demanda, cuando OE les envía una llamada; nunca durante la construcción. Las declaracionesrequired_secretsdocumentan los requisitos de secreto; no restringen el entorno de secreto de inicio.Las herramientas con credenciales permanecen remotas incluso si el SDK proporcionó su configuración predeterminada local, porque la autorización delegada y la inyección de secretos con ámbito de herramienta solo ocurren en la ruta Tool Pod.
Flujo de ejecución
Ejecución normal (Inicio → Finalización)
┌────────┐ ┌─────┐ ┌─────┐ │ Client │ │ OE │ │ AER │ └───┬────┘ └──┬──┘ └──┬──┘ │ │ │ │ POST /invoke │ │ │ ──────────────────────────►│ │ │ │ │ │ │ Create Execution (PENDING) │ │ │ │ │ POST /execute │ │ │─────────────────────────►│ │ │ │ │ │ │ Build LangGraph │ │ │ Start ainvoke() │ │ │ │ ┌───────────────────────┼──────────────────────────┼───────────────┐ │ │ LOOP: For each tool/LLM call │ │ │ │ │ │ │ │ │ │ POST /tool/execute │ │ │ │ │◄─────────────────────────│ │ │ │ │ {tool, args, step} │ │ │ │ │ │ │ │ │ │ Log + Policy Check │ │ │ │ │ │ │ │ │ │ {proceed, route_to} │ │ │ │ │─────────────────────────►│ │ │ │ │ │ │ │ │ │ │ Execute tool │ │ │ │ │ │ │ │ │ POST /tool/result │ │ │ │ │◄─────────────────────────│ │ │ │ │ {result, status} │ │ │ └───────────────────────┼──────────────────────────┼───────────────┘ │ │ │ │ │ │ Graph completes │ │ │ │ │ POST /executor/callback │ │ │◄─────────────────────────│ │ │ {COMPLETED, result} │ │ │ │ │ GET /execution/{id} │ │ │ ──────────────────────────►│ │ │ │ │ │ {status: completed, result}│ │ │ ◄──────────────────────────│ │ │ │ │
Interacción humana (SUSPENDER/REANUDAR)
┌────────┐ ┌─────┐ ┌─────┐ │ Client │ │ OE │ │ AER │ └───┬────┘ └──┬──┘ └──┬──┘ │ │ │ │ POST /invoke │ │ │ ──────────────────────────►│ │ │ │ │ │ │ POST /execute │ │ │─────────────────────────►│ │ │ │ │ │ ... tool calls ... │ │ │ │ │ │ │ app.suspend(reason) │ │ │ → tool pod reports │ │ │ status: "suspend" │ │ │ (out-of-band, not │ │ │ result content) │ │ │ │ │ │ interrupt() on the │ │ │ OE-confirmed status │ │ │ Save checkpoint │ │ │ │ │ POST /executor/callback │ │ │◄─────────────────────────│ │ │ {SUSPENDED, metadata} │ │ │ │ │ GET /execution/{id} │ │ │ ──────────────────────────►│ │ │ {status: suspended} │ │ │ ◄──────────────────────────│ │ │ │ │ ╠════════════════════════════╬══════════════════════════╣ ║ ⏸️ WAITING FOR HUMAN - Reviews and Approves ║ ╠════════════════════════════╬══════════════════════════╣ │ │ │ │ POST /resume/{id} │ │ │ {decision: "approved"} │ │ │ ──────────────────────────►│ │ │ │ │ │ │ Store human decision │ │ │ │ │ │ POST /execute │ │ │ {resume: true} │ │ │─────────────────────────►│ │ │ │ │ │ │ Resume from checkpoint │ │ │ │ ┌───────────────────────┼──────────────────────────┼───────────────┐ │ │ Replayed steps return CACHED results │ │ │ │ │ │ │ │ │ │ POST /tool/execute │ │ │ │ │◄─────────────────────────│ │ │ │ │ │ │ │ │ │ {cached_result} │ │ │ │ │─────────────────────────►│ │ │ └───────────────────────┼──────────────────────────┼───────────────┘ │ │ │ │ │ │ Continue execution │ │ │ (new steps) │ │ │ │ │ POST /executor/callback │ │ │◄─────────────────────────│ │ │ {COMPLETED, result} │ │ │ │ │ GET /execution/{id} │ │ │ ──────────────────────────►│ │ │ {status: completed} │ │ │ ◄──────────────────────────│ │ │ │ │
Nota
Reanudar a través de la puerta de enlace API: El diagrama anterior utiliza la ruta interna /resume/{id} del OE para mayor brevedad. Los desarrolladores llaman a la puerta de enlace API en POST /api/v1/projects/{project_id}/executions/{execution_id}/resume con un cuerpo JSON plano:
{"decision": "approved", "reviewer_notes": "optional context"}
La forma {"human_review": {"decision": "..."}} envuelta es el contrato de punto final interno del OE; la puerta de enlace lo rechaza con HTTP 400.
Drenaje de ejecución (cancelación)
Cuando el OE cancela de forma duradera una ejecución, envía /drain al entorno de ejecución Tool/AER que lo posee. El contrato receptor (fijado por el fixture compartido en client-libraries/test-fixtures/drain/contract.json):
POST /drain {request_id, execution_id, reason, deadline_at_ms, workspace_id}— en un entorno de ejecución con ámbito de espacio de trabajo (conjuntoAPP_ID, es decir, cada entorno de ejecución administrado)workspace_ides obligatorio y debe coincidir exactamente: el token de portador autentica al llamador, pero no prueba que la ejecución nombrada pertenezca a este entorno de ejecución.202 {"outcome": "accepted"}mientras se drena;200con el resultado final (completed/timed_out/delivery_failed) después.acceptedes solo aceptación a nivel HTTP;completedes la única señal de inactividad.Idempotente en
request_id, un drenaje por ejecución: un reintento vuelve a leer el resultado registrado en lugar de volver a ejecutar los efectos secundarios.deadline_at_mses absoluto y nunca se extiende; el tiempo de ejecución rechaza plazos más allá deRUNNER_DRAIN_MAX_DEADLINE_MS(predeterminado 60s). Los registros finalizados sobreviven aRUNNER_DRAIN_RECORD_TTL_S(predeterminado 900s) para que los reintentos del llamador permanezcan idempotentes durante su ventana de reconciliación; manténgalo en o por encima de la ventana de reintento del llamador (OE reintenta un drenaje durante 15 minutos): un TTL más corto expulsa el registro mientras el llamador todavía está reintentando, y los reenvíos tardíos obtienenexecution_not_founden lugar del resultado registrado.Una ejecución que este entorno de ejecución nunca sirvió responde
delivery_failedconreason_code=execution_not_found: una ejecución puede abarcar los entornos de ejecución de AER y Tool de forma independiente, por lo que la selección exacta de OE no hace que uncompletedfalso sea seguro.
Los límites por lenguaje son parte del contrato, no un detalle de implementación. Python cancela las tareas asyncio rastreadas, por lo que el trabajo cooperativo se establece como completed. Una promesa no tiene cancelación, por lo que el entorno de ejecución de TypeScript (agent-engine-runner-shared, que refleja este contrato y debe cambiar junto con él) aborta solo los AbortController registrados —el turno AER y los flujos LLM— mientras que las funciones de herramientas simples no reciben ninguna señal: son rastreadas y bloqueadas por admisión, y sobrevivir al plazo es un timed_out honesto. El estado es local al proceso por diseño: un entorno de ejecución reiniciado ha perdido su trabajo activo y responde delivery_failed; la reconciliación entre reinicios pertenece al registro duradero del llamador, no a este registro.
Interrupción por llamada
POST /interrupt/call es el hermano quirúrgico del drenaje: la interrupción por llamada del OE nombra una llamada por {execution_id, step_number} y el tiempo de ejecución aborta exactamente el identificador rastreado de esa llamada (Python cancela la tarea asyncio, el tiempo de ejecución de TS aborta el AbortController de la llamada), mientras que la ejecución permanece abierta y las llamadas posteriores proceden (sin pestillo de admisión, nunca). Siempre 200 con un resultado: interrupted (señalizado o registrado para dispararse al adjuntar), not_found (no hay tal llamada en curso), already_settled (la llamada terminó primero), not_cancellable (trabajo rastreado sin canal de señal (un cuerpo de herramienta de sincronización en un hilo, una función de herramienta TS simple) informado honestamente, nunca declarado abandonado). El alcance del espacio de trabajo coincide con el de la ruta de drenaje, y los vectores compartidos viven en client-libraries/test-fixtures/interrupt-call/contract.json.
Componentes clave
Cómo se enrutan las llamadas interceptadas a través de OE
Las llamadas a herramientas interceptadas y invoke_llm ahora utilizan el mismo flujo de ejecución propiedad de OE. Cuando el agente que se ejecuta en AER necesita llamar a una herramienta o a un LLM, el contenedor envía la solicitud interceptada a OE y espera a que OE devuelva el resultado final. OE enruta las llamadas a herramientas a través de Tool Pod /execute y las llamadas a LLM a través de Tool Pod /invoke_llm, y luego devuelve el resultado final (éxito, suspensión o error) a AER.
Diagrama de secuencia:
┌─────┐ ┌─────┐ ┌──────────┐ │ AER │ │ OE │ │ Tool Pod │ └──┬──┘ └──┬──┘ └────┬─────┘ │ │ │ │ 1. POST /tool/execute │ │ │ ────────────────────────►│ │ │ {name or invoke_llm, │ │ │ args/messages, step} │ │ │ │ │ │ │ Log + Policy Check │ │ │ │ │ │ 2a. Replay cache hit │ │ 2a. Final cached result │ │ │ ◄────────────────────────│ │ │ {status, result, │ │ │ from_cache} │ │ │ │ │ ├──────────────────────────┼────────────────────────────┤ │ if OE must execute the tool │ │ │ │ │ │ 2b. POST /execute or │ │ │ /invoke_llm │ │ │ ───────────────────────────────────────► │ │ via Tool Pod │ │ │ ◄─────────────────────────────────────── │ │ {status, result} │ │ │ │ ├──────────────────────────┼────────────────────────────┤ │ │ │ │ 3. Final OE-owned result │ │ │ ◄────────────────────────│ │ │ {status, result, error, │ │ │ duration_ms, pod_name} │ │ │ │ │
Puntos clave:
Cada herramienta interceptada o llamada LLM pasa primero por OE para la política, el registro y la propiedad de reproducción.
OE controla el enrutamiento: las herramientas remotas usan
Tool Pod /execute; las herramientas locales aprobadas vuelven a la pila de llamadas AER original para que el contexto nativo del marco permanezca activo; las llamadas LLM interceptadas usanTool Pod /invoke_llm.La función Replay utiliza resultados almacenados en caché: al reanudarse después de SUSPEND, OE devuelve el resultado final almacenado para evitar efectos secundarios duplicados.
La suspensión aún se desenrolla a través del contenedor: OE devuelve la carga útil de suspensión serializada, luego
SecureToolWrapperla convierte de nuevo en la interrupción del marco.``invoke_llm`` ahora coincide con el contrato de la herramienta: OE devuelve la carga útil final
status/result/erroren lugar de pedirle a AER que la ejecute después de la aprobación.Los fragmentos de flujo LLM conservan los metadatos del mensaje final:
id,name,additional_kwargsyresponse_metadataviajan a través del evento de flujo tipificado y se recopilan de nuevo en la misma forma de respuesta final utilizada para la reproducción.
agent.yaml interpolación de variables ambientales
La carga de la configuración en tiempo de ejecución ahora admite la interpolación ${VAR} para una lista permitida reducida de rutas agent.yaml propiedad del inquilino, actualmente mcp.servers.*.url.
Cómo funciona:
load_runtime_agent_config(env_vars=...)Sustituye las referencias${VAR}solo en las rutas permitidas.El entorno de ejecución pasa
tenant_env_vars()en lugar deos.environsin procesar, por lo que las variables de entorno y los secretos propiedad de la plataforma quedan excluidos de la sustitución.Si una ruta permitida hace referencia a una variable no definida o contiene un marcador
${...}mal formado, el cargador genera un claro error de validación.mcp.servers.*.auth.token_envno debe apuntar a una variable de entorno propiedad de la plataforma.
Ejemplo:
mcp: servers: tableau: url: https://${TABLEAU_HOST}/mcp
En tiempo de ejecución, ${TABLEAU_HOST} se resuelve a partir del subconjunto de entorno propiedad del inquilino. Una referencia como ${OPENAI_API_KEY} se rechaza porque esa variable es propiedad de la plataforma y se filtra antes de la interpolación.
SecureToolWrapper (secure_wrapper.py)
El componente de seguridad central que intercepta las llamadas a las herramientas:
# Every tool call goes through this flow: def execute_tool(tool_name, arguments): # 1. Send the intercepted tool call to OE response = request_oe_approval(oe_url, execution_id, tool_name, arguments) # 2. Check for policy denial if not response.proceed: raise PolicyDeniedException(response.reason) # 3. OE either returns a final outcome or routes the call back in process if response.route_to == "callback": result = local_executor() report_oe_result(result) return result if response.status == "error": raise ToolExecutionError(response.error) if response.status == "suspend": interrupt(response.result) return response.result
Invariante clave: Ninguna herramienta ni llamada a LLM se ejecuta sin que OE lo sepa.
Características
Aplicación de políticas
OE puede bloquear las llamadas a herramientas/LLM en función de las reglas de la política:
La aplicación de las políticas está a cargo de Go OE. Véase docs/orchestration-engine/README.md.
Almacenamiento en caché de reproducción
Cuando un agente se reanuda tras una suspensión, la ejecución de LangGraph se repite desde el principio. Sin almacenamiento en caché, esto volvería a ejecutar las herramientas y las llamadas a LLM, lo que provocaría efectos secundarios duplicados (por ejemplo, el envío de notificaciones dos veces).
Cómo funciona:
OE realiza un seguimiento de todos los pasos: cada llamada a la herramienta/LLM se registra con su número de paso, nombre y resultado.
Al reanudarse, el agente vuelve a ejecutar LangGraph desde el principio, realizando las mismas llamadas en orden.
OE devuelve los resultados almacenados en caché: en lugar de volver a ejecutar, OE devuelve el resultado almacenado.
Original Execution: Step 1: lookup_policy("POL-123") → executed, result stored Step 2: query_mongogpt(...) → executed, result stored Step 3: human_review(...) → SUSPEND (waiting for human) Resume Execution: Step 1: lookup_policy("POL-123") → CACHED (returns stored result) Step 2: query_mongogpt(...) → CACHED (returns stored result) Step 3: human_review(...) → CACHED (returns human's decision) Step 4: send_notification(...) → executed (new step)
El almacenamiento en caché de reproducción lo gestiona Go OE. AER/SecureToolWrapper gestiona las respuestas almacenadas en caché de forma transparente:
Manejo de AER/SecureToolWrapper:
# In secure_wrapper.py - OE already returns the final replay outcome response = await request_oe_approval(...) if response.from_cache: log_cached_result(tool_name, step) return response.result
Esto garantiza una reanudación determinista: el agente ve exactamente los mismos resultados que veía antes de la suspensión, lo que evita efectos secundarios duplicados.
Puntos de control
Admite tanto puntos de control en memoria como en MongoDB:
# In runtime.py def get_checkpointer(self): if mongodb_uri: return MongoDBSaver(client, db_name) return MemorySaver() # Development only
Estructura del archivo
src/agent_engine_runner_shared/ ├── runtime.py # TenantRuntime - main SDK entry point ├── secure_wrapper.py # SecureToolWrapper, SecureWrappedLLM ├── models.py # Pydantic models for all API contracts ├── context.py # Per-execution context (contextvars) ├── metrics.py # Metrics collection and @with_metrics decorator ├── utils.py # Logging utilities, env helpers ├── logging.py # Durable execution logging (JSONL + MongoDB) ├── memory.py # MemoryEngine integration, MemoryWriter ├── voyage.py # VoyageService for embeddings ├── events.py # SSE event streaming, observability ├── types.py # Protocol definitions (MemoryEngineProtocol) ├── tracing/ # OpenTelemetry tracing │ ├── setup.py # setup_tracing(), get_current_trace_context() │ └── exporters.py # JSONLSpanExporter, MongoDBSpanExporter └── server/ ├── base.py # BaseServer abstract class ├── aer.py # AER implementation ├── tool.py # Tool Pod implementation └── memory.py # Memory Server implementation
Decisiones de diseño
¿Por qué enrutar todo a través de OE?
Aunque añade latencia, el enrutamiento a través de OE proporciona: 1. Registro de auditoría completo: cada llamada se registra 2. Punto de aplicación de políticas: un único lugar para bloquear llamadas 3. Capacidad de reproducción: OE puede devolver resultados almacenados en caché para reanudar 4. Independiente del marco: OE no conoce LangGraph
¿Por qué separar AER y la herramienta?
AER necesita acceso a la API de LLM pero no debería tener credenciales de base de datos.
La herramienta podría necesitar acceso a la base de datos, pero no debería tener claves LLM.
Las distintas herramientas pueden ejecutarse en diferentes entornos con distintos permisos.
Paridad de características con el entorno de ejecución del agente
El SDK de Runner ahora ofrece paridad de características con el sistema monolítico agent-runtime:
funcionalidad | tiempo de ejecución del agente | SDK de corredor |
|---|---|---|
Decorador @app.tool | ✅ | ✅ |
Metadatos explícitos de red/tiempo de espera | ✅ | ✅ |
Redacción de campo | ✅ | ✅ |
Puntos de control de MongoDB | ✅ | ✅ |
Registro de ejecución (JSONL + MongoDB) | ✅ | ✅ |
Rastreo de OpenTelemetry | ✅ | ✅ |
Integración de memoria | ✅ | ✅ |
Voyage IA Embeddings | ✅ | ✅ |
Multiusuario (org_id/user_id) | ✅ | ✅ |
Transmisión de eventos SSE | ✅ | ✅ |
Servidor gRPC | ✅ | ✅ |
Soporte de transmisión | ✅ | ✅ |
Intervención humana | ❌ | ✅ |
Arquitectura de tres componentes | ❌ | ✅ |
Almacenamiento en caché de reproducción | ❌ | ✅ |
Dependencias opcionales
Instala las funciones según sea necesario. Usa uv add si tu proyecto se gestiona con uv, o pip install en caso contrario:
# uv projects uv add agent-engine-runner-shared uv add "agent-engine-runner-shared[mongodb,tracing]" uv add "agent-engine-runner-shared[tracing]" uv add "agent-engine-runner-shared[mongodb]" # pip pip install agent-engine-runner-shared pip install "agent-engine-runner-shared[mongodb,tracing]" pip install "agent-engine-runner-shared[tracing]" pip install "agent-engine-runner-shared[mongodb]"