From 1a5f4ff57875527ac54549864a3833d004ad5a05 Mon Sep 17 00:00:00 2001 From: Amber Agent Date: Mon, 15 Dec 2025 22:30:22 +0000 Subject: [PATCH] feat(agent): Add pluggable agent runtime support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements a pluggable agent system that allows the platform to support multiple AI agent runtimes beyond Claude SDK (e.g., LangGraph, CrewAI, custom implementations). Changes: - Add runnerConfig field to AgenticSession CRD with type and image properties - Update backend types to include RunnerConfig struct - Modify operator to support runner type selection and image mapping - Add GetRunnerImage() method to config for runtime image resolution - Update backend handlers to parse and pass through runnerConfig - Add environment variables for additional runner images (LANGGRAPH_RUNNER_IMAGE, CREWAI_RUNNER_IMAGE, CUSTOM_RUNNER_IMAGE) The system maintains backward compatibility by defaulting to "claude-sdk" runner type when not specified. Custom container images can be provided per-session via the runnerConfig.image field, or globally via environment variables that map runner types to default images. This enables Phase 1 of the pluggable agent architecture as documented in issue #431. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- components/backend/handlers/sessions.go | 14 +++++++ components/backend/types/session.go | 9 +++++ .../base/crds/agenticsessions-crd.yaml | 16 ++++++++ .../manifests/base/operator-deployment.yaml | 7 ++++ .../minikube/operator-deployment.yaml | 7 ++++ components/operator/internal/config/config.go | 39 +++++++++++++++++++ .../operator/internal/handlers/sessions.go | 11 +++++- 7 files changed, 102 insertions(+), 1 deletion(-) diff --git a/components/backend/handlers/sessions.go b/components/backend/handlers/sessions.go index 12c0cc73b..ca4bb11b0 100644 --- a/components/backend/handlers/sessions.go +++ b/components/backend/handlers/sessions.go @@ -576,6 +576,20 @@ func CreateSession(c *gin.Context) { session["spec"].(map[string]interface{})["autoPushOnComplete"] = *req.AutoPushOnComplete } + // RunnerConfig for pluggable agents + if req.RunnerConfig != nil { + runnerConfig := make(map[string]interface{}) + if req.RunnerConfig.Type != "" { + runnerConfig["type"] = req.RunnerConfig.Type + } + if req.RunnerConfig.Image != "" { + runnerConfig["image"] = req.RunnerConfig.Image + } + if len(runnerConfig) > 0 { + session["spec"].(map[string]interface{})["runnerConfig"] = runnerConfig + } + } + // Set multi-repo configuration on spec (simplified format) { spec := session["spec"].(map[string]interface{}) diff --git a/components/backend/types/session.go b/components/backend/types/session.go index 1ee23676b..a58beb263 100644 --- a/components/backend/types/session.go +++ b/components/backend/types/session.go @@ -24,6 +24,8 @@ type AgenticSessionSpec struct { Repos []SimpleRepo `json:"repos,omitempty"` // Active workflow for dynamic workflow switching ActiveWorkflow *WorkflowSelection `json:"activeWorkflow,omitempty"` + // Runner configuration for pluggable agent support + RunnerConfig *RunnerConfig `json:"runnerConfig,omitempty"` } // SimpleRepo represents a simplified repository configuration @@ -58,6 +60,7 @@ type CreateAgenticSessionRequest struct { EnvironmentVariables map[string]string `json:"environmentVariables,omitempty"` Labels map[string]string `json:"labels,omitempty"` Annotations map[string]string `json:"annotations,omitempty"` + RunnerConfig *RunnerConfig `json:"runnerConfig,omitempty"` } type CloneSessionRequest struct { @@ -86,6 +89,12 @@ type WorkflowSelection struct { Path string `json:"path,omitempty"` } +// RunnerConfig specifies which agent runner to use (enables pluggable agents) +type RunnerConfig struct { + Type string `json:"type,omitempty"` // Runner type: "claude-sdk", "langgraph", "crewai", "custom" + Image string `json:"image,omitempty"` // Optional custom container image override +} + // ReconciledRepo captures reconciliation state for a repository type ReconciledRepo struct { URL string `json:"url"` diff --git a/components/manifests/base/crds/agenticsessions-crd.yaml b/components/manifests/base/crds/agenticsessions-crd.yaml index d0c38ed4f..1aa4d375b 100644 --- a/components/manifests/base/crds/agenticsessions-crd.yaml +++ b/components/manifests/base/crds/agenticsessions-crd.yaml @@ -93,6 +93,22 @@ spec: path: type: string description: "Optional path within repo (for repos with multiple workflows)" + runnerConfig: + type: object + description: "Configuration for the agent runner (enables pluggable agent support)" + properties: + type: + type: string + description: "Runner type identifier (e.g., 'claude-sdk', 'langgraph', 'crewai')" + default: "claude-sdk" + enum: + - "claude-sdk" + - "langgraph" + - "crewai" + - "custom" + image: + type: string + description: "Optional custom runner container image (overrides default for runner type)" status: type: object properties: diff --git a/components/manifests/base/operator-deployment.yaml b/components/manifests/base/operator-deployment.yaml index fa3326676..eaebfc40d 100644 --- a/components/manifests/base/operator-deployment.yaml +++ b/components/manifests/base/operator-deployment.yaml @@ -36,6 +36,13 @@ spec: value: "quay.io/ambient_code/vteam_backend:latest" - name: IMAGE_PULL_POLICY value: "Always" + # Pluggable agent runner images (optional - if not set, defaults to AMBIENT_CODE_RUNNER_IMAGE) + - name: LANGGRAPH_RUNNER_IMAGE + value: "" # Example: "quay.io/ambient_code/vteam_langgraph_runner:latest" + - name: CREWAI_RUNNER_IMAGE + value: "" # Example: "quay.io/ambient_code/vteam_crewai_runner:latest" + - name: CUSTOM_RUNNER_IMAGE + value: "" # Example: "quay.io/your_org/custom_runner:latest" # Vertex AI configuration from ConfigMap - name: CLAUDE_CODE_USE_VERTEX valueFrom: diff --git a/components/manifests/minikube/operator-deployment.yaml b/components/manifests/minikube/operator-deployment.yaml index befdce074..8033c70cf 100644 --- a/components/manifests/minikube/operator-deployment.yaml +++ b/components/manifests/minikube/operator-deployment.yaml @@ -56,6 +56,13 @@ spec: value: "localhost/vteam-backend:latest" - name: IMAGE_PULL_POLICY value: "Never" + # Pluggable agent runner images (optional - if not set, defaults to AMBIENT_CODE_RUNNER_IMAGE) + - name: LANGGRAPH_RUNNER_IMAGE + value: "" # Example: "localhost/vteam-langgraph-runner:latest" + - name: CREWAI_RUNNER_IMAGE + value: "" # Example: "localhost/vteam-crewai-runner:latest" + - name: CUSTOM_RUNNER_IMAGE + value: "" # Example: "localhost/custom-runner:latest" envFrom: - configMapRef: name: operator-config diff --git a/components/operator/internal/config/config.go b/components/operator/internal/config/config.go index 73b33b978..8177b3aa4 100644 --- a/components/operator/internal/config/config.go +++ b/components/operator/internal/config/config.go @@ -25,6 +25,8 @@ type Config struct { AmbientCodeRunnerImage string ContentServiceImage string ImagePullPolicy corev1.PullPolicy + // Runner type to image mappings for pluggable agents + RunnerImages map[string]string } // InitK8sClients initializes the Kubernetes clients @@ -93,11 +95,48 @@ func LoadConfig() *Config { } imagePullPolicy := corev1.PullPolicy(imagePullPolicyStr) + // Initialize runner image mappings for pluggable agents + runnerImages := make(map[string]string) + runnerImages["claude-sdk"] = ambientCodeRunnerImage // Default Claude SDK runner + + // Load additional runner images from environment variables + if langGraphImage := os.Getenv("LANGGRAPH_RUNNER_IMAGE"); langGraphImage != "" { + runnerImages["langgraph"] = langGraphImage + } + if crewAIImage := os.Getenv("CREWAI_RUNNER_IMAGE"); crewAIImage != "" { + runnerImages["crewai"] = crewAIImage + } + if customImage := os.Getenv("CUSTOM_RUNNER_IMAGE"); customImage != "" { + runnerImages["custom"] = customImage + } + return &Config{ Namespace: namespace, BackendNamespace: backendNamespace, AmbientCodeRunnerImage: ambientCodeRunnerImage, ContentServiceImage: contentServiceImage, ImagePullPolicy: imagePullPolicy, + RunnerImages: runnerImages, + } +} + +// GetRunnerImage returns the appropriate runner image based on runnerConfig +// If custom image is specified, it takes precedence +// Otherwise, looks up the runner type in the image registry +// Falls back to default Claude SDK runner if not found +func (c *Config) GetRunnerImage(runnerType string, customImage string) string { + // Custom image override takes precedence + if customImage != "" { + return customImage + } + + // Look up runner type in registry + if runnerType != "" { + if image, ok := c.RunnerImages[runnerType]; ok { + return image + } } + + // Default to Claude SDK runner + return c.AmbientCodeRunnerImage } diff --git a/components/operator/internal/handlers/sessions.go b/components/operator/internal/handlers/sessions.go index 1059c807c..3c60b46ac 100644 --- a/components/operator/internal/handlers/sessions.go +++ b/components/operator/internal/handlers/sessions.go @@ -806,6 +806,15 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error { temperature, _, _ := unstructured.NestedFloat64(llmSettings, "temperature") maxTokens, _, _ := unstructured.NestedInt64(llmSettings, "maxTokens") + // Extract runner configuration for pluggable agents + runnerConfig, _, _ := unstructured.NestedMap(spec, "runnerConfig") + runnerType, _, _ := unstructured.NestedString(runnerConfig, "type") + runnerImage, _, _ := unstructured.NestedString(runnerConfig, "image") + // Default to claude-sdk if not specified + if runnerType == "" { + runnerType = "claude-sdk" + } + // Hardcoded secret names (convention over configuration) const runnerSecretsName = "ambient-runner-secrets" // ANTHROPIC_API_KEY only (ignored when Vertex enabled) const integrationSecretsName = "ambient-non-vertex-integrations" // GIT_*, JIRA_*, custom keys (optional) @@ -1021,7 +1030,7 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error { }, { Name: "ambient-code-runner", - Image: appConfig.AmbientCodeRunnerImage, + Image: appConfig.GetRunnerImage(runnerType, runnerImage), ImagePullPolicy: appConfig.ImagePullPolicy, // 🔒 Container-level security (SCC-compatible, no privileged capabilities) SecurityContext: &corev1.SecurityContext{