gatewaysdk 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gatewaysdk/__init__.py +540 -0
- gatewaysdk/adapter/__init__.py +15 -0
- gatewaysdk/adapter/base.py +94 -0
- gatewaysdk/adapter/messages.py +270 -0
- gatewaysdk/adapter/triplet.py +1026 -0
- gatewaysdk/algorithm/__init__.py +39 -0
- gatewaysdk/algorithm/apo/__init__.py +5 -0
- gatewaysdk/algorithm/apo/apo.py +898 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
- gatewaysdk/algorithm/base.py +262 -0
- gatewaysdk/algorithm/decorator.py +264 -0
- gatewaysdk/algorithm/evals/__init__.py +7 -0
- gatewaysdk/algorithm/evals/evals.py +217 -0
- gatewaysdk/algorithm/fast.py +250 -0
- gatewaysdk/algorithm/gepa/__init__.py +61 -0
- gatewaysdk/algorithm/gepa/adapter.py +495 -0
- gatewaysdk/algorithm/gepa/gepa.py +570 -0
- gatewaysdk/algorithm/gepa/lib/__init__.py +18 -0
- gatewaysdk/algorithm/gepa/lib/adapters/README.md +12 -0
- gatewaysdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +268 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +699 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- gatewaysdk/algorithm/gepa/lib/api.py +382 -0
- gatewaysdk/algorithm/gepa/lib/core/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/core/adapter.py +180 -0
- gatewaysdk/algorithm/gepa/lib/core/data_loader.py +74 -0
- gatewaysdk/algorithm/gepa/lib/core/engine.py +379 -0
- gatewaysdk/algorithm/gepa/lib/core/result.py +233 -0
- gatewaysdk/algorithm/gepa/lib/core/state.py +636 -0
- gatewaysdk/algorithm/gepa/lib/examples/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/examples/aime.py +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +456 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +820 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
- gatewaysdk/algorithm/gepa/lib/gepa_utils.py +117 -0
- gatewaysdk/algorithm/gepa/lib/logging/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
- gatewaysdk/algorithm/gepa/lib/logging/logger.py +75 -0
- gatewaysdk/algorithm/gepa/lib/logging/utils.py +103 -0
- gatewaysdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/base.py +31 -0
- gatewaysdk/algorithm/gepa/lib/proposer/merge.py +357 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
- gatewaysdk/algorithm/gepa/lib/py.typed +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
- gatewaysdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
- gatewaysdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
- gatewaysdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
- gatewaysdk/algorithm/gepa/lib/strategies/instruction_proposal.py +126 -0
- gatewaysdk/algorithm/gepa/lib/utils/__init__.py +10 -0
- gatewaysdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
- gatewaysdk/algorithm/gepa/tracing.py +105 -0
- gatewaysdk/algorithm/utils.py +177 -0
- gatewaysdk/algorithm/verl/__init__.py +5 -0
- gatewaysdk/algorithm/verl/interface.py +202 -0
- gatewaysdk/automations.py +111 -0
- gatewaysdk/benchmark_hub/README.md +88 -0
- gatewaysdk/benchmark_hub/__init__.py +119 -0
- gatewaysdk/benchmark_hub/_archive.py +189 -0
- gatewaysdk/benchmark_hub/_tar.py +6 -0
- gatewaysdk/benchmark_hub/client.py +992 -0
- gatewaysdk/benchmark_hub/dispatch_shard.py +34 -0
- gatewaysdk/benchmark_hub/eval_config.py +20 -0
- gatewaysdk/benchmark_hub/evals.py +351 -0
- gatewaysdk/benchmark_hub/harbor_adapter.py +673 -0
- gatewaysdk/benchmark_hub/path_utils.py +81 -0
- gatewaysdk/benchmark_hub/save_utils.py +101 -0
- gatewaysdk/benchmark_hub/verifiers_adapter.py +327 -0
- gatewaysdk/benchmark_hub/versioning.py +72 -0
- gatewaysdk/build.py +515 -0
- gatewaysdk/cli/__init__.py +58 -0
- gatewaysdk/cli/agent_runner.py +132 -0
- gatewaysdk/cli/http_client.py +115 -0
- gatewaysdk/cli/platform.py +4086 -0
- gatewaysdk/cli/prometheus.py +115 -0
- gatewaysdk/cli/release_gate.py +215 -0
- gatewaysdk/cli/store.py +131 -0
- gatewaysdk/cli/vllm.py +29 -0
- gatewaysdk/client.py +406 -0
- gatewaysdk/config.py +348 -0
- gatewaysdk/connectors/__init__.py +25 -0
- gatewaysdk/connectors/client.py +203 -0
- gatewaysdk/connectors/skill.py +25 -0
- gatewaysdk/connectors/template.py +106 -0
- gatewaysdk/context.py +606 -0
- gatewaysdk/emitter/__init__.py +43 -0
- gatewaysdk/emitter/annotation.py +370 -0
- gatewaysdk/emitter/exception.py +54 -0
- gatewaysdk/emitter/message.py +61 -0
- gatewaysdk/emitter/object.py +117 -0
- gatewaysdk/emitter/reward.py +320 -0
- gatewaysdk/env_var.py +156 -0
- gatewaysdk/environment/__init__.py +108 -0
- gatewaysdk/environment/_bundle.py +281 -0
- gatewaysdk/environment/_harbor.py +276 -0
- gatewaysdk/environment/_materialize.py +97 -0
- gatewaysdk/environment/_tar.py +33 -0
- gatewaysdk/environment/_world.py +346 -0
- gatewaysdk/environment/core.py +527 -0
- gatewaysdk/environment/errors.py +58 -0
- gatewaysdk/environment/runtime.py +150 -0
- gatewaysdk/environment/schema/__init__.py +35 -0
- gatewaysdk/environment/schema/__main__.py +225 -0
- gatewaysdk/environment/schema/_slack_fidelity.py +116 -0
- gatewaysdk/environment/schema/_slack_scenario.py +183 -0
- gatewaysdk/environment/schema/api.py +1628 -0
- gatewaysdk/environment/schema/batch.py +755 -0
- gatewaysdk/environment/schema/compiler.py +615 -0
- gatewaysdk/environment/schema/conform.py +428 -0
- gatewaysdk/environment/schema/connector.py +777 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/handlers.py +17 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/parity.json +28 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/provenance.json +233 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/scope.toml +112 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/world.json +1380 -0
- gatewaysdk/environment/schema/connectors/base.json +28 -0
- gatewaysdk/environment/schema/connectors/custom/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/custom/world.json +123 -0
- gatewaysdk/environment/schema/connectors/github/handlers.py +81 -0
- gatewaysdk/environment/schema/connectors/github/parity.json +37 -0
- gatewaysdk/environment/schema/connectors/github/provenance.json +134 -0
- gatewaysdk/environment/schema/connectors/github/scope.toml +158 -0
- gatewaysdk/environment/schema/connectors/github/world.json +5869 -0
- gatewaysdk/environment/schema/connectors/google-calendar/handlers.py +23 -0
- gatewaysdk/environment/schema/connectors/google-calendar/world.json +209 -0
- gatewaysdk/environment/schema/connectors/google-drive/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/google-drive/world.json +286 -0
- gatewaysdk/environment/schema/connectors/jamf/handlers.py +34 -0
- gatewaysdk/environment/schema/connectors/jamf/parity.json +49 -0
- gatewaysdk/environment/schema/connectors/jamf/provenance.json +181 -0
- gatewaysdk/environment/schema/connectors/jamf/scope.toml +128 -0
- gatewaysdk/environment/schema/connectors/jamf/world.json +2218 -0
- gatewaysdk/environment/schema/connectors/jira/handlers.py +28 -0
- gatewaysdk/environment/schema/connectors/jira/world.json +389 -0
- gatewaysdk/environment/schema/connectors/kandji/handlers.py +38 -0
- gatewaysdk/environment/schema/connectors/kandji/parity.json +29 -0
- gatewaysdk/environment/schema/connectors/kandji/provenance.json +152 -0
- gatewaysdk/environment/schema/connectors/kandji/scope.toml +120 -0
- gatewaysdk/environment/schema/connectors/kandji/world.json +765 -0
- gatewaysdk/environment/schema/connectors/linear/handlers.py +20 -0
- gatewaysdk/environment/schema/connectors/linear/world.json +363 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/fidelity.json +45 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/handlers.py +1 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/provenance.json +851 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/scope.md +231 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/teams-types.json +1710 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/world.json +858 -0
- gatewaysdk/environment/schema/connectors/netsuite/handlers.py +32 -0
- gatewaysdk/environment/schema/connectors/netsuite/parity.json +50 -0
- gatewaysdk/environment/schema/connectors/netsuite/provenance.json +137 -0
- gatewaysdk/environment/schema/connectors/netsuite/scope.toml +78 -0
- gatewaysdk/environment/schema/connectors/netsuite/world.json +16243 -0
- gatewaysdk/environment/schema/connectors/salesforce/handlers.py +806 -0
- gatewaysdk/environment/schema/connectors/salesforce/parity.json +46 -0
- gatewaysdk/environment/schema/connectors/salesforce/provenance.json +143 -0
- gatewaysdk/environment/schema/connectors/salesforce/scope.toml +174 -0
- gatewaysdk/environment/schema/connectors/salesforce/world.json +4695 -0
- gatewaysdk/environment/schema/connectors/slack/MODEL.md +236 -0
- gatewaysdk/environment/schema/connectors/slack/capabilities.json +1039 -0
- gatewaysdk/environment/schema/connectors/slack/handlers.py +762 -0
- gatewaysdk/environment/schema/connectors/slack/parity.json +65 -0
- gatewaysdk/environment/schema/connectors/slack/provenance.json +294 -0
- gatewaysdk/environment/schema/connectors/slack/scope.toml +44 -0
- gatewaysdk/environment/schema/connectors/slack/world.json +4993 -0
- gatewaysdk/environment/schema/connectors/workday/handlers.py +75 -0
- gatewaysdk/environment/schema/connectors/workday/parity.json +36 -0
- gatewaysdk/environment/schema/connectors/workday/provenance.json +179 -0
- gatewaysdk/environment/schema/connectors/workday/scope.toml +180 -0
- gatewaysdk/environment/schema/connectors/workday/world.json +806 -0
- gatewaysdk/environment/schema/data.py +593 -0
- gatewaysdk/environment/schema/extract.py +263 -0
- gatewaysdk/environment/schema/host.py +2437 -0
- gatewaysdk/environment/schema/host_surfaces.py +275 -0
- gatewaysdk/environment/schema/platform.py +1272 -0
- gatewaysdk/environment/schema/scaffold.py +250 -0
- gatewaysdk/environment/schema/skill.py +118 -0
- gatewaysdk/environment/schema/skills/agents/connector-template-author.md +202 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONNECTORS.md +331 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONVENTIONS.md +100 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/SKILL.md +313 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/ROWS.md +223 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/SKILL.md +293 -0
- gatewaysdk/environment/schema/skills/worlds-getting-started/SKILL.md +390 -0
- gatewaysdk/environment/schema/slack.py +757 -0
- gatewaysdk/environment/schema/snapshot.py +479 -0
- gatewaysdk/environment/schema/store.py +904 -0
- gatewaysdk/environment/schema/tasks.py +525 -0
- gatewaysdk/environment/schema/tools_world.py +322 -0
- gatewaysdk/environment/schema/validation.py +577 -0
- gatewaysdk/environment/schema/worker.py +78 -0
- gatewaysdk/environment/schema/workers.py +500 -0
- gatewaysdk/environment/schema/world_tests.py +336 -0
- gatewaysdk/environments/__init__.py +24 -0
- gatewaysdk/environments/client.py +486 -0
- gatewaysdk/environments/types.py +318 -0
- gatewaysdk/execution/__init__.py +15 -0
- gatewaysdk/execution/base.py +64 -0
- gatewaysdk/execution/client_server.py +443 -0
- gatewaysdk/execution/events.py +69 -0
- gatewaysdk/execution/inter_process.py +16 -0
- gatewaysdk/execution/shared_memory.py +282 -0
- gatewaysdk/experiments/__init__.py +90 -0
- gatewaysdk/experiments/assignment.py +177 -0
- gatewaysdk/experiments/client.py +877 -0
- gatewaysdk/experiments/exposure.py +222 -0
- gatewaysdk/experiments/types.py +81 -0
- gatewaysdk/importers/__init__.py +49 -0
- gatewaysdk/importers/_normalize.py +91 -0
- gatewaysdk/importers/builder.py +173 -0
- gatewaysdk/importers/client.py +95 -0
- gatewaysdk/importers/langsmith.py +196 -0
- gatewaysdk/importers/recipes.py +249 -0
- gatewaysdk/instrumentation/__init__.py +300 -0
- gatewaysdk/instrumentation/agentops.py +314 -0
- gatewaysdk/instrumentation/agentops_langchain.py +45 -0
- gatewaysdk/instrumentation/base.py +119 -0
- gatewaysdk/instrumentation/litellm.py +83 -0
- gatewaysdk/instrumentation/registry.py +273 -0
- gatewaysdk/instrumentation/vllm.py +81 -0
- gatewaysdk/instrumentation/weave.py +500 -0
- gatewaysdk/integrations/__init__.py +15 -0
- gatewaysdk/integrations/gateway/__init__.py +11 -0
- gatewaysdk/integrations/gateway/client.py +171 -0
- gatewaysdk/integrations/tool_access.py +549 -0
- gatewaysdk/litagent/__init__.py +11 -0
- gatewaysdk/litagent/decorator.py +536 -0
- gatewaysdk/litagent/litagent.py +252 -0
- gatewaysdk/llm_proxy.py +1742 -0
- gatewaysdk/logging.py +370 -0
- gatewaysdk/memory.py +278 -0
- gatewaysdk/personas.py +186 -0
- gatewaysdk/platform/__init__.py +17 -0
- gatewaysdk/platform/builder.py +221 -0
- gatewaysdk/platform/compatibility.py +67 -0
- gatewaysdk/platform/manifest.py +185 -0
- gatewaysdk/platform/orchestrator.py +901 -0
- gatewaysdk/platform/registry.py +122 -0
- gatewaysdk/platform/worker.py +864 -0
- gatewaysdk/replay/__init__.py +1032 -0
- gatewaysdk/replay/pytest.py +56 -0
- gatewaysdk/reward.py +7 -0
- gatewaysdk/run.py +1781 -0
- gatewaysdk/runner/__init__.py +11 -0
- gatewaysdk/runner/agent.py +878 -0
- gatewaysdk/runner/base.py +182 -0
- gatewaysdk/runner/legacy.py +309 -0
- gatewaysdk/security.py +700 -0
- gatewaysdk/semconv.py +170 -0
- gatewaysdk/server.py +399 -0
- gatewaysdk/sessions.py +282 -0
- gatewaysdk/store/__init__.py +45 -0
- gatewaysdk/store/base.py +908 -0
- gatewaysdk/store/client_server.py +2093 -0
- gatewaysdk/store/collection/__init__.py +30 -0
- gatewaysdk/store/collection/base.py +587 -0
- gatewaysdk/store/collection/memory.py +970 -0
- gatewaysdk/store/collection/mongo.py +1412 -0
- gatewaysdk/store/collection_based.py +1823 -0
- gatewaysdk/store/gateway.py +983 -0
- gatewaysdk/store/gateway_listener.py +465 -0
- gatewaysdk/store/listener.py +58 -0
- gatewaysdk/store/memory.py +396 -0
- gatewaysdk/store/mongo.py +165 -0
- gatewaysdk/store/redis_stream.py +517 -0
- gatewaysdk/store/sqlite.py +3 -0
- gatewaysdk/store/threading.py +370 -0
- gatewaysdk/store/utils.py +142 -0
- gatewaysdk/tracer/__init__.py +14 -0
- gatewaysdk/tracer/base.py +286 -0
- gatewaysdk/tracer/dummy.py +106 -0
- gatewaysdk/tracer/otel.py +559 -0
- gatewaysdk/tracing/__init__.py +110 -0
- gatewaysdk/tracing/api.py +808 -0
- gatewaysdk/tracing/attributes.py +9 -0
- gatewaysdk/tracing/context.py +272 -0
- gatewaysdk/tracing/exporters/__init__.py +10 -0
- gatewaysdk/tracing/exporters/gateway.py +228 -0
- gatewaysdk/tracing/identity.py +288 -0
- gatewaysdk/tracing/init.py +620 -0
- gatewaysdk/tracing/instrumentors/__init__.py +15 -0
- gatewaysdk/tracing/instrumentors/claude_agent_sdk.py +766 -0
- gatewaysdk/tracing/instrumentors/instrumentation_principles.md +294 -0
- gatewaysdk/tracing/instrumentors/registry.py +352 -0
- gatewaysdk/tracing/mapping.py +729 -0
- gatewaysdk/tracing/processors.py +393 -0
- gatewaysdk/tracing/push.py +617 -0
- gatewaysdk/tracing/push_models.py +247 -0
- gatewaysdk/tracing/semconv.py +294 -0
- gatewaysdk/tracing/span_builder.py +356 -0
- gatewaysdk/trainer/__init__.py +6 -0
- gatewaysdk/trainer/init_utils.py +263 -0
- gatewaysdk/trainer/legacy.py +359 -0
- gatewaysdk/trainer/registry.py +12 -0
- gatewaysdk/trainer/trainer.py +638 -0
- gatewaysdk/types/__init__.py +63 -0
- gatewaysdk/types/core.py +556 -0
- gatewaysdk/types/resources.py +204 -0
- gatewaysdk/types/tracer.py +515 -0
- gatewaysdk/types/tracing.py +162 -0
- gatewaysdk/users.py +251 -0
- gatewaysdk/utils/__init__.py +1 -0
- gatewaysdk/utils/id.py +18 -0
- gatewaysdk/utils/metrics.py +1025 -0
- gatewaysdk/utils/otel.py +550 -0
- gatewaysdk/utils/otlp.py +556 -0
- gatewaysdk/utils/redact.py +22 -0
- gatewaysdk/utils/server_launcher.py +1045 -0
- gatewaysdk/utils/system_snapshot.py +90 -0
- gatewaysdk/verl/__init__.py +8 -0
- gatewaysdk/verl/__main__.py +6 -0
- gatewaysdk/verl/async_server.py +46 -0
- gatewaysdk/verl/config.yaml +27 -0
- gatewaysdk/verl/daemon.py +1154 -0
- gatewaysdk/verl/dataset.py +44 -0
- gatewaysdk/verl/entrypoint.py +248 -0
- gatewaysdk/verl/trainer.py +549 -0
- gatewaysdk/world_browser.py +748 -0
- gatewaysdk/world_data.py +453 -0
- gatewaysdk/world_sessions.py +978 -0
- gatewaysdk/world_tasks.py +285 -0
- gatewaysdk/world_tools.py +115 -0
- gatewaysdk/worlds.py +639 -0
- gatewaysdk-0.3.2.dist-info/METADATA +236 -0
- gatewaysdk-0.3.2.dist-info/RECORD +376 -0
- gatewaysdk-0.3.2.dist-info/WHEEL +4 -0
- gatewaysdk-0.3.2.dist-info/entry_points.txt +5 -0
- gatewaysdk-0.3.2.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
# Copyright (c) Gateway. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Orchestrator ABC and DockerOrchestrator implementation.
|
|
4
|
+
|
|
5
|
+
The ``Orchestrator`` base class defines the contract for compute lifecycle
|
|
6
|
+
management. Each backend (Docker, Nomad, KubeRay) implements this contract.
|
|
7
|
+
``ComputeHandle`` is an opaque token that the orchestrator returns from
|
|
8
|
+
``start()`` and accepts back in every subsequent call.
|
|
9
|
+
|
|
10
|
+
Usage (via AlgorithmContext)::
|
|
11
|
+
|
|
12
|
+
orch = DockerOrchestrator()
|
|
13
|
+
handle = orch.start(
|
|
14
|
+
run_id="run-abc",
|
|
15
|
+
image="my-agent:v1",
|
|
16
|
+
store_url="http://host.docker.internal:9876",
|
|
17
|
+
count=3,
|
|
18
|
+
lifecycle=ContainerLifecycle.PERSISTENT,
|
|
19
|
+
)
|
|
20
|
+
orch.wait_ready(handle, timeout=120.0)
|
|
21
|
+
# ... algorithm enqueues rollouts, N agents process them ...
|
|
22
|
+
orch.stop(handle)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import time as _time
|
|
29
|
+
from abc import ABC, abstractmethod
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from enum import Enum
|
|
32
|
+
from typing import Any, Dict, List, Optional
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
38
|
+
# Enums & data classes
|
|
39
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ComputeStatus(str, Enum):
|
|
43
|
+
"""Lifecycle state of orchestrated compute."""
|
|
44
|
+
|
|
45
|
+
PENDING = "pending"
|
|
46
|
+
RUNNING = "running"
|
|
47
|
+
SUCCEEDED = "succeeded"
|
|
48
|
+
FAILED = "failed"
|
|
49
|
+
UNKNOWN = "unknown"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ContainerLifecycle(str, Enum):
|
|
53
|
+
"""How agent containers are recycled during a run.
|
|
54
|
+
|
|
55
|
+
PERSISTENT: containers stay alive for the entire run duration.
|
|
56
|
+
Best for stateless agents -- avoids startup overhead.
|
|
57
|
+
EPHEMERAL: containers are replaced after each rollout batch.
|
|
58
|
+
Guarantees clean state for stateful agents (e.g. CRM sim).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
PERSISTENT = "persistent"
|
|
62
|
+
EPHEMERAL = "ephemeral"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ComputeHandle:
|
|
67
|
+
"""Opaque handle returned by start(), passed back to stop()/status().
|
|
68
|
+
|
|
69
|
+
Each orchestrator subclass stores whatever it needs here.
|
|
70
|
+
Docker stores container IDs; Nomad stores a job ID; KubeRay stores a
|
|
71
|
+
RayJob name; etc.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
run_id: str
|
|
75
|
+
backend: str # "docker", "nomad", "kuberay", ...
|
|
76
|
+
identifiers: Dict[str, str] = field(default_factory=dict)
|
|
77
|
+
# e.g. {"agent-0": "abc123", "agent-1": "def456"} (Docker)
|
|
78
|
+
# e.g. {"job_id": "gateway-run-42"} (Nomad)
|
|
79
|
+
# e.g. {"rayjob": "gateway-verl-run-42"} (KubeRay)
|
|
80
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
84
|
+
# Orchestrator ABC
|
|
85
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Orchestrator(ABC):
|
|
89
|
+
"""Base class for compute orchestrators.
|
|
90
|
+
|
|
91
|
+
An orchestrator manages the lifecycle of compute resources for a
|
|
92
|
+
platform run. Subclasses handle the specifics of Docker, Nomad,
|
|
93
|
+
KubeRay, etc.
|
|
94
|
+
|
|
95
|
+
Lifecycle (called by AlgorithmContext)::
|
|
96
|
+
|
|
97
|
+
handle = orch.start(...) # __aenter__
|
|
98
|
+
orch.wait_ready(handle) # block until healthy
|
|
99
|
+
# ... algorithm runs ...
|
|
100
|
+
orch.status(handle) # health checks during run
|
|
101
|
+
orch.stop(handle) # __aexit__
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def available(self) -> bool:
|
|
107
|
+
"""Can this orchestrator function in the current environment?"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def backend_name(self) -> str:
|
|
113
|
+
"""Short identifier: 'docker', 'nomad', 'kuberay', etc."""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def start(
|
|
118
|
+
self,
|
|
119
|
+
*,
|
|
120
|
+
run_id: str,
|
|
121
|
+
image: str,
|
|
122
|
+
store_url: str,
|
|
123
|
+
count: int = 1,
|
|
124
|
+
lifecycle: ContainerLifecycle = ContainerLifecycle.PERSISTENT,
|
|
125
|
+
env: Optional[Dict[str, str]] = None,
|
|
126
|
+
labels: Optional[Dict[str, str]] = None,
|
|
127
|
+
runner_config: Optional[Dict[str, Any]] = None,
|
|
128
|
+
resource_limits: Optional[Dict[str, Any]] = None,
|
|
129
|
+
# --- reactive scaling (optional) ---
|
|
130
|
+
store: Optional[Any] = None,
|
|
131
|
+
n_runners_per_unit: int = 1,
|
|
132
|
+
max_units: int = 20,
|
|
133
|
+
# --- extended fields (used by KubeRay, ignored by Docker/Nomad) ---
|
|
134
|
+
trainer_resources: Optional[Dict[str, Any]] = None,
|
|
135
|
+
services: Optional[Dict[str, Any]] = None,
|
|
136
|
+
algorithm_config: Optional[Dict[str, Any]] = None,
|
|
137
|
+
) -> ComputeHandle:
|
|
138
|
+
"""Provision compute for a run.
|
|
139
|
+
|
|
140
|
+
For Docker: starts ``count`` agent containers
|
|
141
|
+
For Nomad: submits a job spec with task group ``count``
|
|
142
|
+
For KubeRay: creates a RayJob CR (head + workers)
|
|
143
|
+
|
|
144
|
+
If ``store`` is provided, the orchestrator registers a listener
|
|
145
|
+
for reactive scaling (start containers when rollouts are enqueued)
|
|
146
|
+
and ephemeral recycling (replace containers after rollout completion
|
|
147
|
+
when lifecycle is EPHEMERAL).
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
run_id: GatewayRun ID.
|
|
151
|
+
image: Container image to run.
|
|
152
|
+
store_url: HTTP URL of the worker's LightningStoreServer.
|
|
153
|
+
count: Number of agent instances to start immediately.
|
|
154
|
+
Use 0 for fully reactive (containers start on demand).
|
|
155
|
+
lifecycle: Whether containers persist or are replaced per batch.
|
|
156
|
+
env: Additional environment variables.
|
|
157
|
+
labels: Metadata labels for the containers.
|
|
158
|
+
runner_config: Runner lifecycle config from agent manifest.
|
|
159
|
+
resource_limits: Container resource limits from agent manifest.
|
|
160
|
+
store: LightningStore instance. If provided, enables reactive
|
|
161
|
+
scaling via store listener hooks.
|
|
162
|
+
n_runners_per_unit: Rollouts a single compute unit handles
|
|
163
|
+
concurrently (used for capacity calculation).
|
|
164
|
+
max_units: Hard cap on number of compute units.
|
|
165
|
+
trainer_resources: GPU/memory for trainer (KubeRay only).
|
|
166
|
+
services: Sidecar services (KubeRay only).
|
|
167
|
+
algorithm_config: Algorithm-specific config (KubeRay only).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
ComputeHandle with identifiers for the provisioned compute.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
RuntimeError: If compute cannot be provisioned.
|
|
174
|
+
"""
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def stop(self, handle: ComputeHandle, timeout: int = 30) -> None:
|
|
179
|
+
"""Tear down all compute for a run.
|
|
180
|
+
|
|
181
|
+
Must be idempotent -- safe to call multiple times or after
|
|
182
|
+
compute has already exited.
|
|
183
|
+
"""
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
@abstractmethod
|
|
187
|
+
def status(self, handle: ComputeHandle) -> ComputeStatus:
|
|
188
|
+
"""Check aggregate health of the compute for a run.
|
|
189
|
+
|
|
190
|
+
Returns RUNNING if all members are healthy, FAILED if any have
|
|
191
|
+
crashed, PENDING if still starting up.
|
|
192
|
+
"""
|
|
193
|
+
...
|
|
194
|
+
|
|
195
|
+
@abstractmethod
|
|
196
|
+
def wait_ready(self, handle: ComputeHandle, timeout: float = 120.0) -> None:
|
|
197
|
+
"""Block until all provisioned compute is healthy.
|
|
198
|
+
|
|
199
|
+
Docker: poll container.status until all report 'running'.
|
|
200
|
+
Nomad: poll deployment health.
|
|
201
|
+
KubeRay: poll RayJob status.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
RuntimeError: If timeout exceeded before all members are ready.
|
|
205
|
+
"""
|
|
206
|
+
...
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def scale(self, handle: ComputeHandle, count: int) -> None:
|
|
210
|
+
"""Adjust the number of running instances.
|
|
211
|
+
|
|
212
|
+
Docker: start/stop delta containers, update handle.identifiers.
|
|
213
|
+
Nomad: update job count, scheduler reconciles.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
handle: ComputeHandle to scale.
|
|
217
|
+
count: New desired count.
|
|
218
|
+
"""
|
|
219
|
+
...
|
|
220
|
+
|
|
221
|
+
@abstractmethod
|
|
222
|
+
def replace_member(
|
|
223
|
+
self,
|
|
224
|
+
handle: ComputeHandle,
|
|
225
|
+
member_key: str,
|
|
226
|
+
) -> Optional[str]:
|
|
227
|
+
"""Replace one member of the compute group (ephemeral mode).
|
|
228
|
+
|
|
229
|
+
Docker: stop container, start fresh one, update handle.
|
|
230
|
+
Nomad: ``nomad alloc restart``, or no-op (scheduler handles).
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
handle: ComputeHandle containing the member.
|
|
234
|
+
member_key: Key in handle.identifiers (e.g. "agent-0").
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
New container/alloc ID, or None if replacement failed.
|
|
238
|
+
"""
|
|
239
|
+
...
|
|
240
|
+
|
|
241
|
+
def members(self, handle: ComputeHandle) -> List[Dict[str, Any]]:
|
|
242
|
+
"""List individual members of the compute group.
|
|
243
|
+
|
|
244
|
+
Optional -- returns empty list by default.
|
|
245
|
+
"""
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
def get_logs(self, handle: ComputeHandle, tail: int = 100) -> str:
|
|
249
|
+
"""Fetch recent logs. Optional -- returns empty string by default."""
|
|
250
|
+
return ""
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
254
|
+
# DockerOrchestrator
|
|
255
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class DockerOrchestrator(Orchestrator):
|
|
259
|
+
"""Manages agent containers via Docker SDK.
|
|
260
|
+
|
|
261
|
+
For local development, uses the Docker socket to start/stop containers.
|
|
262
|
+
Gracefully degrades if Docker is not available (e.g., in CI without Docker).
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self, docker_host: Optional[str] = None):
|
|
266
|
+
"""
|
|
267
|
+
Args:
|
|
268
|
+
docker_host: Docker daemon URL. Defaults to unix:///var/run/docker.sock.
|
|
269
|
+
"""
|
|
270
|
+
self._client = None
|
|
271
|
+
self._stop_timeouts: Dict[str, int] = {}
|
|
272
|
+
try:
|
|
273
|
+
import docker
|
|
274
|
+
|
|
275
|
+
self._client = docker.from_env() if not docker_host else docker.DockerClient(base_url=docker_host)
|
|
276
|
+
# Quick connectivity check
|
|
277
|
+
self._client.ping()
|
|
278
|
+
logger.info("DockerOrchestrator connected to Docker daemon")
|
|
279
|
+
except ImportError:
|
|
280
|
+
logger.warning("Docker SDK not installed. Install with: pip install docker")
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.warning(f"Could not connect to Docker daemon: {e}")
|
|
283
|
+
self._client = None
|
|
284
|
+
|
|
285
|
+
# ── Orchestrator properties ─────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def available(self) -> bool:
|
|
289
|
+
return self._client is not None
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def backend_name(self) -> str:
|
|
293
|
+
return "docker"
|
|
294
|
+
|
|
295
|
+
# ── Private helpers ─────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
def _build_container_env(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
run_id: str,
|
|
301
|
+
store_url: str,
|
|
302
|
+
env: Optional[Dict[str, str]],
|
|
303
|
+
runner_config: Optional[Dict[str, Any]],
|
|
304
|
+
) -> Dict[str, str]:
|
|
305
|
+
"""Build the environment dict for an agent container."""
|
|
306
|
+
import os as _os
|
|
307
|
+
|
|
308
|
+
container_env: Dict[str, str] = {
|
|
309
|
+
"GATEWAY_RUN_ID": run_id,
|
|
310
|
+
"GATEWAY_STORE_URL": store_url,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Forward Gateway tracing config from worker env unconditionally
|
|
314
|
+
for _key in (
|
|
315
|
+
"GATEWAY_HOST",
|
|
316
|
+
"GATEWAY_PUBLIC_KEY",
|
|
317
|
+
"GATEWAY_SECRET_KEY",
|
|
318
|
+
):
|
|
319
|
+
_val = _os.environ.get(_key)
|
|
320
|
+
if _val:
|
|
321
|
+
if _key == "GATEWAY_HOST" and ("localhost" in _val or "127.0.0.1" in _val):
|
|
322
|
+
import platform as _plat
|
|
323
|
+
|
|
324
|
+
_access = "host.docker.internal" if _plat.system() == "Darwin" else _val
|
|
325
|
+
_val = _val.replace("localhost", _access).replace("127.0.0.1", _access)
|
|
326
|
+
container_env[_key] = _val
|
|
327
|
+
|
|
328
|
+
# Fallback: forward LLM API keys from worker env ONLY if not already
|
|
329
|
+
# provided via ProjectSecret (caller-provided env takes precedence).
|
|
330
|
+
for _key in (
|
|
331
|
+
"ANTHROPIC_API_KEY",
|
|
332
|
+
"OPENAI_API_KEY",
|
|
333
|
+
"AZURE_API_KEY",
|
|
334
|
+
"GOOGLE_API_KEY",
|
|
335
|
+
"COHERE_API_KEY",
|
|
336
|
+
):
|
|
337
|
+
if _key not in container_env:
|
|
338
|
+
_val = _os.environ.get(_key)
|
|
339
|
+
if _val:
|
|
340
|
+
container_env[_key] = _val
|
|
341
|
+
|
|
342
|
+
# Merge caller-provided env (takes precedence)
|
|
343
|
+
if env:
|
|
344
|
+
container_env.update(env)
|
|
345
|
+
|
|
346
|
+
# Inject runner config as env vars (read by LitAgentRunner)
|
|
347
|
+
if runner_config:
|
|
348
|
+
if runner_config.get("max_rollouts") is not None:
|
|
349
|
+
container_env["GATEWAY_MAX_ROLLOUTS"] = str(runner_config["max_rollouts"])
|
|
350
|
+
if runner_config.get("poll_interval") is not None:
|
|
351
|
+
container_env["GATEWAY_POLL_INTERVAL"] = str(runner_config["poll_interval"])
|
|
352
|
+
if runner_config.get("n_runners") is not None:
|
|
353
|
+
container_env["GATEWAY_N_RUNNERS"] = str(runner_config["n_runners"])
|
|
354
|
+
|
|
355
|
+
return container_env
|
|
356
|
+
|
|
357
|
+
def _detect_network(self) -> str:
|
|
358
|
+
"""Auto-detect the Docker network to attach agent containers to."""
|
|
359
|
+
if not self._client:
|
|
360
|
+
return "bridge"
|
|
361
|
+
try:
|
|
362
|
+
networks = [n.name for n in self._client.networks.list()]
|
|
363
|
+
for candidate in (
|
|
364
|
+
"langfuse-network",
|
|
365
|
+
"gateway_default",
|
|
366
|
+
"gateway_default",
|
|
367
|
+
):
|
|
368
|
+
if candidate in networks:
|
|
369
|
+
logger.info(f"Auto-detected Docker network: {candidate}")
|
|
370
|
+
return candidate
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
return "bridge"
|
|
374
|
+
|
|
375
|
+
def _build_docker_kwargs(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
container_env: Dict[str, str],
|
|
379
|
+
container_labels: Dict[str, str],
|
|
380
|
+
network: str,
|
|
381
|
+
resource_limits: Optional[Dict[str, Any]],
|
|
382
|
+
name: Optional[str] = None,
|
|
383
|
+
) -> Dict[str, Any]:
|
|
384
|
+
"""Build the kwargs dict for ``docker.containers.run()``."""
|
|
385
|
+
docker_kwargs: Dict[str, Any] = {
|
|
386
|
+
"detach": True,
|
|
387
|
+
"network": network,
|
|
388
|
+
"environment": container_env,
|
|
389
|
+
"labels": container_labels,
|
|
390
|
+
"auto_remove": False,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if name:
|
|
394
|
+
docker_kwargs["name"] = name
|
|
395
|
+
|
|
396
|
+
if resource_limits:
|
|
397
|
+
if resource_limits.get("memory"):
|
|
398
|
+
docker_kwargs["mem_limit"] = resource_limits["memory"]
|
|
399
|
+
if resource_limits.get("cpu"):
|
|
400
|
+
docker_kwargs["nano_cpus"] = int(float(resource_limits["cpu"]) * 1e9)
|
|
401
|
+
gpu = resource_limits.get("gpu")
|
|
402
|
+
if gpu and gpu is not False:
|
|
403
|
+
try:
|
|
404
|
+
import docker as _docker
|
|
405
|
+
|
|
406
|
+
gpu_count = -1 if gpu is True else int(gpu)
|
|
407
|
+
docker_kwargs["device_requests"] = [
|
|
408
|
+
_docker.types.DeviceRequest(count=gpu_count, capabilities=[["gpu"]])
|
|
409
|
+
]
|
|
410
|
+
except (ImportError, Exception) as e:
|
|
411
|
+
logger.warning(f"GPU device request failed: {e}")
|
|
412
|
+
|
|
413
|
+
return docker_kwargs
|
|
414
|
+
|
|
415
|
+
def _start_one_container(
|
|
416
|
+
self,
|
|
417
|
+
*,
|
|
418
|
+
image: str,
|
|
419
|
+
run_id: str,
|
|
420
|
+
index: int,
|
|
421
|
+
container_env: Dict[str, str],
|
|
422
|
+
network: str,
|
|
423
|
+
labels: Optional[Dict[str, str]],
|
|
424
|
+
resource_limits: Optional[Dict[str, Any]],
|
|
425
|
+
) -> Optional[str]:
|
|
426
|
+
"""Start a single agent container. Returns container ID or None."""
|
|
427
|
+
if not self._client:
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
name = f"gateway-{run_id[:8]}-agent-{index}"
|
|
431
|
+
container_labels = {
|
|
432
|
+
"gateway.run_id": run_id,
|
|
433
|
+
"gateway.component": "agent",
|
|
434
|
+
"gateway.agent_index": str(index),
|
|
435
|
+
**(labels or {}),
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
docker_kwargs = self._build_docker_kwargs(
|
|
439
|
+
container_env=container_env,
|
|
440
|
+
container_labels=container_labels,
|
|
441
|
+
network=network,
|
|
442
|
+
resource_limits=resource_limits,
|
|
443
|
+
name=name,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
container = self._client.containers.run(image, **docker_kwargs)
|
|
448
|
+
logger.info(f"Started container {name} ({container.short_id}) " f"from {image} for run {run_id}")
|
|
449
|
+
return container.id
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error(f"Failed to start container {name}: {e}")
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
# ── Orchestrator interface ──────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
def start(
|
|
457
|
+
self,
|
|
458
|
+
*,
|
|
459
|
+
run_id: str,
|
|
460
|
+
image: str,
|
|
461
|
+
store_url: str,
|
|
462
|
+
count: int = 0,
|
|
463
|
+
lifecycle: ContainerLifecycle = ContainerLifecycle.PERSISTENT,
|
|
464
|
+
env: Optional[Dict[str, str]] = None,
|
|
465
|
+
labels: Optional[Dict[str, str]] = None,
|
|
466
|
+
runner_config: Optional[Dict[str, Any]] = None,
|
|
467
|
+
resource_limits: Optional[Dict[str, Any]] = None,
|
|
468
|
+
store: Optional[Any] = None,
|
|
469
|
+
n_runners_per_unit: int = 1,
|
|
470
|
+
max_units: int = 20,
|
|
471
|
+
trainer_resources: Optional[Dict[str, Any]] = None,
|
|
472
|
+
services: Optional[Dict[str, Any]] = None,
|
|
473
|
+
algorithm_config: Optional[Dict[str, Any]] = None,
|
|
474
|
+
) -> ComputeHandle:
|
|
475
|
+
if not self._client:
|
|
476
|
+
raise RuntimeError("Docker not available, cannot start containers")
|
|
477
|
+
|
|
478
|
+
container_env = self._build_container_env(
|
|
479
|
+
run_id=run_id,
|
|
480
|
+
store_url=store_url,
|
|
481
|
+
env=env,
|
|
482
|
+
runner_config=runner_config,
|
|
483
|
+
)
|
|
484
|
+
network = self._detect_network()
|
|
485
|
+
|
|
486
|
+
# Store stop timeout for use in stop()
|
|
487
|
+
stop_timeout = (resource_limits or {}).get("stop_timeout")
|
|
488
|
+
if stop_timeout:
|
|
489
|
+
self._stop_timeouts[run_id] = int(stop_timeout)
|
|
490
|
+
|
|
491
|
+
identifiers: Dict[str, str] = {}
|
|
492
|
+
for i in range(count):
|
|
493
|
+
cid = self._start_one_container(
|
|
494
|
+
image=image,
|
|
495
|
+
run_id=run_id,
|
|
496
|
+
index=i,
|
|
497
|
+
container_env=container_env,
|
|
498
|
+
network=network,
|
|
499
|
+
labels=labels,
|
|
500
|
+
resource_limits=resource_limits,
|
|
501
|
+
)
|
|
502
|
+
if cid is None:
|
|
503
|
+
# Clean up any containers that were already started
|
|
504
|
+
for key, started_id in identifiers.items():
|
|
505
|
+
self._stop_container(started_id)
|
|
506
|
+
raise RuntimeError(f"Failed to start container agent-{i} " f"({i}/{count} started successfully)")
|
|
507
|
+
identifiers[f"agent-{i}"] = cid
|
|
508
|
+
|
|
509
|
+
if count > 0:
|
|
510
|
+
logger.info(f"Started {count} agent container(s) for run {run_id} " f"(lifecycle={lifecycle.value})")
|
|
511
|
+
|
|
512
|
+
handle = ComputeHandle(
|
|
513
|
+
run_id=run_id,
|
|
514
|
+
backend=self.backend_name,
|
|
515
|
+
identifiers=identifiers,
|
|
516
|
+
metadata={
|
|
517
|
+
"lifecycle": lifecycle.value,
|
|
518
|
+
"desired_count": count,
|
|
519
|
+
"image": image,
|
|
520
|
+
"store_url": store_url,
|
|
521
|
+
"network": network,
|
|
522
|
+
"env": env or {},
|
|
523
|
+
"runner_config": runner_config or {},
|
|
524
|
+
"resource_limits": resource_limits or {},
|
|
525
|
+
"labels": labels or {},
|
|
526
|
+
},
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# If a store is provided, set up reactive scaling automatically.
|
|
530
|
+
if store is not None:
|
|
531
|
+
self._setup_reactive_listener(
|
|
532
|
+
handle,
|
|
533
|
+
store,
|
|
534
|
+
n_runners_per_unit,
|
|
535
|
+
max_units,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return handle
|
|
539
|
+
|
|
540
|
+
def stop(self, handle: ComputeHandle, timeout: int = 30) -> None:
|
|
541
|
+
if not self._client:
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# Remove reactive listener first so no more events fire.
|
|
545
|
+
self._teardown_reactive_listener(handle)
|
|
546
|
+
|
|
547
|
+
stopped = 0
|
|
548
|
+
for key, cid in list(handle.identifiers.items()):
|
|
549
|
+
self._stop_container(cid)
|
|
550
|
+
stopped += 1
|
|
551
|
+
|
|
552
|
+
if stopped:
|
|
553
|
+
logger.info(f"Killed and removed {stopped} container(s) for " f"run {handle.run_id}")
|
|
554
|
+
|
|
555
|
+
def status(self, handle: ComputeHandle) -> ComputeStatus:
|
|
556
|
+
if not self._client:
|
|
557
|
+
return ComputeStatus.UNKNOWN
|
|
558
|
+
|
|
559
|
+
statuses = []
|
|
560
|
+
for key, cid in handle.identifiers.items():
|
|
561
|
+
s = self._get_container_status(cid)
|
|
562
|
+
statuses.append(s)
|
|
563
|
+
|
|
564
|
+
if not statuses:
|
|
565
|
+
return ComputeStatus.UNKNOWN
|
|
566
|
+
if all(s == "running" for s in statuses):
|
|
567
|
+
return ComputeStatus.RUNNING
|
|
568
|
+
if any(s in ("exited", "dead") for s in statuses):
|
|
569
|
+
return ComputeStatus.FAILED
|
|
570
|
+
if all(s in ("created", "restarting") for s in statuses):
|
|
571
|
+
return ComputeStatus.PENDING
|
|
572
|
+
# Mixed state (some running, some starting) -- still consider running
|
|
573
|
+
if any(s == "running" for s in statuses):
|
|
574
|
+
return ComputeStatus.RUNNING
|
|
575
|
+
return ComputeStatus.UNKNOWN
|
|
576
|
+
|
|
577
|
+
def wait_ready(
|
|
578
|
+
self,
|
|
579
|
+
handle: ComputeHandle,
|
|
580
|
+
timeout: float = 120.0,
|
|
581
|
+
) -> None:
|
|
582
|
+
if not self._client:
|
|
583
|
+
raise RuntimeError("Docker not available")
|
|
584
|
+
|
|
585
|
+
deadline = _time.monotonic() + timeout
|
|
586
|
+
poll_interval = 2.0
|
|
587
|
+
target_count = len(handle.identifiers)
|
|
588
|
+
|
|
589
|
+
while _time.monotonic() < deadline:
|
|
590
|
+
ready = 0
|
|
591
|
+
for key, cid in handle.identifiers.items():
|
|
592
|
+
s = self._get_container_status(cid)
|
|
593
|
+
if s == "running":
|
|
594
|
+
ready += 1
|
|
595
|
+
elif s in ("exited", "dead"):
|
|
596
|
+
raise RuntimeError(f"Container {key} ({cid[:12]}) exited before " f"becoming ready")
|
|
597
|
+
|
|
598
|
+
if ready >= target_count:
|
|
599
|
+
logger.info(f"All {target_count} container(s) ready for " f"run {handle.run_id}")
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
logger.debug(
|
|
603
|
+
f"Waiting for containers: {ready}/{target_count} ready "
|
|
604
|
+
f"({timeout - (_time.monotonic() - (deadline - timeout)):.0f}s remaining)"
|
|
605
|
+
)
|
|
606
|
+
_time.sleep(poll_interval)
|
|
607
|
+
|
|
608
|
+
raise RuntimeError(
|
|
609
|
+
f"Timeout ({timeout}s) waiting for {target_count} container(s) " f"to become ready for run {handle.run_id}"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
def scale(self, handle: ComputeHandle, count: int) -> None:
|
|
613
|
+
if not self._client:
|
|
614
|
+
raise RuntimeError("Docker not available")
|
|
615
|
+
|
|
616
|
+
current = len(handle.identifiers)
|
|
617
|
+
if count == current:
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
meta = handle.metadata
|
|
621
|
+
|
|
622
|
+
if count > current:
|
|
623
|
+
# Scale up: start additional containers
|
|
624
|
+
container_env = self._build_container_env(
|
|
625
|
+
run_id=handle.run_id,
|
|
626
|
+
store_url=meta["store_url"],
|
|
627
|
+
env=meta.get("env"),
|
|
628
|
+
runner_config=meta.get("runner_config"),
|
|
629
|
+
)
|
|
630
|
+
network = meta.get("network", self._detect_network())
|
|
631
|
+
|
|
632
|
+
for i in range(current, count):
|
|
633
|
+
cid = self._start_one_container(
|
|
634
|
+
image=meta["image"],
|
|
635
|
+
run_id=handle.run_id,
|
|
636
|
+
index=i,
|
|
637
|
+
container_env=container_env,
|
|
638
|
+
network=network,
|
|
639
|
+
labels=meta.get("labels"),
|
|
640
|
+
resource_limits=meta.get("resource_limits"),
|
|
641
|
+
)
|
|
642
|
+
if cid is None:
|
|
643
|
+
logger.error(f"Failed to scale up: could not start agent-{i}")
|
|
644
|
+
break
|
|
645
|
+
handle.identifiers[f"agent-{i}"] = cid
|
|
646
|
+
|
|
647
|
+
logger.info(f"Scaled up from {current} to {len(handle.identifiers)} " f"for run {handle.run_id}")
|
|
648
|
+
else:
|
|
649
|
+
# Scale down: stop highest-indexed containers
|
|
650
|
+
keys_sorted = sorted(
|
|
651
|
+
handle.identifiers.keys(),
|
|
652
|
+
key=lambda k: int(k.split("-")[-1]),
|
|
653
|
+
reverse=True,
|
|
654
|
+
)
|
|
655
|
+
to_remove = keys_sorted[: current - count]
|
|
656
|
+
for key in to_remove:
|
|
657
|
+
cid = handle.identifiers.pop(key)
|
|
658
|
+
self._stop_container(cid)
|
|
659
|
+
|
|
660
|
+
logger.info(f"Scaled down from {current} to {len(handle.identifiers)} " f"for run {handle.run_id}")
|
|
661
|
+
|
|
662
|
+
handle.metadata["desired_count"] = count
|
|
663
|
+
|
|
664
|
+
def replace_member(
|
|
665
|
+
self,
|
|
666
|
+
handle: ComputeHandle,
|
|
667
|
+
member_key: str,
|
|
668
|
+
) -> Optional[str]:
|
|
669
|
+
if not self._client:
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
old_cid = handle.identifiers.get(member_key)
|
|
673
|
+
if old_cid:
|
|
674
|
+
self._stop_container(old_cid)
|
|
675
|
+
|
|
676
|
+
meta = handle.metadata
|
|
677
|
+
index = int(member_key.split("-")[-1])
|
|
678
|
+
|
|
679
|
+
container_env = self._build_container_env(
|
|
680
|
+
run_id=handle.run_id,
|
|
681
|
+
store_url=meta["store_url"],
|
|
682
|
+
env=meta.get("env"),
|
|
683
|
+
runner_config=meta.get("runner_config"),
|
|
684
|
+
)
|
|
685
|
+
network = meta.get("network", self._detect_network())
|
|
686
|
+
|
|
687
|
+
new_cid = self._start_one_container(
|
|
688
|
+
image=meta["image"],
|
|
689
|
+
run_id=handle.run_id,
|
|
690
|
+
index=index,
|
|
691
|
+
container_env=container_env,
|
|
692
|
+
network=network,
|
|
693
|
+
labels=meta.get("labels"),
|
|
694
|
+
resource_limits=meta.get("resource_limits"),
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
if new_cid:
|
|
698
|
+
handle.identifiers[member_key] = new_cid
|
|
699
|
+
logger.info(
|
|
700
|
+
f"Replaced {member_key} ({old_cid[:12] if old_cid else '?'}) "
|
|
701
|
+
f"with {new_cid[:12]} for run {handle.run_id}"
|
|
702
|
+
)
|
|
703
|
+
else:
|
|
704
|
+
logger.error(f"Failed to replace {member_key} for run {handle.run_id}")
|
|
705
|
+
|
|
706
|
+
return new_cid
|
|
707
|
+
|
|
708
|
+
def members(self, handle: ComputeHandle) -> List[Dict[str, Any]]:
|
|
709
|
+
if not self._client:
|
|
710
|
+
return []
|
|
711
|
+
|
|
712
|
+
result = []
|
|
713
|
+
for key, cid in sorted(handle.identifiers.items()):
|
|
714
|
+
index = int(key.split("-")[-1])
|
|
715
|
+
status = self._get_container_status(cid) or "unknown"
|
|
716
|
+
result.append(
|
|
717
|
+
{
|
|
718
|
+
"key": key,
|
|
719
|
+
"id": cid,
|
|
720
|
+
"short_id": cid[:12],
|
|
721
|
+
"index": index,
|
|
722
|
+
"status": status,
|
|
723
|
+
"name": f"gateway-{handle.run_id[:8]}-agent-{index}",
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
return result
|
|
727
|
+
|
|
728
|
+
def get_logs(self, handle: ComputeHandle, tail: int = 100) -> str:
|
|
729
|
+
if not self._client:
|
|
730
|
+
return ""
|
|
731
|
+
|
|
732
|
+
parts: List[str] = []
|
|
733
|
+
for key, cid in sorted(handle.identifiers.items()):
|
|
734
|
+
try:
|
|
735
|
+
container = self._client.containers.get(cid)
|
|
736
|
+
logs = container.logs(tail=tail, timestamps=False)
|
|
737
|
+
if isinstance(logs, bytes):
|
|
738
|
+
logs = logs.decode("utf-8", errors="replace")
|
|
739
|
+
parts.append(f"=== {key} ({cid[:12]}) ===\n{logs}")
|
|
740
|
+
except Exception as e:
|
|
741
|
+
parts.append(f"=== {key} ({cid[:12]}) === [error: {e}]")
|
|
742
|
+
return "\n".join(parts)
|
|
743
|
+
|
|
744
|
+
# ── Low-level container helpers ─────────────────────────────────
|
|
745
|
+
|
|
746
|
+
def _stop_container(self, container_id: str) -> None:
|
|
747
|
+
"""Kill and remove a container immediately."""
|
|
748
|
+
if not self._client:
|
|
749
|
+
return
|
|
750
|
+
try:
|
|
751
|
+
container = self._client.containers.get(container_id)
|
|
752
|
+
try:
|
|
753
|
+
container.kill()
|
|
754
|
+
except Exception:
|
|
755
|
+
pass # Already stopped / exited
|
|
756
|
+
container.remove(force=True)
|
|
757
|
+
logger.debug(f"Killed and removed container {container_id[:12]}")
|
|
758
|
+
except Exception as e:
|
|
759
|
+
logger.warning(f"Could not kill/remove container {container_id[:12]}: {e}")
|
|
760
|
+
|
|
761
|
+
def _get_container_status(self, container_id: str) -> Optional[str]:
|
|
762
|
+
"""Get container status (running, exited, etc.)."""
|
|
763
|
+
if not self._client:
|
|
764
|
+
return None
|
|
765
|
+
try:
|
|
766
|
+
container = self._client.containers.get(container_id)
|
|
767
|
+
return container.status
|
|
768
|
+
except Exception as e:
|
|
769
|
+
logger.debug(f"Could not get container status: {e}")
|
|
770
|
+
return None
|
|
771
|
+
|
|
772
|
+
# ── Legacy helpers (used by stop_all_for_run fallback) ──────────
|
|
773
|
+
|
|
774
|
+
def list_run_containers(self, run_id: str) -> List[Dict[str, Any]]:
|
|
775
|
+
"""List all containers associated with a run (by label)."""
|
|
776
|
+
if not self._client:
|
|
777
|
+
return []
|
|
778
|
+
try:
|
|
779
|
+
containers = self._client.containers.list(
|
|
780
|
+
all=True,
|
|
781
|
+
filters={"label": f"gateway.run_id={run_id}"},
|
|
782
|
+
)
|
|
783
|
+
return [
|
|
784
|
+
{
|
|
785
|
+
"id": c.id,
|
|
786
|
+
"short_id": c.short_id,
|
|
787
|
+
"name": c.name,
|
|
788
|
+
"status": c.status,
|
|
789
|
+
"image": str(c.image),
|
|
790
|
+
}
|
|
791
|
+
for c in containers
|
|
792
|
+
]
|
|
793
|
+
except Exception as e:
|
|
794
|
+
logger.debug(f"Could not list containers for run {run_id}: {e}")
|
|
795
|
+
return []
|
|
796
|
+
|
|
797
|
+
def stop_all_for_run(self, run_id: str) -> int:
|
|
798
|
+
"""Kill all containers associated with a run (label-based fallback)."""
|
|
799
|
+
containers = self.list_run_containers(run_id)
|
|
800
|
+
stopped = 0
|
|
801
|
+
for c in containers:
|
|
802
|
+
self._stop_container(c["id"])
|
|
803
|
+
stopped += 1
|
|
804
|
+
return stopped
|
|
805
|
+
|
|
806
|
+
# ── Reactive scaling / ephemeral recycling ───────────────────
|
|
807
|
+
|
|
808
|
+
def _setup_reactive_listener(
|
|
809
|
+
self,
|
|
810
|
+
handle: ComputeHandle,
|
|
811
|
+
store: Any,
|
|
812
|
+
n_runners_per_unit: int,
|
|
813
|
+
max_units: int,
|
|
814
|
+
) -> None:
|
|
815
|
+
"""Register a store listener for on-demand scaling and ephemeral replacement.
|
|
816
|
+
|
|
817
|
+
Called automatically from ``start()`` when a ``store`` is provided.
|
|
818
|
+
"""
|
|
819
|
+
import math
|
|
820
|
+
|
|
821
|
+
lifecycle_str = handle.metadata.get("lifecycle", "persistent")
|
|
822
|
+
is_ephemeral = lifecycle_str == ContainerLifecycle.EPHEMERAL.value
|
|
823
|
+
orch = self
|
|
824
|
+
|
|
825
|
+
class _ReactiveListener:
|
|
826
|
+
"""Store listener that starts/replaces containers on demand."""
|
|
827
|
+
|
|
828
|
+
@property
|
|
829
|
+
def capabilities(self) -> Dict[str, bool]:
|
|
830
|
+
return {}
|
|
831
|
+
|
|
832
|
+
def otlp_traces_endpoint(self) -> Optional[str]:
|
|
833
|
+
return None
|
|
834
|
+
|
|
835
|
+
def get_otlp_headers(self) -> Dict[str, str]:
|
|
836
|
+
return {}
|
|
837
|
+
|
|
838
|
+
async def on_rollout_created(self, rollout: Any) -> None:
|
|
839
|
+
current_units = len(handle.identifiers)
|
|
840
|
+
# Scale to one more container than we currently have,
|
|
841
|
+
# but only if all existing slots are occupied.
|
|
842
|
+
# Don't call wait_ready() here -- it blocks the event
|
|
843
|
+
# loop and causes cascading scale-ups. The container
|
|
844
|
+
# will become ready asynchronously; the runner inside
|
|
845
|
+
# polls the store and picks up work once healthy.
|
|
846
|
+
if current_units < max_units:
|
|
847
|
+
needed = current_units + 1
|
|
848
|
+
try:
|
|
849
|
+
orch.scale(handle, needed)
|
|
850
|
+
logger.info(
|
|
851
|
+
f"Reactive: started container agent-{current_units} "
|
|
852
|
+
f"for run {handle.run_id} "
|
|
853
|
+
f"({needed} total)"
|
|
854
|
+
)
|
|
855
|
+
except Exception as e:
|
|
856
|
+
logger.warning(f"Reactive scale-up failed for " f"run {handle.run_id}: {e}")
|
|
857
|
+
|
|
858
|
+
async def on_rollout_updated(self, rollout: Any) -> None:
|
|
859
|
+
if not is_ephemeral:
|
|
860
|
+
return
|
|
861
|
+
status = getattr(rollout, "status", None)
|
|
862
|
+
if status not in ("succeeded", "failed"):
|
|
863
|
+
return
|
|
864
|
+
|
|
865
|
+
# Replace the first container that is no longer running
|
|
866
|
+
# (exited after processing its rollout).
|
|
867
|
+
for key, cid in list(handle.identifiers.items()):
|
|
868
|
+
cstatus = orch._get_container_status(cid)
|
|
869
|
+
if cstatus in ("exited", "dead", None):
|
|
870
|
+
try:
|
|
871
|
+
new_cid = orch.replace_member(handle, key)
|
|
872
|
+
if new_cid:
|
|
873
|
+
logger.info(
|
|
874
|
+
f"Ephemeral recycle: replaced {key} "
|
|
875
|
+
f"({cid[:12]}) with {new_cid[:12]} "
|
|
876
|
+
f"for run {handle.run_id}"
|
|
877
|
+
)
|
|
878
|
+
except Exception as e:
|
|
879
|
+
logger.warning(f"Ephemeral recycle failed for {key}: {e}")
|
|
880
|
+
break
|
|
881
|
+
|
|
882
|
+
listener = _ReactiveListener()
|
|
883
|
+
if not hasattr(self, "_reactive_listeners"):
|
|
884
|
+
self._reactive_listeners: Dict[str, Any] = {}
|
|
885
|
+
self._reactive_listeners[handle.run_id] = (listener, store)
|
|
886
|
+
|
|
887
|
+
store.add_listener(listener)
|
|
888
|
+
logger.info(
|
|
889
|
+
f"Reactive listener registered for run {handle.run_id} "
|
|
890
|
+
f"(ephemeral={is_ephemeral}, n_runners={n_runners_per_unit}, "
|
|
891
|
+
f"max_units={max_units})"
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
def _teardown_reactive_listener(self, handle: ComputeHandle) -> None:
|
|
895
|
+
"""Remove the reactive listener from the store."""
|
|
896
|
+
listeners = getattr(self, "_reactive_listeners", {})
|
|
897
|
+
entry = listeners.pop(handle.run_id, None)
|
|
898
|
+
if entry:
|
|
899
|
+
listener, store = entry
|
|
900
|
+
store.remove_listener(listener)
|
|
901
|
+
logger.debug(f"Reactive listener removed for run {handle.run_id}")
|