Atlas Agent Engine을 위한 프레임워크에 구애받지 않는 테넌트 런타임 SDK입니다.
이는 내부 핵심 패키지 입니다. 에이전트가 직접 설치하지 않습니다. 종속된 프레임워크 SDK(
agent-engine-sdk-langgraph또는agent-engine-sdk-adk)를 설치합니다.
아키텍처 개요
Runner SDK는 플랫폼 아키텍처의 AER, 도구 파드 및 메모리 서버 통합 구성 요소를 제공합니다. 오케스트레이션 엔진은 별도의 Atlas Agent Engine 서비스입니다.
구성 요소 | 포트 | 신뢰 수준 | 네트워크 액세스 | 시크릿 액세스 | 목적 |
|---|---|---|---|---|---|
AER(에이전트 실행 런타임) | 8001 | 중간 | LLM API 전용 | LLM 키 전용 | 실행 영역 - 에이전트 로직 실행 |
도구 | 8002 | 다름 | 필요에 따라 | 필요에 따라 | 데이터 영역 - 격리된 도구 실행 |
메모리 서버 | 8081 | 중간 | MongoDB | DB 자격 증명 | 시맨틱/에피소딕/분류 메모리를 위한 메모리 서비스 |
Runner-shared auto-detects the listener bind at startup: :: (IPv6 dual-stack — uvicorn renders this as http://[::]:port in URL form) when the kernel has IPv6 enabled with IPV6_V6ONLY=0, otherwise 0.0.0.0. No configuration required — production deploys on IPv6-first networks (cell tenant namespaces) get :: automatically and local Docker dev (where bridge IPv6 is often disabled) falls back to 0.0.0.0. The probe-resolved host is logged at startup (listener bind resolved: host=...) so post-deploy verification doesn’t have to infer from uvicorn’s own startup line.
환경에서 APP_HOST를 설정하여 프로브를 재정의하는 프로브 — 커널이 IPv6를 사용 가능한 것으로 보고하지만 주변 네트워크가 IPv4만 라우팅하는 컨테이너에 필요합니다(예: host_ip: 127.0.0.1을 사용한 Docker 포트 포워딩). 이러한 이유로 통합 테스트 작성 템플릿은 APP_HOST=0.0.0.0를 설정하다 .
리스너는 활성 RUNNER_MODE(8001/8002/8081)에 대해 고정된 기본값 포트를 사용합니다. RUNNER_MODE는 플랫폼 런타임에 필요하며 주입됩니다. local dev는 사용자 .env 재정의를 무시하며, 작업 공간 시크릿은 이름을 완전히 보유합니다.
시작 로깅
The image CMD is python -m agent_engine_runner_shared.launcher (TypeScript: node …/launcher.js). The launcher installs logging before it imports AGENT_ENTRYPOINT, so an import-time crash or an exception from the entrypoint function is an ERROR record (structured JSON when STRUCTURED_LOGGING=true) rather than a raw traceback that workspace logs would show as INFO. TenantRuntime still calls setup_logging / setupLogging later; that second install is idempotent. Failures that happen before the launcher process starts (interpreter/image errors) still rely on the API Gateway read-path heuristic documented in `docs/api-gateway/README.md <../../../../../docs/api-gateway/README.md#agent--operator-logs-s3-read-path>`__.
3가지 구성 요소를 사용해야 하는 이유
이러한 분리를 통해 다음과 같은 이점을 얻을 수 있습니다. - 감사 로깅 - OE는 모든 도구/LLM 호출을 실행하지 않고 확인 - 정책 시행 - OE는 실행 전에 호출을 차단 수 있음 - 최소 권한 - 각 구성 요소에 필요한 액세스 만 부여 - 재생 - OE는 결정론적 재개를 위해 캐시된 결과를 반환할 수 있음
실행 수명 주기 & 시크릿을 사용할 수 있는 위치
전체 규범 계약: `docs/runner/README.md <../../../../../docs/runner/README.md#execution-Lifecycle--secret-availability>`__. 에이전트 작성자가 올바른 코드를 쓰기 (write) 데 필요한 규칙은 다음과 같습니다.
모듈 최상위 코드(함수 본문 외부의 모든 항목)는 프로세스 스타트업 시 AER과 도구 파드 모두에서 한 번 실행됩니다. 이는 MCP OAuth 구성 및
MONGODB_URI및 LLM API 키를 포함한 테넌트 제공 가능 스타트업 비밀에 의존할 수 있습니다. 스타트업 중에는 호출 세션과 위임된 요청 자격 증명 사용할 수 없습니다. MCP OAuth 캐시 쓰기(~/.agentic/mcp-oauth/<server>.json또는 배포된 런타임의AGENTIC_MCP_OAUTH_DIR)는 자문<server>.json.lock파일 에 직렬화되므로 동일한 서버 에 대한 동시 토큰 새로 고침이 서로 방해할 수 없습니다.도구 애플리케이션 구성은 스타트업 중에
@app.entrypoint함수를 시도하여 모든app.llm(llm_id=...)등록을 검색합니다. 모델 요청은 준비된 레지스트리를 재사용합니다. 구성은 정적 스타트업 구성을 사용하며 비즈니스 차례, 모델 호출 또는 도구 본문을 실행해서는 안 됩니다. 구성에 실패하면 경고를 표시하고 이전 등록을 복원하여 이후 모델 요청 다시 시도할 수 있도록 합니다. 성공적인 구성은 명명된 LLM이 없어도 캐시됩니다. AER 그래프 캐싱 및 워밍업은 계속 프레임워크가 소유합니다.``앱.llm(llm, llm_id=...)``는 진입점이 호출 스택 에 있는 동안(본문에서 직접 또는 호출하는 헬퍼 함수에서) 호출되어야 합니다. 그 외부의 모든 곳( 모듈 최상위 수준, 도구 본문, 진입점이 아닌 다른 곳에서 호출된 헬퍼)에서 호출하면 즉시
RuntimeError가 발생하여 문제가 되는 호출 사이트 이름을 지정합니다.로컬 도구 본문(
is_local=True)은 AER의 자체 프로세스 에서만 실행 . AER 및 도구 워크로드는 모두 스타트업 시 테넌트 제공 시크릿을 수신합니다.원격 도구 본문(
is_local=False또는 위임된 자격 증명 필요한 도구)은 OE가 호출을 라우팅할 때 온디맨드 방식으로 도구 파드에서만 실행 , 구성 중에는 절대 실행되지 않습니다.required_secrets선언은 시크릿 요구 사항을 문서 . 스타트업 시크릿 환경의 범위를 좁히지 않습니다.SDK가 로컬 기본값 제공하더라도 자격 권한 부여이 있는 도구는 원격으로 유지됩니다.
실행 흐름
일반 실행(시작 → 완료)
┌────────┐ ┌─────┐ ┌─────┐ │ 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}│ │ │ ◄──────────────────────────│ │ │ │ │
인간-인-더-루프(SUSPEND/RESUME)
┌────────┐ ┌─────┐ ┌─────┐ │ 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} │ │ │ ◄──────────────────────────│ │ │ │ │
참고
API 게이트웨이를 통한 재개: 위 다이어그램은 간결성을 위해 OE의 내부 /resume/{id} 경로를 사용합니다. 개발자는 플랫 JSON 본문을 사용하여 POST /api/v1/projects/{project_id}/executions/{execution_id}/resume에서 API 게이트웨이를 호출합니다.
{"decision": "approved", "reviewer_notes": "optional context"}
래핑된 {"human_review": {"decision": "..."}} 형태는 OE의 내부 엔드포인트 계약입니다. 게이트웨이가 HTTP 400로 이를 거부합니다.
실행 배출(취소)
OE가 실행을 지속적으로 취소하면 이를 소유한 도구/AER 런타임에 /drain를 게시합니다. 수신자 계약(client-libraries/test-fixtures/drain/contract.json의 공유 고정 장치에 의해 고정됨):
POST /drain {request_id, execution_id, reason, deadline_at_ms, workspace_id}— 작업 공간 범위 런타임(APP_ID설정하다 즉, 모든 managed 런타임)에서workspace_id은 필수이며 정확히 일치해야 합니다: 베어러 토큰은 호출자를 인증하지만 명명된 실행이 이 런타임에 속한다는 것을 증명하지는 않습니다.202 {"outcome": "accepted"}배출 동안 ;200를 최종 결과(completed/timed_out/delivery_failed)와 함께 반환합니다.accepted는 HTTP 수준에서만 허용되며,completed는 유일한 정지 신호입니다.request_id에서 멱등원, 실행당 하나의 배출: 재시도 시 부작용을 다시 실행하는 대신 기록된 결과를 다시 읽습니다.deadline_at_ms절대적이며 절대 확장되지 않습니다. 런타임이RUNNER_DRAIN_MAX_DEADLINE_MS(기본값 60) 이후의 기한을 거부합니다. 최종 기록은RUNNER_DRAIN_RECORD_TTL_S(기본값 900) 동안 유지되므로 호출자의 재시 창 동안 멱등 유지 — 이를 호출자의 재시 창 이상으로 유지(OE는 15분 동안 배출 재시도): TTL 짧을 기록 제거됩니다. 발신자가 여전히 재시도하고 있고, 늦은 재전송은 기록된 결과 대신execution_not_found를 받습니다.이 런타임이 제공하지 않은 실행은
delivery_failed에reason_code=execution_not_found로 응답합니다. 한 번의 실행은 AER 및 도구 런타임에 독립적으로 적용될 수 있으므로 정확한 OE 타겟팅이 잘못된completed안전을 보장하지 않습니다.
언어별 제한은 구현 세부 정보가 아닌 계약의 일부입니다. Python 추적된 asyncio 작업을 취소하므로 협업 작업은 completed로 해결됩니다. 프로미스는 취소가 없으므로 TypeScript 런타임(agent-engine-runner-shared, 이 계약을 미러링하고 계약과 함께 변경되어야 함)은 등록된 AbortController(AER 턴 및 LLM 스트림)만 중단하고 일반 도구 함수는 신호를 수신하지 않습니다. 추적 및 허용이 차단되며, 기한을 초과하는 것은 정직한 timed_out입니다. 상태는 의도적으로 프로세스 로컬입니다: 재시작된 런타임이 활성 작업을 손실하고 delivery_failed에 응답합니다. 재시작 시 조정은 이 레지스트리가 아닌 호출자의 지속형 기록 에 속합니다.
호출별 인터럽트
POST /interrupt/call is the surgical sibling of the drain: the OE’s per-call interrupt names one call by {execution_id, step_number} and the runtime aborts exactly that call’s tracked handle — Python cancels the asyncio task, the TS runtime aborts the call’s AbortController — while the execution stays open and later calls proceed (no admission latch, ever). Always 200 with an outcome: interrupted (signalled, or recorded to fire at attach), not_found (no such in-flight call), already_settled (the call ended first), not_cancellable (tracked work with no signal channel — a sync tool body on a thread, a plain TS tool function — reported honestly, never claimed abandoned). Workspace scoping matches the drain route’s, and the shared vectors live in client-libraries/test-fixtures/interrupt-call/contract.json.
주요 구성 요소
가로채기된 통화가 OE를 통해 라우팅되는 방법
차단된 도구 호출과 invoke_llm는 이제 동일한 OE 소유 실행 흐름을 사용합니다. AER에서 실행 에이전트 도구나 LLM을 호출해야 하는 경우 래퍼는 가로채는 요청 OE로 보내고 OE가 최종 결과를 반환할 때까지 기다립니다. OE는 Tool Pod /execute를 통해 도구 호출을 라우팅하고 Tool Pod /invoke_llm를 통해 LLM 호출을 라우팅한 다음 최종 성공, 일시 중단 또는 오류 결과를 AER에 다시 반환합니다.
시퀀스 다이어그램:
┌─────┐ ┌─────┐ ┌──────────┐ │ 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} │ │ │ │ │
요점:
가로채는 모든 도구 또는 LLM 호출은 정책, 로깅 및 재생 소유권을 위해 먼저 OE를 교차합니다.
OE는 라우팅을 소유합니다 — 원격 도구는
Tool Pod /execute을 사용합니다. 승인된 로컬 도구가 원래 AER 호출 스택 으로 돌아가서 프레임워크 네이티브 컨텍스트가 라이브 상태로 유지됩니다. 가로채는 LLM 호출은Tool Pod /invoke_llm을(를) 사용합니다.Replay는 캐시된 결과를 사용합니다 — SUSPEND 후 재개 시 OE는 중복된 부작용을 방지하기 위해 저장된 최종 결과를 반환합니다.
일시 중단은 여전히 래퍼를 통해 풀립니다 — OE는 직렬화된 일시 중단 페이로드를 반환한 다음
SecureToolWrapper가 이를 프레임워크 중단으로 다시 변환합니다.이제 ``invoke_llm``이 도구 계약과 일치합니다. — OE는 승인 후 AER을 실행하도록 요청하는 대신 최종
status/result/error페이로드를 반환합니다.LLM 스트림 청크는 최종 메시지 메타데이터 보존합니다 —
id,name,additional_kwargs및response_metadata는 입력된 스트림 이벤트 통과하여 재생에 사용되는 것과 동일한 최종 응답 형태로 다시 수집됩니다.
agent.yaml 환경 변수 보간
런타임 구성 로딩이 이제 테넌트 소유 agent.yaml 경로의 제한된 허용 목록(현재 mcp.servers.*.url)에 대해 ${VAR} 보간을 지원합니다.
작동 방식:
load_runtime_agent_config(env_vars=...)허용 목록에 있는 경로에서만${VAR}참조를 대체합니다.런타임은 원시
os.environ가 아닌tenant_env_vars()을 전달하므로 플랫폼 소유 환경 변수와 시크릿은 대체에서 제외됩니다.허용 목록에 있는 경로가 설정되지 않은 변수를 참조하거나 잘못된
${...}마커를 포함하는 경우, 로더는 명확한 유효성 검사 오류를 발생시킵니다.mcp.servers.*.auth.token_env플랫폼 소유 환경 변수를 점 않아야 합니다.
예시:
mcp: servers: tableau: url: https://${TABLEAU_HOST}/mcp
런타임에 ${TABLEAU_HOST}가 테넌트 소유 환경 하위 집합에서 확인됩니다. ${OPENAI_API_KEY}과 같은 참조는 해당 변수가 플랫폼 소유이고 보간 전에 필터링되기 때문에 거부됩니다.
SecureToolWrapper(secure_wrapper.py)
도구 호출을 가로채는 중앙 보안 구성 요소:
# 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
키 불변성: OE가 이를 알지 못하면 도구 또는 LLM 호출이 실행되지 않습니다.
기능
정책 시행
OE는 정책 규칙에 따라 도구/LLM 호출을 차단 수 있습니다.
정책 시행은 고 (Go) OE에서 처리합니다. docs/orchestration-engine/README.md를 참조하세요.
Replay 캐싱
SUSPEND 후 에이전트 재개되면 LangGraph 실행이 처음부터 재생됩니다. 캐싱이 없으면 도구와 LLM 호출이 다시 실행되어 중복된 부작용(예: 알림 두 번 전송)이 발생합니다.
작동 방식:
OE는 모든 단계를 추적 - 모든 도구/LLM 호출은 단계 번호, 이름 및 결과와 함께 기록됩니다.
재개 시 에이전트 재생 - LangGraph가 처음부터 다시 실행하여 동일한 호출을 순서대로 수행
OE는 캐시된 결과를 반환 - OE는 다시 실행하는 대신 저장된 결과를 반환합니다.
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)
재생 캐싱은 고 (Go) OE에서 처리합니다. AER/SecureToolWrapper는 캐시된 응답을 투명하게 처리합니다.
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
이렇게 하면 결정론적 재개가 보장되며, 에이전트 일시 중단하기 전에 본 것과 정확히 동일한 결과를 볼 수 있으므로 중복 부작용을 방지할 수 있습니다.
체크포인트
인메모리 및 MongoDB 체크포인터를 모두 지원합니다.
# In runtime.py def get_checkpointer(self): if mongodb_uri: return MongoDBSaver(client, db_name) return MemorySaver() # Development only
파일 구조
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
설계 결정
OE를 통해 모든 것을 라우팅해야 하는 이유
지연 시간 추가되지만, OE를 통한 라우팅은 다음을 제공합니다: 1. 전체 감사 추적 - 모든 통화가 2로 기록됩니다. 정책 시행 점 - 3 호출을 차단 수 있는 단일장소입니다. 재생 역량 - OE는 재개 4에 대해 캐시된 결과를 반환할 수 있습니다. 프레임워크에 구애받지않음 - OE가 LangGraph를 알 수 없음
AER과 도구를 분리해야 하는 이유
AER에는 LLM API 액세스 필요하지만 데이터베이스 자격 증명 없어야 합니다.
도구에 데이터베이스 액세스 필요할 수 있지만 LLM 키는 없어야 합니다.
서로 다른 도구를 서로 다른 권한으로 서로 다른 도구에서 실행 수 있습니다.
에이전트 런타임과 기능 패리티
이제 Runner SDK는 모놀리식 agent-runtime과 기능 패리티를 제공합니다.
기능 | 에이전트 런타임 | 러너 SDK |
|---|---|---|
@ 앱.tool 데코레이터 | ✅ | ✅ |
명시적 네트워크/시간 초과 메타데이터 | ✅ | ✅ |
필드 편집 | ✅ | ✅ |
MongoDB 체크포인트 | ✅ | ✅ |
실행 로깅(JSONL + MongoDB) | ✅ | ✅ |
OpenTelemetry 추적 | ✅ | ✅ |
메모리 통합 | ✅ | ✅ |
Voyage AI 임베딩 | ✅ | ✅ |
멀티 테넌트(org_id/user_id) | ✅ | ✅ |
SSE 이벤트 스트리밍 | ✅ | ✅ |
gRPC 서버 | ✅ | ✅ |
스트리밍 지원 | ✅ | ✅ |
인간-인-더-루프 | ❌ | ✅ |
3 컴포넌트 아키텍처 | ❌ | ✅ |
캐싱 재생 | ❌ | ✅ |
선택적 종속성
필요에 따라 기능을 설치합니다. 프로젝트 가 uv로 managed 경우 uv add를 사용하고, 그렇지 않은 경우 pip install을 사용합니다:
# 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]"